diff --git a/src/app.rs b/src/app.rs index 40577e4e..2d150ea6 100644 --- a/src/app.rs +++ b/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), +#[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), + OpenHelp, + KillProcess { to_kill: Vec }, + ToggleFreeze, + Clean, + Stop, +} + pub struct AppState { - pub dd_err: Option, - - to_delete_process_list: Option<(String, Vec)>, - 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, + pub widget_lookup_map: FxHashMap, pub layout_tree: Arena, pub layout_tree_root: NodeId, - frozen_state: FrozenState, - pub help_dialog: DialogState, + frozen_state: FrozenState, + current_screen: CurrentScreen, + painter: Painter, + terminator: Arc, } 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 { 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> { + 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> { + 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, + ) { + 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); - // 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)> { - // self.to_delete_process_list.clone() - todo!() - } } diff --git a/src/app/frozen_state.rs b/src/app/frozen_state.rs new file mode 100644 index 00000000..4fc7293d --- /dev/null +++ b/src/app/frozen_state.rs @@ -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), +} + +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) { + *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 + } + } +} diff --git a/src/app/layout_manager.rs b/src/app/layout_manager.rs index b5d54480..f589ea14 100644 --- a/src/app/layout_manager.rs +++ b/src/app/layout_manager.rs @@ -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, pub root: NodeId, - pub widget_lookup_map: FxHashMap, + pub widget_lookup_map: FxHashMap, 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 { fn add_widget_to_map( - widget_lookup_map: &mut FxHashMap, widget_type: BottomWidgetType, + widget_lookup_map: &mut FxHashMap, 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, current_widget_id: NodeId, + widget_lookup_map: &mut FxHashMap, 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, - widget_lookup_map: &mut FxHashMap, current_widget_id: NodeId, - direction: MovementDirection, + layout_tree: &mut Arena, widget_lookup_map: &mut FxHashMap, + 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, area: Rect, - lookup_map: &FxHashMap, + lookup_map: &FxHashMap, ) { // 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, - lookup_map: &FxHashMap, mut constraints: LayoutConstraints, + node: NodeId, arena: &mut Arena, lookup_map: &FxHashMap, + mut constraints: LayoutConstraints, ) -> Size { if let Some(layout_node) = arena.get(node).map(|n| n.get()) { match layout_node { diff --git a/src/app/widgets.rs b/src/app/widgets.rs index dd76f411..a6e09781 100644 --- a/src/app/widgets.rs +++ b/src/app/widgets.rs @@ -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 { - Hidden, - Shown(D), -} - -impl Default for DialogState -where - D: Default + Component, -{ - fn default() -> Self { - DialogState::Hidden - } -} - -impl DialogState -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)] diff --git a/src/bin/main.rs b/src/bin/main.rs index c251b388..f39ef474 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -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; diff --git a/src/canvas.rs b/src/canvas.rs index 1cdfff03..19e045fb 100644 --- a/src/canvas.rs +++ b/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, 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( node: NodeId, arena: &Arena, f: &mut Frame<'_, B>, - lookup_map: &mut FxHashMap, painter: &Painter, + lookup_map: &mut FxHashMap, 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) = diff --git a/src/canvas/dialogs.rs b/src/canvas/dialogs.rs deleted file mode 100644 index af15e931..00000000 --- a/src/canvas/dialogs.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod dd_dialog; -pub use dd_dialog::KillDialog; diff --git a/src/canvas/dialogs/dd_dialog.rs b/src/canvas/dialogs/dd_dialog.rs deleted file mode 100644 index 96612197..00000000 --- a/src/canvas/dialogs/dd_dialog.rs +++ /dev/null @@ -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>; - - fn draw_dd_confirm_buttons( - &self, f: &mut Frame<'_, B>, button_draw_loc: &Rect, app_state: &mut AppState, - ); - - fn draw_dd_dialog( - &self, f: &mut Frame<'_, B>, dd_text: Option>, app_state: &mut AppState, - draw_loc: Rect, - ) -> bool; -} - -impl KillDialog for Painter { - fn get_dd_spans(&self, app_state: &AppState) -> Option> { - 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( - &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::>>(); - 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::>(); - - 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( - &self, f: &mut Frame<'_, B>, dd_text: Option>, 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 - } -} diff --git a/src/lib.rs b/src/lib.rs index f9ebc62e..d0047f47 100644 --- a/src/lib.rs +++ b/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), - 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) -> error::Result>, - 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>, ) -> error::Result<()> { @@ -182,7 +162,8 @@ pub fn panic_hook(panic_info: &PanicInfo<'_>) { } pub fn create_input_thread( - sender: std::sync::mpsc::Sender, termination_ctrl_lock: Arc>, + sender: std::sync::mpsc::Sender>, + termination_ctrl_lock: Arc>, ) -> 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, + sender: std::sync::mpsc::Sender>, control_receiver: std::sync::mpsc::Receiver, termination_ctrl_lock: Arc>, termination_ctrl_cvar: Arc, 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; diff --git a/src/options.rs b/src/options.rs index c5cc8ad5..57ff4aae 100644 --- a/src/options.rs +++ b/src/options.rs @@ -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( diff --git a/src/tuice/application.rs b/src/tuice/application.rs new file mode 100644 index 00000000..b32ff3f6 --- /dev/null +++ b/src/tuice/application.rs @@ -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; + +#[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>; + + /// 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) {} +} + +/// Launches some application with tuice. +pub fn launch_with_application( + application: A, receiver: Receiver>, +) { + runtime::launch(application, receiver); +} diff --git a/src/tuice/component.rs b/src/tuice/component.rs new file mode 100644 index 00000000..4667b047 --- /dev/null +++ b/src/tuice/component.rs @@ -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 +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) -> 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>); +} diff --git a/src/tuice/component/base/block.rs b/src/tuice/component/base/block.rs new file mode 100644 index 00000000..015d6342 --- /dev/null +++ b/src/tuice/component/base/block.rs @@ -0,0 +1,18 @@ +use tui::{backend::Backend, layout::Rect, Frame}; + +use crate::tuice::{Component, Event, Status}; + +pub struct Block {} + +impl Component 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) -> Status { + Status::Ignored + } +} diff --git a/src/tuice/component/base/carousel.rs b/src/tuice/component/base/carousel.rs new file mode 100644 index 00000000..ea3ac6b2 --- /dev/null +++ b/src/tuice/component/base/carousel.rs @@ -0,0 +1,18 @@ +use tui::{backend::Backend, layout::Rect, Frame}; + +use crate::tuice::{Component, Event, Status}; + +pub struct Carousel {} + +impl Component 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) -> Status { + Status::Ignored + } +} diff --git a/src/tuice/component/base/column.rs b/src/tuice/component/base/column.rs new file mode 100644 index 00000000..8c8952dd --- /dev/null +++ b/src/tuice/component/base/column.rs @@ -0,0 +1,18 @@ +use tui::{backend::Backend, layout::Rect, Frame}; + +use crate::tuice::{Component, Event, Status}; + +pub struct Column {} + +impl Component 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) -> Status { + Status::Ignored + } +} diff --git a/src/tuice/component/base/mod.rs b/src/tuice/component/base/mod.rs new file mode 100644 index 00000000..95fbcc7b --- /dev/null +++ b/src/tuice/component/base/mod.rs @@ -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; diff --git a/src/tuice/component/base/row.rs b/src/tuice/component/base/row.rs new file mode 100644 index 00000000..51ab91ea --- /dev/null +++ b/src/tuice/component/base/row.rs @@ -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 + '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(children: Vec) -> Self + where + C: Into + 'a>>, + { + Self { + children: children.into_iter().map(Into::into).collect(), + } + } +} + +impl<'a, Message, B> Component 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) -> Status { + Status::Ignored + } +} diff --git a/src/tuice/component/base/shortcut.rs b/src/tuice/component/base/shortcut.rs new file mode 100644 index 00000000..1f7100e6 --- /dev/null +++ b/src/tuice/component/base/shortcut.rs @@ -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 Component 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) -> Status { + Status::Ignored + } +} diff --git a/src/tuine/component/text_table.rs b/src/tuice/component/base/text_table.rs similarity index 69% rename from src/tuine/component/text_table.rs rename to src/tuice/component/base/text_table.rs index bdfb104b..d2f84249 100644 --- a/src/tuine/component/text_table.rs +++ b/src/tuice/component/base/text_table.rs @@ -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, columns: Vec, show_gap: bool, show_selected_entry: bool, - data: Vec>, + rows: Vec>, style_sheet: StyleSheet, sortable: bool, table_gap: u16, + on_select: Option Message>>, + on_selected_click: Option Message>>, } -impl<'a> TextTable<'a> { +impl<'a, Message> TextTable<'a, Message> { pub fn new>>(columns: Vec) -> 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>) -> 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 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 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> for Box + '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, - ) -> Status { - use crate::tuine::MouseBoundIntersect; +impl<'a, Message, B> Component for TextTable<'a, Message> +where + B: Backend, +{ + fn on_event(&mut self, bounds: Rect, event: Event, messages: &mut Vec) -> 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(&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... diff --git a/src/tuine/component/text_table/table_column.rs b/src/tuice/component/base/text_table/table_column.rs similarity index 100% rename from src/tuine/component/text_table/table_column.rs rename to src/tuice/component/base/text_table/table_column.rs diff --git a/src/tuine/component/text_table/table_scroll_state.rs b/src/tuice/component/base/text_table/table_scroll_state.rs similarity index 99% rename from src/tuine/component/text_table/table_scroll_state.rs rename to src/tuice/component/base/text_table/table_scroll_state.rs index 695b7853..729859b1 100644 --- a/src/tuine/component/text_table/table_scroll_state.rs +++ b/src/tuice/component/base/text_table/table_scroll_state.rs @@ -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; diff --git a/src/tuine/event.rs b/src/tuice/event.rs similarity index 96% rename from src/tuine/event.rs rename to src/tuice/event.rs index a73ada6d..c76aa94d 100644 --- a/src/tuine/event.rs +++ b/src/tuice/event.rs @@ -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), diff --git a/src/tuice/layout/length.rs b/src/tuice/layout/length.rs new file mode 100644 index 00000000..e3fea02e --- /dev/null +++ b/src/tuice/layout/length.rs @@ -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), +} diff --git a/src/tuice/layout/mod.rs b/src/tuice/layout/mod.rs new file mode 100644 index 00000000..694d91b5 --- /dev/null +++ b/src/tuice/layout/mod.rs @@ -0,0 +1,2 @@ +pub mod length; +pub use length::Length; diff --git a/src/tuice/mod.rs b/src/tuice/mod.rs new file mode 100644 index 00000000..e4d142c0 --- /dev/null +++ b/src/tuice/mod.rs @@ -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::*; diff --git a/src/tuice/runtime.rs b/src/tuice/runtime.rs new file mode 100644 index 00000000..dddb1722 --- /dev/null +++ b/src/tuice/runtime.rs @@ -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 { + UserInterface(Event), + Resize { width: u16, height: u16 }, + Custom(Message), +} + +pub(crate) fn launch( + mut application: A, receiver: Receiver>, +) { + 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(); +} diff --git a/src/tuine/tui_stuff/block_builder.rs b/src/tuice/tui_rs/block_builder.rs similarity index 100% rename from src/tuine/tui_stuff/block_builder.rs rename to src/tuice/tui_rs/block_builder.rs diff --git a/src/tuine/tui_stuff/custom_legend_chart.rs b/src/tuice/tui_rs/custom_legend_chart.rs similarity index 100% rename from src/tuine/tui_stuff/custom_legend_chart.rs rename to src/tuice/tui_rs/custom_legend_chart.rs diff --git a/src/tuine/tui_stuff.rs b/src/tuice/tui_rs/mod.rs similarity index 100% rename from src/tuine/tui_stuff.rs rename to src/tuice/tui_rs/mod.rs diff --git a/src/tuine/tui_stuff/pipe_gauge.rs b/src/tuice/tui_rs/pipe_gauge.rs similarity index 100% rename from src/tuine/tui_stuff/pipe_gauge.rs rename to src/tuice/tui_rs/pipe_gauge.rs diff --git a/src/tuine/component.rs b/src/tuine/component.rs deleted file mode 100644 index 31a9899f..00000000 --- a/src/tuine/component.rs +++ /dev/null @@ -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, - ) -> 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(&mut self, bounds: Rect, frame: &mut Frame<'_, B>); -} diff --git a/src/tuine/component/block.rs b/src/tuine/component/block.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/src/tuine/component/column.rs b/src/tuine/component/column.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/src/tuine/component/row.rs b/src/tuine/component/row.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/src/tuine/component/shortcut.rs b/src/tuine/component/shortcut.rs deleted file mode 100644 index c2204cc9..00000000 --- a/src/tuine/component/shortcut.rs +++ /dev/null @@ -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 { - _p: std::marker::PhantomData, -} - -impl Component for Shortcut { - type Message = Msg; - - type Properties = (); - - fn on_event( - &mut self, bounds: tui::layout::Rect, event: crate::tuine::Event, - messages: &mut Vec, - ) -> 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( - &mut self, bounds: tui::layout::Rect, frame: &mut tui::Frame<'_, B>, - ) { - todo!() - } -} diff --git a/src/tuine/mod.rs b/src/tuine/mod.rs deleted file mode 100644 index 2ceba889..00000000 --- a/src/tuine/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod tui_stuff; - -pub mod component; -pub use component::*; - -pub mod event; -pub use event::*; diff --git a/src/units/data_units.rs b/src/units/data_units.rs index d0b7c1e1..ce8f4867 100644 --- a/src/units/data_units.rs +++ b/src/units/data_units.rs @@ -3,3 +3,4 @@ pub enum DataUnit { Byte, Bit, } +