diff --git a/Cargo.toml b/Cargo.toml index b7b7b5fe..fc529251 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 5f9ac413..d139f60f 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<PathBuf>, + pub config: Config, // TODO: Is this even used...? + pub config_path: Option<PathBuf>, // 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<query::Query> { 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<Pid>); - 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; - } - } else { - let process = corresponding_filtered_process_list - [proc_widget_state.scroll_state.current_scroll_position] - .clone(); - current_process = (process.name.clone(), vec![process.pid]) - }; + 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; + } + } 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; + } } } } @@ -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<usize, _> = - (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 1860c91b..0f9d65d7 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<Value>, } +pub type StringPidMap = FxHashMap<String, Vec<Pid>>; + +#[derive(Clone, Debug, Default)] +pub struct ProcessData { + /// A PID to process data map. + pub process_harvest: FxHashMap<Pid, ProcessHarvest>, + + /// 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<Pid, Vec<Pid>>, + + /// PIDs corresponding to processes that have no parents. + pub orphan_pids: Vec<Pid>, +} + +impl ProcessData { + fn ingest(&mut self, list_of_processes: Vec<ProcessHarvest>) { + // 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<Pid> = 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<processes::ProcessHarvest>, + pub process_data: ProcessData, pub disk_harvest: Vec<disks::DiskHarvest>, 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<processes::ProcessHarvest>) { - self.process_harvest = list_of_processes; + fn eat_proc(&mut self, list_of_processes: Vec<ProcessHarvest>) { + 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 9dcc377c..979bdadb 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<Vec<Battery>>, 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 3c12fd73..b7980ddb 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<crate::app::Filter>, diff --git a/src/app/data_harvester/processes/linux.rs b/src/app/data_harvester/processes/linux.rs index 081af802..1e91f93a 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<Pid, PrevProcDetails>, 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<Vec<ProcessHarvest>> { // 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 f08e17ca..0c401b0a 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<std::collections::HashMap<i32, f64>> { @@ -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<Vec<ProcessHarvest>> { let mut process_vector: Vec<ProcessHarvest> = 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<i32> = 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 283080b3..40662d7a 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<Pid>, // Remember, parent_pid 0 is root... + + /// The parent PID of the process. Remember, parent_pid 0 is root. + pub parent_pid: Option<Pid>, + + /// 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, + + /// 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, + + /// 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: 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, - pub name: String, - pub command: String, - pub read_bytes_per_sec: u64, - pub write_bytes_per_sec: u64, - pub total_read_bytes: u64, - pub total_write_bytes: u64, - pub process_state: String, - pub process_state_char: char, - - /// This is the *effective* user ID. - #[cfg(target_family = "unix")] - pub uid: Option<libc::uid_t>, +} + +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 8fadc590..75f70bec 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<libc::uid_t, String>, + pub uid_user_mapping: FxHashMap<libc::uid_t, String>, } impl UserTable { diff --git a/src/app/data_harvester/processes/windows.rs b/src/app/data_harvester/processes/windows.rs index af9ead5f..a3564874 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 0f18c901..46c40022 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<Query>; -} +/// 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<Query> { + fn process_string_to_filter(query: &mut VecDeque<String>) -> Result<Query> { + let lhs = process_or(query)?; + let mut list_of_ors = vec![lhs]; -impl ProcessQuery for ProcWidgetState { - fn parse_query(&self) -> Result<Query> { - fn process_string_to_filter(query: &mut VecDeque<String>) -> Result<Query> { - let lhs = process_or(query)?; - let mut list_of_ors = vec![lhs]; - - while query.front().is_some() { - list_of_ors.push(process_or(query)?); - } - - Ok(Query { query: list_of_ors }) + while query.front().is_some() { + list_of_ors.push(process_or(query)?); } - fn process_or(query: &mut VecDeque<String>) -> Result<Or> { - let mut lhs = process_and(query)?; - let mut rhs: Option<Box<And>> = 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<String>) -> Result<Or> { + let mut lhs = process_and(query)?; + let mut rhs: Option<Box<And>> = None; - 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; - } - } else if COMPARISON_LIST.contains(&queue_top.to_lowercase().as_str()) { - return Err(QueryError(Cow::Borrowed("Comparison not valid here"))); - } else { - break; - } - } + 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)?)); - Ok(Or { lhs, rhs }) - } - - fn process_and(query: &mut VecDeque<String>) -> Result<And> { - let mut lhs = process_prefix(query, false)?; - let mut rhs: Option<Box<Prefix>> = 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 { - 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<String>, inside_quotation: bool) -> Result<Prefix> { - 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(); - - while let Some(in_paren_query_top) = query.front() { - if in_paren_query_top != ")" { - list_of_ors.push_back(process_or(query)?); - } else { - break; - } - } - - // 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 { + 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: list_of_ors.pop_front().map(Box::new), - compare_prefix: None, + or: Some(Box::new(Or { lhs, rhs })), regex_prefix: None, + compare_prefix: None, }, rhs: 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, - }); + }; + 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; + } + } - 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, - }); + Ok(Or { lhs, rhs }) + } + + fn process_and(query: &mut VecDeque<String>) -> Result<And> { + let mut lhs = process_prefix(query, false)?; + let mut rhs: Option<Box<Prefix>> = 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 { + 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<String>, inside_quotation: bool) -> Result<Prefix> { + 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 { - return Err(QueryError("Missing closing parentheses".into())); + 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(); + + while let Some(in_paren_query_top) = query.front() { + if in_paren_query_top != ")" { + list_of_ors.push_back(process_or(query)?); + } else { + break; + } + } + + // 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, + }, + 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, + }); + + 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... + } 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())); - } + 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::<PrefixType>()?; - 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::<PrefixType>()?; + 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: 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: [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? - 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<QueryComparison> = None; - let mut value: Option<f64> = None; + let mut condition: Option<QueryComparison> = None; + let mut value: Option<f64> = None; - if content == "=" { - condition = Some(QueryComparison::Equal); - if let Some(queue_next) = query.pop_front() { - value = queue_next.parse::<f64>().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::<f64>().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::<f64>().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 { - condition = Some(if content == ">" { - QueryComparison::Greater - } else { - QueryComparison::Less - }); - value = queue_next.parse::<f64>().ok(); + QueryComparison::LessOrEqual + }); + if let Some(queue_next_next) = query.pop_front() { + value = queue_next_next.parse::<f64>().ok(); + } else { + 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::<f64>().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! + 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; + 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(); - } - _ => {} + 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(); - - 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 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, - )?; - - Ok(process_filter) + Err(QueryError("Invalid query".into())) } + + let mut split_query = VecDeque::new(); + + 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 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 8cbb8ae1..3dff4644 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<usize> { - if change == 0 { - return None; - } - - let csp: Result<i64, _> = self.current_scroll_position.try_into(); - if let Ok(csp) = csp { - let proposed: Result<usize, _> = (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<u16>, + pub calculated_column_widths: Vec<u16>, } #[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<u16>, - pub calculated_column_widths: Vec<u16>, -} - -/// 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<u16>, - // pub max_soft_width: Option<f64>, -} - -pub struct ProcColumn { - pub ordered_columns: Vec<ProcessSorting>, - /// The y location of headers. Since they're all aligned, it's just one value. - pub column_header_y_loc: Option<u16>, - /// The x start and end bounds for each header. - pub column_header_x_locs: Option<Vec<(u16, u16)>>, - pub column_mapping: HashMap<ProcessSorting, ColumnInfo>, - 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<bool> { - 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<bool> { - 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<bool> { - 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<bool> { - 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<String> { - 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<u64, ProcWidgetState>, - pub force_update: Option<u64>, - pub force_update_all: bool, + pub widget_states: HashMap<u64, ProcWidget>, } impl ProcState { - pub fn init(widget_states: HashMap<u64, ProcWidgetState>) -> Self { - ProcState { - widget_states, - force_update: None, - force_update_all: false, - } + pub fn init(widget_states: HashMap<u64, ProcWidget>) -> 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<Instant>, - // pub draw_max_range_cache: f64, - // pub draw_labels_cache: Vec<String>, - // 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<Instant>, - // unit_type: DataUnitTypes, - // scale_type: AxisScaling, - ) -> Self { + pub fn init(current_display_time: u64, autohide_timer: Option<Instant>) -> 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<Instant>, - 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<Instant>) -> 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<ConfigCategory>, -} - -#[derive(Default)] -pub struct ConfigCategory { - pub category_name: &'static str, - pub options_list: Vec<ConfigOption>, -} - -pub struct ConfigOption { - pub set_function: Box<dyn Fn() -> 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<usize>, - 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 00000000..c6ce3b46 --- /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<f32>, + }, + + /// 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<f32>) -> 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<f32>, + ) -> 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<I>(name: I, alt: Option<I>) -> Self + where + I: Into<Cow<'static, str>>, + { + 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<Cow<'static, str>> 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<String> for CellContent { + fn from(s: String) -> Self { + CellContent::Simple(s.into()) + } +} + +pub struct TableComponentColumn<H: TableComponentHeader> { + /// 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<H: TableComponentHeader> TableComponentColumn<H> { + 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<f32>) -> 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<Range<u16>>, + + /// 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<SortOrder>, + + /// 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<SortOrder>, + ) -> 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<usize> { + 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<usize> { + 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<H: TableComponentHeader = CellContent> { + pub current_scroll_position: usize, + pub scroll_bar: usize, + pub scroll_direction: ScrollDirection, + pub table_state: TableState, + pub columns: Vec<TableComponentColumn<H>>, + pub sort_state: SortState, +} + +impl<H: TableComponentHeader> TableComponentState<H> { + pub fn new(columns: Vec<TableComponentColumn<H>>) -> 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<usize> { + if change == 0 { + return None; + } + + let csp: Result<i64, _> = self.current_scroll_position.try_into(); + if let Ok(csp) = csp { + let proposed: Result<usize, _> = (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<usize>, + 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<u16>) { + 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::<Vec<_>>(), + 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<usize>, 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 00000000..9d3a54ba --- /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 00000000..f49ab40a --- /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<Pid> }, + 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<ProcWidgetColumn>, + 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<Pid>, data_collection: &DataCollection, + search_query: &Option<Query>, + ) -> 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::<FxHashMap<_, _>>(); + + 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<Query>, + ) -> TableData { + let mut id_pid_map: FxHashMap<String, ProcessHarvest>; + 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::<Vec<_>>() + } else { + filtered_iter.collect::<Vec<_>>() + }; + + 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<String>, 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::<Pid>() + { + 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 6a1f0aae..e3a86315 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 2070327a..e69c5053 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<Point>, - pub network_data_tx: Vec<Point>, - pub disk_data: Vec<Vec<String>>, - pub temp_sensor_data: Vec<Vec<String>>, - pub single_process_data: HashMap<Pid, ConvertedProcessData>, // Contains single process data, key is PID - pub finalized_process_data_map: HashMap<u64, Vec<ConvertedProcessData>>, // What's actually displayed, key is the widget ID. - pub stringified_process_data_map: HashMap<u64, Vec<(Vec<(String, Option<String>)>, 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<Point>, // TODO: Switch this and all data points over to a better data structure... - pub swap_data: Vec<Point>, - pub load_avg_data: [f32; 3], - pub cpu_data: Vec<ConvertedCpuData>, - pub battery_data: Vec<ConvertedBatteryData>, -} - #[derive(Debug)] pub enum ColourScheme { Default, @@ -94,20 +68,20 @@ pub struct Painter { height: u16, width: u16, styled_help_text: Vec<Spans<'static>>, - 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<Constraint>, col_constraints: Vec<Vec<Constraint>>, col_row_constraints: Vec<Vec<Vec<Constraint>>>, layout_constraints: Vec<Vec<Vec<Vec<Constraint>>>>, derived_widget_draw_locs: Vec<Vec<Vec<Vec<Rect>>>>, 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<Self> { // 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 8b137891..f5fc31e7 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<TextTableTitle<'a>>, + + /// 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<Spans<'_>> { + 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<B: Backend, H: TableComponentHeader>( + &self, f: &mut Frame<'_, B>, draw_loc: Rect, state: &mut TableComponentState<H>, + 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::<Vec<_>>(); + + 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::<Vec<_>>()), + ), + 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<H>], 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<Style>) -> Text<'_> { + let (main_text, alt_text) = match content { + CellContent::Simple(s) => (s, None), + CellContent::HasAlt { + alt: short, + main: long, + } => (long, Some(short)), + }; + + let mut text = { + let graphemes: Vec<&str> = + UnicodeSegmentation::graphemes(main_text.as_ref(), true).collect(); + if graphemes.len() > width && width > 0 { + if let Some(s) = alt_text { + // If an alternative exists, use that. + Text::raw(s.as_ref()) + } else { + // Truncate with ellipsis + let first_n = graphemes[..(width - 1)].concat(); + Text::raw(concat_string!(first_n, "…")) + } + } else { + Text::raw(main_text.as_ref()) + } + }; + + if let Some(row_style) = row_style { + text.patch_style(row_style); + } + + text +} + +fn truncate_suffixed_text<'a>( + content: &'a CellContent, suffix: &str, width: usize, row_style: Option<Style>, +) -> Text<'a> { + let (main_text, alt_text) = match content { + CellContent::Simple(s) => (s, None), + CellContent::HasAlt { + alt: short, + main: long, + } => (long, Some(short)), + }; + + let mut text = { + let suffixed = concat_string!(main_text, suffix); + let graphemes: Vec<&str> = + UnicodeSegmentation::graphemes(suffixed.as_str(), true).collect(); + if graphemes.len() > width && width > 1 { + if let Some(alt) = alt_text { + // If an alternative exists, use that + arrow. + Text::raw(concat_string!(alt, suffix)) + } else { + // Truncate with ellipsis + arrow. + let first_n = graphemes[..(width - 2)].concat(); + Text::raw(concat_string!(first_n, "…", suffix)) + } + } else { + Text::raw(suffixed) + } + }; + + if let Some(row_style) = row_style { + text.patch_style(row_style); + } + + text +} + +/// Gets the starting position of a table. +pub fn get_start_position( + num_rows: usize, scroll_direction: &app::ScrollDirection, scroll_position_bar: &mut usize, + currently_selected_position: usize, is_force_redraw: bool, +) -> usize { + if is_force_redraw { + *scroll_position_bar = 0; + } + + match scroll_direction { + app::ScrollDirection::Down => { + if currently_selected_position < *scroll_position_bar + num_rows { + // If, using previous_scrolled_position, we can see the element + // (so within that and + num_rows) just reuse the current previously scrolled position + *scroll_position_bar + } else if currently_selected_position >= num_rows { + // Else if the current position past the last element visible in the list, omit + // until we can see that element + *scroll_position_bar = currently_selected_position - num_rows + 1; + *scroll_position_bar + } else { + // Else, if it is not past the last element visible, do not omit anything + 0 + } + } + app::ScrollDirection::Up => { + if currently_selected_position <= *scroll_position_bar { + // If it's past the first element, then show from that element downwards + *scroll_position_bar = currently_selected_position; + } else if currently_selected_position >= *scroll_position_bar + num_rows { + *scroll_position_bar = currently_selected_position - num_rows + 1; + } + // Else, don't change what our start position is from whatever it is set to! + *scroll_position_bar + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_get_start_position() { + use crate::app::ScrollDirection::{self, Down, Up}; + + #[track_caller] + fn test_get( + bar: usize, rows: usize, direction: ScrollDirection, selected: usize, force: bool, + expected_posn: usize, expected_bar: usize, + ) { + let mut bar = bar; + assert_eq!( + get_start_position(rows, &direction, &mut bar, selected, force), + expected_posn, + "returned start position should match" + ); + assert_eq!(bar, expected_bar, "bar positions should match"); + } + + // Scrolling down from start + test_get(0, 10, Down, 0, false, 0, 0); + + // Simple scrolling down + test_get(0, 10, Down, 1, false, 0, 0); + + // Scrolling down from the middle high up + test_get(0, 10, Down, 4, false, 0, 0); + + // Scrolling down into boundary + test_get(0, 10, Down, 10, false, 1, 1); + test_get(0, 10, Down, 11, false, 2, 2); + + // Scrolling down from the with non-zero bar + test_get(5, 10, Down, 14, false, 5, 5); + + // Force redraw scrolling down (e.g. resize) + test_get(5, 15, Down, 14, true, 0, 0); + + // Test jumping down + test_get(1, 10, Down, 19, true, 10, 10); + + // Scrolling up from bottom + test_get(10, 10, Up, 19, false, 10, 10); + + // Simple scrolling up + test_get(10, 10, Up, 18, false, 10, 10); + + // Scrolling up from the middle + test_get(10, 10, Up, 10, false, 10, 10); + + // Scrolling up into boundary + test_get(10, 10, Up, 9, false, 9, 9); + + // Force redraw scrolling up (e.g. resize) + test_get(5, 10, Up, 14, true, 5, 5); + + // Test jumping up + test_get(10, 10, Up, 0, false, 0, 0); + } +} diff --git a/src/canvas/components/time_chart.rs b/src/canvas/components/time_chart.rs index 5bb0ff15..7b791d1d 100644 --- a/src/canvas/components/time_chart.rs +++ b/src/canvas/components/time_chart.rs @@ -1,7 +1,4 @@ -use std::{ - borrow::Cow, - cmp::{max, Ordering}, -}; +use std::{borrow::Cow, cmp::max}; use tui::{ buffer::Buffer, layout::{Constraint, Rect}, @@ -15,6 +12,8 @@ use tui::{ }; use unicode_width::UnicodeWidthStr; +use crate::utils::gen_util::partial_ordering; + /// An X or Y axis for the chart widget #[derive(Debug, Clone)] pub struct Axis<'a> { @@ -556,16 +555,11 @@ impl<'a> Widget for TimeChart<'a> { } } -fn bin_cmp(a: &f64, b: &f64) -> Ordering { - // TODO: Switch to `total_cmp` on 1.62 - a.partial_cmp(b).unwrap_or(Ordering::Equal) -} - /// Returns the start index and potential interpolation index given the start time and the dataset. fn get_start(dataset: &Dataset<'_>, start_bound: f64) -> (usize, Option<usize>) { match dataset .data - .binary_search_by(|(x, _y)| bin_cmp(x, &start_bound)) + .binary_search_by(|(x, _y)| partial_ordering(x, &start_bound)) { Ok(index) => (index, None), Err(index) => (index, index.checked_sub(1)), @@ -576,7 +570,7 @@ fn get_start(dataset: &Dataset<'_>, start_bound: f64) -> (usize, Option<usize>) fn get_end(dataset: &Dataset<'_>, end_bound: f64) -> (usize, Option<usize>) { match dataset .data - .binary_search_by(|(x, _y)| bin_cmp(x, &end_bound)) + .binary_search_by(|(x, _y)| partial_ordering(x, &end_bound)) { // In the success case, this means we found an index. Add one since we want to include this index and we // expect to use the returned index as part of a (m..n) range. @@ -621,20 +615,20 @@ mod test { assert_eq!(interpolate_point(&data[0], &data[1], -3.0), 8.0); } + #[test] + fn time_chart_empty_dataset() { + let data = []; + let dataset = Dataset::default().data(&data); + + assert_eq!(get_start(&dataset, -100.0), (0, None)); + assert_eq!(get_start(&dataset, -3.0), (0, None)); + + assert_eq!(get_end(&dataset, 0.0), (0, None)); + assert_eq!(get_end(&dataset, 100.0), (0, None)); + } + #[test] fn time_chart_test_data_trimming() { - // Quick test on a completely empty dataset... - { - let data = []; - let dataset = Dataset::default().data(&data); - - assert_eq!(get_start(&dataset, -100.0), (0, None)); - assert_eq!(get_start(&dataset, -3.0), (0, None)); - - assert_eq!(get_end(&dataset, 0.0), (0, None)); - assert_eq!(get_end(&dataset, 100.0), (0, None)); - } - let data = [ (-3.0, 8.0), (-2.5, 15.0), diff --git a/src/canvas/components/time_graph.rs b/src/canvas/components/time_graph.rs index ab5cec2e..7448ff5c 100644 --- a/src/canvas/components/time_graph.rs +++ b/src/canvas/components/time_graph.rs @@ -25,7 +25,6 @@ pub struct GraphData<'a> { pub name: Option<Cow<'a, str>>, } -#[derive(Default)] pub struct TimeGraph<'a> { /// Whether to use a dot marker over the default braille markers. pub use_dot: bool, @@ -144,14 +143,14 @@ impl<'a> TimeGraph<'a> { .collect() }; + let block = Block::default() + .title(self.generate_title(draw_loc)) + .borders(Borders::ALL) + .border_style(self.border_style); + f.render_widget( TimeChart::new(data) - .block( - Block::default() - .title(self.generate_title(draw_loc)) - .borders(Borders::ALL) - .border_style(self.border_style), - ) + .block(block) .x_axis(x_axis) .y_axis(y_axis) .hidden_legend_constraints( diff --git a/src/canvas/dialogs/dd_dialog.rs b/src/canvas/dialogs/dd_dialog.rs index f7edbedd..1ee4282b 100644 --- a/src/canvas/dialogs/dd_dialog.rs +++ b/src/canvas/dialogs/dd_dialog.rs @@ -9,7 +9,7 @@ use tui::{ }; use crate::{ - app::{App, KillSignal}, + app::{widgets::ProcWidgetMode, App, KillSignal}, canvas::Painter, }; @@ -29,7 +29,13 @@ impl Painter { if let Some(first_pid) = to_kill_processes.1.first() { return Some(Text::from(vec![ Spans::from(""), - if app_state.is_grouped(app_state.current_widget.widget_id) { + if app_state + .proc_state + .widget_states + .get(&app_state.current_widget.widget_id) + .map(|p| matches!(p.mode, ProcWidgetMode::Grouped)) + .unwrap_or(false) + { if to_kill_processes.1.len() != 1 { Spans::from(format!( "Kill {} processes with the name \"{}\"? Press ENTER to confirm.", diff --git a/src/canvas/drawing_utils.rs b/src/canvas/drawing_utils.rs index f7b81281..7ef1b85d 100644 --- a/src/canvas/drawing_utils.rs +++ b/src/canvas/drawing_utils.rs @@ -1,124 +1,10 @@ use tui::layout::Rect; -use crate::app; -use std::{ - cmp::{max, min}, - time::Instant, -}; - -/// Return a (hard)-width vector for column widths. -/// -/// * `total_width` is the, well, total width available. **NOTE:** This function automatically -/// takes away 2 from the width as part of the left/right -/// bounds. -/// * `hard_widths` is inflexible column widths. Use a `None` to represent a soft width. -/// * `soft_widths_min` is the lower limit for a soft width. Use `None` if a hard width goes there. -/// * `soft_widths_max` is the upper limit for a soft width, in percentage of the total width. Use -/// `None` if a hard width goes there. -/// * `soft_widths_desired` is the desired soft width. Use `None` if a hard width goes there. -/// * `left_to_right` is a boolean whether to go from left to right if true, or right to left if -/// false. -/// -/// **NOTE:** This function ASSUMES THAT ALL PASSED SLICES ARE OF THE SAME SIZE. -/// -/// **NOTE:** The returned vector may not be the same size as the slices, this is because including -/// 0-constraints breaks tui-rs. -pub fn get_column_widths( - total_width: u16, hard_widths: &[Option<u16>], soft_widths_min: &[Option<u16>], - soft_widths_max: &[Option<f64>], soft_widths_desired: &[Option<u16>], left_to_right: bool, -) -> Vec<u16> { - debug_assert!( - hard_widths.len() == soft_widths_min.len(), - "hard width length != soft width min length!" - ); - debug_assert!( - soft_widths_min.len() == soft_widths_max.len(), - "soft width min length != soft width max length!" - ); - debug_assert!( - soft_widths_max.len() == soft_widths_desired.len(), - "soft width max length != soft width desired length!" - ); - - if total_width > 2 { - let initial_width = total_width - 2; - let mut total_width_left = initial_width; - let mut column_widths: Vec<u16> = vec![0; hard_widths.len()]; - let range: Vec<usize> = if left_to_right { - (0..hard_widths.len()).collect() - } else { - (0..hard_widths.len()).rev().collect() - }; - - for itx in &range { - if let Some(Some(hard_width)) = hard_widths.get(*itx) { - // Hard width... - let space_taken = min(*hard_width, total_width_left); - - // TODO [COLUMN MOVEMENT]: Remove this - if *hard_width > space_taken { - break; - } - - column_widths[*itx] = space_taken; - total_width_left -= space_taken; - total_width_left = total_width_left.saturating_sub(1); - } else if let ( - Some(Some(soft_width_max)), - Some(Some(soft_width_min)), - Some(Some(soft_width_desired)), - ) = ( - soft_widths_max.get(*itx), - soft_widths_min.get(*itx), - soft_widths_desired.get(*itx), - ) { - // Soft width... - let soft_limit = max( - if soft_width_max.is_sign_negative() { - *soft_width_desired - } else { - (*soft_width_max * initial_width as f64).ceil() as u16 - }, - *soft_width_min, - ); - let space_taken = min(min(soft_limit, *soft_width_desired), total_width_left); - - // TODO [COLUMN MOVEMENT]: Remove this - if *soft_width_min > space_taken { - break; - } - - column_widths[*itx] = space_taken; - total_width_left -= space_taken; - total_width_left = total_width_left.saturating_sub(1); - } - } - - while let Some(0) = column_widths.last() { - column_widths.pop(); - } - - if !column_widths.is_empty() { - // Redistribute remaining. - let amount_per_slot = total_width_left / column_widths.len() as u16; - total_width_left %= column_widths.len() as u16; - for (index, width) in column_widths.iter_mut().enumerate() { - if index < total_width_left.into() { - *width += amount_per_slot + 1; - } else { - *width += amount_per_slot; - } - } - } - - column_widths - } else { - vec![] - } -} +use crate::app::CursorDirection; +use std::{cmp::min, time::Instant}; pub fn get_search_start_position( - num_columns: usize, cursor_direction: &app::CursorDirection, cursor_bar: &mut usize, + num_columns: usize, cursor_direction: &CursorDirection, cursor_bar: &mut usize, current_cursor_position: usize, is_force_redraw: bool, ) -> usize { if is_force_redraw { @@ -126,24 +12,24 @@ pub fn get_search_start_position( } match cursor_direction { - app::CursorDirection::Right => { + CursorDirection::Right => { if current_cursor_position < *cursor_bar + num_columns { // If, using previous_scrolled_position, we can see the element - // (so within that and + num_rows) just reuse the current previously scrolled position + // (so within that and + num_rows) just reuse the current previously scrolled position. *cursor_bar } else if current_cursor_position >= num_columns { // Else if the current position past the last element visible in the list, omit - // until we can see that element + // until we can see that element. *cursor_bar = current_cursor_position - num_columns; *cursor_bar } else { - // Else, if it is not past the last element visible, do not omit anything + // Else, if it is not past the last element visible, do not omit anything. 0 } } - app::CursorDirection::Left => { + CursorDirection::Left => { if current_cursor_position <= *cursor_bar { - // If it's past the first element, then show from that element downwards + // If it's past the first element, then show from that element downwards. *cursor_bar = current_cursor_position; } else if current_cursor_position >= *cursor_bar + num_columns { *cursor_bar = current_cursor_position - num_columns; @@ -154,46 +40,9 @@ pub fn get_search_start_position( } } -pub fn get_start_position( - num_rows: usize, scroll_direction: &app::ScrollDirection, scroll_position_bar: &mut usize, - currently_selected_position: usize, is_force_redraw: bool, -) -> usize { - if is_force_redraw { - *scroll_position_bar = 0; - } - - match scroll_direction { - app::ScrollDirection::Down => { - if currently_selected_position < *scroll_position_bar + num_rows { - // If, using previous_scrolled_position, we can see the element - // (so within that and + num_rows) just reuse the current previously scrolled position - *scroll_position_bar - } else if currently_selected_position >= num_rows { - // Else if the current position past the last element visible in the list, omit - // until we can see that element - *scroll_position_bar = currently_selected_position - num_rows; - *scroll_position_bar - } else { - // Else, if it is not past the last element visible, do not omit anything - 0 - } - } - app::ScrollDirection::Up => { - if currently_selected_position <= *scroll_position_bar { - // If it's past the first element, then show from that element downwards - *scroll_position_bar = currently_selected_position; - } else if currently_selected_position >= *scroll_position_bar + num_rows { - *scroll_position_bar = currently_selected_position - num_rows; - } - // Else, don't change what our start position is from whatever it is set to! - *scroll_position_bar - } - } -} - /// Calculate how many bars are to be drawn within basic mode's components. pub fn calculate_basic_use_bars(use_percentage: f64, num_bars_available: usize) -> usize { - std::cmp::min( + min( (num_bars_available as f64 * use_percentage / 100.0).round() as usize, num_bars_available, ) @@ -224,62 +73,6 @@ mod test { use super::*; - #[test] - fn test_get_start_position() { - use crate::app::ScrollDirection::{self, Down, Up}; - - fn test( - bar: usize, num: usize, direction: ScrollDirection, selected: usize, force: bool, - expected_posn: usize, expected_bar: usize, - ) { - let mut bar = bar; - assert_eq!( - get_start_position(num, &direction, &mut bar, selected, force), - expected_posn - ); - assert_eq!(bar, expected_bar); - } - - // Scrolling down from start - test(0, 10, Down, 0, false, 0, 0); - - // Simple scrolling down - test(0, 10, Down, 1, false, 0, 0); - - // Scrolling down from the middle high up - test(0, 10, Down, 5, false, 0, 0); - - // Scrolling down into boundary - test(0, 10, Down, 11, false, 1, 1); - - // Scrolling down from the with non-zero bar - test(5, 10, Down, 15, false, 5, 5); - - // Force redraw scrolling down (e.g. resize) - test(5, 15, Down, 15, true, 0, 0); - - // Test jumping down - test(1, 10, Down, 20, true, 10, 10); - - // Scrolling up from bottom - test(10, 10, Up, 20, false, 10, 10); - - // Simple scrolling up - test(10, 10, Up, 19, false, 10, 10); - - // Scrolling up from the middle - test(10, 10, Up, 10, false, 10, 10); - - // Scrolling up into boundary - test(10, 10, Up, 9, false, 9, 9); - - // Force redraw scrolling up (e.g. resize) - test(5, 10, Up, 15, true, 5, 5); - - // Test jumping up - test(10, 10, Up, 0, false, 0, 0); - } - #[test] fn test_calculate_basic_use_bars() { // Testing various breakpoints and edge cases. @@ -327,49 +120,4 @@ mod test { )); assert!(over_timer.is_none()); } - - #[test] - fn test_zero_width() { - assert_eq!( - get_column_widths( - 0, - &[Some(1), None, None], - &[None, Some(1), Some(2)], - &[None, Some(0.125), Some(0.5)], - &[None, Some(10), Some(10)], - true - ), - vec![], - ); - } - - #[test] - fn test_two_width() { - assert_eq!( - get_column_widths( - 2, - &[Some(1), None, None], - &[None, Some(1), Some(2)], - &[None, Some(0.125), Some(0.5)], - &[None, Some(10), Some(10)], - true - ), - vec![], - ); - } - - #[test] - fn test_non_zero_width() { - assert_eq!( - get_column_widths( - 16, - &[Some(1), None, None], - &[None, Some(1), Some(2)], - &[None, Some(0.125), Some(0.5)], - &[None, Some(10), Some(10)], - true - ), - vec![2, 2, 7], - ); - } } diff --git a/src/canvas/widgets/battery_display.rs b/src/canvas/widgets/battery_display.rs index e020cf2c..48ed9c46 100644 --- a/src/canvas/widgets/battery_display.rs +++ b/src/canvas/widgets/battery_display.rs @@ -69,7 +69,7 @@ impl Painter { }; let battery_names = app_state - .canvas_data + .converted_data .battery_data .iter() .map(|battery| &battery.battery_name) @@ -106,7 +106,7 @@ impl Painter { .split(draw_loc)[0]; if let Some(battery_details) = app_state - .canvas_data + .converted_data .battery_data .get(battery_widget_state.currently_selected_battery_index) { diff --git a/src/canvas/widgets/cpu_basic.rs b/src/canvas/widgets/cpu_basic.rs index aef2187b..27a05586 100644 --- a/src/canvas/widgets/cpu_basic.rs +++ b/src/canvas/widgets/cpu_basic.rs @@ -20,8 +20,8 @@ impl Painter { &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64, ) { // Skip the first element, it's the "all" element - if app_state.canvas_data.cpu_data.len() > 1 { - let cpu_data: &[ConvertedCpuData] = &app_state.canvas_data.cpu_data[1..]; + if app_state.converted_data.cpu_data.len() > 1 { + let cpu_data: &[ConvertedCpuData] = &app_state.converted_data.cpu_data[1..]; // This is a bit complicated, but basically, we want to draw SOME number // of columns to draw all CPUs. Ideally, as well, we want to not have diff --git a/src/canvas/widgets/cpu_graph.rs b/src/canvas/widgets/cpu_graph.rs index fcb7daa2..f482b75b 100644 --- a/src/canvas/widgets/cpu_graph.rs +++ b/src/canvas/widgets/cpu_graph.rs @@ -1,38 +1,34 @@ -use std::borrow::Cow; +use std::{borrow::Cow, iter}; use crate::{ - app::{layout_manager::WidgetDirection, App}, + app::{layout_manager::WidgetDirection, App, CellContent, CpuWidgetState}, canvas::{ - components::{GraphData, TimeGraph}, - drawing_utils::{get_column_widths, get_start_position, should_hide_x_label}, + components::{GraphData, TextTable, TimeGraph}, + drawing_utils::should_hide_x_label, Painter, }, - constants::*, - data_conversion::ConvertedCpuData, + data_conversion::{ConvertedCpuData, TableData, TableRow}, }; use concat_string::concat_string; +use itertools::Either; use tui::{ backend::Backend, layout::{Constraint, Direction, Layout, Rect}, terminal::Frame, - text::Text, - widgets::{Block, Borders, Row, Table}, }; -const CPU_LEGEND_HEADER: [&str; 2] = ["CPU", "Use%"]; const AVG_POSITION: usize = 1; const ALL_POSITION: usize = 0; -static CPU_LEGEND_HEADER_LENS: [usize; 2] = - [CPU_LEGEND_HEADER[0].len(), CPU_LEGEND_HEADER[1].len()]; - impl Painter { pub fn draw_cpu<B: Backend>( &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64, ) { - if draw_loc.width as f64 * 0.15 <= 6.0 { + let legend_width = (draw_loc.width as f64 * 0.15) as u16; + + if legend_width < 6 { // Skip drawing legend if app_state.current_widget.widget_id == (widget_id + 1) { if app_state.app_config_fields.left_legend { @@ -55,18 +51,25 @@ impl Painter { } } } else { + let graph_width = draw_loc.width - legend_width; let (graph_index, legend_index, constraints) = if app_state.app_config_fields.left_legend { ( 1, 0, - [Constraint::Percentage(15), Constraint::Percentage(85)], + [ + Constraint::Length(legend_width), + Constraint::Length(graph_width), + ], ) } else { ( 0, 1, - [Constraint::Percentage(85), Constraint::Percentage(15)], + [ + Constraint::Length(graph_width), + Constraint::Length(legend_width), + ], ) }; @@ -115,6 +118,56 @@ impl Painter { } } + fn generate_points<'a>( + &self, cpu_widget_state: &CpuWidgetState, cpu_data: &'a [ConvertedCpuData], + show_avg_cpu: bool, + ) -> Vec<GraphData<'a>> { + let show_avg_offset = if show_avg_cpu { AVG_POSITION } else { 0 }; + + let current_scroll_position = cpu_widget_state.table_state.current_scroll_position; + if current_scroll_position == ALL_POSITION { + // This case ensures the other cases cannot have the position be equal to 0. + cpu_data + .iter() + .enumerate() + .rev() + .map(|(itx, cpu)| { + let style = if show_avg_cpu && itx == AVG_POSITION { + self.colours.avg_colour_style + } else if itx == ALL_POSITION { + self.colours.all_colour_style + } else { + let offset_position = itx - 1; // Because of the all position + self.colours.cpu_colour_styles[(offset_position - show_avg_offset) + % self.colours.cpu_colour_styles.len()] + }; + + GraphData { + points: &cpu.cpu_data[..], + style, + name: None, + } + }) + .collect::<Vec<_>>() + } else if let Some(cpu) = cpu_data.get(current_scroll_position) { + let style = if show_avg_cpu && current_scroll_position == AVG_POSITION { + self.colours.avg_colour_style + } else { + let offset_position = current_scroll_position - 1; // Because of the all position + self.colours.cpu_colour_styles + [(offset_position - show_avg_offset) % self.colours.cpu_colour_styles.len()] + }; + + vec![GraphData { + points: &cpu.cpu_data[..], + style, + name: None, + }] + } else { + vec![] + } + } + fn draw_cpu_graph<B: Backend>( &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64, ) { @@ -122,7 +175,7 @@ impl Painter { const Y_LABELS: [Cow<'static, str>; 2] = [Cow::Borrowed(" 0%"), Cow::Borrowed("100%")]; if let Some(cpu_widget_state) = app_state.cpu_state.widget_states.get_mut(&widget_id) { - let cpu_data = &app_state.canvas_data.cpu_data; + let cpu_data = &app_state.converted_data.cpu_data; let border_style = self.get_border_style(widget_id, app_state.current_widget.widget_id); let x_bounds = [0, cpu_widget_state.current_display_time]; let hide_x_labels = should_hide_x_label( @@ -131,56 +184,16 @@ impl Painter { &mut cpu_widget_state.autohide_timer, draw_loc, ); - let show_avg_cpu = app_state.app_config_fields.show_average_cpu; - let show_avg_offset = if show_avg_cpu { AVG_POSITION } else { 0 }; - let points = { - let current_scroll_position = cpu_widget_state.scroll_state.current_scroll_position; - if current_scroll_position == ALL_POSITION { - // This case ensures the other cases cannot have the position be equal to 0. - cpu_data - .iter() - .enumerate() - .rev() - .map(|(itx, cpu)| { - let style = if show_avg_cpu && itx == AVG_POSITION { - self.colours.avg_colour_style - } else if itx == ALL_POSITION { - self.colours.all_colour_style - } else { - let offset_position = itx - 1; // Because of the all position - self.colours.cpu_colour_styles[(offset_position - show_avg_offset) - % self.colours.cpu_colour_styles.len()] - }; - GraphData { - points: &cpu.cpu_data[..], - style, - name: None, - } - }) - .collect::<Vec<_>>() - } else if let Some(cpu) = cpu_data.get(current_scroll_position) { - let style = if show_avg_cpu && current_scroll_position == AVG_POSITION { - self.colours.avg_colour_style - } else { - let offset_position = current_scroll_position - 1; // Because of the all position - self.colours.cpu_colour_styles[(offset_position - show_avg_offset) - % self.colours.cpu_colour_styles.len()] - }; - - vec![GraphData { - points: &cpu.cpu_data[..], - style, - name: None, - }] - } else { - vec![] - } - }; + let points = self.generate_points( + cpu_widget_state, + cpu_data, + app_state.app_config_fields.show_average_cpu, + ); // TODO: Maybe hide load avg if too long? Or maybe the CPU part. let title = if cfg!(target_family = "unix") { - let load_avg = app_state.canvas_data.load_avg_data; + let load_avg = app_state.converted_data.load_avg_data; let load_avg_str = format!( "─ {:.2} {:.2} {:.2} ", load_avg[0], load_avg[1], load_avg[2] @@ -214,148 +227,86 @@ impl Painter { let recalculate_column_widths = app_state.should_get_widget_bounds(); if let Some(cpu_widget_state) = app_state.cpu_state.widget_states.get_mut(&(widget_id - 1)) { + // TODO: This line (and the one above, see caller) is pretty dumb but I guess needed. cpu_widget_state.is_legend_hidden = false; - let cpu_data: &mut [ConvertedCpuData] = &mut app_state.canvas_data.cpu_data; - let cpu_table_state = &mut cpu_widget_state.scroll_state.table_state; - let is_on_widget = widget_id == app_state.current_widget.widget_id; - let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT { - 0 - } else { - app_state.app_config_fields.table_gap - }; - let start_position = get_start_position( - usize::from( - (draw_loc.height + (1 - table_gap)).saturating_sub(self.table_height_offset), - ), - &cpu_widget_state.scroll_state.scroll_direction, - &mut cpu_widget_state.scroll_state.scroll_bar, - cpu_widget_state.scroll_state.current_scroll_position, - app_state.is_force_redraw, - ); - cpu_table_state.select(Some( - cpu_widget_state - .scroll_state - .current_scroll_position - .saturating_sub(start_position), - )); - let sliced_cpu_data = &cpu_data[start_position..]; - - let offset_scroll_index = cpu_widget_state - .scroll_state - .current_scroll_position - .saturating_sub(start_position); let show_avg_cpu = app_state.app_config_fields.show_average_cpu; - - // Calculate widths - if recalculate_column_widths { - cpu_widget_state.table_width_state.desired_column_widths = vec![6, 4]; - cpu_widget_state.table_width_state.calculated_column_widths = get_column_widths( - draw_loc.width, - &[None, None], - &(CPU_LEGEND_HEADER_LENS - .iter() - .map(|width| Some(*width as u16)) - .collect::<Vec<_>>()), - &[Some(0.5), Some(0.5)], - &(cpu_widget_state - .table_width_state - .desired_column_widths - .iter() - .map(|width| Some(*width)) - .collect::<Vec<_>>()), - false, - ); - } - - let dcw = &cpu_widget_state.table_width_state.desired_column_widths; - let ccw = &cpu_widget_state.table_width_state.calculated_column_widths; - let cpu_rows = sliced_cpu_data.iter().enumerate().map(|(itx, cpu)| { - let mut truncated_name = - if let (Some(desired_column_width), Some(calculated_column_width)) = - (dcw.get(0), ccw.get(0)) - { - if *desired_column_width > *calculated_column_width { - Text::raw(&cpu.short_cpu_name) - } else { - Text::raw(&cpu.cpu_name) - } - } else { - Text::raw(&cpu.cpu_name) - }; - - let is_first_column_hidden = if let Some(calculated_column_width) = ccw.get(0) { - *calculated_column_width == 0 + let cpu_data = { + let col_widths = vec![1, 3]; // TODO: Should change this to take const generics (usize) and an array. + let colour_iter = if show_avg_cpu { + Either::Left( + iter::once(&self.colours.all_colour_style) + .chain(iter::once(&self.colours.avg_colour_style)) + .chain(self.colours.cpu_colour_styles.iter().cycle()), + ) } else { - false + Either::Right( + iter::once(&self.colours.all_colour_style) + .chain(self.colours.cpu_colour_styles.iter().cycle()), + ) }; - let truncated_legend = if is_first_column_hidden && cpu.legend_value.is_empty() { - // For the case where we only have room for one column, display "All" in the normally blank area. - Text::raw("All") - } else { - Text::raw(&cpu.legend_value) - }; - - if !is_first_column_hidden - && itx == offset_scroll_index - && itx + start_position == ALL_POSITION - { - truncated_name.patch_style(self.colours.currently_selected_text_style); - Row::new(vec![truncated_name, truncated_legend]) - } else { - let cpu_string_row = vec![truncated_name, truncated_legend]; - - Row::new(cpu_string_row).style(if itx == offset_scroll_index { - self.colours.currently_selected_text_style - } else if itx + start_position == ALL_POSITION { - self.colours.all_colour_style - } else if show_avg_cpu { - if itx + start_position == AVG_POSITION { - self.colours.avg_colour_style - } else { - self.colours.cpu_colour_styles[(itx + start_position - - AVG_POSITION - - 1) - % self.colours.cpu_colour_styles.len()] - } + let data = { + let iter = app_state.converted_data.cpu_data.iter().zip(colour_iter); + const CPU_WIDTH_CHECK: u16 = 10; // This is hard-coded, it's terrible. + if draw_loc.width < CPU_WIDTH_CHECK { + Either::Left(iter.map(|(cpu, style)| { + let row = vec![ + CellContent::Simple("".into()), + CellContent::Simple(if cpu.legend_value.is_empty() { + cpu.cpu_name.clone().into() + } else { + cpu.legend_value.clone().into() + }), + ]; + TableRow::Styled(row, *style) + })) } else { - self.colours.cpu_colour_styles[(itx + start_position - ALL_POSITION - 1) - % self.colours.cpu_colour_styles.len()] - }) + Either::Right(iter.map(|(cpu, style)| { + let row = vec![ + CellContent::HasAlt { + alt: cpu.short_cpu_name.clone().into(), + main: cpu.cpu_name.clone().into(), + }, + CellContent::Simple(cpu.legend_value.clone().into()), + ]; + TableRow::Styled(row, *style) + })) + } } - }); + .collect(); - // Note we don't set highlight_style, as it should always be shown for this widget. - let border_and_title_style = if is_on_widget { + TableData { data, col_widths } + }; + + let is_on_widget = widget_id == app_state.current_widget.widget_id; + let border_style = if is_on_widget { self.colours.highlighted_border_style } else { self.colours.border_style }; - // Draw - f.render_stateful_widget( - Table::new(cpu_rows) - .block( - Block::default() - .borders(Borders::ALL) - .border_style(border_and_title_style), - ) - .header( - Row::new(CPU_LEGEND_HEADER.to_vec()) - .style(self.colours.table_header_style) - .bottom_margin(table_gap), - ) - .widths( - &(cpu_widget_state - .table_width_state - .calculated_column_widths - .iter() - .map(|calculated_width| Constraint::Length(*calculated_width as u16)) - .collect::<Vec<_>>()), - ), + TextTable { + table_gap: app_state.app_config_fields.table_gap, + is_force_redraw: app_state.is_force_redraw, + recalculate_column_widths, + header_style: self.colours.table_header_style, + border_style, + highlighted_text_style: self.colours.currently_selected_text_style, // We always highlight the selected CPU entry... not sure if I like this though. + title: None, + is_on_widget, + draw_border: true, + show_table_scroll_position: app_state.app_config_fields.show_table_scroll_position, + title_style: self.colours.widget_title_style, + text_style: self.colours.text_style, + left_to_right: false, + } + .draw_text_table( + f, draw_loc, - cpu_table_state, + &mut cpu_widget_state.table_state, + &cpu_data, + None, ); } } diff --git a/src/canvas/widgets/disk_table.rs b/src/canvas/widgets/disk_table.rs index 252ec70b..1e52cc5f 100644 --- a/src/canvas/widgets/disk_table.rs +++ b/src/canvas/widgets/disk_table.rs @@ -1,31 +1,12 @@ -use once_cell::sync::Lazy; -use tui::{ - backend::Backend, - layout::{Constraint, Direction, Layout, Rect}, - terminal::Frame, - text::Span, - text::{Spans, Text}, - widgets::{Block, Borders, Row, Table}, -}; +use tui::{backend::Backend, layout::Rect, terminal::Frame}; use crate::{ app, canvas::{ - drawing_utils::{get_column_widths, get_start_position}, + components::{TextTable, TextTableTitle}, Painter, }, - constants::*, }; -use unicode_segmentation::UnicodeSegmentation; - -const DISK_HEADERS: [&str; 7] = ["Disk", "Mount", "Used", "Free", "Total", "R/s", "W/s"]; - -static DISK_HEADERS_LENS: Lazy<Vec<u16>> = Lazy::new(|| { - DISK_HEADERS - .iter() - .map(|entry| entry.len() as u16) - .collect::<Vec<_>>() -}); impl Painter { pub fn draw_disk_table<B: Backend>( @@ -34,120 +15,8 @@ impl Painter { ) { let recalculate_column_widths = app_state.should_get_widget_bounds(); if let Some(disk_widget_state) = app_state.disk_state.widget_states.get_mut(&widget_id) { - let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT { - 0 - } else { - app_state.app_config_fields.table_gap - }; - let start_position = get_start_position( - usize::from( - (draw_loc.height + (1 - table_gap)).saturating_sub(self.table_height_offset), - ), - &disk_widget_state.scroll_state.scroll_direction, - &mut disk_widget_state.scroll_state.scroll_bar, - disk_widget_state.scroll_state.current_scroll_position, - app_state.is_force_redraw, - ); let is_on_widget = app_state.current_widget.widget_id == widget_id; - let disk_table_state = &mut disk_widget_state.scroll_state.table_state; - disk_table_state.select(Some( - disk_widget_state - .scroll_state - .current_scroll_position - .saturating_sub(start_position), - )); - let sliced_vec = &app_state.canvas_data.disk_data[start_position..]; - - // Calculate widths - let hard_widths = [None, None, Some(4), Some(6), Some(6), Some(7), Some(7)]; - if recalculate_column_widths { - disk_widget_state.table_width_state.desired_column_widths = { - let mut column_widths = DISK_HEADERS_LENS.clone(); - for row in sliced_vec { - for (col, entry) in row.iter().enumerate() { - if entry.len() as u16 > column_widths[col] { - column_widths[col] = entry.len() as u16; - } - } - } - column_widths - }; - disk_widget_state.table_width_state.desired_column_widths = disk_widget_state - .table_width_state - .desired_column_widths - .iter() - .zip(&hard_widths) - .map(|(current, hard)| { - if let Some(hard) = hard { - if *hard > *current { - *hard - } else { - *current - } - } else { - *current - } - }) - .collect::<Vec<_>>(); - - disk_widget_state.table_width_state.calculated_column_widths = get_column_widths( - draw_loc.width, - &hard_widths, - &(DISK_HEADERS_LENS - .iter() - .map(|w| Some(*w)) - .collect::<Vec<_>>()), - &[Some(0.2), Some(0.2), None, None, None, None, None], - &(disk_widget_state - .table_width_state - .desired_column_widths - .iter() - .map(|w| Some(*w)) - .collect::<Vec<_>>()), - true, - ); - } - - let dcw = &disk_widget_state.table_width_state.desired_column_widths; - let ccw = &disk_widget_state.table_width_state.calculated_column_widths; - let disk_rows = - sliced_vec.iter().map(|disk_row| { - let truncated_data = disk_row.iter().zip(&hard_widths).enumerate().map( - |(itx, (entry, width))| { - if width.is_none() { - if let (Some(desired_col_width), Some(calculated_col_width)) = - (dcw.get(itx), ccw.get(itx)) - { - if *desired_col_width > *calculated_col_width - && *calculated_col_width > 0 - { - let calculated_col_width: usize = - (*calculated_col_width).into(); - - let graphemes = - UnicodeSegmentation::graphemes(entry.as_str(), true) - .collect::<Vec<&str>>(); - - if graphemes.len() > calculated_col_width - && calculated_col_width > 1 - { - // Truncate with ellipsis - let first_n = - graphemes[..(calculated_col_width - 1)].concat(); - return Text::raw(format!("{}…", first_n)); - } - } - } - } - - Text::raw(entry) - }, - ); - - Row::new(truncated_data) - }); - - let (border_style, highlight_style) = if is_on_widget { + let (border_style, highlighted_text_style) = if is_on_widget { ( self.colours.highlighted_border_style, self.colours.currently_selected_text_style, @@ -155,117 +24,31 @@ impl Painter { } else { (self.colours.border_style, self.colours.text_style) }; - - let title_base = if app_state.app_config_fields.show_table_scroll_position { - let title_string = format!( - " Disk ({} of {}) ", - disk_widget_state - .scroll_state - .current_scroll_position - .saturating_add(1), - app_state.canvas_data.disk_data.len() - ); - - if title_string.len() <= draw_loc.width.into() { - title_string - } else { - " Disk ".to_string() - } - } else { - " Disk ".to_string() - }; - - let title = if app_state.is_expanded { - const ESCAPE_ENDING: &str = "── Esc to go back "; - - let (chosen_title_base, expanded_title_base) = { - let temp_title_base = format!("{}{}", title_base, ESCAPE_ENDING); - - if temp_title_base.len() > draw_loc.width.into() { - ( - " Disk ".to_string(), - format!("{}{}", " Disk ", ESCAPE_ENDING), - ) - } else { - (title_base, temp_title_base) - } - }; - - Spans::from(vec![ - Span::styled(chosen_title_base, self.colours.widget_title_style), - Span::styled( - format!( - "─{}─ Esc to go back ", - "─".repeat( - usize::from(draw_loc.width).saturating_sub( - UnicodeSegmentation::graphemes( - expanded_title_base.as_str(), - true - ) - .count() - + 2 - ) - ) - ), - border_style, - ), - ]) - } else { - Spans::from(Span::styled(title_base, self.colours.widget_title_style)) - }; - - let disk_block = if draw_border { - Block::default() - .title(title) - .borders(Borders::ALL) - .border_style(border_style) - } else if is_on_widget { - Block::default() - .borders(SIDE_BORDERS) - .border_style(self.colours.highlighted_border_style) - } else { - Block::default().borders(Borders::NONE) - }; - - let margined_draw_loc = Layout::default() - .constraints([Constraint::Percentage(100)]) - .horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 }) - .direction(Direction::Horizontal) - .split(draw_loc)[0]; - - // Draw! - f.render_stateful_widget( - Table::new(disk_rows) - .block(disk_block) - .header( - Row::new(DISK_HEADERS.to_vec()) - .style(self.colours.table_header_style) - .bottom_margin(table_gap), - ) - .highlight_style(highlight_style) - .style(self.colours.text_style) - .widths( - &(disk_widget_state - .table_width_state - .calculated_column_widths - .iter() - .map(|calculated_width| Constraint::Length(*calculated_width)) - .collect::<Vec<_>>()), - ), - margined_draw_loc, - disk_table_state, - ); - - if app_state.should_get_widget_bounds() { - // Update draw loc in widget map - if let Some(widget) = app_state.widget_map.get_mut(&widget_id) { - widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y)); - widget.bottom_right_corner = Some(( - margined_draw_loc.x + margined_draw_loc.width, - margined_draw_loc.y + margined_draw_loc.height, - )); - } + TextTable { + table_gap: app_state.app_config_fields.table_gap, + is_force_redraw: app_state.is_force_redraw, + recalculate_column_widths, + header_style: self.colours.table_header_style, + border_style, + highlighted_text_style, + title: Some(TextTableTitle { + title: " Disks ".into(), + is_expanded: app_state.is_expanded, + }), + is_on_widget, + draw_border, + show_table_scroll_position: app_state.app_config_fields.show_table_scroll_position, + title_style: self.colours.widget_title_style, + text_style: self.colours.text_style, + left_to_right: true, } + .draw_text_table( + f, + draw_loc, + &mut disk_widget_state.table_state, + &app_state.converted_data.disk_data, + app_state.widget_map.get_mut(&widget_id), + ); } } } diff --git a/src/canvas/widgets/mem_basic.rs b/src/canvas/widgets/mem_basic.rs index 16d79572..ae2b7f1f 100644 --- a/src/canvas/widgets/mem_basic.rs +++ b/src/canvas/widgets/mem_basic.rs @@ -17,8 +17,8 @@ impl Painter { pub fn draw_basic_memory<B: Backend>( &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64, ) { - let mem_data: &[(f64, f64)] = &app_state.canvas_data.mem_data; - let swap_data: &[(f64, f64)] = &app_state.canvas_data.swap_data; + let mem_data: &[(f64, f64)] = &app_state.converted_data.mem_data; + let swap_data: &[(f64, f64)] = &app_state.converted_data.swap_data; let margined_loc = Layout::default() .constraints([Constraint::Percentage(100)]) @@ -48,14 +48,14 @@ impl Painter { const EMPTY_MEMORY_FRAC_STRING: &str = "0.0B/0.0B"; let trimmed_memory_frac = - if let Some((_label_percent, label_frac)) = &app_state.canvas_data.mem_labels { + if let Some((_label_percent, label_frac)) = &app_state.converted_data.mem_labels { label_frac.trim() } else { EMPTY_MEMORY_FRAC_STRING }; let trimmed_swap_frac = - if let Some((_label_percent, label_frac)) = &app_state.canvas_data.swap_labels { + if let Some((_label_percent, label_frac)) = &app_state.converted_data.swap_labels { label_frac.trim() } else { EMPTY_MEMORY_FRAC_STRING diff --git a/src/canvas/widgets/mem_graph.rs b/src/canvas/widgets/mem_graph.rs index e5d0fbcc..442044aa 100644 --- a/src/canvas/widgets/mem_graph.rs +++ b/src/canvas/widgets/mem_graph.rs @@ -33,18 +33,18 @@ impl Painter { ); let points = { let mut points = Vec::with_capacity(2); - if let Some((label_percent, label_frac)) = &app_state.canvas_data.mem_labels { + if let Some((label_percent, label_frac)) = &app_state.converted_data.mem_labels { let mem_label = format!("RAM:{}{}", label_percent, label_frac); points.push(GraphData { - points: &app_state.canvas_data.mem_data, + points: &app_state.converted_data.mem_data, style: self.colours.ram_style, name: Some(mem_label.into()), }); } - if let Some((label_percent, label_frac)) = &app_state.canvas_data.swap_labels { + if let Some((label_percent, label_frac)) = &app_state.converted_data.swap_labels { let swap_label = format!("SWP:{}{}", label_percent, label_frac); points.push(GraphData { - points: &app_state.canvas_data.swap_data, + points: &app_state.converted_data.swap_data, style: self.colours.swap_style, name: Some(swap_label.into()), }); diff --git a/src/canvas/widgets/network_basic.rs b/src/canvas/widgets/network_basic.rs index 8a5c5e50..b58da96f 100644 --- a/src/canvas/widgets/network_basic.rs +++ b/src/canvas/widgets/network_basic.rs @@ -38,10 +38,10 @@ impl Painter { ); } - let rx_label = format!("RX: {}", &app_state.canvas_data.rx_display); - let tx_label = format!("TX: {}", &app_state.canvas_data.tx_display); - let total_rx_label = format!("Total RX: {}", &app_state.canvas_data.total_rx_display); - let total_tx_label = format!("Total TX: {}", &app_state.canvas_data.total_tx_display); + let rx_label = format!("RX: {}", &app_state.converted_data.rx_display); + let tx_label = format!("TX: {}", &app_state.converted_data.tx_display); + let total_rx_label = format!("Total RX: {}", &app_state.converted_data.total_rx_display); + let total_tx_label = format!("Total TX: {}", &app_state.converted_data.total_tx_display); let net_text = vec![ Spans::from(Span::styled(rx_label, self.colours.rx_style)), diff --git a/src/canvas/widgets/network_graph.rs b/src/canvas/widgets/network_graph.rs index 2c3256f0..f96be42f 100644 --- a/src/canvas/widgets/network_graph.rs +++ b/src/canvas/widgets/network_graph.rs @@ -1,14 +1,10 @@ -use once_cell::sync::Lazy; -use std::cmp::max; - use crate::{ app::{App, AxisScaling}, canvas::{ components::{GraphData, TimeGraph}, - drawing_utils::{get_column_widths, should_hide_x_label}, + drawing_utils::should_hide_x_label, Painter, Point, }, - constants::*, units::data_units::DataUnit, utils::gen_util::*, }; @@ -21,26 +17,18 @@ use tui::{ widgets::{Block, Borders, Row, Table}, }; -const NETWORK_HEADERS: [&str; 4] = ["RX", "TX", "Total RX", "Total TX"]; - -static NETWORK_HEADERS_LENS: Lazy<Vec<u16>> = Lazy::new(|| { - NETWORK_HEADERS - .iter() - .map(|entry| entry.len() as u16) - .collect::<Vec<_>>() -}); - impl Painter { pub fn draw_network<B: Backend>( &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64, ) { if app_state.app_config_fields.use_old_network_legend { + const LEGEND_HEIGHT: u16 = 4; let network_chunk = Layout::default() .direction(Direction::Vertical) .margin(0) .constraints([ - Constraint::Length(max(draw_loc.height as i64 - 5, 0) as u16), - Constraint::Length(5), + Constraint::Length(draw_loc.height.saturating_sub(LEGEND_HEIGHT)), + Constraint::Length(LEGEND_HEIGHT), ]) .split(draw_loc); @@ -67,8 +55,8 @@ impl Painter { hide_legend: bool, ) { if let Some(network_widget_state) = app_state.net_state.widget_states.get_mut(&widget_id) { - let network_data_rx: &[(f64, f64)] = &app_state.canvas_data.network_data_rx; - let network_data_tx: &[(f64, f64)] = &app_state.canvas_data.network_data_tx; + let network_data_rx: &[(f64, f64)] = &app_state.converted_data.network_data_rx; + let network_data_tx: &[(f64, f64)] = &app_state.converted_data.network_data_tx; let time_start = -(network_widget_state.current_display_time as f64); let border_style = self.get_border_style(widget_id, app_state.current_widget.widget_id); let x_bounds = [0, network_widget_state.current_display_time]; @@ -115,18 +103,18 @@ impl Painter { GraphData { points: network_data_rx, style: self.colours.rx_style, - name: Some(format!("RX: {:7}", app_state.canvas_data.rx_display).into()), + name: Some(format!("RX: {:7}", app_state.converted_data.rx_display).into()), }, GraphData { points: network_data_tx, style: self.colours.tx_style, - name: Some(format!("TX: {:7}", app_state.canvas_data.tx_display).into()), + name: Some(format!("TX: {:7}", app_state.converted_data.tx_display).into()), }, GraphData { points: &[], style: self.colours.total_rx_style, name: Some( - format!("Total RX: {:7}", app_state.canvas_data.total_rx_display) + format!("Total RX: {:7}", app_state.converted_data.total_rx_display) .into(), ), }, @@ -134,7 +122,7 @@ impl Painter { points: &[], style: self.colours.total_tx_style, name: Some( - format!("Total TX: {:7}", app_state.canvas_data.total_tx_display) + format!("Total TX: {:7}", app_state.converted_data.total_tx_display) .into(), ), }, @@ -144,12 +132,12 @@ impl Painter { GraphData { points: network_data_rx, style: self.colours.rx_style, - name: Some((&app_state.canvas_data.rx_display).into()), + name: Some((&app_state.converted_data.rx_display).into()), }, GraphData { points: network_data_tx, style: self.colours.tx_style, - name: Some((&app_state.canvas_data.tx_display).into()), + name: Some((&app_state.converted_data.tx_display).into()), }, ] }; @@ -174,52 +162,25 @@ impl Painter { fn draw_network_labels<B: Backend>( &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64, ) { - let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT { - 0 - } else { - app_state.app_config_fields.table_gap - }; + const NETWORK_HEADERS: [&str; 4] = ["RX", "TX", "Total RX", "Total TX"]; - let rx_display = &app_state.canvas_data.rx_display; - let tx_display = &app_state.canvas_data.tx_display; - let total_rx_display = &app_state.canvas_data.total_rx_display; - let total_tx_display = &app_state.canvas_data.total_tx_display; + let rx_display = &app_state.converted_data.rx_display; + let tx_display = &app_state.converted_data.tx_display; + let total_rx_display = &app_state.converted_data.total_rx_display; + let total_tx_display = &app_state.converted_data.total_tx_display; // Gross but I need it to work... - let total_network = vec![vec![ - Text::raw(rx_display), - Text::raw(tx_display), - Text::raw(total_rx_display), - Text::raw(total_tx_display), - ]]; - let mapped_network = total_network - .into_iter() - .map(|val| Row::new(val).style(self.colours.text_style)); - - // Calculate widths - let intrinsic_widths = get_column_widths( - draw_loc.width, - &[None, None, None, None], - &(NETWORK_HEADERS_LENS - .iter() - .map(|s| Some(*s)) - .collect::<Vec<_>>()), - &[Some(0.25); 4], - &(NETWORK_HEADERS_LENS - .iter() - .map(|s| Some(*s)) - .collect::<Vec<_>>()), - true, - ); + let total_network = vec![Row::new(vec![ + Text::styled(rx_display, self.colours.rx_style), + Text::styled(tx_display, self.colours.tx_style), + Text::styled(total_rx_display, self.colours.total_rx_style), + Text::styled(total_tx_display, self.colours.total_tx_style), + ])]; // Draw f.render_widget( - Table::new(mapped_network) - .header( - Row::new(NETWORK_HEADERS.to_vec()) - .style(self.colours.table_header_style) - .bottom_margin(table_gap), - ) + Table::new(total_network) + .header(Row::new(NETWORK_HEADERS.to_vec()).style(self.colours.table_header_style)) .block(Block::default().borders(Borders::ALL).border_style( if app_state.current_widget.widget_id == widget_id { self.colours.highlighted_border_style @@ -229,9 +190,9 @@ impl Painter { )) .style(self.colours.text_style) .widths( - &(intrinsic_widths - .iter() - .map(|calculated_width| Constraint::Length(*calculated_width)) + &((std::iter::repeat(draw_loc.width.saturating_sub(2) / 4)) + .take(4) + .map(Constraint::Length) .collect::<Vec<_>>()), ), draw_loc, @@ -295,7 +256,7 @@ fn get_max_entry( (None, Some(filtered_tx)) => { match filtered_tx .iter() - .max_by(|(_, data_a), (_, data_b)| get_ordering(data_a, data_b, false)) + .max_by(|(_, data_a), (_, data_b)| partial_ordering(data_a, data_b)) { Some((best_time, max_val)) => { if *max_val == 0.0 { @@ -316,7 +277,7 @@ fn get_max_entry( (Some(filtered_rx), None) => { match filtered_rx .iter() - .max_by(|(_, data_a), (_, data_b)| get_ordering(data_a, data_b, false)) + .max_by(|(_, data_a), (_, data_b)| partial_ordering(data_a, data_b)) { Some((best_time, max_val)) => { if *max_val == 0.0 { @@ -338,7 +299,7 @@ fn get_max_entry( match filtered_rx .iter() .chain(filtered_tx) - .max_by(|(_, data_a), (_, data_b)| get_ordering(data_a, data_b, false)) + .max_by(|(_, data_a), (_, data_b)| partial_ordering(data_a, data_b)) { Some((best_time, max_val)) => { if *max_val == 0.0 { diff --git a/src/canvas/widgets/process_table.rs b/src/canvas/widgets/process_table.rs index 4e7ca9f9..b38d09cb 100644 --- a/src/canvas/widgets/process_table.rs +++ b/src/canvas/widgets/process_table.rs @@ -1,106 +1,40 @@ use crate::{ app::App, canvas::{ - drawing_utils::{get_column_widths, get_search_start_position, get_start_position}, + components::{TextTable, TextTableTitle}, + drawing_utils::get_search_start_position, Painter, }, constants::*, + data_conversion::{TableData, TableRow}, }; use tui::{ backend::Backend, layout::{Alignment, Constraint, Direction, Layout, Rect}, terminal::Frame, - text::{Span, Spans, Text}, - widgets::{Block, Borders, Paragraph, Row, Table}, + text::{Span, Spans}, + widgets::{Block, Borders, Paragraph}, }; use unicode_segmentation::{GraphemeIndices, UnicodeSegmentation}; use unicode_width::UnicodeWidthStr; -const PROCESS_HEADERS_HARD_WIDTH_NO_GROUP: &[Option<u16>] = &[ - Some(7), - None, - Some(8), - Some(8), - Some(8), - Some(8), - Some(7), - Some(8), - #[cfg(target_family = "unix")] - None, - None, -]; -const PROCESS_HEADERS_HARD_WIDTH_GROUPED: &[Option<u16>] = &[ - Some(7), - None, - Some(8), - Some(8), - Some(8), - Some(8), - Some(7), - Some(8), -]; - -const PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_COMMAND: &[Option<f64>] = - &[None, Some(0.7), None, None, None, None, None, None]; -const PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_ELSE: &[Option<f64>] = - &[None, Some(0.3), None, None, None, None, None, None]; - -const PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_COMMAND: &[Option<f64>] = &[ - None, - Some(0.7), - None, - None, - None, - None, - None, - None, - #[cfg(target_family = "unix")] - Some(0.05), - Some(0.2), -]; -const PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_TREE: &[Option<f64>] = &[ - None, - Some(0.5), - None, - None, - None, - None, - None, - None, - #[cfg(target_family = "unix")] - Some(0.05), - Some(0.2), -]; -const PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_ELSE: &[Option<f64>] = &[ - None, - Some(0.3), - None, - None, - None, - None, - None, - None, - #[cfg(target_family = "unix")] - Some(0.05), - Some(0.2), -]; +const SORT_MENU_WIDTH: u16 = 7; impl Painter { /// Draws and handles all process-related drawing. Use this. /// - `widget_id` here represents the widget ID of the process widget itself! - pub fn draw_process_features<B: Backend>( + pub fn draw_process_widget<B: Backend>( &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool, widget_id: u64, ) { - if let Some(process_widget_state) = app_state.proc_state.widget_states.get(&widget_id) { + if let Some(proc_widget_state) = app_state.proc_state.widget_states.get(&widget_id) { let search_height = if draw_border { 5 } else { 3 }; - let is_sort_open = process_widget_state.is_sort_open; - let header_len = process_widget_state.columns.longest_header_len; + let is_sort_open = proc_widget_state.is_sort_open; let mut proc_draw_loc = draw_loc; - if process_widget_state.is_search_enabled() { + if proc_widget_state.is_search_enabled() { let processes_chunk = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Min(0), Constraint::Length(search_height)]) @@ -119,25 +53,26 @@ impl Painter { if is_sort_open { let processes_chunk = Layout::default() .direction(Direction::Horizontal) - .constraints([Constraint::Length(header_len + 4), Constraint::Min(0)]) + .constraints([Constraint::Length(SORT_MENU_WIDTH + 4), Constraint::Min(0)]) .split(proc_draw_loc); proc_draw_loc = processes_chunk[1]; - self.draw_process_sort( - f, - app_state, - processes_chunk[0], - draw_border, - widget_id + 2, - ); + self.draw_sort_table(f, app_state, processes_chunk[0], draw_border, widget_id + 2); } self.draw_processes_table(f, app_state, proc_draw_loc, draw_border, widget_id); } + + if let Some(proc_widget_state) = app_state.proc_state.widget_states.get_mut(&widget_id) { + // Reset redraw marker. + if proc_widget_state.force_rerender { + proc_widget_state.force_rerender = false; + } + } } /// Draws the process sort box. - /// - `widget_id` represents the widget ID of the process widget itself. + /// - `widget_id` represents the widget ID of the process widget itself.an /// /// This should not be directly called. fn draw_processes_table<B: Backend>( @@ -147,19 +82,10 @@ impl Painter { let should_get_widget_bounds = app_state.should_get_widget_bounds(); if let Some(proc_widget_state) = app_state.proc_state.widget_states.get_mut(&widget_id) { let recalculate_column_widths = - should_get_widget_bounds || proc_widget_state.requires_redraw; - if proc_widget_state.requires_redraw { - proc_widget_state.requires_redraw = false; - } + should_get_widget_bounds || proc_widget_state.force_rerender; let is_on_widget = widget_id == app_state.current_widget.widget_id; - let margined_draw_loc = Layout::default() - .constraints([Constraint::Percentage(100)]) - .horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 }) - .direction(Direction::Horizontal) - .split(draw_loc)[0]; - - let (border_style, highlight_style) = if is_on_widget { + let (border_style, highlighted_text_style) = if is_on_widget { ( self.colours.highlighted_border_style, self.colours.currently_selected_text_style, @@ -168,357 +94,39 @@ impl Painter { (self.colours.border_style, self.colours.text_style) }; - let title_base = if app_state.app_config_fields.show_table_scroll_position { - if let Some(finalized_process_data) = app_state - .canvas_data - .finalized_process_data_map - .get(&widget_id) - { - let title = format!( - " Processes ({} of {}) ", - proc_widget_state - .scroll_state - .current_scroll_position - .saturating_add(1), - finalized_process_data.len() - ); - - if title.len() <= draw_loc.width.into() { - title - } else { - " Processes ".to_string() - } - } else { - " Processes ".to_string() - } - } else { - " Processes ".to_string() - }; - - let title = if app_state.is_expanded - && !proc_widget_state - .process_search_state - .search_state - .is_enabled - && !proc_widget_state.is_sort_open - { - const ESCAPE_ENDING: &str = "── Esc to go back "; - - let (chosen_title_base, expanded_title_base) = { - let temp_title_base = format!("{}{}", title_base, ESCAPE_ENDING); - - if temp_title_base.len() > draw_loc.width.into() { - ( - " Processes ".to_string(), - format!("{}{}", " Processes ", ESCAPE_ENDING), - ) - } else { - (title_base, temp_title_base) - } - }; - - Spans::from(vec![ - Span::styled(chosen_title_base, self.colours.widget_title_style), - Span::styled( - format!( - "─{}─ Esc to go back ", - "─".repeat( - usize::from(draw_loc.width).saturating_sub( - UnicodeSegmentation::graphemes( - expanded_title_base.as_str(), - true - ) - .count() - + 2 - ) - ) - ), - border_style, - ), - ]) - } else { - Spans::from(Span::styled(title_base, self.colours.widget_title_style)) - }; - - let process_block = if draw_border { - Block::default() - .title(title) - .borders(Borders::ALL) - .border_style(border_style) - } else if is_on_widget { - Block::default() - .borders(SIDE_BORDERS) - .border_style(self.colours.highlighted_border_style) - } else { - Block::default().borders(Borders::NONE) - }; - - if let Some(process_data) = &app_state - .canvas_data - .stringified_process_data_map - .get(&widget_id) - { - let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT { - 0 - } else { - app_state.app_config_fields.table_gap - }; - let position = get_start_position( - usize::from( - (draw_loc.height + (1 - table_gap)) - .saturating_sub(self.table_height_offset), - ), - &proc_widget_state.scroll_state.scroll_direction, - &mut proc_widget_state.scroll_state.scroll_bar, - proc_widget_state.scroll_state.current_scroll_position, - app_state.is_force_redraw, - ); - - // Sanity check - let start_position = if position >= process_data.len() { - process_data.len().saturating_sub(1) - } else { - position - }; - - let sliced_vec = &process_data[start_position..]; - let processed_sliced_vec = sliced_vec.iter().map(|(data, disabled)| { - ( - data.iter() - .map(|(entry, _alternative)| entry) - .collect::<Vec<_>>(), - disabled, - ) - }); - - let proc_table_state = &mut proc_widget_state.scroll_state.table_state; - proc_table_state.select(Some( - proc_widget_state - .scroll_state - .current_scroll_position - .saturating_sub(start_position), - )); - - // Draw! - let process_headers = proc_widget_state.columns.get_column_headers( - &proc_widget_state.process_sorting_type, - proc_widget_state.is_process_sort_descending, - ); - - // Calculate widths - // FIXME: See if we can move this into the recalculate block? I want to move column widths into the column widths - let hard_widths = if proc_widget_state.is_grouped { - PROCESS_HEADERS_HARD_WIDTH_GROUPED - } else { - PROCESS_HEADERS_HARD_WIDTH_NO_GROUP - }; - - if recalculate_column_widths { - let mut column_widths = process_headers - .iter() - .map(|entry| UnicodeWidthStr::width(entry.as_str()) as u16) - .collect::<Vec<_>>(); - - let soft_widths_min = column_widths - .iter() - .map(|width| Some(*width)) - .collect::<Vec<_>>(); - - proc_widget_state.table_width_state.desired_column_widths = { - for (row, _disabled) in processed_sliced_vec.clone() { - for (col, entry) in row.iter().enumerate() { - if let Some(col_width) = column_widths.get_mut(col) { - let grapheme_len = UnicodeWidthStr::width(entry.as_str()); - if grapheme_len as u16 > *col_width { - *col_width = grapheme_len as u16; - } - } - } - } - column_widths - }; - - proc_widget_state.table_width_state.desired_column_widths = proc_widget_state - .table_width_state - .desired_column_widths - .iter() - .zip(hard_widths) - .map(|(current, hard)| { - if let Some(hard) = hard { - if *hard > *current { - *hard - } else { - *current - } - } else { - *current - } - }) - .collect::<Vec<_>>(); - - let soft_widths_max = if proc_widget_state.is_grouped { - // Note grouped trees are not a thing. - - if proc_widget_state.is_using_command { - PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_COMMAND - } else { - PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_ELSE - } - } else if proc_widget_state.is_using_command { - PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_COMMAND - } else if proc_widget_state.is_tree_mode { - PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_TREE - } else { - PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_ELSE - }; - - proc_widget_state.table_width_state.calculated_column_widths = - get_column_widths( - draw_loc.width, - hard_widths, - &soft_widths_min, - soft_widths_max, - &(proc_widget_state - .table_width_state - .desired_column_widths - .iter() - .map(|width| Some(*width)) - .collect::<Vec<_>>()), - true, - ); - - // debug!( - // "DCW: {:?}", - // proc_widget_state.table_width_state.desired_column_widths - // ); - // debug!( - // "CCW: {:?}", - // proc_widget_state.table_width_state.calculated_column_widths - // ); - } - - let dcw = &proc_widget_state.table_width_state.desired_column_widths; - let ccw = &proc_widget_state.table_width_state.calculated_column_widths; - - let process_rows = sliced_vec.iter().map(|(data, disabled)| { - let truncated_data = data.iter().zip(hard_widths).enumerate().map( - |(itx, ((entry, alternative), width))| { - if let (Some(desired_col_width), Some(calculated_col_width)) = - (dcw.get(itx), ccw.get(itx)) - { - if width.is_none() { - if *desired_col_width > *calculated_col_width - && *calculated_col_width > 0 - { - let calculated_col_width: usize = - (*calculated_col_width).into(); - - let graphemes = - UnicodeSegmentation::graphemes(entry.as_str(), true) - .collect::<Vec<&str>>(); - - if let Some(alternative) = alternative { - Text::raw(alternative) - } else if graphemes.len() > calculated_col_width - && calculated_col_width > 1 - { - // Truncate with ellipsis - let first_n = - graphemes[..(calculated_col_width - 1)].concat(); - Text::raw(format!("{}…", first_n)) - } else { - Text::raw(entry) - } - } else { - Text::raw(entry) - } - } else { - Text::raw(entry) - } - } else { - Text::raw(entry) - } - }, - ); - - if *disabled { - Row::new(truncated_data).style(self.colours.disabled_text_style) - } else { - Row::new(truncated_data) - } - }); - - f.render_stateful_widget( - Table::new(process_rows) - .header( - Row::new(process_headers) - .style(self.colours.table_header_style) - .bottom_margin(table_gap), - ) - .block(process_block) - .highlight_style(highlight_style) - .style(self.colours.text_style) - .widths( - &(proc_widget_state - .table_width_state - .calculated_column_widths - .iter() - .map(|calculated_width| Constraint::Length(*calculated_width)) - .collect::<Vec<_>>()), - ), - margined_draw_loc, - proc_table_state, - ); - } else { - f.render_widget(process_block, margined_draw_loc); - } - - // Check if we need to update columnar bounds... - if recalculate_column_widths - || proc_widget_state.columns.column_header_x_locs.is_none() - || proc_widget_state.columns.column_header_y_loc.is_none() - { - // y location is just the y location of the widget + border size (1 normally, 0 in basic) - proc_widget_state.columns.column_header_y_loc = - Some(draw_loc.y + if draw_border { 1 } else { 0 }); - - // x location is determined using the x locations of the widget; just offset from the left bound - // as appropriate, and use the right bound as limiter. - - let mut current_x_left = draw_loc.x + 1; - let max_x_right = draw_loc.x + draw_loc.width - 1; - - let mut x_locs = vec![]; - - for width in proc_widget_state - .table_width_state - .calculated_column_widths - .iter() - { - let right_bound = current_x_left + width; - - if right_bound < max_x_right { - x_locs.push((current_x_left, right_bound)); - current_x_left = right_bound + 1; - } else { - x_locs.push((current_x_left, max_x_right)); - break; - } - } - - proc_widget_state.columns.column_header_x_locs = Some(x_locs); - } - - if app_state.should_get_widget_bounds() { - // Update draw loc in widget map - if let Some(widget) = app_state.widget_map.get_mut(&widget_id) { - widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y)); - widget.bottom_right_corner = Some(( - margined_draw_loc.x + margined_draw_loc.width, - margined_draw_loc.y + margined_draw_loc.height, - )); + // TODO: [Refactor] This is an ugly hack to add the disabled style... + // this could be solved by storing style locally to the widget. + for row in &mut proc_widget_state.table_data.data { + if let TableRow::Styled(_, style) = row { + *style = style.patch(self.colours.disabled_text_style); } } + + TextTable { + table_gap: app_state.app_config_fields.table_gap, + is_force_redraw: app_state.is_force_redraw, + recalculate_column_widths, + header_style: self.colours.table_header_style, + border_style, + highlighted_text_style, + title: Some(TextTableTitle { + title: " Processes ".into(), + is_expanded: app_state.is_expanded, + }), + is_on_widget, + draw_border, + show_table_scroll_position: app_state.app_config_fields.show_table_scroll_position, + title_style: self.colours.widget_title_style, + text_style: self.colours.text_style, + left_to_right: true, + } + .draw_text_table( + f, + draw_loc, + &mut proc_widget_state.table_state, + &proc_widget_state.table_data, + app_state.widget_map.get_mut(&widget_id), + ); } } @@ -583,14 +191,8 @@ impl Painter { let start_position: usize = get_search_start_position( num_columns - num_chars_for_text - 5, - &proc_widget_state - .process_search_state - .search_state - .cursor_direction, - &mut proc_widget_state - .process_search_state - .search_state - .cursor_bar, + &proc_widget_state.proc_search.search_state.cursor_direction, + &mut proc_widget_state.proc_search.search_state.cursor_bar, current_cursor_position, app_state.is_force_redraw, ); @@ -625,32 +227,26 @@ impl Painter { })]; // Text options shamelessly stolen from VS Code. - let case_style = if !proc_widget_state.process_search_state.is_ignoring_case { + let case_style = if !proc_widget_state.proc_search.is_ignoring_case { self.colours.currently_selected_text_style } else { self.colours.text_style }; - let whole_word_style = if proc_widget_state - .process_search_state - .is_searching_whole_word - { + let whole_word_style = if proc_widget_state.proc_search.is_searching_whole_word { self.colours.currently_selected_text_style } else { self.colours.text_style }; - let regex_style = if proc_widget_state - .process_search_state - .is_searching_with_regex - { + let regex_style = if proc_widget_state.proc_search.is_searching_with_regex { self.colours.currently_selected_text_style } else { self.colours.text_style }; - // FIXME: [MOUSE] Mouse support for these in search - // FIXME: [MOVEMENT] Movement support for these in search + // TODO: [MOUSE] Mouse support for these in search + // TODO: [MOVEMENT] Movement support for these in search let option_text = Spans::from(vec![ Span::styled( format!("Case({})", if self.is_mac_os { "F1" } else { "Alt+C" }), @@ -669,11 +265,7 @@ impl Painter { ]); search_text.push(Spans::from(Span::styled( - if let Some(err) = &proc_widget_state - .process_search_state - .search_state - .error_message - { + if let Some(err) = &proc_widget_state.proc_search.search_state.error_message { err.as_str() } else { "" @@ -682,17 +274,14 @@ impl Painter { ))); search_text.push(option_text); - let current_border_style = if proc_widget_state - .process_search_state - .search_state - .is_invalid_search - { - self.colours.invalid_query_style - } else if is_on_widget { - self.colours.highlighted_border_style - } else { - self.colours.border_style - }; + let current_border_style = + if proc_widget_state.proc_search.search_state.is_invalid_search { + self.colours.invalid_query_style + } else if is_on_widget { + self.colours.highlighted_border_style + } else { + self.colours.border_style + }; let title = Span::styled( if draw_border { @@ -751,127 +340,70 @@ impl Painter { /// state that is stored. /// /// This should not be directly called. - fn draw_process_sort<B: Backend>( + fn draw_sort_table<B: Backend>( &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool, widget_id: u64, ) { - let is_on_widget = widget_id == app_state.current_widget.widget_id; - + let should_get_widget_bounds = app_state.should_get_widget_bounds(); if let Some(proc_widget_state) = app_state.proc_state.widget_states.get_mut(&(widget_id - 2)) { - let current_scroll_position = proc_widget_state.columns.current_scroll_position; - let sort_string = proc_widget_state - .columns - .ordered_columns - .iter() - .filter(|column_type| { - proc_widget_state - .columns - .column_mapping - .get(column_type) - .unwrap() - .enabled - }) - .map(|column_type| column_type.to_string()) - .collect::<Vec<_>>(); + let recalculate_column_widths = + should_get_widget_bounds || proc_widget_state.force_rerender; - let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT { - 0 + let is_on_widget = widget_id == app_state.current_widget.widget_id; + let (border_style, highlighted_text_style) = if is_on_widget { + ( + self.colours.highlighted_border_style, + self.colours.currently_selected_text_style, + ) } else { - app_state.app_config_fields.table_gap - }; - let position = get_start_position( - usize::from( - (draw_loc.height + (1 - table_gap)).saturating_sub(self.table_height_offset), - ), - &proc_widget_state.columns.scroll_direction, - &mut proc_widget_state.columns.previous_scroll_position, - current_scroll_position, - app_state.is_force_redraw, - ); - - // Sanity check - let start_position = if position >= sort_string.len() { - sort_string.len().saturating_sub(1) - } else { - position + (self.colours.border_style, self.colours.text_style) }; - let sliced_vec = &sort_string[start_position..]; - - let sort_options = sliced_vec - .iter() - .map(|column| Row::new(vec![column.as_str()])); - - let column_state = &mut proc_widget_state.columns.column_state; - column_state.select(Some( - proc_widget_state + // TODO: [PROC] Perhaps move this generation elsewhere... or leave it as is but look at partial rendering? + let table_data = { + let data = proc_widget_state + .table_state .columns - .current_scroll_position - .saturating_sub(start_position), - )); - let current_border_style = if proc_widget_state - .process_search_state - .search_state - .is_invalid_search - { - self.colours.invalid_query_style - } else if is_on_widget { - self.colours.highlighted_border_style - } else { - self.colours.border_style - }; + .iter() + .filter_map(|col| { + if col.is_hidden { + None + } else { + Some(TableRow::Raw(vec![col.header.text().clone()])) + } + }) + .collect(); - let process_sort_block = if draw_border { - Block::default() - .borders(Borders::ALL) - .border_style(current_border_style) - } else if is_on_widget { - Block::default() - .borders(SIDE_BORDERS) - .border_style(current_border_style) - } else { - Block::default().borders(Borders::NONE) - }; - - let highlight_style = if is_on_widget { - self.colours.currently_selected_text_style - } else { - self.colours.text_style - }; - - let margined_draw_loc = Layout::default() - .constraints([Constraint::Percentage(100)]) - .horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 }) - .direction(Direction::Horizontal) - .split(draw_loc)[0]; - - f.render_stateful_widget( - Table::new(sort_options) - .header( - Row::new(vec!["Sort By"]) - .style(self.colours.table_header_style) - .bottom_margin(table_gap), - ) - .block(process_sort_block) - .highlight_style(highlight_style) - .style(self.colours.text_style) - .widths(&[Constraint::Percentage(100)]), - margined_draw_loc, - column_state, - ); - - if app_state.should_get_widget_bounds() { - // Update draw loc in widget map - if let Some(widget) = app_state.widget_map.get_mut(&widget_id) { - widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y)); - widget.bottom_right_corner = Some(( - margined_draw_loc.x + margined_draw_loc.width, - margined_draw_loc.y + margined_draw_loc.height, - )); + TableData { + data, + col_widths: vec![usize::from(SORT_MENU_WIDTH)], } + }; + + TextTable { + table_gap: app_state.app_config_fields.table_gap, + is_force_redraw: app_state.is_force_redraw, + recalculate_column_widths, + header_style: self.colours.table_header_style, + border_style, + highlighted_text_style, + title: None, + is_on_widget, + draw_border, + show_table_scroll_position: app_state.app_config_fields.show_table_scroll_position, + title_style: self.colours.widget_title_style, + text_style: self.colours.text_style, + left_to_right: true, } + .draw_text_table( + f, + draw_loc, + &mut proc_widget_state.sort_table_state, + &table_data, + app_state.widget_map.get_mut(&widget_id), + ); } } } diff --git a/src/canvas/widgets/temp_table.rs b/src/canvas/widgets/temp_table.rs index 974afd14..45554faa 100644 --- a/src/canvas/widgets/temp_table.rs +++ b/src/canvas/widgets/temp_table.rs @@ -1,31 +1,12 @@ -use once_cell::sync::Lazy; -use tui::{ - backend::Backend, - layout::{Constraint, Direction, Layout, Rect}, - terminal::Frame, - text::Span, - text::{Spans, Text}, - widgets::{Block, Borders, Row, Table}, -}; +use tui::{backend::Backend, layout::Rect, terminal::Frame}; use crate::{ app, canvas::{ - drawing_utils::{get_column_widths, get_start_position}, + components::{TextTable, TextTableTitle}, Painter, }, - constants::*, }; -use unicode_segmentation::UnicodeSegmentation; - -const TEMP_HEADERS: [&str; 2] = ["Sensor", "Temp"]; - -static TEMP_HEADERS_LENS: Lazy<Vec<u16>> = Lazy::new(|| { - TEMP_HEADERS - .iter() - .map(|entry| entry.len() as u16) - .collect::<Vec<_>>() -}); impl Painter { pub fn draw_temp_table<B: Backend>( @@ -34,109 +15,9 @@ impl Painter { ) { let recalculate_column_widths = app_state.should_get_widget_bounds(); if let Some(temp_widget_state) = app_state.temp_state.widget_states.get_mut(&widget_id) { - let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT { - 0 - } else { - app_state.app_config_fields.table_gap - }; - let start_position = get_start_position( - usize::from( - (draw_loc.height + (1 - table_gap)).saturating_sub(self.table_height_offset), - ), - &temp_widget_state.scroll_state.scroll_direction, - &mut temp_widget_state.scroll_state.scroll_bar, - temp_widget_state.scroll_state.current_scroll_position, - app_state.is_force_redraw, - ); - let is_on_widget = widget_id == app_state.current_widget.widget_id; - let temp_table_state = &mut temp_widget_state.scroll_state.table_state; - temp_table_state.select(Some( - temp_widget_state - .scroll_state - .current_scroll_position - .saturating_sub(start_position), - )); - let sliced_vec = &app_state.canvas_data.temp_sensor_data[start_position..]; + let is_on_widget = app_state.current_widget.widget_id == widget_id; - // Calculate widths - let hard_widths = [None, None]; - if recalculate_column_widths { - temp_widget_state.table_width_state.desired_column_widths = { - let mut column_widths = TEMP_HEADERS_LENS.clone(); - for row in sliced_vec { - for (col, entry) in row.iter().enumerate() { - if entry.len() as u16 > column_widths[col] { - column_widths[col] = entry.len() as u16; - } - } - } - - column_widths - }; - temp_widget_state.table_width_state.calculated_column_widths = get_column_widths( - draw_loc.width, - &hard_widths, - &(TEMP_HEADERS_LENS - .iter() - .map(|width| Some(*width)) - .collect::<Vec<_>>()), - &[Some(0.80), Some(-1.0)], - &temp_widget_state - .table_width_state - .desired_column_widths - .iter() - .map(|width| Some(*width)) - .collect::<Vec<_>>(), - false, - ); - } - - let dcw = &temp_widget_state.table_width_state.desired_column_widths; - let ccw = &temp_widget_state.table_width_state.calculated_column_widths; - let temperature_rows = - sliced_vec.iter().map(|temp_row| { - let truncated_data = temp_row.iter().zip(&hard_widths).enumerate().map( - |(itx, (entry, width))| { - if width.is_none() { - if let (Some(desired_col_width), Some(calculated_col_width)) = - (dcw.get(itx), ccw.get(itx)) - { - if *desired_col_width > *calculated_col_width - && *calculated_col_width > 0 - { - let calculated_col_width: usize = - (*calculated_col_width).into(); - - let graphemes = - UnicodeSegmentation::graphemes(entry.as_str(), true) - .collect::<Vec<&str>>(); - - if graphemes.len() > calculated_col_width - && calculated_col_width > 1 - { - // Truncate with ellipsis - let first_n = - graphemes[..(calculated_col_width - 1)].concat(); - Text::raw(format!("{}…", first_n)) - } else { - Text::raw(entry) - } - } else { - Text::raw(entry) - } - } else { - Text::raw(entry) - } - } else { - Text::raw(entry) - } - }, - ); - - Row::new(truncated_data) - }); - - let (border_style, highlight_style) = if is_on_widget { + let (border_style, highlighted_text_style) = if is_on_widget { ( self.colours.highlighted_border_style, self.colours.currently_selected_text_style, @@ -144,118 +25,31 @@ impl Painter { } else { (self.colours.border_style, self.colours.text_style) }; - - let title_base = if app_state.app_config_fields.show_table_scroll_position { - let title_string = format!( - " Temperatures ({} of {}) ", - temp_widget_state - .scroll_state - .current_scroll_position - .saturating_add(1), - app_state.canvas_data.temp_sensor_data.len() - ); - - if title_string.len() <= draw_loc.width.into() { - title_string - } else { - " Temperatures ".to_string() - } - } else { - " Temperatures ".to_string() - }; - - let title = if app_state.is_expanded { - const ESCAPE_ENDING: &str = "── Esc to go back "; - - let (chosen_title_base, expanded_title_base) = { - let temp_title_base = format!("{}{}", title_base, ESCAPE_ENDING); - - if temp_title_base.len() > draw_loc.width.into() { - ( - " Temperatures ".to_string(), - format!("{}{}", " Temperatures ", ESCAPE_ENDING), - ) - } else { - (title_base, temp_title_base) - } - }; - - Spans::from(vec![ - Span::styled(chosen_title_base, self.colours.widget_title_style), - Span::styled( - format!( - "─{}─ Esc to go back ", - "─".repeat( - usize::from(draw_loc.width).saturating_sub( - UnicodeSegmentation::graphemes( - expanded_title_base.as_str(), - true - ) - .count() - + 2 - ) - ) - ), - border_style, - ), - ]) - } else { - Spans::from(Span::styled(title_base, self.colours.widget_title_style)) - }; - - let temp_block = if draw_border { - Block::default() - .title(title) - .borders(Borders::ALL) - .border_style(border_style) - } else if is_on_widget { - Block::default() - .borders(SIDE_BORDERS) - .border_style(self.colours.highlighted_border_style) - } else { - Block::default().borders(Borders::NONE) - }; - - let margined_draw_loc = Layout::default() - .constraints([Constraint::Percentage(100)]) - .horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 }) - .direction(Direction::Horizontal) - .split(draw_loc)[0]; - - // Draw - f.render_stateful_widget( - Table::new(temperature_rows) - .header( - Row::new(TEMP_HEADERS.to_vec()) - .style(self.colours.table_header_style) - .bottom_margin(table_gap), - ) - .block(temp_block) - .highlight_style(highlight_style) - .style(self.colours.text_style) - .widths( - &(temp_widget_state - .table_width_state - .calculated_column_widths - .iter() - .map(|calculated_width| Constraint::Length(*calculated_width)) - .collect::<Vec<_>>()), - ), - margined_draw_loc, - temp_table_state, - ); - - if app_state.should_get_widget_bounds() { - // Update draw loc in widget map - // Note there is no difference between this and using draw_loc, but I'm too lazy to fix it. - if let Some(widget) = app_state.widget_map.get_mut(&widget_id) { - widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y)); - widget.bottom_right_corner = Some(( - margined_draw_loc.x + margined_draw_loc.width, - margined_draw_loc.y + margined_draw_loc.height, - )); - } + TextTable { + table_gap: app_state.app_config_fields.table_gap, + is_force_redraw: app_state.is_force_redraw, + recalculate_column_widths, + header_style: self.colours.table_header_style, + border_style, + highlighted_text_style, + title: Some(TextTableTitle { + title: " Temperatures ".into(), + is_expanded: app_state.is_expanded, + }), + is_on_widget, + draw_border, + show_table_scroll_position: app_state.app_config_fields.show_table_scroll_position, + title_style: self.colours.widget_title_style, + text_style: self.colours.text_style, + left_to_right: false, } + .draw_text_table( + f, + draw_loc, + &mut temp_widget_state.table_state, + &app_state.converted_data.temp_sensor_data, + app_state.widget_map.get_mut(&widget_id), + ); } } } diff --git a/src/clap.rs b/src/clap.rs index 0fbd3223..c667088a 100644 --- a/src/clap.rs +++ b/src/clap.rs @@ -148,7 +148,7 @@ pub fn build_app() -> Command<'static> { .help("Uses a dot marker for graphs.") .long_help("Uses a dot marker for graphs as opposed to the default braille marker."); - let group = Arg::new("group") // FIXME: Rename this to something like "group_process", would be "breaking" though. + let group = Arg::new("group") // TODO: Rename this to something like "group_process", would be "breaking" though. .short('g') .long("group") .help("Groups processes with the same name by default.") diff --git a/src/data_conversion.rs b/src/data_conversion.rs index 7f9d184c..98bbee2c 100644 --- a/src/data_conversion.rs +++ b/src/data_conversion.rs @@ -1,17 +1,16 @@ //! This mainly concerns converting collected data into things that the canvas //! can actually handle. + +use crate::app::CellContent; +use crate::canvas::Point; use crate::{app::AxisScaling, units::data_units::DataUnit, Pid}; use crate::{ - app::{data_farmer, data_harvester, App, ProcWidgetState}, - utils::{self, gen_util::*}, + app::{data_farmer, data_harvester, App}, + utils::gen_util::*, }; -use data_harvester::processes::ProcessSorting; -use fxhash::FxBuildHasher; -use indexmap::IndexSet; -use std::collections::{HashMap, VecDeque}; -/// Point is of time, data -type Point = (f64, f64); +use concat_string::concat_string; +use fxhash::FxHashMap; #[derive(Default, Debug)] pub struct ConvertedBatteryData { @@ -23,6 +22,27 @@ pub struct ConvertedBatteryData { pub health: String, } +#[derive(Default, Debug)] +pub struct TableData { + pub data: Vec<TableRow>, + pub col_widths: Vec<usize>, +} + +#[derive(Debug)] +pub enum TableRow { + Raw(Vec<CellContent>), + Styled(Vec<CellContent>, tui::style::Style), +} + +impl TableRow { + pub fn row(&self) -> &[CellContent] { + match self { + TableRow::Raw(data) => data, + TableRow::Styled(data, _) => data, + } + } +} + #[derive(Default, Debug)] pub struct ConvertedNetworkData { pub rx: Vec<Point>, @@ -40,39 +60,6 @@ pub struct ConvertedNetworkData { // mean_tx: f64, } -// TODO: [REFACTOR] Process data... stuff really needs a rewrite. Again. -#[derive(Clone, Default, Debug)] -pub struct ConvertedProcessData { - pub pid: Pid, - pub ppid: Option<Pid>, - pub name: String, - pub command: String, - pub is_thread: Option<bool>, - pub cpu_percent_usage: f64, - pub mem_percent_usage: f64, - pub mem_usage_bytes: u64, - pub mem_usage_str: (f64, String), - pub group_pids: Vec<Pid>, - pub read_per_sec: String, - pub write_per_sec: String, - pub total_read: String, - pub total_write: String, - pub rps_f64: f64, - pub wps_f64: f64, - pub tr_f64: f64, - pub tw_f64: f64, - pub process_state: String, - pub process_char: char, - pub user: Option<String>, - - /// Prefix printed before the process when displayed. - pub process_description_prefix: Option<String>, - /// Whether to mark this process entry as disabled (mostly for tree mode). - pub is_disabled_entry: bool, - /// Whether this entry is collapsed, hiding all its children (for tree mode). - pub is_collapsed_entry: bool, -} - #[derive(Clone, Default, Debug)] pub struct ConvertedCpuData { pub cpu_name: String, @@ -83,35 +70,81 @@ pub struct ConvertedCpuData { pub legend_value: String, } -pub fn convert_temp_row(app: &App) -> Vec<Vec<String>> { +#[derive(Default)] +pub struct ConvertedData { + pub rx_display: String, + pub tx_display: String, + pub total_rx_display: String, + pub total_tx_display: String, + pub network_data_rx: Vec<Point>, + pub network_data_tx: Vec<Point>, + pub disk_data: TableData, + pub temp_sensor_data: TableData, + + /// A mapping from a process name to any PID with that name. + pub process_name_pid_map: FxHashMap<String, Vec<Pid>>, + + /// A mapping from a process command to any PID with that name. + pub process_cmd_pid_map: FxHashMap<String, Vec<Pid>>, + + pub mem_labels: Option<(String, String)>, + pub swap_labels: Option<(String, String)>, + + pub mem_data: Vec<Point>, // TODO: Switch this and all data points over to a better data structure... + pub swap_data: Vec<Point>, + pub load_avg_data: [f32; 3], + pub cpu_data: Vec<ConvertedCpuData>, + pub battery_data: Vec<ConvertedBatteryData>, +} + +pub fn convert_temp_row(app: &App) -> TableData { let current_data = &app.data_collection; let temp_type = &app.app_config_fields.temperature_type; + let mut col_widths = vec![0; 2]; - let mut sensor_vector: Vec<Vec<String>> = current_data + let mut sensor_vector: Vec<TableRow> = current_data .temp_harvest .iter() .map(|temp_harvest| { - vec![ - temp_harvest.name.clone(), - (temp_harvest.temperature.ceil() as u64).to_string() - + match temp_type { - data_harvester::temperature::TemperatureType::Celsius => "°C", - data_harvester::temperature::TemperatureType::Kelvin => "K", - data_harvester::temperature::TemperatureType::Fahrenheit => "°F", - }, - ] + let row = vec![ + CellContent::Simple(temp_harvest.name.clone().into()), + CellContent::Simple( + concat_string!( + (temp_harvest.temperature.ceil() as u64).to_string(), + match temp_type { + data_harvester::temperature::TemperatureType::Celsius => "°C", + data_harvester::temperature::TemperatureType::Kelvin => "K", + data_harvester::temperature::TemperatureType::Fahrenheit => "°F", + } + ) + .into(), + ), + ]; + + col_widths.iter_mut().zip(&row).for_each(|(curr, r)| { + *curr = std::cmp::max(*curr, r.len()); + }); + + TableRow::Raw(row) }) .collect(); if sensor_vector.is_empty() { - sensor_vector.push(vec!["No Sensors Found".to_string(), "".to_string()]); + sensor_vector.push(TableRow::Raw(vec![ + CellContent::Simple("No Sensors Found".into()), + CellContent::Simple("".into()), + ])); } - sensor_vector + TableData { + data: sensor_vector, + col_widths, + } } -pub fn convert_disk_row(current_data: &data_farmer::DataCollection) -> Vec<Vec<String>> { - let mut disk_vector: Vec<Vec<String>> = Vec::new(); +pub fn convert_disk_row(current_data: &data_farmer::DataCollection) -> TableData { + let mut disk_vector: Vec<TableRow> = Vec::new(); + let mut col_widths = vec![0; 8]; current_data .disk_harvest @@ -120,9 +153,9 @@ pub fn convert_disk_row(current_data: &data_farmer::DataCollection) -> Vec<Vec<S .for_each(|(disk, (io_read, io_write))| { let free_space_fmt = if let Some(free_space) = disk.free_space { let converted_free_space = get_decimal_bytes(free_space); - format!("{:.*}{}", 0, converted_free_space.0, converted_free_space.1) + format!("{:.*}{}", 0, converted_free_space.0, converted_free_space.1).into() } else { - "N/A".to_string() + "N/A".into() }; let total_space_fmt = if let Some(total_space) = disk.total_space { let converted_total_space = get_decimal_bytes(total_space); @@ -130,46 +163,52 @@ pub fn convert_disk_row(current_data: &data_farmer::DataCollection) -> Vec<Vec<S "{:.*}{}", 0, converted_total_space.0, converted_total_space.1 ) + .into() } else { - "N/A".to_string() + "N/A".into() }; let usage_fmt = if let (Some(used_space), Some(total_space)) = (disk.used_space, disk.total_space) { - format!("{:.0}%", used_space as f64 / total_space as f64 * 100_f64) + format!("{:.0}%", used_space as f64 / total_space as f64 * 100_f64).into() } else { - "N/A".to_string() + "N/A".into() }; - disk_vector.push(vec![ - disk.name.to_string(), - disk.mount_point.to_string(), - usage_fmt, - free_space_fmt, - total_space_fmt, - io_read.to_string(), - io_write.to_string(), - ]); + let row = vec![ + CellContent::Simple(disk.name.clone().into()), + CellContent::Simple(disk.mount_point.clone().into()), + CellContent::Simple(usage_fmt), + CellContent::Simple(free_space_fmt), + CellContent::Simple(total_space_fmt), + CellContent::Simple(io_read.clone().into()), + CellContent::Simple(io_write.clone().into()), + ]; + col_widths.iter_mut().zip(&row).for_each(|(curr, r)| { + *curr = std::cmp::max(*curr, r.len()); + }); + disk_vector.push(TableRow::Raw(row)); }); if disk_vector.is_empty() { - disk_vector.push(vec!["No Disks Found".to_string(), "".to_string()]); + disk_vector.push(TableRow::Raw(vec![ + CellContent::Simple("No Disks Found".into()), + CellContent::Simple("".into()), + ])); } - disk_vector + TableData { + data: disk_vector, + col_widths, + } } pub fn convert_cpu_data_points( current_data: &data_farmer::DataCollection, existing_cpu_data: &mut Vec<ConvertedCpuData>, - is_frozen: bool, ) { - let current_time = if is_frozen { - if let Some(frozen_instant) = current_data.frozen_instant { - frozen_instant - } else { - current_data.current_instant - } + let current_time = if let Some(frozen_instant) = current_data.frozen_instant { + frozen_instant } else { current_data.current_instant }; @@ -179,7 +218,7 @@ pub fn convert_cpu_data_points( if data.cpu_data.len() + 1 != existing_cpu_data.len() { *existing_cpu_data = vec![ConvertedCpuData { cpu_name: "All".to_string(), - short_cpu_name: "All".to_string(), + short_cpu_name: "".to_string(), cpu_data: vec![], legend_value: String::new(), }]; @@ -240,16 +279,10 @@ pub fn convert_cpu_data_points( } } -pub fn convert_mem_data_points( - current_data: &data_farmer::DataCollection, is_frozen: bool, -) -> Vec<Point> { +pub fn convert_mem_data_points(current_data: &data_farmer::DataCollection) -> Vec<Point> { let mut result: Vec<Point> = Vec::new(); - let current_time = if is_frozen { - if let Some(frozen_instant) = current_data.frozen_instant { - frozen_instant - } else { - current_data.current_instant - } + let current_time = if let Some(frozen_instant) = current_data.frozen_instant { + frozen_instant } else { current_data.current_instant }; @@ -268,16 +301,10 @@ pub fn convert_mem_data_points( result } -pub fn convert_swap_data_points( - current_data: &data_farmer::DataCollection, is_frozen: bool, -) -> Vec<Point> { +pub fn convert_swap_data_points(current_data: &data_farmer::DataCollection) -> Vec<Point> { let mut result: Vec<Point> = Vec::new(); - let current_time = if is_frozen { - if let Some(frozen_instant) = current_data.frozen_instant { - frozen_instant - } else { - current_data.current_instant - } + let current_time = if let Some(frozen_instant) = current_data.frozen_instant { + frozen_instant } else { current_data.current_instant }; @@ -367,18 +394,14 @@ pub fn convert_mem_labels( } pub fn get_rx_tx_data_points( - current_data: &data_farmer::DataCollection, is_frozen: bool, network_scale_type: &AxisScaling, + current_data: &data_farmer::DataCollection, network_scale_type: &AxisScaling, network_unit_type: &DataUnit, network_use_binary_prefix: bool, ) -> (Vec<Point>, Vec<Point>) { let mut rx: Vec<Point> = Vec::new(); let mut tx: Vec<Point> = Vec::new(); - let current_time = if is_frozen { - if let Some(frozen_instant) = current_data.frozen_instant { - frozen_instant - } else { - current_data.current_instant - } + let current_time = if let Some(frozen_instant) = current_data.frozen_instant { + frozen_instant } else { current_data.current_instant }; @@ -422,13 +445,12 @@ pub fn get_rx_tx_data_points( } pub fn convert_network_data_points( - current_data: &data_farmer::DataCollection, is_frozen: bool, need_four_points: bool, + current_data: &data_farmer::DataCollection, need_four_points: bool, network_scale_type: &AxisScaling, network_unit_type: &DataUnit, network_use_binary_prefix: bool, ) -> ConvertedNetworkData { let (rx, tx) = get_rx_tx_data_points( current_data, - is_frozen, network_scale_type, network_unit_type, network_use_binary_prefix, @@ -550,816 +572,26 @@ pub fn convert_network_data_points( } } -pub enum ProcessGroupingType { - Grouped, - Ungrouped, +/// Returns a string given a value that is converted to the closest binary variant. +/// If the value is greater than a gibibyte, then it will return a decimal place. +pub fn binary_byte_string(value: u64) -> String { + let converted_values = get_binary_bytes(value); + if value >= GIBI_LIMIT { + format!("{:.*}{}", 1, converted_values.0, converted_values.1) + } else { + format!("{:.*}{}", 0, converted_values.0, converted_values.1) + } } -pub enum ProcessNamingType { - Name, - Path, -} - -/// Given read/s, write/s, total read, and total write values, return 4 strings that represent read/s, write/s, total read, and total write -fn get_disk_io_strings( - rps: u64, wps: u64, total_read: u64, total_write: u64, -) -> (String, String, String, String) { - // Note we always use bytes for total read/write here (for now). - let converted_rps = get_decimal_bytes(rps); - let converted_wps = get_decimal_bytes(wps); - let converted_total_read = get_decimal_bytes(total_read); - let converted_total_write = get_decimal_bytes(total_write); - - ( - if rps >= GIGA_LIMIT { - format!("{:.*}{}/s", 1, converted_rps.0, converted_rps.1) - } else { - format!("{:.*}{}/s", 0, converted_rps.0, converted_rps.1) - }, - if wps >= GIGA_LIMIT { - format!("{:.*}{}/s", 1, converted_wps.0, converted_wps.1) - } else { - format!("{:.*}{}/s", 0, converted_wps.0, converted_wps.1) - }, - if total_read >= GIGA_LIMIT { - format!("{:.*}{}", 1, converted_total_read.0, converted_total_read.1) - } else { - format!("{:.*}{}", 0, converted_total_read.0, converted_total_read.1) - }, - if total_write >= GIGA_LIMIT { - format!( - "{:.*}{}", - 1, converted_total_write.0, converted_total_write.1 - ) - } else { - format!( - "{:.*}{}", - 0, converted_total_write.0, converted_total_write.1 - ) - }, - ) -} - -/// Because we needed to UPDATE data entries rather than REPLACING entries, we instead update -/// the existing vector. -pub fn convert_process_data( - current_data: &data_farmer::DataCollection, - existing_converted_process_data: &mut HashMap<Pid, ConvertedProcessData>, - #[cfg(target_family = "unix")] user_table: &mut data_harvester::processes::UserTable, -) { - // TODO [THREAD]: Thread highlighting and hiding support - // For macOS see https://github.com/hishamhm/htop/pull/848/files - - let mut complete_pid_set: fxhash::FxHashSet<Pid> = - existing_converted_process_data.keys().copied().collect(); - - for process in ¤t_data.process_harvest { - let (read_per_sec, write_per_sec, total_read, total_write) = get_disk_io_strings( - process.read_bytes_per_sec, - process.write_bytes_per_sec, - process.total_read_bytes, - process.total_write_bytes, - ); - - let mem_usage_str = get_binary_bytes(process.mem_usage_bytes); - - let user = { - #[cfg(target_family = "unix")] - { - if let Some(uid) = process.uid { - user_table.get_uid_to_username_mapping(uid).ok() - } else { - None - } - } - #[cfg(not(target_family = "unix"))] - { - None - } - }; - - if let Some(process_entry) = existing_converted_process_data.get_mut(&process.pid) { - complete_pid_set.remove(&process.pid); - - // Very dumb way to see if there's PID reuse... - if process_entry.ppid == process.parent_pid { - process_entry.name = process.name.to_string(); - process_entry.command = process.command.to_string(); - process_entry.cpu_percent_usage = process.cpu_usage_percent; - process_entry.mem_percent_usage = process.mem_usage_percent; - process_entry.mem_usage_bytes = process.mem_usage_bytes; - process_entry.mem_usage_str = mem_usage_str; - process_entry.group_pids = vec![process.pid]; - process_entry.read_per_sec = read_per_sec; - process_entry.write_per_sec = write_per_sec; - process_entry.total_read = total_read; - process_entry.total_write = total_write; - process_entry.rps_f64 = process.read_bytes_per_sec as f64; - process_entry.wps_f64 = process.write_bytes_per_sec as f64; - process_entry.tr_f64 = process.total_read_bytes as f64; - process_entry.tw_f64 = process.total_write_bytes as f64; - process_entry.process_state = process.process_state.to_owned(); - process_entry.process_char = process.process_state_char; - process_entry.process_description_prefix = None; - process_entry.is_disabled_entry = false; - process_entry.user = user; - } else { - // ...I hate that I can't combine if let and an if statement in one line... - *process_entry = ConvertedProcessData { - pid: process.pid, - ppid: process.parent_pid, - is_thread: None, - name: process.name.to_string(), - command: process.command.to_string(), - cpu_percent_usage: process.cpu_usage_percent, - mem_percent_usage: process.mem_usage_percent, - mem_usage_bytes: process.mem_usage_bytes, - mem_usage_str, - group_pids: vec![process.pid], - read_per_sec, - write_per_sec, - total_read, - total_write, - rps_f64: process.read_bytes_per_sec as f64, - wps_f64: process.write_bytes_per_sec as f64, - tr_f64: process.total_read_bytes as f64, - tw_f64: process.total_write_bytes as f64, - process_state: process.process_state.to_owned(), - process_char: process.process_state_char, - process_description_prefix: None, - is_disabled_entry: false, - is_collapsed_entry: false, - user, - }; - } - } else { - existing_converted_process_data.insert( - process.pid, - ConvertedProcessData { - pid: process.pid, - ppid: process.parent_pid, - is_thread: None, - name: process.name.to_string(), - command: process.command.to_string(), - cpu_percent_usage: process.cpu_usage_percent, - mem_percent_usage: process.mem_usage_percent, - mem_usage_bytes: process.mem_usage_bytes, - mem_usage_str, - group_pids: vec![process.pid], - read_per_sec, - write_per_sec, - total_read, - total_write, - rps_f64: process.read_bytes_per_sec as f64, - wps_f64: process.write_bytes_per_sec as f64, - tr_f64: process.total_read_bytes as f64, - tw_f64: process.total_write_bytes as f64, - process_state: process.process_state.to_owned(), - process_char: process.process_state_char, - process_description_prefix: None, - is_disabled_entry: false, - is_collapsed_entry: false, - user, - }, - ); - } +/// Returns a string given a value that is converted to the closest SI-variant, per second. +/// If the value is greater than a giga-X, then it will return a decimal place. +pub fn dec_bytes_per_second_string(value: u64) -> String { + let converted_values = get_decimal_bytes(value); + if value >= GIGA_LIMIT { + format!("{:.*}{}/s", 1, converted_values.0, converted_values.1) + } else { + format!("{:.*}{}/s", 0, converted_values.0, converted_values.1) } - - // Now clean up any spare entries that weren't visited, to avoid clutter: - complete_pid_set.iter().for_each(|pid| { - existing_converted_process_data.remove(pid); - }) -} - -const BRANCH_ENDING: char = '└'; -const BRANCH_VERTICAL: char = '│'; -const BRANCH_SPLIT: char = '├'; -const BRANCH_HORIZONTAL: char = '─'; - -pub fn tree_process_data( - filtered_process_data: &[ConvertedProcessData], is_using_command: bool, - sorting_type: &ProcessSorting, is_sort_descending: bool, -) -> Vec<ConvertedProcessData> { - // TODO: [TREE] Option to sort usage by total branch usage or individual value usage? - - // Let's first build up a (really terrible) parent -> child mapping... - // At the same time, let's make a mapping of PID -> process data! - let mut parent_child_mapping: HashMap<Pid, IndexSet<Pid, FxBuildHasher>> = HashMap::default(); - let mut pid_process_mapping: HashMap<Pid, &ConvertedProcessData> = HashMap::default(); // We actually already have this stored, but it's unfiltered... oh well. - let mut orphan_set: IndexSet<Pid, FxBuildHasher> = - IndexSet::with_hasher(FxBuildHasher::default()); - let mut collapsed_set: IndexSet<Pid, FxBuildHasher> = - IndexSet::with_hasher(FxBuildHasher::default()); - - filtered_process_data.iter().for_each(|process| { - if let Some(ppid) = process.ppid { - orphan_set.insert(ppid); - } - orphan_set.insert(process.pid); - }); - - filtered_process_data.iter().for_each(|process| { - // Create a mapping for the process if it DNE. - parent_child_mapping - .entry(process.pid) - .or_insert_with(|| IndexSet::with_hasher(FxBuildHasher::default())); - pid_process_mapping.insert(process.pid, process); - - if process.is_collapsed_entry { - collapsed_set.insert(process.pid); - } - - // Insert its mapping to the process' parent if needed (create if it DNE). - if let Some(ppid) = process.ppid { - orphan_set.remove(&process.pid); - parent_child_mapping - .entry(ppid) - .or_insert_with(|| IndexSet::with_hasher(FxBuildHasher::default())) - .insert(process.pid); - } - }); - - // Keep only orphans, or promote children of orphans to a top-level orphan - // if their parents DNE in our pid to process mapping... - let old_orphan_set = orphan_set.clone(); - old_orphan_set.iter().for_each(|pid| { - if pid_process_mapping.get(pid).is_none() { - // DNE! Promote the mapped children and remove the current parent... - orphan_set.remove(pid); - if let Some(children) = parent_child_mapping.get(pid) { - orphan_set.extend(children); - } - } - }); - - // Turn the parent-child mapping into a "list" via DFS... - let mut pids_to_explore: VecDeque<Pid> = orphan_set.into_iter().collect(); - let mut explored_pids: Vec<Pid> = vec![]; - let mut lines: Vec<String> = vec![]; - - /// A post-order traversal to correctly prune entire branches that only contain children - /// that are disabled and themselves are also disabled ~~wait that sounds wrong~~. - /// Basically, go through the hashmap, and prune out all branches that are no longer relevant. - fn prune_disabled_pids( - current_pid: Pid, parent_child_mapping: &mut HashMap<Pid, IndexSet<Pid, FxBuildHasher>>, - pid_process_mapping: &HashMap<Pid, &ConvertedProcessData>, - ) -> bool { - // Let's explore all the children first, and make sure they (and their children) - // aren't all disabled... - let mut are_all_children_disabled = true; - if let Some(children) = parent_child_mapping.get(¤t_pid) { - for child_pid in children.clone() { - let is_child_disabled = - prune_disabled_pids(child_pid, parent_child_mapping, pid_process_mapping); - - if is_child_disabled { - if let Some(current_mapping) = parent_child_mapping.get_mut(¤t_pid) { - current_mapping.remove(&child_pid); - } - } else if are_all_children_disabled { - are_all_children_disabled = false; - } - } - } - - // Now consider the current pid and whether to prune... - // If the node itself is not disabled, then never prune. If it is, then check if all - // of its are disabled. - if let Some(process) = pid_process_mapping.get(¤t_pid) { - if process.is_disabled_entry && are_all_children_disabled { - parent_child_mapping.remove(¤t_pid); - return true; - } - } - - false - } - - fn sort_remaining_pids( - current_pid: Pid, sort_type: &ProcessSorting, is_sort_descending: bool, - parent_child_mapping: &mut HashMap<Pid, IndexSet<Pid, FxBuildHasher>>, - pid_process_mapping: &HashMap<Pid, &ConvertedProcessData>, - ) { - // Sorting is special for tree data. So, by default, things are "sorted" - // via the DFS. Otherwise, since this is DFS of the scanned PIDs (which are in order), - // you actually get a REVERSE order --- so, you get higher PIDs earlier than lower ones. - // - // So how do we "sort"? The current idea is that: - // - We sort *per-level*. Say, I want to sort by CPU. The "first level" is sorted - // by CPU in terms of its usage. All its direct children are sorted by CPU - // with *their* siblings. Etc. - // - The default is thus PIDs in ascending order. We set it to this when - // we first enable the mode. - - // So first, let's look at the children... (post-order again) - if let Some(children) = parent_child_mapping.get(¤t_pid) { - let mut to_sort_vec: Vec<(Pid, &ConvertedProcessData)> = vec![]; - for child_pid in children.clone() { - if let Some(child_process) = pid_process_mapping.get(&child_pid) { - to_sort_vec.push((child_pid, child_process)); - } - sort_remaining_pids( - child_pid, - sort_type, - is_sort_descending, - parent_child_mapping, - pid_process_mapping, - ); - } - - // Now let's sort the immediate children! - sort_vec(&mut to_sort_vec, sort_type, is_sort_descending); - - // Need to reverse what we got, apparently... - if let Some(current_mapping) = parent_child_mapping.get_mut(¤t_pid) { - *current_mapping = to_sort_vec - .iter() - .rev() - .map(|(pid, _proc)| *pid) - .collect::<IndexSet<Pid, FxBuildHasher>>(); - } - } - } - - fn sort_vec( - to_sort_vec: &mut [(Pid, &ConvertedProcessData)], sort_type: &ProcessSorting, - is_sort_descending: bool, - ) { - // Sort by PID first (descending) - to_sort_vec.sort_by(|a, b| utils::gen_util::get_ordering(a.1.pid, b.1.pid, false)); - - match sort_type { - ProcessSorting::CpuPercent => { - to_sort_vec.sort_by(|a, b| { - utils::gen_util::get_ordering( - a.1.cpu_percent_usage, - b.1.cpu_percent_usage, - is_sort_descending, - ) - }); - } - ProcessSorting::Mem => { - to_sort_vec.sort_by(|a, b| { - utils::gen_util::get_ordering( - a.1.mem_usage_bytes, - b.1.mem_usage_bytes, - is_sort_descending, - ) - }); - } - ProcessSorting::MemPercent => { - to_sort_vec.sort_by(|a, b| { - utils::gen_util::get_ordering( - a.1.mem_percent_usage, - b.1.mem_percent_usage, - is_sort_descending, - ) - }); - } - ProcessSorting::ProcessName => { - to_sort_vec.sort_by(|a, b| { - utils::gen_util::get_ordering( - &a.1.name.to_lowercase(), - &b.1.name.to_lowercase(), - is_sort_descending, - ) - }); - } - ProcessSorting::Command => to_sort_vec.sort_by(|a, b| { - utils::gen_util::get_ordering( - &a.1.command.to_lowercase(), - &b.1.command.to_lowercase(), - is_sort_descending, - ) - }), - ProcessSorting::Pid => { - if is_sort_descending { - to_sort_vec.sort_by(|a, b| { - utils::gen_util::get_ordering(a.0, b.0, is_sort_descending) - }); - } - } - ProcessSorting::ReadPerSecond => { - to_sort_vec.sort_by(|a, b| { - utils::gen_util::get_ordering(a.1.rps_f64, b.1.rps_f64, is_sort_descending) - }); - } - ProcessSorting::WritePerSecond => { - to_sort_vec.sort_by(|a, b| { - utils::gen_util::get_ordering(a.1.wps_f64, b.1.wps_f64, is_sort_descending) - }); - } - ProcessSorting::TotalRead => { - to_sort_vec.sort_by(|a, b| { - utils::gen_util::get_ordering(a.1.tr_f64, b.1.tr_f64, is_sort_descending) - }); - } - ProcessSorting::TotalWrite => { - to_sort_vec.sort_by(|a, b| { - utils::gen_util::get_ordering(a.1.tw_f64, b.1.tw_f64, is_sort_descending) - }); - } - ProcessSorting::State => to_sort_vec.sort_by(|a, b| { - utils::gen_util::get_ordering( - &a.1.process_state.to_lowercase(), - &b.1.process_state.to_lowercase(), - is_sort_descending, - ) - }), - ProcessSorting::User => to_sort_vec.sort_by(|a, b| match (&a.1.user, &b.1.user) { - (Some(user_a), Some(user_b)) => utils::gen_util::get_ordering( - user_a.to_lowercase(), - user_b.to_lowercase(), - is_sort_descending, - ), - (Some(_), None) => std::cmp::Ordering::Less, - (None, Some(_)) => std::cmp::Ordering::Greater, - (None, None) => std::cmp::Ordering::Less, - }), - ProcessSorting::Count => { - // Should never occur in this case, tree mode explicitly disables grouping. - } - } - } - - /// A DFS traversal to correctly build the prefix lines (the pretty '├' and '─' lines) and - /// the correct order to the PID tree as a vector. - fn build_explored_pids( - current_pid: Pid, parent_child_mapping: &HashMap<Pid, IndexSet<Pid, FxBuildHasher>>, - prev_drawn_lines: &str, collapsed_set: &IndexSet<Pid, FxBuildHasher>, - ) -> (Vec<Pid>, Vec<String>) { - let mut explored_pids: Vec<Pid> = vec![current_pid]; - let mut lines: Vec<String> = vec![]; - - if collapsed_set.contains(¤t_pid) { - return (explored_pids, lines); - } else if let Some(children) = parent_child_mapping.get(¤t_pid) { - for (itx, child) in children.iter().rev().enumerate() { - let new_drawn_lines = if itx == children.len() - 1 { - format!("{} ", prev_drawn_lines) - } else { - format!("{}{} ", prev_drawn_lines, BRANCH_VERTICAL) - }; - - let (pid_res, branch_res) = build_explored_pids( - *child, - parent_child_mapping, - new_drawn_lines.as_str(), - collapsed_set, - ); - - if itx == children.len() - 1 { - lines.push(format!( - "{}{}", - prev_drawn_lines, - if !new_drawn_lines.is_empty() { - format!("{}{} ", BRANCH_ENDING, BRANCH_HORIZONTAL) - } else { - String::default() - } - )); - } else { - lines.push(format!( - "{}{}", - prev_drawn_lines, - if !new_drawn_lines.is_empty() { - format!("{}{} ", BRANCH_SPLIT, BRANCH_HORIZONTAL) - } else { - String::default() - } - )); - } - - explored_pids.extend(pid_res); - lines.extend(branch_res); - } - } - - (explored_pids, lines) - } - - /// Returns the total sum of CPU, MEM%, MEM, R/s, W/s, Total Read, and Total Write via DFS traversal. - fn get_usage_of_all_children( - parent_pid: Pid, parent_child_mapping: &HashMap<Pid, IndexSet<Pid, FxBuildHasher>>, - pid_process_mapping: &HashMap<Pid, &ConvertedProcessData>, - ) -> (f64, f64, u64, f64, f64, f64, f64) { - if let Some(&converted_process_data) = pid_process_mapping.get(&parent_pid) { - let ( - mut cpu, - mut mem_percent, - mut mem, - mut rps, - mut wps, - mut total_read, - mut total_write, - ) = ( - (converted_process_data.cpu_percent_usage * 10.0).round() / 10.0, - (converted_process_data.mem_percent_usage * 10.0).round() / 10.0, - converted_process_data.mem_usage_bytes, - (converted_process_data.rps_f64 * 10.0).round() / 10.0, - (converted_process_data.wps_f64 * 10.0).round() / 10.0, - (converted_process_data.tr_f64 * 10.0).round() / 10.0, - (converted_process_data.tw_f64 * 10.0).round() / 10.0, - ); - - if let Some(children) = parent_child_mapping.get(&parent_pid) { - for &child_pid in children { - let ( - child_cpu, - child_mem_percent, - child_mem, - child_rps, - child_wps, - child_total_read, - child_total_write, - ) = get_usage_of_all_children( - child_pid, - parent_child_mapping, - pid_process_mapping, - ); - - cpu += child_cpu; - mem_percent += child_mem_percent; - mem += child_mem; - rps += child_rps; - wps += child_wps; - total_read += child_total_read; - total_write += child_total_write; - } - } - - (cpu, mem_percent, mem, rps, wps, total_read, total_write) - } else { - (0.0_f64, 0.0_f64, 0, 0.0_f64, 0.0_f64, 0.0_f64, 0.0_f64) - } - } - - let mut to_sort_vec = Vec::new(); - for pid in pids_to_explore { - if let Some(process) = pid_process_mapping.get(&pid) { - to_sort_vec.push((pid, *process)); - } - } - sort_vec(&mut to_sort_vec, sorting_type, is_sort_descending); - pids_to_explore = to_sort_vec.iter().map(|(pid, _proc)| *pid).collect(); - - while let Some(current_pid) = pids_to_explore.pop_front() { - if !prune_disabled_pids(current_pid, &mut parent_child_mapping, &pid_process_mapping) { - sort_remaining_pids( - current_pid, - sorting_type, - is_sort_descending, - &mut parent_child_mapping, - &pid_process_mapping, - ); - - let (pid_res, branch_res) = - build_explored_pids(current_pid, &parent_child_mapping, "", &collapsed_set); - lines.push(String::default()); - lines.extend(branch_res); - explored_pids.extend(pid_res); - } - } - - // Now let's "rearrange" our current list of converted process data into the correct - // order required... and we're done! - explored_pids - .iter() - .zip(lines) - .filter_map(|(pid, prefix)| match pid_process_mapping.get(pid) { - Some(process) => { - let mut p = (*process).clone(); - p.process_description_prefix = Some(format!( - "{}{}{}", - prefix, - if p.is_collapsed_entry { "+ " } else { "" }, // I do the + sign thing here because I'm kinda too lazy to do it in the prefix, tbh. - if is_using_command { - &p.command - } else { - &p.name - } - )); - - // As part of https://github.com/ClementTsang/bottom/issues/424, also append their statistics to the parent if - // collapsed. - // - // Note that this will technically be "missing" entries, it collapses + sums based on what is visible - // since this runs *after* pruning steps. - if p.is_collapsed_entry { - if let Some(children) = parent_child_mapping.get(&p.pid) { - // Do some rounding. - p.cpu_percent_usage = (p.cpu_percent_usage * 10.0).round() / 10.0; - p.mem_percent_usage = (p.mem_percent_usage * 10.0).round() / 10.0; - p.rps_f64 = (p.rps_f64 * 10.0).round() / 10.0; - p.wps_f64 = (p.wps_f64 * 10.0).round() / 10.0; - p.tr_f64 = (p.tr_f64 * 10.0).round() / 10.0; - p.tw_f64 = (p.tw_f64 * 10.0).round() / 10.0; - - for &child_pid in children { - // Let's just do a simple DFS traversal... - let ( - child_cpu, - child_mem_percent, - child_mem, - child_rps, - child_wps, - child_total_read, - child_total_write, - ) = get_usage_of_all_children( - child_pid, - &parent_child_mapping, - &pid_process_mapping, - ); - - p.cpu_percent_usage += child_cpu; - p.mem_percent_usage += child_mem_percent; - p.mem_usage_bytes += child_mem; - p.rps_f64 += child_rps; - p.wps_f64 += child_wps; - p.tr_f64 += child_total_read; - p.tw_f64 += child_total_write; - } - - let disk_io_strings = get_disk_io_strings( - p.rps_f64 as u64, - p.wps_f64 as u64, - p.tr_f64 as u64, - p.tw_f64 as u64, - ); - - p.mem_usage_str = get_binary_bytes(p.mem_usage_bytes); - - p.read_per_sec = disk_io_strings.0; - p.write_per_sec = disk_io_strings.1; - p.total_read = disk_io_strings.2; - p.total_write = disk_io_strings.3; - } - } - - Some(p) - } - None => None, - }) - .collect::<Vec<_>>() -} - -// FIXME: [OPT] This is an easy target for optimization, too many to_strings! -pub fn stringify_process_data( - proc_widget_state: &ProcWidgetState, finalized_process_data: &[ConvertedProcessData], -) -> Vec<(Vec<(String, Option<String>)>, bool)> { - let is_proc_widget_grouped = proc_widget_state.is_grouped; - let is_using_command = proc_widget_state.is_using_command; - let is_tree = proc_widget_state.is_tree_mode; - let mem_enabled = proc_widget_state.columns.is_enabled(&ProcessSorting::Mem); - - finalized_process_data - .iter() - .map(|process| { - ( - vec![ - ( - if is_proc_widget_grouped { - process.group_pids.len().to_string() - } else { - process.pid.to_string() - }, - None, - ), - ( - if is_tree { - if let Some(prefix) = &process.process_description_prefix { - prefix.clone() - } else { - String::default() - } - } else if is_using_command { - process.command.clone() - } else { - process.name.clone() - }, - None, - ), - (format!("{:.1}%", process.cpu_percent_usage), None), - ( - if mem_enabled { - if process.mem_usage_bytes <= GIBI_LIMIT { - format!("{:.0}{}", process.mem_usage_str.0, process.mem_usage_str.1) - } else { - format!("{:.1}{}", process.mem_usage_str.0, process.mem_usage_str.1) - } - } else { - format!("{:.1}%", process.mem_percent_usage) - }, - None, - ), - (process.read_per_sec.clone(), None), - (process.write_per_sec.clone(), None), - (process.total_read.clone(), None), - (process.total_write.clone(), None), - #[cfg(target_family = "unix")] - ( - if let Some(user) = &process.user { - user.clone() - } else { - "N/A".to_string() - }, - None, - ), - ( - process.process_state.clone(), - Some(process.process_char.to_string()), - ), - ], - process.is_disabled_entry, - ) - }) - .collect() -} - -/// Takes a set of converted process data and groups it together. -/// -/// To be honest, I really don't like how this is done, even though I've rewritten this like 3 times. -pub fn group_process_data( - single_process_data: &[ConvertedProcessData], is_using_command: bool, -) -> Vec<ConvertedProcessData> { - #[derive(Clone, Default, Debug)] - struct SingleProcessData { - pub pid: Pid, - pub cpu_percent_usage: f64, - pub mem_percent_usage: f64, - pub mem_usage_bytes: u64, - pub group_pids: Vec<Pid>, - pub read_per_sec: f64, - pub write_per_sec: f64, - pub total_read: f64, - pub total_write: f64, - pub process_state: String, - } - - let mut grouped_hashmap: HashMap<String, SingleProcessData> = std::collections::HashMap::new(); - - single_process_data.iter().for_each(|process| { - let entry = grouped_hashmap - .entry(if is_using_command { - process.command.to_string() - } else { - process.name.to_string() - }) - .or_insert(SingleProcessData { - pid: process.pid, - ..SingleProcessData::default() - }); - - (*entry).cpu_percent_usage += process.cpu_percent_usage; - (*entry).mem_percent_usage += process.mem_percent_usage; - (*entry).mem_usage_bytes += process.mem_usage_bytes; - (*entry).group_pids.push(process.pid); - (*entry).read_per_sec += process.rps_f64; - (*entry).write_per_sec += process.wps_f64; - (*entry).total_read += process.tr_f64; - (*entry).total_write += process.tw_f64; - }); - - grouped_hashmap - .iter() - .map(|(identifier, process_details)| { - let p = process_details.clone(); - - let (read_per_sec, write_per_sec, total_read, total_write) = get_disk_io_strings( - p.read_per_sec as u64, - p.write_per_sec as u64, - p.total_read as u64, - p.total_write as u64, - ); - - ConvertedProcessData { - pid: p.pid, - ppid: None, - is_thread: None, - name: identifier.to_string(), - command: identifier.to_string(), - cpu_percent_usage: p.cpu_percent_usage, - mem_percent_usage: p.mem_percent_usage, - mem_usage_bytes: p.mem_usage_bytes, - mem_usage_str: get_decimal_bytes(p.mem_usage_bytes), - group_pids: p.group_pids, - read_per_sec, - write_per_sec, - total_read, - total_write, - rps_f64: p.read_per_sec, - wps_f64: p.write_per_sec, - tr_f64: p.total_read, - tw_f64: p.total_write, - process_state: p.process_state, - process_description_prefix: None, - process_char: char::default(), - is_disabled_entry: false, - is_collapsed_entry: false, - user: None, - } - }) - .collect::<Vec<_>>() } #[cfg(feature = "battery")] @@ -1410,3 +642,68 @@ pub fn convert_battery_harvest( }) .collect() } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_binary_byte_string() { + assert_eq!(binary_byte_string(0), "0B".to_string()); + assert_eq!(binary_byte_string(1), "1B".to_string()); + assert_eq!(binary_byte_string(1000), "1000B".to_string()); + assert_eq!(binary_byte_string(1023), "1023B".to_string()); + assert_eq!(binary_byte_string(KIBI_LIMIT), "1KiB".to_string()); + assert_eq!(binary_byte_string(KIBI_LIMIT + 1), "1KiB".to_string()); + assert_eq!(binary_byte_string(MEBI_LIMIT), "1MiB".to_string()); + assert_eq!(binary_byte_string(GIBI_LIMIT), "1.0GiB".to_string()); + assert_eq!(binary_byte_string(2 * GIBI_LIMIT), "2.0GiB".to_string()); + assert_eq!( + binary_byte_string((2.5 * GIBI_LIMIT as f64) as u64), + "2.5GiB".to_string() + ); + assert_eq!( + binary_byte_string((10.34 * TEBI_LIMIT as f64) as u64), + "10.3TiB".to_string() + ); + assert_eq!( + binary_byte_string((10.36 * TEBI_LIMIT as f64) as u64), + "10.4TiB".to_string() + ); + } + + #[test] + fn test_dec_bytes_per_second_string() { + assert_eq!(dec_bytes_per_second_string(0), "0B/s".to_string()); + assert_eq!(dec_bytes_per_second_string(1), "1B/s".to_string()); + assert_eq!(dec_bytes_per_second_string(900), "900B/s".to_string()); + assert_eq!(dec_bytes_per_second_string(999), "999B/s".to_string()); + assert_eq!(dec_bytes_per_second_string(KILO_LIMIT), "1KB/s".to_string()); + assert_eq!( + dec_bytes_per_second_string(KILO_LIMIT + 1), + "1KB/s".to_string() + ); + assert_eq!(dec_bytes_per_second_string(KIBI_LIMIT), "1KB/s".to_string()); + assert_eq!(dec_bytes_per_second_string(MEGA_LIMIT), "1MB/s".to_string()); + assert_eq!( + dec_bytes_per_second_string(GIGA_LIMIT), + "1.0GB/s".to_string() + ); + assert_eq!( + dec_bytes_per_second_string(2 * GIGA_LIMIT), + "2.0GB/s".to_string() + ); + assert_eq!( + dec_bytes_per_second_string((2.5 * GIGA_LIMIT as f64) as u64), + "2.5GB/s".to_string() + ); + assert_eq!( + dec_bytes_per_second_string((10.34 * TERA_LIMIT as f64) as u64), + "10.3TB/s".to_string() + ); + assert_eq!( + dec_bytes_per_second_string((10.36 * TERA_LIMIT as f64) as u64), + "10.4TB/s".to_string() + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index 4ae7ed07..735ace77 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,7 +27,7 @@ use crossterm::{ }; use app::{ - data_harvester::{self, processes::ProcessSorting}, + data_harvester, layout_manager::{UsedWidgets, WidgetDirection}, App, }; @@ -302,295 +302,40 @@ pub fn panic_hook(panic_info: &PanicInfo<'_>) { .unwrap(); } -pub fn handle_force_redraws(app: &mut App) { - // Currently we use an Option... because we might want to future-proof this - // if we eventually get widget-specific redrawing! - if app.proc_state.force_update_all { - update_all_process_lists(app); - app.proc_state.force_update_all = false; - } else if let Some(widget_id) = app.proc_state.force_update { - update_final_process_list(app, widget_id); - app.proc_state.force_update = None; +pub fn update_data(app: &mut App) { + for proc in app.proc_state.widget_states.values_mut() { + if proc.force_update_data { + proc.update_displayed_process_data(&app.data_collection); + proc.force_update_data = false; + } } if app.cpu_state.force_update.is_some() { - convert_cpu_data_points( - &app.data_collection, - &mut app.canvas_data.cpu_data, - app.is_frozen, - ); - app.canvas_data.load_avg_data = app.data_collection.load_avg_harvest; + convert_cpu_data_points(&app.data_collection, &mut app.converted_data.cpu_data); + app.converted_data.load_avg_data = app.data_collection.load_avg_harvest; app.cpu_state.force_update = None; } - // FIXME: [OPT] Prefer reassignment over new vectors? + // TODO: [OPT] Prefer reassignment over new vectors? if app.mem_state.force_update.is_some() { - app.canvas_data.mem_data = convert_mem_data_points(&app.data_collection, app.is_frozen); - app.canvas_data.swap_data = convert_swap_data_points(&app.data_collection, app.is_frozen); + app.converted_data.mem_data = convert_mem_data_points(&app.data_collection); + app.converted_data.swap_data = convert_swap_data_points(&app.data_collection); app.mem_state.force_update = None; } if app.net_state.force_update.is_some() { let (rx, tx) = get_rx_tx_data_points( &app.data_collection, - app.is_frozen, &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 = rx; - app.canvas_data.network_data_tx = tx; + app.converted_data.network_data_rx = rx; + app.converted_data.network_data_tx = tx; app.net_state.force_update = None; } } -#[allow(clippy::needless_collect)] -pub fn update_all_process_lists(app: &mut App) { - // According to clippy, I can avoid a collect... but if I follow it, - // I end up conflicting with the borrow checker since app is used within the closure... hm. - if !app.is_frozen { - let widget_ids = app - .proc_state - .widget_states - .keys() - .cloned() - .collect::<Vec<_>>(); - - widget_ids.into_iter().for_each(|widget_id| { - update_final_process_list(app, widget_id); - }); - } -} - -fn update_final_process_list(app: &mut App, widget_id: u64) { - let process_states = app - .proc_state - .widget_states - .get(&widget_id) - .map(|process_state| { - ( - process_state - .process_search_state - .search_state - .is_invalid_or_blank_search(), - process_state.is_using_command, - process_state.is_grouped, - process_state.is_tree_mode, - ) - }); - - if let Some((is_invalid_or_blank, is_using_command, is_grouped, is_tree)) = process_states { - if !app.is_frozen { - convert_process_data( - &app.data_collection, - &mut app.canvas_data.single_process_data, - #[cfg(target_family = "unix")] - &mut app.user_table, - ); - } - let process_filter = app.get_process_filter(widget_id); - let filtered_process_data: Vec<ConvertedProcessData> = if is_tree { - app.canvas_data - .single_process_data - .iter() - .map(|(_pid, process)| { - let mut process_clone = process.clone(); - if !is_invalid_or_blank { - if let Some(process_filter) = process_filter { - process_clone.is_disabled_entry = - !process_filter.check(&process_clone, is_using_command); - } - } - process_clone - }) - .collect::<Vec<_>>() - } else { - app.canvas_data - .single_process_data - .iter() - .filter_map(|(_pid, process)| { - if !is_invalid_or_blank { - if let Some(process_filter) = process_filter { - if process_filter.check(process, is_using_command) { - Some(process) - } else { - None - } - } else { - Some(process) - } - } else { - Some(process) - } - }) - .cloned() - .collect::<Vec<_>>() - }; - - if let Some(proc_widget_state) = app.proc_state.get_mut_widget_state(widget_id) { - let mut finalized_process_data = if is_tree { - tree_process_data( - &filtered_process_data, - is_using_command, - &proc_widget_state.process_sorting_type, - proc_widget_state.is_process_sort_descending, - ) - } else if is_grouped { - group_process_data(&filtered_process_data, is_using_command) - } else { - filtered_process_data - }; - - // Note tree mode is sorted well before this, as it's special. - if !is_tree { - sort_process_data(&mut finalized_process_data, proc_widget_state); - } - - if proc_widget_state.scroll_state.current_scroll_position - >= finalized_process_data.len() - { - proc_widget_state.scroll_state.current_scroll_position = - finalized_process_data.len().saturating_sub(1); - proc_widget_state.scroll_state.scroll_bar = 0; - proc_widget_state.scroll_state.scroll_direction = app::ScrollDirection::Down; - } - - app.canvas_data.stringified_process_data_map.insert( - widget_id, - stringify_process_data(proc_widget_state, &finalized_process_data), - ); - app.canvas_data - .finalized_process_data_map - .insert(widget_id, finalized_process_data); - } - } -} - -fn sort_process_data( - to_sort_vec: &mut [ConvertedProcessData], proc_widget_state: &app::ProcWidgetState, -) { - to_sort_vec.sort_by_cached_key(|c| c.name.to_lowercase()); - - match &proc_widget_state.process_sorting_type { - ProcessSorting::CpuPercent => { - to_sort_vec.sort_by(|a, b| { - utils::gen_util::get_ordering( - a.cpu_percent_usage, - b.cpu_percent_usage, - proc_widget_state.is_process_sort_descending, - ) - }); - } - ProcessSorting::Mem => { - to_sort_vec.sort_by(|a, b| { - utils::gen_util::get_ordering( - a.mem_usage_bytes, - b.mem_usage_bytes, - proc_widget_state.is_process_sort_descending, - ) - }); - } - ProcessSorting::MemPercent => { - to_sort_vec.sort_by(|a, b| { - utils::gen_util::get_ordering( - a.mem_percent_usage, - b.mem_percent_usage, - proc_widget_state.is_process_sort_descending, - ) - }); - } - ProcessSorting::ProcessName => { - // Don't repeat if false... it sorts by name by default anyways. - if proc_widget_state.is_process_sort_descending { - to_sort_vec.sort_by_cached_key(|c| c.name.to_lowercase()); - if proc_widget_state.is_process_sort_descending { - to_sort_vec.reverse(); - } - } - } - ProcessSorting::Command => { - to_sort_vec.sort_by_cached_key(|c| c.command.to_lowercase()); - if proc_widget_state.is_process_sort_descending { - to_sort_vec.reverse(); - } - } - ProcessSorting::Pid => { - if !proc_widget_state.is_grouped { - to_sort_vec.sort_by(|a, b| { - utils::gen_util::get_ordering( - a.pid, - b.pid, - proc_widget_state.is_process_sort_descending, - ) - }); - } - } - ProcessSorting::ReadPerSecond => { - to_sort_vec.sort_by(|a, b| { - utils::gen_util::get_ordering( - a.rps_f64, - b.rps_f64, - proc_widget_state.is_process_sort_descending, - ) - }); - } - ProcessSorting::WritePerSecond => { - to_sort_vec.sort_by(|a, b| { - utils::gen_util::get_ordering( - a.wps_f64, - b.wps_f64, - proc_widget_state.is_process_sort_descending, - ) - }); - } - ProcessSorting::TotalRead => { - to_sort_vec.sort_by(|a, b| { - utils::gen_util::get_ordering( - a.tr_f64, - b.tr_f64, - proc_widget_state.is_process_sort_descending, - ) - }); - } - ProcessSorting::TotalWrite => { - to_sort_vec.sort_by(|a, b| { - utils::gen_util::get_ordering( - a.tw_f64, - b.tw_f64, - proc_widget_state.is_process_sort_descending, - ) - }); - } - ProcessSorting::State => { - to_sort_vec.sort_by_cached_key(|c| c.process_state.to_lowercase()); - if proc_widget_state.is_process_sort_descending { - to_sort_vec.reverse(); - } - } - ProcessSorting::User => to_sort_vec.sort_by(|a, b| match (&a.user, &b.user) { - (Some(user_a), Some(user_b)) => utils::gen_util::get_ordering( - user_a.to_lowercase(), - user_b.to_lowercase(), - proc_widget_state.is_process_sort_descending, - ), - (Some(_), None) => std::cmp::Ordering::Less, - (None, Some(_)) => std::cmp::Ordering::Greater, - (None, None) => std::cmp::Ordering::Less, - }), - ProcessSorting::Count => { - if proc_widget_state.is_grouped { - to_sort_vec.sort_by(|a, b| { - utils::gen_util::get_ordering( - a.group_pids.len(), - b.group_pids.len(), - proc_widget_state.is_process_sort_descending, - ) - }); - } - } - } -} - pub fn create_input_thread( sender: std::sync::mpsc::Sender< BottomEvent<crossterm::event::KeyEvent, crossterm::event::MouseEvent>, @@ -651,7 +396,7 @@ pub fn create_collection_thread( thread::spawn(move || { let mut data_state = data_harvester::DataCollector::new(filters); - data_state.set_collected_data(used_widget_set); + data_state.set_data_collection(used_widget_set); data_state.set_temperature_type(temp_type); data_state.set_use_current_cpu_total(use_current_cpu_total); data_state.set_show_average_cpu(show_average_cpu); @@ -682,7 +427,7 @@ pub fn create_collection_thread( data_state.set_show_average_cpu(app_config_fields.show_average_cpu); } ThreadControlEvent::UpdateUsedWidgets(used_widget_set) => { - data_state.set_collected_data(*used_widget_set); + data_state.set_data_collection(*used_widget_set); } ThreadControlEvent::UpdateUpdateTime(new_time) => { update_time = new_time; diff --git a/src/options.rs b/src/options.rs index 1bebd502..2ff82be9 100644 --- a/src/options.rs +++ b/src/options.rs @@ -10,7 +10,11 @@ use std::{ }; use crate::{ - app::{layout_manager::*, *}, + app::{ + layout_manager::*, + widgets::{ProcWidget, ProcWidgetMode}, + *, + }, canvas::ColourScheme, constants::*, units::data_units::DataUnit, @@ -265,7 +269,7 @@ pub fn build_app( let mut cpu_state_map: HashMap<u64, CpuWidgetState> = HashMap::new(); let mut mem_state_map: HashMap<u64, MemWidgetState> = HashMap::new(); let mut net_state_map: HashMap<u64, NetWidgetState> = HashMap::new(); - let mut proc_state_map: HashMap<u64, ProcWidgetState> = HashMap::new(); + let mut proc_state_map: HashMap<u64, ProcWidget> = HashMap::new(); let mut temp_state_map: HashMap<u64, TempWidgetState> = HashMap::new(); let mut disk_state_map: HashMap<u64, DiskWidgetState> = HashMap::new(); let mut battery_state_map: HashMap<u64, BatteryWidgetState> = HashMap::new(); @@ -344,33 +348,37 @@ pub fn build_app( Net => { net_state_map.insert( widget.widget_id, - NetWidgetState::init( - default_time_value, - autohide_timer, - // network_unit_type.clone(), - // network_scale_type.clone(), - ), + NetWidgetState::init(default_time_value, autohide_timer), ); } Proc => { + let mode = if is_grouped { + ProcWidgetMode::Grouped + } else if is_default_tree { + ProcWidgetMode::Tree { + collapsed_pids: Default::default(), + } + } else { + ProcWidgetMode::Normal + }; + proc_state_map.insert( widget.widget_id, - ProcWidgetState::init( + ProcWidget::init( + mode, is_case_sensitive, is_match_whole_word, is_use_regex, - is_grouped, show_memory_as_values, - is_default_tree, is_default_command, ), ); } Disk => { - disk_state_map.insert(widget.widget_id, DiskWidgetState::init()); + disk_state_map.insert(widget.widget_id, DiskWidgetState::default()); } Temp => { - temp_state_map.insert(widget.widget_id, TempWidgetState::init()); + temp_state_map.insert(widget.widget_id, TempWidgetState::default()); } Battery => { battery_state_map @@ -466,7 +474,7 @@ pub fn build_app( let mapping = HashMap::new(); for widget in search_case_enabled_widgets { if let Some(proc_widget) = proc_state_map.get_mut(&widget.id) { - proc_widget.process_search_state.is_ignoring_case = !widget.enabled; + proc_widget.proc_search.is_ignoring_case = !widget.enabled; } } flags.search_case_enabled_widgets_map = Some(mapping); @@ -480,7 +488,7 @@ pub fn build_app( let mapping = HashMap::new(); for widget in search_whole_word_enabled_widgets { if let Some(proc_widget) = proc_state_map.get_mut(&widget.id) { - proc_widget.process_search_state.is_searching_whole_word = widget.enabled; + proc_widget.proc_search.is_searching_whole_word = widget.enabled; } } flags.search_whole_word_enabled_widgets_map = Some(mapping); @@ -492,7 +500,7 @@ pub fn build_app( let mapping = HashMap::new(); for widget in search_regex_enabled_widgets { if let Some(proc_widget) = proc_state_map.get_mut(&widget.id) { - proc_widget.process_search_state.is_searching_with_regex = widget.enabled; + proc_widget.proc_search.is_searching_with_regex = widget.enabled; } } flags.search_regex_enabled_widgets_map = Some(mapping); diff --git a/src/utils/gen_util.rs b/src/utils/gen_util.rs index e0265290..c91a6852 100644 --- a/src/utils/gen_util.rs +++ b/src/utils/gen_util.rs @@ -92,30 +92,50 @@ pub fn get_decimal_prefix(quantity: u64, unit: &str) -> (f64, String) { } } -/// Gotta get partial ordering? No problem, here's something to deal with it~ -/// -/// Note that https://github.com/reem/rust-ordered-float exists, maybe move to it one day? IDK. -pub fn get_ordering<T: std::cmp::PartialOrd>( - a_val: T, b_val: T, reverse_order: bool, -) -> std::cmp::Ordering { - match a_val.partial_cmp(&b_val) { - Some(x) => match x { - Ordering::Greater => { - if reverse_order { - std::cmp::Ordering::Less - } else { - std::cmp::Ordering::Greater - } - } - Ordering::Less => { - if reverse_order { - std::cmp::Ordering::Greater - } else { - std::cmp::Ordering::Less - } - } - Ordering::Equal => Ordering::Equal, - }, - None => Ordering::Equal, +#[inline] +pub fn sort_partial_fn<T: std::cmp::PartialOrd>(is_reverse: bool) -> fn(T, T) -> Ordering { + if is_reverse { + partial_ordering_rev + } else { + partial_ordering + } +} + +/// Returns an [`Ordering`] between two [`PartialOrd`]s. +#[inline] +pub fn partial_ordering<T: std::cmp::PartialOrd>(a: T, b: T) -> Ordering { + // TODO: Switch to `total_cmp` on 1.62 + a.partial_cmp(&b).unwrap_or(Ordering::Equal) +} + +/// Returns a reversed [`Ordering`] between two [`PartialOrd`]s. +/// +/// This is simply a wrapper function around [`partial_ordering`] that reverses +/// the result. +#[inline] +pub fn partial_ordering_rev<T: std::cmp::PartialOrd>(a: T, b: T) -> Ordering { + partial_ordering(a, b).reverse() +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_sort_partial_fn() { + let mut x = vec![9, 5, 20, 15, 10, 5]; + let mut y = vec![1.0, 15.0, -1.0, -100.0, -100.1, 16.15, -100.0]; + + x.sort_by(|a, b| sort_partial_fn(false)(a, b)); + assert_eq!(x, vec![5, 5, 9, 10, 15, 20]); + + x.sort_by(|a, b| sort_partial_fn(true)(a, b)); + assert_eq!(x, vec![20, 15, 10, 9, 5, 5]); + + y.sort_by(|a, b| sort_partial_fn(false)(a, b)); + assert_eq!(y, vec![-100.1, -100.0, -100.0, -1.0, 1.0, 15.0, 16.15]); + + y.sort_by(|a, b| sort_partial_fn(true)(a, b)); + assert_eq!(y, vec![16.15, 15.0, 1.0, -1.0, -100.0, -100.0, -100.1]); } }