From e7b9c729123eda386f12ce90faaa413bf516fed3 Mon Sep 17 00:00:00 2001 From: ClementTsang Date: Wed, 8 Sep 2021 23:12:49 -0400 Subject: refactor: add general keybinds, fix buggy movement Adds back some of the general program keybinds, and fixes both a bug causing widget movement via keybinds to be incorrect, and not correcting the last selected widget in the layout tree rows/cols after clicking/setting the default widget! --- docs/content/usage/general-usage.md | 4 +- src/app.rs | 187 ++++++++++++++++++++-------- src/app/data_farmer.rs | 37 +++--- src/app/event.rs | 2 +- src/app/layout_manager.rs | 133 +++++++++++++------- src/app/widgets.rs | 3 + src/app/widgets/bottom_widgets/basic_mem.rs | 4 +- src/app/widgets/bottom_widgets/basic_net.rs | 1 - src/app/widgets/bottom_widgets/cpu.rs | 150 ++++++++++++++-------- src/app/widgets/bottom_widgets/mem.rs | 4 +- src/app/widgets/bottom_widgets/net.rs | 2 - src/app/widgets/bottom_widgets/process.rs | 83 +++++++++++- src/bin/main.rs | 5 +- src/canvas.rs | 2 +- src/constants.rs | 4 +- src/data_conversion.rs | 80 ++++-------- src/lib.rs | 56 ++++----- 17 files changed, 490 insertions(+), 267 deletions(-) diff --git a/docs/content/usage/general-usage.md b/docs/content/usage/general-usage.md index 4bfcdb61..eb457aef 100644 --- a/docs/content/usage/general-usage.md +++ b/docs/content/usage/general-usage.md @@ -41,8 +41,8 @@ Note that key bindings are generally case-sensitive. | ------------------------------------------------------------ | ------------------------------------------------------------ | | ++q++ , ++ctrl+c++ | Quit | | ++esc++ | Close dialog windows, search, widgets, or exit expanded mode | -| ++ctrl+r++ | Reset display and any collected data | -| ++f++ | Freeze/unfreeze updating with new data | +| ++ctrl+r++ | Resets any collected data | +| ++f++ | Toggles freezing, which stops new data from being shown | | ++question++ | Open help menu | | ++e++ | Toggle expanding the currently selected widget | | ++ctrl+up++
++shift+up++
++K++
++W++ | Select the widget above | diff --git a/src/app.rs b/src/app.rs index 7e2451b6..b3409d3f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -104,19 +104,29 @@ pub struct AppConfigFields { pub no_write: bool, pub show_table_scroll_position: bool, pub is_advanced_kill: bool, - // TODO: Remove these, move network details state-side. pub network_unit_type: DataUnit, pub network_scale_type: AxisScaling, 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(DataCollection), +} + +impl Default for FrozenState { + fn default() -> Self { + Self::NotFrozen + } +} + pub struct AppState { pub dd_err: Option, to_delete_process_list: Option<(String, Vec)>, - pub is_frozen: bool, - pub canvas_data: canvas::DisplayableData, pub data_collection: DataCollection, @@ -161,6 +171,7 @@ pub struct AppState { pub widget_lookup_map: FxHashMap, pub layout_tree: Arena, pub layout_tree_root: NodeId, + frozen_state: FrozenState, } impl AppState { @@ -189,7 +200,6 @@ impl AppState { // Use defaults. dd_err: Default::default(), to_delete_process_list: Default::default(), - is_frozen: Default::default(), canvas_data: Default::default(), data_collection: Default::default(), is_expanded: Default::default(), @@ -211,37 +221,30 @@ impl AppState { basic_mode_use_percent: Default::default(), is_force_redraw: Default::default(), is_determining_widget_boundary: Default::default(), + frozen_state: Default::default(), } } - pub fn reset(&mut self) { - // Reset multi - self.reset_multi_tap_keys(); + pub fn is_frozen(&self) -> bool { + matches!(self.frozen_state, FrozenState::Frozen(_)) + } - // Reset dialog state - self.help_dialog_state.is_showing_help = false; - self.delete_dialog_state.is_showing_dd = false; + pub fn toggle_freeze(&mut self) { + if matches!(self.frozen_state, FrozenState::Frozen(_)) { + self.frozen_state = FrozenState::NotFrozen; + } else { + self.frozen_state = FrozenState::Frozen(self.data_collection.clone()); + } + } - // Close all searches and reset it - self.proc_state - .widget_states + pub fn reset(&mut self) { + // Call reset on all widgets. + self.widget_lookup_map .values_mut() - .for_each(|state| { - state.process_search_state.search_state.reset(); - }); - self.proc_state.force_update_all = true; - - // Clear current delete list - self.to_delete_process_list = None; - self.dd_err = None; + .for_each(|widget| widget.reset()); // Unfreeze. - self.is_frozen = false; - - // Reset zoom - self.reset_cpu_zoom(); - self.reset_mem_zoom(); - self.reset_net_zoom(); + self.frozen_state = FrozenState::NotFrozen; // Reset data self.data_collection.reset(); @@ -259,10 +262,77 @@ impl AppState { 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.app_config_fields.use_basic_mode { + EventResult::NoRedraw + } else { + self.is_expanded = !self.is_expanded; + EventResult::Redraw + } + } + '?' => { + self.help_dialog_state.is_showing_help = true; + EventResult::Redraw + } + 'f' => { + 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 { + let layout_tree = &mut self.layout_tree; + let previous_selected = self.selected_widget; + if let Some(widget) = self.widget_lookup_map.get_mut(&self.selected_widget) { + match move_widget_selection(layout_tree, widget, self.selected_widget, direction) { + MoveWidgetResult::ForceRedraw(new_widget_id) => { + self.selected_widget = new_widget_id; + EventResult::Redraw + } + MoveWidgetResult::NodeId(new_widget_id) => { + self.selected_widget = new_widget_id; + + if previous_selected != self.selected_widget { + EventResult::Redraw + } else { + EventResult::NoRedraw + } + } + } + } else { + EventResult::NoRedraw + } + } + /// Handles a global [`KeyEvent`], and returns an [`EventResult`]. fn handle_global_shortcut(&mut self, event: KeyEvent) -> EventResult { - // TODO: Write this. - if event.modifiers.is_empty() { match event.code { KeyCode::Esc => { @@ -280,24 +350,33 @@ impl AppState { EventResult::NoRedraw } } - KeyCode::Char('q') => EventResult::Quit, - KeyCode::Char('e') => { - if self.app_config_fields.use_basic_mode { - EventResult::NoRedraw - } else { - self.is_expanded = !self.is_expanded; - EventResult::Redraw - } - } - KeyCode::Char('?') => { - self.help_dialog_state.is_showing_help = true; + KeyCode::Char(c) => self.handle_global_char(c), + _ => 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::CONTROL = event.modifiers { + } else if let KeyModifiers::SHIFT = event.modifiers { match event.code { - KeyCode::Char('c') => EventResult::Quit, + 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 { @@ -317,7 +396,14 @@ impl AppState { } ReturnSignal::Update => { if let Some(widget) = self.widget_lookup_map.get_mut(&self.selected_widget) { - widget.update_data(&self.data_collection); + match &self.frozen_state { + FrozenState::NotFrozen => { + widget.update_data(&self.data_collection); + } + FrozenState::Frozen(frozen_data) => { + widget.update_data(frozen_data); + } + } } EventResult::Redraw } @@ -379,7 +465,11 @@ impl AppState { if was_id_already_selected { return self.convert_widget_event_result(result); } else { - // If the weren't equal, *force* a redraw. + // 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); return EventResult::Redraw; } @@ -402,7 +492,7 @@ impl AppState { BottomEvent::Update(new_data) => { self.data_collection.eat_data(new_data); - if !self.is_frozen { + if !self.is_frozen() { let data_collection = &self.data_collection; self.widget_lookup_map .iter_mut() @@ -1599,12 +1689,7 @@ impl AppState { 'G' => self.skip_to_last(), 'k' => self.on_up_key(), 'j' => self.on_down_key(), - 'f' => { - self.is_frozen = !self.is_frozen; - if self.is_frozen { - self.data_collection.set_frozen_time(); - } - } + 'f' => {} 'C' => { // self.open_config(), } diff --git a/src/app/data_farmer.rs b/src/app/data_farmer.rs index 25d265ab..c2747289 100644 --- a/src/app/data_farmer.rs +++ b/src/app/data_farmer.rs @@ -26,7 +26,7 @@ use regex::Regex; pub type TimeOffset = f64; pub type Value = f64; -#[derive(Debug, Default)] +#[derive(Clone, Debug, Default)] pub struct TimedData { pub rx_data: Value, pub tx_data: Value, @@ -41,14 +41,14 @@ pub struct TimedData { /// collected, and what is needed to convert into a displayable form. /// /// If the app is *frozen* - that is, we do not want to *display* any changing -/// data, keep updating this, don't convert to canvas displayable data! +/// data, keep updating this. As of 2021-09-08, we just clone the current collection +/// when it freezes to have a snapshot floating around. /// /// Note that with this method, the *app* thread is responsible for cleaning - /// not the data collector. -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct DataCollection { pub current_instant: Instant, - pub frozen_instant: Option, pub timed_data_vec: Vec<(Instant, TimedData)>, pub network_harvest: network::NetworkHarvest, pub memory_harvest: memory::MemHarvest, @@ -70,7 +70,6 @@ impl Default for DataCollection { fn default() -> Self { DataCollection { current_instant: Instant::now(), - frozen_instant: None, timed_data_vec: Vec::default(), network_harvest: network::NetworkHarvest::default(), memory_harvest: memory::MemHarvest::default(), @@ -92,21 +91,19 @@ impl Default for DataCollection { impl DataCollection { pub fn reset(&mut self) { - self.timed_data_vec = Vec::default(); - self.network_harvest = network::NetworkHarvest::default(); - self.memory_harvest = memory::MemHarvest::default(); - self.swap_harvest = memory::MemHarvest::default(); - self.cpu_harvest = cpu::CpuHarvest::default(); - self.process_harvest = Vec::default(); - self.disk_harvest = Vec::default(); - self.io_harvest = disks::IoHarvest::default(); - self.io_labels_and_prev = Vec::default(); - self.temp_harvest = Vec::default(); - self.battery_harvest = Vec::default(); - } - - pub fn set_frozen_time(&mut self) { - self.frozen_instant = Some(self.current_instant); + self.timed_data_vec = Default::default(); + self.network_harvest = Default::default(); + self.memory_harvest = Default::default(); + self.swap_harvest = Default::default(); + self.cpu_harvest = Default::default(); + self.process_harvest = Default::default(); + self.process_name_pid_map = Default::default(); + self.process_cmd_pid_map = Default::default(); + self.disk_harvest = Default::default(); + self.io_harvest = Default::default(); + self.io_labels_and_prev = Default::default(); + self.temp_harvest = Default::default(); + self.battery_harvest = Default::default(); } pub fn clean_data(&mut self, max_time_millis: u64) { diff --git a/src/app/event.rs b/src/app/event.rs index a6afb2d8..9201e2c0 100644 --- a/src/app/event.rs +++ b/src/app/event.rs @@ -45,7 +45,7 @@ pub enum WidgetEventResult { /// How a widget should handle a widget selection request. pub enum SelectionAction { - /// This event occurs if the widget internally handled the selection action. + /// This event occurs if the widget internally handled the selection action. A redraw is required. Handled, /// This event occurs if the widget did not handle the selection action; the caller must handle it. NotHandled, diff --git a/src/app/layout_manager.rs b/src/app/layout_manager.rs index 9b88fb8a..61d17924 100644 --- a/src/app/layout_manager.rs +++ b/src/app/layout_manager.rs @@ -997,9 +997,9 @@ Supported widget names: // --- New stuff --- /// Represents a row in the layout tree. -#[derive(PartialEq, Eq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone)] pub struct RowLayout { - last_selected_index: usize, + last_selected: Option, pub parent_rule: LayoutRule, pub bound: Rect, } @@ -1007,7 +1007,7 @@ pub struct RowLayout { impl RowLayout { fn new(parent_rule: LayoutRule) -> Self { Self { - last_selected_index: 0, + last_selected: None, parent_rule, bound: Rect::default(), } @@ -1015,9 +1015,9 @@ impl RowLayout { } /// Represents a column in the layout tree. -#[derive(PartialEq, Eq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone)] pub struct ColLayout { - last_selected_index: usize, + last_selected: Option, pub parent_rule: LayoutRule, pub bound: Rect, } @@ -1025,7 +1025,7 @@ pub struct ColLayout { impl ColLayout { fn new(parent_rule: LayoutRule) -> Self { Self { - last_selected_index: 0, + last_selected: None, parent_rule, bound: Rect::default(), } @@ -1033,7 +1033,7 @@ impl ColLayout { } /// Represents a widget in the layout tree. -#[derive(PartialEq, Eq, Clone, Default)] +#[derive(Debug, PartialEq, Eq, Clone, Default)] pub struct WidgetLayout { pub bound: Rect, } @@ -1042,7 +1042,7 @@ pub struct WidgetLayout { /// - [`LayoutNode::Row`] (a non-leaf that distributes its children horizontally) /// - [`LayoutNode::Col`] (a non-leaf node that distributes its children vertically) /// - [`LayoutNode::Widget`] (a leaf node that contains the ID of the widget it is associated with) -#[derive(PartialEq, Eq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone)] pub enum LayoutNode { /// A non-leaf that distributes its children horizontally Row(RowLayout), @@ -1382,6 +1382,8 @@ pub fn create_layout_tree( )); } + correct_layout_last_selections(&mut arena, selected); + Ok(LayoutCreationOutput { layout_tree: arena, root: root_id, @@ -1391,6 +1393,35 @@ pub fn create_layout_tree( }) } +/// We may have situations where we also have to make sure the correct layout indices are selected. +/// For example, when we select a widget by clicking, we want to update the layout so that it's as if a user +/// manually moved to it via keybinds. +/// +/// We can do this by just going through the ancestors, starting from the widget itself. +pub fn correct_layout_last_selections(arena: &mut Arena, selected: NodeId) { + let mut selected_ancestors = selected.ancestors(&arena).collect::>(); + let prev_node = selected_ancestors.pop(); + if let Some(mut prev_node) = prev_node { + for node in selected_ancestors { + if let Some(layout_node) = arena.get_mut(node).map(|n| n.get_mut()) { + match layout_node { + LayoutNode::Row(RowLayout { last_selected, .. }) + | LayoutNode::Col(ColLayout { last_selected, .. }) => { + *last_selected = Some(prev_node); + } + LayoutNode::Widget(_) => {} + } + } + prev_node = node; + } + } +} + +pub enum MoveWidgetResult { + ForceRedraw(NodeId), + NodeId(NodeId), +} + /// Attempts to find and return the selected [`BottomWidgetId`] after moving in a direction. /// /// Note this function assumes a properly built tree - if not, bad things may happen! We generally assume that: @@ -1399,7 +1430,7 @@ pub fn create_layout_tree( pub fn move_widget_selection( layout_tree: &mut Arena, current_widget: &mut TmpBottomWidget, current_widget_id: NodeId, direction: MovementDirection, -) -> NodeId { +) -> MoveWidgetResult { // We first give our currently-selected widget a chance to react to the movement - it may handle it internally! let handled = match direction { MovementDirection::Left => current_widget.handle_widget_selection_left(), @@ -1408,16 +1439,18 @@ pub fn move_widget_selection( MovementDirection::Down => current_widget.handle_widget_selection_down(), }; + // TODO: Do testing. + match handled { SelectionAction::Handled => { // If it was handled by the widget, then we don't have to do anything - return the current one. - current_widget_id + MoveWidgetResult::ForceRedraw(current_widget_id) } SelectionAction::NotHandled => { /// Keeps traversing up the `layout_tree` until it hits a parent where `current_id` is a child and parent /// is a [`LayoutNode::Row`], returning its parent's [`NodeId`] and the child's [`NodeId`] (in that order). /// If this crawl fails (i.e. hits a root, it is an invalid tree for some reason), it returns [`None`]. - fn find_first_row( + fn find_parent_row( layout_tree: &Arena, current_id: NodeId, ) -> Option<(NodeId, NodeId)> { layout_tree @@ -1430,7 +1463,7 @@ pub fn move_widget_selection( }) .and_then(|(parent_id, parent_node)| match parent_node.get() { LayoutNode::Row(_) => Some((parent_id, current_id)), - LayoutNode::Col(_) => find_first_row(layout_tree, parent_id), + LayoutNode::Col(_) => find_parent_row(layout_tree, parent_id), LayoutNode::Widget(_) => None, }) } @@ -1438,7 +1471,7 @@ pub fn move_widget_selection( /// Keeps traversing up the `layout_tree` until it hits a parent where `current_id` is a child and parent /// is a [`LayoutNode::Col`], returning its parent's [`NodeId`] and the child's [`NodeId`] (in that order). /// If this crawl fails (i.e. hits a root, it is an invalid tree for some reason), it returns [`None`]. - fn find_first_col( + fn find_parent_col( layout_tree: &Arena, current_id: NodeId, ) -> Option<(NodeId, NodeId)> { layout_tree @@ -1450,7 +1483,7 @@ pub fn move_widget_selection( .map(|parent_node| (parent_id, parent_node)) }) .and_then(|(parent_id, parent_node)| match parent_node.get() { - LayoutNode::Row(_) => find_first_col(layout_tree, parent_id), + LayoutNode::Row(_) => find_parent_col(layout_tree, parent_id), LayoutNode::Col(_) => Some((parent_id, current_id)), LayoutNode::Widget(_) => None, }) @@ -1461,21 +1494,19 @@ pub fn move_widget_selection( if let Some(current_node) = layout_tree.get(current_id) { match current_node.get() { LayoutNode::Row(RowLayout { - last_selected_index, + last_selected, parent_rule: _, bound: _, }) | LayoutNode::Col(ColLayout { - last_selected_index, + last_selected, parent_rule: _, bound: _, }) => { - if let Some(next_child) = - current_id.children(layout_tree).nth(*last_selected_index) - { + if let Some(next_child) = *last_selected { descend_to_leaf(layout_tree, next_child) } else { - current_id + current_node.first_child().unwrap_or(current_id) } } LayoutNode::Widget(_) => { @@ -1493,7 +1524,7 @@ pub fn move_widget_selection( // on the tree layout to help us decide where to go. // Movement logic is inspired by i3. When we enter a new column/row, we go to the *last* selected // element; if we can't, go to the nearest one. - match direction { + let proposed_id = match direction { MovementDirection::Left => { // When we move "left": // 1. Look for the parent of the current widget. @@ -1513,7 +1544,8 @@ pub fn move_widget_selection( fn find_left( layout_tree: &mut Arena, current_id: NodeId, ) -> NodeId { - if let Some((parent_id, child_id)) = find_first_row(layout_tree, current_id) + if let Some((parent_id, child_id)) = + find_parent_row(layout_tree, current_id) { if let Some(prev_sibling) = child_id.preceding_siblings(layout_tree).nth(1) @@ -1521,16 +1553,17 @@ pub fn move_widget_selection( // Subtract one from the currently selected index... if let Some(parent) = layout_tree.get_mut(parent_id) { if let LayoutNode::Row(row) = parent.get_mut() { - row.last_selected_index = - row.last_selected_index.saturating_sub(1); + row.last_selected = Some(prev_sibling); } } // Now descend downwards! descend_to_leaf(layout_tree, prev_sibling) - } else { + } else if parent_id != current_id { // Darn, we can't go further back! Recurse on this ID. - find_left(layout_tree, child_id) + find_left(layout_tree, parent_id) + } else { + current_id } } else { // Failed, just return the current ID. @@ -1546,23 +1579,26 @@ pub fn move_widget_selection( fn find_right( layout_tree: &mut Arena, current_id: NodeId, ) -> NodeId { - if let Some((parent_id, child_id)) = find_first_row(layout_tree, current_id) + if let Some((parent_id, child_id)) = + find_parent_row(layout_tree, current_id) { - if let Some(prev_sibling) = + if let Some(following_sibling) = child_id.following_siblings(layout_tree).nth(1) { // Add one to the currently selected index... if let Some(parent) = layout_tree.get_mut(parent_id) { if let LayoutNode::Row(row) = parent.get_mut() { - row.last_selected_index += 1; + row.last_selected = Some(following_sibling); } } // Now descend downwards! - descend_to_leaf(layout_tree, prev_sibling) - } else { + descend_to_leaf(layout_tree, following_sibling) + } else if parent_id != current_id { // Darn, we can't go further back! Recurse on this ID. - find_right(layout_tree, child_id) + find_right(layout_tree, parent_id) + } else { + current_id } } else { // Failed, just return the current ID. @@ -1578,7 +1614,8 @@ pub fn move_widget_selection( fn find_above( layout_tree: &mut Arena, current_id: NodeId, ) -> NodeId { - if let Some((parent_id, child_id)) = find_first_col(layout_tree, current_id) + if let Some((parent_id, child_id)) = + find_parent_col(layout_tree, current_id) { if let Some(prev_sibling) = child_id.preceding_siblings(layout_tree).nth(1) @@ -1586,16 +1623,17 @@ pub fn move_widget_selection( // Subtract one from the currently selected index... if let Some(parent) = layout_tree.get_mut(parent_id) { if let LayoutNode::Col(row) = parent.get_mut() { - row.last_selected_index = - row.last_selected_index.saturating_sub(1); + row.last_selected = Some(prev_sibling); } } // Now descend downwards! descend_to_leaf(layout_tree, prev_sibling) - } else { + } else if parent_id != current_id { // Darn, we can't go further back! Recurse on this ID. - find_above(layout_tree, child_id) + find_above(layout_tree, parent_id) + } else { + current_id } } else { // Failed, just return the current ID. @@ -1611,23 +1649,26 @@ pub fn move_widget_selection( fn find_below( layout_tree: &mut Arena, current_id: NodeId, ) -> NodeId { - if let Some((parent_id, child_id)) = find_first_col(layout_tree, current_id) + if let Some((parent_id, child_id)) = + find_parent_col(layout_tree, current_id) { - if let Some(prev_sibling) = + if let Some(following_sibling) = child_id.following_siblings(layout_tree).nth(1) { // Add one to the currently selected index... if let Some(parent) = layout_tree.get_mut(parent_id) { if let LayoutNode::Col(row) = parent.get_mut() { - row.last_selected_index += 1; + row.last_selected = Some(following_sibling); } } // Now descend downwards! - descend_to_leaf(layout_tree, prev_sibling) - } else { + descend_to_leaf(layout_tree, following_sibling) + } else if parent_id != current_id { // Darn, we can't go further back! Recurse on this ID. - find_below(layout_tree, child_id) + find_below(layout_tree, parent_id) + } else { + current_id } } else { // Failed, just return the current ID. @@ -1636,6 +1677,12 @@ pub fn move_widget_selection( } find_below(layout_tree, current_widget_id) } + }; + + if let Some(LayoutNode::Widget(_)) = layout_tree.get(proposed_id).map(|n| n.get()) { + MoveWidgetResult::NodeId(proposed_id) + } else { + MoveWidgetResult::NodeId(current_widget_id) } } } diff --git a/src/app/widgets.rs b/src/app/widgets.rs index cac6a032..ffc08cd6 100644 --- a/src/app/widgets.rs +++ b/src/app/widgets.rs @@ -149,6 +149,9 @@ pub trait Widget { fn selectable_type(&self) -> SelectableType { SelectableType::Selectable } + + /// Resets state in a [`Widget`]; used when a reset signal is given. The default implementation does nothing. + fn reset(&mut self) {} } /// Whether a widget can be selected, not selected, or redirected upon selection. diff --git a/src/app/widgets/bottom_widgets/basic_mem.rs b/src/app/widgets/bottom_widgets/basic_mem.rs index 2f33183f..d70d0e04 100644 --- a/src/app/widgets/bottom_widgets/basic_mem.rs +++ b/src/app/widgets/bottom_widgets/basic_mem.rs @@ -129,7 +129,7 @@ impl Widget for BasicMem { // TODO: [Data update optimization] Probably should just make another function altogether for basic mode. self.mem_data = if let (Some(data), Some((_, fraction))) = ( - convert_mem_data_points(data_collection, false).last(), + convert_mem_data_points(data_collection).last(), memory_labels, ) { ( @@ -141,7 +141,7 @@ impl Widget for BasicMem { (0.0, "0.0B/0.0B".to_string(), "0%".to_string()) }; self.swap_data = if let (Some(data), Some((_, fraction))) = ( - convert_swap_data_points(data_collection, false).last(), + convert_swap_data_points(data_collection).last(), swap_labels, ) { Some(( diff --git a/src/app/widgets/bottom_widgets/basic_net.rs b/src/app/widgets/bottom_widgets/basic_net.rs index bdb15554..a739fd25 100644 --- a/src/app/widgets/bottom_widgets/basic_net.rs +++ b/src/app/widgets/bottom_widgets/basic_net.rs @@ -109,7 +109,6 @@ impl Widget for BasicNet { fn update_data(&mut self, data_collection: &DataCollection) { let network_data = convert_network_data_points( data_collection, - false, // TODO: I think the is_frozen here is also useless; see mem and cpu true, &AxisScaling::Linear, &self.unit_type, diff --git a/src/app/widgets/bottom_widgets/cpu.rs b/src/app/widgets/bottom_widgets/cpu.rs index 0b5cbae2..9397a7e1 100644 --- a/src/app/widgets/bottom_widgets/cpu.rs +++ b/src/app/widgets/bottom_widgets/cpu.rs @@ -9,7 +9,9 @@ use tui::{ use crate::{ app::{ - event::WidgetEventResult, text_table::SimpleColumn, time_graph::TimeGraphData, + event::{SelectionAction, WidgetEventResult}, + text_table::SimpleColumn, + time_graph::TimeGraphData, AppConfigFields, AppScrollWidgetState, CanvasTableWidthState, Component, DataCollection, TextTable, TimeGraph, Widget, }, @@ -47,7 +49,6 @@ impl CpuState { enum CpuGraphSelection { Graph, Legend, - None, } /// Whether the [`CpuGraph`]'s legend is placed on the left or right. @@ -95,7 +96,7 @@ impl CpuGraph { legend_position, showing_avg, bounds: Rect::default(), - selected: CpuGraphSelection::None, + selected: CpuGraphSelection::Graph, display_data: Default::default(), load_avg_data: [0.0; 3], width: LayoutRule::default(), @@ -121,7 +122,6 @@ impl Component for CpuGraph { match self.selected { CpuGraphSelection::Graph => self.graph.handle_key_event(event), CpuGraphSelection::Legend => self.legend.handle_key_event(event), - CpuGraphSelection::None => WidgetEventResult::NoRedraw, } } @@ -190,6 +190,55 @@ impl Widget for CpuGraph { CpuGraphLegendPosition::Right => (split_area[0], split_area[1]), }; + let legend_block = self + .block() + .selected(selected && matches!(&self.selected, CpuGraphSelection::Legend)) + .expanded(expanded) + .hide_title(true); + + let legend_data = self + .display_data + .iter() + .enumerate() + .map(|(cpu_index, core_data)| { + let style = Some(if cpu_index == 0 { + painter.colours.all_colour_style + } else if self.showing_avg && cpu_index == 1 { + painter.colours.avg_colour_style + } else { + let cpu_style_index = if self.showing_avg { + // No underflow should occur, as if cpu_index was + // 1 and avg is showing, it's caught by the above case! + cpu_index - 2 + } else { + cpu_index - 1 + }; + painter.colours.cpu_colour_styles + [cpu_style_index % painter.colours.cpu_colour_styles.len()] + }); + + vec![ + ( + core_data.cpu_name.clone().into(), + Some(core_data.short_cpu_name.clone().into()), + style, + ), + (core_data.legend_value.clone().into(), None, style), + ] + }) + .collect::>(); + + // TODO: You MUST draw the table first, otherwise the index may mismatch after a reset. This is a bad gotcha - we should look into auto-updating the table's length! + self.legend.draw_tui_table( + painter, + f, + &legend_data, + legend_block, + legend_block_area, + true, + false, + ); + const Y_BOUNDS: [f64; 2] = [0.0, 100.5]; let y_bound_labels: [Cow<'static, str>; 2] = ["0%".into(), "100%".into()]; @@ -243,59 +292,10 @@ impl Widget for CpuGraph { graph_block, graph_block_area, ); - - let legend_block = self - .block() - .selected(selected && matches!(&self.selected, CpuGraphSelection::Legend)) - .expanded(expanded) - .hide_title(true); - - let legend_data = self - .display_data - .iter() - .enumerate() - .map(|(cpu_index, core_data)| { - let style = Some(if cpu_index == 0 { - painter.colours.all_colour_style - } else if self.showing_avg && cpu_index == 1 { - painter.colours.avg_colour_style - } else { - let cpu_style_index = if self.showing_avg { - // No underflow should occur, as if cpu_index was - // 1 and avg is showing, it's caught by the above case! - cpu_index - 2 - } else { - cpu_index - 1 - }; - painter.colours.cpu_colour_styles - [cpu_style_index % painter.colours.cpu_colour_styles.len()] - }); - - vec![ - ( - core_data.cpu_name.clone().into(), - Some(core_data.short_cpu_name.clone().into()), - style, - ), - (core_data.legend_value.clone().into(), None, style), - ] - }) - .collect::>(); - - self.legend.draw_tui_table( - painter, - f, - &legend_data, - legend_block, - legend_block_area, - true, - false, - ); } fn update_data(&mut self, data_collection: &DataCollection) { - // TODO: *Maybe* look into only taking in enough data for the current retention? Though this isn't great, it means you have to be like process with the whole updating thing. - convert_cpu_data_points(data_collection, &mut self.display_data, false); // TODO: Again, the "is_frozen" is probably useless + convert_cpu_data_points(data_collection, &mut self.display_data); self.load_avg_data = data_collection.load_avg_harvest; } @@ -306,4 +306,46 @@ impl Widget for CpuGraph { fn height(&self) -> LayoutRule { self.height } + + fn handle_widget_selection_left(&mut self) -> SelectionAction { + match self.legend_position { + CpuGraphLegendPosition::Left => { + if let CpuGraphSelection::Graph = self.selected { + self.selected = CpuGraphSelection::Legend; + SelectionAction::Handled + } else { + SelectionAction::NotHandled + } + } + CpuGraphLegendPosition::Right => { + if let CpuGraphSelection::Legend = self.selected { + self.selected = CpuGraphSelection::Graph; + SelectionAction::Handled + } else { + SelectionAction::NotHandled + } + } + } + } + + fn handle_widget_selection_right(&mut self) -> SelectionAction { + match self.legend_position { + CpuGraphLegendPosition::Left => { + if let CpuGraphSelection::Legend = self.selected { + self.selected = CpuGraphSelection::Graph; + SelectionAction::Handled + } else { + SelectionAction::NotHandled + } + } + CpuGraphLegendPosition::Right => { + if let CpuGraphSelection::Graph = self.selected { + self.selected = CpuGraphSelection::Legend; + SelectionAction::Handled + } else { + SelectionAction::NotHandled + } + } + } + } } diff --git a/src/app/widgets/bottom_widgets/mem.rs b/src/app/widgets/bottom_widgets/mem.rs index 1531290b..11880421 100644 --- a/src/app/widgets/bottom_widgets/mem.rs +++ b/src/app/widgets/bottom_widgets/mem.rs @@ -129,8 +129,8 @@ impl Widget for MemGraph { } fn update_data(&mut self, data_collection: &DataCollection) { - self.mem_data = convert_mem_data_points(data_collection, false); // TODO: I think the "is_frozen" part is useless... it's always false now. - self.swap_data = convert_swap_data_points(data_collection, false); + self.mem_data = convert_mem_data_points(data_collection); + self.swap_data = convert_swap_data_points(data_collection); let (memory_labels, swap_labels) = convert_mem_labels(data_collection); self.mem_labels = memory_labels; diff --git a/src/app/widgets/bottom_widgets/net.rs b/src/app/widgets/bottom_widgets/net.rs index d9c0b799..b02b957f 100644 --- a/src/app/widgets/bottom_widgets/net.rs +++ b/src/app/widgets/bottom_widgets/net.rs @@ -568,7 +568,6 @@ impl Widget for NetGraph { fn update_data(&mut self, data_collection: &DataCollection) { let network_data = convert_network_data_points( data_collection, - false, // TODO: I think the is_frozen here is also useless; see mem and cpu false, &self.scale_type, &self.unit_type, @@ -709,7 +708,6 @@ impl Widget for OldNetGraph { fn update_data(&mut self, data_collection: &DataCollection) { let network_data = convert_network_data_points( data_collection, - false, // TODO: I think the is_frozen here is also useless; see mem and cpu true, &self.net_graph.scale_type, &self.net_graph.unit_type, diff --git a/src/app/widgets/bottom_widgets/process.rs b/src/app/widgets/bottom_widgets/process.rs index 87177b22..eeff0b54 100644 --- a/src/app/widgets/bottom_widgets/process.rs +++ b/src/app/widgets/bottom_widgets/process.rs @@ -15,7 +15,7 @@ use tui::{ use crate::{ app::{ data_harvester::processes::ProcessHarvest, - event::{MultiKey, MultiKeyResult, ReturnSignal, WidgetEventResult}, + event::{MultiKey, MultiKeyResult, ReturnSignal, SelectionAction, WidgetEventResult}, query::*, text_table::DesiredColumnWidth, widgets::tui_stuff::BlockBuilder, @@ -575,6 +575,7 @@ impl ProcState { } /// The currently selected part of a [`ProcessManager`] +#[derive(PartialEq, Eq, Clone, Copy)] enum ProcessManagerSelection { Processes, Sort, @@ -775,6 +776,7 @@ pub struct ProcessManager { dd_multi: MultiKey, selected: ProcessManagerSelection, + prev_selected: ProcessManagerSelection, in_tree_mode: bool, show_sort: bool, @@ -819,6 +821,7 @@ impl ProcessManager { search_block_bounds: Rect::default(), dd_multi: MultiKey::register(vec!['d', 'd']), // TODO: Maybe use something static... selected: ProcessManagerSelection::Processes, + prev_selected: ProcessManagerSelection::Processes, in_tree_mode: false, show_sort: false, show_search: false, @@ -871,6 +874,7 @@ impl ProcessManager { WidgetEventResult::NoRedraw } else { self.show_search = true; + self.prev_selected = self.selected; self.selected = ProcessManagerSelection::Search; WidgetEventResult::Redraw } @@ -883,6 +887,7 @@ impl ProcessManager { self.sort_menu .set_index(self.process_table.current_sorting_column_index()); self.show_sort = true; + self.prev_selected = self.selected; self.selected = ProcessManagerSelection::Sort; WidgetEventResult::Redraw } @@ -966,6 +971,7 @@ impl ProcessManager { fn hide_sort(&mut self) { self.show_sort = false; if let ProcessManagerSelection::Sort = self.selected { + self.prev_selected = self.selected; self.selected = ProcessManagerSelection::Processes; } } @@ -973,6 +979,7 @@ impl ProcessManager { fn hide_search(&mut self) { self.show_search = false; if let ProcessManagerSelection::Search = self.selected { + self.prev_selected = self.selected; self.selected = ProcessManagerSelection::Processes; } } @@ -1129,6 +1136,7 @@ impl Component for ProcessManager { if let ProcessManagerSelection::Processes = self.selected { self.process_table.handle_mouse_event(event) } else { + self.prev_selected = self.selected; self.selected = ProcessManagerSelection::Processes; match self.process_table.handle_mouse_event(event) { WidgetEventResult::Quit => WidgetEventResult::Quit, @@ -1142,6 +1150,7 @@ impl Component for ProcessManager { if let ProcessManagerSelection::Sort = self.selected { self.sort_menu.handle_mouse_event(event) } else { + self.prev_selected = self.selected; self.selected = ProcessManagerSelection::Sort; self.sort_menu.handle_mouse_event(event); WidgetEventResult::Redraw @@ -1154,6 +1163,7 @@ impl Component for ProcessManager { if let ProcessManagerSelection::Search = self.selected { self.search_input.handle_mouse_event(event) } else { + self.prev_selected = self.selected; self.selected = ProcessManagerSelection::Search; self.search_input.handle_mouse_event(event); WidgetEventResult::Redraw @@ -1261,9 +1271,11 @@ impl Widget for ProcessManager { area }; + let process_selected = + selected && matches!(self.selected, ProcessManagerSelection::Processes); let process_block = self .block() - .selected(selected && matches!(self.selected, ProcessManagerSelection::Processes)) + .selected(process_selected) .borders(self.block_border) .expanded(expanded && !self.show_sort && !self.show_search); @@ -1273,7 +1285,7 @@ impl Widget for ProcessManager { &self.display_data, process_block, area, - selected, + process_selected, self.show_scroll_index, ); } @@ -1485,4 +1497,69 @@ impl Widget for ProcessManager { fn height(&self) -> LayoutRule { self.height } + + fn handle_widget_selection_left(&mut self) -> SelectionAction { + if self.show_sort { + if let ProcessManagerSelection::Processes = self.selected { + self.prev_selected = self.selected; + self.selected = ProcessManagerSelection::Sort; + SelectionAction::Handled + } else { + SelectionAction::NotHandled + } + } else { + SelectionAction::NotHandled + } + } + + fn handle_widget_selection_right(&mut self) -> SelectionAction { + if self.show_sort { + if let ProcessManagerSelection::Sort = self.selected { + self.prev_selected = self.selected; + self.selected = ProcessManagerSelection::Processes; + SelectionAction::Handled + } else { + SelectionAction::NotHandled + } + } else { + SelectionAction::NotHandled + } + } + + fn handle_widget_selection_up(&mut self) -> SelectionAction { + if self.show_search { + if let ProcessManagerSelection::Search = self.selected { + let prev = self.prev_selected; + self.prev_selected = self.selected; + if self.show_sort && prev == ProcessManagerSelection::Sort { + self.selected = ProcessManagerSelection::Sort; + } else { + self.selected = ProcessManagerSelection::Processes; + } + SelectionAction::Handled + } else { + SelectionAction::NotHandled + } + } else { + SelectionAction::NotHandled + } + } + + fn handle_widget_selection_down(&mut self) -> SelectionAction { + if self.show_search { + if let ProcessManagerSelection::Processes = self.selected { + self.prev_selected = self.selected; + self.selected = ProcessManagerSelection::Search; + SelectionAction::Handled + } else if self.show_sort && self.selected == ProcessManagerSelection::Sort { + self.prev_selected = self.selected; + self.selected = ProcessManagerSelection::Search; + SelectionAction::Handled + } else { + SelectionAction::NotHandled + } + } else { + SelectionAction::NotHandled + } + } } diff --git a/src/bin/main.rs b/src/bin/main.rs index 3c52a978..358b95be 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -51,7 +51,7 @@ fn main() -> Result<()> { // Set up input handling let (sender, receiver) = mpsc::channel(); // FIXME: Make this bounded, prevents overloading. - let _input_thread = create_input_thread(sender.clone(), thread_termination_lock.clone()); + let input_thread = create_input_thread(sender.clone(), thread_termination_lock.clone()); // Cleaning loop // TODO: Probably worth spinning this off into an async thread or something... @@ -102,6 +102,7 @@ fn main() -> Result<()> { terminal.hide_cursor()?; // Set panic hook + // TODO: Make this close all the child threads too! panic::set_hook(Box::new(|info| panic_hook(info))); // Set termination hook @@ -135,6 +136,8 @@ fn main() -> Result<()> { // I think doing it in this order is safe... *thread_termination_lock.lock().unwrap() = true; thread_termination_cvar.notify_all(); + + let _ = input_thread.join(); cleanup_terminal(&mut terminal)?; Ok(()) diff --git a/src/canvas.rs b/src/canvas.rs index ecf40633..0c91978d 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -207,7 +207,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 app_state.is_frozen() { let split_loc = Layout::default() .constraints([Constraint::Min(0), Constraint::Length(1)]) .split(f.size()); diff --git a/src/constants.rs b/src/constants.rs index a95ef6db..636616c7 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -243,8 +243,8 @@ pub const GENERAL_HELP_TEXT: [&str; 30] = [ "1 - General", "q, Ctrl-c Quit", "Esc Close dialog windows, search, widgets, or exit expanded mode", - "Ctrl-r Reset display and any collected data", - "f Freeze/unfreeze updating with new data", + "Ctrl-r Resets any collected data", + "f Toggles freezing, which stops new data from being shown", "Ctrl-Left, ", "Shift-Left, Move widget selection left", "H, A ", diff --git a/src/data_conversion.rs b/src/data_conversion.rs index d75d98cc..a201a3a8 100644 --- a/src/data_conversion.rs +++ b/src/data_conversion.rs @@ -189,17 +189,9 @@ pub fn convert_disk_row(current_data: &DataCollection) -> TextTableData { } pub fn convert_cpu_data_points( - current_data: &DataCollection, existing_cpu_data: &mut Vec, is_frozen: bool, + current_data: &DataCollection, existing_cpu_data: &mut Vec, ) { - let current_time = if is_frozen { - if let Some(frozen_instant) = current_data.frozen_instant { - frozen_instant - } else { - current_data.current_instant - } - } else { - current_data.current_instant - }; + let current_time = current_data.current_instant; // Initialize cpu_data_vector if the lengths don't match... if let Some((_time, data)) = ¤t_data.timed_data_vec.last() { @@ -250,34 +242,32 @@ pub fn convert_cpu_data_points( cpu.legend_value = format!("{:.0}%", cpu_usage.round()); }); } - } - for (time, data) in ¤t_data.timed_data_vec { - let time_from_start: f64 = (current_time.duration_since(*time).as_millis() as f64).floor(); + for (time, data) in ¤t_data.timed_data_vec { + let time_from_start: f64 = + (current_time.duration_since(*time).as_millis() as f64).floor(); - for (itx, cpu) in data.cpu_data.iter().enumerate() { - if let Some(cpu_data) = existing_cpu_data.get_mut(itx + 1) { - cpu_data.cpu_data.push((-time_from_start, *cpu)); + for (itx, cpu) in data.cpu_data.iter().enumerate() { + if let Some(cpu_data) = existing_cpu_data.get_mut(itx + 1) { + cpu_data.cpu_data.push((-time_from_start, *cpu)); + } } - } - if *time == current_time { - break; + if *time == current_time { + break; + } + } + } else { + // No data, clear if non-empty. This is usually the case after a reset. + if !existing_cpu_data.is_empty() { + *existing_cpu_data = vec![]; } } } -pub fn convert_mem_data_points(current_data: &DataCollection, is_frozen: bool) -> Vec { +pub fn convert_mem_data_points(current_data: &DataCollection) -> Vec { let mut result: Vec = Vec::new(); - let current_time = if is_frozen { - if let Some(frozen_instant) = current_data.frozen_instant { - frozen_instant - } else { - current_data.current_instant - } - } else { - current_data.current_instant - }; + let current_time = current_data.current_instant; for (time, data) in ¤t_data.timed_data_vec { if let Some(mem_data) = data.mem_data { @@ -293,17 +283,9 @@ pub fn convert_mem_data_points(current_data: &DataCollection, is_frozen: bool) - result } -pub fn convert_swap_data_points(current_data: &DataCollection, is_frozen: bool) -> Vec { +pub fn convert_swap_data_points(current_data: &DataCollection) -> Vec { let mut result: Vec = Vec::new(); - let current_time = if is_frozen { - if let Some(frozen_instant) = current_data.frozen_instant { - frozen_instant - } else { - current_data.current_instant - } - } else { - current_data.current_instant - }; + let current_time = current_data.current_instant; for (time, data) in ¤t_data.timed_data_vec { if let Some(swap_data) = data.swap_data { @@ -391,21 +373,13 @@ pub fn convert_mem_labels( } pub fn get_rx_tx_data_points( - current_data: &DataCollection, is_frozen: bool, network_scale_type: &AxisScaling, - network_unit_type: &DataUnit, network_use_binary_prefix: bool, + current_data: &DataCollection, network_scale_type: &AxisScaling, network_unit_type: &DataUnit, + network_use_binary_prefix: bool, ) -> (Vec, Vec) { let mut rx: Vec = Vec::new(); let mut tx: Vec = Vec::new(); - let current_time = if is_frozen { - if let Some(frozen_instant) = current_data.frozen_instant { - frozen_instant - } else { - current_data.current_instant - } - } else { - current_data.current_instant - }; + let current_time = current_data.current_instant; for (time, data) in ¤t_data.timed_data_vec { let time_from_start: f64 = (current_time.duration_since(*time).as_millis() as f64).floor(); @@ -446,13 +420,11 @@ pub fn get_rx_tx_data_points( } pub fn convert_network_data_points( - current_data: &DataCollection, is_frozen: bool, need_four_points: bool, - network_scale_type: &AxisScaling, network_unit_type: &DataUnit, - network_use_binary_prefix: bool, + current_data: &DataCollection, need_four_points: bool, network_scale_type: &AxisScaling, + network_unit_type: &DataUnit, network_use_binary_prefix: bool, ) -> ConvertedNetworkData { let (rx, tx) = get_rx_tx_data_points( current_data, - is_frozen, network_scale_type, network_unit_type, network_use_binary_prefix, diff --git a/src/lib.rs b/src/lib.rs index 4716881c..05718df6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,7 +21,7 @@ use std::{ use crossterm::{ event::{ - read, DisableMouseCapture, Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent, + poll, read, DisableMouseCapture, Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind, }, execute, @@ -251,16 +251,11 @@ pub fn cleanup_terminal( )?; terminal.show_cursor()?; - // if is_debug { - // let mut tmp_dir = std::env::temp_dir(); - // tmp_dir.push("bottom_debug.log"); - // println!("Your debug file is located at {:?}", tmp_dir.as_os_str()); - // } - Ok(()) } /// Based on https://github.com/Rigellute/spotify-tui/blob/master/src/main.rs +#[allow(clippy::mutex_atomic)] pub fn panic_hook(panic_info: &PanicInfo<'_>) { let mut stdout = stdout(); @@ -307,32 +302,37 @@ pub fn create_input_thread( } } - if let Ok(event) = read() { - match event { - Event::Key(event) => { - if Instant::now().duration_since(keyboard_timer).as_millis() >= 20 { - if sender.send(BottomEvent::KeyInput(event)).is_err() { - break; + if let Ok(poll) = poll(Duration::from_millis(20)) { + if poll { + if let Ok(event) = read() { + match event { + Event::Key(event) => { + if Instant::now().duration_since(keyboard_timer).as_millis() >= 20 { + if sender.send(BottomEvent::KeyInput(event)).is_err() { + break; + } + keyboard_timer = Instant::now(); + } } - keyboard_timer = Instant::now(); - } - } - 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() { + Event::Mouse(event) => match &event.kind { + MouseEventKind::Drag(_) => {} + MouseEventKind::Moved => {} + _ => { + if Instant::now().duration_since(mouse_timer).as_millis() >= 20 + { + if sender.send(BottomEvent::MouseI