diff options
author | Clement Tsang <34804052+ClementTsang@users.noreply.github.com> | 2020-08-15 17:35:49 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-08-15 20:35:49 -0400 |
commit | f3897f0538f90c682b96bc340c3c05e80be10b2d (patch) | |
tree | f24673d4d5702e48b9d3d1889f7498c97ac238a1 | |
parent | 84f63f2f8306382dbf5cab819589161bf0b7c093 (diff) |
feature: Allow sorting by any column
This feature allows any column to be sortable.
This also adds:
- Inverting sort for current column with `I`
- Invoking a sort widget with `s` or `F6`. Close with same key or esc.
And:
- A bugfix in regards the basic menu and battery widget
- A lot of refactoring
-rw-r--r-- | .vscode/settings.json | 1 | ||||
-rw-r--r-- | CHANGELOG.md | 4 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | README.md | 13 | ||||
-rw-r--r-- | src/app.rs | 933 | ||||
-rw-r--r-- | src/app/data_farmer.rs | 8 | ||||
-rw-r--r-- | src/app/data_harvester.rs | 2 | ||||
-rw-r--r-- | src/app/data_harvester/cpu.rs | 10 | ||||
-rw-r--r-- | src/app/data_harvester/processes.rs | 43 | ||||
-rw-r--r-- | src/app/layout_manager.rs | 267 | ||||
-rw-r--r-- | src/app/states.rs | 253 | ||||
-rw-r--r-- | src/canvas.rs | 47 | ||||
-rw-r--r-- | src/canvas/dialogs/help_dialog.rs | 2 | ||||
-rw-r--r-- | src/canvas/drawing_utils.rs | 8 | ||||
-rw-r--r-- | src/canvas/widgets/basic_table_arrows.rs | 43 | ||||
-rw-r--r-- | src/canvas/widgets/cpu_graph.rs | 6 | ||||
-rw-r--r-- | src/canvas/widgets/process_table.rs | 226 | ||||
-rw-r--r-- | src/constants.rs | 49 | ||||
-rw-r--r-- | src/data_conversion.rs | 6 | ||||
-rw-r--r-- | src/main.rs | 95 | ||||
-rw-r--r-- | src/options.rs | 20 | ||||
-rw-r--r-- | src/options/layout_options.rs | 92 | ||||
-rw-r--r-- | tests/widget_movement_tests.rs | 1 |
23 files changed, 1379 insertions, 751 deletions
diff --git a/.vscode/settings.json b/.vscode/settings.json index 1f96e429..50026c86 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -31,6 +31,7 @@ "shilangyu", "softirq", "stime", + "subwidget", "sysinfo", "tokei", "twrite", diff --git a/CHANGELOG.md b/CHANGELOG.md index 80715eed..f1ae3685 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [179](https://github.com/ClementTsang/bottom/pull/179): Show full command/process path as an option. +- [183](https://github.com/ClementTsang/bottom/pull/183): Added sorting capabilities to any column. + ### Changes - Added `WASD` as an alternative widget movement system. @@ -25,6 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Bug Fixes +- [183](https://github.com/ClementTsang/bottom/pull/183): Fixed bug in basic mode where the battery widget was placed incorrectly. + ## [0.4.5] - 2020-07-08 - No changes here, just an uptick for Crates.io using the wrong Cargo.lock. @@ -38,6 +38,7 @@ backtrace = "0.3" serde = {version = "1.0", features = ["derive"] } unicode-segmentation = "1.6.0" unicode-width = "0.1.7" +# tui = {version = "0.10.0", features = ["crossterm"], default-features = false, git = "https://github.com/fdehau/tui-rs.git"} tui = {version = "0.10.0", features = ["crossterm"], default-features = false } # For debugging only... @@ -28,6 +28,7 @@ A cross-platform graphical process/system monitor with a customizable interface - [CPU bindings](#cpu-bindings) - [Process bindings](#process-bindings) - [Process search bindings](#process-search-bindings) + - [Process sort bindings](#process-sort-bindings) - [Battery bindings](#battery-bindings) - [Process searching keywords](#process-searching-keywords) - [Supported keywords](#supported-keywords) @@ -222,6 +223,8 @@ Run using `btm`. | `Tab` | Group/un-group processes with the same name | | `Ctrl-f`, `/` | Open process search widget | | `P` | Toggle between showing the full path or just the process name | +| `s, F6` | Open process sort widget | +| `I` | Invert current sort | #### Process search bindings @@ -240,6 +243,16 @@ Run using `btm`. | `Left` | Move cursor left | | `Right` | Move cursor right | +### Process sort bindings + +| | | +| -------------- | ------------------------------- | +| `Down`, `j` | Scroll down in list | +| `Up`, `k` | Scroll up in list | +| `Mouse scroll` | Scroll through sort widget | +| `Esc` | Close the sort widget | +| `Enter` | Sort by current selected column | + #### Battery bindings | | | @@ -144,20 +144,23 @@ impl App { } self.is_force_redraw = true; - } else if self.is_filtering_or_searching() { + } else { match self.current_widget.widget_type { BottomWidgetType::Proc => { if let Some(current_proc_state) = self .proc_state .get_mut_widget_state(self.current_widget.widget_id) { - if current_proc_state.is_search_enabled() { + if current_proc_state.is_search_enabled() || current_proc_state.is_sort_open + { current_proc_state .process_search_state .search_state .is_enabled = false; + current_proc_state.is_sort_open = false; + self.is_force_redraw = true; + return; } - self.is_force_redraw = true; } } BottomWidgetType::ProcSearch => { @@ -170,16 +173,32 @@ impl App { .process_search_state .search_state .is_enabled = false; - self.move_widget_selection_up(); + self.move_widget_selection(&WidgetDirection::Up); + return; + } + } + } + BottomWidgetType::ProcSort => { + if let Some(current_proc_state) = self + .proc_state + .get_mut_widget_state(self.current_widget.widget_id - 2) + { + if current_proc_state.is_sort_open { + current_proc_state.columns.current_scroll_position = + current_proc_state.columns.backup_prev_scroll_position; + current_proc_state.is_sort_open = false; + self.move_widget_selection(&WidgetDirection::Right); + return; } - self.is_force_redraw = true; } } _ => {} } - } else if self.is_expanded { - self.is_expanded = false; - self.is_force_redraw = true; + + if self.is_expanded { + self.is_expanded = false; + self.is_force_redraw = true; + } } } @@ -190,40 +209,6 @@ impl App { } } - fn is_filtering_or_searching(&self) -> bool { - match self.current_widget.widget_type { - BottomWidgetType::Proc => { - if let Some(proc_widget_state) = self - .proc_state - .widget_states - .get(&self.current_widget.widget_id) - { - proc_widget_state - .process_search_state - .search_state - .is_enabled - } else { - false - } - } - BottomWidgetType::ProcSearch => { - if let Some(proc_widget_state) = self - .proc_state - .widget_states - .get(&(self.current_widget.widget_id - 1)) - { - proc_widget_state - .process_search_state - .search_state - .is_enabled - } else { - false - } - } - _ => false, - } - } - fn reset_multi_tap_keys(&mut self) { self.awaiting_second_char = false; self.second_char = None; @@ -254,6 +239,14 @@ impl App { { // Toggles process widget grouping state proc_widget_state.is_grouped = !(proc_widget_state.is_grouped); + + proc_widget_state + .columns + .column_mapping + .get_mut(&processes::ProcessSorting::State) + .unwrap() + .enabled = !(proc_widget_state.is_grouped); + self.proc_state.force_update = Some(self.current_widget.widget_id); } } @@ -287,12 +280,64 @@ impl App { .process_search_state .search_state .is_enabled = true; - self.move_widget_selection_down(); + self.move_widget_selection(&WidgetDirection::Down); } } } } + pub fn toggle_sort(&mut self) { + match &self.current_widget.widget_type { + widget_type @ BottomWidgetType::Proc | widget_type @ BottomWidgetType::ProcSort => { + let widget_id = self.current_widget.widget_id + - match &widget_type { + BottomWidgetType::Proc => 0, + BottomWidgetType::ProcSort => 2, + _ => 0, + }; + + if let Some(proc_widget_state) = self.proc_state.get_mut_widget_state(widget_id) { + // Open up sorting dialog for that specific proc widget. + // TODO: It might be a decent idea to allow sorting ALL? I dunno. + + proc_widget_state.is_sort_open = !proc_widget_state.is_sort_open; + if proc_widget_state.is_sort_open { + // If it just opened, move left + proc_widget_state + .columns + .set_to_sorted_index(&proc_widget_state.process_sorting_type); + self.move_widget_selection(&WidgetDirection::Left); + } else { + // Otherwise, move right + self.move_widget_selection(&WidgetDirection::Right); + } + } + } + _ => {} + } + } + + pub fn invert_sort(&mut self) { + match &self.current_widget.widget_type { + widget_type @ BottomWidgetType::Proc | widget_type @ BottomWidgetType::ProcSort => { + let widget_id = self.current_widget.widget_id + - match &widget_type { + BottomWidgetType::Proc => 0, + BottomWidgetType::ProcSort => 2, + _ => 0, + }; + + if let Some(proc_widget_state) = self.proc_state.get_mut_widget_state(widget_id) { + proc_widget_state.process_sorting_reverse = + !proc_widget_state.process_sorting_reverse; + + self.proc_state.force_update = Some(widget_id); + } + } + _ => {} + } + } + pub fn toggle_ignore_case(&mut self) { let is_in_search_widget = self.is_in_search_widget(); if let Some(proc_widget_state) = self @@ -362,6 +407,16 @@ impl App { } else { self.delete_dialog_state.is_showing_dd = false; } + } else if let BottomWidgetType::ProcSort = self.current_widget.widget_type { + if let Some(proc_widget_state) = self + .proc_state + .widget_states + .get_mut(&(self.current_widget.widget_id - 2)) + { + self.proc_state.force_update = Some(self.current_widget.widget_id - 2); + proc_widget_state.update_sorting_with_columns(); + self.toggle_sort(); + } } } @@ -457,7 +512,7 @@ impl App { proc_widget_state .process_search_state .search_state - .cursor_direction = CursorDirection::LEFT; + .cursor_direction = CursorDirection::Left; proc_widget_state.update_query(); self.proc_state.force_update = Some(self.current_widget.widget_id - 1); @@ -496,13 +551,18 @@ impl App { if !self.is_in_dialog() { match self.current_widget.widget_type { BottomWidgetType::Proc => { - if let Some(proc_widget_state) = self - .proc_state - .get_mut_widget_state(self.current_widget.widget_id) - { - proc_widget_state.current_column_index = - proc_widget_state.current_column_index.saturating_sub(1); - } + // if let Some(proc_widget_state) = self + // .proc_state + // .get_mut_widget_state(self.current_widget.widget_id) + // { + // proc_widget_state.current_column_index = + // proc_widget_state.current_column_index.saturating_sub(1); + + // debug!( + // "Current column index <: {}", + // proc_widget_state.current_column_index + // ); + // } } BottomWidgetType::ProcSearch => { let is_in_search_widget = self.is_in_search_widget(); @@ -527,7 +587,7 @@ impl App { proc_widget_state .process_search_state .search_state - .cursor_direction = CursorDirection::LEFT; + .cursor_direction = CursorDirection::Left; } } } @@ -555,14 +615,20 @@ impl App { if !self.is_in_dialog() { match self.current_widget.widget_type { BottomWidgetType::Proc => { - if let Some(proc_widget_state) = self - .proc_state - .get_mut_widget_state(self.current_widget.widget_id) - { - if proc_widget_state.current_column_index < proc_widget_state.num_columns { - proc_widget_state.current_column_index += 1; - } - } + // if let Some(proc_widget_state) = self + // .proc_state + // .get_mut_widget_state(self.current_widget.widget_id) + // { + // if proc_widget_state.current_column_index + // < proc_widget_state.columns.get_enabled_columns() + // { + // proc_widget_state.current_column_index += 1; + // } + // debug!( + // "Current column index >: {}", + // proc_widget_state.current_column_index + // ); + // } } BottomWidgetType::ProcSearch => { let is_in_search_widget = self.is_in_search_widget(); @@ -587,7 +653,7 @@ impl App { proc_widget_state .process_search_state .search_state - .cursor_direction = CursorDirection::RIGHT; + .cursor_direction = CursorDirection::Right; } } } @@ -643,7 +709,7 @@ impl App { proc_widget_state .process_search_state .search_state - .cursor_direction = CursorDirection::LEFT; + .cursor_direction = CursorDirection::Left; } } } @@ -689,7 +755,7 @@ impl App { proc_widget_state .process_search_state .search_state - .cursor_direction = CursorDirection::RIGHT; + .cursor_direction = CursorDirection::Right; } } } @@ -815,7 +881,7 @@ impl App { proc_widget_state .process_search_state .search_state - .cursor_direction = CursorDirection::RIGHT; + .cursor_direction = CursorDirection::Right; return; } @@ -827,11 +893,17 @@ impl App { // more obvious that we are separating dialog logic and normal logic IMO. // This is even more so as most logic already checks for dialog state. match caught_char { - '1' => self.help_scroll_to_or_max(self.help_dialog_state.index_shortcuts[1]), - '2' => self.help_scroll_to_or_max(self.help_dialog_state.index_shortcuts[2]), - '3' => self.help_scroll_to_or_max(self.help_dialog_state.index_shortcuts[3]), - '4' => self.help_scroll_to_or_max(self.help_dialog_state.index_shortcuts[4]), - '5' => self.help_scroll_to_or_max(self.help_dialog_state.index_shortcuts[5]), + '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' => { + let potential_index = caught_char.to_digit(10); + if let Some(potential_index) = potential_index { + if (potential_index as usize) < self.help_dialog_state.index_shortcuts.len() + { + self.help_scroll_to_or_max( + self.help_dialog_state.index_shortcuts[potential_index as usize], + ); + } + } + } 'j' | 'k' | 'g' | 'G' => self.handle_char(caught_char), _ => {} } @@ -900,13 +972,13 @@ impl App { .get_mut_widget_state(self.current_widget.widget_id) { match proc_widget_state.process_sorting_type { - processes::ProcessSorting::CPU => { + processes::ProcessSorting::CpuPercent => { proc_widget_state.process_sorting_reverse = !proc_widget_state.process_sorting_reverse } _ => { proc_widget_state.process_sorting_type = - processes::ProcessSorting::CPU; + processes::ProcessSorting::CpuPercent; proc_widget_state.process_sorting_reverse = true; } } @@ -923,13 +995,13 @@ impl App { .get_mut_widget_state(self.current_widget.widget_id) { match proc_widget_state.process_sorting_type { - processes::ProcessSorting::MEM => { + processes::ProcessSorting::MemPercent => { proc_widget_state.process_sorting_reverse = !proc_widget_state.process_sorting_reverse } _ => { proc_widget_state.process_sorting_type = - processes::ProcessSorting::MEM; + processes::ProcessSorting::MemPercent; proc_widget_state.process_sorting_reverse = true; } } @@ -947,13 +1019,13 @@ impl App { // Skip if grouped if !proc_widget_state.is_grouped { match proc_widget_state.process_sorting_type { - processes::ProcessSorting::PID => { + processes::ProcessSorting::Pid => { proc_widget_state.process_sorting_reverse = !proc_widget_state.process_sorting_reverse } _ => { proc_widget_state.process_sorting_type = - processes::ProcessSorting::PID; + processes::ProcessSorting::Pid; proc_widget_state.process_sorting_reverse = false; } } @@ -969,8 +1041,24 @@ impl App { .proc_state .get_mut_widget_state(self.current_widget.widget_id) { - proc_widget_state.is_using_full_path = - !proc_widget_state.is_using_full_path; + proc_widget_state.is_using_command = !proc_widget_state.is_using_command; + proc_widget_state + .toggle_command_and_name(proc_widget_state.is_using_command); + + match &proc_widget_state.process_sorting_type { + processes::ProcessSorting::Command + | processes::ProcessSorting::ProcessName => { + if proc_widget_state.is_using_command { + proc_widget_state.process_sorting_type = + processes::ProcessSorting::Command; + } else { + proc_widget_state.process_sorting_type = + processes::ProcessSorting::ProcessName; + } + } + _ => {} + } + self.proc_state.force_update = Some(self.current_widget.widget_id); } } @@ -982,13 +1070,18 @@ impl App { .get_mut_widget_state(self.current_widget.widget_id) { match proc_widget_state.process_sorting_type { - processes::ProcessSorting::IDENTIFIER => { + processes::ProcessSorting::ProcessName + | processes::ProcessSorting::Command => { proc_widget_state.process_sorting_reverse = !proc_widget_state.process_sorting_reverse } _ => { proc_widget_state.process_sorting_type = - processes::ProcessSorting::IDENTIFIER; + if proc_widget_state.is_using_command { + processes::ProcessSorting::Command + } else { + processes::ProcessSorting::ProcessName + }; proc_widget_state.process_sorting_reverse = false; } } @@ -1001,15 +1094,17 @@ impl App { self.help_dialog_state.is_showing_help = true; self.is_force_redraw = true; } - 'H' | 'A' => self.move_widget_selection_left(), - 'L' | 'D' => self.move_widget_selection_right(), - 'K' | 'W' => self.move_widget_selection_up(), - 'J' | 'S' => self.move_widget_selection_down(), + 'H' | 'A' => self.move_widget_selection(&WidgetDirection::Left), + 'L' | 'D' => self.move_widget_selection(&WidgetDirection::Right), + 'K' | 'W' => self.move_widget_selection(&WidgetDirection::Up), + 'J' | 'S' => self.move_widget_selection(&WidgetDirection::Down), ' ' => self.on_space(), '+' => self.zoom_in(), '-' => self.zoom_out(), '=' => self.reset_zoom(), 'e' => self.expand_widget(), + 's' => self.toggle_sort(), + 'I' => self.invert_sort(), _ => {} } @@ -1055,77 +1150,357 @@ impl App { } } - pub fn move_widget_selection_left(&mut self) { + pub fn move_widget_selection(&mut self, direction: &WidgetDirection) { + /* + We follow these following steps: + 1. Send a movement signal in `direction`. + 2. Check if this new widget we've landed on is hidden. If not, halt. + 3. If it hidden, loop and either send: + - A signal equal to the current direction, if it is opposite of the reflection. + - Reflection direction. + */ + if !self.is_in_dialog() && !self.is_expanded { - if let Some(current_widget) = self.widget_map.get(&self.current_widget.widget_id) { - if let Some(new_widget_id) = current_widget.left_neighbour { - if let Some(new_widget) = self.widget_map.get(&new_widget_id) { - match new_widget.widget_type { - BottomWidgetType::Temp - | BottomWidgetType::Proc - | BottomWidgetType::ProcSearch - | BottomWidgetType::Disk - | BottomWidgetType::Battery - if self.basic_table_widget_state.is_some() => - { - if let Some(basic_table_widget_state) = - &mut self.basic_table_widget_state + if let Some(new_widget_id) = &(match direction { + WidgetDirection::Left => self.current_widget.left_neighbour, + WidgetDirection::Right => self.current_widget.right_neighbour, + WidgetDirection::Up => self.current_widget.up_neighbour, + WidgetDirection::Down => self.current_widget.down_neighbour, + }) { + if let Some(new_widget) = self.widget_map.get(&new_widget_id) { + match &new_widget.widget_type { + BottomWidgetType::Temp + | BottomWidgetType::Proc + | BottomWidgetType::ProcSearch + | BottomWidgetType::ProcSort + | BottomWidgetType::Disk + | BottomWidgetType::Battery + if self.basic_table_widget_state.is_some() + && (*direction == WidgetDirection::Left + || *direction == WidgetDirection::Right) => + { + // Gotta do this for the sort widget + if let BottomWidgetType::ProcSort = new_widget.widget_type { + if let Some(proc_widget_state) = + self.proc_state.widget_states.get(&(new_widget_id - 2)) { - basic_table_widget_state.currently_displayed_widget_id = - new_widget_id; - basic_table_widget_state.currently_displayed_widget_type = - new_widget.widget_type.clone(); + if proc_widget_state.is_sort_open { + self.current_widget = new_widget.clone(); + } else if let Some(next_new_widget_id) = match direction { + WidgetDirection::Left => new_widget.left_neighbour, + _ => new_widget.right_neighbour, + } { + if let Some(next_new_widget) = + self.widget_map.get(&next_new_widget_id) + { + self.current_widget = next_new_widget.clone(); + } + } } + } else { self.current_widget = new_widget.clone(); } - BottomWidgetType::CpuLegend => { - if let Some(cpu_widget_state) = - self.cpu_state.widget_states.get(&(new_widget_id - 1)) - { - if cpu_widget_state.is_legend_hidden { - if let Some(next_new_widget_id) = new_widget.left_neighbour + + if let Some(basic_table_widget_state) = + &mut self.basic_table_widget_state + { + basic_table_widget_state.currently_displayed_widget_id = + self.current_widget.widget_id; + basic_table_widget_state.currently_displayed_widget_type = + self.current_widget.widget_type.clone(); + } + } + BottomWidgetType::BasicTables => { + match &direction { + WidgetDirection::Up => { + // Note this case would fail if it moved up into a hidden + // widget, but it's for basic so whatever, it's all hard-coded + // right now anyways. + if let Some(next_new_widget_id) = new_widget.up_neighbour { + if let Some(next_new_widget) = + self.widget_map.get(&next_new_widget_id) { - if let Some(next_new_widget) = - self.widget_map.get(&next_new_widget_id) + self.current_widget = next_new_widget.clone(); + } + } + } + WidgetDirection::Down => { + // This means we're in basic mode. As such, then + // we want to move DOWN to the currently shown widget + if let Some(basic_table_widget_state) = + &self.basic_table_widget_state + { + if let Some(next_new_widget) = self.widget_map.get( + &basic_table_widget_state.currently_displayed_widget_id, + ) { + self.current_widget = next_new_widget.clone(); + } + } + } + _ => self.current_widget = new_widget.clone(), + } + } + _ if new_widget.parent_reflector.is_some() => { + // It may be hidden... + if let Some((parent_direction, offset)) = &new_widget.parent_reflector { + if direction.is_opposite(parent_direction) { + // Keep going in the current direction if hidden... + let next_neighbour_id = match &direction { + WidgetDirection::Left => new_widget.left_neighbour, + WidgetDirection::Right => new_widget.right_neighbour, + WidgetDirection::Up => new_widget.up_neighbour, + WidgetDirection::Down => new_widget.down_neighbour, + } + .unwrap_or(*new_widget_id); + match &new_widget.widget_type { + BottomWidgetType::CpuLegend => { + if let Some(cpu_widget_state) = self + |