diff --git a/Cargo.toml b/Cargo.toml index b7b7b5fe7..fc5292510 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,10 +25,10 @@ doc = false [profile.release] debug = 0 +strip = "symbols" lto = true opt-level = 3 codegen-units = 1 -strip = "symbols" [features] default = ["fern", "log", "battery", "gpu"] @@ -40,11 +40,12 @@ nvidia = ["nvml-wrapper"] [dependencies] anyhow = "1.0.57" backtrace = "0.3.65" +cfg-if = "1.0.0" crossterm = "0.18.2" ctrlc = { version = "3.1.9", features = ["termination"] } clap = { version = "3.1.12", features = ["default", "cargo", "wrap_help"] } -cfg-if = "1.0.0" concat-string = "1.0.1" +# const_format = "0.2.23" dirs = "4.0.0" futures = "0.3.21" futures-timer = "3.0.2" diff --git a/src/app.rs b/src/app.rs index 5f9ac413a..d139f60f0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,7 +1,6 @@ use std::{ cmp::{max, min}, collections::HashMap, - convert::TryInto, path::PathBuf, time::Instant, }; @@ -12,12 +11,13 @@ use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; use typed_builder::*; use data_farmer::*; -use data_harvester::{processes, temperature}; +use data_harvester::temperature; use layout_manager::*; pub use states::*; use crate::{ - canvas, constants, + constants, + data_conversion::ConvertedData, options::Config, options::ConfigFlags, options::WidgetIdEnabled, @@ -26,12 +26,15 @@ use crate::{ Pid, }; +use self::widgets::{ProcWidget, ProcWidgetMode}; + pub mod data_farmer; pub mod data_harvester; pub mod layout_manager; mod process_killer; pub mod query; pub mod states; +pub mod widgets; const MAX_SEARCH_LENGTH: usize = 200; @@ -104,7 +107,7 @@ pub struct App { last_key_press: Instant, #[builder(default, setter(skip))] - pub canvas_data: canvas::DisplayableData, + pub converted_data: ConvertedData, #[builder(default, setter(skip))] pub data_collection: DataCollection, @@ -127,12 +130,9 @@ pub struct App { #[builder(default = false, setter(skip))] pub basic_mode_use_percent: bool, - #[builder(default = false, setter(skip))] - pub did_config_fail_to_save: bool, - #[cfg(target_family = "unix")] #[builder(default, setter(skip))] - pub user_table: processes::UserTable, + pub user_table: data_harvester::processes::UserTable, pub cpu_state: CpuState, pub mem_state: MemState, @@ -147,8 +147,8 @@ pub struct App { pub current_widget: BottomWidget, pub used_widgets: UsedWidgets, pub filters: DataFilters, - pub config: Config, - pub config_path: Option, + pub config: Config, // TODO: Is this even used...? + pub config_path: Option, // TODO: Is this even used...? } #[cfg(target_os = "windows")] @@ -172,9 +172,8 @@ impl App { .widget_states .values_mut() .for_each(|state| { - state.process_search_state.search_state.reset(); + state.proc_search.search_state.reset(); }); - self.proc_state.force_update_all = true; // Clear current delete list self.to_delete_process_list = None; @@ -218,32 +217,25 @@ impl App { } else { match self.current_widget.widget_type { BottomWidgetType::Proc => { - if let Some(current_proc_state) = self + if let Some(pws) = self .proc_state .get_mut_widget_state(self.current_widget.widget_id) { - 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; + if pws.is_search_enabled() || pws.is_sort_open { + pws.proc_search.search_state.is_enabled = false; + pws.is_sort_open = false; self.is_force_redraw = true; return; } } } BottomWidgetType::ProcSearch => { - if let Some(current_proc_state) = self + if let Some(pws) = self .proc_state .get_mut_widget_state(self.current_widget.widget_id - 1) { - if current_proc_state.is_search_enabled() { - current_proc_state - .process_search_state - .search_state - .is_enabled = false; + if pws.is_search_enabled() { + pws.proc_search.search_state.is_enabled = false; self.move_widget_selection(&WidgetDirection::Up); self.is_force_redraw = true; return; @@ -251,14 +243,12 @@ impl App { } } BottomWidgetType::ProcSort => { - if let Some(current_proc_state) = self + if let Some(pws) = 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; + if pws.is_sort_open { + pws.is_sort_open = false; self.move_widget_selection(&WidgetDirection::Right); self.is_force_redraw = true; return; @@ -314,53 +304,7 @@ impl App { .proc_state .get_mut_widget_state(self.current_widget.widget_id) { - // Do NOT allow when in tree mode! - if !proc_widget_state.is_tree_mode { - // Toggles process widget grouping state - proc_widget_state.is_grouped = !(proc_widget_state.is_grouped); - - // Forcefully switch off column if we were on it... - if (proc_widget_state.is_grouped - && (proc_widget_state.process_sorting_type - == processes::ProcessSorting::Pid - || proc_widget_state.process_sorting_type - == processes::ProcessSorting::User - || proc_widget_state.process_sorting_type - == processes::ProcessSorting::State)) - || (!proc_widget_state.is_grouped - && proc_widget_state.process_sorting_type - == processes::ProcessSorting::Count) - { - proc_widget_state.process_sorting_type = - processes::ProcessSorting::CpuPercent; // Go back to default, negate PID for group - proc_widget_state.is_process_sort_descending = true; - } - - proc_widget_state.columns.set_to_sorted_index_from_type( - &proc_widget_state.process_sorting_type, - ); - - proc_widget_state.columns.try_set( - &processes::ProcessSorting::State, - !(proc_widget_state.is_grouped), - ); - - #[cfg(target_family = "unix")] - proc_widget_state.columns.try_set( - &processes::ProcessSorting::User, - !(proc_widget_state.is_grouped), - ); - - proc_widget_state - .columns - .toggle(&processes::ProcessSorting::Count); - proc_widget_state - .columns - .toggle(&processes::ProcessSorting::Pid); - - proc_widget_state.requires_redraw = true; - self.proc_state.force_update = Some(self.current_widget.widget_id); - } + proc_widget_state.toggle_tab(); } } _ => {} @@ -368,16 +312,6 @@ impl App { } } - /// I don't like this, but removing it causes a bunch of breakage. - /// Use ``proc_widget_state.is_grouped`` if possible! - pub fn is_grouped(&self, widget_id: u64) -> bool { - if let Some(proc_widget_state) = self.proc_state.widget_states.get(&widget_id) { - proc_widget_state.is_grouped - } else { - false - } - } - pub fn on_slash(&mut self) { if !self.ignore_normal_keybinds() { match &self.current_widget.widget_type { @@ -390,10 +324,7 @@ impl App { _ => 0, }, ) { - proc_widget_state - .process_search_state - .search_state - .is_enabled = true; + proc_widget_state.proc_search.search_state.is_enabled = true; self.move_widget_selection(&WidgetDirection::Down); self.is_force_redraw = true; } @@ -404,37 +335,30 @@ impl App { } pub fn toggle_sort(&mut self) { - match &self.current_widget.widget_type { - BottomWidgetType::Proc | BottomWidgetType::ProcSort => { - let widget_id = self.current_widget.widget_id - - match &self.current_widget.widget_type { - BottomWidgetType::Proc => 0, - BottomWidgetType::ProcSort => 2, - _ => 0, - }; + let widget_id = self.current_widget.widget_id + - match &self.current_widget.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. + if let Some(pws) = self.proc_state.get_mut_widget_state(widget_id) { + pws.is_sort_open = !pws.is_sort_open; + pws.force_rerender = true; - 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_from_type(&proc_widget_state.process_sorting_type); - self.move_widget_selection(&WidgetDirection::Left); - } else { - // Otherwise, move right if currently on the sort widget - if let BottomWidgetType::ProcSort = self.current_widget.widget_type { - self.move_widget_selection(&WidgetDirection::Right); - } - } + // If the sort is now open, move left. Otherwise, if the proc sort was selected, force move right. + if pws.is_sort_open { + if let SortState::Sortable(st) = &pws.table_state.sort_state { + pws.sort_table_state.scroll_bar = 0; + pws.sort_table_state.current_scroll_position = st + .current_index + .clamp(0, pws.num_enabled_columns().saturating_sub(1)); } - - self.is_force_redraw = true; + self.move_widget_selection(&WidgetDirection::Left); + } else if let BottomWidgetType::ProcSort = self.current_widget.widget_type { + self.move_widget_selection(&WidgetDirection::Right); } - _ => {} + self.is_force_redraw = true; } } @@ -449,10 +373,12 @@ impl App { }; if let Some(proc_widget_state) = self.proc_state.get_mut_widget_state(widget_id) { - proc_widget_state.is_process_sort_descending = - !proc_widget_state.is_process_sort_descending; - - self.proc_state.force_update = Some(widget_id); + if let SortState::Sortable(state) = + &mut proc_widget_state.table_state.sort_state + { + state.toggle_order(); + proc_widget_state.force_data_update(); + } } } _ => {} @@ -470,30 +396,8 @@ impl App { .widget_states .get_mut(&self.current_widget.widget_id) { - proc_widget_state - .columns - .toggle(&processes::ProcessSorting::Mem); - if let Some(mem_percent_state) = proc_widget_state - .columns - .toggle(&processes::ProcessSorting::MemPercent) - { - if proc_widget_state.process_sorting_type - == processes::ProcessSorting::MemPercent - || proc_widget_state.process_sorting_type - == processes::ProcessSorting::Mem - { - if mem_percent_state { - proc_widget_state.process_sorting_type = - processes::ProcessSorting::MemPercent; - } else { - proc_widget_state.process_sorting_type = - processes::ProcessSorting::Mem; - } - } - } - - proc_widget_state.requires_redraw = true; - self.proc_state.force_update = Some(self.current_widget.widget_id); + proc_widget_state.toggle_mem_percentage(); + proc_widget_state.force_data_update(); } } _ => {} @@ -509,14 +413,11 @@ impl App { .get_mut(&(self.current_widget.widget_id - 1)) { if is_in_search_widget && proc_widget_state.is_search_enabled() { - proc_widget_state - .process_search_state - .search_toggle_ignore_case(); + proc_widget_state.proc_search.search_toggle_ignore_case(); proc_widget_state.update_query(); - self.proc_state.force_update = Some(self.current_widget.widget_id - 1); // Remember, it's the opposite (ignoring case is case "in"sensitive) - is_case_sensitive = Some(!proc_widget_state.process_search_state.is_ignoring_case); + is_case_sensitive = Some(!proc_widget_state.proc_search.is_ignoring_case); } } @@ -550,8 +451,6 @@ impl App { .build(), ); } - - // self.did_config_fail_to_save = self.update_config_file().is_err(); } } @@ -564,17 +463,11 @@ impl App { .get_mut(&(self.current_widget.widget_id - 1)) { if is_in_search_widget && proc_widget_state.is_search_enabled() { - proc_widget_state - .process_search_state - .search_toggle_whole_word(); + proc_widget_state.proc_search.search_toggle_whole_word(); proc_widget_state.update_query(); - self.proc_state.force_update = Some(self.current_widget.widget_id - 1); - is_searching_whole_word = Some( - proc_widget_state - .process_search_state - .is_searching_whole_word, - ); + is_searching_whole_word = + Some(proc_widget_state.proc_search.is_searching_whole_word); } } @@ -624,15 +517,11 @@ impl App { .get_mut(&(self.current_widget.widget_id - 1)) { if is_in_search_widget && proc_widget_state.is_search_enabled() { - proc_widget_state.process_search_state.search_toggle_regex(); + proc_widget_state.proc_search.search_toggle_regex(); proc_widget_state.update_query(); - self.proc_state.force_update = Some(self.current_widget.widget_id - 1); - is_searching_with_regex = Some( - proc_widget_state - .process_search_state - .is_searching_with_regex, - ); + is_searching_with_regex = + Some(proc_widget_state.proc_search.is_searching_with_regex); } } @@ -666,8 +555,6 @@ impl App { .build(), ); } - - // self.did_config_fail_to_save = self.update_config_file().is_err(); } } @@ -677,37 +564,19 @@ impl App { .widget_states .get_mut(&(self.current_widget.widget_id)) { - proc_widget_state.is_tree_mode = !proc_widget_state.is_tree_mode; - - // FIXME: For consistency, either disable tree mode if grouped, or allow grouped mode if in tree mode. - if proc_widget_state.is_tree_mode { - // Disable grouping if so! - proc_widget_state.is_grouped = false; - - proc_widget_state - .columns - .try_enable(&processes::ProcessSorting::State); - - #[cfg(target_family = "unix")] - proc_widget_state - .columns - .try_enable(&processes::ProcessSorting::User); - - proc_widget_state - .columns - .try_disable(&processes::ProcessSorting::Count); - - proc_widget_state - .columns - .try_enable(&processes::ProcessSorting::Pid); - - // We enabled... set PID sort type to ascending. - proc_widget_state.process_sorting_type = processes::ProcessSorting::Pid; - proc_widget_state.is_process_sort_descending = false; + match proc_widget_state.mode { + ProcWidgetMode::Tree { .. } => { + proc_widget_state.mode = ProcWidgetMode::Normal; + proc_widget_state.force_rerender_and_update(); + } + ProcWidgetMode::Normal => { + proc_widget_state.mode = ProcWidgetMode::Tree { + collapsed_pids: Default::default(), + }; + proc_widget_state.force_rerender_and_update(); + } + ProcWidgetMode::Grouped => {} } - - self.proc_state.force_update = Some(self.current_widget.widget_id); - proc_widget_state.requires_redraw = true; } } @@ -744,9 +613,9 @@ impl App { .widget_states .get_mut(&(self.current_widget.widget_id - 2)) { - proc_widget_state.update_sorting_with_columns(); - self.proc_state.force_update = Some(self.current_widget.widget_id - 2); - self.toggle_sort(); + proc_widget_state.use_sort_table_value(); + self.move_widget_selection(&WidgetDirection::Right); + self.is_force_redraw = true; } } } @@ -761,13 +630,10 @@ impl App { .get_mut(&(self.current_widget.widget_id - 1)) { if is_in_search_widget { - if proc_widget_state - .process_search_state - .search_state - .is_enabled + if proc_widget_state.proc_search.search_state.is_enabled && proc_widget_state.get_search_cursor_position() < proc_widget_state - .process_search_state + .proc_search .search_state .current_search_query .len() @@ -777,27 +643,24 @@ impl App { .search_walk_forward(proc_widget_state.get_search_cursor_position()); let _removed_chars: String = proc_widget_state - .process_search_state + .proc_search .search_state .current_search_query .drain(current_cursor..proc_widget_state.get_search_cursor_position()) .collect(); - proc_widget_state - .process_search_state - .search_state - .grapheme_cursor = GraphemeCursor::new( - current_cursor, - proc_widget_state - .process_search_state - .search_state - .current_search_query - .len(), - true, - ); + proc_widget_state.proc_search.search_state.grapheme_cursor = + GraphemeCursor::new( + current_cursor, + proc_widget_state + .proc_search + .search_state + .current_search_query + .len(), + true, + ); proc_widget_state.update_query(); - self.proc_state.force_update = Some(self.current_widget.widget_id - 1); } } else { self.start_killing_process() @@ -815,10 +678,7 @@ impl App { .get_mut(&(self.current_widget.widget_id - 1)) { if is_in_search_widget - && proc_widget_state - .process_search_state - .search_state - .is_enabled + && proc_widget_state.proc_search.search_state.is_enabled && proc_widget_state.get_search_cursor_position() > 0 { let current_cursor = proc_widget_state.get_search_cursor_position(); @@ -826,37 +686,32 @@ impl App { .search_walk_back(proc_widget_state.get_search_cursor_position()); let removed_chars: String = proc_widget_state - .process_search_state + .proc_search .search_state .current_search_query .drain(proc_widget_state.get_search_cursor_position()..current_cursor) .collect(); - proc_widget_state - .process_search_state - .search_state - .grapheme_cursor = GraphemeCursor::new( - proc_widget_state.get_search_cursor_position(), - proc_widget_state - .process_search_state - .search_state - .current_search_query - .len(), - true, - ); + proc_widget_state.proc_search.search_state.grapheme_cursor = + GraphemeCursor::new( + proc_widget_state.get_search_cursor_position(), + proc_widget_state + .proc_search + .search_state + .current_search_query + .len(), + true, + ); proc_widget_state - .process_search_state + .proc_search .search_state .char_cursor_position -= UnicodeWidthStr::width(removed_chars.as_str()); - proc_widget_state - .process_search_state - .search_state - .cursor_direction = CursorDirection::Left; + proc_widget_state.proc_search.search_state.cursor_direction = + CursorDirection::Left; proc_widget_state.update_query(); - self.proc_state.force_update = Some(self.current_widget.widget_id - 1); } } } @@ -864,7 +719,7 @@ impl App { pub fn get_process_filter(&self, widget_id: u64) -> &Option { if let Some(process_widget_state) = self.proc_state.widget_states.get(&widget_id) { - &process_widget_state.process_search_state.search_state.query + &process_widget_state.proc_search.search_state.query } else { &None } @@ -961,24 +816,22 @@ impl App { .search_walk_back(proc_widget_state.get_search_cursor_position()); if proc_widget_state.get_search_cursor_position() < prev_cursor { let str_slice = &proc_widget_state - .process_search_state + .proc_search .search_state .current_search_query [proc_widget_state.get_search_cursor_position()..prev_cursor]; proc_widget_state - .process_search_state + .proc_search .search_state .char_cursor_position -= UnicodeWidthStr::width(str_slice); - proc_widget_state - .process_search_state - .search_state - .cursor_direction = CursorDirection::Left; + proc_widget_state.proc_search.search_state.cursor_direction = + CursorDirection::Left; } } } } BottomWidgetType::Battery => { - if !self.canvas_data.battery_data.is_empty() { + if !self.converted_data.battery_data.is_empty() { if let Some(battery_widget_state) = self .battery_state .get_mut_widget_state(self.current_widget.widget_id) @@ -1033,25 +886,23 @@ impl App { ); if proc_widget_state.get_search_cursor_position() > prev_cursor { let str_slice = &proc_widget_state - .process_search_state + .proc_search .search_state .current_search_query [prev_cursor..proc_widget_state.get_search_cursor_position()]; proc_widget_state - .process_search_state + .proc_search .search_state .char_cursor_position += UnicodeWidthStr::width(str_slice); - proc_widget_state - .process_search_state - .search_state - .cursor_direction = CursorDirection::Right; + proc_widget_state.proc_search.search_state.cursor_direction = + CursorDirection::Right; } } } } BottomWidgetType::Battery => { - if !self.canvas_data.battery_data.is_empty() { - let battery_count = self.canvas_data.battery_data.len(); + if !self.converted_data.battery_data.is_empty() { + let battery_count = self.converted_data.battery_data.len(); if let Some(battery_widget_state) = self .battery_state .get_mut_widget_state(self.current_widget.widget_id) @@ -1111,12 +962,8 @@ impl App { &self.current_widget.bottom_right_corner, ) { let border_offset = if self.is_drawing_border() { 1 } else { 0 }; - let header_gap_offset = 1 + if self.is_drawing_gap(&self.current_widget) { - self.app_config_fields.table_gap - } else { - 0 - }; - let height = brc_y - tlc_y - 2 * border_offset - header_gap_offset; + let header_offset = self.header_offset(&self.current_widget); + let height = brc_y - tlc_y - 2 * border_offset - header_offset; self.change_position_count(-(height as i64)); } } @@ -1138,12 +985,8 @@ impl App { &self.current_widget.bottom_right_corner, ) { let border_offset = if self.is_drawing_border() { 1 } else { 0 }; - let header_gap_offset = 1 + if self.is_drawing_gap(&self.current_widget) { - self.app_config_fields.table_gap - } else { - 0 - }; - let height = brc_y - tlc_y - 2 * border_offset - header_gap_offset; + let header_offset = self.header_offset(&self.current_widget); + let height = brc_y - tlc_y - 2 * border_offset - header_offset; self.change_position_count(height as i64); } } @@ -1159,26 +1002,22 @@ impl App { .get_mut(&(self.current_widget.widget_id - 1)) { if is_in_search_widget { + proc_widget_state.proc_search.search_state.grapheme_cursor = + GraphemeCursor::new( + 0, + proc_widget_state + .proc_search + .search_state + .current_search_query + .len(), + true, + ); proc_widget_state - .process_search_state - .search_state - .grapheme_cursor = GraphemeCursor::new( - 0, - proc_widget_state - .process_search_state - .search_state - .current_search_query - .len(), - true, - ); - proc_widget_state - .process_search_state + .proc_search .search_state .char_cursor_position = 0; - proc_widget_state - .process_search_state - .search_state - .cursor_direction = CursorDirection::Left; + proc_widget_state.proc_search.search_state.cursor_direction = + CursorDirection::Left; } } } @@ -1195,36 +1034,32 @@ impl App { .get_mut(&(self.current_widget.widget_id - 1)) { if is_in_search_widget { + proc_widget_state.proc_search.search_state.grapheme_cursor = + GraphemeCursor::new( + proc_widget_state + .proc_search + .search_state + .current_search_query + .len(), + proc_widget_state + .proc_search + .search_state + .current_search_query + .len(), + true, + ); proc_widget_state - .process_search_state - .search_state - .grapheme_cursor = GraphemeCursor::new( - proc_widget_state - .process_search_state - .search_state - .current_search_query - .len(), - proc_widget_state - .process_search_state - .search_state - .current_search_query - .len(), - true, - ); - proc_widget_state - .process_search_state + .proc_search .search_state .char_cursor_position = UnicodeWidthStr::width( proc_widget_state - .process_search_state + .proc_search .search_state .current_search_query .as_str(), ); - proc_widget_state - .process_search_state - .search_state - .cursor_direction = CursorDirection::Right; + proc_widget_state.proc_search.search_state.cursor_direction = + CursorDirection::Right; } } } @@ -1239,7 +1074,6 @@ impl App { .get_mut(&(self.current_widget.widget_id - 1)) { proc_widget_state.clear_search(); - self.proc_state.force_update = Some(self.current_widget.widget_id - 1); } } } @@ -1279,19 +1113,16 @@ impl App { } let removed_chars: String = proc_widget_state - .process_search_state + .proc_search .search_state .current_search_query .drain(start_index..end_index) .collect(); - proc_widget_state - .process_search_state - .search_state - .grapheme_cursor = GraphemeCursor::new( + proc_widget_state.proc_search.search_state.grapheme_cursor = GraphemeCursor::new( start_index, proc_widget_state - .process_search_state + .proc_search .search_state .current_search_query .len(), @@ -1299,23 +1130,13 @@ impl App { ); proc_widget_state - .process_search_state + .proc_search .search_state .char_cursor_position -= UnicodeWidthStr::width(removed_chars.as_str()); - proc_widget_state - .process_search_state - .search_state - .cursor_direction = CursorDirection::Left; + proc_widget_state.proc_search.search_state.cursor_direction = CursorDirection::Left; proc_widget_state.update_query(); - self.proc_state.force_update = Some(self.current_widget.widget_id - 1); - - // Now, convert this range into a String-friendly range and remove it all at once! - - // Now make sure to also update our current cursor positions... - - self.proc_state.force_update = Some(self.current_widget.widget_id - 1); } } } @@ -1323,38 +1144,36 @@ impl App { pub fn start_killing_process(&mut self) { self.reset_multi_tap_keys(); - if let Some(proc_widget_state) = self + if let Some(pws) = self .proc_state .widget_states .get(&self.current_widget.widget_id) { - if let Some(corresponding_filtered_process_list) = self - .canvas_data - .finalized_process_data_map - .get(&self.current_widget.widget_id) + if let Some(table_row) = pws + .table_data + .data + .get(pws.table_state.current_scroll_position) { - if proc_widget_state.scroll_state.current_scroll_position - < corresponding_filtered_process_list.len() - { - let current_process: (String, Vec); - if self.is_grouped(self.current_widget.widget_id) { - if let Some(process) = &corresponding_filtered_process_list - .get(proc_widget_state.scroll_state.current_scroll_position) + if let Some(col_value) = table_row.row().get(ProcWidget::PROC_NAME_OR_CMD) { + let val = col_value.main_text().to_string(); + if pws.is_using_command() { + if let Some(pids) = self.data_collection.process_data.cmd_pid_map.get(&val) { - current_process = (process.name.to_string(), process.group_pids.clone()) - } else { - return; + let current_process = (val, pids.clone()); + + self.to_delete_process_list = Some(current_process); + self.delete_dialog_state.is_showing_dd = true; + self.is_determining_widget_boundary = true; } - } else { - let process = corresponding_filtered_process_list - [proc_widget_state.scroll_state.current_scroll_position] - .clone(); - current_process = (process.name.clone(), vec![process.pid]) - }; + } else if let Some(pids) = + self.data_collection.process_data.name_pid_map.get(&val) + { + let current_process = (val, pids.clone()); - self.to_delete_process_list = Some(current_process); - self.delete_dialog_state.is_showing_dd = true; - self.is_determining_widget_boundary = true; + self.to_delete_process_list = Some(current_process); + self.delete_dialog_state.is_showing_dd = true; + self.is_determining_widget_boundary = true; + } } } } @@ -1389,45 +1208,40 @@ impl App { && proc_widget_state.is_search_enabled() && UnicodeWidthStr::width( proc_widget_state - .process_search_state + .proc_search .search_state .current_search_query .as_str(), ) <= MAX_SEARCH_LENGTH { proc_widget_state - .process_search_state + .proc_search .search_state .current_search_query .insert(proc_widget_state.get_search_cursor_position(), caught_char); - proc_widget_state - .process_search_state - .search_state - .grapheme_cursor = GraphemeCursor::new( - proc_widget_state.get_search_cursor_position(), - proc_widget_state - .process_search_state - .search_state - .current_search_query - .len(), - true, - ); + proc_widget_state.proc_search.search_state.grapheme_cursor = + GraphemeCursor::new( + proc_widget_state.get_search_cursor_position(), + proc_widget_state + .proc_search + .search_state + .current_search_query + .len(), + true, + ); proc_widget_state .search_walk_forward(proc_widget_state.get_search_cursor_position()); proc_widget_state - .process_search_state + .proc_search .search_state .char_cursor_position += UnicodeWidthChar::width(caught_char).unwrap_or(0); proc_widget_state.update_query(); - self.proc_state.force_update = Some(self.current_widget.widget_id - 1); - proc_widget_state - .process_search_state - .search_state - .cursor_direction = CursorDirection::Right; + proc_widget_state.proc_search.search_state.cursor_direction = + CursorDirection::Right; return; } @@ -1528,23 +1342,18 @@ impl App { 'f' => { self.is_frozen = !self.is_frozen; if self.is_frozen { - self.data_collection.set_frozen_time(); + self.data_collection.freeze(); + } else { + self.data_collection.thaw(); } } - 'C' => { - // self.open_config(), - } 'c' => { if let BottomWidgetType::Proc = self.current_widget.widget_type { if let Some(proc_widget_state) = self .proc_state .get_mut_widget_state(self.current_widget.widget_id) { - proc_widget_state - .columns - .set_to_sorted_index_from_type(&processes::ProcessSorting::CpuPercent); - proc_widget_state.update_sorting_with_columns(); - self.proc_state.force_update = Some(self.current_widget.widget_id); + proc_widget_state.select_column(ProcWidget::CPU); } } } @@ -1554,18 +1363,7 @@ impl App { .proc_state .get_mut_widget_state(self.current_widget.widget_id) { - proc_widget_state.columns.set_to_sorted_index_from_type( - &(if proc_widget_state - .columns - .is_enabled(&processes::ProcessSorting::MemPercent) - { - processes::ProcessSorting::MemPercent - } else { - processes::ProcessSorting::Mem - }), - ); - proc_widget_state.update_sorting_with_columns(); - self.proc_state.force_update = Some(self.current_widget.widget_id); + proc_widget_state.select_column(ProcWidget::MEM); } } } @@ -1575,14 +1373,7 @@ impl App { .proc_state .get_mut_widget_state(self.current_widget.widget_id) { - // Skip if grouped - if !proc_widget_state.is_grouped { - proc_widget_state - .columns - .set_to_sorted_index_from_type(&processes::ProcessSorting::Pid); - proc_widget_state.update_sorting_with_columns(); - self.proc_state.force_update = Some(self.current_widget.widget_id); - } + proc_widget_state.select_column(ProcWidget::PID_OR_COUNT); } } } @@ -1592,25 +1383,7 @@ impl App { .proc_state .get_mut_widget_state(self.current_widget.widget_id) { - 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; - } - } - _ => {} - } - proc_widget_state.requires_redraw = true; - self.proc_state.force_update = Some(self.current_widget.widget_id); + proc_widget_state.toggle_command(); } } } @@ -1620,15 +1393,7 @@ impl App { .proc_state .get_mut_widget_state(self.current_widget.widget_id) { - proc_widget_state.columns.set_to_sorted_index_from_type( - &(if proc_widget_state.is_using_command { - processes::ProcessSorting::Command - } else { - processes::ProcessSorting::ProcessName - }), - ); - proc_widget_state.update_sorting_with_columns(); - self.proc_state.force_update = Some(self.current_widget.widget_id); + proc_widget_state.select_column(ProcWidget::PROC_NAME_OR_CMD); } } } @@ -1648,7 +1413,6 @@ impl App { 's' => self.toggle_sort(), 'I' => self.invert_sort(), '%' => self.toggle_percentages(), - ' ' => self.on_space(), _ => {} } @@ -1659,31 +1423,6 @@ impl App { } } - pub fn on_space(&mut self) {} - - /// TODO: Disabled. - /// Call this whenever the config value is updated! - // fn update_config_file(&mut self) -> anyhow::Result<()> { - // if self.app_config_fields.no_write { - // // debug!("No write enabled. Config will not be written."); - // // Don't write! - // // FIXME: [CONFIG] This should be made VERY clear to the user... make a thing saying "it will not write due to no_write option" - // Ok(()) - // } else if let Some(config_path) = &self.config_path { - // // Update - // // debug!("Updating config file - writing to: {:?}", config_path); - // std::fs::File::create(config_path)? - // .write_all(self.config.get_config_as_bytes()?.as_ref())?; - // Ok(()) - // } else { - // // FIXME: [CONFIG] Put an actual error message? - // Err(anyhow::anyhow!( - // "Config path was missing, please try restarting bottom..." - // )) - // } - // Ok(()) - // } - pub fn kill_highlighted_process(&mut self) -> Result<()> { if let BottomWidgetType::Proc = self.current_widget.widget_type { if let Some(current_selected_processes) = &self.to_delete_process_list { @@ -2198,8 +1937,8 @@ impl App { .proc_state .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.table_state.current_scroll_position = 0; + proc_widget_state.table_state.scroll_direction = ScrollDirection::Up; } } BottomWidgetType::ProcSort => { @@ -2207,8 +1946,8 @@ impl App { .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; + proc_widget_state.sort_table_state.current_scroll_position = 0; + proc_widget_state.sort_table_state.scroll_direction = ScrollDirection::Up; } } BottomWidgetType::Temp => { @@ -2216,8 +1955,8 @@ impl App { .temp_state .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.table_state.current_scroll_position = 0; + temp_widget_state.table_state.scroll_direction = ScrollDirection::Up; } } BottomWidgetType::Disk => { @@ -2225,8 +1964,8 @@ impl App { .disk_state .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.table_state.current_scroll_position = 0; + disk_widget_state.table_state.scroll_direction = ScrollDirection::Up; } } BottomWidgetType::CpuLegend => { @@ -2234,8 +1973,8 @@ impl App { .cpu_state .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.table_state.current_scroll_position = 0; + cpu_widget_state.table_state.scroll_direction = ScrollDirection::Up; } } @@ -2257,18 +1996,9 @@ impl App { .proc_state .get_mut_widget_state(self.current_widget.widget_id) { - if let Some(finalized_process_data) = self - .canvas_data - .finalized_process_data_map - .get(&self.current_widget.widget_id) - { - if !self.canvas_data.finalized_process_data_map.is_empty() { - proc_widget_state.scroll_state.current_scroll_position = - finalized_process_data.len() - 1; - proc_widget_state.scroll_state.scroll_direction = - ScrollDirection::Down; - } - } + proc_widget_state.table_state.current_scroll_position = + proc_widget_state.table_data.data.len().saturating_sub(1); + proc_widget_state.table_state.scroll_direction = ScrollDirection::Down; } } BottomWidgetType::ProcSort => { @@ -2276,9 +2006,9 @@ impl App { .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; + proc_widget_state.sort_table_state.current_scroll_position = + proc_widget_state.num_enabled_columns() - 1; + proc_widget_state.sort_table_state.scroll_direction = ScrollDirection::Down; } } BottomWidgetType::Temp => { @@ -2286,10 +2016,10 @@ impl App { .temp_state .get_mut_widget_state(self.current_widget.widget_id) { - 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; + if !self.converted_data.temp_sensor_data.data.is_empty() { + temp_widget_state.table_state.current_scroll_position = + self.converted_data.temp_sensor_data.data.len() - 1; + temp_widget_state.table_state.scroll_direction = ScrollDirection::Down; } } } @@ -2298,10 +2028,10 @@ impl App { .disk_state .get_mut_widget_state(self.current_widget.widget_id) { - 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; + if !self.converted_data.disk_data.data.is_empty() { + disk_widget_state.table_state.current_scroll_position = + self.converted_data.disk_data.data.len() - 1; + disk_widget_state.table_state.scroll_direction = ScrollDirection::Down; } } } @@ -2310,10 +2040,10 @@ impl App { .cpu_state .get_mut_widget_state(self.current_widget.widget_id - 1) { - let cap = self.canvas_data.cpu_data.len(); + let cap = self.converted_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.table_state.current_scroll_position = cap - 1; + cpu_widget_state.table_state.scroll_direction = ScrollDirection::Down; } } } @@ -2359,23 +2089,10 @@ impl App { .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(); - let prop: core::result::Result = - (current_posn as i64 + num_to_change_by).try_into(); - - if let Ok(prop) = prop { - if prop < num_columns { - 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; - } - } + let num_entries = proc_widget_state.num_enabled_columns(); + proc_widget_state + .sort_table_state + .update_position(num_to_change_by, num_entries); } } @@ -2386,8 +2103,8 @@ impl App { .get_mut(&(self.current_widget.widget_id - 1)) { cpu_widget_state - .scroll_state - .update_position(num_to_change_by, self.canvas_data.cpu_data.len()); + .table_state + .update_position(num_to_change_by, self.converted_data.cpu_data.len()); } } @@ -2397,17 +2114,9 @@ impl App { .proc_state .get_mut_widget_state(self.current_widget.widget_id) { - if let Some(finalized_process_data) = self - .canvas_data - .finalized_process_data_map - .get(&self.current_widget.widget_id) - { - proc_widget_state - .scroll_state - .update_position(num_to_change_by, finalized_process_data.len()) - } else { - None - } + proc_widget_state + .table_state + .update_position(num_to_change_by, proc_widget_state.table_data.data.len()) } else { None } @@ -2419,9 +2128,10 @@ impl App { .widget_states .get_mut(&self.current_widget.widget_id) { - temp_widget_state - .scroll_state - .update_position(num_to_change_by, self.canvas_data.temp_sensor_data.len()); + temp_widget_state.table_state.update_position( + num_to_change_by, + self.converted_data.temp_sensor_data.data.len(), + ); } } @@ -2432,8 +2142,8 @@ impl App { .get_mut(&self.current_widget.widget_id) { disk_widget_state - .scroll_state - .update_position(num_to_change_by, self.canvas_data.disk_data.len()); + .table_state + .update_position(num_to_change_by, self.converted_data.disk_data.data.len()); } } @@ -2513,31 +2223,12 @@ impl App { } fn toggle_collapsing_process_branch(&mut self) { - if let Some(proc_widget_state) = self + if let Some(pws) = self .proc_state .widget_states .get_mut(&self.current_widget.widget_id) { - let current_posn = proc_widget_state.scroll_state.current_scroll_position; - - if let Some(displayed_process_list) = self - .canvas_data - .finalized_process_data_map - .get(&self.current_widget.widget_id) - { - if let Some(corresponding_process) = displayed_process_list.get(current_posn) { - let corresponding_pid = corresponding_process.pid; - - if let Some(process_data) = self - .canvas_data - .single_process_data - .get_mut(&corresponding_pid) - { - process_data.is_collapsed_entry = !process_data.is_collapsed_entry; - self.proc_state.force_update = Some(self.current_widget.widget_id); - } - } - } + pws.toggle_tree_branch(); } } @@ -2858,7 +2549,6 @@ impl App { if (x >= tlc_x && y >= tlc_y) && (x < brc_x && y < brc_y) { if let Some(new_widget) = self.widget_map.get(new_widget_id) { self.current_widget = new_widget.clone(); - match &self.current_widget.widget_type { BottomWidgetType::Temp | BottomWidgetType::Proc @@ -2905,13 +2595,8 @@ impl App { | BottomWidgetType::Disk => { // Get our index... let clicked_entry = y - *tlc_y; - // + 1 so we start at 0. - let header_gap_offset = 1 + if self.is_drawing_gap(&self.current_widget) { - self.app_config_fields.table_gap - } else { - 0 - }; - let offset = border_offset + header_gap_offset; + let header_offset = self.header_offset(&self.current_widget); + let offset = border_offset + header_offset; if clicked_entry >= offset { let offset_clicked_entry = clicked_entry - offset; match &self.current_widget.widget_type { @@ -2921,16 +2606,20 @@ impl App { .get_widget_state(self.current_widget.widget_id) { if let Some(visual_index) = - proc_widget_state.scroll_state.table_state.selected() + proc_widget_state.table_state.table_state.selected() { // If in tree mode, also check to see if this click is on // the same entry as the already selected one - if it is, // then we minimize. + let is_tree_mode = matches!( + proc_widget_state.mode, + ProcWidgetMode::Tree { .. } + ); + let previous_scroll_position = proc_widget_state - .scroll_state + .table_state .current_scroll_position; - let is_tree_mode = proc_widget_state.is_tree_mode; let new_position = self.change_process_position( offset_clicked_entry as i64 - visual_index as i64, @@ -2947,13 +2636,15 @@ impl App { } } BottomWidgetType::ProcSort => { - // TODO: This should sort if you double click! + // TODO: [Feature] This could sort if you double click! if let Some(proc_widget_state) = self .proc_state .get_widget_state(self.current_widget.widget_id - 2) { - if let Some(visual_index) = - proc_widget_state.columns.column_state.selected() + if let Some(visual_index) = proc_widget_state + .sort_table_state + .table_state + .selected() { self.change_process_sort_position( offset_clicked_entry as i64 - visual_index as i64, @@ -2967,7 +2658,7 @@ impl App { .get_widget_state(self.current_widget.widget_id - 1) { if let Some(visual_index) = - cpu_widget_state.scroll_state.table_state.selected() + cpu_widget_state.table_state.table_state.selected() { self.change_cpu_legend_position( offset_clicked_entry as i64 - visual_index as i64, @@ -2981,7 +2672,7 @@ impl App { .get_widget_state(self.current_widget.widget_id) { if let Some(visual_index) = - temp_widget_state.scroll_state.table_state.selected() + temp_widget_state.table_state.table_state.selected() { self.change_temp_position( offset_clicked_entry as i64 - visual_index as i64, @@ -2995,7 +2686,7 @@ impl App { .get_widget_state(self.current_widget.widget_id) { if let Some(visual_index) = - disk_widget_state.scroll_state.table_state.selected() + disk_widget_state.table_state.table_state.selected() { self.change_disk_position( offset_clicked_entry as i64 - visual_index as i64, @@ -3009,45 +2700,19 @@ impl App { // We might have clicked on a header! Check if we only exceeded the table + border offset, and // it's implied we exceeded the gap offset. if clicked_entry == border_offset { - #[allow(clippy::single_match)] - 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 let BottomWidgetType::Proc = &self.current_widget.widget_type { + if let Some(proc_widget_state) = self + .proc_state + .get_mut_widget_state(self.current_widget.widget_id) + { + if let SortState::Sortable(st) = + &mut proc_widget_state.table_state.sort_state { - // Let's now check if it's a column header. - if let (Some(y_loc), Some(x_locs)) = ( - &proc_widget_state.columns.column_header_y_loc, - &proc_widget_state.columns.column_header_x_locs, - ) { - // debug!("x, y: {}, {}", x, y); - // debug!("y_loc: {}", y_loc); - // debug!("x_locs: {:?}", x_locs); - - if y == *y_loc { - for (itx, (x_left, x_right)) in - x_locs.iter().enumerate() - { - if x >= *x_left && x <= *x_right { - // Found our column! - proc_widget_state - .columns - .set_to_sorted_index_from_visual_index( - itx, - ); - proc_widget_state - .update_sorting_with_columns(); - self.proc_state.force_update = - Some(self.current_widget.widget_id); - break; - } - } - } + if st.try_select_location(x, y).is_some() { + proc_widget_state.force_data_update(); } } } - _ => {} } } } @@ -3080,13 +2745,23 @@ impl App { self.is_expanded || !self.app_config_fields.use_basic_mode } - fn is_drawing_gap(&self, widget: &BottomWidget) -> bool { + fn header_offset(&self, widget: &BottomWidget) -> u16 { if let (Some((_tlc_x, tlc_y)), Some((_brc_x, brc_y))) = (widget.top_left_corner, widget.bottom_right_corner) { - brc_y - tlc_y >= constants::TABLE_GAP_HEIGHT_LIMIT + let height_diff = brc_y - tlc_y; + if height_diff >= constants::TABLE_GAP_HEIGHT_LIMIT { + 1 + self.app_config_fields.table_gap + } else { + let min_height_for_header = if self.is_drawing_border() { 3 } else { 1 }; + if height_diff > min_height_for_header { + 1 + } else { + 0 + } + } } else { - self.app_config_fields.table_gap == 0 + 1 + self.app_config_fields.table_gap } } } diff --git a/src/app/data_farmer.rs b/src/app/data_farmer.rs index 1860c91bc..0f9d65d7e 100644 --- a/src/app/data_farmer.rs +++ b/src/app/data_farmer.rs @@ -1,27 +1,32 @@ -/// In charge of cleaning, processing, and managing data. I couldn't think of -/// a better name for the file. Since I called data collection "harvesting", -/// then this is the farmer I guess. -/// -/// Essentially the main goal is to shift the initial calculation and distribution -/// of joiner points and data to one central location that will only do it -/// *once* upon receiving the data --- as opposed to doing it on canvas draw, -/// which will be a costly process. -/// -/// This will also handle the *cleaning* of stale data. That should be done -/// in some manner (timer on another thread, some loop) that will occasionally -/// call the purging function. Failure to do so *will* result in a growing -/// memory usage and higher CPU usage - you will be trying to process more and -/// more points as this is used! +//! In charge of cleaning, processing, and managing data. I couldn't think of +//! a better name for the file. Since I called data collection "harvesting", +//! then this is the farmer I guess. +//! +//! Essentially the main goal is to shift the initial calculation and distribution +//! of joiner points and data to one central location that will only do it +//! *once* upon receiving the data --- as opposed to doing it on canvas draw, +//! which will be a costly process. +//! +//! This will also handle the *cleaning* of stale data. That should be done +//! in some manner (timer on another thread, some loop) that will occasionally +//! call the purging function. Failure to do so *will* result in a growing +//! memory usage and higher CPU usage - you will be trying to process more and +//! more points as this is used! + use once_cell::sync::Lazy; +use fxhash::FxHashMap; +use itertools::Itertools; + use std::{time::Instant, vec::Vec}; #[cfg(feature = "battery")] use crate::data_harvester::batteries; use crate::{ - data_harvester::{cpu, disks, memory, network, processes, temperature, Data}, + data_harvester::{cpu, disks, memory, network, processes::ProcessHarvest, temperature, Data}, utils::gen_util::{get_decimal_bytes, GIGA_LIMIT}, + Pid, }; use regex::Regex; @@ -38,6 +43,97 @@ pub struct TimedData { pub swap_data: Option, } +pub type StringPidMap = FxHashMap>; + +#[derive(Clone, Debug, Default)] +pub struct ProcessData { + /// A PID to process data map. + pub process_harvest: FxHashMap, + + /// A mapping from a process name to any PID with that name. + pub name_pid_map: StringPidMap, + + /// A mapping from a process command to any PID with that name. + pub cmd_pid_map: StringPidMap, + + /// A mapping between a process PID to any children process PIDs. + pub process_parent_mapping: FxHashMap>, + + /// PIDs corresponding to processes that have no parents. + pub orphan_pids: Vec, +} + +impl ProcessData { + fn ingest(&mut self, list_of_processes: Vec) { + // TODO: [Optimization] Probably more efficient to all of this in the data collection step, but it's fine for now. + self.name_pid_map.clear(); + self.cmd_pid_map.clear(); + self.process_parent_mapping.clear(); + + // Reverse as otherwise the pid mappings are in the wrong order. + list_of_processes.iter().rev().for_each(|process_harvest| { + if let Some(entry) = self.name_pid_map.get_mut(&process_harvest.name) { + entry.push(process_harvest.pid); + } else { + self.name_pid_map + .insert(process_harvest.name.to_string(), vec![process_harvest.pid]); + } + + if let Some(entry) = self.cmd_pid_map.get_mut(&process_harvest.command) { + entry.push(process_harvest.pid); + } else { + self.cmd_pid_map.insert( + process_harvest.command.to_string(), + vec![process_harvest.pid], + ); + } + + if let Some(parent_pid) = process_harvest.parent_pid { + if let Some(entry) = self.process_parent_mapping.get_mut(&parent_pid) { + entry.push(process_harvest.pid); + } else { + self.process_parent_mapping + .insert(parent_pid, vec![process_harvest.pid]); + } + } + }); + + self.name_pid_map.shrink_to_fit(); + self.cmd_pid_map.shrink_to_fit(); + self.process_parent_mapping.shrink_to_fit(); + + let process_pid_map = list_of_processes + .into_iter() + .map(|process| (process.pid, process)) + .collect(); + self.process_harvest = process_pid_map; + + // This also needs a quick sort + reverse to be in the correct order. + self.orphan_pids = { + let mut res: Vec = self + .process_harvest + .iter() + .filter_map(|(pid, process_harvest)| { + if let Some(parent_pid) = process_harvest.parent_pid { + if self.process_harvest.contains_key(&parent_pid) { + None + } else { + Some(*pid) + } + } else { + Some(*pid) + } + }) + .sorted() + .collect(); + + res.reverse(); + + res + } + } +} + /// AppCollection represents the pooled data stored within the main app /// thread. Basically stores a (occasionally cleaned) record of the data /// collected, and what is needed to convert into a displayable form. @@ -57,7 +153,7 @@ pub struct DataCollection { pub swap_harvest: memory::MemHarvest, pub cpu_harvest: cpu::CpuHarvest, pub load_avg_harvest: cpu::LoadAvgHarvest, - pub process_harvest: Vec, + pub process_data: ProcessData, pub disk_harvest: Vec, pub io_harvest: disks::IoHarvest, pub io_labels_and_prev: Vec<((u64, u64), (u64, u64))>, @@ -78,7 +174,7 @@ impl Default for DataCollection { swap_harvest: memory::MemHarvest::default(), cpu_harvest: cpu::CpuHarvest::default(), load_avg_harvest: cpu::LoadAvgHarvest::default(), - process_harvest: Vec::default(), + process_data: Default::default(), disk_harvest: Vec::default(), io_harvest: disks::IoHarvest::default(), io_labels_and_prev: Vec::default(), @@ -97,7 +193,7 @@ impl DataCollection { self.memory_harvest = memory::MemHarvest::default(); self.swap_harvest = memory::MemHarvest::default(); self.cpu_harvest = cpu::CpuHarvest::default(); - self.process_harvest = Vec::default(); + self.process_data = Default::default(); self.disk_harvest = Vec::default(); self.io_harvest = disks::IoHarvest::default(); self.io_labels_and_prev = Vec::default(); @@ -108,10 +204,14 @@ impl DataCollection { } } - pub fn set_frozen_time(&mut self) { + pub fn freeze(&mut self) { self.frozen_instant = Some(self.current_instant); } + pub fn thaw(&mut self) { + self.frozen_instant = None; + } + pub fn clean_data(&mut self, max_time_millis: u64) { let current_time = Instant::now(); @@ -319,8 +419,8 @@ impl DataCollection { self.io_harvest = io; } - fn eat_proc(&mut self, list_of_processes: Vec) { - self.process_harvest = list_of_processes; + fn eat_proc(&mut self, list_of_processes: Vec) { + self.process_data.ingest(list_of_processes); } #[cfg(feature = "battery")] diff --git a/src/app/data_harvester.rs b/src/app/data_harvester.rs index 9dcc377cc..979bdadb1 100644 --- a/src/app/data_harvester.rs +++ b/src/app/data_harvester.rs @@ -104,6 +104,9 @@ pub struct DataCollector { #[cfg(feature = "battery")] battery_list: Option>, filters: DataFilters, + + #[cfg(target_family = "unix")] + user_table: self::processes::UserTable, } impl DataCollector { @@ -133,6 +136,8 @@ impl DataCollector { #[cfg(feature = "battery")] battery_list: None, filters, + #[cfg(target_family = "unix")] + user_table: Default::default(), } } @@ -191,7 +196,7 @@ impl DataCollector { }; } - pub fn set_collected_data(&mut self, used_widgets: UsedWidgets) { + pub fn set_data_collection(&mut self, used_widgets: UsedWidgets) { self.widgets_to_harvest = used_widgets; } @@ -270,15 +275,28 @@ impl DataCollector { .duration_since(self.last_collection_time) .as_secs(), self.mem_total_kb, + &mut self.user_table, ) } #[cfg(not(target_os = "linux"))] { - processes::get_process_data( - &self.sys, - self.use_current_cpu_total, - self.mem_total_kb, - ) + #[cfg(target_family = "unix")] + { + processes::get_process_data( + &self.sys, + self.use_current_cpu_total, + self.mem_total_kb, + &mut self.user_table, + ) + } + #[cfg(not(target_family = "unix"))] + { + processes::get_process_data( + &self.sys, + self.use_current_cpu_total, + self.mem_total_kb, + ) + } } } { self.data.list_of_processes = Some(process_list); diff --git a/src/app/data_harvester/network/heim.rs b/src/app/data_harvester/network/heim.rs index 3c12fd739..b7980ddb0 100644 --- a/src/app/data_harvester/network/heim.rs +++ b/src/app/data_harvester/network/heim.rs @@ -3,7 +3,7 @@ use super::NetworkHarvest; use std::time::Instant; -// FIXME: Eventually make it so that this thing also takes individual usage into account, so we can allow for showing per-interface! +// TODO: Eventually make it so that this thing also takes individual usage into account, so we can show per-interface! pub async fn get_network_data( prev_net_access_time: Instant, prev_net_rx: &mut u64, prev_net_tx: &mut u64, curr_time: Instant, actually_get: bool, filter: &Option, diff --git a/src/app/data_harvester/processes/linux.rs b/src/app/data_harvester/processes/linux.rs index 081af802e..1e91f93af 100644 --- a/src/app/data_harvester/processes/linux.rs +++ b/src/app/data_harvester/processes/linux.rs @@ -5,7 +5,7 @@ use std::collections::hash_map::Entry; use crate::utils::error::{self, BottomError}; use crate::Pid; -use super::ProcessHarvest; +use super::{ProcessHarvest, UserTable}; use sysinfo::ProcessStatus; @@ -120,6 +120,7 @@ fn get_linux_cpu_usage( fn read_proc( prev_proc: &PrevProcDetails, stat: &Stat, cpu_usage: f64, cpu_fraction: f64, use_current_cpu_total: bool, time_difference_in_secs: u64, mem_total_kb: u64, + user_table: &mut UserTable, ) -> error::Result<(ProcessHarvest, u64)> { use std::convert::TryFrom; @@ -156,7 +157,10 @@ fn read_proc( }; let process_state_char = stat.state; - let process_state = ProcessStatus::from(process_state_char).to_string(); + let process_state = ( + ProcessStatus::from(process_state_char).to_string(), + process_state_char, + ); let (cpu_usage_percent, new_process_times) = get_linux_cpu_usage( stat, cpu_usage, @@ -199,7 +203,7 @@ fn read_proc( (0, 0, 0, 0) }; - let uid = Some(process.owner); + let uid = process.owner; Ok(( ProcessHarvest { @@ -215,8 +219,11 @@ fn read_proc( total_read_bytes, total_write_bytes, process_state, - process_state_char, uid, + user: user_table + .get_uid_to_username_mapping(uid) + .map(Into::into) + .unwrap_or_else(|_| "N/A".into()), }, new_process_times, )) @@ -225,7 +232,7 @@ fn read_proc( pub fn get_process_data( prev_idle: &mut f64, prev_non_idle: &mut f64, pid_mapping: &mut FxHashMap, use_current_cpu_total: bool, - time_difference_in_secs: u64, mem_total_kb: u64, + time_difference_in_secs: u64, mem_total_kb: u64, user_table: &mut UserTable, ) -> crate::utils::error::Result> { // TODO: [PROC THREADS] Add threads @@ -268,6 +275,7 @@ pub fn get_process_data( use_current_cpu_total, time_difference_in_secs, mem_total_kb, + user_table, ) { prev_proc_details.cpu_time = new_process_times; prev_proc_details.total_read_bytes = diff --git a/src/app/data_harvester/processes/macos.rs b/src/app/data_harvester/processes/macos.rs index f08e17ca9..0c401b0ae 100644 --- a/src/app/data_harvester/processes/macos.rs +++ b/src/app/data_harvester/processes/macos.rs @@ -3,6 +3,8 @@ use super::ProcessHarvest; use sysinfo::{PidExt, ProcessExt, ProcessStatus, ProcessorExt, System, SystemExt}; +use crate::data_harvester::processes::UserTable; + fn get_macos_process_cpu_usage( pids: &[i32], ) -> std::io::Result> { @@ -35,7 +37,7 @@ fn get_macos_process_cpu_usage( } pub fn get_process_data( - sys: &System, use_current_cpu_total: bool, mem_total_kb: u64, + sys: &System, use_current_cpu_total: bool, mem_total_kb: u64, user_table: &mut UserTable, ) -> crate::utils::error::Result> { let mut process_vector: Vec = Vec::new(); let process_hashmap = sys.processes(); @@ -86,6 +88,11 @@ pub fn get_process_data( }; let disk_usage = process_val.disk_usage(); + let process_state = { + let ps = process_val.status(); + (ps.to_string(), convert_process_status_to_char(ps)) + }; + let uid = process_val.uid; process_vector.push(ProcessHarvest { pid: process_val.pid().as_u32() as _, parent_pid: process_val.parent().map(|p| p.as_u32() as _), @@ -102,16 +109,19 @@ pub fn get_process_data( write_bytes_per_sec: disk_usage.written_bytes, total_read_bytes: disk_usage.total_read_bytes, total_write_bytes: disk_usage.total_written_bytes, - process_state: process_val.status().to_string(), - process_state_char: convert_process_status_to_char(process_val.status()), - uid: Some(process_val.uid), + process_state, + uid, + user: user_table + .get_uid_to_username_mapping(uid) + .map(Into::into) + .unwrap_or_else(|_| "N/A".into()), }); } let unknown_state = ProcessStatus::Unknown(0).to_string(); let cpu_usage_unknown_pids: Vec = process_vector .iter() - .filter(|process| process.process_state == unknown_state) + .filter(|process| process.process_state.0 == unknown_state) .map(|process| process.pid) .collect(); let cpu_usages = get_macos_process_cpu_usage(&cpu_usage_unknown_pids)?; diff --git a/src/app/data_harvester/processes/mod.rs b/src/app/data_harvester/processes/mod.rs index 283080b3c..40662d7ad 100644 --- a/src/app/data_harvester/processes/mod.rs +++ b/src/app/data_harvester/processes/mod.rs @@ -25,73 +25,64 @@ cfg_if::cfg_if! { use crate::Pid; -// TODO: Add value so we know if it's sorted ascending or descending by default? -#[derive(Clone, PartialEq, Eq, Hash, Debug)] -pub enum ProcessSorting { - CpuPercent, - Mem, - MemPercent, - Pid, - ProcessName, - Command, - ReadPerSecond, - WritePerSecond, - TotalRead, - TotalWrite, - State, - User, - Count, -} - -impl std::fmt::Display for ProcessSorting { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match &self { - ProcessSorting::CpuPercent => "CPU%", - ProcessSorting::MemPercent => "Mem%", - ProcessSorting::Mem => "Mem", - ProcessSorting::ReadPerSecond => "R/s", - ProcessSorting::WritePerSecond => "W/s", - ProcessSorting::TotalRead => "T.Read", - ProcessSorting::TotalWrite => "T.Write", - ProcessSorting::State => "State", - ProcessSorting::ProcessName => "Name", - ProcessSorting::Command => "Command", - ProcessSorting::Pid => "PID", - ProcessSorting::Count => "Count", - ProcessSorting::User => "User", - } - ) - } -} - -impl Default for ProcessSorting { - fn default() -> Self { - ProcessSorting::CpuPercent - } -} - #[derive(Debug, Clone, Default)] pub struct ProcessHarvest { + /// The pid of the process. pub pid: Pid, - pub parent_pid: Option, // Remember, parent_pid 0 is root... + + /// The parent PID of the process. Remember, parent_pid 0 is root. + pub parent_pid: Option, + + /// CPU usage as a percentage. pub cpu_usage_percent: f64, + + /// Memory usage as a percentage. pub mem_usage_percent: f64, + + /// Memory usage as bytes. pub mem_usage_bytes: u64, - // pub rss_kb: u64, - // pub virt_kb: u64, + + /// The name of the process. pub name: String, + + /// The exact command for the process. pub command: String, + + /// Bytes read per second. pub read_bytes_per_sec: u64, + + /// Bytes written per second. pub write_bytes_per_sec: u64, + + /// The total number of bytes read by the process. pub total_read_bytes: u64, + + /// The total number of bytes written by the process. pub total_write_bytes: u64, - pub process_state: String, - pub process_state_char: char, - /// This is the *effective* user ID. + /// The current state of the process (e.g. zombie, asleep) + pub process_state: (String, char), + + /// This is the *effective* user ID of the process. This is only used on Unix platforms. #[cfg(target_family = "unix")] - pub uid: Option, + pub uid: libc::uid_t, + + /// This is the process' user. This is only used on Unix platforms. + #[cfg(target_family = "unix")] + pub user: std::borrow::Cow<'static, str>, + // TODO: Additional fields + // pub rss_kb: u64, + // pub virt_kb: u64, +} + +impl ProcessHarvest { + pub(crate) fn add(&mut self, rhs: &ProcessHarvest) { + self.cpu_usage_percent += rhs.cpu_usage_percent; + self.mem_usage_bytes += rhs.mem_usage_bytes; + self.mem_usage_percent += rhs.mem_usage_percent; + self.read_bytes_per_sec += rhs.read_bytes_per_sec; + self.write_bytes_per_sec += rhs.write_bytes_per_sec; + self.total_read_bytes += rhs.total_read_bytes; + self.total_write_bytes += rhs.total_write_bytes; + } } diff --git a/src/app/data_harvester/processes/unix.rs b/src/app/data_harvester/processes/unix.rs index 8fadc590c..75f70bec9 100644 --- a/src/app/data_harvester/processes/unix.rs +++ b/src/app/data_harvester/processes/unix.rs @@ -1,10 +1,12 @@ //! Unix-specific parts of process collection. +use fxhash::FxHashMap; + use crate::utils::error; #[derive(Debug, Default)] pub struct UserTable { - pub uid_user_mapping: std::collections::HashMap, + pub uid_user_mapping: FxHashMap, } impl UserTable { diff --git a/src/app/data_harvester/processes/windows.rs b/src/app/data_harvester/processes/windows.rs index af9ead5f7..a35648740 100644 --- a/src/app/data_harvester/processes/windows.rs +++ b/src/app/data_harvester/processes/windows.rs @@ -55,6 +55,7 @@ pub fn get_process_data( }; let disk_usage = process_val.disk_usage(); + let process_state = (process_val.status().to_string(), 'R'); process_vector.push(ProcessHarvest { pid: process_val.pid().as_u32() as _, parent_pid: process_val.parent().map(|p| p.as_u32() as _), @@ -71,8 +72,7 @@ pub fn get_process_data( write_bytes_per_sec: disk_usage.written_bytes, total_read_bytes: disk_usage.total_read_bytes, total_write_bytes: disk_usage.total_written_bytes, - process_state: process_val.status().to_string(), - process_state_char: 'R', + process_state, }); } diff --git a/src/app/query.rs b/src/app/query.rs index 0f18c901e..46c400226 100644 --- a/src/app/query.rs +++ b/src/app/query.rs @@ -1,449 +1,434 @@ -use super::ProcWidgetState; -use crate::{ - data_conversion::ConvertedProcessData, - utils::error::{ - BottomError::{self, QueryError}, - Result, - }, +use crate::utils::error::{ + BottomError::{self, QueryError}, + Result, }; use std::fmt::Debug; use std::{borrow::Cow, collections::VecDeque}; +use super::data_harvester::processes::ProcessHarvest; + const DELIMITER_LIST: [char; 6] = ['=', '>', '<', '(', ')', '\"']; const COMPARISON_LIST: [&str; 3] = [">", "=", "<"]; const OR_LIST: [&str; 2] = ["or", "||"]; const AND_LIST: [&str; 2] = ["and", "&&"]; -/// I only separated this as otherwise, the states.rs file gets huge... and this should -/// belong in another file anyways, IMO. -pub trait ProcessQuery { - /// In charge of parsing the given query. - /// We are defining the following language for a query (case-insensitive prefixes): - /// - /// - Process names: No prefix required, can use regex, match word, or case. - /// Enclosing anything, including prefixes, in quotes, means we treat it as an entire process - /// rather than a prefix. - /// - PIDs: Use prefix `pid`, can use regex or match word (case is irrelevant). - /// - CPU: Use prefix `cpu`, cannot use r/m/c (regex, match word, case). Can compare. - /// - MEM: Use prefix `mem`, cannot use r/m/c. Can compare. - /// - STATE: Use prefix `state`, can use regex, match word, or case. - /// - USER: Use prefix `user`, can use regex, match word, or case. - /// - Read/s: Use prefix `r`. Can compare. - /// - Write/s: Use prefix `w`. Can compare. - /// - Total read: Use prefix `read`. Can compare. - /// - Total write: Use prefix `write`. Can compare. - /// - /// For queries, whitespaces are our delimiters. We will merge together any adjacent non-prefixed - /// or quoted elements after splitting to treat as process names. - /// Furthermore, we want to support boolean joiners like AND and OR, and brackets. - fn parse_query(&self) -> Result; -} +/// In charge of parsing the given query. +/// We are defining the following language for a query (case-insensitive prefixes): +/// +/// - Process names: No prefix required, can use regex, match word, or case. +/// Enclosing anything, including prefixes, in quotes, means we treat it as an entire process +/// rather than a prefix. +/// - PIDs: Use prefix `pid`, can use regex or match word (case is irrelevant). +/// - CPU: Use prefix `cpu`, cannot use r/m/c (regex, match word, case). Can compare. +/// - MEM: Use prefix `mem`, cannot use r/m/c. Can compare. +/// - STATE: Use prefix `state`, can use regex, match word, or case. +/// - USER: Use prefix `user`, can use regex, match word, or case. +/// - Read/s: Use prefix `r`. Can compare. +/// - Write/s: Use prefix `w`. Can compare. +/// - Total read: Use prefix `read`. Can compare. +/// - Total write: Use prefix `write`. Can compare. +/// +/// For queries, whitespaces are our delimiters. We will merge together any adjacent non-prefixed +/// or quoted elements after splitting to treat as process names. +/// Furthermore, we want to support boolean joiners like AND and OR, and brackets. +pub fn parse_query( + search_query: &str, is_searching_whole_word: bool, is_ignoring_case: bool, + is_searching_with_regex: bool, +) -> Result { + fn process_string_to_filter(query: &mut VecDeque) -> Result { + let lhs = process_or(query)?; + let mut list_of_ors = vec![lhs]; + + while query.front().is_some() { + list_of_ors.push(process_or(query)?); + } -impl ProcessQuery for ProcWidgetState { - fn parse_query(&self) -> Result { - fn process_string_to_filter(query: &mut VecDeque) -> Result { - let lhs = process_or(query)?; - let mut list_of_ors = vec![lhs]; + Ok(Query { query: list_of_ors }) + } - while query.front().is_some() { - list_of_ors.push(process_or(query)?); - } + fn process_or(query: &mut VecDeque) -> Result { + let mut lhs = process_and(query)?; + let mut rhs: Option> = None; - Ok(Query { query: list_of_ors }) - } + while let Some(queue_top) = query.front() { + // debug!("OR QT: {:?}", queue_top); + if OR_LIST.contains(&queue_top.to_lowercase().as_str()) { + query.pop_front(); + rhs = Some(Box::new(process_and(query)?)); - fn process_or(query: &mut VecDeque) -> Result { - let mut lhs = process_and(query)?; - let mut rhs: Option> = None; - - while let Some(queue_top) = query.front() { - // debug!("OR QT: {:?}", queue_top); - if OR_LIST.contains(&queue_top.to_lowercase().as_str()) { - query.pop_front(); - rhs = Some(Box::new(process_and(query)?)); - - if let Some(queue_next) = query.front() { - if OR_LIST.contains(&queue_next.to_lowercase().as_str()) { - // Must merge LHS and RHS - lhs = And { - lhs: Prefix { - or: Some(Box::new(Or { lhs, rhs })), - regex_prefix: None, - compare_prefix: None, - }, - rhs: None, - }; - rhs = None; - } - } else { - break; + if let Some(queue_next) = query.front() { + if OR_LIST.contains(&queue_next.to_lowercase().as_str()) { + // Must merge LHS and RHS + lhs = And { + lhs: Prefix { + or: Some(Box::new(Or { lhs, rhs })), + regex_prefix: None, + compare_prefix: None, + }, + rhs: None, + }; + rhs = None; } - } else if COMPARISON_LIST.contains(&queue_top.to_lowercase().as_str()) { - return Err(QueryError(Cow::Borrowed("Comparison not valid here"))); } else { break; } + } else if COMPARISON_LIST.contains(&queue_top.to_lowercase().as_str()) { + return Err(QueryError(Cow::Borrowed("Comparison not valid here"))); + } else { + break; } - - Ok(Or { lhs, rhs }) } - fn process_and(query: &mut VecDeque) -> Result { - let mut lhs = process_prefix(query, false)?; - let mut rhs: Option> = None; - - while let Some(queue_top) = query.front() { - // debug!("AND QT: {:?}", queue_top); - if AND_LIST.contains(&queue_top.to_lowercase().as_str()) { - query.pop_front(); - - rhs = Some(Box::new(process_prefix(query, false)?)); - - if let Some(next_queue_top) = query.front() { - if AND_LIST.contains(&next_queue_top.to_lowercase().as_str()) { - // Must merge LHS and RHS - lhs = Prefix { - or: Some(Box::new(Or { - lhs: And { lhs, rhs }, - rhs: None, - })), - regex_prefix: None, - compare_prefix: None, - }; - rhs = None; - } else { - break; - } + Ok(Or { lhs, rhs }) + } + + fn process_and(query: &mut VecDeque) -> Result { + let mut lhs = process_prefix(query, false)?; + let mut rhs: Option> = None; + + while let Some(queue_top) = query.front() { + // debug!("AND QT: {:?}", queue_top); + if AND_LIST.contains(&queue_top.to_lowercase().as_str()) { + query.pop_front(); + + rhs = Some(Box::new(process_prefix(query, false)?)); + + if let Some(next_queue_top) = query.front() { + if AND_LIST.contains(&next_queue_top.to_lowercase().as_str()) { + // Must merge LHS and RHS + lhs = Prefix { + or: Some(Box::new(Or { + lhs: And { lhs, rhs }, + rhs: None, + })), + regex_prefix: None, + compare_prefix: None, + }; + rhs = None; } else { break; } - } else if COMPARISON_LIST.contains(&queue_top.to_lowercase().as_str()) { - return Err(QueryError(Cow::Borrowed("Comparison not valid here"))); } else { break; } + } else if COMPARISON_LIST.contains(&queue_top.to_lowercase().as_str()) { + return Err(QueryError(Cow::Borrowed("Comparison not valid here"))); + } else { + break; } - - Ok(And { lhs, rhs }) } - fn process_prefix(query: &mut VecDeque, inside_quotation: bool) -> Result { - if let Some(queue_top) = query.pop_front() { - if inside_quotation { - if queue_top == "\"" { - // This means we hit something like "". Return an empty prefix, and to deal with - // the close quote checker, add one to the top of the stack. Ugly fix but whatever. - query.push_front("\"".to_string()); - return Ok(Prefix { - or: None, - regex_prefix: Some(( - PrefixType::Name, - StringQuery::Value(String::default()), - )), - compare_prefix: None, - }); - } else { - let mut quoted_string = queue_top; - while let Some(next_str) = query.front() { - if next_str == "\"" { - // Stop! - break; - } else { - quoted_string.push_str(next_str); - query.pop_front(); - } - } - return Ok(Prefix { - or: None, - regex_prefix: Some(( - PrefixType::Name, - StringQuery::Value(quoted_string), - )), - compare_prefix: None, - }); - } - } else if queue_top == "(" { - if query.is_empty() { - return Err(QueryError(Cow::Borrowed("Missing closing parentheses"))); - } - - let mut list_of_ors = VecDeque::new(); + Ok(And { lhs, rhs }) + } - while let Some(in_paren_query_top) = query.front() { - if in_paren_query_top != ")" { - list_of_ors.push_back(process_or(query)?); - } else { + fn process_prefix(query: &mut VecDeque, inside_quotation: bool) -> Result { + if let Some(queue_top) = query.pop_front() { + if inside_quotation { + if queue_top == "\"" { + // This means we hit something like "". Return an empty prefix, and to deal with + // the close quote checker, add one to the top of the stack. Ugly fix but whatever. + query.push_front("\"".to_string()); + return Ok(Prefix { + or: None, + regex_prefix: Some(( + PrefixType::Name, + StringQuery::Value(String::default()), + )), + compare_prefix: None, + }); + } else { + let mut quoted_string = queue_top; + while let Some(next_str) = query.front() { + if next_str == "\"" { + // Stop! break; + } else { + quoted_string.push_str(next_str); + query.pop_front(); } } + return Ok(Prefix { + or: None, + regex_prefix: Some((PrefixType::Name, StringQuery::Value(quoted_string))), + compare_prefix: None, + }); + } + } else if queue_top == "(" { + if query.is_empty() { + return Err(QueryError(Cow::Borrowed("Missing closing parentheses"))); + } - // Ensure not empty - if list_of_ors.is_empty() { - return Err(QueryError("No values within parentheses group".into())); + let mut list_of_ors = VecDeque::new(); + + while let Some(in_paren_query_top) = query.front() { + if in_paren_query_top != ")" { + list_of_ors.push_back(process_or(query)?); + } else { + break; } + } - // Now convert this back to a OR... - let initial_or = Or { - lhs: And { - lhs: Prefix { - or: list_of_ors.pop_front().map(Box::new), - compare_prefix: None, - regex_prefix: None, - }, - rhs: None, + // Ensure not empty + if list_of_ors.is_empty() { + return Err(QueryError("No values within parentheses group".into())); + } + + // Now convert this back to a OR... + let initial_or = Or { + lhs: And { + lhs: Prefix { + or: list_of_ors.pop_front().map(Box::new), + compare_prefix: None, + regex_prefix: None, }, rhs: None, - }; - let returned_or = list_of_ors.into_iter().fold(initial_or, |lhs, rhs| Or { - lhs: And { - lhs: Prefix { - or: Some(Box::new(lhs)), - compare_prefix: None, - regex_prefix: None, - }, - rhs: Some(Box::new(Prefix { - or: Some(Box::new(rhs)), - compare_prefix: None, - regex_prefix: None, - })), + }, + rhs: None, + }; + let returned_or = list_of_ors.into_iter().fold(initial_or, |lhs, rhs| Or { + lhs: And { + lhs: Prefix { + or: Some(Box::new(lhs)), + compare_prefix: None, + regex_prefix: None, }, - rhs: None, - }); - - if let Some(close_paren) = query.pop_front() { - if close_paren == ")" { - return Ok(Prefix { - or: Some(Box::new(returned_or)), - regex_prefix: None, - compare_prefix: None, - }); - } else { - return Err(QueryError("Missing closing parentheses".into())); - } + rhs: Some(Box::new(Prefix { + or: Some(Box::new(rhs)), + compare_prefix: None, + regex_prefix: None, + })), + }, + rhs: None, + }); + + if let Some(close_paren) = query.pop_front() { + if close_paren == ")" { + return Ok(Prefix { + or: Some(Box::new(returned_or)), + regex_prefix: None, + compare_prefix: None, + }); } else { return Err(QueryError("Missing closing parentheses".into())); } - } else if queue_top == ")" { - return Err(QueryError("Missing opening parentheses".into())); - } else if queue_top == "\"" { - // Similar to parentheses, trap and check for missing closing quotes. Note, however, that we - // will DIRECTLY call another process_prefix call... - - let prefix = process_prefix(query, true)?; - if let Some(close_paren) = query.pop_front() { - if close_paren == "\"" { - return Ok(prefix); - } else { - return Err(QueryError("Missing closing quotation".into())); - } + } else { + return Err(QueryError("Missing closing parentheses".into())); + } + } else if queue_top == ")" { + return Err(QueryError("Missing opening parentheses".into())); + } else if queue_top == "\"" { + // Similar to parentheses, trap and check for missing closing quotes. Note, however, that we + // will DIRECTLY call another process_prefix call... + + let prefix = process_prefix(query, true)?; + if let Some(close_paren) = query.pop_front() { + if close_paren == "\"" { + return Ok(prefix); } else { return Err(QueryError("Missing closing quotation".into())); } } else { - // Get prefix type... - let prefix_type = queue_top.parse::()?; - let content = if let PrefixType::Name = prefix_type { - Some(queue_top) - } else { - query.pop_front() - }; + return Err(QueryError("Missing closing quotation".into())); + } + } else { + // Get prefix type... + let prefix_type = queue_top.parse::()?; + let content = if let PrefixType::Name = prefix_type { + Some(queue_top) + } else { + query.pop_front() + }; + + if let Some(content) = content { + match &prefix_type { + PrefixType::Name => { + return Ok(Prefix { + or: None, + regex_prefix: Some((prefix_type, StringQuery::Value(content))), + compare_prefix: None, + }) + } + PrefixType::Pid | PrefixType::State | PrefixType::User => { + // We have to check if someone put an "="... + if content == "=" { + // Check next string if possible + if let Some(queue_next) = query.pop_front() { + // TODO: [Query, ???] Need to consider the following cases: + // - (test) + // - (test + // - test) + // These are split into 2 to 3 different strings due to parentheses being + // delimiters in our query system. + // + // Do we want these to be valid? They should, as a string, right? - if let Some(content) = content { - match &prefix_type { - PrefixType::Name => { - return Ok(Prefix { - or: None, - regex_prefix: Some((prefix_type, StringQuery::Value(content))), - compare_prefix: None, - }) - } - PrefixType::Pid | PrefixType::State | PrefixType::User => { - // We have to check if someone put an "="... - if content == "=" { - // Check next string if possible - if let Some(queue_next) = query.pop_front() { - // TODO: Need to consider the following cases: - // - (test) - // - (test - // - test) - // These are split into 2 to 3 different strings due to parentheses being - // delimiters in our query system. - // - // Do we want these to be valid? They should, as a string, right? - - return Ok(Prefix { - or: None, - regex_prefix: Some(( - prefix_type, - StringQuery::Value(queue_next), - )), - compare_prefix: None, - }); - } - } else { return Ok(Prefix { or: None, regex_prefix: Some(( prefix_type, - StringQuery::Value(content), + StringQuery::Value(queue_next), )), compare_prefix: None, }); } + } else { + return Ok(Prefix { + or: None, + regex_prefix: Some((prefix_type, StringQuery::Value(content))), + compare_prefix: None, + }); } - _ => { - // Now we gotta parse the content... yay. + } + _ => { + // Now we gotta parse the content... yay. - let mut condition: Option = None; - let mut value: Option = None; + let mut condition: Option = None; + let mut value: Option = None; - if content == "=" { - condition = Some(QueryComparison::Equal); - if let Some(queue_next) = query.pop_front() { - value = queue_next.parse::().ok(); - } else { - return Err(QueryError("Missing value".into())); - } - } else if content == ">" || content == "<" { - // We also have to check if the next string is an "="... - if let Some(queue_next) = query.pop_front() { - if queue_next == "=" { - condition = Some(if content == ">" { - QueryComparison::GreaterOrEqual - } else { - QueryComparison::LessOrEqual - }); - if let Some(queue_next_next) = query.pop_front() { - value = queue_next_next.parse::().ok(); - } else { - return Err(QueryError("Missing value".into())); - } + if content == "=" { + condition = Some(QueryComparison::Equal); + if let Some(queue_next) = query.pop_front() { + value = queue_next.parse::().ok(); + } else { + return Err(QueryError("Missing value".into())); + } + } else if content == ">" || content == "<" { + // We also have to check if the next string is an "="... + if let Some(queue_next) = query.pop_front() { + if queue_next == "=" { + condition = Some(if content == ">" { + QueryComparison::GreaterOrEqual + } else { + QueryComparison::LessOrEqual + }); + if let Some(queue_next_next) = query.pop_front() { + value = queue_next_next.parse::().ok(); } else { - condition = Some(if content == ">" { - QueryComparison::Greater - } else { - QueryComparison::Less - }); - value = queue_next.parse::().ok(); + return Err(QueryError("Missing value".into())); } } else { - return Err(QueryError("Missing value".into())); + condition = Some(if content == ">" { + QueryComparison::Greater + } else { + QueryComparison::Less + }); + value = queue_next.parse::().ok(); } + } else { + return Err(QueryError("Missing value".into())); } + } - if let Some(condition) = condition { - if let Some(read_value) = value { - // Now we want to check one last thing - is there a unit? - // If no unit, assume base. - // Furthermore, base must be PEEKED at initially, and will - // require (likely) prefix_type specific checks - // Lastly, if it *is* a unit, remember to POP! - - let mut value = read_value; - - match prefix_type { - PrefixType::MemBytes - | PrefixType::Rps - | PrefixType::Wps - | PrefixType::TRead - | PrefixType::TWrite => { - if let Some(potential_unit) = query.front() { - match potential_unit.to_lowercase().as_str() { - "tb" => { - value *= 1_000_000_000_000.0; - query.pop_front(); - } - "tib" => { - value *= 1_099_511_627_776.0; - query.pop_front(); - } - "gb" => { - value *= 1_000_000_000.0; - query.pop_front(); - } - "gib" => { - value *= 1_073_741_824.0; - query.pop_front(); - } - "mb" => { - value *= 1_000_000.0; - query.pop_front(); - } - "mib" => { - value *= 1_048_576.0; - query.pop_front(); - } - "kb" => { - value *= 1000.0; - query.pop_front(); - } - "kib" => { - value *= 1024.0; - query.pop_front(); - } - "b" => { - // Just gotta pop. - query.pop_front(); - } - _ => {} + if let Some(condition) = condition { + if let Some(read_value) = value { + // Now we want to check one last thing - is there a unit? + // If no unit, assume base. + // Furthermore, base must be PEEKED at initially, and will + // require (likely) prefix_type specific checks + // Lastly, if it *is* a unit, remember to POP! + + let mut value = read_value; + + match prefix_type { + PrefixType::MemBytes + | PrefixType::Rps + | PrefixType::Wps + | PrefixType::TRead + | PrefixType::TWrite => { + if let Some(potential_unit) = query.front() { + match potential_unit.to_lowercase().as_str() { + "tb" => { + value *= 1_000_000_000_000.0; + query.pop_front(); + } + "tib" => { + value *= 1_099_511_627_776.0; + query.pop_front(); + } + "gb" => { + value *= 1_000_000_000.0; + query.pop_front(); } + "gib" => { + value *= 1_073_741_824.0; + query.pop_front(); + } + "mb" => { + value *= 1_000_000.0; + query.pop_front(); + } + "mib" => { + value *= 1_048_576.0; + query.pop_front(); + } + "kb" => { + value *= 1000.0; + query.pop_front(); + } + "kib" => { + value *= 1024.0; + query.pop_front(); + } + "b" => { + // Just gotta pop. + query.pop_front(); + } + _ => {} } } - _ => {} } - - return Ok(Prefix { - or: None, - regex_prefix: None, - compare_prefix: Some(( - prefix_type, - NumericalQuery { condition, value }, - )), - }); + _ => {} } + + return Ok(Prefix { + or: None, + regex_prefix: None, + compare_prefix: Some(( + prefix_type, + NumericalQuery { condition, value }, + )), + }); } } } - } else { - return Err(QueryError("Missing argument for search prefix".into())); } + } else { + return Err(QueryError("Missing argument for search prefix".into())); } - } else if inside_quotation { - // Uh oh, it's empty with quotes! - return Err(QueryError("Missing closing quotation".into())); } - - Err(QueryError("Invalid query".into())) + } else if inside_quotation { + // Uh oh, it's empty with quotes! + return Err(QueryError("Missing closing quotation".into())); } - let mut split_query = VecDeque::new(); + Err(QueryError("Invalid query".into())) + } - self.get_current_search_query() - .split_whitespace() - .for_each(|s| { - // From https://stackoverflow.com/a/56923739 in order to get a split but include the parentheses - let mut last = 0; - for (index, matched) in s.match_indices(|x| DELIMITER_LIST.contains(&x)) { - if last != index { - split_query.push_back(s[last..index].to_owned()); - } - split_query.push_back(matched.to_owned()); - last = index + matched.len(); - } - if last < s.len() { - split_query.push_back(s[last..].to_owned()); - } - }); + let mut split_query = VecDeque::new(); - let mut process_filter = process_string_to_filter(&mut split_query)?; - process_filter.process_regexes( - self.process_search_state.is_searching_whole_word, - self.process_search_state.is_ignoring_case, - self.process_search_state.is_searching_with_regex, - )?; + search_query.split_whitespace().for_each(|s| { + // From https://stackoverflow.com/a/56923739 in order to get a split but include the parentheses + let mut last = 0; + for (index, matched) in s.match_indices(|x| DELIMITER_LIST.contains(&x)) { + if last != index { + split_query.push_back(s[last..index].to_owned()); + } + split_query.push_back(matched.to_owned()); + last = index + matched.len(); + } + if last < s.len() { + split_query.push_back(s[last..].to_owned()); + } + }); - Ok(process_filter) - } + let mut process_filter = process_string_to_filter(&mut split_query)?; + process_filter.process_regexes( + is_searching_whole_word, + is_ignoring_case, + is_searching_with_regex, + )?; + + Ok(process_filter) } pub struct Query { @@ -467,7 +452,7 @@ impl Query { Ok(()) } - pub fn check(&self, process: &ConvertedProcessData, is_using_command: bool) -> bool { + pub fn check(&self, process: &ProcessHarvest, is_using_command: bool) -> bool { self.query .iter() .all(|ok| ok.check(process, is_using_command)) @@ -507,7 +492,7 @@ impl Or { Ok(()) } - pub fn check(&self, process: &ConvertedProcessData, is_using_command: bool) -> bool { + pub fn check(&self, process: &ProcessHarvest, is_using_command: bool) -> bool { if let Some(rhs) = &self.rhs { self.lhs.check(process, is_using_command) || rhs.check(process, is_using_command) } else { @@ -552,7 +537,7 @@ impl And { Ok(()) } - pub fn check(&self, process: &ConvertedProcessData, is_using_command: bool) -> bool { + pub fn check(&self, process: &ProcessHarvest, is_using_command: bool) -> bool { if let Some(rhs) = &self.rhs { self.lhs.check(process, is_using_command) && rhs.check(process, is_using_command) } else { @@ -662,7 +647,7 @@ impl Prefix { Ok(()) } - pub fn check(&self, process: &ConvertedProcessData, is_using_command: bool) -> bool { + pub fn check(&self, process: &ProcessHarvest, is_using_command: bool) -> bool { fn matches_condition(condition: &QueryComparison, lhs: f64, rhs: f64) -> bool { match condition { QueryComparison::Equal => (lhs - rhs).abs() < std::f64::EPSILON, @@ -684,11 +669,14 @@ impl Prefix { process.name.as_str() }), PrefixType::Pid => r.is_match(process.pid.to_string().as_str()), - PrefixType::State => r.is_match(process.process_state.as_str()), + PrefixType::State => r.is_match(process.process_state.0.as_str()), PrefixType::User => { - if let Some(user) = &process.user { - r.is_match(user.as_str()) - } else { + #[cfg(target_family = "unix")] + { + r.is_match(process.user.as_ref()) + } + #[cfg(not(target_family = "unix"))] + { false } } @@ -701,12 +689,12 @@ impl Prefix { match prefix_type { PrefixType::PCpu => matches_condition( &numerical_query.condition, - process.cpu_percent_usage, + process.cpu_usage_percent, numerical_query.value, ), PrefixType::PMem => matches_condition( &numerical_query.condition, - process.mem_percent_usage, + process.mem_usage_percent, numerical_query.value, ), PrefixType::MemBytes => matches_condition( @@ -716,22 +704,22 @@ impl Prefix { ), PrefixType::Rps => matches_condition( &numerical_query.condition, - process.rps_f64, + process.read_bytes_per_sec as f64, numerical_query.value, ), PrefixType::Wps => matches_condition( &numerical_query.condition, - process.wps_f64, + process.write_bytes_per_sec as f64, numerical_query.value, ), PrefixType::TRead => matches_condition( &numerical_query.condition, - process.tr_f64, + process.total_read_bytes as f64, numerical_query.value, ), PrefixType::TWrite => matches_condition( &numerical_query.condition, - process.tw_f64, + process.total_write_bytes as f64, numerical_query.value, ), _ => true, diff --git a/src/app/states.rs b/src/app/states.rs index 8cbb8ae12..3dff4644e 100644 --- a/src/app/states.rs +++ b/src/app/states.rs @@ -1,15 +1,16 @@ -use std::{collections::HashMap, convert::TryInto, time::Instant}; +use std::{collections::HashMap, time::Instant}; use unicode_segmentation::GraphemeCursor; -use tui::widgets::TableState; - use crate::{ app::{layout_manager::BottomWidgetType, query::*}, constants, - data_harvester::processes::{self, ProcessSorting}, }; -use ProcessSorting::*; + +pub mod table_state; +pub use table_state::*; + +use super::widgets::ProcWidget; #[derive(Debug)] pub enum ScrollDirection { @@ -31,41 +32,11 @@ pub enum CursorDirection { Right, } -/// AppScrollWidgetState deals with fields for a scrollable app's current state. +/// Meant for canvas operations involving table column widths. #[derive(Default)] -pub struct AppScrollWidgetState { - pub current_scroll_position: usize, - pub scroll_bar: usize, - pub scroll_direction: ScrollDirection, - pub table_state: TableState, -} - -impl AppScrollWidgetState { - /// Updates the position if possible, and if there is a valid change, returns the new position. - pub fn update_position(&mut self, change: i64, num_entries: usize) -> Option { - if change == 0 { - return None; - } - - let csp: Result = self.current_scroll_position.try_into(); - if let Ok(csp) = csp { - let proposed: Result = (csp + change).try_into(); - if let Ok(proposed) = proposed { - if proposed < num_entries { - self.current_scroll_position = proposed; - if change < 0 { - self.scroll_direction = ScrollDirection::Up; - } else { - self.scroll_direction = ScrollDirection::Down; - } - - return Some(self.current_scroll_position); - } - } - } - - None - } +pub struct CanvasTableWidthState { + pub desired_column_widths: Vec, + pub calculated_column_widths: Vec, } #[derive(PartialEq)] @@ -159,561 +130,20 @@ impl AppSearchState { } } -/// Meant for canvas operations involving table column widths. -#[derive(Default)] -pub struct CanvasTableWidthState { - pub desired_column_widths: Vec, - pub calculated_column_widths: Vec, -} - -/// ProcessSearchState only deals with process' search's current settings and state. -pub struct ProcessSearchState { - pub search_state: AppSearchState, - pub is_ignoring_case: bool, - pub is_searching_whole_word: bool, - pub is_searching_with_regex: bool, -} - -impl Default for ProcessSearchState { - fn default() -> Self { - ProcessSearchState { - search_state: AppSearchState::default(), - is_ignoring_case: true, - is_searching_whole_word: false, - is_searching_with_regex: false, - } - } -} - -impl ProcessSearchState { - pub fn search_toggle_ignore_case(&mut self) { - self.is_ignoring_case = !self.is_ignoring_case; - } - - pub fn search_toggle_whole_word(&mut self) { - self.is_searching_whole_word = !self.is_searching_whole_word; - } - - pub fn search_toggle_regex(&mut self) { - self.is_searching_with_regex = !self.is_searching_with_regex; - } -} - -pub struct ColumnInfo { - pub enabled: bool, - pub shortcut: Option<&'static str>, - // FIXME: Move column width logic here! - // pub hard_width: Option, - // pub max_soft_width: Option, -} - -pub struct ProcColumn { - pub ordered_columns: Vec, - /// The y location of headers. Since they're all aligned, it's just one value. - pub column_header_y_loc: Option, - /// The x start and end bounds for each header. - pub column_header_x_locs: Option>, - 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 { - let ordered_columns = vec![ - Count, - Pid, - ProcessName, - Command, - CpuPercent, - Mem, - MemPercent, - ReadPerSecond, - WritePerSecond, - TotalRead, - TotalWrite, - User, - 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"), - // hard_width: None, - // max_soft_width: None, - }, - ); - } - MemPercent => { - column_mapping.insert( - column, - ColumnInfo { - enabled: true, - shortcut: Some("m"), - // hard_width: None, - // max_soft_width: None, - }, - ); - } - Mem => { - column_mapping.insert( - column, - ColumnInfo { - enabled: false, - shortcut: Some("m"), - // hard_width: None, - // max_soft_width: None, - }, - ); - } - ProcessName => { - column_mapping.insert( - column, - ColumnInfo { - enabled: true, - shortcut: Some("n"), - // hard_width: None, - // max_soft_width: None, - }, - ); - } - Command => { - column_mapping.insert( - column, - ColumnInfo { - enabled: false, - shortcut: Some("n"), - // hard_width: None, - // max_soft_width: None, - }, - ); - } - Pid => { - column_mapping.insert( - column, - ColumnInfo { - enabled: true, - shortcut: Some("p"), - // hard_width: None, - // max_soft_width: None, - }, - ); - } - Count => { - column_mapping.insert( - column, - ColumnInfo { - enabled: false, - shortcut: None, - // hard_width: None, - // max_soft_width: None, - }, - ); - } - User => { - column_mapping.insert( - column, - ColumnInfo { - enabled: cfg!(target_family = "unix"), - shortcut: None, - }, - ); - } - _ => { - column_mapping.insert( - column, - ColumnInfo { - enabled: true, - shortcut: None, - // hard_width: None, - // max_soft_width: 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, - column_header_y_loc: None, - column_header_x_locs: None, - } - } -} - -impl ProcColumn { - /// Returns its new status. - pub fn toggle(&mut self, column: &ProcessSorting) -> Option { - if let Some(mapping) = self.column_mapping.get_mut(column) { - mapping.enabled = !(mapping.enabled); - Some(mapping.enabled) - } else { - None - } - } - - pub fn try_set(&mut self, column: &ProcessSorting, setting: bool) -> Option { - if let Some(mapping) = self.column_mapping.get_mut(column) { - mapping.enabled = setting; - Some(mapping.enabled) - } else { - None - } - } - - pub fn try_enable(&mut self, column: &ProcessSorting) -> Option { - if let Some(mapping) = self.column_mapping.get_mut(column) { - mapping.enabled = true; - Some(mapping.enabled) - } else { - None - } - } - - pub fn try_disable(&mut self, column: &ProcessSorting) -> Option { - if let Some(mapping) = self.column_mapping.get_mut(column) { - mapping.enabled = false; - Some(mapping.enabled) - } else { - None - } - } - - pub fn is_enabled(&self, column: &ProcessSorting) -> bool { - if let Some(mapping) = self.column_mapping.get(column) { - mapping.enabled - } else { - false - } - } - - pub fn get_enabled_columns_len(&self) -> usize { - self.ordered_columns - .iter() - .filter_map(|column_type| { - if let Some(col_map) = self.column_mapping.get(column_type) { - if col_map.enabled { - Some(1) - } else { - None - } - } else { - None - } - }) - .sum() - } - - /// NOTE: ALWAYS call this when opening the sorted window. - pub fn set_to_sorted_index_from_type(&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; - } - - /// This function sets the scroll position based on the index. - pub fn set_to_sorted_index_from_visual_index(&mut self, visual_index: usize) { - self.current_scroll_position = visual_index; - self.backup_prev_scroll_position = self.previous_scroll_position; - } - - pub fn get_column_headers( - &self, proc_sorting_type: &ProcessSorting, sort_reverse: bool, - ) -> Vec { - const DOWN_ARROW: char = '▼'; - const UP_ARROW: char = '▲'; - - // 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(format!( - "{}{}{}", - column_type, - command_str, - if proc_sorting_type == column_type { - if sort_reverse { - DOWN_ARROW - } else { - UP_ARROW - } - } else { - ' ' - } - )) - } 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 is_process_sort_descending: bool, - pub is_using_command: bool, - pub current_column_index: usize, - pub is_sort_open: bool, - pub columns: ProcColumn, - pub is_tree_mode: bool, - pub table_width_state: CanvasTableWidthState, - pub requires_redraw: bool, -} - -impl ProcWidgetState { - pub fn init( - is_case_sensitive: bool, is_match_whole_word: bool, is_use_regex: bool, is_grouped: bool, - show_memory_as_values: bool, is_tree_mode: bool, is_using_command: bool, - ) -> Self { - let mut process_search_state = ProcessSearchState::default(); - - if is_case_sensitive { - // By default it's off - process_search_state.search_toggle_ignore_case(); - } - if is_match_whole_word { - process_search_state.search_toggle_whole_word(); - } - if is_use_regex { - process_search_state.search_toggle_regex(); - } - - let (process_sorting_type, is_process_sort_descending) = if is_tree_mode { - (processes::ProcessSorting::Pid, false) - } else { - (processes::ProcessSorting::CpuPercent, true) - }; - - // TODO: If we add customizable columns, this should pull from config - let mut columns = ProcColumn::default(); - columns.set_to_sorted_index_from_type(&process_sorting_type); - if is_grouped { - // Normally defaults to showing by PID, toggle count on instead. - columns.toggle(&ProcessSorting::Count); - columns.toggle(&ProcessSorting::Pid); - } - if show_memory_as_values { - // Normally defaults to showing by percent, toggle value on instead. - columns.toggle(&ProcessSorting::Mem); - columns.toggle(&ProcessSorting::MemPercent); - } - if is_using_command { - columns.toggle(&ProcessSorting::ProcessName); - columns.toggle(&ProcessSorting::Command); - } - - ProcWidgetState { - process_search_state, - is_grouped, - scroll_state: AppScrollWidgetState::default(), - process_sorting_type, - is_process_sort_descending, - is_using_command, - current_column_index: 0, - is_sort_open: false, - columns, - is_tree_mode, - table_width_state: CanvasTableWidthState::default(), - requires_redraw: false, - } - } - - /// 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.is_process_sort_descending = !(self.is_process_sort_descending); - } 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.is_process_sort_descending = false; - } - _ => { - self.is_process_sort_descending = true; - } - } - } - } - } - - pub fn toggle_command_and_name(&mut self, is_using_command: bool) { - if let Some(pn) = self - .columns - .column_mapping - .get_mut(&ProcessSorting::ProcessName) - { - pn.enabled = !is_using_command; - } - if let Some(c) = self - .columns - .column_mapping - .get_mut(&ProcessSorting::Command) - { - c.enabled = is_using_command; - } - } - - pub fn get_search_cursor_position(&self) -> usize { - self.process_search_state - .search_state - .grapheme_cursor - .cur_cursor() - } - - pub fn get_char_cursor_position(&self) -> usize { - self.process_search_state.search_state.char_cursor_position - } - - pub fn is_search_enabled(&self) -> bool { - self.process_search_state.search_state.is_enabled - } - - pub fn get_current_search_query(&self) -> &String { - &self.process_search_state.search_state.current_search_query - } - - pub fn update_query(&mut self) { - if self - .process_search_state - .search_state - .current_search_query - .is_empty() - { - self.process_search_state.search_state.is_blank_search = true; - self.process_search_state.search_state.is_invalid_search = false; - self.process_search_state.search_state.error_message = None; - } else { - let parsed_query = self.parse_query(); - // debug!("Parsed query: {:#?}", parsed_query); - - if let Ok(parsed_query) = parsed_query { - self.process_search_state.search_state.query = Some(parsed_query); - self.process_search_state.search_state.is_blank_search = false; - self.process_search_state.search_state.is_invalid_search = false; - self.process_search_state.search_state.error_message = None; - } else if let Err(err) = parsed_query { - self.process_search_state.search_state.is_blank_search = false; - self.process_search_state.search_state.is_invalid_search = true; - self.process_search_state.search_state.error_message = Some(err.to_string()); - } - } - self.scroll_state.scroll_bar = 0; - self.scroll_state.current_scroll_position = 0; - } - - pub fn clear_search(&mut self) { - self.process_search_state.search_state.reset(); - } - - pub fn search_walk_forward(&mut self, start_position: usize) { - self.process_search_state - .search_state - .grapheme_cursor - .next_boundary( - &self.process_search_state.search_state.current_search_query[start_position..], - start_position, - ) - .unwrap(); - } - - pub fn search_walk_back(&mut self, start_position: usize) { - self.process_search_state - .search_state - .grapheme_cursor - .prev_boundary( - &self.process_search_state.search_state.current_search_query[..start_position], - 0, - ) - .unwrap(); - } -} - pub struct ProcState { - pub widget_states: HashMap, - pub force_update: Option, - pub force_update_all: bool, + pub widget_states: HashMap, } impl ProcState { - pub fn init(widget_states: HashMap) -> Self { - ProcState { - widget_states, - force_update: None, - force_update_all: false, - } + pub fn init(widget_states: HashMap) -> Self { + ProcState { widget_states } } - pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut ProcWidgetState> { + pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut ProcWidget> { self.widget_states.get_mut(&widget_id) } - pub fn get_widget_state(&self, widget_id: u64) -> Option<&ProcWidgetState> { + pub fn get_widget_state(&self, widget_id: u64) -> Option<&ProcWidget> { self.widget_states.get(&widget_id) } } @@ -721,29 +151,13 @@ impl ProcState { pub struct NetWidgetState { pub current_display_time: u64, pub autohide_timer: Option, - // pub draw_max_range_cache: f64, - // pub draw_labels_cache: Vec, - // pub draw_time_start_cache: f64, - // TODO: Re-enable these when we move net details state-side! - // pub unit_type: DataUnitTypes, - // pub scale_type: AxisScaling, } impl NetWidgetState { - pub fn init( - current_display_time: u64, - autohide_timer: Option, - // unit_type: DataUnitTypes, - // scale_type: AxisScaling, - ) -> Self { + pub fn init(current_display_time: u64, autohide_timer: Option) -> Self { NetWidgetState { current_display_time, autohide_timer, - // draw_max_range_cache: 0.0, - // draw_labels_cache: vec![], - // draw_time_start_cache: 0.0, - // unit_type, - // scale_type, } } } @@ -774,20 +188,34 @@ pub struct CpuWidgetState { pub current_display_time: u64, pub is_legend_hidden: bool, pub autohide_timer: Option, - pub scroll_state: AppScrollWidgetState, + pub table_state: TableComponentState, pub is_multi_graph_mode: bool, - pub table_width_state: CanvasTableWidthState, } impl CpuWidgetState { pub fn init(current_display_time: u64, autohide_timer: Option) -> Self { + const CPU_LEGEND_HEADER: [&str; 2] = ["CPU", "Use%"]; + const WIDTHS: [WidthBounds; CPU_LEGEND_HEADER.len()] = [ + WidthBounds::soft_from_str("CPU", Some(0.5)), + WidthBounds::soft_from_str("Use%", Some(0.5)), + ]; + + let table_state = TableComponentState::new( + CPU_LEGEND_HEADER + .iter() + .zip(WIDTHS) + .map(|(c, width)| { + TableComponentColumn::new_custom(CellContent::new(*c, None), width) + }) + .collect(), + ); + CpuWidgetState { current_display_time, is_legend_hidden: false, autohide_timer, - scroll_state: AppScrollWidgetState::default(), + table_state, is_multi_graph_mode: false, - table_width_state: CanvasTableWidthState::default(), } } } @@ -850,15 +278,27 @@ impl MemState { } pub struct TempWidgetState { - pub scroll_state: AppScrollWidgetState, - pub table_width_state: CanvasTableWidthState, + pub table_state: TableComponentState, } -impl TempWidgetState { - pub fn init() -> Self { +impl Default for TempWidgetState { + fn default() -> Self { + const TEMP_HEADERS: [&str; 2] = ["Sensor", "Temp"]; + const WIDTHS: [WidthBounds; TEMP_HEADERS.len()] = [ + WidthBounds::soft_from_str(TEMP_HEADERS[0], Some(0.8)), + WidthBounds::soft_from_str(TEMP_HEADERS[1], None), + ]; + TempWidgetState { - scroll_state: AppScrollWidgetState::default(), - table_width_state: CanvasTableWidthState::default(), + table_state: TableComponentState::new( + TEMP_HEADERS + .iter() + .zip(WIDTHS) + .map(|(header, width)| { + TableComponentColumn::new_custom(CellContent::new(*header, None), width) + }) + .collect(), + ), } } } @@ -882,15 +322,32 @@ impl TempState { } pub struct DiskWidgetState { - pub scroll_state: AppScrollWidgetState, - pub table_width_state: CanvasTableWidthState, + pub table_state: TableComponentState, } -impl DiskWidgetState { - pub fn init() -> Self { +impl Default for DiskWidgetState { + fn default() -> Self { + const DISK_HEADERS: [&str; 7] = ["Disk", "Mount", "Used", "Free", "Total", "R/s", "W/s"]; + const WIDTHS: [WidthBounds; DISK_HEADERS.len()] = [ + WidthBounds::soft_from_str(DISK_HEADERS[0], Some(0.2)), + WidthBounds::soft_from_str(DISK_HEADERS[1], Some(0.2)), + WidthBounds::Hard(4), + WidthBounds::Hard(6), + WidthBounds::Hard(6), + WidthBounds::Hard(7), + WidthBounds::Hard(7), + ]; + DiskWidgetState { - scroll_state: AppScrollWidgetState::default(), - table_width_state: CanvasTableWidthState::default(), + table_state: TableComponentState::new( + DISK_HEADERS + .iter() + .zip(WIDTHS) + .map(|(header, width)| { + TableComponentColumn::new_custom(CellContent::new(*header, None), width) + }) + .collect(), + ), } } } @@ -954,76 +411,3 @@ pub struct ParagraphScrollState { pub current_scroll_index: u16, pub max_scroll_index: u16, } - -#[derive(Default)] -pub struct ConfigState { - pub current_category_index: usize, - pub category_list: Vec, -} - -#[derive(Default)] -pub struct ConfigCategory { - pub category_name: &'static str, - pub options_list: Vec, -} - -pub struct ConfigOption { - pub set_function: Box anyhow::Result<()>>, -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_scroll_update_position() { - fn check_scroll_update( - scroll: &mut AppScrollWidgetState, change: i64, max: usize, ret: Option, - new_position: usize, - ) { - assert_eq!(scroll.update_position(change, max), ret); - assert_eq!(scroll.current_scroll_position, new_position); - } - - let mut scroll = AppScrollWidgetState { - current_scroll_position: 5, - scroll_bar: 0, - scroll_direction: ScrollDirection::Down, - table_state: Default::default(), - }; - let s = &mut scroll; - - // Update by 0. Should not change. - check_scroll_update(s, 0, 15, None, 5); - - // Update by 5. Should increment to index 10. - check_scroll_update(s, 5, 15, Some(10), 10); - - // Update by 5. Should not change. - check_scroll_update(s, 5, 15, None, 10); - - // Update by 4. Should increment to index 14 (supposed max). - check_scroll_update(s, 4, 15, Some(14), 14); - - // Update by 1. Should do nothing. - check_scroll_update(s, 1, 15, None, 14); - - // Update by -15. Should do nothing. - check_scroll_update(s, -15, 15, None, 14); - - // Update by -14. Should land on position 0. - check_scroll_update(s, -14, 15, Some(0), 0); - - // Update by -1. Should do nothing. - check_scroll_update(s, -15, 15, None, 0); - - // Update by 0. Should do nothing. - check_scroll_update(s, 0, 15, None, 0); - - // Update by 15. Should do nothing. - check_scroll_update(s, 15, 15, None, 0); - - // Update by 15 but with a larger bound. Should increment to 15. - check_scroll_update(s, 15, 16, Some(15), 15); - } -} diff --git a/src/app/states/table_state.rs b/src/app/states/table_state.rs new file mode 100644 index 000000000..c6ce3b46c --- /dev/null +++ b/src/app/states/table_state.rs @@ -0,0 +1,681 @@ +use std::{borrow::Cow, convert::TryInto, ops::Range}; + +use itertools::Itertools; +use tui::{layout::Rect, widgets::TableState}; + +use super::ScrollDirection; + +/// A bound on the width of a column. +#[derive(Clone, Copy, Debug)] +pub enum WidthBounds { + /// A width of this type is either as long as `min`, but can otherwise shrink and grow up to a point. + Soft { + /// The minimum amount before giving up and hiding. + min_width: u16, + + /// The desired, calculated width. Take this if possible as the base starting width. + desired: u16, + + /// The max width, as a percentage of the total width available. If [`None`], + /// then it can grow as desired. + max_percentage: Option, + }, + + /// A width of this type is either as long as specified, or does not appear at all. + Hard(u16), + + /// Always uses the width of the [`CellContent`]. + CellWidth, +} + +impl WidthBounds { + pub const fn soft_from_str(name: &'static str, max_percentage: Option) -> WidthBounds { + let len = name.len() as u16; + WidthBounds::Soft { + min_width: len, + desired: len, + max_percentage, + } + } + + pub const fn soft_from_str_with_alt( + name: &'static str, alt: &'static str, max_percentage: Option, + ) -> WidthBounds { + WidthBounds::Soft { + min_width: alt.len() as u16, + desired: name.len() as u16, + max_percentage, + } + } +} + +/// A [`CellContent`] contains text information for display in a table. +#[derive(Clone, Debug)] +pub enum CellContent { + Simple(Cow<'static, str>), + HasAlt { + alt: Cow<'static, str>, + main: Cow<'static, str>, + }, +} + +impl CellContent { + /// Creates a new [`CellContent`]. + pub fn new(name: I, alt: Option) -> Self + where + I: Into>, + { + if let Some(alt) = alt { + CellContent::HasAlt { + alt: alt.into(), + main: name.into(), + } + } else { + CellContent::Simple(name.into()) + } + } + + /// Returns the length of the [`CellContent`]. Note that for a [`CellContent::HasAlt`], it will return + /// the length of the "main" field. + pub fn len(&self) -> usize { + match self { + CellContent::Simple(s) => s.len(), + CellContent::HasAlt { alt: _, main: long } => long.len(), + } + } + + /// Whether the [`CellContent`]'s text is empty. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn main_text(&self) -> &Cow<'static, str> { + match self { + CellContent::Simple(main) => main, + CellContent::HasAlt { alt: _, main } => main, + } + } +} + +pub trait TableComponentHeader { + fn header_text(&self) -> &CellContent; +} + +impl TableComponentHeader for CellContent { + fn header_text(&self) -> &CellContent { + self + } +} +impl From> for CellContent { + fn from(c: Cow<'static, str>) -> Self { + CellContent::Simple(c) + } +} + +impl From<&'static str> for CellContent { + fn from(s: &'static str) -> Self { + CellContent::Simple(s.into()) + } +} + +impl From for CellContent { + fn from(s: String) -> Self { + CellContent::Simple(s.into()) + } +} + +pub struct TableComponentColumn { + /// The header of the column. + pub header: H, + + /// A restriction on this column's width, if desired. + pub width_bounds: WidthBounds, + + /// The calculated width of the column. + pub calculated_width: u16, + + /// Marks that this column is currently "hidden", and should *always* be skipped. + pub is_hidden: bool, +} + +impl TableComponentColumn { + pub fn new_custom(header: H, width_bounds: WidthBounds) -> Self { + Self { + header, + width_bounds, + calculated_width: 0, + is_hidden: false, + } + } + + pub fn new(header: H) -> Self { + Self { + header, + width_bounds: WidthBounds::CellWidth, + calculated_width: 0, + is_hidden: false, + } + } + + pub fn new_hard(header: H, width: u16) -> Self { + Self { + header, + width_bounds: WidthBounds::Hard(width), + calculated_width: 0, + is_hidden: false, + } + } + + pub fn new_soft(header: H, max_percentage: Option) -> Self { + let min_width = header.header_text().len() as u16; + Self { + header, + width_bounds: WidthBounds::Soft { + min_width, + desired: min_width, + max_percentage, + }, + calculated_width: 0, + is_hidden: false, + } + } + + pub fn is_zero_width(&self) -> bool { + self.calculated_width == 0 + } + + pub fn is_skipped(&self) -> bool { + self.is_zero_width() || self.is_hidden + } +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum SortOrder { + Ascending, + Descending, +} + +impl SortOrder { + pub fn is_descending(&self) -> bool { + matches!(self, SortOrder::Descending) + } +} + +/// Represents the current table's sorting state. +#[derive(Debug)] +pub enum SortState { + Unsortable, + Sortable(SortableState), +} + +#[derive(Debug)] +pub struct SortableState { + /// The "x locations" of the headers. + visual_mappings: Vec>, + + /// The "y location" of the header row. Since all headers share the same y-location we just set it once here. + y_loc: u16, + + /// This is a bit of a lazy hack to handle this for now - ideally the entire [`SortableState`] + /// is instead handled by a separate table struct that also can access the columns and their default sort orderings. + default_sort_orderings: Vec, + + /// The currently selected sort index. + pub current_index: usize, + + /// The current sorting order. + pub order: SortOrder, +} + +impl SortableState { + /// Creates a new [`SortableState`]. + pub fn new( + default_index: usize, default_order: SortOrder, default_sort_orderings: Vec, + ) -> Self { + Self { + visual_mappings: Default::default(), + y_loc: 0, + default_sort_orderings, + current_index: default_index, + order: default_order, + } + } + + /// Toggles the current sort order. + pub fn toggle_order(&mut self) { + self.order = match self.order { + SortOrder::Ascending => SortOrder::Descending, + SortOrder::Descending => SortOrder::Ascending, + } + } + + /// Updates the visual index. + /// + /// This function will create a *sorted* range list - in debug mode, + /// the program will assert this, but it will not do so in release mode! + pub fn update_visual_index(&mut self, draw_loc: Rect, row_widths: &[u16]) { + let mut start = draw_loc.x; + let visual_index = row_widths + .iter() + .map(|width| { + let range_start = start; + let range_end = start + width + 1; // +1 for the gap b/w cols. + start = range_end; + range_start..range_end + }) + .collect_vec(); + + debug_assert!(visual_index.iter().all(|a| { a.start <= a.end })); + + debug_assert!(visual_index + .iter() + .tuple_windows() + .all(|(a, b)| { b.start >= a.end })); + + self.visual_mappings = visual_index; + self.y_loc = draw_loc.y; + } + + /// Given some `x` and `y`, if possible, select the corresponding column or toggle the column if already selected, + /// and otherwise do nothing. + /// + /// If there was some update, the corresponding column type will be returned. If nothing happens, [`None`] is + /// returned. + pub fn try_select_location(&mut self, x: u16, y: u16) -> Option { + if self.y_loc == y { + if let Some(index) = self.get_range(x) { + self.update_sort_index(index); + Some(self.current_index) + } else { + None + } + } else { + None + } + } + + /// Updates the sort index, and sets the sort order as appropriate. + /// + /// If the index is different from the previous one, it will move to the new index and set the sort order + /// to the prescribed default sort order. + /// + /// If the index is the same as the previous one, it will simply toggle the current sort order. + pub fn update_sort_index(&mut self, index: usize) { + if self.current_index == index { + self.toggle_order(); + } else { + self.current_index = index; + self.order = self.default_sort_orderings[index]; + } + } + + /// Given a `needle` coordinate, select the corresponding index and value. + fn get_range(&self, needle: u16) -> Option { + match self + .visual_mappings + .binary_search_by_key(&needle, |range| range.start) + { + Ok(index) => Some(index), + Err(index) => index.checked_sub(1), + } + .and_then(|index| { + if needle < self.visual_mappings[index].end { + Some(index) + } else { + None + } + }) + } +} + +/// [`TableComponentState`] deals with fields for a scrollable's current state. +pub struct TableComponentState { + pub current_scroll_position: usize, + pub scroll_bar: usize, + pub scroll_direction: ScrollDirection, + pub table_state: TableState, + pub columns: Vec>, + pub sort_state: SortState, +} + +impl TableComponentState { + pub fn new(columns: Vec>) -> Self { + Self { + current_scroll_position: 0, + scroll_bar: 0, + scroll_direction: ScrollDirection::Down, + table_state: Default::default(), + columns, + sort_state: SortState::Unsortable, + } + } + + pub fn sort_state(mut self, sort_state: SortState) -> Self { + self.sort_state = sort_state; + self + } + + /// Calculates widths for the columns for this table. + /// + /// * `total_width` is the, well, total width available. + /// * `left_to_right` is a boolean whether to go from left to right if true, or right to left if + /// false. + /// + /// **NOTE:** Trailing 0's may break tui-rs, remember to filter them out later! + pub fn calculate_column_widths(&mut self, total_width: u16, left_to_right: bool) { + use itertools::Either; + use std::cmp::{max, min}; + + let mut total_width_left = total_width; + + let columns = if left_to_right { + Either::Left(self.columns.iter_mut()) + } else { + Either::Right(self.columns.iter_mut().rev()) + }; + + let arrow_offset = match self.sort_state { + SortState::Unsortable => 0, + SortState::Sortable { .. } => 1, + }; + + let mut num_columns = 0; + let mut skip_iter = false; + for column in columns { + column.calculated_width = 0; + + if column.is_hidden || skip_iter { + continue; + } + + match &column.width_bounds { + WidthBounds::Soft { + min_width, + desired, + max_percentage, + } => { + let min_width = *min_width + arrow_offset; + if min_width > total_width_left { + skip_iter = true; + continue; + } + + let soft_limit = max( + if let Some(max_percentage) = max_percentage { + // TODO: Rust doesn't have an `into()` or `try_into()` for floats to integers. + ((*max_percentage * f32::from(total_width)).ceil()) as u16 + } else { + *desired + }, + min_width, + ); + let space_taken = min(min(soft_limit, *desired), total_width_left); + + if min_width > space_taken || min_width == 0 { + skip_iter = true; + } else if space_taken > 0 { + total_width_left = total_width_left.saturating_sub(space_taken + 1); + column.calculated_width = space_taken; + num_columns += 1; + } + } + WidthBounds::CellWidth => { + let width = column.header.header_text().len() as u16; + let min_width = width + arrow_offset; + + if min_width > total_width_left || min_width == 0 { + skip_iter = true; + } else if min_width > 0 { + total_width_left = total_width_left.saturating_sub(min_width + 1); + column.calculated_width = min_width; + num_columns += 1; + } + } + WidthBounds::Hard(width) => { + let min_width = *width + arrow_offset; + + if min_width > total_width_left || min_width == 0 { + skip_iter = true; + } else if min_width > 0 { + total_width_left = total_width_left.saturating_sub(min_width + 1); + column.calculated_width = min_width; + num_columns += 1; + } + } + } + } + + if num_columns > 0 { + // Redistribute remaining. + let mut num_dist = num_columns; + let amount_per_slot = total_width_left / num_dist; + total_width_left %= num_dist; + + for column in self.columns.iter_mut() { + if num_dist == 0 { + break; + } + + if column.calculated_width > 0 { + if total_width_left > 0 { + column.calculated_width += amount_per_slot + 1; + total_width_left -= 1; + } else { + column.calculated_width += amount_per_slot; + } + + num_dist -= 1; + } + } + } + } + + /// Updates the position if possible, and if there is a valid change, returns the new position. + pub fn update_position(&mut self, change: i64, num_entries: usize) -> Option { + if change == 0 { + return None; + } + + let csp: Result = self.current_scroll_position.try_into(); + if let Ok(csp) = csp { + let proposed: Result = (csp + change).try_into(); + if let Ok(proposed) = proposed { + if proposed < num_entries { + self.current_scroll_position = proposed; + if change < 0 { + self.scroll_direction = ScrollDirection::Up; + } else { + self.scroll_direction = ScrollDirection::Down; + } + + return Some(self.current_scroll_position); + } + } + } + + None + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_scroll_update_position() { + #[track_caller] + fn check_scroll_update( + scroll: &mut TableComponentState, change: i64, max: usize, ret: Option, + new_position: usize, + ) { + assert_eq!(scroll.update_position(change, max), ret); + assert_eq!(scroll.current_scroll_position, new_position); + } + + let mut scroll = TableComponentState { + current_scroll_position: 5, + scroll_bar: 0, + scroll_direction: ScrollDirection::Down, + table_state: Default::default(), + columns: vec![], + sort_state: SortState::Unsortable, + }; + let s = &mut scroll; + + // Update by 0. Should not change. + check_scroll_update(s, 0, 15, None, 5); + + // Update by 5. Should increment to index 10. + check_scroll_update(s, 5, 15, Some(10), 10); + + // Update by 5. Should not change. + check_scroll_update(s, 5, 15, None, 10); + + // Update by 4. Should increment to index 14 (supposed max). + check_scroll_update(s, 4, 15, Some(14), 14); + + // Update by 1. Should do nothing. + check_scroll_update(s, 1, 15, None, 14); + + // Update by -15. Should do nothing. + check_scroll_update(s, -15, 15, None, 14); + + // Update by -14. Should land on position 0. + check_scroll_update(s, -14, 15, Some(0), 0); + + // Update by -1. Should do nothing. + check_scroll_update(s, -15, 15, None, 0); + + // Update by 0. Should do nothing. + check_scroll_update(s, 0, 15, None, 0); + + // Update by 15. Should do nothing. + check_scroll_update(s, 15, 15, None, 0); + + // Update by 15 but with a larger bound. Should increment to 15. + check_scroll_update(s, 15, 16, Some(15), 15); + } + + #[test] + fn test_table_width_calculation() { + #[track_caller] + fn test_calculation(state: &mut TableComponentState, width: u16, expected: Vec) { + state.calculate_column_widths(width, true); + assert_eq!( + state + .columns + .iter() + .filter_map(|c| if c.calculated_width == 0 { + None + } else { + Some(c.calculated_width) + }) + .collect::>(), + expected + ) + } + + let mut state = TableComponentState::new(vec![ + TableComponentColumn::new(CellContent::from("a")), + TableComponentColumn::new_custom( + "a".into(), + WidthBounds::Soft { + min_width: 1, + desired: 10, + max_percentage: Some(0.125), + }, + ), + TableComponentColumn::new_custom( + "a".into(), + WidthBounds::Soft { + min_width: 2, + desired: 10, + max_percentage: Some(0.5), + }, + ), + ]); + + test_calculation(&mut state, 0, vec![]); + test_calculation(&mut state, 1, vec![1]); + test_calculation(&mut state, 2, vec![1]); + test_calculation(&mut state, 3, vec![1, 1]); + test_calculation(&mut state, 4, vec![1, 1]); + test_calculation(&mut state, 5, vec![2, 1]); + test_calculation(&mut state, 6, vec![1, 1, 2]); + test_calculation(&mut state, 7, vec![1, 1, 3]); + test_calculation(&mut state, 8, vec![1, 1, 4]); + test_calculation(&mut state, 14, vec![2, 2, 7]); + test_calculation(&mut state, 20, vec![2, 4, 11]); + test_calculation(&mut state, 100, vec![27, 35, 35]); + + state.sort_state = SortState::Sortable(SortableState::new(1, SortOrder::Ascending, vec![])); + + test_calculation(&mut state, 0, vec![]); + test_calculation(&mut state, 1, vec![]); + test_calculation(&mut state, 2, vec![2]); + test_calculation(&mut state, 3, vec![2]); + test_calculation(&mut state, 4, vec![3]); + test_calculation(&mut state, 5, vec![2, 2]); + test_calculation(&mut state, 6, vec![2, 2]); + test_calculation(&mut state, 7, vec![3, 2]); + test_calculation(&mut state, 8, vec![3, 3]); + test_calculation(&mut state, 14, vec![2, 2, 7]); + test_calculation(&mut state, 20, vec![3, 4, 10]); + test_calculation(&mut state, 100, vec![27, 35, 35]); + } + + #[test] + fn test_visual_index_selection() { + let mut state = SortableState::new( + 0, + SortOrder::Ascending, + vec![SortOrder::Ascending, SortOrder::Descending], + ); + + const X_OFFSET: u16 = 10; + const Y_OFFSET: u16 = 15; + state.update_visual_index(Rect::new(X_OFFSET, Y_OFFSET, 20, 15), &[4, 14]); + + #[track_caller] + fn test_selection( + state: &mut SortableState, from_x_offset: u16, from_y_offset: u16, + result: (Option, SortOrder), + ) { + assert_eq!( + state.try_select_location(X_OFFSET + from_x_offset, Y_OFFSET + from_y_offset), + result.0 + ); + assert_eq!(state.order, result.1); + } + + use SortOrder::*; + + // Clicking on these don't do anything, so don't show any change. + test_selection(&mut state, 5, 1, (None, Ascending)); + test_selection(&mut state, 21, 0, (None, Ascending)); + + // Clicking on the first column should toggle it as it is already selected. + test_selection(&mut state, 3, 0, (Some(0), Descending)); + + // Clicking on the first column should toggle it again as it is already selected. + test_selection(&mut state, 4, 0, (Some(0), Ascending)); + + // Clicking on second column should select and switch to the descending ordering as that is its default. + test_selection(&mut state, 5, 0, (Some(1), Descending)); + + // Clicking on second column should toggle it. + test_selection(&mut state, 19, 0, (Some(1), Ascending)); + + // Overshoot, should not do anything. + test_selection(&mut state, 20, 0, (None, Ascending)); + + // Further overshoot, should not do anything. + test_selection(&mut state, 25, 0, (None, Ascending)); + + // Go back to first column, should be ascending to match default for index 0. + test_selection(&mut state, 3, 0, (Some(0), Ascending)); + + // Click on first column should then go to descending as it is already selected and ascending. + test_selection(&mut state, 3, 0, (Some(0), Descending)); + } +} diff --git a/src/app/widgets.rs b/src/app/widgets.rs new file mode 100644 index 000000000..9d3a54ba0 --- /dev/null +++ b/src/app/widgets.rs @@ -0,0 +1,2 @@ +pub mod process_widget; +pub use process_widget::*; diff --git a/src/app/widgets/process_widget.rs b/src/app/widgets/process_widget.rs new file mode 100644 index 000000000..f49ab40aa --- /dev/null +++ b/src/app/widgets/process_widget.rs @@ -0,0 +1,1164 @@ +use crate::{ + app::{ + data_farmer::{DataCollection, ProcessData, StringPidMap}, + data_harvester::processes::ProcessHarvest, + query::*, + AppSearchState, CellContent, ScrollDirection, SortOrder, SortState, SortableState, + TableComponentColumn, TableComponentHeader, TableComponentState, WidthBounds, + }, + data_conversion::{binary_byte_string, dec_bytes_per_second_string, TableData, TableRow}, + utils::gen_util::sort_partial_fn, + Pid, +}; + +use concat_string::concat_string; +use fxhash::{FxHashMap, FxHashSet}; +use itertools::Itertools; +use std::{ + borrow::Cow, + cmp::{max, Reverse}, +}; + +/// ProcessSearchState only deals with process' search's current settings and state. +pub struct ProcessSearchState { + pub search_state: AppSearchState, + pub is_ignoring_case: bool, + pub is_searching_whole_word: bool, + pub is_searching_with_regex: bool, +} + +impl Default for ProcessSearchState { + fn default() -> Self { + ProcessSearchState { + search_state: AppSearchState::default(), + is_ignoring_case: true, + is_searching_whole_word: false, + is_searching_with_regex: false, + } + } +} + +impl ProcessSearchState { + pub fn search_toggle_ignore_case(&mut self) { + self.is_ignoring_case = !self.is_ignoring_case; + } + + pub fn search_toggle_whole_word(&mut self) { + self.is_searching_whole_word = !self.is_searching_whole_word; + } + + pub fn search_toggle_regex(&mut self) { + self.is_searching_with_regex = !self.is_searching_with_regex; + } +} + +#[derive(Clone, Debug)] +pub enum ProcWidgetMode { + Tree { collapsed_pids: FxHashSet }, + Grouped, + Normal, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum ProcWidgetColumn { + CpuPercent, + Memory { show_percentage: bool }, + PidOrCount { is_count: bool }, + ProcNameOrCommand { is_command: bool }, + ReadPerSecond, + WritePerSecond, + TotalRead, + TotalWrite, + State, + User, +} + +impl ProcWidgetColumn { + const CPU_PERCENT: CellContent = CellContent::Simple(Cow::Borrowed("CPU%")); + const MEM_PERCENT: CellContent = CellContent::Simple(Cow::Borrowed("Mem%")); + const MEM: CellContent = CellContent::Simple(Cow::Borrowed("Mem")); + const READS_PER_SECOND: CellContent = CellContent::Simple(Cow::Borrowed("R/s")); + const WRITES_PER_SECOND: CellContent = CellContent::Simple(Cow::Borrowed("W/s")); + const TOTAL_READ: CellContent = CellContent::Simple(Cow::Borrowed("T.Read")); + const TOTAL_WRITE: CellContent = CellContent::Simple(Cow::Borrowed("T.Write")); + const STATE: CellContent = CellContent::Simple(Cow::Borrowed("State")); + const PROCESS_NAME: CellContent = CellContent::Simple(Cow::Borrowed("Name")); + const COMMAND: CellContent = CellContent::Simple(Cow::Borrowed("Command")); + const PID: CellContent = CellContent::Simple(Cow::Borrowed("PID")); + const COUNT: CellContent = CellContent::Simple(Cow::Borrowed("Count")); + const USER: CellContent = CellContent::Simple(Cow::Borrowed("User")); + + const SHORTCUT_CPU_PERCENT: CellContent = CellContent::Simple(Cow::Borrowed("CPU%(c)")); + const SHORTCUT_MEM_PERCENT: CellContent = CellContent::Simple(Cow::Borrowed("Mem%(m)")); + const SHORTCUT_MEM: CellContent = CellContent::Simple(Cow::Borrowed("Mem(m)")); + const SHORTCUT_PROCESS_NAME: CellContent = CellContent::Simple(Cow::Borrowed("Name(n)")); + const SHORTCUT_COMMAND: CellContent = CellContent::Simple(Cow::Borrowed("Command(n)")); + const SHORTCUT_PID: CellContent = CellContent::Simple(Cow::Borrowed("PID(p)")); + + pub fn text(&self) -> &CellContent { + match self { + ProcWidgetColumn::CpuPercent => &Self::CPU_PERCENT, + ProcWidgetColumn::Memory { show_percentage } => { + if *show_percentage { + &Self::MEM_PERCENT + } else { + &Self::MEM + } + } + ProcWidgetColumn::PidOrCount { is_count } => { + if *is_count { + &Self::COUNT + } else { + &Self::PID + } + } + ProcWidgetColumn::ProcNameOrCommand { is_command } => { + if *is_command { + &Self::COMMAND + } else { + &Self::PROCESS_NAME + } + } + ProcWidgetColumn::ReadPerSecond => &Self::READS_PER_SECOND, + ProcWidgetColumn::WritePerSecond => &Self::WRITES_PER_SECOND, + ProcWidgetColumn::TotalRead => &Self::TOTAL_READ, + ProcWidgetColumn::TotalWrite => &Self::TOTAL_WRITE, + ProcWidgetColumn::State => &Self::STATE, + ProcWidgetColumn::User => &Self::USER, + } + } + + /// Sorts the given data in-place. + pub fn sort( + &self, sort_descending: bool, data: &mut [&ProcessHarvest], is_using_command: bool, + cmd_pid_map: &StringPidMap, name_pid_map: &StringPidMap, + ) { + match self { + ProcWidgetColumn::CpuPercent => { + data.sort_by_cached_key(|p| p.name.to_lowercase()); + data.sort_by(|a, b| { + sort_partial_fn(sort_descending)(a.cpu_usage_percent, b.cpu_usage_percent) + }); + } + ProcWidgetColumn::Memory { show_percentage } => { + data.sort_by_cached_key(|p| p.name.to_lowercase()); + if *show_percentage { + data.sort_by(|a, b| { + sort_partial_fn(sort_descending)(a.mem_usage_percent, b.mem_usage_percent) + }); + } else { + data.sort_by(|a, b| { + sort_partial_fn(sort_descending)(a.mem_usage_bytes, b.mem_usage_bytes) + }); + } + } + ProcWidgetColumn::PidOrCount { is_count } => { + data.sort_by_cached_key(|c| c.name.to_lowercase()); + if *is_count { + if is_using_command { + if sort_descending { + data.sort_by_cached_key(|p| { + Reverse(cmd_pid_map.get(&p.command).map(|v| v.len()).unwrap_or(0)) + }) + } else { + data.sort_by_cached_key(|p| { + cmd_pid_map.get(&p.command).map(|v| v.len()).unwrap_or(0) + }) + } + } else { + #[allow(clippy::collapsible-else-if)] + if sort_descending { + data.sort_by_cached_key(|p| { + Reverse(name_pid_map.get(&p.name).map(|v| v.len()).unwrap_or(0)) + }) + } else { + data.sort_by_cached_key(|p| { + name_pid_map.get(&p.name).map(|v| v.len()).unwrap_or(0) + }) + } + } + } else { + data.sort_by(|a, b| sort_partial_fn(sort_descending)(a.pid, b.pid)); + } + } + ProcWidgetColumn::ProcNameOrCommand { is_command } => { + if *is_command { + if sort_descending { + data.sort_by_cached_key(|p| Reverse(p.command.to_lowercase())); + } else { + data.sort_by_cached_key(|p| p.command.to_lowercase()); + } + } else if sort_descending { + data.sort_by_cached_key(|p| Reverse(p.name.to_lowercase())); + } else { + data.sort_by_cached_key(|p| p.name.to_lowercase()); + } + } + ProcWidgetColumn::ReadPerSecond => { + data.sort_by_cached_key(|p| p.name.to_lowercase()); + if sort_descending { + data.sort_by_key(|a| Reverse(a.read_bytes_per_sec)); + } else { + data.sort_by_key(|a| a.read_bytes_per_sec); + } + } + ProcWidgetColumn::WritePerSecond => { + data.sort_by_cached_key(|p| p.name.to_lowercase()); + if sort_descending { + data.sort_by_key(|a| Reverse(a.write_bytes_per_sec)); + } else { + data.sort_by_key(|a| a.write_bytes_per_sec); + } + } + ProcWidgetColumn::TotalRead => { + data.sort_by_cached_key(|p| p.name.to_lowercase()); + if sort_descending { + data.sort_by_key(|a| Reverse(a.total_read_bytes)); + } else { + data.sort_by_key(|a| a.total_read_bytes); + } + } + ProcWidgetColumn::TotalWrite => { + data.sort_by_cached_key(|p| p.name.to_lowercase()); + if sort_descending { + data.sort_by_key(|a| Reverse(a.total_write_bytes)); + } else { + data.sort_by_key(|a| a.total_write_bytes); + } + } + ProcWidgetColumn::State => { + data.sort_by_cached_key(|p| p.name.to_lowercase()); + if sort_descending { + data.sort_by_cached_key(|p| Reverse(p.process_state.0.to_lowercase())); + } else { + data.sort_by_cached_key(|p| p.process_state.0.to_lowercase()); + } + } + ProcWidgetColumn::User => { + #[cfg(target_family = "unix")] + { + data.sort_by_cached_key(|p| p.name.to_lowercase()); + if sort_descending { + data.sort_by_cached_key(|p| Reverse(p.user.to_lowercase())); + } else { + data.sort_by_cached_key(|p| p.user.to_lowercase()); + } + } + } + } + } + + /// Basically, anything "alphabetical" should sort in ascending order by default. This also includes something like + /// PID, as one would probably want PID to sort by default starting from 0 or 1. + fn default_sort_order(&self) -> SortOrder { + match self { + ProcWidgetColumn::PidOrCount { is_count: true } + | ProcWidgetColumn::CpuPercent + | ProcWidgetColumn::ReadPerSecond + | ProcWidgetColumn::WritePerSecond + | ProcWidgetColumn::TotalRead + | ProcWidgetColumn::TotalWrite + | ProcWidgetColumn::Memory { .. } => SortOrder::Descending, + + ProcWidgetColumn::PidOrCount { is_count: false } + | ProcWidgetColumn::ProcNameOrCommand { .. } + | ProcWidgetColumn::State + | ProcWidgetColumn::User => SortOrder::Ascending, + } + } +} + +impl TableComponentHeader for ProcWidgetColumn { + fn header_text(&self) -> &CellContent { + match self { + ProcWidgetColumn::CpuPercent => &Self::SHORTCUT_CPU_PERCENT, + ProcWidgetColumn::Memory { show_percentage } => { + if *show_percentage { + &Self::SHORTCUT_MEM_PERCENT + } else { + &Self::SHORTCUT_MEM + } + } + ProcWidgetColumn::PidOrCount { is_count } => { + if *is_count { + &Self::COUNT + } else { + &Self::SHORTCUT_PID + } + } + ProcWidgetColumn::ProcNameOrCommand { is_command } => { + if *is_command { + &Self::SHORTCUT_COMMAND + } else { + &Self::SHORTCUT_PROCESS_NAME + } + } + ProcWidgetColumn::ReadPerSecond => &Self::READS_PER_SECOND, + ProcWidgetColumn::WritePerSecond => &Self::WRITES_PER_SECOND, + ProcWidgetColumn::TotalRead => &Self::TOTAL_READ, + ProcWidgetColumn::TotalWrite => &Self::TOTAL_WRITE, + ProcWidgetColumn::State => &Self::STATE, + ProcWidgetColumn::User => &Self::USER, + } + } +} + +pub struct ProcWidget { + pub mode: ProcWidgetMode, + + pub proc_search: ProcessSearchState, + pub table_state: TableComponentState, + pub sort_table_state: TableComponentState, + + pub is_sort_open: bool, + pub force_rerender: bool, + pub force_update_data: bool, + + pub table_data: TableData, +} + +impl ProcWidget { + pub const PID_OR_COUNT: usize = 0; + pub const PROC_NAME_OR_CMD: usize = 1; + pub const CPU: usize = 2; + pub const MEM: usize = 3; + pub const RPS: usize = 4; + pub const WPS: usize = 5; + pub const T_READ: usize = 6; + pub const T_WRITE: usize = 7; + #[cfg(target_family = "unix")] + pub const USER: usize = 8; + #[cfg(target_family = "unix")] + pub const STATE: usize = 9; + #[cfg(not(target_family = "unix"))] + pub const STATE: usize = 8; + + pub fn init( + mode: ProcWidgetMode, is_case_sensitive: bool, is_match_whole_word: bool, + is_use_regex: bool, show_memory_as_values: bool, is_command: bool, + ) -> Self { + let mut process_search_state = ProcessSearchState::default(); + + if is_case_sensitive { + // By default it's off + process_search_state.search_toggle_ignore_case(); + } + if is_match_whole_word { + process_search_state.search_toggle_whole_word(); + } + if is_use_regex { + process_search_state.search_toggle_regex(); + } + + let is_count = matches!(mode, ProcWidgetMode::Grouped); + + let mut sort_table_state = TableComponentState::new(vec![TableComponentColumn::new_hard( + CellContent::Simple("Sort By".into()), + 7, + )]); + sort_table_state.columns[0].calculated_width = 7; + + let table_state = { + let (default_index, default_order) = if matches!(mode, ProcWidgetMode::Tree { .. }) { + (Self::PID_OR_COUNT, SortOrder::Ascending) + } else { + (Self::CPU, SortOrder::Descending) + }; + + let columns = vec![ + TableComponentColumn::new(ProcWidgetColumn::PidOrCount { is_count }), + TableComponentColumn::new_soft( + ProcWidgetColumn::ProcNameOrCommand { is_command }, + Some(0.3), + ), + TableComponentColumn::new(ProcWidgetColumn::CpuPercent), + TableComponentColumn::new(ProcWidgetColumn::Memory { + show_percentage: !show_memory_as_values, + }), + TableComponentColumn::new_hard(ProcWidgetColumn::ReadPerSecond, 8), + TableComponentColumn::new_hard(ProcWidgetColumn::WritePerSecond, 8), + TableComponentColumn::new_hard(ProcWidgetColumn::TotalRead, 8), + TableComponentColumn::new_hard(ProcWidgetColumn::TotalWrite, 8), + #[cfg(target_family = "unix")] + TableComponentColumn::new_soft(ProcWidgetColumn::User, Some(0.05)), + TableComponentColumn::new_hard(ProcWidgetColumn::State, 7), + ]; + + let default_sort_orderings = columns + .iter() + .map(|column| column.header.default_sort_order()) + .collect(); + + TableComponentState::new(columns).sort_state(SortState::Sortable(SortableState::new( + default_index, + default_order, + default_sort_orderings, + ))) + }; + + ProcWidget { + proc_search: process_search_state, + table_state, + sort_table_state, + is_sort_open: false, + mode, + force_rerender: true, + force_update_data: false, + table_data: TableData::default(), + } + } + + pub fn is_using_command(&self) -> bool { + if let Some(ProcWidgetColumn::ProcNameOrCommand { is_command }) = self + .table_state + .columns + .get(ProcWidget::PROC_NAME_OR_CMD) + .map(|col| &col.header) + { + *is_command + } else { + // Technically impossible. + false + } + } + + /// This function *only* updates the displayed process data. If there is a need to update the actual *stored* data, + /// call it before this function. + pub fn update_displayed_process_data(&mut self, data_collection: &DataCollection) { + let search_query = if self.proc_search.search_state.is_invalid_or_blank_search() { + &None + } else { + &self.proc_search.search_state.query + }; + let table_data = match &self.mode { + ProcWidgetMode::Tree { collapsed_pids } => { + self.get_tree_table_data(collapsed_pids, data_collection, search_query) + } + ProcWidgetMode::Grouped | ProcWidgetMode::Normal => { + self.get_normal_table_data(data_collection, search_query) + } + }; + + // Now also update the scroll position if needed (that is, the old scroll position was too big for the new list). + if self.table_state.current_scroll_position >= table_data.data.len() { + self.table_state.current_scroll_position = table_data.data.len().saturating_sub(1); + self.table_state.scroll_bar = 0; + self.table_state.scroll_direction = ScrollDirection::Down; + } + + // Finally, move this data to the widget itself. + self.table_data = table_data; + } + + fn get_tree_table_data( + &self, collapsed_pids: &FxHashSet, data_collection: &DataCollection, + search_query: &Option, + ) -> TableData { + const BRANCH_ENDING: char = '└'; + const BRANCH_VERTICAL: char = '│'; + const BRANCH_SPLIT: char = '├'; + const BRANCH_HORIZONTAL: char = '─'; + + let ProcessData { + process_harvest, + cmd_pid_map, + name_pid_map, + process_parent_mapping, + orphan_pids, + .. + } = &data_collection.process_data; + + let mut col_widths = vec![ + 0; + self.table_state + .columns + .iter() + .filter(|c| c.is_skipped()) + .count() + ]; + + let matching_pids = data_collection + .process_data + .process_harvest + .iter() + .map(|(pid, process)| { + ( + *pid, + search_query + .as_ref() + .map(|q| q.check(process, self.is_using_command())) + .unwrap_or(true), + ) + }) + .collect::>(); + + let filtered_tree = { + let mut filtered_tree = FxHashMap::default(); + + let mut stack = orphan_pids + .iter() + .filter_map(|process| process_harvest.get(process)) + .collect_vec(); + let mut visited_pids = FxHashMap::default(); + + while let Some(process) = stack.last() { + let is_process_matching = *matching_pids.get(&process.pid).unwrap_or(&false); + + if let Some(children_pids) = process_parent_mapping.get(&process.pid) { + if children_pids + .iter() + .all(|pid| visited_pids.contains_key(pid)) + { + let shown_children = children_pids + .iter() + .filter(|pid| visited_pids.get(*pid).copied().unwrap_or(false)) + .collect_vec(); + let is_shown = is_process_matching || !shown_children.is_empty(); + visited_pids.insert(process.pid, is_shown); + + if is_shown { + filtered_tree.insert( + process.pid, + shown_children + .into_iter() + .filter_map(|pid| { + process_harvest.get(pid).map(|process| process.pid) + }) + .collect_vec(), + ); + } + + stack.pop(); + } else { + children_pids + .iter() + .filter_map(|process| process_harvest.get(process)) + .rev() + .for_each(|process| { + stack.push(process); + }); + } + } else { + visited_pids.insert(process.pid, is_process_matching); + stack.pop(); + } + } + + filtered_tree + }; + + let mut resulting_strings = vec![]; + let mut prefixes = vec![]; + let mut stack = orphan_pids + .iter() + .filter(|pid| filtered_tree.contains_key(*pid)) + .filter_map(|child| process_harvest.get(child)) + .collect_vec(); + + self.try_sort(&mut stack, data_collection); + + let mut length_stack = vec![stack.len()]; + + while let (Some(process), Some(siblings_left)) = (stack.pop(), length_stack.last_mut()) { + *siblings_left -= 1; + + let is_disabled = !*matching_pids.get(&process.pid).unwrap_or(&false); + let is_last = *siblings_left == 0; + + if collapsed_pids.contains(&process.pid) { + let mut summed_process = process.clone(); + + if let Some(children_pids) = filtered_tree.get(&process.pid) { + let mut sum_queue = children_pids + .iter() + .filter_map(|child| process_harvest.get(child)) + .collect_vec(); + + while let Some(process) = sum_queue.pop() { + summed_process.add(process); + + if let Some(pids) = filtered_tree.get(&process.pid) { + sum_queue.extend(pids.iter().filter_map(|c| process_harvest.get(c))); + } + } + } + + let prefix = if prefixes.is_empty() { + "+ ".to_string() + } else { + format!( + "{}{}{} + ", + prefixes.join(""), + if is_last { BRANCH_ENDING } else { BRANCH_SPLIT }, + BRANCH_HORIZONTAL + ) + }; + + let process_text = self.process_to_text( + &summed_process, + &mut col_widths, + cmd_pid_map, + name_pid_map, + Some(prefix), + is_disabled, + ); + resulting_strings.push(process_text); + } else { + let prefix = if prefixes.is_empty() { + String::default() + } else { + format!( + "{}{}{} ", + prefixes.join(""), + if is_last { BRANCH_ENDING } else { BRANCH_SPLIT }, + BRANCH_HORIZONTAL + ) + }; + let process_text = self.process_to_text( + process, + &mut col_widths, + cmd_pid_map, + name_pid_map, + Some(prefix), + is_disabled, + ); + resulting_strings.push(process_text); + + if let Some(children_pids) = filtered_tree.get(&process.pid) { + if prefixes.is_empty() { + prefixes.push(String::default()); + } else { + prefixes.push(if is_last { + " ".to_string() + } else { + format!("{} ", BRANCH_VERTICAL) + }); + } + + let mut children = children_pids + .iter() + .filter_map(|child_pid| process_harvest.get(child_pid)) + .collect_vec(); + self.try_sort(&mut children, data_collection); + length_stack.push(children.len()); + stack.extend(children); + } + } + + while let Some(children_left) = length_stack.last() { + if *children_left == 0 { + length_stack.pop(); + prefixes.pop(); + } else { + break; + } + } + } + + TableData { + data: resulting_strings, + col_widths, + } + } + + fn get_normal_table_data( + &self, data_collection: &DataCollection, search_query: &Option, + ) -> TableData { + let mut id_pid_map: FxHashMap; + let filtered_iter = data_collection + .process_data + .process_harvest + .values() + .filter(|p| { + search_query + .as_ref() + .map(|q| q.check(p, self.is_using_command())) + .unwrap_or(true) + }); + + let mut filtered_data = if let ProcWidgetMode::Grouped = self.mode { + id_pid_map = FxHashMap::default(); + filtered_iter.for_each(|process| { + let id = if self.is_using_command() { + &process.command + } else { + &process.name + }; + + if let Some(grouped_process_harvest) = id_pid_map.get_mut(id) { + grouped_process_harvest.add(process); + } else { + id_pid_map.insert(id.clone(), process.clone()); + } + }); + + id_pid_map.values().collect::>() + } else { + filtered_iter.collect::>() + }; + + self.try_sort(&mut filtered_data, data_collection); + self.harvest_to_table_data(&filtered_data, data_collection) + } + + fn try_sort(&self, filtered_data: &mut [&ProcessHarvest], data_collection: &DataCollection) { + let cmd_pid_map = &data_collection.process_data.cmd_pid_map; + let name_pid_map = &data_collection.process_data.name_pid_map; + + if let SortState::Sortable(state) = &self.table_state.sort_state { + let index = state.current_index; + let order = &state.order; + + if let Some(column) = self.table_state.columns.get(index) { + column.header.sort( + order.is_descending(), + filtered_data, + self.is_using_command(), + cmd_pid_map, + name_pid_map, + ); + } + } + } + + fn process_to_text( + &self, process: &ProcessHarvest, col_widths: &mut [usize], cmd_pid_map: &StringPidMap, + name_pid_map: &StringPidMap, proc_prefix: Option, is_disabled: bool, + ) -> TableRow { + let mut contents = Vec::with_capacity(self.num_shown_columns()); + + contents.extend( + self.table_state + .columns + .iter() + .enumerate() + .map(|(itx, column)| { + let col_text = match column.header { + ProcWidgetColumn::CpuPercent => { + format!("{:.1}%", process.cpu_usage_percent).into() + } + ProcWidgetColumn::Memory { show_percentage } => { + if show_percentage { + format!("{:.1}%", process.mem_usage_percent).into() + } else { + binary_byte_string(process.mem_usage_bytes).into() + } + } + ProcWidgetColumn::PidOrCount { is_count } => { + if is_count { + if self.is_using_command() { + cmd_pid_map + .get(&process.command) + .map(|v| v.len()) + .unwrap_or(0) + .to_string() + .into() + } else { + name_pid_map + .get(&process.name) + .map(|v| v.len()) + .unwrap_or(0) + .to_string() + .into() + } + } else { + process.pid.to_string().into() + } + } + ProcWidgetColumn::ProcNameOrCommand { is_command } => { + let val = if is_command { + process.command.clone() + } else { + process.name.clone() + }; + + if let Some(prefix) = &proc_prefix { + concat_string!(prefix, val).into() + } else { + val.into() + } + } + ProcWidgetColumn::ReadPerSecond => { + dec_bytes_per_second_string(process.read_bytes_per_sec).into() + } + ProcWidgetColumn::WritePerSecond => { + dec_bytes_per_second_string(process.write_bytes_per_sec).into() + } + ProcWidgetColumn::TotalRead => { + dec_bytes_per_second_string(process.total_read_bytes).into() + } + ProcWidgetColumn::TotalWrite => { + dec_bytes_per_second_string(process.total_write_bytes).into() + } + ProcWidgetColumn::State => CellContent::HasAlt { + main: process.process_state.0.clone().into(), + alt: process.process_state.1.to_string().into(), + }, + ProcWidgetColumn::User => { + #[cfg(target_family = "unix")] + { + process.user.clone().into() + } + #[cfg(not(target_family = "unix"))] + { + "".into() + } + } + }; + + if let Some(curr) = col_widths.get_mut(itx) { + *curr = max(*curr, col_text.len()); + } + + col_text + }), + ); + + if is_disabled { + TableRow::Styled(contents, tui::style::Style::default()) + } else { + TableRow::Raw(contents) + } + } + + fn harvest_to_table_data( + &self, process_data: &[&ProcessHarvest], data_collection: &DataCollection, + ) -> TableData { + let cmd_pid_map = &data_collection.process_data.cmd_pid_map; + let name_pid_map = &data_collection.process_data.name_pid_map; + + let mut col_widths = vec![0; self.table_state.columns.len()]; + + let data = process_data + .iter() + .map(|process| { + self.process_to_text( + process, + &mut col_widths, + cmd_pid_map, + name_pid_map, + None, + false, + ) + }) + .collect(); + + TableData { data, col_widths } + } + + fn get_mut_proc_col(&mut self, index: usize) -> Option<&mut ProcWidgetColumn> { + self.table_state + .columns + .get_mut(index) + .map(|col| &mut col.header) + } + + pub fn toggle_mem_percentage(&mut self) { + if let Some(ProcWidgetColumn::Memory { show_percentage }) = self.get_mut_proc_col(Self::MEM) + { + *show_percentage = !*show_percentage; + self.force_data_update(); + } + } + + /// Forces an update of the data stored. + #[inline] + pub fn force_data_update(&mut self) { + self.force_update_data = true; + } + + /// Forces an entire rerender and update of the data stored. + #[inline] + pub fn force_rerender_and_update(&mut self) { + self.force_rerender = true; + self.force_update_data = true; + } + + /// Marks the selected column as hidden, and automatically resets the selected column if currently selected. + fn hide_column(&mut self, index: usize) { + if let Some(col) = self.table_state.columns.get_mut(index) { + col.is_hidden = true; + + if let SortState::Sortable(state) = &mut self.table_state.sort_state { + if state.current_index == index { + state.current_index = Self::CPU; + state.order = SortOrder::Descending; + } + } + } + } + + /// Marks the selected column as shown. + fn show_column(&mut self, index: usize) { + if let Some(col) = self.table_state.columns.get_mut(index) { + col.is_hidden = false; + } + } + + /// Select a column. If the column is already selected, then just toggle the sort order. + pub fn select_column(&mut self, new_sort_index: usize) { + if let SortState::Sortable(state) = &mut self.table_state.sort_state { + state.update_sort_index(new_sort_index); + self.force_data_update(); + } + } + + pub fn toggle_tree_branch(&mut self) { + if let ProcWidgetMode::Tree { collapsed_pids } = &mut self.mode { + let current_posn = self.table_state.current_scroll_position; + if let Some(current_row) = self.table_data.data.get(current_posn) { + if let Ok(pid) = current_row.row()[ProcWidget::PID_OR_COUNT] + .main_text() + .parse::() + { + if !collapsed_pids.remove(&pid) { + collapsed_pids.insert(pid); + } + self.force_data_update(); + } + } + } + } + + pub fn toggle_command(&mut self) { + if let Some(col) = self.table_state.columns.get_mut(Self::PROC_NAME_OR_CMD) { + if let ProcWidgetColumn::ProcNameOrCommand { is_command } = &mut col.header { + *is_command = !*is_command; + + if let WidthBounds::Soft { max_percentage, .. } = &mut col.width_bounds { + if *is_command { + *max_percentage = Some(0.7); + } else { + *max_percentage = match self.mode { + ProcWidgetMode::Tree { .. } => Some(0.5), + ProcWidgetMode::Grouped | ProcWidgetMode::Normal => Some(0.3), + }; + } + } + + self.force_rerender_and_update(); + } + } + } + + /// Toggles the appropriate columns/settings when tab is pressed. + /// + /// If count is enabled, we should set the mode to [`ProcWidgetMode::Grouped`], and switch off the User and State + /// columns. We should also move the user off of the columns if they were selected, as those columns are now hidden + /// (handled by internal method calls), and go back to the "defaults". + /// + /// Otherwise, if count is disabled, then the User and State columns should be re-enabled, and the mode switched + /// to [`ProcWidgetMode::Normal`]. + pub fn toggle_tab(&mut self) { + if !matches!(self.mode, ProcWidgetMode::Tree { .. }) { + if let Some(ProcWidgetColumn::PidOrCount { is_count }) = + self.get_mut_proc_col(Self::PID_OR_COUNT) + { + *is_count = !*is_count; + + if *is_count { + #[cfg(target_family = "unix")] + self.hide_column(Self::USER); + self.hide_column(Self::STATE); + self.mode = ProcWidgetMode::Grouped; + + self.sort_table_state.current_scroll_position = self + .sort_table_state + .current_scroll_position + .clamp(0, self.num_enabled_columns().saturating_sub(1)); + } else { + #[cfg(target_family = "unix")] + self.show_column(Self::USER); + self.show_column(Self::STATE); + self.mode = ProcWidgetMode::Normal; + } + self.force_rerender_and_update(); + } + } + } + + pub fn get_search_cursor_position(&self) -> usize { + self.proc_search.search_state.grapheme_cursor.cur_cursor() + } + + pub fn get_char_cursor_position(&self) -> usize { + self.proc_search.search_state.char_cursor_position + } + + pub fn is_search_enabled(&self) -> bool { + self.proc_search.search_state.is_enabled + } + + pub fn get_current_search_query(&self) -> &String { + &self.proc_search.search_state.current_search_query + } + + pub fn update_query(&mut self) { + if self + .proc_search + .search_state + .current_search_query + .is_empty() + { + self.proc_search.search_state.is_blank_search = true; + self.proc_search.search_state.is_invalid_search = false; + self.proc_search.search_state.error_message = None; + } else { + match parse_query( + &self.proc_search.search_state.current_search_query, + self.proc_search.is_searching_whole_word, + self.proc_search.is_ignoring_case, + self.proc_search.is_searching_with_regex, + ) { + Ok(parsed_query) => { + self.proc_search.search_state.query = Some(parsed_query); + self.proc_search.search_state.is_blank_search = false; + self.proc_search.search_state.is_invalid_search = false; + self.proc_search.search_state.error_message = None; + } + Err(err) => { + self.proc_search.search_state.is_blank_search = false; + self.proc_search.search_state.is_invalid_search = true; + self.proc_search.search_state.error_message = Some(err.to_string()); + } + } + } + self.table_state.scroll_bar = 0; + self.table_state.current_scroll_position = 0; + + self.force_data_update(); + } + + pub fn clear_search(&mut self) { + self.proc_search.search_state.reset(); + self.force_data_update(); + } + + pub fn search_walk_forward(&mut self, start_position: usize) { + self.proc_search + .search_state + .grapheme_cursor + .next_boundary( + &self.proc_search.search_state.current_search_query[start_position..], + start_position, + ) + .unwrap(); + } + + pub fn search_walk_back(&mut self, start_position: usize) { + self.proc_search + .search_state + .grapheme_cursor + .prev_boundary( + &self.proc_search.search_state.current_search_query[..start_position], + 0, + ) + .unwrap(); + } + + /// Returns the number of columns *visible*. + pub fn num_shown_columns(&self) -> usize { + self.table_state + .columns + .iter() + .filter(|c| !c.is_skipped()) + .count() + } + + /// Returns the number of columns *enabled*. Note this differs from *visible* - a column may be enabled but not + /// visible (e.g. off screen). + pub fn num_enabled_columns(&self) -> usize { + self.table_state + .columns + .iter() + .filter(|c| !c.is_hidden) + .count() + } + + /// Sets the [`ProcWidget`]'s current sort index to whatever was in the sort table. + pub(crate) fn use_sort_table_value(&mut self) { + if let SortState::Sortable(st) = &mut self.table_state.sort_state { + st.update_sort_index(self.sort_table_state.current_scroll_position); + + self.is_sort_open = false; + self.force_rerender_and_update(); + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_sort() {} + + #[test] + fn assert_correct_columns() { + #[track_caller] + fn test_columns(mode: ProcWidgetMode, mem_as_val: bool, is_cmd: bool) { + let is_count = matches!(mode, ProcWidgetMode::Grouped); + let is_command = is_cmd; + let show_percentage = !mem_as_val; + + let proc = ProcWidget::init(mode, false, false, false, mem_as_val, is_command); + let columns = &proc.table_state.columns; + + assert_eq!( + columns[ProcWidget::PID_OR_COUNT].header, + ProcWidgetColumn::PidOrCount { is_count } + ); + assert_eq!( + columns[ProcWidget::PROC_NAME_OR_CMD].header, + ProcWidgetColumn::ProcNameOrCommand { is_command } + ); + assert!(matches!( + columns[ProcWidget::CPU].header, + ProcWidgetColumn::CpuPercent + )); + assert_eq!( + columns[ProcWidget::MEM].header, + ProcWidgetColumn::Memory { show_percentage } + ); + assert!(matches!( + columns[ProcWidget::RPS].header, + ProcWidgetColumn::ReadPerSecond + )); + assert!(matches!( + columns[ProcWidget::WPS].header, + ProcWidgetColumn::WritePerSecond + )); + assert!(matches!( + columns[ProcWidget::T_READ].header, + ProcWidgetColumn::TotalRead + )); + assert!(matches!( + columns[ProcWidget::T_WRITE].header, + ProcWidgetColumn::TotalWrite + )); + #[cfg(target_family = "unix")] + { + assert!(matches!( + columns[ProcWidget::USER].header, + ProcWidgetColumn::User + )); + } + assert!(matches!( + columns[ProcWidget::STATE].header, + ProcWidgetColumn::State + )); + } + + test_columns(ProcWidgetMode::Grouped, true, true); + test_columns(ProcWidgetMode::Grouped, false, true); + test_columns(ProcWidgetMode::Grouped, true, false); + test_columns( + ProcWidgetMode::Tree { + collapsed_pids: Default::default(), + }, + true, + true, + ); + test_columns(ProcWidgetMode::Normal, true, true); + } +} diff --git a/src/bin/main.rs b/src/bin/main.rs index 6a1f0aae7..e3a863151 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -54,13 +54,8 @@ fn main() -> Result<()> { )?; // Create painter and set colours. - let mut painter = canvas::Painter::init( - widget_layout, - app.app_config_fields.table_gap, - app.app_config_fields.use_basic_mode, - &config, - get_color_scheme(&matches, &config)?, - )?; + let mut painter = + canvas::Painter::init(widget_layout, &config, get_color_scheme(&matches, &config)?)?; // Create termination mutex and cvar #[allow(clippy::mutex_atomic)] @@ -135,11 +130,11 @@ fn main() -> Result<()> { if handle_key_event_or_break(event, &mut app, &collection_thread_ctrl_sender) { break; } - handle_force_redraws(&mut app); + update_data(&mut app); } BottomEvent::MouseInput(event) => { handle_mouse_event(event, &mut app); - handle_force_redraws(&mut app); + update_data(&mut app); } BottomEvent::Update(data) => { app.data_collection.eat_data(data); @@ -158,46 +153,45 @@ fn main() -> Result<()> { if app.used_widgets.use_net { let network_data = convert_network_data_points( &app.data_collection, - false, app.app_config_fields.use_basic_mode || app.app_config_fields.use_old_network_legend, &app.app_config_fields.network_scale_type, &app.app_config_fields.network_unit_type, app.app_config_fields.network_use_binary_prefix, ); - app.canvas_data.network_data_rx = network_data.rx; - app.canvas_data.network_data_tx = network_data.tx; - app.canvas_data.rx_display = network_data.rx_display; - app.canvas_data.tx_display = network_data.tx_display; + app.converted_data.network_data_rx = network_data.rx; + app.converted_data.network_data_tx = network_data.tx; + app.converted_data.rx_display = network_data.rx_display; + app.converted_data.tx_display = network_data.tx_display; if let Some(total_rx_display) = network_data.total_rx_display { - app.canvas_data.total_rx_display = total_rx_display; + app.converted_data.total_rx_display = total_rx_display; } if let Some(total_tx_display) = network_data.total_tx_display { - app.canvas_data.total_tx_display = total_tx_display; + app.converted_data.total_tx_display = total_tx_display; } } // Disk if app.used_widgets.use_disk { - app.canvas_data.disk_data = convert_disk_row(&app.data_collection); + app.converted_data.disk_data = convert_disk_row(&app.data_collection); } // Temperatures if app.used_widgets.use_temp { - app.canvas_data.temp_sensor_data = convert_temp_row(&app); + app.converted_data.temp_sensor_data = convert_temp_row(&app); } // Memory if app.used_widgets.use_mem { - app.canvas_data.mem_data = - convert_mem_data_points(&app.data_collection, false); - app.canvas_data.swap_data = - convert_swap_data_points(&app.data_collection, false); + app.converted_data.mem_data = + convert_mem_data_points(&app.data_collection); + app.converted_data.swap_data = + convert_swap_data_points(&app.data_collection); let (memory_labels, swap_labels) = convert_mem_labels(&app.data_collection); - app.canvas_data.mem_labels = memory_labels; - app.canvas_data.swap_labels = swap_labels; + app.converted_data.mem_labels = memory_labels; + app.converted_data.swap_labels = swap_labels; } if app.used_widgets.use_cpu { @@ -205,25 +199,28 @@ fn main() -> Result<()> { convert_cpu_data_points( &app.data_collection, - &mut app.canvas_data.cpu_data, - false, + &mut app.converted_data.cpu_data, ); - app.canvas_data.load_avg_data = app.data_collection.load_avg_harvest; + app.converted_data.load_avg_data = app.data_collection.load_avg_harvest; } // Processes if app.used_widgets.use_proc { - update_all_process_lists(&mut app); + for proc in app.proc_state.widget_states.values_mut() { + proc.force_data_update(); + } } // Battery #[cfg(feature = "battery")] { if app.used_widgets.use_battery { - app.canvas_data.battery_data = + app.converted_data.battery_data = convert_battery_harvest(&app.data_collection); } } + + update_data(&mut app); } } BottomEvent::Clean => { diff --git a/src/canvas.rs b/src/canvas.rs index 2070327af..e69c50539 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -1,5 +1,5 @@ use itertools::izip; -use std::{collections::HashMap, str::FromStr}; +use std::str::FromStr; use tui::{ backend::Backend, @@ -18,11 +18,9 @@ use crate::{ App, }, constants::*, - data_conversion::{ConvertedBatteryData, ConvertedCpuData, ConvertedProcessData}, options::Config, utils::error, utils::error::BottomError, - Pid, }; pub use self::components::Point; @@ -33,30 +31,6 @@ mod dialogs; mod drawing_utils; mod widgets; -#[derive(Default)] -pub struct DisplayableData { - pub rx_display: String, - pub tx_display: String, - pub total_rx_display: String, - pub total_tx_display: String, - pub network_data_rx: Vec, - pub network_data_tx: Vec, - pub disk_data: Vec>, - pub temp_sensor_data: Vec>, - pub single_process_data: HashMap, // Contains single process data, key is PID - pub finalized_process_data_map: HashMap>, // What's actually displayed, key is the widget ID. - pub stringified_process_data_map: HashMap)>, bool)>>, // Represents the row and whether it is disabled, key is the widget ID - - pub mem_labels: Option<(String, String)>, - pub swap_labels: Option<(String, String)>, - - pub mem_data: Vec, // TODO: Switch this and all data points over to a better data structure... - pub swap_data: Vec, - pub load_avg_data: [f32; 3], - pub cpu_data: Vec, - pub battery_data: Vec, -} - #[derive(Debug)] pub enum ColourScheme { Default, @@ -94,20 +68,20 @@ pub struct Painter { height: u16, width: u16, styled_help_text: Vec>, - is_mac_os: bool, // FIXME: This feels out of place... + is_mac_os: bool, // TODO: This feels out of place... + + // TODO: Redo this entire thing. row_constraints: Vec, col_constraints: Vec>, col_row_constraints: Vec>>, layout_constraints: Vec>>>, derived_widget_draw_locs: Vec>>>, widget_layout: BottomLayout, - table_height_offset: u16, } impl Painter { pub fn init( - widget_layout: BottomLayout, table_gap: u16, is_basic_mode: bool, config: &Config, - colour_scheme: ColourScheme, + widget_layout: BottomLayout, config: &Config, colour_scheme: ColourScheme, ) -> anyhow::Result { // Now for modularity; we have to also initialize the base layouts! // We want to do this ONCE and reuse; after this we can just construct @@ -188,7 +162,6 @@ impl Painter { layout_constraints, widget_layout, derived_widget_draw_locs: Vec::default(), - table_height_offset: if is_basic_mode { 2 } else { 4 } + table_gap, }; if let ColourScheme::Custom = colour_scheme { @@ -338,12 +311,6 @@ impl Painter { for battery_widget in app_state.battery_state.widget_states.values_mut() { battery_widget.tab_click_locs = None; } - - // Reset column headers for sorting in process widget... - for proc_widget in app_state.proc_state.widget_states.values_mut() { - proc_widget.columns.column_header_y_loc = None; - proc_widget.columns.column_header_x_locs = None; - } } if app_state.help_dialog_state.is_showing_help { @@ -506,7 +473,7 @@ impl Painter { _ => 0, }; - self.draw_process_features(f, app_state, rect[0], true, widget_id); + self.draw_process_widget(f, app_state, rect[0], true, widget_id); } Battery => self.draw_battery_display( f, @@ -524,7 +491,7 @@ impl Painter { self.draw_frozen_indicator(f, frozen_draw_loc); } - let actual_cpu_data_len = app_state.canvas_data.cpu_data.len().saturating_sub(1); + let actual_cpu_data_len = app_state.converted_data.cpu_data.len().saturating_sub(1); // This fixes #397, apparently if the height is 1, it can't render the CPU bars... let cpu_height = { @@ -585,7 +552,7 @@ impl Painter { ProcSort => 2, _ => 0, }; - self.draw_process_features( + self.draw_process_widget( f, app_state, vertical_chunks[3], @@ -736,7 +703,7 @@ impl Painter { Disk => { self.draw_disk_table(f, app_state, *widget_draw_loc, true, widget.widget_id) } - Proc => self.draw_process_features( + Proc => self.draw_process_widget( f, app_state, *widget_draw_loc, diff --git a/src/canvas/components/text_table.rs b/src/canvas/components/text_table.rs index 8b1378917..f5fc31e73 100644 --- a/src/canvas/components/text_table.rs +++ b/src/canvas/components/text_table.rs @@ -1 +1,502 @@ +use std::{ + borrow::Cow, + cmp::{max, min}, +}; +use concat_string::concat_string; +use tui::{ + backend::Backend, + layout::{Constraint, Direction, Layout, Rect}, + style::Style, + text::{Span, Spans, Text}, + widgets::{Block, Borders, Row, Table}, + Frame, +}; +use unicode_segmentation::UnicodeSegmentation; + +use crate::{ + app::{ + self, layout_manager::BottomWidget, CellContent, SortState, TableComponentColumn, + TableComponentHeader, TableComponentState, WidthBounds, + }, + constants::{SIDE_BORDERS, TABLE_GAP_HEIGHT_LIMIT}, + data_conversion::{TableData, TableRow}, +}; + +pub struct TextTableTitle<'a> { + pub title: Cow<'a, str>, + pub is_expanded: bool, +} + +pub struct TextTable<'a> { + pub table_gap: u16, + pub is_force_redraw: bool, // TODO: Is this force redraw thing needed? Or is there a better way? + pub recalculate_column_widths: bool, + + /// The header style. + pub header_style: Style, + + /// The border style. + pub border_style: Style, + + /// The highlighted text style. + pub highlighted_text_style: Style, + + /// The graph title and whether it is expanded (if there is one). + pub title: Option>, + + /// Whether this widget is selected. + pub is_on_widget: bool, + + /// Whether to draw all borders. + pub draw_border: bool, + + /// Whether to show the scroll position. + pub show_table_scroll_position: bool, + + /// The title style. + pub title_style: Style, + + /// The text style. + pub text_style: Style, + + /// Whether to determine widths from left to right. + pub left_to_right: bool, +} + +impl<'a> TextTable<'a> { + /// Generates a title for the [`TextTable`] widget, given the available space. + fn generate_title(&self, draw_loc: Rect, pos: usize, total: usize) -> Option> { + self.title + .as_ref() + .map(|TextTableTitle { title, is_expanded }| { + let title = if self.show_table_scroll_position { + let title_string = concat_string!( + title, + "(", + pos.to_string(), + " of ", + total.to_string(), + ") " + ); + + if title_string.len() + 2 <= draw_loc.width.into() { + title_string + } else { + title.to_string() + } + } else { + title.to_string() + }; + + if *is_expanded { + let title_base = concat_string!(title, "── Esc to go back "); + let esc = concat_string!( + "─", + "─".repeat(usize::from(draw_loc.width).saturating_sub( + UnicodeSegmentation::graphemes(title_base.as_str(), true).count() + 2 + )), + "─ Esc to go back " + ); + Spans::from(vec![ + Span::styled(title, self.title_style), + Span::styled(esc, self.border_style), + ]) + } else { + Spans::from(Span::styled(title, self.title_style)) + } + }) + } + + pub fn draw_text_table( + &self, f: &mut Frame<'_, B>, draw_loc: Rect, state: &mut TableComponentState, + table_data: &TableData, btm_widget: Option<&mut BottomWidget>, + ) { + // TODO: This is a *really* ugly hack to get basic mode to hide the border when not selected, without shifting everything. + let is_not_basic = self.is_on_widget || self.draw_border; + let margined_draw_loc = Layout::default() + .constraints([Constraint::Percentage(100)]) + .horizontal_margin(if is_not_basic { 0 } else { 1 }) + .direction(Direction::Horizontal) + .split(draw_loc)[0]; + + let block = if self.draw_border { + let block = Block::default() + .borders(Borders::ALL) + .border_style(self.border_style); + + if let Some(title) = self.generate_title( + draw_loc, + state.current_scroll_position.saturating_add(1), + table_data.data.len(), + ) { + block.title(title) + } else { + block + } + } else if self.is_on_widget { + Block::default() + .borders(SIDE_BORDERS) + .border_style(self.border_style) + } else { + Block::default().borders(Borders::NONE) + }; + + let inner_rect = block.inner(margined_draw_loc); + let (inner_width, inner_height) = { (inner_rect.width, inner_rect.height) }; + + if inner_width == 0 || inner_height == 0 { + f.render_widget(block, margined_draw_loc); + } else { + let show_header = inner_height > 1; + let header_height = if show_header { 1 } else { 0 }; + let table_gap = if !show_header || draw_loc.height < TABLE_GAP_HEIGHT_LIMIT { + 0 + } else { + self.table_gap + }; + + let sliced_vec = { + let num_rows = usize::from(inner_height.saturating_sub(table_gap + header_height)); + let start = get_start_position( + num_rows, + &state.scroll_direction, + &mut state.scroll_bar, + state.current_scroll_position, + self.is_force_redraw, + ); + let end = min(table_data.data.len(), start + num_rows); + state + .table_state + .select(Some(state.current_scroll_position.saturating_sub(start))); + &table_data.data[start..end] + }; + + // Calculate widths + if self.recalculate_column_widths { + state + .columns + .iter_mut() + .zip(&table_data.col_widths) + .for_each(|(column, data_width)| match &mut column.width_bounds { + WidthBounds::Soft { + min_width: _, + desired, + max_percentage: _, + } => { + *desired = max( + *desired, + max(column.header.header_text().len(), *data_width) as u16, + ); + } + WidthBounds::CellWidth => {} + WidthBounds::Hard(_width) => {} + }); + + state.calculate_column_widths(inner_width, self.left_to_right); + + if let SortState::Sortable(st) = &mut state.sort_state { + let row_widths = state + .columns + .iter() + .filter_map(|c| { + if c.calculated_width == 0 { + None + } else { + Some(c.calculated_width) + } + }) + .collect::>(); + + st.update_visual_index(inner_rect, &row_widths); + } + + // Update draw loc in widget map + if let Some(btm_widget) = btm_widget { + btm_widget.top_left_corner = Some((draw_loc.x, draw_loc.y)); + btm_widget.bottom_right_corner = + Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height)); + } + } + + let columns = &state.columns; + let header = build_header(columns, &state.sort_state) + .style(self.header_style) + .bottom_margin(table_gap); + let table_rows = sliced_vec.iter().map(|row| { + let (row, style) = match row { + TableRow::Raw(row) => (row, None), + TableRow::Styled(row, style) => (row, Some(*style)), + }; + + Row::new(row.iter().zip(columns).filter_map(|(cell, c)| { + if c.calculated_width == 0 { + None + } else { + Some(truncate_text(cell, c.calculated_width.into(), style)) + } + })) + }); + + if !table_data.data.is_empty() { + let widget = { + let mut table = Table::new(table_rows) + .block(block) + .highlight_style(self.highlighted_text_style) + .style(self.text_style); + + if show_header { + table = table.header(header); + } + + table + }; + + f.render_stateful_widget( + widget.widths( + &(columns + .iter() + .filter_map(|c| { + if c.calculated_width == 0 { + None + } else { + Some(Constraint::Length(c.calculated_width)) + } + }) + .collect::>()), + ), + margined_draw_loc, + &mut state.table_state, + ); + } else { + f.render_widget(block, margined_draw_loc); + } + } + } +} + +/// Constructs the table header. +fn build_header<'a, H: TableComponentHeader>( + columns: &'a [TableComponentColumn], sort_state: &SortState, +) -> Row<'a> { + use itertools::Either; + + const UP_ARROW: &str = "▲"; + const DOWN_ARROW: &str = "▼"; + + let iter = match sort_state { + SortState::Unsortable => Either::Left(columns.iter().filter_map(|c| { + if c.calculated_width == 0 { + None + } else { + Some(truncate_text( + c.header.header_text(), + c.calculated_width.into(), + None, + )) + } + })), + SortState::Sortable(s) => { + let order = &s.order; + let index = s.current_index; + + let arrow = match order { + app::SortOrder::Ascending => UP_ARROW, + app::SortOrder::Descending => DOWN_ARROW, + }; + + Either::Right(columns.iter().enumerate().filter_map(move |(itx, c)| { + if c.calculated_width == 0 { + None + } else if itx == index { + Some(truncate_suffixed_text( + c.header.header_text(), + arrow, + c.calculated_width.into(), + None, + )) + } else { + Some(truncate_text( + c.header.header_text(), + c.calculated_width.into(), + None, + )) + } + })) + } + }; + + Row::new(iter) +} + +/// Truncates text if it is too long, and adds an ellipsis at the end if needed. +fn truncate_text(content: &CellContent, width: usize, row_style: Option