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. diff --git a/Cargo.toml b/Cargo.toml index 10106589..1b225266 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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... diff --git a/README.md b/README.md index 7797f8d0..f8acd08d 100644 --- a/README.md +++ b/README.md @@ -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 | | | diff --git a/src/app.rs b/src/app.rs index 0d7a40aa..c884013f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -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,55 +1150,46 @@ 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 - { - basic_table_widget_state.currently_displayed_widget_id = - new_widget_id; - basic_table_widget_state.currently_displayed_widget_type = - new_widget.widget_type.clone(); - } - 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(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::ProcSearch => { + 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 - 1)) + self.proc_state.widget_states.get(&(new_widget_id - 2)) { - if proc_widget_state.is_search_enabled() { + if proc_widget_state.is_sort_open { self.current_widget = new_widget.clone(); - } else if let Some(next_new_widget_id) = new_widget.up_neighbour - { + } 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) { @@ -1111,21 +1197,310 @@ impl App { } } } + } else { + self.current_widget = new_widget.clone(); + } + + 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(); } - _ => self.current_widget = new_widget.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) + { + 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 + .cpu_state + .widget_states + .get(&(new_widget_id - *offset)) + { + if cpu_widget_state.is_legend_hidden { + if let Some(next_neighbour_widget) = + self.widget_map.get(&next_neighbour_id) + { + self.current_widget = + next_neighbour_widget.clone(); + } + } else { + self.current_widget = new_widget.clone(); + } + } + } + BottomWidgetType::ProcSearch + | BottomWidgetType::ProcSort => { + if let Some(proc_widget_state) = self + .proc_state + .widget_states + .get(&(new_widget_id - *offset)) + { + match &new_widget.widget_type { + BottomWidgetType::ProcSearch => { + if !proc_widget_state.is_search_enabled() { + if let Some(next_neighbour_widget) = + self.widget_map + .get(&next_neighbour_id) + { + self.current_widget = + next_neighbour_widget.clone(); + } + } else { + self.current_widget = + new_widget.clone(); + } + } + BottomWidgetType::ProcSort => { + if !proc_widget_state.is_sort_open { + if let Some(next_neighbour_widget) = + self.widget_map + .get(&next_neighbour_id) + { + self.current_widget = + next_neighbour_widget.clone(); + } + } else { + self.current_widget = + new_widget.clone(); + } + } + _ => { + self.current_widget = new_widget.clone(); + } + } + } + } + _ => { + self.current_widget = new_widget.clone(); + } + } + } else { + // Reflect + match &new_widget.widget_type { + BottomWidgetType::CpuLegend => { + if let Some(cpu_widget_state) = self + .cpu_state + .widget_states + .get(&(new_widget_id - *offset)) + { + if cpu_widget_state.is_legend_hidden { + if let Some(parent_cpu_widget) = self + .widget_map + .get(&(new_widget_id - *offset)) + { + self.current_widget = + parent_cpu_widget.clone(); + } + } else { + self.current_widget = new_widget.clone(); + } + } + } + BottomWidgetType::ProcSearch + | BottomWidgetType::ProcSort => { + if let Some(proc_widget_state) = self + .proc_state + .widget_states + .get(&(new_widget_id - *offset)) + { + match &new_widget.widget_type { + BottomWidgetType::ProcSearch => { + if !proc_widget_state.is_search_enabled() { + if let Some(parent_proc_widget) = self + .widget_map + .get(&(new_widget_id - *offset)) + { + self.current_widget = + parent_proc_widget.clone(); + } + } else { + self.current_widget = + new_widget.clone(); + } + } + BottomWidgetType::ProcSort => { + if !proc_widget_state.is_sort_open { + if let Some(parent_proc_widget) = self + .widget_map + .get(&(new_widget_id - *offset)) + { + self.current_widget = + parent_proc_widget.clone(); + } + } else { + self.current_widget = + new_widget.clone(); + } + } + _ => { + self.current_widget = new_widget.clone(); + } + } + } + } + _ => { + self.current_widget = new_widget.clone(); + } + } + } + } + } + _ => { + // Cannot be hidden, does not special treatment. + self.current_widget = new_widget.clone(); + } + } + + let mut reflection_dir: Option = None; + if let Some((parent_direction, offset)) = &self.current_widget.parent_reflector + { + match &self.current_widget.widget_type { + BottomWidgetType::CpuLegend => { + if let Some(cpu_widget_state) = self + .cpu_state + .widget_states + .get(&(self.current_widget.widget_id - *offset)) + { + if cpu_widget_state.is_legend_hidden { + reflection_dir = Some(parent_direction.clone()); + } + } + } + BottomWidgetType::ProcSearch | BottomWidgetType::ProcSort => { + if let Some(proc_widget_state) = self + .proc_state + .widget_states + .get(&(self.current_widget.widget_id - *offset)) + { + match &self.current_widget.widget_type { + BottomWidgetType::ProcSearch => { + if !proc_widget_state.is_search_enabled() { + reflection_dir = Some(parent_direction.clone()); + } + } + BottomWidgetType::ProcSort => { + if !proc_widget_state.is_sort_open { + reflection_dir = Some(parent_direction.clone()); + } + } + _ => {} + } + } + } + _ => {} + } + } + + if let Some(ref_dir) = &reflection_dir { + self.move_widget_selection(ref_dir); } } } - } else if self.is_expanded { - self.handle_left_expanded_cpu_movement(); + } else { + match direction { + WidgetDirection::Left => self.handle_left_expanded_movement(), + WidgetDirection::Right => self.handle_right_expanded_movement(), + WidgetDirection::Up => { + if let BottomWidgetType::ProcSearch = self.current_widget.widget_type { + if let Some(current_widget) = + self.widget_map.get(&self.current_widget.widget_id) + { + if let Some(new_widget_id) = current_widget.up_neighbour { + if let Some(new_widget) = self.widget_map.get(&new_widget_id) { + self.current_widget = new_widget.clone(); + } + } + } + } + } + WidgetDirection::Down => match &self.current_widget.widget_type { + proc_type @ BottomWidgetType::Proc | proc_type @ BottomWidgetType::ProcSort => { + let widget_id = self.current_widget.widget_id + - match proc_type { + BottomWidgetType::ProcSort => 2, + _ => 0, + }; + if let Some(current_widget) = self.widget_map.get(&widget_id) { + if let Some(new_widget_id) = current_widget.down_neighbour { + if let Some(new_widget) = self.widget_map.get(&new_widget_id) { + if let Some(proc_widget_state) = + self.proc_state.get_widget_state(widget_id) + { + if proc_widget_state.is_search_enabled() { + self.current_widget = new_widget.clone(); + } + } + } + } + } + } + _ => {} + }, + } } self.reset_multi_tap_keys(); } - fn handle_left_expanded_cpu_movement(&mut self) { - if self.app_config_fields.left_legend { + fn handle_left_expanded_movement(&mut self) { + if let BottomWidgetType::Proc = self.current_widget.widget_type { + if let Some(new_widget_id) = self.current_widget.left_neighbour { + if let Some(proc_widget_state) = self + .proc_state + .widget_states + .get(&self.current_widget.widget_id) + { + if proc_widget_state.is_sort_open { + if let Some(proc_sort_widget) = self.widget_map.get(&new_widget_id) { + self.current_widget = proc_sort_widget.clone(); // TODO: Could I remove this clone w/ static references? + } + } + } + } + } else if self.app_config_fields.left_legend { if let BottomWidgetType::Cpu = self.current_widget.widget_type { if let Some(current_widget) = self.widget_map.get(&self.current_widget.widget_id) { if let Some(cpu_widget_state) = self @@ -1154,80 +1529,14 @@ impl App { } } - pub fn move_widget_selection_right(&mut self) { - 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.right_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 - { - basic_table_widget_state.currently_displayed_widget_id = - new_widget_id; - basic_table_widget_state.currently_displayed_widget_type = - new_widget.widget_type.clone(); - } - 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.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::ProcSearch => { - if let Some(proc_widget_state) = - self.proc_state.widget_states.get(&(new_widget_id - 1)) - { - if proc_widget_state.is_search_enabled() { - self.current_widget = new_widget.clone(); - } else 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) - { - self.current_widget = next_new_widget.clone(); - } - } - } - } - - _ => { - self.current_widget = new_widget.clone(); - } - } - } + fn handle_right_expanded_movement(&mut self) { + if let BottomWidgetType::ProcSort = self.current_widget.widget_type { + if let Some(new_widget_id) = self.current_widget.right_neighbour { + if let Some(proc_sort_widget) = self.widget_map.get(&new_widget_id) { + self.current_widget = proc_sort_widget.clone(); } } - } else if self.is_expanded { - self.handle_right_expanded_cpu_movement(); - } - - self.reset_multi_tap_keys(); - } - - fn handle_right_expanded_cpu_movement(&mut self) { - if self.app_config_fields.left_legend { + } else if self.app_config_fields.left_legend { if let BottomWidgetType::CpuLegend = self.current_widget.widget_type { if let Some(current_widget) = self.widget_map.get(&self.current_widget.widget_id) { if let Some(new_widget_id) = current_widget.right_neighbour { @@ -1256,154 +1565,6 @@ impl App { } } - pub fn move_widget_selection_up(&mut self) { - 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.up_neighbour { - if let Some(new_widget) = self.widget_map.get(&new_widget_id) { - match new_widget.widget_type { - 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) = - self.widget_map.get(&(new_widget_id - 1)) - { - self.current_widget = next_new_widget.clone(); - } - } else { - self.current_widget = new_widget.clone(); - } - } - } - BottomWidgetType::ProcSearch => { - if let Some(proc_widget_state) = - self.proc_state.widget_states.get(&(new_widget_id - 1)) - { - if proc_widget_state.is_search_enabled() { - self.current_widget = new_widget.clone(); - } else 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) - { - self.current_widget = next_new_widget.clone(); - } - } - } - } - BottomWidgetType::BasicTables => { - 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) - { - self.current_widget = next_new_widget.clone(); - } - } - } - _ => { - self.current_widget = new_widget.clone(); - } - } - } - } - } - } else if self.is_expanded { - if let BottomWidgetType::ProcSearch = self.current_widget.widget_type { - if let Some(current_widget) = self.widget_map.get(&self.current_widget.widget_id) { - if let Some(new_widget_id) = current_widget.up_neighbour { - if let Some(new_widget) = self.widget_map.get(&new_widget_id) { - self.current_widget = new_widget.clone(); - } - } - } - } - } - - self.reset_multi_tap_keys(); - } - - pub fn move_widget_selection_down(&mut self) { - 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.down_neighbour { - if let Some(new_widget) = self.widget_map.get(&new_widget_id) { - match new_widget.widget_type { - 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) = - self.widget_map.get(&(new_widget_id - 1)) - { - self.current_widget = next_new_widget.clone(); - } - } else { - self.current_widget = new_widget.clone(); - } - } - } - BottomWidgetType::ProcSearch => { - if let Some(proc_widget_state) = - self.proc_state.widget_states.get(&(new_widget_id - 1)) - { - if proc_widget_state.is_search_enabled() { - self.current_widget = new_widget.clone(); - } else if let Some(next_new_widget_id) = - new_widget.down_neighbour - { - if let Some(next_new_widget) = - self.widget_map.get(&next_new_widget_id) - { - self.current_widget = next_new_widget.clone(); - } - } - } - } - BottomWidgetType::BasicTables => { - // 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(); - } - } - } - } - } - } else if self.is_expanded { - if let BottomWidgetType::Proc = self.current_widget.widget_type { - if let Some(current_widget) = self.widget_map.get(&self.current_widget.widget_id) { - if let Some(new_widget_id) = current_widget.down_neighbour { - if let Some(new_widget) = self.widget_map.get(&new_widget_id) { - if let Some(proc_widget_state) = self - .proc_state - .get_widget_state(self.current_widget.widget_id) - { - if proc_widget_state.is_search_enabled() { - self.current_widget = new_widget.clone(); - } - } - } - } - } - } - } - - self.reset_multi_tap_keys(); - } - pub fn skip_to_first(&mut self) { if !self.is_in_dialog() { match self.current_widget.widget_type { @@ -1413,7 +1574,16 @@ impl App { .get_mut_widget_state(self.current_widget.widget_id) { proc_widget_state.scroll_state.current_scroll_position = 0; - proc_widget_state.scroll_state.scroll_direction = ScrollDirection::UP; + proc_widget_state.scroll_state.scroll_direction = ScrollDirection::Up; + } + } + BottomWidgetType::ProcSort => { + if let Some(proc_widget_state) = self + .proc_state + .get_mut_widget_state(self.current_widget.widget_id - 2) + { + proc_widget_state.columns.current_scroll_position = 0; + proc_widget_state.columns.scroll_direction = ScrollDirection::Up; } } BottomWidgetType::Temp => { @@ -1422,7 +1592,7 @@ impl App { .get_mut_widget_state(self.current_widget.widget_id) { temp_widget_state.scroll_state.current_scroll_position = 0; - temp_widget_state.scroll_state.scroll_direction = ScrollDirection::UP; + temp_widget_state.scroll_state.scroll_direction = ScrollDirection::Up; } } BottomWidgetType::Disk => { @@ -1431,7 +1601,7 @@ impl App { .get_mut_widget_state(self.current_widget.widget_id) { disk_widget_state.scroll_state.current_scroll_position = 0; - disk_widget_state.scroll_state.scroll_direction = ScrollDirection::UP; + disk_widget_state.scroll_state.scroll_direction = ScrollDirection::Up; } } BottomWidgetType::CpuLegend => { @@ -1440,7 +1610,7 @@ impl App { .get_mut_widget_state(self.current_widget.widget_id - 1) { cpu_widget_state.scroll_state.current_scroll_position = 0; - cpu_widget_state.scroll_state.scroll_direction = ScrollDirection::UP; + cpu_widget_state.scroll_state.scroll_direction = ScrollDirection::Up; } } @@ -1469,11 +1639,21 @@ impl App { proc_widget_state.scroll_state.current_scroll_position = finalized_process_data.len() - 1; proc_widget_state.scroll_state.scroll_direction = - ScrollDirection::DOWN; + ScrollDirection::Down; } } } } + BottomWidgetType::ProcSort => { + if let Some(proc_widget_state) = self + .proc_state + .get_mut_widget_state(self.current_widget.widget_id - 2) + { + proc_widget_state.columns.current_scroll_position = + proc_widget_state.columns.get_enabled_columns_len() - 1; + proc_widget_state.columns.scroll_direction = ScrollDirection::Down; + } + } BottomWidgetType::Temp => { if let Some(temp_widget_state) = self .temp_state @@ -1482,7 +1662,7 @@ impl App { if !self.canvas_data.temp_sensor_data.is_empty() { temp_widget_state.scroll_state.current_scroll_position = self.canvas_data.temp_sensor_data.len() - 1; - temp_widget_state.scroll_state.scroll_direction = ScrollDirection::DOWN; + temp_widget_state.scroll_state.scroll_direction = ScrollDirection::Down; } } } @@ -1494,7 +1674,7 @@ impl App { if !self.canvas_data.disk_data.is_empty() { disk_widget_state.scroll_state.current_scroll_position = self.canvas_data.disk_data.len() - 1; - disk_widget_state.scroll_state.scroll_direction = ScrollDirection::DOWN; + disk_widget_state.scroll_state.scroll_direction = ScrollDirection::Down; } } } @@ -1506,7 +1686,7 @@ impl App { let cap = self.canvas_data.cpu_data.len(); if cap > 0 { cpu_widget_state.scroll_state.current_scroll_position = cap - 1; - cpu_widget_state.scroll_state.scroll_direction = ScrollDirection::DOWN; + cpu_widget_state.scroll_state.scroll_direction = ScrollDirection::Down; } } } @@ -1526,6 +1706,7 @@ impl App { if !self.is_in_dialog() { match self.current_widget.widget_type { BottomWidgetType::Proc => self.change_process_position(-1), + BottomWidgetType::ProcSort => self.change_process_sort_position(-1), BottomWidgetType::Temp => self.change_temp_position(-1), BottomWidgetType::Disk => self.change_disk_position(-1), BottomWidgetType::CpuLegend => self.change_cpu_table_position(-1), @@ -1538,6 +1719,7 @@ impl App { if !self.is_in_dialog() { match self.current_widget.widget_type { BottomWidgetType::Proc => self.change_process_position(1), + BottomWidgetType::ProcSort => self.change_process_sort_position(1), BottomWidgetType::Temp => self.change_temp_position(1), BottomWidgetType::Disk => self.change_disk_position(1), BottomWidgetType::CpuLegend => self.change_cpu_table_position(1), @@ -1546,6 +1728,29 @@ impl App { } } + fn change_process_sort_position(&mut self, num_to_change_by: i64) { + if let Some(proc_widget_state) = self + .proc_state + .get_mut_widget_state(self.current_widget.widget_id - 2) + { + let current_posn = proc_widget_state.columns.current_scroll_position; + let num_columns = proc_widget_state.columns.get_enabled_columns_len(); + + if current_posn as i64 + num_to_change_by >= 0 + && current_posn as i64 + num_to_change_by < num_columns as i64 + { + proc_widget_state.columns.current_scroll_position = + (current_posn as i64 + num_to_change_by) as usize; + } + + if num_to_change_by < 0 { + proc_widget_state.columns.scroll_direction = ScrollDirection::Up; + } else { + proc_widget_state.columns.scroll_direction = ScrollDirection::Down; + } + } + } + fn change_cpu_table_position(&mut self, num_to_change_by: i64) { if let Some(cpu_widget_state) = self .cpu_state @@ -1563,9 +1768,9 @@ impl App { } if num_to_change_by < 0 { - cpu_widget_state.scroll_state.scroll_direction = ScrollDirection::UP; + cpu_widget_state.scroll_state.scroll_direction = ScrollDirection::Up; } else { - cpu_widget_state.scroll_state.scroll_direction = ScrollDirection::DOWN; + cpu_widget_state.scroll_state.scroll_direction = ScrollDirection::Down; } } } @@ -1591,9 +1796,9 @@ impl App { } if num_to_change_by < 0 { - proc_widget_state.scroll_state.scroll_direction = ScrollDirection::UP; + proc_widget_state.scroll_state.scroll_direction = ScrollDirection::Up; } else { - proc_widget_state.scroll_state.scroll_direction = ScrollDirection::DOWN; + proc_widget_state.scroll_state.scroll_direction = ScrollDirection::Down; } } } @@ -1615,9 +1820,9 @@ impl App { } if num_to_change_by < 0 { - temp_widget_state.scroll_state.scroll_direction = ScrollDirection::UP; + temp_widget_state.scroll_state.scroll_direction = ScrollDirection::Up; } else { - temp_widget_state.scroll_state.scroll_direction = ScrollDirection::DOWN; + temp_widget_state.scroll_state.scroll_direction = ScrollDirection::Down; } } } @@ -1638,9 +1843,9 @@ impl App { } if num_to_change_by < 0 { - disk_widget_state.scroll_state.scroll_direction = ScrollDirection::UP; + disk_widget_state.scroll_state.scroll_direction = ScrollDirection::Up; } else { - disk_widget_state.scroll_state.scroll_direction = ScrollDirection::DOWN; + disk_widget_state.scroll_state.scroll_direction = ScrollDirection::Down; } } } diff --git a/src/app/data_farmer.rs b/src/app/data_farmer.rs index cf2b3fbc..2948b0ff 100644 --- a/src/app/data_farmer.rs +++ b/src/app/data_farmer.rs @@ -49,7 +49,7 @@ pub struct DataCollection { pub network_harvest: network::NetworkHarvest, pub memory_harvest: mem::MemHarvest, pub swap_harvest: mem::MemHarvest, - pub cpu_harvest: cpu::CPUHarvest, + pub cpu_harvest: cpu::CpuHarvest, pub process_harvest: Vec, pub disk_harvest: Vec, pub io_harvest: disks::IOHarvest, @@ -67,7 +67,7 @@ impl Default for DataCollection { network_harvest: network::NetworkHarvest::default(), memory_harvest: mem::MemHarvest::default(), swap_harvest: mem::MemHarvest::default(), - cpu_harvest: cpu::CPUHarvest::default(), + cpu_harvest: cpu::CpuHarvest::default(), process_harvest: Vec::default(), disk_harvest: Vec::default(), io_harvest: disks::IOHarvest::default(), @@ -84,7 +84,7 @@ impl DataCollection { self.network_harvest = network::NetworkHarvest::default(); self.memory_harvest = mem::MemHarvest::default(); self.swap_harvest = mem::MemHarvest::default(); - self.cpu_harvest = cpu::CPUHarvest::default(); + self.cpu_harvest = cpu::CpuHarvest::default(); self.process_harvest = Vec::default(); self.disk_harvest = Vec::default(); self.io_harvest = disks::IOHarvest::default(); @@ -205,7 +205,7 @@ impl DataCollection { self.network_harvest = network.clone(); } - fn eat_cpu(&mut self, cpu: &[cpu::CPUData], new_entry: &mut TimedData) { + fn eat_cpu(&mut self, cpu: &[cpu::CpuData], new_entry: &mut TimedData) { // Note this only pre-calculates the data points - the names will be // within the local copy of cpu_harvest. Since it's all sequential // it probably doesn't matter anyways. diff --git a/src/app/data_harvester.rs b/src/app/data_harvester.rs index c699365a..b5b61194 100644 --- a/src/app/data_harvester.rs +++ b/src/app/data_harvester.rs @@ -24,7 +24,7 @@ pub mod temperature; #[derive(Clone, Debug)] pub struct Data { pub last_collection_time: Instant, - pub cpu: Option, + pub cpu: Option, pub memory: Option, pub swap: Option, pub temperature_sensors: Option>, diff --git a/src/app/data_harvester/cpu.rs b/src/app/data_harvester/cpu.rs index 9a3f48db..56a57fd5 100644 --- a/src/app/data_harvester/cpu.rs +++ b/src/app/data_harvester/cpu.rs @@ -1,27 +1,27 @@ use sysinfo::{ProcessorExt, System, SystemExt}; #[derive(Default, Debug, Clone)] -pub struct CPUData { +pub struct CpuData { pub cpu_name: String, pub cpu_usage: f64, } -pub type CPUHarvest = Vec; +pub type CpuHarvest = Vec; -pub fn get_cpu_data_list(sys: &System, show_average_cpu: bool) -> CPUHarvest { +pub fn get_cpu_data_list(sys: &System, show_average_cpu: bool) -> CpuHarvest { let cpu_data = sys.get_processors(); let avg_cpu_usage = sys.get_global_processor_info().get_cpu_usage(); let mut cpu_vec = vec![]; if show_average_cpu { - cpu_vec.push(CPUData { + cpu_vec.push(CpuData { cpu_name: "AVG".to_string(), cpu_usage: avg_cpu_usage as f64, }); } for (itx, cpu) in cpu_data.iter().enumerate() { - cpu_vec.push(CPUData { + cpu_vec.push(CpuData { cpu_name: format!("CPU{}", itx), cpu_usage: f64::from(cpu.get_cpu_usage()), }); diff --git a/src/app/data_harvester/processes.rs b/src/app/data_harvester/processes.rs index bc6a2d40..03fc898e 100644 --- a/src/app/data_harvester/processes.rs +++ b/src/app/data_harvester/processes.rs @@ -12,17 +12,48 @@ use std::{ #[cfg(not(target_os = "linux"))] use sysinfo::{ProcessExt, ProcessorExt, System, SystemExt}; -#[derive(Clone)] +// TODO: Add value so we know if it's sorted ascending or descending by default? +#[derive(Clone, PartialEq, Eq, Hash, Debug)] pub enum ProcessSorting { - CPU, - MEM, - PID, - IDENTIFIER, + CpuPercent, + Mem, + MemPercent, + Pid, + ProcessName, + Command, + ReadPerSecond, + WritePerSecond, + TotalRead, + TotalWrite, + State, +} + +impl std::fmt::Display for ProcessSorting { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use ProcessSorting::*; + write!( + f, + "{}", + match &self { + CpuPercent => "CPU%", + MemPercent => "Mem%", + Mem => "Mem", + ReadPerSecond => "R/s", + WritePerSecond => "W/s", + TotalRead => "Read", + TotalWrite => "Write", + State => "State", + ProcessName => "Name", + Command => "Command", + Pid => "PID", + } + ) + } } impl Default for ProcessSorting { fn default() -> Self { - ProcessSorting::CPU + ProcessSorting::CpuPercent } } diff --git a/src/app/layout_manager.rs b/src/app/layout_manager.rs index 739b732b..9ce89d7c 100644 --- a/src/app/layout_manager.rs +++ b/src/app/layout_manager.rs @@ -541,7 +541,7 @@ impl BottomLayout { .widget_id(4) .up_neighbour(Some(100)) .left_neighbour(Some(8)) - .right_neighbour(Some(DEFAULT_WIDGET_ID)) + .right_neighbour(Some(DEFAULT_WIDGET_ID + 2)) .build()]) .build()]) .build(), @@ -550,15 +550,29 @@ impl BottomLayout { .children(vec![ BottomColRow::builder() .canvas_handle_height(true) - .children(vec![BottomWidget::builder() - .canvas_handle_width(true) - .widget_type(BottomWidgetType::Proc) - .widget_id(DEFAULT_WIDGET_ID) - .up_neighbour(Some(100)) - .down_neighbour(Some(DEFAULT_WIDGET_ID + 1)) - .left_neighbour(Some(4)) - .right_neighbour(Some(8)) - .build()]) + .total_widget_ratio(3) + .children(vec![ + BottomWidget::builder() + .canvas_handle_width(true) + .widget_type(BottomWidgetType::ProcSort) + .widget_id(DEFAULT_WIDGET_ID + 2) + .up_neighbour(Some(100)) + .down_neighbour(Some(DEFAULT_WIDGET_ID + 1)) + .left_neighbour(Some(4)) + .right_neighbour(Some(DEFAULT_WIDGET_ID)) + .width_ratio(1) + .build(), + BottomWidget::builder() + .canvas_handle_width(true) + .widget_type(BottomWidgetType::Proc) + .widget_id(DEFAULT_WIDGET_ID) + .up_neighbour(Some(100)) + .down_neighbour(Some(DEFAULT_WIDGET_ID + 1)) + .left_neighbour(Some(DEFAULT_WIDGET_ID + 2)) + .right_neighbour(Some(7)) + .width_ratio(2) + .build(), + ]) .build(), BottomColRow::builder() .canvas_handle_height(true) @@ -614,7 +628,7 @@ impl BottomLayout { .widget_id(4) .up_neighbour(Some(100)) .left_neighbour(Some(7)) - .right_neighbour(Some(DEFAULT_WIDGET_ID)) + .right_neighbour(Some(DEFAULT_WIDGET_ID + 2)) .build()]) .build()]) .build(), @@ -623,15 +637,26 @@ impl BottomLayout { .children(vec![ BottomColRow::builder() .canvas_handle_height(true) - .children(vec![BottomWidget::builder() - .canvas_handle_width(true) - .widget_type(BottomWidgetType::Proc) - .widget_id(DEFAULT_WIDGET_ID) - .up_neighbour(Some(100)) - .down_neighbour(Some(DEFAULT_WIDGET_ID + 1)) - .left_neighbour(Some(4)) - .right_neighbour(Some(7)) - .build()]) + .children(vec![ + BottomWidget::builder() + .canvas_handle_width(true) + .widget_type(BottomWidgetType::ProcSort) + .widget_id(DEFAULT_WIDGET_ID + 2) + .up_neighbour(Some(100)) + .down_neighbour(Some(DEFAULT_WIDGET_ID + 1)) + .left_neighbour(Some(4)) + .right_neighbour(Some(DEFAULT_WIDGET_ID)) + .build(), + BottomWidget::builder() + .canvas_handle_width(true) + .widget_type(BottomWidgetType::Proc) + .widget_id(DEFAULT_WIDGET_ID) + .up_neighbour(Some(100)) + .down_neighbour(Some(DEFAULT_WIDGET_ID + 1)) + .left_neighbour(Some(DEFAULT_WIDGET_ID + 2)) + .right_neighbour(Some(7)) + .build(), + ]) .build(), BottomColRow::builder() .canvas_handle_height(true) @@ -730,180 +755,6 @@ impl BottomLayout { ], } } - - pub fn init_default(left_legend: bool, use_battery: bool) -> Self { - let cpu_layout = if left_legend { - vec![ - BottomWidget::builder() - .width_ratio(3) - .widget_type(BottomWidgetType::CpuLegend) - .widget_id(2) - .down_neighbour(Some(11)) - .right_neighbour(Some(1)) - .canvas_handle_width(true) - .build(), - BottomWidget::builder() - .width_ratio(17) - .widget_type(BottomWidgetType::Cpu) - .widget_id(1) - .down_neighbour(Some(12)) - .left_neighbour(Some(2)) - .right_neighbour(if use_battery { Some(99) } else { None }) - .flex_grow(true) - .build(), - ] - } else { - vec![ - BottomWidget::builder() - .width_ratio(17) - .widget_type(BottomWidgetType::Cpu) - .widget_id(1) - .down_neighbour(Some(11)) - .right_neighbour(Some(2)) - .flex_grow(true) - .build(), - BottomWidget::builder() - .width_ratio(3) - .widget_type(BottomWidgetType::CpuLegend) - .widget_id(2) - .down_neighbour(Some(12)) - .left_neighbour(Some(1)) - .right_neighbour(if use_battery { Some(99) } else { None }) - .canvas_handle_width(true) - .build(), - ] - }; - - let first_row_layout = if use_battery { - vec![ - BottomCol::builder() - .col_width_ratio(2) - .children(vec![BottomColRow::builder() - .total_widget_ratio(20) - .children(cpu_layout) - .build()]) - .build(), - BottomCol::builder() - .col_width_ratio(1) - .children(vec![BottomColRow::builder() - .children(vec![BottomWidget::builder() - .widget_type(BottomWidgetType::Battery) - .widget_id(99) - .down_neighbour(Some(12)) - .left_neighbour(Some(if left_legend { 1 } else { 2 })) - .canvas_handle_width(true) - .build()]) - .build()]) - .build(), - ] - } else { - vec![BottomCol::builder() - .children(vec![BottomColRow::builder() - .total_widget_ratio(20) - .children(cpu_layout) - .build()]) - .build()] - }; - - BottomLayout { - total_row_height_ratio: 100, - rows: vec![ - BottomRow::builder() - .row_height_ratio(30) - .total_col_ratio(if use_battery { 3 } else { 1 }) - .children(first_row_layout) - .build(), - BottomRow::builder() - .total_col_ratio(7) - .row_height_ratio(40) - .children(vec![ - BottomCol::builder() - .col_width_ratio(4) - .children(vec![BottomColRow::builder() - .children(vec![BottomWidget::builder() - .widget_type(BottomWidgetType::Mem) - .widget_id(11) - .right_neighbour(Some(12)) - .up_neighbour(Some(1)) - .down_neighbour(Some(21)) - .build()]) - .build()]) - .build(), - BottomCol::builder() - .total_col_row_ratio(2) - .col_width_ratio(3) - .children(vec![ - BottomColRow::builder() - .col_row_height_ratio(1) - .total_widget_ratio(2) - .children(vec![BottomWidget::builder() - .widget_type(BottomWidgetType::Temp) - .widget_id(12) - .left_neighbour(Some(11)) - .up_neighbour(Some(1)) - .down_neighbour(Some(13)) - .build()]) - .build(), - BottomColRow::builder() - .col_row_height_ratio(1) - .children(vec![BottomWidget::builder() - .widget_type(BottomWidgetType::Disk) - .widget_id(13) - .left_neighbour(Some(11)) - .up_neighbour(Some(12)) - .down_neighbour(Some(DEFAULT_WIDGET_ID)) - .build()]) - .build(), - ]) - .build(), - ]) - .build(), - BottomRow::builder() - .total_col_ratio(2) - .row_height_ratio(30) - .children(vec![ - BottomCol::builder() - .children(vec![BottomColRow::builder() - .col_row_height_ratio(1) - .children(vec![BottomWidget::builder() - .widget_type(BottomWidgetType::Net) - .widget_id(21) - .right_neighbour(Some(DEFAULT_WIDGET_ID)) - .up_neighbour(Some(11)) - .build()]) - .build()]) - .build(), - BottomCol::builder() - .total_col_row_ratio(2) - .children(vec![ - BottomColRow::builder() - .col_row_height_ratio(1) - .children(vec![BottomWidget::builder() - .widget_type(BottomWidgetType::Proc) - .widget_id(DEFAULT_WIDGET_ID) - .left_neighbour(Some(21)) - .up_neighbour(Some(13)) - .down_neighbour(Some(DEFAULT_WIDGET_ID + 1)) - .build()]) - .flex_grow(true) - .build(), - BottomColRow::builder() - .col_row_height_ratio(1) - .children(vec![BottomWidget::builder() - .widget_type(BottomWidgetType::ProcSearch) - .widget_id(DEFAULT_WIDGET_ID + 1) - .up_neighbour(Some(DEFAULT_WIDGET_ID)) - .left_neighbour(Some(21)) - .build()]) - .canvas_handle_height(true) - .build(), - ]) - .build(), - ]) - .build(), - ], - } - } } /// Represents a single row in the layout. @@ -961,6 +812,25 @@ pub struct BottomColRow { pub flex_grow: bool, } +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum WidgetDirection { + Left, + Right, + Up, + Down, +} + +impl WidgetDirection { + pub fn is_opposite(&self, other_direction: &WidgetDirection) -> bool { + match &self { + WidgetDirection::Left => *other_direction == WidgetDirection::Right, + WidgetDirection::Right => *other_direction == WidgetDirection::Left, + WidgetDirection::Up => *other_direction == WidgetDirection::Down, + WidgetDirection::Down => *other_direction == WidgetDirection::Up, + } + } +} + /// Represents a single widget. #[derive(Debug, Default, Clone, TypedBuilder)] pub struct BottomWidget { @@ -982,11 +852,17 @@ pub struct BottomWidget { #[builder(default = None)] pub down_neighbour: Option, + /// If set to true, the canvas will override any ratios. #[builder(default = false)] pub canvas_handle_width: bool, + /// Whether we want this widget to take up all available room (and ignore any ratios). #[builder(default = false)] pub flex_grow: bool, + + /// The value is the direction to bounce, as well as the parent offset. + #[builder(default = None)] + pub parent_reflector: Option<(WidgetDirection, u64)>, } #[derive(Debug, Clone, Eq, PartialEq, Hash)] @@ -998,6 +874,7 @@ pub enum BottomWidgetType { Net, Proc, ProcSearch, + ProcSort, Temp, Disk, BasicCpu, @@ -1011,7 +888,7 @@ impl BottomWidgetType { pub fn is_widget_table(&self) -> bool { use BottomWidgetType::*; match self { - Disk | Proc | Temp | CpuLegend => true, + Disk | Proc | ProcSort | Temp | CpuLegend => true, _ => false, } } diff --git a/src/app/states.rs b/src/app/states.rs index 89de370d..3da1c84c 100644 --- a/src/app/states.rs +++ b/src/app/states.rs @@ -7,27 +7,27 @@ use tui::widgets::TableState; use crate::{ app::{layout_manager::BottomWidgetType, query::*}, constants, - data_harvester::processes, + data_harvester::processes::{self, ProcessSorting}, }; #[derive(Debug)] pub enum ScrollDirection { // UP means scrolling up --- this usually DECREMENTS - UP, + Up, // DOWN means scrolling down --- this usually INCREMENTS - DOWN, + Down, } impl Default for ScrollDirection { fn default() -> Self { - ScrollDirection::DOWN + ScrollDirection::Down } } #[derive(Debug)] pub enum CursorDirection { - LEFT, - RIGHT, + Left, + Right, } /// AppScrollWidgetState deals with fields for a scrollable app's current state. @@ -85,7 +85,7 @@ impl Default for AppSearchState { is_invalid_search: false, is_blank_search: true, grapheme_cursor: GraphemeCursor::new(0, 0, true), - cursor_direction: CursorDirection::RIGHT, + cursor_direction: CursorDirection::Right, cursor_bar: 0, char_cursor_position: 0, query: None, @@ -141,15 +141,183 @@ impl ProcessSearchState { } } +pub struct ColumnInfo { + pub enabled: bool, + pub shortcut: Option<&'static str>, +} + +pub struct ProcColumn { + pub ordered_columns: Vec, + pub column_mapping: HashMap, + pub longest_header_len: u16, + pub column_state: TableState, + pub scroll_direction: ScrollDirection, + pub current_scroll_position: usize, + pub previous_scroll_position: usize, + pub backup_prev_scroll_position: usize, +} + +impl Default for ProcColumn { + fn default() -> Self { + use ProcessSorting::*; + let ordered_columns = vec![ + Pid, + ProcessName, + Command, + CpuPercent, + MemPercent, + ReadPerSecond, + WritePerSecond, + TotalRead, + TotalWrite, + State, + ]; + + let mut column_mapping = HashMap::new(); + let mut longest_header_len = 0; + for column in ordered_columns.clone() { + longest_header_len = std::cmp::max(longest_header_len, column.to_string().len()); + match column { + CpuPercent => { + column_mapping.insert( + column, + ColumnInfo { + enabled: true, + shortcut: Some("c"), + }, + ); + } + MemPercent | Mem => { + column_mapping.insert( + column, + ColumnInfo { + enabled: true, + shortcut: Some("m"), + }, + ); + } + ProcessName => { + column_mapping.insert( + column, + ColumnInfo { + enabled: true, + shortcut: Some("n"), + }, + ); + } + Command => { + column_mapping.insert( + column, + ColumnInfo { + enabled: false, + shortcut: Some("n"), + }, + ); + } + Pid => { + column_mapping.insert( + column, + ColumnInfo { + enabled: true, + shortcut: Some("p"), + }, + ); + } + _ => { + column_mapping.insert( + column, + ColumnInfo { + enabled: true, + shortcut: None, + }, + ); + } + } + } + let longest_header_len = longest_header_len as u16; + + ProcColumn { + ordered_columns, + column_mapping, + longest_header_len, + column_state: TableState::default(), + scroll_direction: ScrollDirection::default(), + current_scroll_position: 0, + previous_scroll_position: 0, + backup_prev_scroll_position: 0, + } + } +} + +impl ProcColumn { + pub fn get_enabled_columns_len(&self) -> usize { + self.ordered_columns + .iter() + .filter_map(|column_type| { + if self.column_mapping.get(&column_type).unwrap().enabled { + Some(1) + } else { + None + } + }) + .sum() + } + + pub fn set_to_sorted_index(&mut self, proc_sorting_type: &ProcessSorting) { + // TODO [Custom Columns]: If we add custom columns, this may be needed! Since column indices will change, this runs the risk of OOB. So, when you change columns, CALL THIS AND ADAPT! + let mut true_index = 0; + for column in &self.ordered_columns { + if *column == *proc_sorting_type { + break; + } + if self.column_mapping.get(column).unwrap().enabled { + true_index += 1; + } + } + + self.current_scroll_position = true_index; + self.backup_prev_scroll_position = self.previous_scroll_position; + } + + pub fn get_column_headers( + &self, proc_sorting_type: &ProcessSorting, sort_reverse: bool, + ) -> Vec { + // TODO: Gonna have to figure out how to do left/right GUI notation if we add it. + self.ordered_columns + .iter() + .filter_map(|column_type| { + let mapping = self.column_mapping.get(&column_type).unwrap(); + let mut command_str = String::default(); + if let Some(command) = mapping.shortcut { + command_str = format!("({})", command); + } + + if mapping.enabled { + Some(if proc_sorting_type == column_type { + column_type.to_string() + + command_str.as_str() + + if sort_reverse { "▼" } else { "▲" } + } else { + column_type.to_string() + command_str.as_str() + }) + } else { + None + } + }) + .collect() + } +} + pub struct ProcWidgetState { pub process_search_state: ProcessSearchState, pub is_grouped: bool, pub scroll_state: AppScrollWidgetState, pub process_sorting_type: processes::ProcessSorting, pub process_sorting_reverse: bool, - pub is_using_full_path: bool, + pub is_using_command: bool, pub current_column_index: usize, - pub num_columns: usize, + pub is_sort_open: bool, + pub columns: ProcColumn, } impl ProcWidgetState { @@ -168,18 +336,79 @@ impl ProcWidgetState { process_search_state.search_toggle_regex(); } + let process_sorting_type = processes::ProcessSorting::CpuPercent; + + // TODO: If we add customizable columns, this should pull from config + let mut columns = ProcColumn::default(); + columns.set_to_sorted_index(&process_sorting_type); + ProcWidgetState { process_search_state, is_grouped, scroll_state: AppScrollWidgetState::default(), - process_sorting_type: processes::ProcessSorting::CPU, + process_sorting_type, process_sorting_reverse: true, - is_using_full_path: false, + is_using_command: false, current_column_index: 0, - num_columns: 1, + is_sort_open: false, + columns, } } + /// Updates sorting when using the column list. + /// ...this really should be part of the ProcColumn struct (along with the sorting fields), + /// but I'm too lazy. + /// + /// Sorry, future me, you're gonna have to refactor this later. Too busy getting + /// the feature to work in the first place! :) + pub fn update_sorting_with_columns(&mut self) { + let mut true_index = 0; + let mut enabled_index = 0; + let target_itx = self.columns.current_scroll_position; + for column in &self.columns.ordered_columns { + let enabled = self.columns.column_mapping.get(column).unwrap().enabled; + if enabled_index == target_itx && enabled { + break; + } + if enabled { + enabled_index += 1; + } + true_index += 1; + } + + if let Some(new_sort_type) = self.columns.ordered_columns.get(true_index) { + if *new_sort_type == self.process_sorting_type { + // Just reverse the search if we're reselecting! + self.process_sorting_reverse = !(self.process_sorting_reverse); + } else { + self.process_sorting_type = new_sort_type.clone(); + match self.process_sorting_type { + ProcessSorting::State + | ProcessSorting::Pid + | ProcessSorting::ProcessName + | ProcessSorting::Command => { + // Also invert anything that uses alphabetical sorting by default. + self.process_sorting_reverse = false; + } + _ => {} + } + } + } + } + + pub fn toggle_command_and_name(&mut self, is_using_command: bool) { + self.columns + .column_mapping + .get_mut(&ProcessSorting::ProcessName) + .unwrap() + .enabled = !is_using_command; + self.columns + .column_mapping + .get_mut(&ProcessSorting::Command) + .unwrap() + .enabled = is_using_command; + } + pub fn get_cursor_position(&self) -> usize { self.process_search_state .search_state diff --git a/src/canvas.rs b/src/canvas.rs index 4e846bf6..8ff9c31b 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -356,20 +356,16 @@ impl Painter { app_state.current_widget.widget_id, false, ), - Proc => self.draw_process_and_search( - &mut f, - app_state, - rect[0], - true, - app_state.current_widget.widget_id, - ), - ProcSearch => self.draw_process_and_search( - &mut f, - app_state, - rect[0], - true, - app_state.current_widget.widget_id - 1, - ), + proc_type @ Proc | proc_type @ ProcSearch | proc_type @ ProcSort => { + let widget_id = app_state.current_widget.widget_id + - match proc_type { + ProcSearch => 1, + ProcSort => 2, + _ => 0, + }; + + self.draw_process_features(&mut f, app_state, rect[0], true, widget_id); + } Battery => self.draw_battery_display( &mut f, app_state, @@ -430,13 +426,20 @@ impl Painter { false, widget_id, ), - Proc => self.draw_process_and_search( - &mut f, - app_state, - vertical_chunks[4], - false, - widget_id, - ), + Proc | ProcSort => { + let wid = widget_id + - match basic_table_widget_state.currently_displayed_widget_type { + ProcSort => 2, + _ => 0, + }; + self.draw_process_features( + &mut f, + app_state, + vertical_chunks[4], + false, + wid, + ); + } Temp => self.draw_temp_table( &mut f, app_state, @@ -575,7 +578,7 @@ impl Painter { Disk => { self.draw_disk_table(f, app_state, *widget_draw_loc, true, widget.widget_id) } - Proc => self.draw_process_and_search( + Proc => self.draw_process_features( f, app_state, *widget_draw_loc, diff --git a/src/canvas/dialogs/help_dialog.rs b/src/canvas/dialogs/help_dialog.rs index ae7d85b6..138622ce 100644 --- a/src/canvas/dialogs/help_dialog.rs +++ b/src/canvas/dialogs/help_dialog.rs @@ -72,7 +72,7 @@ impl HelpDialog for Painter { app_state.help_dialog_state.scroll_state.max_scroll_index = (self.styled_help_text.len() as u16 - + (constants::HELP_TEXT.len() as u16 - 3) + + (constants::HELP_TEXT.len() as u16 - 4) + overflow_buffer) .saturating_sub(draw_loc.height); diff --git a/src/canvas/drawing_utils.rs b/src/canvas/drawing_utils.rs index 410b8916..283d8538 100644 --- a/src/canvas/drawing_utils.rs +++ b/src/canvas/drawing_utils.rs @@ -85,7 +85,7 @@ pub fn get_search_start_position( } match cursor_direction { - app::CursorDirection::RIGHT => { + app::CursorDirection::Right => { if current_cursor_position < *cursor_bar + num_columns { // If, using previous_scrolled_position, we can see the element // (so within that and + num_rows) just reuse the current previously scrolled position @@ -100,7 +100,7 @@ pub fn get_search_start_position( 0 } } - app::CursorDirection::LEFT => { + app::CursorDirection::Left => { if current_cursor_position <= *cursor_bar { // If it's past the first element, then show from that element downwards *cursor_bar = current_cursor_position; @@ -125,7 +125,7 @@ pub fn get_start_position( } match scroll_direction { - app::ScrollDirection::DOWN => { + app::ScrollDirection::Down => { if currently_selected_position < *scroll_position_bar + num_rows { // If, using previous_scrolled_position, we can see the element // (so within that and + num_rows) just reuse the current previously scrolled position @@ -140,7 +140,7 @@ pub fn get_start_position( 0 } } - app::ScrollDirection::UP => { + app::ScrollDirection::Up => { if currently_selected_position <= *scroll_position_bar { // If it's past the first element, then show from that element downwards *scroll_position_bar = currently_selected_position; diff --git a/src/canvas/widgets/basic_table_arrows.rs b/src/canvas/widgets/basic_table_arrows.rs index 5e5c306c..18beab83 100644 --- a/src/canvas/widgets/basic_table_arrows.rs +++ b/src/canvas/widgets/basic_table_arrows.rs @@ -24,6 +24,15 @@ impl BasicTableArrows for Painter { fn draw_basic_table_arrows( &self, f: &mut Frame<'_, B>, app_state: &App, draw_loc: Rect, current_table: &BottomWidget, ) { + let current_table = if let BottomWidgetType::ProcSort = current_table.widget_type { + current_table + .right_neighbour + .map(|id| app_state.widget_map.get(&id).unwrap()) + .unwrap() + } else { + current_table + }; + // Effectively a paragraph with a ton of spacing let (left_table, right_table) = ( { @@ -33,7 +42,21 @@ impl BasicTableArrows for Painter { app_state .widget_map .get(&left_widget_id) - .map(|left_widget| &left_widget.widget_type) + .map(|left_widget| { + if left_widget.widget_type == BottomWidgetType::ProcSort { + left_widget + .left_neighbour + .map(|left_left_widget_id| { + app_state.widget_map.get(&left_left_widget_id).map( + |left_left_widget| &left_left_widget.widget_type, + ) + }) + .unwrap_or_else(|| Some(&BottomWidgetType::Temp)) + .unwrap_or_else(|| &BottomWidgetType::Temp) + } else { + &left_widget.widget_type + } + }) .unwrap_or_else(|| &BottomWidgetType::Temp) }) .unwrap_or_else(|| &BottomWidgetType::Temp) @@ -45,7 +68,23 @@ impl BasicTableArrows for Painter { app_state .widget_map .get(&right_widget_id) - .map(|right_widget| &right_widget.widget_type) + .map(|right_widget| { + if right_widget.widget_type == BottomWidgetType::ProcSort { + right_widget + .right_neighbour + .map(|right_right_widget_id| { + app_state.widget_map.get(&right_right_widget_id).map( + |right_right_widget| { + &right_right_widget.widget_type + }, + ) + }) + .unwrap_or_else(|| Some(&BottomWidgetType::Disk)) + .unwrap_or_else(|| &BottomWidgetType::Disk) + } else { + &right_widget.widget_type + } + }) .unwrap_or_else(|| &BottomWidgetType::Disk) }) .unwrap_or_else(|| &BottomWidgetType::Disk) diff --git a/src/canvas/widgets/cpu_graph.rs b/src/canvas/widgets/cpu_graph.rs index 097d91b8..a01b7f5f 100644 --- a/src/canvas/widgets/cpu_graph.rs +++ b/src/canvas/widgets/cpu_graph.rs @@ -3,7 +3,7 @@ use std::borrow::Cow; use std::cmp::max; use crate::{ - app::App, + app::{layout_manager::WidgetDirection, App}, canvas::{ drawing_utils::{get_start_position, get_variable_intrinsic_widths}, Painter, @@ -57,9 +57,9 @@ impl CpuGraphWidget for Painter { // Skip drawing legend if app_state.current_widget.widget_id == (widget_id + 1) { if app_state.app_config_fields.left_legend { - app_state.move_widget_selection_right(); + app_state.move_widget_selection(&WidgetDirection::Right); } else { - app_state.move_widget_selection_left(); + app_state.move_widget_selection(&WidgetDirection::Left); } } self.draw_cpu_graph(f, app_state, draw_loc, widget_id); diff --git a/src/canvas/widgets/process_table.rs b/src/canvas/widgets/process_table.rs index 2f041a16..54f0a5ad 100644 --- a/src/canvas/widgets/process_table.rs +++ b/src/canvas/widgets/process_table.rs @@ -1,5 +1,5 @@ use crate::{ - app::{self, App}, + app::App, canvas::{ drawing_utils::{ get_search_start_position, get_start_position, get_variable_intrinsic_widths, @@ -14,43 +14,68 @@ use tui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, terminal::Frame, text::{Span, Spans}, - widgets::{Block, Borders, Paragraph, Row, Table, Wrap}, + widgets::{Block, Borders, Paragraph, Row, Table}, }; use unicode_segmentation::{GraphemeIndices, UnicodeSegmentation}; use unicode_width::UnicodeWidthStr; pub trait ProcessTableWidget { - fn draw_process_and_search( + /// Draws and handles all process-related drawing. Use this. + /// - `widget_id` here represents the widget ID of the process widget itself! + fn draw_process_features( &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool, widget_id: u64, ); + /// Draws the process sort box. + /// - `widget_id` represents the widget ID of the process widget itself. + /// + /// This should not be directly called. fn draw_processes_table( &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool, widget_id: u64, ); + /// Draws the process sort box. + /// - `widget_id` represents the widget ID of the search box itself --- NOT the process widget + /// state that is stored. + /// + /// This should not be directly called. fn draw_search_field( &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool, widget_id: u64, ); + + /// Draws the process sort box. + /// - `widget_id` represents the widget ID of the sort box itself --- NOT the process widget + /// state that is stored. + /// + /// This should not be directly called. + fn draw_process_sort( + &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool, + widget_id: u64, + ); } impl ProcessTableWidget for Painter { - fn draw_process_and_search( + fn draw_process_features( &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool, widget_id: u64, ) { if let Some(process_widget_state) = app_state.proc_state.widget_states.get(&widget_id) { let search_height = if draw_border { 5 } else { 3 }; + let is_sort_open = process_widget_state.is_sort_open; + let header_len = process_widget_state.columns.longest_header_len; + + let mut proc_draw_loc = draw_loc; if process_widget_state.is_search_enabled() { let processes_chunk = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Min(0), Constraint::Length(search_height)].as_ref()) .split(draw_loc); + proc_draw_loc = processes_chunk[0]; - self.draw_processes_table(f, app_state, processes_chunk[0], draw_border, widget_id); self.draw_search_field( f, app_state, @@ -58,9 +83,25 @@ impl ProcessTableWidget for Painter { draw_border, widget_id + 1, ); - } else { - self.draw_processes_table(f, app_state, draw_loc, draw_border, widget_id); } + + if is_sort_open { + let processes_chunk = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Length(header_len + 4), Constraint::Min(0)].as_ref()) + .split(proc_draw_loc); + proc_draw_loc = processes_chunk[1]; + + self.draw_process_sort( + f, + app_state, + processes_chunk[0], + draw_border, + widget_id + 2, + ); + } + + self.draw_processes_table(f, app_state, proc_draw_loc, draw_border, widget_id); } } @@ -127,71 +168,17 @@ impl ProcessTableWidget for Painter { process.write_per_sec.to_string(), process.total_read.to_string(), process.total_write.to_string(), - process.process_states.to_string(), + process.process_state.to_string(), ] .into_iter(), ) }); - use app::data_harvester::processes::ProcessSorting; - let mut pid_or_count = if proc_widget_state.is_grouped { - "Count" - } else { - "PID(p)" - } - .to_string(); - let mut identifier = if proc_widget_state.is_using_full_path { - "Command(n)".to_string() - } else { - "Name(n)".to_string() - }; - let mut cpu = "CPU%(c)".to_string(); - let mut mem = "Mem%(m)".to_string(); - let rps = "R/s".to_string(); - let wps = "W/s".to_string(); - let total_read = "Read".to_string(); - let total_write = "Write".to_string(); - let process_state = "State ".to_string(); + let process_headers = proc_widget_state.columns.get_column_headers( + &proc_widget_state.process_sorting_type, + proc_widget_state.process_sorting_reverse, + ); - let direction_val = if proc_widget_state.process_sorting_reverse { - "▼".to_string() - } else { - "▲".to_string() - }; - - match proc_widget_state.process_sorting_type { - ProcessSorting::CPU => cpu += &direction_val, - ProcessSorting::MEM => mem += &direction_val, - ProcessSorting::PID => pid_or_count += &direction_val, - ProcessSorting::IDENTIFIER => identifier += &direction_val, - }; - - // TODO: Gonna have to figure out how to do left/right GUI notation. - let process_headers = if proc_widget_state.is_grouped { - vec![ - pid_or_count, - identifier, - cpu, - mem, - rps, - wps, - total_read, - total_write, - ] - } else { - vec![ - pid_or_count, - identifier, - cpu, - mem, - rps, - wps, - total_read, - total_write, - process_state, - ] - }; - proc_widget_state.num_columns = process_headers.len(); let process_headers_lens: Vec = process_headers .iter() .map(|entry| entry.len()) @@ -202,12 +189,12 @@ impl ProcessTableWidget for Painter { // TODO: This is a ugly work-around for now. let width_ratios = if proc_widget_state.is_grouped { - if proc_widget_state.is_using_full_path { + if proc_widget_state.is_using_command { vec![0.05, 0.7, 0.05, 0.05, 0.0375, 0.0375, 0.0375, 0.0375] } else { vec![0.1, 0.2, 0.1, 0.1, 0.1, 0.1, 0.15, 0.15] } - } else if proc_widget_state.is_using_full_path { + } else if proc_widget_state.is_using_command { vec![0.05, 0.7, 0.05, 0.05, 0.03, 0.03, 0.03, 0.03] } else { vec![0.1, 0.2, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1] @@ -235,6 +222,7 @@ impl ProcessTableWidget for Painter { .process_search_state .search_state .is_enabled + && !proc_widget_state.is_sort_open { const TITLE_BASE: &str = " Processes ── Esc to go back "; Span::styled( @@ -502,10 +490,108 @@ impl ProcessTableWidget for Painter { Paragraph::new(search_text) .block(process_search_block) .style(self.colours.text_style) - .alignment(Alignment::Left) - .wrap(Wrap { trim: false }), + .alignment(Alignment::Left), margined_draw_loc[0], ); } } + + fn draw_process_sort( + &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool, + widget_id: u64, + ) { + let is_on_widget = widget_id == app_state.current_widget.widget_id; + + if let Some(proc_widget_state) = + app_state.proc_state.widget_states.get_mut(&(widget_id - 2)) + { + let current_scroll_position = proc_widget_state.columns.current_scroll_position; + let sort_string = proc_widget_state + .columns + .ordered_columns + .iter() + .filter(|column_type| { + proc_widget_state + .columns + .column_mapping + .get(&column_type) + .unwrap() + .enabled + }) + .enumerate() + .map(|(itx, column_type)| { + if current_scroll_position == itx { + ( + column_type.to_string(), + self.colours.currently_selected_text_style, + ) + } else { + (column_type.to_string(), self.colours.text_style) + } + }) + .collect::>(); + + let position = get_start_position( + usize::from(draw_loc.height.saturating_sub(self.table_height_offset)), + &proc_widget_state.columns.scroll_direction, + &mut proc_widget_state.columns.previous_scroll_position, + current_scroll_position, + app_state.is_force_redraw, + ); + + // Sanity check + let start_position = if position >= sort_string.len() { + sort_string.len().saturating_sub(1) + } else { + position + }; + + let sliced_vec = &sort_string[start_position..]; + + let sort_options = sliced_vec + .iter() + .map(|(column, style)| Row::StyledData(vec![column].into_iter(), *style)); + + let column_state = &mut proc_widget_state.columns.column_state; + let current_border_style = if proc_widget_state + .process_search_state + .search_state + .is_invalid_search + { + self.colours.invalid_query_style + } else if is_on_widget { + self.colours.highlighted_border_style + } else { + self.colours.border_style + }; + + let process_sort_block = if draw_border { + Block::default() + .borders(Borders::ALL) + .border_style(current_border_style) + } else if is_on_widget { + Block::default() + .borders(*SIDE_BORDERS) + .border_style(current_border_style) + } else { + Block::default().borders(Borders::NONE) + }; + + let margined_draw_loc = Layout::default() + .constraints([Constraint::Percentage(100)].as_ref()) + .horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 }) + .direction(Direction::Horizontal) + .split(draw_loc); + + f.render_stateful_widget( + Table::new(["Sort By"].iter(), sort_options) + .block(process_sort_block) + .header_style(self.colours.table_header_style) + .widths(&[Constraint::Percentage(100)]) + .header_gap(1), + margined_draw_loc[0], + column_state, + ); + } + } } diff --git a/src/constants.rs b/src/constants.rs index d2c2cb4f..b9a6533d 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -42,13 +42,14 @@ lazy_static! { } // Help text -pub const HELP_CONTENTS_TEXT: [&str; 6] = [ +pub const HELP_CONTENTS_TEXT: [&str; 7] = [ "Press the corresponding numbers to jump to the section, or scroll:", "1 - General", "2 - CPU widget", "3 - Process widget", "4 - Process search widget", - "5 - Battery widget", + "5 - Process sort widget", + "6 - Battery widget", ]; pub const GENERAL_HELP_TEXT: [&str; 29] = [ @@ -88,7 +89,9 @@ pub const CPU_HELP_TEXT: [&str; 2] = [ "Mouse scroll Scrolling over an CPU core/average shows only that entry on the chart", ]; -pub const PROCESS_HELP_TEXT: [&str; 9] = [ +// TODO [Help]: Search in help? +// TODO [Help]: Move to using tables for easier formatting? +pub const PROCESS_HELP_TEXT: [&str; 11] = [ "3 - Process widget", "dd Kill the selected process", "c Sort by CPU usage, press again to reverse sorting order", @@ -98,6 +101,8 @@ pub const PROCESS_HELP_TEXT: [&str; 9] = [ "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", ]; pub const SEARCH_HELP_TEXT: [&str; 43] = [ @@ -146,8 +151,17 @@ pub const SEARCH_HELP_TEXT: [&str; 43] = [ "TiB ex: read > 1 tib", ]; +pub const SORT_HELP_TEXT: [&str; 6] = [ + "5 - Sort widget", + "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", +]; + pub const BATTERY_HELP_TEXT: [&str; 3] = [ - "5 - Battery widget", + "6 - Battery widget", "Left Go to previous battery", "Right Go to next battery", ]; @@ -159,10 +173,37 @@ lazy_static! { CPU_HELP_TEXT.to_vec(), PROCESS_HELP_TEXT.to_vec(), SEARCH_HELP_TEXT.to_vec(), + SORT_HELP_TEXT.to_vec(), BATTERY_HELP_TEXT.to_vec(), ]; } +// Default layout +pub const DEFAULT_LAYOUT: &str = r##" +[[row]] + ratio=30 + [[row.child]] + type="cpu" +[[row]] + ratio=40 + [[row.child]] + ratio=4 + type="mem" + [[row.child]] + ratio=3 + [[row.child.child]] + type="temp" + [[row.child.child]] + type="disk" +[[row]] + ratio=30 + [[row.child]] + type="net" + [[row.child]] + type="proc" + default=true +"##; + // Config and flags pub const DEFAULT_CONFIG_FILE_PATH: &str = "bottom/bottom.toml"; diff --git a/src/data_conversion.rs b/src/data_conversion.rs index 200ac998..7218d305 100644 --- a/src/data_conversion.rs +++ b/src/data_conversion.rs @@ -45,7 +45,7 @@ pub struct ConvertedProcessData { pub wps_f64: f64, pub tr_f64: f64, pub tw_f64: f64, - pub process_states: String, + pub process_state: String, } #[derive(Clone, Default, Debug)] @@ -409,7 +409,7 @@ pub fn convert_process_data( wps_f64: process.write_bytes_per_sec as f64, tr_f64: process.total_read_bytes as f64, tw_f64: process.total_write_bytes as f64, - process_states: process.process_state.to_owned(), + process_state: process.process_state.to_owned(), } }) .collect::>(), @@ -469,7 +469,7 @@ pub fn convert_process_data( wps_f64: p.write_per_sec as f64, tr_f64: p.total_read as f64, tw_f64: p.total_write as f64, - process_states: p.process_state, + process_state: p.process_state, } }) .collect::>() diff --git a/src/main.rs b/src/main.rs index 29c7630e..b3624046 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,7 +28,7 @@ use tui::{backend::CrosstermBackend, Terminal}; use app::{ data_harvester::{self, processes::ProcessSorting}, - layout_manager::UsedWidgets, + layout_manager::{UsedWidgets, WidgetDirection}, App, }; use constants::*; @@ -286,15 +286,10 @@ fn handle_key_event_or_break( KeyCode::Tab => app.on_tab(), KeyCode::Backspace => app.on_backspace(), KeyCode::Delete => app.on_delete(), - KeyCode::F(1) => { - app.toggle_ignore_case(); - } - KeyCode::F(2) => { - app.toggle_search_whole_word(); - } - KeyCode::F(3) => { - app.toggle_search_regex(); - } + KeyCode::F(1) => app.toggle_ignore_case(), + KeyCode::F(2) => app.toggle_search_whole_word(), + KeyCode::F(3) => app.toggle_search_regex(), + KeyCode::F(6) => app.toggle_sort(), _ => {} } } else { @@ -315,10 +310,10 @@ fn handle_key_event_or_break( match event.code { KeyCode::Char('f') => app.on_slash(), - KeyCode::Left => app.move_widget_selection_left(), - KeyCode::Right => app.move_widget_selection_right(), - KeyCode::Up => app.move_widget_selection_up(), - KeyCode::Down => app.move_widget_selection_down(), + KeyCode::Left => app.move_widget_selection(&WidgetDirection::Left), + KeyCode::Right => app.move_widget_selection(&WidgetDirection::Right), + KeyCode::Up => app.move_widget_selection(&WidgetDirection::Up), + KeyCode::Down => app.move_widget_selection(&WidgetDirection::Down), KeyCode::Char('r') => { if reset_sender.send(ResetEvent::Reset).is_ok() { app.reset(); @@ -338,10 +333,10 @@ fn handle_key_event_or_break( } } else if let KeyModifiers::SHIFT = event.modifiers { match event.code { - KeyCode::Left => app.move_widget_selection_left(), - KeyCode::Right => app.move_widget_selection_right(), - KeyCode::Up => app.move_widget_selection_up(), - KeyCode::Down => app.move_widget_selection_down(), + KeyCode::Left => app.move_widget_selection(&WidgetDirection::Left), + KeyCode::Right => app.move_widget_selection(&WidgetDirection::Right), + KeyCode::Up => app.move_widget_selection(&WidgetDirection::Up), + KeyCode::Down => app.move_widget_selection(&WidgetDirection::Down), KeyCode::Char(caught_char) => app.on_char_key(caught_char), _ => {} } @@ -607,7 +602,7 @@ fn update_final_process_list(app: &mut App, widget_id: u64) { } else { ProcessGroupingType::Ungrouped }, - if proc_widget_state.is_using_full_path { + if proc_widget_state.is_using_command { ProcessNamingType::Path } else { ProcessNamingType::Name @@ -636,12 +631,12 @@ fn update_final_process_list(app: &mut App, widget_id: u64) { // Quick fix for tab updating the table headers if let Some(proc_widget_state) = app.proc_state.get_mut_widget_state(widget_id) { - if let data_harvester::processes::ProcessSorting::PID = + if let data_harvester::processes::ProcessSorting::Pid = proc_widget_state.process_sorting_type { if proc_widget_state.is_grouped { proc_widget_state.process_sorting_type = - data_harvester::processes::ProcessSorting::CPU; // Go back to default, negate PID for group + data_harvester::processes::ProcessSorting::CpuPercent; // Go back to default, negate PID for group proc_widget_state.process_sorting_reverse = true; } } @@ -653,7 +648,7 @@ fn update_final_process_list(app: &mut App, widget_id: u64) { proc_widget_state.scroll_state.current_scroll_position = resulting_processes.len().saturating_sub(1); proc_widget_state.scroll_state.previous_scroll_position = 0; - proc_widget_state.scroll_state.scroll_direction = app::ScrollDirection::DOWN; + proc_widget_state.scroll_state.scroll_direction = app::ScrollDirection::Down; } app.canvas_data @@ -670,7 +665,7 @@ fn sort_process_data( }); match proc_widget_state.process_sorting_type { - ProcessSorting::CPU => { + ProcessSorting::CpuPercent => { to_sort_vec.sort_by(|a, b| { utils::gen_util::get_ordering( a.cpu_usage, @@ -679,7 +674,10 @@ fn sort_process_data( ) }); } - ProcessSorting::MEM => { + ProcessSorting::Mem => { + // TODO: Do when I do mem values in processes + } + ProcessSorting::MemPercent => { to_sort_vec.sort_by(|a, b| { utils::gen_util::get_ordering( a.mem_usage, @@ -688,8 +686,8 @@ fn sort_process_data( ) }); } - ProcessSorting::IDENTIFIER => { - // Don't repeat if false... + ProcessSorting::ProcessName | ProcessSorting::Command => { + // Don't repeat if false... it sorts by name by default anyways. if proc_widget_state.process_sorting_reverse { to_sort_vec.sort_by(|a, b| { utils::gen_util::get_ordering( @@ -700,7 +698,7 @@ fn sort_process_data( }) } } - ProcessSorting::PID => { + ProcessSorting::Pid => { if !proc_widget_state.is_grouped { to_sort_vec.sort_by(|a, b| { utils::gen_util::get_ordering( @@ -711,6 +709,49 @@ fn sort_process_data( }); } } + ProcessSorting::ReadPerSecond => { + to_sort_vec.sort_by(|a, b| { + utils::gen_util::get_ordering( + a.rps_f64, + b.rps_f64, + proc_widget_state.process_sorting_reverse, + ) + }); + } + ProcessSorting::WritePerSecond => { + to_sort_vec.sort_by(|a, b| { + utils::gen_util::get_ordering( + a.wps_f64, + b.wps_f64, + proc_widget_state.process_sorting_reverse, + ) + }); + } + ProcessSorting::TotalRead => { + to_sort_vec.sort_by(|a, b| { + utils::gen_util::get_ordering( + a.tr_f64, + b.tr_f64, + proc_widget_state.process_sorting_reverse, + ) + }); + } + ProcessSorting::TotalWrite => { + to_sort_vec.sort_by(|a, b| { + utils::gen_util::get_ordering( + a.tw_f64, + b.tw_f64, + proc_widget_state.process_sorting_reverse, + ) + }); + } + ProcessSorting::State => to_sort_vec.sort_by(|a, b| { + utils::gen_util::get_ordering( + &a.process_state.to_lowercase(), + &b.process_state.to_lowercase(), + proc_widget_state.process_sorting_reverse, + ) + }), } } diff --git a/src/options.rs b/src/options.rs index 634a478f..81ac4c99 100644 --- a/src/options.rs +++ b/src/options.rs @@ -180,8 +180,7 @@ pub fn build_app( battery_state_map .insert(widget.widget_id, BatteryWidgetState::default()); } - Empty | BasicCpu | BasicMem | BasicNet | BasicTables | ProcSearch - | CpuLegend => {} + _ => {} } } } @@ -262,7 +261,17 @@ pub fn get_widget_layout( let bottom_layout = if get_use_basic_mode(matches, config) { default_widget_id = DEFAULT_WIDGET_ID; BottomLayout::init_basic_default(get_use_battery(matches, config)) - } else if let Some(rows) = &config.row { + } else { + let ref_row: Vec; // Required to handle reference + let rows = match &config.row { + Some(r) => r, + None => { + // This cannot (like it really shouldn't) fail! + ref_row = toml::from_str::(DEFAULT_LAYOUT)?.row.unwrap(); + &ref_row + } + }; + let mut iter_id = 0; // A lazy way of forcing unique IDs *shrugs* let mut total_height_ratio = 0; @@ -286,15 +295,14 @@ pub fn get_widget_layout( // Confirm that we have at least ONE widget - if not, error out! if iter_id > 0 { ret_bottom_layout.get_movement_mappings(); + // debug!("Bottom layout: {:#?}", ret_bottom_layout); + ret_bottom_layout } else { return Err(error::BottomError::ConfigError( "invalid layout config: please have at least one widget.".to_string(), )); } - } else { - default_widget_id = DEFAULT_WIDGET_ID; - BottomLayout::init_default(left_legend, get_use_battery(matches, config)) }; Ok((bottom_layout, default_widget_id)) diff --git a/src/options/layout_options.rs b/src/options/layout_options.rs index 770a9f2a..f7ccb1aa 100644 --- a/src/options/layout_options.rs +++ b/src/options/layout_options.rs @@ -17,7 +17,7 @@ impl Row { default_widget_type: &Option, default_widget_count: &mut u64, left_legend: bool, ) -> Result { - // In the future we want to also add percentages. + // TODO: In the future we want to also add percentages. // But for MVP, we aren't going to bother. let row_ratio = self.ratio.unwrap_or(1); let mut children = Vec::new(); @@ -53,7 +53,7 @@ impl Row { children.push(match widget_type { BottomWidgetType::Cpu => { - let iter_old_id = *iter_id; + let cpu_id = *iter_id; *iter_id += 1; BottomCol::builder() .col_width_ratio(width_ratio) @@ -66,11 +66,15 @@ impl Row { .widget_type(BottomWidgetType::CpuLegend) .widget_id(*iter_id) .canvas_handle_width(true) + .parent_reflector(Some(( + WidgetDirection::Right, + 1, + ))) .build(), BottomWidget::builder() .width_ratio(17) .widget_type(BottomWidgetType::Cpu) - .widget_id(iter_old_id) + .widget_id(cpu_id) .flex_grow(true) .build(), ]) @@ -82,7 +86,7 @@ impl Row { BottomWidget::builder() .width_ratio(17) .widget_type(BottomWidgetType::Cpu) - .widget_id(iter_old_id) + .widget_id(cpu_id) .flex_grow(true) .build(), BottomWidget::builder() @@ -90,6 +94,10 @@ impl Row { .widget_type(BottomWidgetType::CpuLegend) .widget_id(*iter_id) .canvas_handle_width(true) + .parent_reflector(Some(( + WidgetDirection::Left, + 1, + ))) .build(), ]) .build()] @@ -97,23 +105,39 @@ impl Row { .build() } BottomWidgetType::Proc => { - let iter_old_id = *iter_id; - *iter_id += 1; + let proc_id = *iter_id; + let proc_search_id = *iter_id + 1; + *iter_id += 2; BottomCol::builder() .total_col_row_ratio(2) .col_width_ratio(width_ratio) .children(vec![ BottomColRow::builder() - .children(vec![BottomWidget::builder() - .widget_type(BottomWidgetType::Proc) - .widget_id(iter_old_id) - .build()]) + .children(vec![ + BottomWidget::builder() + .widget_type(BottomWidgetType::ProcSort) + .widget_id(*iter_id) + .canvas_handle_width(true) + .parent_reflector(Some(( + WidgetDirection::Right, + 2, + ))) + .width_ratio(1) + .build(), + BottomWidget::builder() + .widget_type(BottomWidgetType::Proc) + .widget_id(proc_id) + .width_ratio(2) + .build(), + ]) + .total_widget_ratio(3) .flex_grow(true) .build(), BottomColRow::builder() .children(vec![BottomWidget::builder() .widget_type(BottomWidgetType::ProcSearch) - .widget_id(*iter_id) + .widget_id(proc_search_id) + .parent_reflector(Some((WidgetDirection::Up, 1))) .build()]) .canvas_handle_height(true) .build(), @@ -137,7 +161,7 @@ impl Row { let mut total_col_row_ratio = 0; let mut contains_proc = false; - let mut col_row_children = Vec::new(); + let mut col_row_children: Vec = Vec::new(); for widget in child { let widget_type = widget.widget_type.parse::()?; @@ -165,7 +189,7 @@ impl Row { match widget_type { BottomWidgetType::Cpu => { - let iter_old_id = *iter_id; + let cpu_id = *iter_id; *iter_id += 1; if left_legend { col_row_children.push( @@ -178,11 +202,15 @@ impl Row { .widget_type(BottomWidgetType::CpuLegend) .widget_id(*iter_id) .canvas_handle_width(true) + .parent_reflector(Some(( + WidgetDirection::Right, + 1, + ))) .build(), BottomWidget::builder() .width_ratio(17) .widget_type(BottomWidgetType::Cpu) - .widget_id(iter_old_id) + .widget_id(cpu_id) .flex_grow(true) .build(), ]) @@ -197,7 +225,7 @@ impl Row { BottomWidget::builder() .width_ratio(17) .widget_type(BottomWidgetType::Cpu) - .widget_id(iter_old_id) + .widget_id(cpu_id) .flex_grow(true) .build(), BottomWidget::builder() @@ -205,6 +233,10 @@ impl Row { .widget_type(BottomWidgetType::CpuLegend) .widget_id(*iter_id) .canvas_handle_width(true) + .parent_reflector(Some(( + WidgetDirection::Left, + 1, + ))) .build(), ]) .build(), @@ -213,15 +245,30 @@ impl Row { } BottomWidgetType::Proc => { contains_proc = true; - let iter_old_id = *iter_id; - *iter_id += 1; + let proc_id = *iter_id; + let proc_search_id = *iter_id + 1; + *iter_id += 2; col_row_children.push( BottomColRow::builder() + .children(vec![ + BottomWidget::builder() + .widget_type(BottomWidgetType::ProcSort) + .widget_id(*iter_id) + .canvas_handle_width(true) + .parent_reflector(Some(( + WidgetDirection::Right, + 2, + ))) + .width_ratio(1) + .build(), + BottomWidget::builder() + .widget_type(BottomWidgetType::Proc) + .widget_id(proc_id) + .width_ratio(2) + .build(), + ]) .col_row_height_ratio(col_row_height_ratio) - .children(vec![BottomWidget::builder() - .widget_type(BottomWidgetType::Proc) - .widget_id(iter_old_id) - .build()]) + .total_widget_ratio(3) .build(), ); col_row_children.push( @@ -229,7 +276,8 @@ impl Row { .col_row_height_ratio(col_row_height_ratio) .children(vec![BottomWidget::builder() .widget_type(BottomWidgetType::ProcSearch) - .widget_id(*iter_id) + .widget_id(proc_search_id) + .parent_reflector(Some((WidgetDirection::Up, 1))) .build()]) .canvas_handle_height(true) .build(), diff --git a/tests/widget_movement_tests.rs b/tests/widget_movement_tests.rs new file mode 100644 index 00000000..b4bd4444 --- /dev/null +++ b/tests/widget_movement_tests.rs @@ -0,0 +1 @@ +// TODO: See if we can mock widget movements and test if they work \ No newline at end of file