mirror of
https://github.com/ClementTsang/bottom.git
synced 2025-07-26 23:24:20 +02:00
tuice tuice
This commit is contained in:
parent
7029412929
commit
f1ec2fd70f
702
src/app.rs
702
src/app.rs
@ -1,28 +1,44 @@
|
||||
pub mod data_farmer;
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering::SeqCst},
|
||||
Arc,
|
||||
};
|
||||
|
||||
pub use data_farmer::*;
|
||||
|
||||
pub mod data_harvester;
|
||||
use data_harvester::temperature;
|
||||
|
||||
pub mod event;
|
||||
|
||||
pub mod filter;
|
||||
pub use filter::*;
|
||||
|
||||
pub mod layout_manager;
|
||||
use layout_manager::*;
|
||||
|
||||
pub mod widgets;
|
||||
pub use widgets::*;
|
||||
|
||||
mod process_killer;
|
||||
pub mod query;
|
||||
pub mod widgets;
|
||||
|
||||
use std::time::Instant;
|
||||
mod frozen_state;
|
||||
use frozen_state::FrozenState;
|
||||
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent};
|
||||
use crate::{
|
||||
canvas::Painter,
|
||||
constants,
|
||||
tuice::{Application, Row},
|
||||
units::data_units::DataUnit,
|
||||
Pid,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use indextree::{Arena, NodeId};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
pub use data_farmer::*;
|
||||
use data_harvester::temperature;
|
||||
pub use filter::*;
|
||||
use layout_manager::*;
|
||||
pub use widgets::*;
|
||||
|
||||
use crate::{constants, units::data_units::DataUnit, utils::error::Result, BottomEvent, Pid};
|
||||
|
||||
use self::event::{ComponentEventResult, EventResult, ReturnSignal};
|
||||
|
||||
// FIXME: Move this!
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AxisScaling {
|
||||
Log,
|
||||
@ -95,54 +111,55 @@ pub struct AppConfigFields {
|
||||
pub network_use_binary_prefix: bool,
|
||||
}
|
||||
|
||||
/// The [`FrozenState`] indicates whether the application state should be frozen; if it is, save a snapshot of
|
||||
/// the data collected at that instant.
|
||||
pub enum FrozenState {
|
||||
NotFrozen,
|
||||
Frozen(Box<DataCollection>),
|
||||
#[derive(PartialEq, Eq)]
|
||||
enum CurrentScreen {
|
||||
Main,
|
||||
Expanded,
|
||||
Help,
|
||||
Delete,
|
||||
}
|
||||
|
||||
impl Default for FrozenState {
|
||||
impl Default for CurrentScreen {
|
||||
fn default() -> Self {
|
||||
Self::NotFrozen
|
||||
Self::Main
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AppMessages {
|
||||
Update(Box<data_harvester::Data>),
|
||||
OpenHelp,
|
||||
KillProcess { to_kill: Vec<Pid> },
|
||||
ToggleFreeze,
|
||||
Clean,
|
||||
Stop,
|
||||
}
|
||||
|
||||
pub struct AppState {
|
||||
pub dd_err: Option<String>,
|
||||
|
||||
to_delete_process_list: Option<(String, Vec<Pid>)>,
|
||||
|
||||
pub data_collection: DataCollection,
|
||||
|
||||
pub is_expanded: bool,
|
||||
|
||||
pub used_widgets: UsedWidgets,
|
||||
pub filters: DataFilters,
|
||||
pub app_config_fields: AppConfigFields,
|
||||
|
||||
// --- FIXME: TO DELETE/REWRITE ---
|
||||
pub delete_dialog_state: AppDeleteDialogState,
|
||||
pub is_force_redraw: bool,
|
||||
|
||||
pub is_determining_widget_boundary: bool,
|
||||
|
||||
// --- NEW STUFF ---
|
||||
pub selected_widget: NodeId,
|
||||
pub widget_lookup_map: FxHashMap<NodeId, TmpBottomWidget>,
|
||||
pub widget_lookup_map: FxHashMap<NodeId, BottomWidget>,
|
||||
pub layout_tree: Arena<LayoutNode>,
|
||||
pub layout_tree_root: NodeId,
|
||||
frozen_state: FrozenState,
|
||||
|
||||
pub help_dialog: DialogState<HelpDialog>,
|
||||
frozen_state: FrozenState,
|
||||
current_screen: CurrentScreen,
|
||||
painter: Painter,
|
||||
terminator: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
/// Creates a new [`AppState`].
|
||||
pub fn new(
|
||||
app_config_fields: AppConfigFields, filters: DataFilters,
|
||||
layout_tree_output: LayoutCreationOutput,
|
||||
) -> Self {
|
||||
layout_tree_output: LayoutCreationOutput, painter: Painter,
|
||||
) -> Result<Self> {
|
||||
let LayoutCreationOutput {
|
||||
layout_tree,
|
||||
root: layout_tree_root,
|
||||
@ -151,7 +168,7 @@ impl AppState {
|
||||
used_widgets,
|
||||
} = layout_tree_output;
|
||||
|
||||
Self {
|
||||
Ok(Self {
|
||||
app_config_fields,
|
||||
filters,
|
||||
used_widgets,
|
||||
@ -159,546 +176,83 @@ impl AppState {
|
||||
widget_lookup_map,
|
||||
layout_tree,
|
||||
layout_tree_root,
|
||||
painter,
|
||||
|
||||
// Use defaults.
|
||||
dd_err: Default::default(),
|
||||
to_delete_process_list: Default::default(),
|
||||
data_collection: Default::default(),
|
||||
is_expanded: Default::default(),
|
||||
delete_dialog_state: Default::default(),
|
||||
is_force_redraw: Default::default(),
|
||||
is_determining_widget_boundary: Default::default(),
|
||||
frozen_state: Default::default(),
|
||||
help_dialog: Default::default(),
|
||||
current_screen: Default::default(),
|
||||
|
||||
terminator: Self::register_terminator()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn register_terminator() -> Result<Arc<AtomicBool>> {
|
||||
let it = Arc::new(AtomicBool::new(false));
|
||||
let it_clone = it.clone();
|
||||
ctrlc::set_handler(move || {
|
||||
it_clone.store(true, SeqCst);
|
||||
})?;
|
||||
|
||||
Ok(it)
|
||||
}
|
||||
|
||||
fn set_current_screen(&mut self, screen_type: CurrentScreen) {
|
||||
if self.current_screen == screen_type {
|
||||
self.current_screen = screen_type;
|
||||
// TODO: Redraw
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Application for AppState {
|
||||
type Message = AppMessages;
|
||||
|
||||
fn update(&mut self, message: Self::Message) {
|
||||
match message {
|
||||
AppMessages::Update(new_data) => {
|
||||
self.data_collection.eat_data(new_data);
|
||||
}
|
||||
AppMessages::OpenHelp => {
|
||||
self.set_current_screen(CurrentScreen::Help);
|
||||
}
|
||||
AppMessages::KillProcess { to_kill } => {}
|
||||
AppMessages::ToggleFreeze => {
|
||||
self.frozen_state.toggle(&self.data_collection);
|
||||
}
|
||||
AppMessages::Clean => {
|
||||
self.data_collection
|
||||
.clean_data(constants::STALE_MAX_MILLISECONDS);
|
||||
}
|
||||
AppMessages::Stop => {
|
||||
self.terminator.store(true, SeqCst);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_terminated(&self) -> bool {
|
||||
self.terminator.load(SeqCst)
|
||||
}
|
||||
|
||||
fn view(
|
||||
&mut self,
|
||||
) -> Box<dyn crate::tuice::Component<Self::Message, crate::tuice::CrosstermBackend>> {
|
||||
Box::new(Row::with_children(vec![crate::tuice::TextTable::new(
|
||||
vec!["A", "B", "C"],
|
||||
)]))
|
||||
}
|
||||
|
||||
fn destroy(&mut self) {
|
||||
// TODO: Eventually move some thread logic into the app creation, and destroy here?
|
||||
}
|
||||
|
||||
fn global_event_handler(
|
||||
&mut self, event: crate::tuice::Event, _messages: &mut Vec<Self::Message>,
|
||||
) {
|
||||
use crate::tuice::Event;
|
||||
match event {
|
||||
Event::Keyboard(_) => {}
|
||||
Event::Mouse(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_frozen(&self) -> bool {
|
||||
matches!(self.frozen_state, FrozenState::Frozen(_))
|
||||
}
|
||||
|
||||
pub fn toggle_freeze(&mut self) {
|
||||
if matches!(self.frozen_state, FrozenState::Frozen(_)) {
|
||||
self.frozen_state = FrozenState::NotFrozen;
|
||||
} else {
|
||||
self.frozen_state = FrozenState::Frozen(Box::new(self.data_collection.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
// Call reset on all widgets.
|
||||
self.widget_lookup_map
|
||||
.values_mut()
|
||||
.for_each(|widget| widget.reset());
|
||||
|
||||
// Unfreeze.
|
||||
self.frozen_state = FrozenState::NotFrozen;
|
||||
|
||||
// Reset data
|
||||
self.data_collection.reset();
|
||||
}
|
||||
|
||||
pub fn should_get_widget_bounds(&self) -> bool {
|
||||
self.is_force_redraw || self.is_determining_widget_boundary
|
||||
}
|
||||
|
||||
fn close_dd(&mut self) {
|
||||
self.delete_dialog_state.is_showing_dd = false;
|
||||
self.delete_dialog_state.selected_signal = KillSignal::default();
|
||||
self.delete_dialog_state.scroll_pos = 0;
|
||||
self.to_delete_process_list = None;
|
||||
self.dd_err = None;
|
||||
}
|
||||
|
||||
/// Handles a global event involving a char.
|
||||
fn handle_global_char(&mut self, c: char) -> EventResult {
|
||||
if c.is_ascii_control() {
|
||||
EventResult::NoRedraw
|
||||
} else {
|
||||
// Check for case-sensitive bindings first.
|
||||
match c {
|
||||
'H' | 'A' => self.move_to_widget(MovementDirection::Left),
|
||||
'L' | 'D' => self.move_to_widget(MovementDirection::Right),
|
||||
'K' | 'W' => self.move_to_widget(MovementDirection::Up),
|
||||
'J' | 'S' => self.move_to_widget(MovementDirection::Down),
|
||||
_ => {
|
||||
let c = c.to_ascii_lowercase();
|
||||
match c {
|
||||
'q' => EventResult::Quit,
|
||||
'e' if !self.help_dialog.is_showing() => {
|
||||
if self.app_config_fields.use_basic_mode {
|
||||
EventResult::NoRedraw
|
||||
} else {
|
||||
self.is_expanded = !self.is_expanded;
|
||||
EventResult::Redraw
|
||||
}
|
||||
}
|
||||
'?' if !self.help_dialog.is_showing() => {
|
||||
self.help_dialog.show();
|
||||
EventResult::Redraw
|
||||
}
|
||||
'f' if !self.help_dialog.is_showing() => {
|
||||
self.toggle_freeze();
|
||||
if !self.is_frozen() {
|
||||
let data_collection = &self.data_collection;
|
||||
self.widget_lookup_map
|
||||
.iter_mut()
|
||||
.for_each(|(_id, widget)| widget.update_data(data_collection));
|
||||
}
|
||||
EventResult::Redraw
|
||||
}
|
||||
_ => EventResult::NoRedraw,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves to a widget.
|
||||
fn move_to_widget(&mut self, direction: MovementDirection) -> EventResult {
|
||||
match if self.is_expanded {
|
||||
move_expanded_widget_selection(
|
||||
&mut self.widget_lookup_map,
|
||||
self.selected_widget,
|
||||
direction,
|
||||
)
|
||||
} else {
|
||||
let layout_tree = &mut self.layout_tree;
|
||||
|
||||
move_widget_selection(
|
||||
layout_tree,
|
||||
&mut self.widget_lookup_map,
|
||||
self.selected_widget,
|
||||
direction,
|
||||
)
|
||||
} {
|
||||
MoveWidgetResult::ForceRedraw(new_widget_id) => {
|
||||
self.selected_widget = new_widget_id;
|
||||
EventResult::Redraw
|
||||
}
|
||||
MoveWidgetResult::NodeId(new_widget_id) => {
|
||||
let previous_selected = self.selected_widget;
|
||||
self.selected_widget = new_widget_id;
|
||||
|
||||
if previous_selected != self.selected_widget {
|
||||
EventResult::Redraw
|
||||
} else {
|
||||
EventResult::NoRedraw
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Quick and dirty handler to convert [`ComponentEventResult`]s to [`EventResult`]s, and handle [`ReturnSignal`]s.
|
||||
fn convert_widget_event_result(&mut self, w: ComponentEventResult) -> EventResult {
|
||||
match w {
|
||||
ComponentEventResult::Unhandled => EventResult::NoRedraw,
|
||||
ComponentEventResult::Redraw => EventResult::Redraw,
|
||||
ComponentEventResult::NoRedraw => EventResult::NoRedraw,
|
||||
ComponentEventResult::Signal(signal) => match signal {
|
||||
ReturnSignal::KillProcess => {
|
||||
todo!()
|
||||
}
|
||||
ReturnSignal::Update => {
|
||||
if let Some(widget) = self.widget_lookup_map.get_mut(&self.selected_widget) {
|
||||
match &self.frozen_state {
|
||||
FrozenState::NotFrozen => {
|
||||
widget.update_data(&self.data_collection);
|
||||
}
|
||||
FrozenState::Frozen(frozen_data) => {
|
||||
widget.update_data(frozen_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
EventResult::Redraw
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles a [`KeyEvent`], and returns an [`EventResult`].
|
||||
fn handle_key_event(&mut self, event: KeyEvent) -> EventResult {
|
||||
let result = if let DialogState::Shown(help_dialog) = &mut self.help_dialog {
|
||||
help_dialog.handle_key_event(event)
|
||||
} else if let Some(widget) = self.widget_lookup_map.get_mut(&self.selected_widget) {
|
||||
widget.handle_key_event(event)
|
||||
} else {
|
||||
ComponentEventResult::Unhandled
|
||||
};
|
||||
|
||||
match result {
|
||||
ComponentEventResult::Unhandled => self.handle_global_key_event(event),
|
||||
_ => self.convert_widget_event_result(result),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles a global [`KeyEvent`], and returns an [`EventResult`].
|
||||
fn handle_global_key_event(&mut self, event: KeyEvent) -> EventResult {
|
||||
if event.modifiers.is_empty() {
|
||||
match event.code {
|
||||
KeyCode::Esc => {
|
||||
if self.is_expanded {
|
||||
self.is_expanded = false;
|
||||
EventResult::Redraw
|
||||
} else if self.help_dialog.is_showing() {
|
||||
self.help_dialog.hide();
|
||||
EventResult::Redraw
|
||||
} else if self.delete_dialog_state.is_showing_dd {
|
||||
self.close_dd();
|
||||
EventResult::Redraw
|
||||
} else {
|
||||
EventResult::NoRedraw
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if let KeyCode::Char(c) = event.code {
|
||||
self.handle_global_char(c)
|
||||
} else {
|
||||
EventResult::NoRedraw
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let KeyModifiers::CONTROL = event.modifiers {
|
||||
match event.code {
|
||||
KeyCode::Char('c') | KeyCode::Char('C') => EventResult::Quit,
|
||||
KeyCode::Char('r') | KeyCode::Char('R') => {
|
||||
self.reset();
|
||||
let data_collection = &self.data_collection;
|
||||
self.widget_lookup_map
|
||||
.iter_mut()
|
||||
.for_each(|(_id, widget)| widget.update_data(data_collection));
|
||||
EventResult::Redraw
|
||||
}
|
||||
KeyCode::Left => self.move_to_widget(MovementDirection::Left),
|
||||
KeyCode::Right => self.move_to_widget(MovementDirection::Right),
|
||||
KeyCode::Up => self.move_to_widget(MovementDirection::Up),
|
||||
KeyCode::Down => self.move_to_widget(MovementDirection::Down),
|
||||
_ => EventResult::NoRedraw,
|
||||
}
|
||||
} else if let KeyModifiers::SHIFT = event.modifiers {
|
||||
match event.code {
|
||||
KeyCode::Left => self.move_to_widget(MovementDirection::Left),
|
||||
KeyCode::Right => self.move_to_widget(MovementDirection::Right),
|
||||
KeyCode::Up => self.move_to_widget(MovementDirection::Up),
|
||||
KeyCode::Down => self.move_to_widget(MovementDirection::Down),
|
||||
KeyCode::Char(c) => self.handle_global_char(c),
|
||||
_ => EventResult::NoRedraw,
|
||||
}
|
||||
} else {
|
||||
EventResult::NoRedraw
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles a [`MouseEvent`].
|
||||
fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult {
|
||||
if let DialogState::Shown(help_dialog) = &mut self.help_dialog {
|
||||
let result = help_dialog.handle_mouse_event(event);
|
||||
self.convert_widget_event_result(result)
|
||||
} else if self.is_expanded {
|
||||
if let Some(widget) = self.widget_lookup_map.get_mut(&self.selected_widget) {
|
||||
let result = widget.handle_mouse_event(event);
|
||||
self.convert_widget_event_result(result)
|
||||
} else {
|
||||
EventResult::NoRedraw
|
||||
}
|
||||
} else {
|
||||
let mut returned_result = EventResult::NoRedraw;
|
||||
for (id, widget) in self.widget_lookup_map.iter_mut() {
|
||||
if widget.does_border_intersect_mouse(&event) {
|
||||
let result = widget.handle_mouse_event(event);
|
||||
match widget.selectable_type() {
|
||||
SelectableType::Selectable => {
|
||||
let was_id_already_selected = self.selected_widget == *id;
|
||||
self.selected_widget = *id;
|
||||
|
||||
if was_id_already_selected {
|
||||
returned_result = self.convert_widget_event_result(result);
|
||||
} else {
|
||||
// If the weren't equal, *force* a redraw, and correct the layout tree.
|
||||
correct_layout_last_selections(
|
||||
&mut self.layout_tree,
|
||||
self.selected_widget,
|
||||
);
|
||||
let _ = self.convert_widget_event_result(result);
|
||||
returned_result = EventResult::Redraw;
|
||||
}
|
||||
break;
|
||||
}
|
||||
SelectableType::Unselectable => {
|
||||
let result = widget.handle_mouse_event(event);
|
||||
return self.convert_widget_event_result(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
returned_result
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles a [`BottomEvent`] and updates the [`AppState`] if needed. Returns an [`EventResult`] indicating
|
||||
/// whether the app now requires a redraw.
|
||||
pub fn handle_event(&mut self, event: BottomEvent) -> EventResult {
|
||||
match event {
|
||||
BottomEvent::KeyInput(event) => self.handle_key_event(event),
|
||||
BottomEvent::MouseInput(event) => {
|
||||
// Not great, but basically a blind lookup through the table for anything that clips the click location.
|
||||
self.handle_mouse_event(event)
|
||||
}
|
||||
BottomEvent::Update(new_data) => {
|
||||
self.data_collection.eat_data(new_data);
|
||||
|
||||
// TODO: [Optimization] Optimization for dialogs - don't redraw on an update!
|
||||
|
||||
if !self.is_frozen() {
|
||||
let data_collection = &self.data_collection;
|
||||
self.widget_lookup_map
|
||||
.iter_mut()
|
||||
.for_each(|(_id, widget)| widget.update_data(data_collection));
|
||||
|
||||
EventResult::Redraw
|
||||
} else {
|
||||
EventResult::NoRedraw
|
||||
}
|
||||
}
|
||||
BottomEvent::Resize {
|
||||
width: _,
|
||||
height: _,
|
||||
} => EventResult::Redraw,
|
||||
BottomEvent::Clean => {
|
||||
self.data_collection
|
||||
.clean_data(constants::STALE_MAX_MILLISECONDS);
|
||||
EventResult::NoRedraw
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
fn on_number(&mut self, number_char: char) {
|
||||
if self.delete_dialog_state.is_showing_dd {
|
||||
if self
|
||||
.delete_dialog_state
|
||||
.last_number_press
|
||||
.map_or(100, |ins| ins.elapsed().as_millis())
|
||||
>= 400
|
||||
{
|
||||
self.delete_dialog_state.keyboard_signal_select = 0;
|
||||
}
|
||||
let mut kbd_signal = self.delete_dialog_state.keyboard_signal_select * 10;
|
||||
kbd_signal += number_char.to_digit(10).unwrap() as usize;
|
||||
if kbd_signal > 64 {
|
||||
kbd_signal %= 100;
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
if kbd_signal > 64 || kbd_signal == 32 || kbd_signal == 33 {
|
||||
kbd_signal %= 10;
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
if kbd_signal > 31 {
|
||||
kbd_signal %= 10;
|
||||
}
|
||||
self.delete_dialog_state.selected_signal = KillSignal::Kill(kbd_signal);
|
||||
if kbd_signal < 10 {
|
||||
self.delete_dialog_state.keyboard_signal_select = kbd_signal;
|
||||
} else {
|
||||
self.delete_dialog_state.keyboard_signal_select = 0;
|
||||
}
|
||||
self.delete_dialog_state.last_number_press = Some(Instant::now());
|
||||
}
|
||||
}
|
||||
|
||||
fn on_left_key(&mut self) {
|
||||
// if !self.is_in_dialog() {
|
||||
// match self.current_widget.widget_type {
|
||||
// BottomWidgetType::ProcSearch => {
|
||||
// let is_in_search_widget = self.is_in_search_widget();
|
||||
// if let Some(proc_widget_state) = self
|
||||
// .proc_state
|
||||
// .get_mut_widget_state(self.current_widget.widget_id - 1)
|
||||
// {
|
||||
// if is_in_search_widget {
|
||||
// let prev_cursor = proc_widget_state.get_search_cursor_position();
|
||||
// proc_widget_state
|
||||
// .search_walk_back(proc_widget_state.get_search_cursor_position());
|
||||
// if proc_widget_state.get_search_cursor_position() < prev_cursor {
|
||||
// let str_slice = &proc_widget_state
|
||||
// .process_search_state
|
||||
// .search_state
|
||||
// .current_search_query
|
||||
// [proc_widget_state.get_search_cursor_position()..prev_cursor];
|
||||
// proc_widget_state
|
||||
// .process_search_state
|
||||
// .search_state
|
||||
// .char_cursor_position -= UnicodeWidthStr::width(str_slice);
|
||||
// proc_widget_state
|
||||
// .process_search_state
|
||||
// .search_state
|
||||
// .cursor_direction = CursorDirection::Left;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// _ => {}
|
||||
// }
|
||||
// } else if self.delete_dialog_state.is_showing_dd {
|
||||
// #[cfg(target_family = "unix")]
|
||||
// {
|
||||
// if self.app_config_fields.is_advanced_kill {
|
||||
// match self.delete_dialog_state.selected_signal {
|
||||
// KillSignal::Kill(prev_signal) => {
|
||||
// self.delete_dialog_state.selected_signal = match prev_signal - 1 {
|
||||
// 0 => KillSignal::Cancel,
|
||||
// // 32+33 are skipped
|
||||
// 33 => KillSignal::Kill(31),
|
||||
// signal => KillSignal::Kill(signal),
|
||||
// };
|
||||
// }
|
||||
// KillSignal::Cancel => {}
|
||||
// };
|
||||
// } else {
|
||||
// self.delete_dialog_state.selected_signal = KillSignal::default();
|
||||
// }
|
||||
// }
|
||||
// #[cfg(target_os = "windows")]
|
||||
// {
|
||||
// self.delete_dialog_state.selected_signal = KillSignal::Kill(1);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
fn on_right_key(&mut self) {
|
||||
// if !self.is_in_dialog() {
|
||||
// match self.current_widget.widget_type {
|
||||
// BottomWidgetType::ProcSearch => {
|
||||
// let is_in_search_widget = self.is_in_search_widget();
|
||||
// if let Some(proc_widget_state) = self
|
||||
// .proc_state
|
||||
// .get_mut_widget_state(self.current_widget.widget_id - 1)
|
||||
// {
|
||||
// if is_in_search_widget {
|
||||
// let prev_cursor = proc_widget_state.get_search_cursor_position();
|
||||
// proc_widget_state.search_walk_forward(
|
||||
// proc_widget_state.get_search_cursor_position(),
|
||||
// );
|
||||
// if proc_widget_state.get_search_cursor_position() > prev_cursor {
|
||||
// let str_slice = &proc_widget_state
|
||||
// .process_search_state
|
||||
// .search_state
|
||||
// .current_search_query
|
||||
// [prev_cursor..proc_widget_state.get_search_cursor_position()];
|
||||
// proc_widget_state
|
||||
// .process_search_state
|
||||
// .search_state
|
||||
// .char_cursor_position += UnicodeWidthStr::width(str_slice);
|
||||
// proc_widget_state
|
||||
// .process_search_state
|
||||
// .search_state
|
||||
// .cursor_direction = CursorDirection::Right;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// _ => {}
|
||||
// }
|
||||
// } else if self.delete_dialog_state.is_showing_dd {
|
||||
// #[cfg(target_family = "unix")]
|
||||
// {
|
||||
// if self.app_config_fields.is_advanced_kill {
|
||||
// let new_signal = match self.delete_dialog_state.selected_signal {
|
||||
// KillSignal::Cancel => 1,
|
||||
// // 32+33 are skipped
|
||||
// #[cfg(target_os = "linux")]
|
||||
// KillSignal::Kill(31) => 34,
|
||||
// #[cfg(target_os = "macos")]
|
||||
// KillSignal::Kill(31) => 31,
|
||||
// KillSignal::Kill(64) => 64,
|
||||
// KillSignal::Kill(signal) => signal + 1,
|
||||
// };
|
||||
// self.delete_dialog_state.selected_signal = KillSignal::Kill(new_signal);
|
||||
// } else {
|
||||
// self.delete_dialog_state.selected_signal = KillSignal::Cancel;
|
||||
// }
|
||||
// }
|
||||
// #[cfg(target_os = "windows")]
|
||||
// {
|
||||
// self.delete_dialog_state.selected_signal = KillSignal::Cancel;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
fn start_killing_process(&mut self) {
|
||||
todo!()
|
||||
|
||||
// if let Some(proc_widget_state) = self
|
||||
// .proc_state
|
||||
// .widget_states
|
||||
// .get(&self.current_widget.widget_id)
|
||||
// {
|
||||
// if let Some(corresponding_filtered_process_list) = self
|
||||
// .canvas_data
|
||||
// .finalized_process_data_map
|
||||
// .get(&self.current_widget.widget_id)
|
||||
// {
|
||||
// if proc_widget_state.scroll_state.current_scroll_position
|
||||
// < corresponding_filtered_process_list.len()
|
||||
// {
|
||||
// let current_process: (String, Vec<Pid>);
|
||||
// if self.is_grouped(self.current_widget.widget_id) {
|
||||
// if let Some(process) = &corresponding_filtered_process_list
|
||||
// .get(proc_widget_state.scroll_state.current_scroll_position)
|
||||
// {
|
||||
// current_process = (process.name.to_string(), process.group_pids.clone())
|
||||
// } else {
|
||||
// return;
|
||||
// }
|
||||
// } else {
|
||||
// let process = corresponding_filtered_process_list
|
||||
// [proc_widget_state.scroll_state.current_scroll_position]
|
||||
// .clone();
|
||||
// current_process = (process.name.clone(), vec![process.pid])
|
||||
// };
|
||||
|
||||
// self.to_delete_process_list = Some(current_process);
|
||||
// self.delete_dialog_state.is_showing_dd = true;
|
||||
// self.is_determining_widget_boundary = true;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
fn kill_highlighted_process(&mut self) -> Result<()> {
|
||||
// if let BottomWidgetType::Proc = self.current_widget.widget_type {
|
||||
// if let Some(current_selected_processes) = &self.to_delete_process_list {
|
||||
// #[cfg(target_family = "unix")]
|
||||
// let signal = match self.delete_dialog_state.selected_signal {
|
||||
// KillSignal::Kill(sig) => sig,
|
||||
// KillSignal::Cancel => 15, // should never happen, so just TERM
|
||||
// };
|
||||
// for pid in ¤t_selected_processes.1 {
|
||||
// #[cfg(target_family = "unix")]
|
||||
// {
|
||||
// process_killer::kill_process_given_pid(*pid, signal)?;
|
||||
// }
|
||||
// #[cfg(target_os = "windows")]
|
||||
// {
|
||||
// process_killer::kill_process_given_pid(*pid)?;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// self.to_delete_process_list = None;
|
||||
// Ok(())
|
||||
// } else {
|
||||
// Err(BottomError::GenericError(
|
||||
// "Cannot kill processes if the current widget is not the Process widget!"
|
||||
// .to_string(),
|
||||
// ))
|
||||
// }
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_to_delete_processes(&self) -> Option<(String, Vec<Pid>)> {
|
||||
// self.to_delete_process_list.clone()
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
44
src/app/frozen_state.rs
Normal file
44
src/app/frozen_state.rs
Normal file
@ -0,0 +1,44 @@
|
||||
use super::DataCollection;
|
||||
|
||||
/// The [`FrozenState`] indicates whether the application state should be frozen. It is either not frozen or
|
||||
/// frozen and containing a copy of the state at the time.
|
||||
pub enum FrozenState {
|
||||
NotFrozen,
|
||||
Frozen(Box<DataCollection>),
|
||||
}
|
||||
|
||||
impl Default for FrozenState {
|
||||
fn default() -> Self {
|
||||
Self::NotFrozen
|
||||
}
|
||||
}
|
||||
|
||||
pub type IsFrozen = bool;
|
||||
|
||||
impl FrozenState {
|
||||
/// Checks whether the [`FrozenState`] is currently frozen.
|
||||
pub fn is_frozen(&self) -> IsFrozen {
|
||||
matches!(self, FrozenState::Frozen(_))
|
||||
}
|
||||
|
||||
/// Freezes the [`FrozenState`].
|
||||
pub fn freeze(&mut self, data: Box<DataCollection>) {
|
||||
*self = FrozenState::Frozen(data);
|
||||
}
|
||||
|
||||
/// Unfreezes the [`FrozenState`].
|
||||
pub fn thaw(&mut self) {
|
||||
*self = FrozenState::NotFrozen;
|
||||
}
|
||||
|
||||
/// Toggles the [`FrozenState`] and returns whether it is now frozen.
|
||||
pub fn toggle(&mut self, data: &DataCollection) -> IsFrozen {
|
||||
if self.is_frozen() {
|
||||
self.thaw();
|
||||
false
|
||||
} else {
|
||||
self.freeze(Box::new(data.clone()));
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
@ -17,7 +17,7 @@ use tui::layout::Rect;
|
||||
use crate::app::widgets::Widget;
|
||||
|
||||
use super::{
|
||||
event::SelectionAction, AppConfigFields, CpuGraph, TimeGraph, TmpBottomWidget, UsedWidgets,
|
||||
event::SelectionAction, AppConfigFields, BottomWidget, CpuGraph, TimeGraph, UsedWidgets,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
@ -197,7 +197,7 @@ pub enum MovementDirection {
|
||||
pub struct LayoutCreationOutput {
|
||||
pub layout_tree: Arena<LayoutNode>,
|
||||
pub root: NodeId,
|
||||
pub widget_lookup_map: FxHashMap<NodeId, TmpBottomWidget>,
|
||||
pub widget_lookup_map: FxHashMap<NodeId, BottomWidget>,
|
||||
pub selected: NodeId,
|
||||
pub used_widgets: UsedWidgets,
|
||||
}
|
||||
@ -210,7 +210,7 @@ pub fn create_layout_tree(
|
||||
rows: &[Row], process_defaults: ProcessDefaults, app_config_fields: &AppConfigFields,
|
||||
) -> Result<LayoutCreationOutput> {
|
||||
fn add_widget_to_map(
|
||||
widget_lookup_map: &mut FxHashMap<NodeId, TmpBottomWidget>, widget_type: BottomWidgetType,
|
||||
widget_lookup_map: &mut FxHashMap<NodeId, BottomWidget>, widget_type: BottomWidgetType,
|
||||
widget_id: NodeId, process_defaults: &ProcessDefaults, app_config_fields: &AppConfigFields,
|
||||
width: LayoutRule, height: LayoutRule,
|
||||
) -> Result<()> {
|
||||
@ -518,7 +518,7 @@ pub enum MoveWidgetResult {
|
||||
|
||||
/// A more restricted movement, only within a single widget.
|
||||
pub fn move_expanded_widget_selection(
|
||||
widget_lookup_map: &mut FxHashMap<NodeId, TmpBottomWidget>, current_widget_id: NodeId,
|
||||
widget_lookup_map: &mut FxHashMap<NodeId, BottomWidget>, current_widget_id: NodeId,
|
||||
direction: MovementDirection,
|
||||
) -> MoveWidgetResult {
|
||||
if let Some(current_widget) = widget_lookup_map.get_mut(¤t_widget_id) {
|
||||
@ -542,9 +542,8 @@ pub fn move_expanded_widget_selection(
|
||||
/// - Only [`LayoutNode::Widget`]s are leaves.
|
||||
/// - Only [`LayoutNode::Row`]s or [`LayoutNode::Col`]s are non-leaves.
|
||||
pub fn move_widget_selection(
|
||||
layout_tree: &mut Arena<LayoutNode>,
|
||||
widget_lookup_map: &mut FxHashMap<NodeId, TmpBottomWidget>, current_widget_id: NodeId,
|
||||
direction: MovementDirection,
|
||||
layout_tree: &mut Arena<LayoutNode>, widget_lookup_map: &mut FxHashMap<NodeId, BottomWidget>,
|
||||
current_widget_id: NodeId, direction: MovementDirection,
|
||||
) -> MoveWidgetResult {
|
||||
// We first give our currently-selected widget a chance to react to the movement - it may handle it internally!
|
||||
let handled = {
|
||||
@ -832,7 +831,7 @@ pub fn move_widget_selection(
|
||||
/// - [How Flutter does sublinear layout](https://flutter.dev/docs/resources/inside-flutter#sublinear-layout)
|
||||
pub fn generate_layout(
|
||||
root: NodeId, arena: &mut Arena<LayoutNode>, area: Rect,
|
||||
lookup_map: &FxHashMap<NodeId, TmpBottomWidget>,
|
||||
lookup_map: &FxHashMap<NodeId, BottomWidget>,
|
||||
) {
|
||||
// TODO: [Optimization, Layout] Add some caching/dirty mechanisms to reduce calls.
|
||||
|
||||
@ -887,8 +886,8 @@ pub fn generate_layout(
|
||||
|
||||
/// The internal recursive call to build a layout. Builds off of `arena` and stores bounds inside it.
|
||||
fn layout(
|
||||
node: NodeId, arena: &mut Arena<LayoutNode>,
|
||||
lookup_map: &FxHashMap<NodeId, TmpBottomWidget>, mut constraints: LayoutConstraints,
|
||||
node: NodeId, arena: &mut Arena<LayoutNode>, lookup_map: &FxHashMap<NodeId, BottomWidget>,
|
||||
mut constraints: LayoutConstraints,
|
||||
) -> Size {
|
||||
if let Some(layout_node) = arena.get(node).map(|n| n.get()) {
|
||||
match layout_node {
|
||||
|
@ -156,7 +156,7 @@ pub enum SelectableType {
|
||||
/// The "main" widgets that are used by bottom to display information!
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[enum_dispatch(Component, Widget)]
|
||||
pub enum TmpBottomWidget {
|
||||
pub enum BottomWidget {
|
||||
MemGraph,
|
||||
TempTable,
|
||||
DiskTable,
|
||||
@ -172,7 +172,7 @@ pub enum TmpBottomWidget {
|
||||
Empty,
|
||||
}
|
||||
|
||||
impl Debug for TmpBottomWidget {
|
||||
impl Debug for BottomWidget {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::MemGraph(_) => write!(f, "MemGraph"),
|
||||
@ -192,41 +192,6 @@ impl Debug for TmpBottomWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// The states a dialog can be in. Consists of either:
|
||||
/// - [`DialogState::Hidden`] - the dialog is currently not showing.
|
||||
/// - [`DialogState::Shown`] - the dialog is showing.
|
||||
#[derive(Debug)]
|
||||
pub enum DialogState<D: Default + Component> {
|
||||
Hidden,
|
||||
Shown(D),
|
||||
}
|
||||
|
||||
impl<D> Default for DialogState<D>
|
||||
where
|
||||
D: Default + Component,
|
||||
{
|
||||
fn default() -> Self {
|
||||
DialogState::Hidden
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> DialogState<D>
|
||||
where
|
||||
D: Default + Component,
|
||||
{
|
||||
pub fn is_showing(&self) -> bool {
|
||||
matches!(self, DialogState::Shown(_))
|
||||
}
|
||||
|
||||
pub fn hide(&mut self) {
|
||||
*self = DialogState::Hidden;
|
||||
}
|
||||
|
||||
pub fn show(&mut self) {
|
||||
*self = DialogState::Shown(D::default());
|
||||
}
|
||||
}
|
||||
|
||||
// ----- FIXME: Delete the old stuff below -----
|
||||
|
||||
#[derive(PartialEq)]
|
||||
|
@ -4,16 +4,13 @@
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
use bottom::{app::event::EventResult, canvas, options::*, *};
|
||||
use bottom::{app::AppMessages, options::*, tuice::RuntimeEvent, *};
|
||||
|
||||
use std::{
|
||||
boxed::Box,
|
||||
io::stdout,
|
||||
panic,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
mpsc, Arc, Condvar, Mutex,
|
||||
},
|
||||
sync::{mpsc, Arc, Condvar, Mutex},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
@ -39,10 +36,7 @@ fn main() -> Result<()> {
|
||||
.context("Unable to properly parse or create the config file.")?;
|
||||
|
||||
// Create "app" struct, which will control most of the program and store settings/state
|
||||
let mut app = build_app(&matches, &mut config)?;
|
||||
|
||||
// Create painter and set colours.
|
||||
let mut painter = canvas::Painter::init(&config, get_color_scheme(&matches, &config)?)?;
|
||||
let app = build_app(&matches, &mut config)?;
|
||||
|
||||
// Create termination mutex and cvar
|
||||
#[allow(clippy::mutex_atomic)]
|
||||
@ -71,7 +65,10 @@ fn main() -> Result<()> {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if cleaning_sender.send(BottomEvent::Clean).is_err() {
|
||||
if cleaning_sender
|
||||
.send(RuntimeEvent::Custom(AppMessages::Clean))
|
||||
.is_err()
|
||||
{
|
||||
// debug!("Failed to send cleaning sender...");
|
||||
break;
|
||||
}
|
||||
@ -105,33 +102,7 @@ fn main() -> Result<()> {
|
||||
// TODO: [Threads, Panic] Make this close all the child threads too!
|
||||
panic::set_hook(Box::new(|info| panic_hook(info)));
|
||||
|
||||
// Set termination hook
|
||||
let is_terminated = Arc::new(AtomicBool::new(false));
|
||||
let ist_clone = is_terminated.clone();
|
||||
ctrlc::set_handler(move || {
|
||||
ist_clone.store(true, Ordering::SeqCst);
|
||||
})?;
|
||||
|
||||
// Paint once first.
|
||||
try_drawing(&mut terminal, &mut app, &mut painter)?;
|
||||
|
||||
while !is_terminated.load(Ordering::SeqCst) {
|
||||
if let Ok(recv) = receiver.recv() {
|
||||
match app.handle_event(recv) {
|
||||
EventResult::Quit => {
|
||||
break;
|
||||
}
|
||||
EventResult::Redraw => {
|
||||
try_drawing(&mut terminal, &mut app, &mut painter)?;
|
||||
}
|
||||
EventResult::NoRedraw => {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
tuice::launch_with_application(app, receiver);
|
||||
|
||||
// I think doing it in this order is safe...
|
||||
*thread_termination_lock.lock().unwrap() = true;
|
||||
|
125
src/canvas.rs
125
src/canvas.rs
@ -4,21 +4,20 @@ use indextree::{Arena, NodeId};
|
||||
use rustc_hash::FxHashMap;
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
layout::{Constraint, Layout, Rect},
|
||||
text::Span,
|
||||
widgets::Paragraph,
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
use canvas_colours::*;
|
||||
use dialogs::*;
|
||||
|
||||
use crate::{
|
||||
app::{
|
||||
self,
|
||||
layout_manager::{generate_layout, ColLayout, LayoutNode, RowLayout},
|
||||
widgets::{Component, Widget},
|
||||
DialogState, TmpBottomWidget,
|
||||
BottomWidget,
|
||||
},
|
||||
constants::*,
|
||||
options::Config,
|
||||
@ -27,7 +26,6 @@ use crate::{
|
||||
};
|
||||
|
||||
mod canvas_colours;
|
||||
mod dialogs;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ColourScheme {
|
||||
@ -138,7 +136,7 @@ impl Painter {
|
||||
&mut self, terminal: &mut Terminal<B>, app_state: &mut app::AppState,
|
||||
) -> error::Result<()> {
|
||||
terminal.draw(|mut f| {
|
||||
let (draw_area, frozen_draw_loc) = if app_state.is_frozen() {
|
||||
let (draw_area, frozen_draw_loc) = if false {
|
||||
let split_loc = Layout::default()
|
||||
.constraints([Constraint::Min(0), Constraint::Length(1)])
|
||||
.split(f.size());
|
||||
@ -149,118 +147,7 @@ impl Painter {
|
||||
let terminal_height = draw_area.height;
|
||||
let terminal_width = draw_area.width;
|
||||
|
||||
if let DialogState::Shown(help_dialog) = &mut app_state.help_dialog {
|
||||
let gen_help_len = GENERAL_HELP_TEXT.len() as u16 + 3;
|
||||
let border_len = terminal_height.saturating_sub(gen_help_len) / 2;
|
||||
let vertical_dialog_chunk = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(border_len),
|
||||
Constraint::Length(gen_help_len),
|
||||
Constraint::Length(border_len),
|
||||
])
|
||||
.split(draw_area);
|
||||
|
||||
let middle_dialog_chunk = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(if terminal_width < 100 {
|
||||
// TODO: [Drawing, Hard-coded] The point we start changing size at currently hard-coded in.
|
||||
[
|
||||
Constraint::Percentage(0),
|
||||
Constraint::Percentage(100),
|
||||
Constraint::Percentage(0),
|
||||
]
|
||||
} else {
|
||||
[
|
||||
Constraint::Percentage(20),
|
||||
Constraint::Percentage(60),
|
||||
Constraint::Percentage(20),
|
||||
]
|
||||
})
|
||||
.split(vertical_dialog_chunk[1]);
|
||||
|
||||
help_dialog.draw_help(self, f, middle_dialog_chunk[1]);
|
||||
} else if app_state.delete_dialog_state.is_showing_dd {
|
||||
// TODO: [Drawing] Better dd sizing needs the paragraph wrap feature from tui-rs to be pushed to
|
||||
// complete... but for now it's pretty close!
|
||||
// The main problem right now is that I cannot properly calculate the height offset since
|
||||
// line-wrapping is NOT the same as taking the width of the text and dividing by width.
|
||||
// So, I need the height AFTER wrapping.
|
||||
// See: https://github.com/fdehau/tui-rs/pull/349. Land this after this pushes to release.
|
||||
//
|
||||
// ADDENDUM: I could probably use the same textwrap trick I did with the help menu for this.
|
||||
|
||||
let dd_text = self.get_dd_spans(app_state);
|
||||
|
||||
let text_width = if terminal_width < 100 {
|
||||
terminal_width * 90 / 100
|
||||
} else {
|
||||
terminal_width * 50 / 100
|
||||
};
|
||||
|
||||
let text_height = if cfg!(target_os = "windows")
|
||||
|| !app_state.app_config_fields.is_advanced_kill
|
||||
{
|
||||
7
|
||||
} else {
|
||||
22
|
||||
};
|
||||
|
||||
// let (text_width, text_height) = if let Some(dd_text) = &dd_text {
|
||||
// let width = if current_width < 100 {
|
||||
// current_width * 90 / 100
|
||||
// } else {
|
||||
// let min_possible_width = (current_width * 50 / 100) as usize;
|
||||
// let mut width = dd_text.width();
|
||||
|
||||
// // This should theoretically never allow width to be 0... we can be safe and do an extra check though.
|
||||
// while width > (current_width as usize) && width / 2 > min_possible_width {
|
||||
// width /= 2;
|
||||
// }
|
||||
|
||||
// std::cmp::max(width, min_possible_width) as u16
|
||||
// };
|
||||
|
||||
// (
|
||||
// width,
|
||||
// (dd_text.height() + 2 + (dd_text.width() / width as usize)) as u16,
|
||||
// )
|
||||
// } else {
|
||||
// // AFAIK this shouldn't happen, unless something went wrong...
|
||||
// (
|
||||
// if current_width < 100 {
|
||||
// current_width * 90 / 100
|
||||
// } else {
|
||||
// current_width * 50 / 100
|
||||
// },
|
||||
// 7,
|
||||
// )
|
||||
// };
|
||||
|
||||
let vertical_bordering = terminal_height.saturating_sub(text_height) / 2;
|
||||
let vertical_dialog_chunk = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(vertical_bordering),
|
||||
Constraint::Length(text_height),
|
||||
Constraint::Length(vertical_bordering),
|
||||
])
|
||||
.split(draw_area);
|
||||
|
||||
let horizontal_bordering = terminal_width.saturating_sub(text_width) / 2;
|
||||
let middle_dialog_chunk = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([
|
||||
Constraint::Length(horizontal_bordering),
|
||||
Constraint::Length(text_width),
|
||||
Constraint::Length(horizontal_bordering),
|
||||
])
|
||||
.split(vertical_dialog_chunk[1]);
|
||||
|
||||
// This is a bit nasty, but it works well... I guess.
|
||||
app_state.delete_dialog_state.is_showing_dd =
|
||||
self.draw_dd_dialog(&mut f, dd_text, app_state, middle_dialog_chunk[1]);
|
||||
} else if app_state.is_expanded {
|
||||
if false {
|
||||
if let Some(frozen_draw_loc) = frozen_draw_loc {
|
||||
self.draw_frozen_indicator(&mut f, frozen_draw_loc);
|
||||
}
|
||||
@ -276,7 +163,7 @@ impl Painter {
|
||||
/// A simple traversal through the `arena`, drawing all leaf elements.
|
||||
fn traverse_and_draw_tree<B: Backend>(
|
||||
node: NodeId, arena: &Arena<LayoutNode>, f: &mut Frame<'_, B>,
|
||||
lookup_map: &mut FxHashMap<NodeId, TmpBottomWidget>, painter: &Painter,
|
||||
lookup_map: &mut FxHashMap<NodeId, BottomWidget>, painter: &Painter,
|
||||
selected_id: NodeId, offset_x: u16, offset_y: u16,
|
||||
) {
|
||||
if let Some(layout_node) = arena.get(node).map(|n| n.get()) {
|
||||
@ -306,7 +193,7 @@ impl Painter {
|
||||
);
|
||||
|
||||
if let Some(widget) = lookup_map.get_mut(&node) {
|
||||
if let TmpBottomWidget::Carousel(carousel) = widget {
|
||||
if let BottomWidget::Carousel(carousel) = widget {
|
||||
let remaining_area: Rect =
|
||||
carousel.draw_carousel(painter, f, area);
|
||||
if let Some(to_draw_node) =
|
||||
|
@ -1,2 +0,0 @@
|
||||
pub mod dd_dialog;
|
||||
pub use dd_dialog::KillDialog;
|
@ -1,409 +0,0 @@
|
||||
#[cfg(target_family = "unix")]
|
||||
use std::cmp::min;
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
terminal::Frame,
|
||||
text::{Span, Spans, Text},
|
||||
widgets::{Block, Borders, Paragraph, Wrap},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app::{AppState, KillSignal},
|
||||
canvas::Painter,
|
||||
};
|
||||
|
||||
const DD_BASE: &str = " Confirm Kill Process ── Esc to close ";
|
||||
const DD_ERROR_BASE: &str = " Error ── Esc to close ";
|
||||
|
||||
pub trait KillDialog {
|
||||
fn get_dd_spans(&self, app_state: &AppState) -> Option<Text<'_>>;
|
||||
|
||||
fn draw_dd_confirm_buttons<B: Backend>(
|
||||
&self, f: &mut Frame<'_, B>, button_draw_loc: &Rect, app_state: &mut AppState,
|
||||
);
|
||||
|
||||
fn draw_dd_dialog<B: Backend>(
|
||||
&self, f: &mut Frame<'_, B>, dd_text: Option<Text<'_>>, app_state: &mut AppState,
|
||||
draw_loc: Rect,
|
||||
) -> bool;
|
||||
}
|
||||
|
||||
impl KillDialog for Painter {
|
||||
fn get_dd_spans(&self, app_state: &AppState) -> Option<Text<'_>> {
|
||||
if let Some(dd_err) = &app_state.dd_err {
|
||||
return Some(Text::from(vec![
|
||||
Spans::default(),
|
||||
Spans::from("Failed to kill process."),
|
||||
Spans::from(dd_err.clone()),
|
||||
Spans::from("Please press ENTER or ESC to close this dialog."),
|
||||
]));
|
||||
} else if let Some(_to_kill_processes) = app_state.get_to_delete_processes() {
|
||||
// if let Some(first_pid) = to_kill_processes.1.first() {
|
||||
// return Some(Text::from(vec![
|
||||
// Spans::from(""),
|
||||
// if app_state.is_grouped(app_state.current_widget.widget_id) {
|
||||
// if to_kill_processes.1.len() != 1 {
|
||||
// Spans::from(format!(
|
||||
// "Kill {} processes with the name \"{}\"? Press ENTER to confirm.",
|
||||
// to_kill_processes.1.len(),
|
||||
// to_kill_processes.0
|
||||
// ))
|
||||
// } else {
|
||||
// Spans::from(format!(
|
||||
// "Kill 1 process with the name \"{}\"? Press ENTER to confirm.",
|
||||
// to_kill_processes.0
|
||||
// ))
|
||||
// }
|
||||
// } else {
|
||||
// Spans::from(format!(
|
||||
// "Kill process \"{}\" with PID {}? Press ENTER to confirm.",
|
||||
// to_kill_processes.0, first_pid
|
||||
// ))
|
||||
// },
|
||||
// ]));
|
||||
// }
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn draw_dd_confirm_buttons<B: Backend>(
|
||||
&self, f: &mut Frame<'_, B>, button_draw_loc: &Rect, app_state: &mut AppState,
|
||||
) {
|
||||
if cfg!(target_os = "windows") || !app_state.app_config_fields.is_advanced_kill {
|
||||
let (yes_button, no_button) = match app_state.delete_dialog_state.selected_signal {
|
||||
KillSignal::Kill(_) => (
|
||||
Span::styled("Yes", self.colours.currently_selected_text_style),
|
||||
Span::raw("No"),
|
||||
),
|
||||
KillSignal::Cancel => (
|
||||
Span::raw("Yes"),
|
||||
Span::styled("No", self.colours.currently_selected_text_style),
|
||||
),
|
||||
};
|
||||
|
||||
let button_layout = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(35),
|
||||
Constraint::Percentage(30),
|
||||
Constraint::Percentage(35),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(*button_draw_loc);
|
||||
|
||||
f.render_widget(
|
||||
Paragraph::new(yes_button)
|
||||
.block(Block::default())
|
||||
.alignment(Alignment::Right),
|
||||
button_layout[0],
|
||||
);
|
||||
f.render_widget(
|
||||
Paragraph::new(no_button)
|
||||
.block(Block::default())
|
||||
.alignment(Alignment::Left),
|
||||
button_layout[2],
|
||||
);
|
||||
|
||||
if app_state.should_get_widget_bounds() {
|
||||
// This is kinda weird, but the gist is:
|
||||
// - We have three sections; we put our mouse bounding box for the "yes" button at the very right edge
|
||||
// of the left section and 3 characters back. We then give it a buffer size of 1 on the x-coordinate.
|
||||
// - Same for the "no" button, except it is the right section and we do it from the start of the right
|
||||
// section.
|
||||
//
|
||||
// Lastly, note that mouse detection for the dd buttons assume correct widths. As such, we correct
|
||||
// them here and check with >= and <= mouse bound checks, as opposed to how we do it elsewhere with
|
||||
// >= and <. See https://github.com/ClementTsang/bottom/pull/459 for details.
|
||||
app_state.delete_dialog_state.button_positions = vec![
|
||||
// Yes
|
||||
(
|
||||
button_layout[0].x + button_layout[0].width - 4,
|
||||
button_layout[0].y,
|
||||
button_layout[0].x + button_layout[0].width,
|
||||
button_layout[0].y,
|
||||
if cfg!(target_os = "windows") { 1 } else { 15 },
|
||||
),
|
||||
// No
|
||||
(
|
||||
button_layout[2].x - 1,
|
||||
button_layout[2].y,
|
||||
button_layout[2].x + 2,
|
||||
button_layout[2].y,
|
||||
0,
|
||||
),
|
||||
];
|
||||
}
|
||||
} else {
|
||||
#[cfg(target_family = "unix")]
|
||||
{
|
||||
// TODO: [Optimization, Const] Can probably make this const.
|
||||
let signal_text;
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
signal_text = vec![
|
||||
"0: Cancel",
|
||||
"1: HUP",
|
||||
"2: INT",
|
||||
"3: QUIT",
|
||||
"4: ILL",
|
||||
"5: TRAP",
|
||||
"6: ABRT",
|
||||
"7: BUS",
|
||||
"8: FPE",
|
||||
"9: KILL",
|
||||
"10: USR1",
|
||||
"11: SEGV",
|
||||
"12: USR2",
|
||||
"13: PIPE",
|
||||
"14: ALRM",
|
||||
"15: TERM",
|
||||
"16: STKFLT",
|
||||
"17: CHLD",
|
||||
"18: CONT",
|
||||
"19: STOP",
|
||||
"20: TSTP",
|
||||
"21: TTIN",
|
||||
"22: TTOU",
|
||||
"23: URG",
|
||||
"24: XCPU",
|
||||
"25: XFSZ",
|
||||
"26: VTALRM",
|
||||
"27: PROF",
|
||||
"28: WINCH",
|
||||
"29: IO",
|
||||
"30: PWR",
|
||||
"31: SYS",
|
||||
"34: RTMIN",
|
||||
"35: RTMIN+1",
|
||||
"36: RTMIN+2",
|
||||
"37: RTMIN+3",
|
||||
"38: RTMIN+4",
|
||||
"39: RTMIN+5",
|
||||
"40: RTMIN+6",
|
||||
"41: RTMIN+7",
|
||||
"42: RTMIN+8",
|
||||
"43: RTMIN+9",
|
||||
"44: RTMIN+10",
|
||||
"45: RTMIN+11",
|
||||
"46: RTMIN+12",
|
||||
"47: RTMIN+13",
|
||||
"48: RTMIN+14",
|
||||
"49: RTMIN+15",
|
||||
"50: RTMAX-14",
|
||||
"51: RTMAX-13",
|
||||
"52: RTMAX-12",
|
||||
"53: RTMAX-11",
|
||||
"54: RTMAX-10",
|
||||
"55: RTMAX-9",
|
||||
"56: RTMAX-8",
|
||||
"57: RTMAX-7",
|
||||
"58: RTMAX-6",
|
||||
"59: RTMAX-5",
|
||||
"60: RTMAX-4",
|
||||
"61: RTMAX-3",
|
||||
"62: RTMAX-2",
|
||||
"63: RTMAX-1",
|
||||
"64: RTMAX",
|
||||
];
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
signal_text = vec![
|
||||
"0: Cancel",
|
||||
"1: HUP",
|
||||
"2: INT",
|
||||
"3: QUIT",
|
||||
"4: ILL",
|
||||
"5: TRAP",
|
||||
"6: ABRT",
|
||||
"7: EMT",
|
||||
"8: FPE",
|
||||
"9: KILL",
|
||||
"10: BUS",
|
||||
"11: SEGV",
|
||||
"12: SYS",
|
||||
"13: PIPE",
|
||||
"14: ALRM",
|
||||
"15: TERM",
|
||||
"16: URG",
|
||||
"17: STOP",
|
||||
"18: TSTP",
|
||||
"19: CONT",
|
||||
"20: CHLD",
|
||||
"21: TTIN",
|
||||
"22: TTOU",
|
||||
"23: IO",
|
||||
"24: XCPU",
|
||||
"25: XFSZ",
|
||||
"26: VTALRM",
|
||||
"27: PROF",
|
||||
"28: WINCH",
|
||||
"29: INFO",
|
||||
"30: USR1",
|
||||
"31: USR2",
|
||||
];
|
||||
}
|
||||
|
||||
let button_rect = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.margin(1)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length((button_draw_loc.width - 14) / 2),
|
||||
Constraint::Min(0),
|
||||
Constraint::Length((button_draw_loc.width - 14) / 2),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(*button_draw_loc)[1];
|
||||
|
||||
let mut selected = match app_state.delete_dialog_state.selected_signal {
|
||||
KillSignal::Cancel => 0,
|
||||
KillSignal::Kill(signal) => signal,
|
||||
};
|
||||
// 32+33 are skipped
|
||||
if selected > 31 {
|
||||
selected -= 2;
|
||||
}
|
||||
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![Constraint::Min(1); button_rect.height as usize])
|
||||
.split(button_rect);
|
||||
|
||||
let prev_offset: usize = app_state.delete_dialog_state.scroll_pos;
|
||||
app_state.delete_dialog_state.scroll_pos = if selected == 0 {
|
||||
0
|
||||
} else if selected < prev_offset + 1 {
|
||||
selected - 1
|
||||
} else if selected > prev_offset + (layout.len() as usize) - 1 {
|
||||
selected - (layout.len() as usize) + 1
|
||||
} else {
|
||||
prev_offset
|
||||
};
|
||||
let scroll_offset: usize = app_state.delete_dialog_state.scroll_pos;
|
||||
|
||||
let mut buttons = signal_text[scroll_offset + 1
|
||||
..min((layout.len() as usize) + scroll_offset, signal_text.len())]
|
||||
.iter()
|
||||
.map(|text| Span::raw(*text))
|
||||
.collect::<Vec<Span<'_>>>();
|
||||
buttons.insert(0, Span::raw(signal_text[0]));
|
||||
buttons[selected - scroll_offset] = Span::styled(
|
||||
signal_text[selected],
|
||||
self.colours.currently_selected_text_style,
|
||||
);
|
||||
|
||||
app_state.delete_dialog_state.button_positions = layout
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, pos)| {
|
||||
(
|
||||
pos.x,
|
||||
pos.y,
|
||||
pos.x + pos.width - 1,
|
||||
pos.y + pos.height - 1,
|
||||
if i == 0 { 0 } else { scroll_offset } + i,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<(u16, u16, u16, u16, usize)>>();
|
||||
|
||||
for (btn, pos) in buttons.into_iter().zip(layout.into_iter()) {
|
||||
f.render_widget(Paragraph::new(btn).alignment(Alignment::Left), pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_dd_dialog<B: Backend>(
|
||||
&self, f: &mut Frame<'_, B>, dd_text: Option<Text<'_>>, app_state: &mut AppState,
|
||||
draw_loc: Rect,
|
||||
) -> bool {
|
||||
if let Some(dd_text) = dd_text {
|
||||
let dd_title = if app_state.dd_err.is_some() {
|
||||
Spans::from(vec![
|
||||
Span::styled(" Error ", self.colours.widget_title_style),
|
||||
Span::styled(
|
||||
format!(
|
||||
"─{}─ Esc to close ",
|
||||
"─".repeat(
|
||||
usize::from(draw_loc.width)
|
||||
.saturating_sub(DD_ERROR_BASE.chars().count() + 2)
|
||||
)
|
||||
),
|
||||
self.colours.border_style,
|
||||
),
|
||||
])
|
||||
} else {
|
||||
Spans::from(vec![
|
||||
Span::styled(" Confirm Kill Process ", self.colours.widget_title_style),
|
||||
Span::styled(
|
||||
format!(
|
||||
"─{}─ Esc to close ",
|
||||
"─".repeat(
|
||||
usize::from(draw_loc.width)
|
||||
.saturating_sub(DD_BASE.chars().count() + 2)
|
||||
)
|
||||
),
|
||||
self.colours.border_style,
|
||||
),
|
||||
])
|
||||
};
|
||||
|
||||
f.render_widget(
|
||||
Paragraph::new(dd_text)
|
||||
.block(
|
||||
Block::default()
|
||||
.title(dd_title)
|
||||
.style(self.colours.border_style)
|
||||
.borders(Borders::ALL)
|
||||
.border_style(self.colours.border_style),
|
||||
)
|
||||
.style(self.colours.text_style)
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(Wrap { trim: true }),
|
||||
draw_loc,
|
||||
);
|
||||
|
||||
let btn_height =
|
||||
if cfg!(target_os = "windows") || !app_state.app_config_fields.is_advanced_kill {
|
||||
3
|
||||
} else {
|
||||
20
|
||||
};
|
||||
|
||||
// Now draw buttons if needed...
|
||||
let split_draw_loc = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
if app_state.dd_err.is_some() {
|
||||
vec![Constraint::Percentage(100)]
|
||||
} else {
|
||||
vec![Constraint::Min(3), Constraint::Length(btn_height)]
|
||||
}
|
||||
.as_ref(),
|
||||
)
|
||||
.split(draw_loc);
|
||||
|
||||
// This being true implies that dd_err is none.
|
||||
if let Some(button_draw_loc) = split_draw_loc.get(1) {
|
||||
self.draw_dd_confirm_buttons(f, button_draw_loc, app_state);
|
||||
}
|
||||
|
||||
if app_state.dd_err.is_some() {
|
||||
return app_state.delete_dialog_state.is_showing_dd;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Currently we just return "false" if things go wrong finding
|
||||
// the process or a first PID (if an error arises it should be caught).
|
||||
// I don't really like this, and I find it ugly, but it works for now.
|
||||
false
|
||||
}
|
||||
}
|
53
src/lib.rs
53
src/lib.rs
@ -20,15 +20,16 @@ use std::{
|
||||
};
|
||||
|
||||
use crossterm::{
|
||||
event::{poll, read, DisableMouseCapture, Event, KeyEvent, MouseEvent, MouseEventKind},
|
||||
event::{poll, read, DisableMouseCapture, MouseEventKind},
|
||||
execute,
|
||||
style::Print,
|
||||
terminal::{disable_raw_mode, LeaveAlternateScreen},
|
||||
};
|
||||
|
||||
use app::{data_harvester, AppState, UsedWidgets};
|
||||
use app::{data_harvester, AppMessages, UsedWidgets};
|
||||
use constants::*;
|
||||
use options::*;
|
||||
use tuice::{Event, RuntimeEvent};
|
||||
use utils::error;
|
||||
|
||||
pub mod app;
|
||||
@ -42,7 +43,7 @@ pub mod clap;
|
||||
pub mod constants;
|
||||
pub mod data_conversion;
|
||||
pub mod options;
|
||||
pub(crate) mod tuine;
|
||||
pub mod tuice;
|
||||
pub(crate) mod units;
|
||||
|
||||
// FIXME: Use newtype pattern for PID
|
||||
@ -52,15 +53,6 @@ pub type Pid = usize;
|
||||
#[cfg(target_family = "unix")]
|
||||
pub type Pid = libc::pid_t;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum BottomEvent {
|
||||
KeyInput(KeyEvent),
|
||||
MouseInput(MouseEvent),
|
||||
Update(Box<data_harvester::Data>),
|
||||
Resize { width: u16, height: u16 },
|
||||
Clean,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ThreadControlEvent {
|
||||
Reset,
|
||||
@ -124,18 +116,6 @@ pub fn create_or_get_config(config_path: &Option<PathBuf>) -> error::Result<Conf
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_drawing(
|
||||
terminal: &mut tui::terminal::Terminal<tui::backend::CrosstermBackend<std::io::Stdout>>,
|
||||
app: &mut AppState, painter: &mut canvas::Painter,
|
||||
) -> error::Result<()> {
|
||||
if let Err(err) = painter.draw_data(terminal, app) {
|
||||
cleanup_terminal(terminal)?;
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn cleanup_terminal(
|
||||
terminal: &mut tui::terminal::Terminal<tui::backend::CrosstermBackend<std::io::Stdout>>,
|
||||
) -> error::Result<()> {
|
||||
@ -182,7 +162,8 @@ pub fn panic_hook(panic_info: &PanicInfo<'_>) {
|
||||
}
|
||||
|
||||
pub fn create_input_thread(
|
||||
sender: std::sync::mpsc::Sender<BottomEvent>, termination_ctrl_lock: Arc<Mutex<bool>>,
|
||||
sender: std::sync::mpsc::Sender<RuntimeEvent<AppMessages>>,
|
||||
termination_ctrl_lock: Arc<Mutex<bool>>,
|
||||
) -> std::thread::JoinHandle<()> {
|
||||
thread::spawn(move || {
|
||||
// TODO: [Optimization, Input] Maybe experiment with removing these timers. Look into using buffers instead?
|
||||
@ -202,29 +183,35 @@ pub fn create_input_thread(
|
||||
if poll {
|
||||
if let Ok(event) = read() {
|
||||
match event {
|
||||
Event::Key(event) => {
|
||||
crossterm::event::Event::Key(event) => {
|
||||
if Instant::now().duration_since(keyboard_timer).as_millis() >= 20 {
|
||||
if sender.send(BottomEvent::KeyInput(event)).is_err() {
|
||||
if sender
|
||||
.send(RuntimeEvent::UserInterface(Event::Keyboard(event)))
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
keyboard_timer = Instant::now();
|
||||
}
|
||||
}
|
||||
Event::Mouse(event) => match &event.kind {
|
||||
crossterm::event::Event::Mouse(event) => match &event.kind {
|
||||
MouseEventKind::Drag(_) => {}
|
||||
MouseEventKind::Moved => {}
|
||||
_ => {
|
||||
if Instant::now().duration_since(mouse_timer).as_millis() >= 20
|
||||
{
|
||||
if sender.send(BottomEvent::MouseInput(event)).is_err() {
|
||||
if sender
|
||||
.send(RuntimeEvent::UserInterface(Event::Mouse(event)))
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
mouse_timer = Instant::now();
|
||||
}
|
||||
}
|
||||
},
|
||||
Event::Resize(width, height) => {
|
||||
if sender.send(BottomEvent::Resize { width, height }).is_err() {
|
||||
crossterm::event::Event::Resize(width, height) => {
|
||||
if sender.send(RuntimeEvent::Resize { width, height }).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -237,7 +224,7 @@ pub fn create_input_thread(
|
||||
}
|
||||
|
||||
pub fn create_collection_thread(
|
||||
sender: std::sync::mpsc::Sender<BottomEvent>,
|
||||
sender: std::sync::mpsc::Sender<RuntimeEvent<AppMessages>>,
|
||||
control_receiver: std::sync::mpsc::Receiver<ThreadControlEvent>,
|
||||
termination_ctrl_lock: Arc<Mutex<bool>>, termination_ctrl_cvar: Arc<Condvar>,
|
||||
app_config_fields: &app::AppConfigFields, filters: app::DataFilters,
|
||||
@ -300,7 +287,7 @@ pub fn create_collection_thread(
|
||||
}
|
||||
}
|
||||
|
||||
let event = BottomEvent::Update(Box::from(data_state.data));
|
||||
let event = RuntimeEvent::Custom(AppMessages::Update(Box::from(data_state.data)));
|
||||
data_state.data = data_harvester::Data::default();
|
||||
if sender.send(event).is_err() {
|
||||
break;
|
||||
|
@ -4,7 +4,7 @@ use std::str::FromStr;
|
||||
|
||||
use crate::{
|
||||
app::{layout_manager::*, *},
|
||||
canvas::ColourScheme,
|
||||
canvas::{ColourScheme, Painter},
|
||||
constants::*,
|
||||
units::data_units::DataUnit,
|
||||
utils::error::{self, BottomError},
|
||||
@ -255,11 +255,8 @@ pub fn build_app(matches: &clap::ArgMatches<'static>, config: &mut Config) -> Re
|
||||
net_filter,
|
||||
};
|
||||
|
||||
Ok(AppState::new(
|
||||
app_config_fields,
|
||||
data_filter,
|
||||
layout_tree_output,
|
||||
))
|
||||
let painter = Painter::init(&config, get_color_scheme(&matches, &config)?)?;
|
||||
AppState::new(app_config_fields, data_filter, layout_tree_output, painter)
|
||||
}
|
||||
|
||||
fn get_update_rate_in_milliseconds(
|
||||
|
40
src/tuice/application.rs
Normal file
40
src/tuice/application.rs
Normal file
@ -0,0 +1,40 @@
|
||||
use std::{fmt::Debug, sync::mpsc::Receiver};
|
||||
|
||||
use super::{
|
||||
runtime::{self, RuntimeEvent},
|
||||
Component, Event,
|
||||
};
|
||||
|
||||
/// An alias to the [`tui::backend::CrosstermBackend`] writing to [`std::io::Stdout`].
|
||||
pub type CrosstermBackend = tui::backend::CrosstermBackend<std::io::Stdout>;
|
||||
|
||||
#[allow(unused_variables)]
|
||||
pub trait Application: Sized {
|
||||
type Message: Debug;
|
||||
|
||||
/// Determines how to handle a given message.
|
||||
fn update(&mut self, message: Self::Message);
|
||||
|
||||
/// Returns whether to stop the application. Defaults to
|
||||
/// always returning false.
|
||||
fn is_terminated(&self) -> bool;
|
||||
|
||||
fn view(&mut self) -> Box<dyn Component<Self::Message, CrosstermBackend>>;
|
||||
|
||||
/// To run upon stopping the application.
|
||||
fn destroy(&mut self) {}
|
||||
|
||||
/// An optional event handler, intended for use with global shortcuts or events.
|
||||
/// This will be run *after* trying to send the events into the user interface, and
|
||||
/// *only* if it is not handled at all by it.
|
||||
///
|
||||
/// Defaults to not doing anything.
|
||||
fn global_event_handler(&mut self, event: Event, messages: &mut Vec<Self::Message>) {}
|
||||
}
|
||||
|
||||
/// Launches some application with tuice.
|
||||
pub fn launch_with_application<A: Application + 'static>(
|
||||
application: A, receiver: Receiver<RuntimeEvent<A::Message>>,
|
||||
) {
|
||||
runtime::launch(application, receiver);
|
||||
}
|
24
src/tuice/component.rs
Normal file
24
src/tuice/component.rs
Normal file
@ -0,0 +1,24 @@
|
||||
pub mod base;
|
||||
pub use base::*;
|
||||
|
||||
use tui::{layout::Rect, Frame};
|
||||
|
||||
use super::{Event, Status};
|
||||
|
||||
/// A component displays information and can be interacted with.
|
||||
#[allow(unused_variables)]
|
||||
pub trait Component<Message, Backend>
|
||||
where
|
||||
Backend: tui::backend::Backend,
|
||||
{
|
||||
/// Handles an [`Event`]. Defaults to just ignoring the event.
|
||||
fn on_event(&mut self, bounds: Rect, event: Event, messages: &mut Vec<Message>) -> Status {
|
||||
Status::Ignored
|
||||
}
|
||||
|
||||
/// Returns the desired layout of the component. Defaults to returning
|
||||
fn layout(&self) {}
|
||||
|
||||
/// Draws the component.
|
||||
fn draw(&mut self, bounds: Rect, frame: &mut Frame<'_, Backend>);
|
||||
}
|
18
src/tuice/component/base/block.rs
Normal file
18
src/tuice/component/base/block.rs
Normal file
@ -0,0 +1,18 @@
|
||||
use tui::{backend::Backend, layout::Rect, Frame};
|
||||
|
||||
use crate::tuice::{Component, Event, Status};
|
||||
|
||||
pub struct Block {}
|
||||
|
||||
impl<Message, B> Component<Message, B> for Block
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
fn draw(&mut self, _bounds: Rect, _frame: &mut Frame<'_, B>) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn on_event(&mut self, _bounds: Rect, _event: Event, _messages: &mut Vec<Message>) -> Status {
|
||||
Status::Ignored
|
||||
}
|
||||
}
|
18
src/tuice/component/base/carousel.rs
Normal file
18
src/tuice/component/base/carousel.rs
Normal file
@ -0,0 +1,18 @@
|
||||
use tui::{backend::Backend, layout::Rect, Frame};
|
||||
|
||||
use crate::tuice::{Component, Event, Status};
|
||||
|
||||
pub struct Carousel {}
|
||||
|
||||
impl<Message, B> Component<Message, B> for Carousel
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
fn draw(&mut self, _bounds: Rect, _frame: &mut Frame<'_, B>) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn on_event(&mut self, _bounds: Rect, _event: Event, _messages: &mut Vec<Message>) -> Status {
|
||||
Status::Ignored
|
||||
}
|
||||
}
|
18
src/tuice/component/base/column.rs
Normal file
18
src/tuice/component/base/column.rs
Normal file
@ -0,0 +1,18 @@
|
||||
use tui::{backend::Backend, layout::Rect, Frame};
|
||||
|
||||
use crate::tuice::{Component, Event, Status};
|
||||
|
||||
pub struct Column {}
|
||||
|
||||
impl<Message, B> Component<Message, B> for Column
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
fn draw(&mut self, _bounds: Rect, _frame: &mut Frame<'_, B>) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn on_event(&mut self, _bounds: Rect, _event: Event, _messages: &mut Vec<Message>) -> Status {
|
||||
Status::Ignored
|
||||
}
|
||||
}
|
17
src/tuice/component/base/mod.rs
Normal file
17
src/tuice/component/base/mod.rs
Normal file
@ -0,0 +1,17 @@
|
||||
pub mod text_table;
|
||||
pub use text_table::{TextColumn, TextColumnConstraint, TextTable};
|
||||
|
||||
pub mod shortcut;
|
||||
pub use shortcut::Shortcut;
|
||||
|
||||
pub mod row;
|
||||
pub use row::Row;
|
||||
|
||||
pub mod column;
|
||||
pub use column::Column;
|
||||
|
||||
pub mod block;
|
||||
pub use block::Block;
|
||||
|
||||
pub mod carousel;
|
||||
pub use carousel::Carousel;
|
42
src/tuice/component/base/row.rs
Normal file
42
src/tuice/component/base/row.rs
Normal file
@ -0,0 +1,42 @@
|
||||
use tui::{backend::Backend, layout::Rect, Frame};
|
||||
|
||||
use crate::tuice::{Component, Event, Status};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Row<'a, Message, B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
children: Vec<Box<dyn Component<Message, B> + 'a>>, // FIXME: For performance purposes, let's cheat and use enum-dispatch
|
||||
}
|
||||
|
||||
impl<'a, Message, B> Row<'a, Message, B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
/// Creates a new [`Row`] with the given children.
|
||||
pub fn with_children<C>(children: Vec<C>) -> Self
|
||||
where
|
||||
C: Into<Box<dyn Component<Message, B> + 'a>>,
|
||||
{
|
||||
Self {
|
||||
children: children.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, B> Component<Message, B> for Row<'a, Message, B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
fn draw(&mut self, bounds: Rect, frame: &mut Frame<'_, B>) {
|
||||
self.children.iter_mut().for_each(|child| {
|
||||
// TODO: This is just temp! We need layout!
|
||||
child.draw(bounds, frame);
|
||||
})
|
||||
}
|
||||
|
||||
fn on_event(&mut self, _bounds: Rect, _event: Event, _messages: &mut Vec<Message>) -> Status {
|
||||
Status::Ignored
|
||||
}
|
||||
}
|
21
src/tuice/component/base/shortcut.rs
Normal file
21
src/tuice/component/base/shortcut.rs
Normal file
@ -0,0 +1,21 @@
|
||||
use tui::{backend::Backend, layout::Rect, Frame};
|
||||
|
||||
use crate::tuice::{Component, Event, Status};
|
||||
|
||||
/// A [`Component`] to handle keyboard shortcuts and assign actions to them.
|
||||
///
|
||||
/// Inspired by [Flutter's approach](https://docs.flutter.dev/development/ui/advanced/actions_and_shortcuts).
|
||||
pub struct Shortcut {}
|
||||
|
||||
impl<Message, B> Component<Message, B> for Shortcut
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
fn draw(&mut self, _bounds: Rect, _frame: &mut Frame<'_, B>) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn on_event(&mut self, _bounds: Rect, _event: Event, _messages: &mut Vec<Message>) -> Status {
|
||||
Status::Ignored
|
||||
}
|
||||
}
|
@ -14,14 +14,12 @@ use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use crate::{
|
||||
constants::TABLE_GAP_HEIGHT_LIMIT,
|
||||
tuine::{Event, Status},
|
||||
tuice::{Component, Event, Status},
|
||||
};
|
||||
|
||||
pub use self::table_column::{TextColumn, TextColumnConstraint};
|
||||
use self::table_scroll_state::ScrollState as TextTableState;
|
||||
|
||||
use super::{Component, ShouldRender};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct StyleSheet {
|
||||
text: Style,
|
||||
@ -32,19 +30,21 @@ pub struct StyleSheet {
|
||||
pub enum TextTableMsg {}
|
||||
|
||||
/// A sortable, scrollable table for text data.
|
||||
pub struct TextTable<'a> {
|
||||
pub struct TextTable<'a, Message> {
|
||||
state: TextTableState,
|
||||
column_widths: Vec<u16>,
|
||||
columns: Vec<TextColumn>,
|
||||
show_gap: bool,
|
||||
show_selected_entry: bool,
|
||||
data: Vec<Row<'a>>,
|
||||
rows: Vec<Row<'a>>,
|
||||
style_sheet: StyleSheet,
|
||||
sortable: bool,
|
||||
table_gap: u16,
|
||||
on_select: Option<Box<dyn Fn(usize) -> Message>>,
|
||||
on_selected_click: Option<Box<dyn Fn(usize) -> Message>>,
|
||||
}
|
||||
|
||||
impl<'a> TextTable<'a> {
|
||||
impl<'a, Message> TextTable<'a, Message> {
|
||||
pub fn new<S: Into<Cow<'static, str>>>(columns: Vec<S>) -> Self {
|
||||
Self {
|
||||
state: TextTableState::default(),
|
||||
@ -55,17 +55,27 @@ impl<'a> TextTable<'a> {
|
||||
.collect(),
|
||||
show_gap: true,
|
||||
show_selected_entry: true,
|
||||
data: Vec::default(),
|
||||
rows: Vec::default(),
|
||||
style_sheet: StyleSheet::default(),
|
||||
sortable: false,
|
||||
table_gap: 0,
|
||||
on_select: None,
|
||||
on_selected_click: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the row to display in the table.
|
||||
///
|
||||
/// Defaults to displaying no data if not set.
|
||||
pub fn rows(mut self, rows: Vec<Row<'a>>) -> Self {
|
||||
self.rows = rows;
|
||||
self
|
||||
}
|
||||
|
||||
/// Whether to try to show a gap between the table headers and data.
|
||||
/// Note that if there isn't enough room, the gap will still be hidden.
|
||||
///
|
||||
/// Defaults to `true`.
|
||||
/// Defaults to `true` if not set.
|
||||
pub fn show_gap(mut self, show_gap: bool) -> Self {
|
||||
self.show_gap = show_gap;
|
||||
self
|
||||
@ -73,7 +83,7 @@ impl<'a> TextTable<'a> {
|
||||
|
||||
/// Whether to highlight the selected entry.
|
||||
///
|
||||
/// Defaults to `true`.
|
||||
/// Defaults to `true` if not set.
|
||||
pub fn show_selected_entry(mut self, show_selected_entry: bool) -> Self {
|
||||
self.show_selected_entry = show_selected_entry;
|
||||
self
|
||||
@ -81,12 +91,31 @@ impl<'a> TextTable<'a> {
|
||||
|
||||
/// Whether the table should display as sortable.
|
||||
///
|
||||
/// Defaults to `false`.
|
||||
/// Defaults to `false` if not set.
|
||||
pub fn sortable(mut self, sortable: bool) -> Self {
|
||||
self.sortable = sortable;
|
||||
self
|
||||
}
|
||||
|
||||
/// What to do when selecting an entry. Expects a boxed function that takes in
|
||||
/// the currently selected index and returns a [`Message`].
|
||||
///
|
||||
/// Defaults to `None` if not set.
|
||||
pub fn on_select(mut self, on_select: Option<Box<dyn Fn(usize) -> Message>>) -> Self {
|
||||
self.on_select = on_select;
|
||||
self
|
||||
}
|
||||
|
||||
/// What to do when clicking on an entry that is already selected.
|
||||
///
|
||||
/// Defaults to `None` if not set.
|
||||
pub fn on_selected_click(
|
||||
mut self, on_selected_click: Option<Box<dyn Fn(usize) -> Message>>,
|
||||
) -> Self {
|
||||
self.on_selected_click = on_selected_click;
|
||||
self
|
||||
}
|
||||
|
||||
fn update_column_widths(&mut self, bounds: Rect) {
|
||||
let total_width = bounds.width;
|
||||
let mut width_remaining = bounds.width;
|
||||
@ -136,19 +165,34 @@ impl<'a> TextTable<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Component for TextTable<'a> {
|
||||
type Message = TextTableMsg;
|
||||
impl<'a, Message, B> From<TextTable<'a, Message>> for Box<dyn Component<Message, B> + 'a>
|
||||
where
|
||||
Message: 'a,
|
||||
B: Backend,
|
||||
{
|
||||
fn from(table: TextTable<'a, Message>) -> Self {
|
||||
Box::new(table)
|
||||
}
|
||||
}
|
||||
|
||||
type Properties = ();
|
||||
|
||||
fn on_event(
|
||||
&mut self, bounds: Rect, event: Event, messages: &mut Vec<Self::Message>,
|
||||
) -> Status {
|
||||
use crate::tuine::MouseBoundIntersect;
|
||||
impl<'a, Message, B> Component<Message, B> for TextTable<'a, Message>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
fn on_event(&mut self, bounds: Rect, event: Event, messages: &mut Vec<Message>) -> Status {
|
||||
use crate::tuice::MouseBoundIntersect;
|
||||
use crossterm::event::{MouseButton, MouseEventKind};
|
||||
|
||||
match event {
|
||||
Event::Keyboard(_) => Status::Ignored,
|
||||
Event::Keyboard(key_event) => {
|
||||
if key_event.modifiers.is_empty() {
|
||||
match key_event.code {
|
||||
_ => Status::Ignored,
|
||||
}
|
||||
} else {
|
||||
Status::Ignored
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse_event) => {
|
||||
if mouse_event.does_mouse_intersect_bounds(bounds) {
|
||||
match mouse_event.kind {
|
||||
@ -156,8 +200,7 @@ impl<'a> Component for TextTable<'a> {
|
||||
let y = mouse_event.row - bounds.top();
|
||||
|
||||
if self.sortable && y == 0 {
|
||||
// TODO: Do this
|
||||
Status::Captured
|
||||
todo!()
|
||||
} else if y > self.table_gap {
|
||||
let visual_index = usize::from(y - self.table_gap);
|
||||
self.state.set_visual_index(visual_index)
|
||||
@ -165,8 +208,20 @@ impl<'a> Component for TextTable<'a> {
|
||||
Status::Ignored
|
||||
}
|
||||
}
|
||||
MouseEventKind::ScrollDown => self.state.move_down(1),
|
||||
MouseEventKind::ScrollUp => self.state.move_up(1),
|
||||
MouseEventKind::ScrollDown => {
|
||||
let status = self.state.move_down(1);
|
||||
if let Some(on_select) = &self.on_select {
|
||||
messages.push(on_select(self.state.current_index()));
|
||||
}
|
||||
status
|
||||
}
|
||||
MouseEventKind::ScrollUp => {
|
||||
let status = self.state.move_up(1);
|
||||
if let Some(on_select) = &self.on_select {
|
||||
messages.push(on_select(self.state.current_index()));
|
||||
}
|
||||
status
|
||||
}
|
||||
_ => Status::Ignored,
|
||||
}
|
||||
} else {
|
||||
@ -176,15 +231,9 @@ impl<'a> Component for TextTable<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Self::Message) -> ShouldRender {
|
||||
match message {}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn draw<B: Backend>(&mut self, bounds: Rect, frame: &mut Frame<'_, B>) {
|
||||
fn draw(&mut self, bounds: Rect, frame: &mut Frame<'_, B>) {
|
||||
self.table_gap = if !self.show_gap
|
||||
|| (self.data.len() + 2 > bounds.height.into()
|
||||
|| (self.rows.len() + 2 > bounds.height.into()
|
||||
&& bounds.height < TABLE_GAP_HEIGHT_LIMIT)
|
||||
{
|
||||
0
|
||||
@ -212,7 +261,7 @@ impl<'a> Component for TextTable<'a> {
|
||||
.display_start_index(bounds, scrollable_height as usize);
|
||||
let end = min(self.state.num_items(), start + scrollable_height as usize);
|
||||
|
||||
self.data[start..end].to_vec()
|
||||
self.rows[start..end].to_vec()
|
||||
};
|
||||
|
||||
// Now build up our headers...
|
@ -1,7 +1,7 @@
|
||||
use std::cmp::{min, Ordering};
|
||||
use tui::{layout::Rect, widgets::TableState};
|
||||
|
||||
use crate::tuine::Status;
|
||||
use crate::tuice::Status;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum ScrollDirection {
|
||||
@ -197,7 +197,7 @@ impl ScrollState {
|
||||
mod test {
|
||||
use tui::layout::Rect;
|
||||
|
||||
use crate::tuine::{text_table::table_scroll_state::ScrollDirection, Status};
|
||||
use crate::tuice::{text_table::table_scroll_state::ScrollDirection, Status};
|
||||
|
||||
use super::ScrollState;
|
||||
|
@ -9,6 +9,7 @@ pub enum Status {
|
||||
}
|
||||
|
||||
/// An [`Event`] represents some sort of user interface event.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum Event {
|
||||
/// A keyboard event
|
||||
Keyboard(KeyEvent),
|
11
src/tuice/layout/length.rs
Normal file
11
src/tuice/layout/length.rs
Normal file
@ -0,0 +1,11 @@
|
||||
/// Which strategy to use while laying out widgets.
|
||||
pub enum Length {
|
||||
/// Fill in remaining space. Equivalent to `Length::FlexRatio(1)`.
|
||||
Flex,
|
||||
|
||||
/// Fill in remaining space, with the value being a ratio.
|
||||
FlexRatio(u16),
|
||||
|
||||
/// Fill in a fixed amount of space.
|
||||
Fixed(u16),
|
||||
}
|
2
src/tuice/layout/mod.rs
Normal file
2
src/tuice/layout/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod length;
|
||||
pub use length::Length;
|
16
src/tuice/mod.rs
Normal file
16
src/tuice/mod.rs
Normal file
@ -0,0 +1,16 @@
|
||||
mod tui_rs;
|
||||
|
||||
pub mod component;
|
||||
pub use component::*;
|
||||
|
||||
pub mod event;
|
||||
pub use event::*;
|
||||
|
||||
pub mod application;
|
||||
pub use application::*;
|
||||
|
||||
pub mod runtime;
|
||||
pub use runtime::RuntimeEvent;
|
||||
|
||||
pub mod layout;
|
||||
pub use layout::*;
|
58
src/tuice/runtime.rs
Normal file
58
src/tuice/runtime.rs
Normal file
@ -0,0 +1,58 @@
|
||||
use std::sync::mpsc::Receiver;
|
||||
|
||||
use tui::layout::Rect;
|
||||
|
||||
use crate::tuice::Status;
|
||||
|
||||
use super::{Application, Event};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum RuntimeEvent<Message> {
|
||||
UserInterface(Event),
|
||||
Resize { width: u16, height: u16 },
|
||||
Custom(Message),
|
||||
}
|
||||
|
||||
pub(crate) fn launch<A: Application + 'static>(
|
||||
mut application: A, receiver: Receiver<RuntimeEvent<A::Message>>,
|
||||
) {
|
||||
let mut user_interface = application.view();
|
||||
|
||||
while !application.is_terminated() {
|
||||
if let Ok(event) = receiver.recv() {
|
||||
match event {
|
||||
RuntimeEvent::UserInterface(event) => {
|
||||
let mut messages = vec![];
|
||||
|
||||
let bounds = Rect::default(); // TODO: TEMP
|
||||
match user_interface.on_event(bounds, event, &mut messages) {
|
||||
Status::Captured => {}
|
||||
Status::Ignored => {
|
||||
application.global_event_handler(event, &mut messages);
|
||||
}
|
||||
}
|
||||
|
||||
for msg in messages {
|
||||
debug!("Message: {:?}", msg); // FIXME: Remove this debug line!
|
||||
application.update(msg);
|
||||
}
|
||||
|
||||
user_interface = application.view();
|
||||
}
|
||||
RuntimeEvent::Custom(message) => {
|
||||
application.update(message);
|
||||
}
|
||||
RuntimeEvent::Resize {
|
||||
width: _,
|
||||
height: _,
|
||||
} => {
|
||||
user_interface = application.view();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
application.destroy();
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
pub mod text_table;
|
||||
pub use text_table::{TextColumn, TextColumnConstraint, TextTable};
|
||||
|
||||
pub mod shortcut;
|
||||
pub use shortcut::Shortcut;
|
||||
|
||||
use tui::{backend::Backend, layout::Rect, Frame};
|
||||
|
||||
use super::{Event, Status};
|
||||
|
||||
pub type ShouldRender = bool;
|
||||
|
||||
/// A is an element that displays information and can be interacted with.
|
||||
#[allow(unused_variables)]
|
||||
pub trait Component {
|
||||
/// How to inform a component after some event takes place. Typically some enum.
|
||||
type Message: 'static;
|
||||
|
||||
/// Information passed to the component from its parent.
|
||||
type Properties;
|
||||
|
||||
/// Handles an [`Event`]. Defaults to just ignoring the event.
|
||||
fn on_event(
|
||||
&mut self, bounds: Rect, event: Event, messages: &mut Vec<Self::Message>,
|
||||
) -> Status {
|
||||
Status::Ignored
|
||||
}
|
||||
|
||||
/// How the component should handle a [`Self::Message`]. Defaults to doing nothing.
|
||||
fn update(&mut self, message: Self::Message) -> ShouldRender {
|
||||
false
|
||||
}
|
||||
|
||||
/// How the component should handle an update to its properties. Defaults to doing nothing.
|
||||
fn change(&mut self, props: Self::Properties) -> ShouldRender {
|
||||
false
|
||||
}
|
||||
|
||||
/// Draws the component.
|
||||
fn draw<B: Backend>(&mut self, bounds: Rect, frame: &mut Frame<'_, B>);
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
use super::Component;
|
||||
|
||||
/// A [`Component`] to handle keyboard shortcuts and assign actions to them.
|
||||
///
|
||||
/// Inspired by [Flutter's approach](https://docs.flutter.dev/development/ui/advanced/actions_and_shortcuts).
|
||||
pub struct Shortcut<Msg: 'static> {
|
||||
_p: std::marker::PhantomData<Msg>,
|
||||
}
|
||||
|
||||
impl<Msg> Component for Shortcut<Msg> {
|
||||
type Message = Msg;
|
||||
|
||||
type Properties = ();
|
||||
|
||||
fn on_event(
|
||||
&mut self, bounds: tui::layout::Rect, event: crate::tuine::Event,
|
||||
messages: &mut Vec<Self::Message>,
|
||||
) -> crate::tuine::Status {
|
||||
crate::tuine::Status::Ignored
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Self::Message) -> super::ShouldRender {
|
||||
false
|
||||
}
|
||||
|
||||
fn change(&mut self, props: Self::Properties) -> super::ShouldRender {
|
||||
false
|
||||
}
|
||||
|
||||
fn draw<B: tui::backend::Backend>(
|
||||
&mut self, bounds: tui::layout::Rect, frame: &mut tui::Frame<'_, B>,
|
||||
) {
|
||||
todo!()
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
mod tui_stuff;
|
||||
|
||||
pub mod component;
|
||||
pub use component::*;
|
||||
|
||||
pub mod event;
|
||||
pub use event::*;
|
@ -3,3 +3,4 @@ pub enum DataUnit {
|
||||
Byte,
|
||||
Bit,
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user