diff --git a/Cargo.toml b/Cargo.toml index b7b7b5fe..f6938416 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 47e25d49..34acc539 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, }; @@ -32,6 +31,7 @@ pub mod layout_manager; mod process_killer; pub mod query; pub mod states; +pub mod widgets; const MAX_SEARCH_LENGTH: usize = 200; @@ -127,9 +127,6 @@ 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, @@ -172,9 +169,8 @@ impl App { .widget_states .values_mut() .for_each(|state| { - state.process_search_state.search_state.reset(); + state.search_state.search_state.reset(); }); - self.proc_state.force_update_all = true; // Clear current delete list self.to_delete_process_list = None; @@ -224,10 +220,7 @@ impl App { { 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.search_state.search_state.is_enabled = false; current_proc_state.is_sort_open = false; self.is_force_redraw = true; return; @@ -240,10 +233,7 @@ impl App { .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; + current_proc_state.search_state.search_state.is_enabled = false; self.move_widget_selection(&WidgetDirection::Up); self.is_force_redraw = true; return; @@ -256,8 +246,6 @@ impl App { .get_mut_widget_state(self.current_widget.widget_id - 2) { if current_proc_state.is_sort_open { - current_proc_state.columns.current_scroll_position = - current_proc_state.columns.backup_prev_scroll_position; current_proc_state.is_sort_open = false; self.move_widget_selection(&WidgetDirection::Right); self.is_force_redraw = true; @@ -314,53 +302,55 @@ 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); + todo!() + // FIXME: [Proc] Fix this. + // // 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; - } + // // 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.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), - ); + // 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), - ); + // #[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 + // .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.requires_redraw = true; + // self.proc_state.force_update = Some(self.current_widget.widget_id); + // } } } _ => {} @@ -368,16 +358,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 +370,7 @@ impl App { _ => 0, }, ) { - proc_widget_state - .process_search_state - .search_state - .is_enabled = true; + proc_widget_state.search_state.search_state.is_enabled = true; self.move_widget_selection(&WidgetDirection::Down); self.is_force_redraw = true; } @@ -404,37 +381,22 @@ 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(proc_widget_state) = self.proc_state.get_mut_widget_state(widget_id) { + proc_widget_state.is_sort_open = !proc_widget_state.is_sort_open; - 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); - } - } - } - - self.is_force_redraw = true; + // If the sort is now open, move left. Otherwise, if the proc sort was selected, force move right. + if proc_widget_state.is_sort_open { + self.move_widget_selection(&WidgetDirection::Left); + } else if let BottomWidgetType::ProcSort = self.current_widget.widget_type { + self.move_widget_selection(&WidgetDirection::Right); } - _ => {} } } @@ -452,7 +414,7 @@ impl App { proc_widget_state.is_process_sort_descending = !proc_widget_state.is_process_sort_descending; - self.proc_state.force_update = Some(widget_id); + proc_widget_state.force_update = true; } } _ => {} @@ -470,30 +432,10 @@ 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; - } - } - } + // FIXME: [Proc]Handle this! + todo!(); - proc_widget_state.requires_redraw = true; - self.proc_state.force_update = Some(self.current_widget.widget_id); + proc_widget_state.force_update = true; } } _ => {} @@ -509,14 +451,12 @@ 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.search_state.search_toggle_ignore_case(); proc_widget_state.update_query(); - self.proc_state.force_update = Some(self.current_widget.widget_id - 1); + proc_widget_state.force_update = true; // 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.search_state.is_ignoring_case); } } @@ -550,8 +490,6 @@ impl App { .build(), ); } - - // self.did_config_fail_to_save = self.update_config_file().is_err(); } } @@ -564,17 +502,12 @@ 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.search_state.search_toggle_whole_word(); proc_widget_state.update_query(); - self.proc_state.force_update = Some(self.current_widget.widget_id - 1); + proc_widget_state.force_update = true; - is_searching_whole_word = Some( - proc_widget_state - .process_search_state - .is_searching_whole_word, - ); + is_searching_whole_word = + Some(proc_widget_state.search_state.is_searching_whole_word); } } @@ -624,15 +557,12 @@ 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.search_state.search_toggle_regex(); proc_widget_state.update_query(); - self.proc_state.force_update = Some(self.current_widget.widget_id - 1); + proc_widget_state.force_update = true; - is_searching_with_regex = Some( - proc_widget_state - .process_search_state - .is_searching_with_regex, - ); + is_searching_with_regex = + Some(proc_widget_state.search_state.is_searching_with_regex); } } @@ -666,8 +596,6 @@ impl App { .build(), ); } - - // self.did_config_fail_to_save = self.update_config_file().is_err(); } } @@ -677,37 +605,37 @@ impl App { .widget_states .get_mut(&(self.current_widget.widget_id)) { - proc_widget_state.is_tree_mode = !proc_widget_state.is_tree_mode; + // 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; + // // 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); + // proc_widget_state + // .columns + // .try_enable(&processes::ProcessSorting::State); - #[cfg(target_family = "unix")] - proc_widget_state - .columns - .try_enable(&processes::ProcessSorting::User); + // #[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_disable(&processes::ProcessSorting::Count); - proc_widget_state - .columns - .try_enable(&processes::ProcessSorting::Pid); + // 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; - } + // // 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; + // } - self.proc_state.force_update = Some(self.current_widget.widget_id); - proc_widget_state.requires_redraw = true; + // self.proc_state.force_update = Some(self.current_widget.widget_id); + // proc_widget_state.requires_redraw = true; } } @@ -744,9 +672,8 @@ 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(); + // TODO: [Proc] Handle this + proc_widget_state.force_update = true; } } } @@ -761,13 +688,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.search_state.search_state.is_enabled && proc_widget_state.get_search_cursor_position() < proc_widget_state - .process_search_state + .search_state .search_state .current_search_query .len() @@ -777,27 +701,25 @@ impl App { .search_walk_forward(proc_widget_state.get_search_cursor_position()); let _removed_chars: String = proc_widget_state - .process_search_state + .search_state .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.search_state.search_state.grapheme_cursor = + GraphemeCursor::new( + current_cursor, + proc_widget_state + .search_state + .search_state + .current_search_query + .len(), + true, + ); proc_widget_state.update_query(); - self.proc_state.force_update = Some(self.current_widget.widget_id - 1); + proc_widget_state.force_update = true; } } else { self.start_killing_process() @@ -815,10 +737,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.search_state.search_state.is_enabled && proc_widget_state.get_search_cursor_position() > 0 { let current_cursor = proc_widget_state.get_search_cursor_position(); @@ -826,37 +745,33 @@ impl App { .search_walk_back(proc_widget_state.get_search_cursor_position()); let removed_chars: String = proc_widget_state - .process_search_state + .search_state .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.search_state.search_state.grapheme_cursor = + GraphemeCursor::new( + proc_widget_state.get_search_cursor_position(), + proc_widget_state + .search_state + .search_state + .current_search_query + .len(), + true, + ); proc_widget_state - .process_search_state + .search_state .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.search_state.search_state.cursor_direction = + CursorDirection::Left; proc_widget_state.update_query(); - self.proc_state.force_update = Some(self.current_widget.widget_id - 1); + proc_widget_state.force_update = true; } } } @@ -864,7 +779,7 @@ impl App { pub fn get_process_filter(&self, widget_id: u64) -> &Option { if let Some(process_widget_state) = self.proc_state.widget_states.get(&widget_id) { - &process_widget_state.process_search_state.search_state.query + &process_widget_state.search_state.search_state.query } else { &None } @@ -961,18 +876,16 @@ 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 + .search_state .search_state .current_search_query [proc_widget_state.get_search_cursor_position()..prev_cursor]; proc_widget_state - .process_search_state + .search_state .search_state .char_cursor_position -= UnicodeWidthStr::width(str_slice); - proc_widget_state - .process_search_state - .search_state - .cursor_direction = CursorDirection::Left; + proc_widget_state.search_state.search_state.cursor_direction = + CursorDirection::Left; } } } @@ -1033,18 +946,16 @@ impl App { ); if proc_widget_state.get_search_cursor_position() > prev_cursor { let str_slice = &proc_widget_state - .process_search_state + .search_state .search_state .current_search_query [prev_cursor..proc_widget_state.get_search_cursor_position()]; proc_widget_state - .process_search_state + .search_state .search_state .char_cursor_position += UnicodeWidthStr::width(str_slice); - proc_widget_state - .process_search_state - .search_state - .cursor_direction = CursorDirection::Right; + proc_widget_state.search_state.search_state.cursor_direction = + CursorDirection::Right; } } } @@ -1151,26 +1062,22 @@ impl App { .get_mut(&(self.current_widget.widget_id - 1)) { if is_in_search_widget { + proc_widget_state.search_state.search_state.grapheme_cursor = + GraphemeCursor::new( + 0, + proc_widget_state + .search_state + .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 .search_state .char_cursor_position = 0; - proc_widget_state - .process_search_state - .search_state - .cursor_direction = CursorDirection::Left; + proc_widget_state.search_state.search_state.cursor_direction = + CursorDirection::Left; } } } @@ -1187,36 +1094,32 @@ impl App { .get_mut(&(self.current_widget.widget_id - 1)) { if is_in_search_widget { + proc_widget_state.search_state.search_state.grapheme_cursor = + GraphemeCursor::new( + proc_widget_state + .search_state + .search_state + .current_search_query + .len(), + proc_widget_state + .search_state + .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 .search_state .char_cursor_position = UnicodeWidthStr::width( proc_widget_state - .process_search_state + .search_state .search_state .current_search_query .as_str(), ); - proc_widget_state - .process_search_state - .search_state - .cursor_direction = CursorDirection::Right; + proc_widget_state.search_state.search_state.cursor_direction = + CursorDirection::Right; } } } @@ -1231,7 +1134,7 @@ 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); + proc_widget_state.force_update = true; } } } @@ -1271,19 +1174,16 @@ impl App { } let removed_chars: String = proc_widget_state - .process_search_state + .search_state .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.search_state.search_state.grapheme_cursor = GraphemeCursor::new( start_index, proc_widget_state - .process_search_state + .search_state .search_state .current_search_query .len(), @@ -1291,23 +1191,15 @@ impl App { ); proc_widget_state - .process_search_state + .search_state .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.search_state.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); + proc_widget_state.force_update = true; } } } @@ -1325,28 +1217,30 @@ impl App { .finalized_process_data_map .get(&self.current_widget.widget_id) { - if proc_widget_state.scroll_state.current_scroll_position + if proc_widget_state.table_state.current_scroll_position < corresponding_filtered_process_list.len() { - let current_process: (String, Vec); - if self.is_grouped(self.current_widget.widget_id) { - if let Some(process) = &corresponding_filtered_process_list - .get(proc_widget_state.scroll_state.current_scroll_position) - { - 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]) - }; + todo!() + // FIXME: [Proc] Handle this + // let current_process: (String, Vec); + // if self.is_grouped(self.current_widget.widget_id) { + // if let Some(process) = &corresponding_filtered_process_list + // .get(proc_widget_state.table_state.current_scroll_position) + // { + // current_process = (process.name.to_string(), process.group_pids.clone()) + // } else { + // return; + // } + // } else { + // let process = corresponding_filtered_process_list + // [proc_widget_state.table_state.current_scroll_position] + // .clone(); + // current_process = (process.name.clone(), vec![process.pid]) + // }; - 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; } } } @@ -1381,45 +1275,41 @@ impl App { && proc_widget_state.is_search_enabled() && UnicodeWidthStr::width( proc_widget_state - .process_search_state + .search_state .search_state .current_search_query .as_str(), ) <= MAX_SEARCH_LENGTH { proc_widget_state - .process_search_state + .search_state .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.search_state.search_state.grapheme_cursor = + GraphemeCursor::new( + proc_widget_state.get_search_cursor_position(), + proc_widget_state + .search_state + .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 + .search_state .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.force_update = true; + proc_widget_state.search_state.search_state.cursor_direction = + CursorDirection::Right; return; } @@ -1525,20 +1415,14 @@ impl App { 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); + // FIXME: [Proc] Handle this, this should toggle the CPU sorter. + proc_widget_state.force_update = true; } } } @@ -1548,18 +1432,8 @@ 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); + // FIXME: [Proc] Handle this, this should toggle mem + proc_widget_state.force_update = true; } } } @@ -1569,14 +1443,16 @@ 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); - } + todo!() + // FIXME: [Proc], this should handle pid + // // 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); + // } } } } @@ -1586,25 +1462,26 @@ 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); + // FIXME: [Proc] Handle this, this should toggle proc name/command + // 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; - } - } - _ => {} - } + // 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.force_update = true; } } } @@ -1614,15 +1491,16 @@ 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); + // FIXME: [Proc] Handle this, this should toggle name/column selection + // 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(); + proc_widget_state.force_update = true; } } } @@ -1642,7 +1520,6 @@ impl App { 's' => self.toggle_sort(), 'I' => self.invert_sort(), '%' => self.toggle_percentages(), - ' ' => self.on_space(), _ => {} } @@ -1653,31 +1530,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 { @@ -2192,8 +2044,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 => { @@ -2201,8 +2053,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 => { @@ -2257,9 +2109,9 @@ impl App { .get(&self.current_widget.widget_id) { if !self.canvas_data.finalized_process_data_map.is_empty() { - proc_widget_state.scroll_state.current_scroll_position = + proc_widget_state.table_state.current_scroll_position = finalized_process_data.len() - 1; - proc_widget_state.scroll_state.scroll_direction = + proc_widget_state.table_state.scroll_direction = ScrollDirection::Down; } } @@ -2270,9 +2122,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 => { @@ -2353,23 +2205,10 @@ impl App { .proc_state .get_mut_widget_state(self.current_widget.widget_id - 2) { - let current_posn = proc_widget_state.columns.current_scroll_position; - let num_columns = proc_widget_state.columns.get_enabled_columns_len(); - let prop: core::result::Result = - (current_posn as i64 + num_to_change_by).try_into(); - - if let Ok(prop) = prop { - if prop < num_columns { - proc_widget_state.columns.current_scroll_position = - (current_posn as i64 + num_to_change_by) as usize; - } - - if num_to_change_by < 0 { - proc_widget_state.columns.scroll_direction = ScrollDirection::Up; - } else { - proc_widget_state.columns.scroll_direction = ScrollDirection::Down; - } - } + let num_entries = proc_widget_state.num_enabled_columns(); + proc_widget_state + .sort_table_state + .update_position(num_to_change_by, num_entries); } } @@ -2397,7 +2236,7 @@ impl App { .get(&self.current_widget.widget_id) { proc_widget_state - .scroll_state + .table_state .update_position(num_to_change_by, finalized_process_data.len()) } else { None @@ -2513,7 +2352,7 @@ impl App { .widget_states .get_mut(&self.current_widget.widget_id) { - let current_posn = proc_widget_state.scroll_state.current_scroll_position; + let current_posn = proc_widget_state.table_state.current_scroll_position; if let Some(displayed_process_list) = self .canvas_data @@ -2529,7 +2368,7 @@ impl App { .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); + proc_widget_state.force_update = true; } } } @@ -2911,28 +2750,30 @@ 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 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, - ); + // FIXME: [Proc] Handle this, tree mode code? + // let is_tree_mode = proc_widget_state.is_tree_mode; - if is_tree_mode { - if let Some(new_position) = new_position { - if previous_scroll_position == new_position { - self.toggle_collapsing_process_branch(); - } - } - } + // let new_position = self.change_process_position( + // offset_clicked_entry as i64 - visual_index as i64, + // ); + + // if is_tree_mode { + // if let Some(new_position) = new_position { + // if previous_scroll_position == new_position { + // self.toggle_collapsing_process_branch(); + // } + // } + // } } } } @@ -2942,8 +2783,10 @@ impl App { .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, @@ -3007,34 +2850,37 @@ impl App { .get_mut_widget_state(self.current_widget.widget_id) { // 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; - } - } - } - } + // FIXME: [Proc] Handle column header sorting! + + // 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; + // } + // } + // } + // } } } _ => {} diff --git a/src/app/data_harvester/processes/mod.rs b/src/app/data_harvester/processes/mod.rs index 283080b3..7dbaca1a 100644 --- a/src/app/data_harvester/processes/mod.rs +++ b/src/app/data_harvester/processes/mod.rs @@ -75,23 +75,48 @@ impl Default for ProcessSorting { #[derive(Debug, Clone, Default)] pub struct ProcessHarvest { + /// The pid of the process. pub pid: Pid, - pub parent_pid: Option, // Remember, parent_pid 0 is root... + + /// The parent PID of the process. Remember, parent_pid 0 is root. + pub parent_pid: Option, + + /// CPU usage as a percentage. pub cpu_usage_percent: f64, + + /// Memory usage as a percentage. pub mem_usage_percent: f64, + + /// Memory usage as bytes. pub mem_usage_bytes: u64, - // pub rss_kb: u64, - // pub virt_kb: u64, + + /// The name of the process. pub name: String, + + /// The exact command for the process. pub command: String, + + /// Bytes read per second. pub read_bytes_per_sec: u64, + + /// Bytes written per second. pub write_bytes_per_sec: u64, + + /// The total number of bytes read by the process. pub total_read_bytes: u64, + + /// The total number of bytes written by the process. pub total_write_bytes: u64, + + /// The current state of the process (e.g. zombie, asleep) pub process_state: String, + + /// The process state represented by a character. TODO: Merge with above as a single struct. pub process_state_char: char, - /// This is the *effective* user ID. + /// This is the *effective* user ID of the process. #[cfg(target_family = "unix")] pub uid: Option, + // pub rss_kb: u64, + // pub virt_kb: u64, } diff --git a/src/app/query.rs b/src/app/query.rs index 0f18c901..5bc1027a 100644 --- a/src/app/query.rs +++ b/src/app/query.rs @@ -1,4 +1,3 @@ -use super::ProcWidgetState; use crate::{ data_conversion::ConvertedProcessData, utils::error::{ @@ -9,6 +8,8 @@ use crate::{ use std::fmt::Debug; use std::{borrow::Cow, collections::VecDeque}; +use super::widgets::ProcWidget; + const DELIMITER_LIST: [char; 6] = ['=', '>', '<', '(', ')', '\"']; const COMPARISON_LIST: [&str; 3] = [">", "=", "<"]; const OR_LIST: [&str; 2] = ["or", "||"]; @@ -39,7 +40,7 @@ pub trait ProcessQuery { fn parse_query(&self) -> Result; } -impl ProcessQuery for ProcWidgetState { +impl ProcessQuery for ProcWidget { fn parse_query(&self) -> Result { fn process_string_to_filter(query: &mut VecDeque) -> Result { let lhs = process_or(query)?; @@ -437,9 +438,9 @@ impl ProcessQuery for ProcWidgetState { 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, + self.search_state.is_searching_whole_word, + self.search_state.is_ignoring_case, + self.search_state.is_searching_with_regex, )?; Ok(process_filter) diff --git a/src/app/states.rs b/src/app/states.rs index 74c8c409..a1952cda 100644 --- a/src/app/states.rs +++ b/src/app/states.rs @@ -1,16 +1,17 @@ -use std::{borrow::Cow, 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_conversion::CellContent, - data_harvester::processes::{self, ProcessSorting}, + data_harvester::processes::ProcessSorting, }; -use ProcessSorting::*; + +pub mod table_state; +pub use table_state::*; + +use super::widgets::ProcWidget; #[derive(Debug)] pub enum ScrollDirection { @@ -39,218 +40,6 @@ pub struct CanvasTableWidthState { pub calculated_column_widths: Vec, } -/// A bound on the width of a column. -#[derive(Clone, Copy, Debug)] -pub enum WidthBounds { - /// A width of this type is either as long as `min`, but can otherwise shrink and grow up to a point. - Soft { - /// The minimum amount before giving up and hiding. - min_width: u16, - - /// The desired, calculated width. Take this if possible as the base starting width. - desired: u16, - - /// The max width, as a percentage of the total width available. If [`None`], - /// then it can grow as desired. - max_percentage: Option, - }, - - /// A width of this type is either as long as specified, or does not appear at all. - Hard(u16), -} - -impl WidthBounds { - pub const fn soft_from_str(name: &'static str, max_percentage: Option) -> WidthBounds { - let len = name.len() as u16; - WidthBounds::Soft { - min_width: len, - desired: len, - max_percentage, - } - } - - pub const fn soft_from_str_with_alt( - name: &'static str, alt: &'static str, max_percentage: Option, - ) -> WidthBounds { - WidthBounds::Soft { - min_width: alt.len() as u16, - desired: name.len() as u16, - max_percentage, - } - } -} - -pub struct TableComponentColumn { - /// The name of the column. Displayed if possible as the header. - pub name: CellContent, - - /// A restriction on this column's width, if desired. - pub width_bounds: WidthBounds, - - /// The calculated width of the column. - pub calculated_width: u16, -} - -impl TableComponentColumn { - pub fn new(name: I, alt: Option, width_bounds: WidthBounds) -> Self - where - I: Into>, - { - Self { - name: if let Some(alt) = alt { - CellContent::HasAlt { - alt: alt.into(), - main: name.into(), - } - } else { - CellContent::Simple(name.into()) - }, - width_bounds, - calculated_width: 0, - } - } - - pub fn should_skip(&self) -> bool { - self.calculated_width == 0 - } -} - -/// [`TableComponentState`] deals with fields for a scrollable's current state. -#[derive(Default)] -pub struct TableComponentState { - pub current_scroll_position: usize, - pub scroll_bar: usize, - pub scroll_direction: ScrollDirection, - pub table_state: TableState, - pub columns: Vec, -} - -impl TableComponentState { - pub fn new(columns: Vec) -> Self { - Self { - current_scroll_position: 0, - scroll_bar: 0, - scroll_direction: ScrollDirection::Down, - table_state: Default::default(), - columns, - } - } - - /// 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; - - for column in self.columns.iter_mut() { - column.calculated_width = 0; - } - - let columns = if left_to_right { - Either::Left(self.columns.iter_mut()) - } else { - Either::Right(self.columns.iter_mut().rev()) - }; - - let mut num_columns = 0; - for column in columns { - match &column.width_bounds { - WidthBounds::Soft { - min_width, - desired, - max_percentage, - } => { - let soft_limit = max( - if let Some(max_percentage) = max_percentage { - // 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 { - break; - } 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::Hard(width) => { - let space_taken = min(*width, total_width_left); - - if *width > space_taken { - break; - } else if space_taken > 0 { - total_width_left = total_width_left.saturating_sub(space_taken + 1); - column.calculated_width = space_taken; - num_columns += 1; - } - } - } - } - - if num_columns > 0 { - // Redistribute remaining. - let mut num_dist = num_columns; - let amount_per_slot = total_width_left / num_dist; - total_width_left %= num_dist; - for column in self.columns.iter_mut() { - if num_dist == 0 { - break; - } - - if column.calculated_width > 0 { - if total_width_left > 0 { - column.calculated_width += amount_per_slot + 1; - total_width_left -= 1; - } else { - column.calculated_width += amount_per_slot; - } - - num_dist -= 1; - } - } - } - } - - /// Updates the position if possible, and if there is a valid change, returns the new position. - pub fn update_position(&mut self, change: i64, num_entries: usize) -> Option { - if change == 0 { - return None; - } - - let csp: Result = self.current_scroll_position.try_into(); - if let Ok(csp) = csp { - let proposed: Result = (csp + change).try_into(); - if let Ok(proposed) = proposed { - if proposed < num_entries { - self.current_scroll_position = proposed; - if change < 0 { - self.scroll_direction = ScrollDirection::Up; - } else { - self.scroll_direction = ScrollDirection::Down; - } - - return Some(self.current_scroll_position); - } - } - } - - None - } -} - #[derive(PartialEq)] pub enum KillSignal { Cancel, @@ -342,555 +131,20 @@ impl AppSearchState { } } -/// ProcessSearchState only deals with process' search's current settings and state. -pub struct ProcessSearchState { - pub search_state: AppSearchState, - pub is_ignoring_case: bool, - pub is_searching_whole_word: bool, - pub is_searching_with_regex: bool, -} - -impl Default for ProcessSearchState { - fn default() -> Self { - ProcessSearchState { - search_state: AppSearchState::default(), - is_ignoring_case: true, - is_searching_whole_word: false, - is_searching_with_regex: false, - } - } -} - -impl ProcessSearchState { - pub fn search_toggle_ignore_case(&mut self) { - self.is_ignoring_case = !self.is_ignoring_case; - } - - pub fn search_toggle_whole_word(&mut self) { - self.is_searching_whole_word = !self.is_searching_whole_word; - } - - pub fn search_toggle_regex(&mut self) { - self.is_searching_with_regex = !self.is_searching_with_regex; - } -} - -pub struct ColumnInfo { - pub enabled: bool, - pub shortcut: Option<&'static str>, - // FIXME: Move column width logic here! - // pub hard_width: Option, - // pub max_soft_width: Option, -} - -pub struct ProcColumn { - pub ordered_columns: Vec, - /// The y location of headers. Since they're all aligned, it's just one value. - pub column_header_y_loc: Option, - /// The x start and end bounds for each header. - pub column_header_x_locs: Option>, - pub column_mapping: HashMap, - pub longest_header_len: u16, - pub column_state: TableState, - pub scroll_direction: ScrollDirection, - pub current_scroll_position: usize, - pub previous_scroll_position: usize, - pub backup_prev_scroll_position: usize, -} - -impl Default for ProcColumn { - fn default() -> Self { - let ordered_columns = vec![ - Count, - Pid, - ProcessName, - Command, - CpuPercent, - Mem, - MemPercent, - ReadPerSecond, - WritePerSecond, - TotalRead, - TotalWrite, - User, - State, - ]; - - let mut column_mapping = HashMap::new(); - let mut longest_header_len = 0; - for column in ordered_columns.clone() { - longest_header_len = std::cmp::max(longest_header_len, column.to_string().len()); - match column { - CpuPercent => { - column_mapping.insert( - column, - ColumnInfo { - enabled: true, - shortcut: Some("c"), - // hard_width: None, - // max_soft_width: None, - }, - ); - } - MemPercent => { - column_mapping.insert( - column, - ColumnInfo { - enabled: true, - shortcut: Some("m"), - // hard_width: None, - // max_soft_width: None, - }, - ); - } - Mem => { - column_mapping.insert( - column, - ColumnInfo { - enabled: false, - shortcut: Some("m"), - // hard_width: None, - // max_soft_width: None, - }, - ); - } - ProcessName => { - column_mapping.insert( - column, - ColumnInfo { - enabled: true, - shortcut: Some("n"), - // hard_width: None, - // max_soft_width: None, - }, - ); - } - Command => { - column_mapping.insert( - column, - ColumnInfo { - enabled: false, - shortcut: Some("n"), - // hard_width: None, - // max_soft_width: None, - }, - ); - } - Pid => { - column_mapping.insert( - column, - ColumnInfo { - enabled: true, - shortcut: Some("p"), - // hard_width: None, - // max_soft_width: None, - }, - ); - } - Count => { - column_mapping.insert( - column, - ColumnInfo { - enabled: false, - shortcut: None, - // hard_width: None, - // max_soft_width: None, - }, - ); - } - User => { - column_mapping.insert( - column, - ColumnInfo { - enabled: cfg!(target_family = "unix"), - shortcut: None, - }, - ); - } - _ => { - column_mapping.insert( - column, - ColumnInfo { - enabled: true, - shortcut: None, - // hard_width: None, - // max_soft_width: None, - }, - ); - } - } - } - let longest_header_len = longest_header_len as u16; - - ProcColumn { - ordered_columns, - column_mapping, - longest_header_len, - column_state: TableState::default(), - scroll_direction: ScrollDirection::default(), - current_scroll_position: 0, - previous_scroll_position: 0, - backup_prev_scroll_position: 0, - column_header_y_loc: None, - column_header_x_locs: None, - } - } -} - -impl ProcColumn { - /// Returns its new status. - pub fn toggle(&mut self, column: &ProcessSorting) -> Option { - if let Some(mapping) = self.column_mapping.get_mut(column) { - mapping.enabled = !(mapping.enabled); - Some(mapping.enabled) - } else { - None - } - } - - pub fn try_set(&mut self, column: &ProcessSorting, setting: bool) -> Option { - if let Some(mapping) = self.column_mapping.get_mut(column) { - mapping.enabled = setting; - Some(mapping.enabled) - } else { - None - } - } - - pub fn try_enable(&mut self, column: &ProcessSorting) -> Option { - if let Some(mapping) = self.column_mapping.get_mut(column) { - mapping.enabled = true; - Some(mapping.enabled) - } else { - None - } - } - - pub fn try_disable(&mut self, column: &ProcessSorting) -> Option { - if let Some(mapping) = self.column_mapping.get_mut(column) { - mapping.enabled = false; - Some(mapping.enabled) - } else { - None - } - } - - pub fn is_enabled(&self, column: &ProcessSorting) -> bool { - if let Some(mapping) = self.column_mapping.get(column) { - mapping.enabled - } else { - false - } - } - - pub fn get_enabled_columns_len(&self) -> usize { - self.ordered_columns - .iter() - .filter_map(|column_type| { - if let Some(col_map) = self.column_mapping.get(column_type) { - if col_map.enabled { - Some(1) - } else { - None - } - } else { - None - } - }) - .sum() - } - - /// NOTE: ALWAYS call this when opening the sorted window. - pub fn set_to_sorted_index_from_type(&mut self, proc_sorting_type: &ProcessSorting) { - // TODO [Custom Columns]: If we add custom columns, this may be needed! - // Since column indices will change, this runs the risk of OOB. So, if you change columns, CALL THIS AND ADAPT! - let mut true_index = 0; - for column in &self.ordered_columns { - if *column == *proc_sorting_type { - break; - } - if self.column_mapping.get(column).unwrap().enabled { - true_index += 1; - } - } - - self.current_scroll_position = true_index; - self.backup_prev_scroll_position = self.previous_scroll_position; - } - - /// This function sets the scroll position based on the index. - pub fn set_to_sorted_index_from_visual_index(&mut self, visual_index: usize) { - self.current_scroll_position = visual_index; - self.backup_prev_scroll_position = self.previous_scroll_position; - } - - pub fn get_column_headers( - &self, proc_sorting_type: &ProcessSorting, sort_reverse: bool, - ) -> Vec { - const DOWN_ARROW: char = '▼'; - const UP_ARROW: char = '▲'; - - // TODO: Gonna have to figure out how to do left/right GUI notation if we add it. - self.ordered_columns - .iter() - .filter_map(|column_type| { - let mapping = self.column_mapping.get(column_type).unwrap(); - let mut command_str = String::default(); - if let Some(command) = mapping.shortcut { - command_str = format!("({})", command); - } - - if mapping.enabled { - Some(format!( - "{}{}{}", - column_type, - command_str, - if proc_sorting_type == column_type { - if sort_reverse { - DOWN_ARROW - } else { - UP_ARROW - } - } else { - ' ' - } - )) - } else { - None - } - }) - .collect() - } -} - -pub struct ProcWidgetState { - pub process_search_state: ProcessSearchState, - pub is_grouped: bool, - pub scroll_state: TableComponentState, - 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: TableComponentState::default(), - process_sorting_type, - is_process_sort_descending, - is_using_command, - current_column_index: 0, - is_sort_open: false, - columns, - is_tree_mode, - table_width_state: CanvasTableWidthState::default(), - requires_redraw: false, - } - } - - /// Updates sorting when using the column list. - /// ...this really should be part of the ProcColumn struct (along with the sorting fields), - /// but I'm too lazy. - /// - /// Sorry, future me, you're gonna have to refactor this later. Too busy getting - /// the feature to work in the first place! :) - pub fn update_sorting_with_columns(&mut self) { - let mut true_index = 0; - let mut enabled_index = 0; - let target_itx = self.columns.current_scroll_position; - for column in &self.columns.ordered_columns { - let enabled = self.columns.column_mapping.get(column).unwrap().enabled; - if enabled_index == target_itx && enabled { - break; - } - if enabled { - enabled_index += 1; - } - true_index += 1; - } - - if let Some(new_sort_type) = self.columns.ordered_columns.get(true_index) { - if *new_sort_type == self.process_sorting_type { - // Just reverse the search if we're reselecting! - self.is_process_sort_descending = !(self.is_process_sort_descending); - } else { - self.process_sorting_type = new_sort_type.clone(); - match self.process_sorting_type { - ProcessSorting::State - | ProcessSorting::Pid - | ProcessSorting::ProcessName - | ProcessSorting::Command => { - // Also invert anything that uses alphabetical sorting by default. - self.is_process_sort_descending = false; - } - _ => { - self.is_process_sort_descending = true; - } - } - } - } - } - - pub fn toggle_command_and_name(&mut self, is_using_command: bool) { - if let Some(pn) = self - .columns - .column_mapping - .get_mut(&ProcessSorting::ProcessName) - { - pn.enabled = !is_using_command; - } - if let Some(c) = self - .columns - .column_mapping - .get_mut(&ProcessSorting::Command) - { - c.enabled = is_using_command; - } - } - - pub fn get_search_cursor_position(&self) -> usize { - self.process_search_state - .search_state - .grapheme_cursor - .cur_cursor() - } - - pub fn get_char_cursor_position(&self) -> usize { - self.process_search_state.search_state.char_cursor_position - } - - pub fn is_search_enabled(&self) -> bool { - self.process_search_state.search_state.is_enabled - } - - pub fn get_current_search_query(&self) -> &String { - &self.process_search_state.search_state.current_search_query - } - - pub fn update_query(&mut self) { - if self - .process_search_state - .search_state - .current_search_query - .is_empty() - { - self.process_search_state.search_state.is_blank_search = true; - self.process_search_state.search_state.is_invalid_search = false; - self.process_search_state.search_state.error_message = None; - } else { - let parsed_query = self.parse_query(); - // debug!("Parsed query: {:#?}", parsed_query); - - if let Ok(parsed_query) = parsed_query { - self.process_search_state.search_state.query = Some(parsed_query); - self.process_search_state.search_state.is_blank_search = false; - self.process_search_state.search_state.is_invalid_search = false; - self.process_search_state.search_state.error_message = None; - } else if let Err(err) = parsed_query { - self.process_search_state.search_state.is_blank_search = false; - self.process_search_state.search_state.is_invalid_search = true; - self.process_search_state.search_state.error_message = Some(err.to_string()); - } - } - self.scroll_state.scroll_bar = 0; - self.scroll_state.current_scroll_position = 0; - } - - pub fn clear_search(&mut self) { - self.process_search_state.search_state.reset(); - } - - pub fn search_walk_forward(&mut self, start_position: usize) { - self.process_search_state - .search_state - .grapheme_cursor - .next_boundary( - &self.process_search_state.search_state.current_search_query[start_position..], - start_position, - ) - .unwrap(); - } - - pub fn search_walk_back(&mut self, start_position: usize) { - self.process_search_state - .search_state - .grapheme_cursor - .prev_boundary( - &self.process_search_state.search_state.current_search_query[..start_position], - 0, - ) - .unwrap(); - } -} - pub struct ProcState { - pub widget_states: HashMap, - pub force_update: Option, - pub force_update_all: bool, + pub widget_states: HashMap, } impl ProcState { - pub fn init(widget_states: HashMap) -> Self { - ProcState { - widget_states, - force_update: None, - force_update_all: false, - } + pub fn init(widget_states: HashMap) -> Self { + ProcState { widget_states } } - pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut ProcWidgetState> { + pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut ProcWidget> { self.widget_states.get_mut(&widget_id) } - pub fn get_widget_state(&self, widget_id: u64) -> Option<&ProcWidgetState> { + pub fn get_widget_state(&self, widget_id: u64) -> Option<&ProcWidget> { self.widget_states.get(&widget_id) } } @@ -941,8 +195,7 @@ pub struct CpuWidgetState { impl CpuWidgetState { pub fn init(current_display_time: u64, autohide_timer: Option) -> Self { - const CPU_LEGEND_HEADER: [(Cow<'static, str>, Option>); 2] = - [(Cow::Borrowed("CPU"), None), (Cow::Borrowed("Use%"), None)]; + 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)), @@ -952,7 +205,7 @@ impl CpuWidgetState { CPU_LEGEND_HEADER .iter() .zip(WIDTHS) - .map(|(c, width)| TableComponentColumn::new(c.0.clone(), c.1.clone(), width)) + .map(|(c, width)| TableComponentColumn::new(CellContent::new(*c, None), width)) .collect(), ); @@ -1040,7 +293,9 @@ impl Default for TempWidgetState { TEMP_HEADERS .iter() .zip(WIDTHS) - .map(|(header, width)| TableComponentColumn::new(*header, None, width)) + .map(|(header, width)| { + TableComponentColumn::new(CellContent::new(*header, None), width) + }) .collect(), ), } @@ -1087,7 +342,9 @@ impl Default for DiskWidgetState { DISK_HEADERS .iter() .zip(WIDTHS) - .map(|(header, width)| TableComponentColumn::new(*header, None, width)) + .map(|(header, width)| { + TableComponentColumn::new(CellContent::new(*header, None), width) + }) .collect(), ), } @@ -1169,61 +426,3 @@ pub struct ConfigCategory { pub struct ConfigOption { pub set_function: Box anyhow::Result<()>>, } - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_scroll_update_position() { - fn check_scroll_update( - scroll: &mut TableComponentState, change: i64, max: usize, ret: Option, - new_position: usize, - ) { - assert_eq!(scroll.update_position(change, max), ret); - assert_eq!(scroll.current_scroll_position, new_position); - } - - let mut scroll = TableComponentState { - current_scroll_position: 5, - scroll_bar: 0, - scroll_direction: ScrollDirection::Down, - table_state: Default::default(), - columns: vec![], - }; - 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..95e8730e --- /dev/null +++ b/src/app/states/table_state.rs @@ -0,0 +1,455 @@ +use std::{borrow::Cow, convert::TryInto}; + +use tui::widgets::TableState; + +use super::ScrollDirection; + +/// A bound on the width of a column. +#[derive(Clone, Copy, Debug)] +pub enum WidthBounds { + /// A width of this type is either as long as `min`, but can otherwise shrink and grow up to a point. + Soft { + /// The minimum amount before giving up and hiding. + min_width: u16, + + /// The desired, calculated width. Take this if possible as the base starting width. + desired: u16, + + /// The max width, as a percentage of the total width available. If [`None`], + /// then it can grow as desired. + max_percentage: Option, + }, + + /// A width of this type is either as long as specified, or does not appear at all. + Hard(u16), +} + +impl WidthBounds { + pub const fn soft_from_str(name: &'static str, max_percentage: Option) -> WidthBounds { + let len = name.len() as u16; + WidthBounds::Soft { + min_width: len, + desired: len, + max_percentage, + } + } + + pub const fn soft_from_str_with_alt( + name: &'static str, alt: &'static str, max_percentage: Option, + ) -> WidthBounds { + WidthBounds::Soft { + min_width: alt.len() as u16, + desired: name.len() as u16, + max_percentage, + } + } +} + +/// A [`CellContent`] contains text information for display in a table. +#[derive(Clone)] +pub enum CellContent { + Simple(Cow<'static, str>), + HasAlt { + alt: Cow<'static, str>, + main: Cow<'static, str>, + }, +} + +impl CellContent { + /// Creates a new [`CellContent`]. + pub fn new(name: I, alt: Option) -> Self + where + I: Into>, + { + if let Some(alt) = alt { + CellContent::HasAlt { + alt: alt.into(), + main: name.into(), + } + } else { + CellContent::Simple(name.into()) + } + } + + /// Returns the length of the [`CellContent`]. Note that for a [`CellContent::HasAlt`], it will return + /// the length of the "main" field. + pub fn len(&self) -> usize { + match self { + CellContent::Simple(s) => s.len(), + CellContent::HasAlt { alt: _, main: long } => long.len(), + } + } + + /// Whether the [`CellContent`]'s text is empty. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +pub trait TableComponentHeader { + fn header_text(&self) -> &CellContent; +} + +impl TableComponentHeader for CellContent { + fn header_text(&self) -> &CellContent { + self + } +} + +impl From<&'static str> for CellContent { + fn from(s: &'static str) -> Self { + CellContent::Simple(s.into()) + } +} + +pub struct TableComponentColumn { + /// The header of the column. + pub header: H, + + /// A restriction on this column's width, if desired. + pub width_bounds: WidthBounds, + + /// The calculated width of the column. + pub calculated_width: u16, + + /// Marks that this column is currently "hidden", and should *always* be skipped. + pub is_hidden: bool, +} + +impl TableComponentColumn { + pub fn new(header: H, width_bounds: WidthBounds) -> Self { + Self { + header, + width_bounds, + calculated_width: 0, + is_hidden: false, + } + } + + pub fn default_hard(header: H) -> Self { + let width = header.header_text().len() as u16; + Self { + header, + width_bounds: WidthBounds::Hard(width), + calculated_width: 0, + is_hidden: false, + } + } + + pub fn default_soft(header: H, max_percentage: Option) -> Self { + let min_width = header.header_text().len() as u16; + Self { + header, + width_bounds: WidthBounds::Soft { + min_width, + desired: min_width, + max_percentage, + }, + calculated_width: 0, + is_hidden: false, + } + } + + pub fn is_zero_width(&self) -> bool { + self.calculated_width == 0 + } + + pub fn is_skipped(&self) -> bool { + self.is_zero_width() || self.is_hidden + } +} + +pub enum SortOrder { + Ascending, + Descending, +} + +/// Represents the current table's sorting state. +pub enum SortState { + Unsortable, + Sortable { index: usize, order: SortOrder }, +} + +impl Default for SortState { + fn default() -> Self { + SortState::Unsortable + } +} + +/// [`TableComponentState`] deals with fields for a scrollable's current state. +pub struct TableComponentState { + pub current_scroll_position: usize, + pub scroll_bar: usize, + pub scroll_direction: ScrollDirection, + pub table_state: TableState, + pub columns: Vec>, + pub sort_state: SortState, +} + +impl TableComponentState { + pub fn new(columns: Vec>) -> Self { + Self { + current_scroll_position: 0, + scroll_bar: 0, + scroll_direction: ScrollDirection::Down, + table_state: Default::default(), + columns, + sort_state: Default::default(), + } + } + + /// 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; + + for column in self.columns.iter_mut() { + column.calculated_width = 0; + } + + 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 { index: _, order: _ } => 1, + }; + let mut num_columns = 0; + for column in columns { + if column.is_hidden { + continue; + } + + match &column.width_bounds { + WidthBounds::Soft { + min_width, + desired, + max_percentage, + } => { + let offset_min = *min_width + arrow_offset; + let soft_limit = max( + if let Some(max_percentage) = max_percentage { + // Rust doesn't have an `into()` or `try_into()` for floats to integers??? + ((*max_percentage * f32::from(total_width)).ceil()) as u16 + } else { + *desired + }, + offset_min, + ); + let space_taken = min(min(soft_limit, *desired), total_width_left); + + if offset_min > space_taken { + break; + } 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::Hard(width) => { + let min_width = *width + arrow_offset; + let space_taken = min(min_width, total_width_left); + + if min_width > space_taken { + break; + } else if space_taken > 0 { + total_width_left = total_width_left.saturating_sub(space_taken + 1); + column.calculated_width = space_taken; + num_columns += 1; + } + } + } + } + + if num_columns > 0 { + // Redistribute remaining. + let mut num_dist = num_columns; + let amount_per_slot = total_width_left / num_dist; + total_width_left %= num_dist; + for column in self.columns.iter_mut() { + if num_dist == 0 { + break; + } + + if column.calculated_width > 0 { + if total_width_left > 0 { + column.calculated_width += amount_per_slot + 1; + total_width_left -= 1; + } else { + column.calculated_width += amount_per_slot; + } + + num_dist -= 1; + } + } + } + } + + /// Updates the position if possible, and if there is a valid change, returns the new position. + pub fn update_position(&mut self, change: i64, num_entries: usize) -> Option { + if change == 0 { + return None; + } + + let csp: Result = self.current_scroll_position.try_into(); + if let Ok(csp) = csp { + let proposed: Result = (csp + change).try_into(); + if let Ok(proposed) = proposed { + if proposed < num_entries { + self.current_scroll_position = proposed; + if change < 0 { + self.scroll_direction = ScrollDirection::Up; + } else { + self.scroll_direction = ScrollDirection::Down; + } + + return Some(self.current_scroll_position); + } + } + } + + None + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_scroll_update_position() { + #[track_caller] + fn check_scroll_update( + scroll: &mut TableComponentState, change: i64, max: usize, ret: Option, + new_position: usize, + ) { + assert_eq!(scroll.update_position(change, max), ret); + assert_eq!(scroll.current_scroll_position, new_position); + } + + let mut scroll = TableComponentState { + current_scroll_position: 5, + scroll_bar: 0, + scroll_direction: ScrollDirection::Down, + table_state: Default::default(), + columns: vec![], + sort_state: 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); + } + + #[test] + fn test_table_width_calculation() { + #[track_caller] + fn test_calculation(state: &mut TableComponentState, width: u16, expected: Vec) { + state.calculate_column_widths(width, true); + assert_eq!( + state + .columns + .iter() + .filter_map(|c| if c.calculated_width == 0 { + None + } else { + Some(c.calculated_width) + }) + .collect::>(), + expected + ) + } + + let mut state = TableComponentState::new(vec![ + TableComponentColumn::default_hard(CellContent::from("a")), + TableComponentColumn::new( + "a".into(), + WidthBounds::Soft { + min_width: 1, + desired: 10, + max_percentage: Some(0.125), + }, + ), + TableComponentColumn::new( + "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 { + index: 1, + order: SortOrder::Ascending, + }; + + 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]); + } +} diff --git a/src/app/widgets.rs b/src/app/widgets.rs new file mode 100644 index 00000000..612c880b --- /dev/null +++ b/src/app/widgets.rs @@ -0,0 +1,2 @@ +pub mod process; +pub use process::*; diff --git a/src/app/widgets/process.rs b/src/app/widgets/process.rs new file mode 100644 index 00000000..ea361d66 --- /dev/null +++ b/src/app/widgets/process.rs @@ -0,0 +1,320 @@ +use std::borrow::Cow; + +use crate::{ + app::{ + query::*, AppSearchState, CanvasTableWidthState, CellContent, TableComponentColumn, + TableComponentHeader, TableComponentState, WidthBounds, + }, + data_harvester::processes, +}; + +/// 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(Copy, Clone, Debug)] +pub enum ProcWidgetMode { + Tree, + Grouped, + Normal, +} + +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)")); + + 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, + } + } +} + +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 requires_redraw: bool, + + pub search_state: ProcessSearchState, + pub table_state: TableComponentState, + pub sort_table_state: TableComponentState, + + pub is_sort_open: bool, + pub force_update: bool, + + // Hmm... + pub is_process_sort_descending: bool, + pub process_sorting_type: processes::ProcessSorting, + + // TO REMOVE + pub is_using_command: bool, + pub table_width_state: CanvasTableWidthState, +} + +impl ProcWidget { + pub fn init( + mode: ProcWidgetMode, is_case_sensitive: bool, is_match_whole_word: bool, + is_use_regex: bool, show_memory_as_values: 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 matches!(mode, ProcWidgetMode::Tree) { + (processes::ProcessSorting::Pid, false) + } else { + (processes::ProcessSorting::CpuPercent, true) + }; + + let is_count = matches!(mode, ProcWidgetMode::Grouped); + + let sort_table_state = TableComponentState::new(vec![TableComponentColumn::new( + CellContent::Simple("Sort By".into()), + WidthBounds::Hard(7), + )]); + let table_state = TableComponentState::new(vec![ + TableComponentColumn::default_hard(ProcWidgetColumn::PidOrCount { is_count }), + TableComponentColumn::default_soft( + ProcWidgetColumn::ProcNameOrCommand { + is_command: is_using_command, + }, + Some(0.7), + ), + TableComponentColumn::default_hard(ProcWidgetColumn::CpuPercent), + TableComponentColumn::default_hard(ProcWidgetColumn::Memory { + show_percentage: !show_memory_as_values, + }), + TableComponentColumn::default_hard(ProcWidgetColumn::ReadPerSecond), + TableComponentColumn::default_hard(ProcWidgetColumn::WritePerSecond), + TableComponentColumn::default_hard(ProcWidgetColumn::TotalRead), + TableComponentColumn::default_hard(ProcWidgetColumn::TotalWrite), + TableComponentColumn::default_hard(ProcWidgetColumn::User), + TableComponentColumn::default_hard(ProcWidgetColumn::State), + ]); + + ProcWidget { + search_state: process_search_state, + table_state, + sort_table_state, + process_sorting_type, + is_process_sort_descending, + is_using_command, + is_sort_open: false, + table_width_state: CanvasTableWidthState::default(), + requires_redraw: false, + mode, + force_update: false, + } + } + + pub fn get_search_cursor_position(&self) -> usize { + self.search_state.search_state.grapheme_cursor.cur_cursor() + } + + pub fn get_char_cursor_position(&self) -> usize { + self.search_state.search_state.char_cursor_position + } + + pub fn is_search_enabled(&self) -> bool { + self.search_state.search_state.is_enabled + } + + pub fn get_current_search_query(&self) -> &String { + &self.search_state.search_state.current_search_query + } + + pub fn update_query(&mut self) { + if self + .search_state + .search_state + .current_search_query + .is_empty() + { + self.search_state.search_state.is_blank_search = true; + self.search_state.search_state.is_invalid_search = false; + self.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.search_state.search_state.query = Some(parsed_query); + self.search_state.search_state.is_blank_search = false; + self.search_state.search_state.is_invalid_search = false; + self.search_state.search_state.error_message = None; + } else if let Err(err) = parsed_query { + self.search_state.search_state.is_blank_search = false; + self.search_state.search_state.is_invalid_search = true; + self.search_state.search_state.error_message = Some(err.to_string()); + } + } + self.table_state.scroll_bar = 0; + self.table_state.current_scroll_position = 0; + } + + pub fn clear_search(&mut self) { + self.search_state.search_state.reset(); + } + + pub fn search_walk_forward(&mut self, start_position: usize) { + self.search_state + .search_state + .grapheme_cursor + .next_boundary( + &self.search_state.search_state.current_search_query[start_position..], + start_position, + ) + .unwrap(); + } + + pub fn search_walk_back(&mut self, start_position: usize) { + self.search_state + .search_state + .grapheme_cursor + .prev_boundary( + &self.search_state.search_state.current_search_query[..start_position], + 0, + ) + .unwrap(); + } + + pub fn num_enabled_columns(&self) -> usize { + self.table_state + .columns + .iter() + .filter(|c| !c.is_skipped()) + .count() + } +} diff --git a/src/canvas.rs b/src/canvas.rs index 21cd5197..12dc9ee6 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -341,8 +341,9 @@ impl Painter { // 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; + // FIXME: [Proc] Handle this? + // proc_widget.columns.column_header_y_loc = None; + // proc_widget.columns.column_header_x_locs = None; } } @@ -506,7 +507,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, @@ -585,7 +586,7 @@ impl Painter { ProcSort => 2, _ => 0, }; - self.draw_process_features( + self.draw_process_widget( f, app_state, vertical_chunks[3], @@ -736,7 +737,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 cf9788ad..f75a5eb3 100644 --- a/src/canvas/components/text_table.rs +++ b/src/canvas/components/text_table.rs @@ -1,4 +1,7 @@ -use std::{borrow::Cow, cmp::min}; +use std::{ + borrow::Cow, + cmp::{max, min}, +}; use concat_string::concat_string; use tui::{ @@ -12,9 +15,12 @@ use tui::{ use unicode_segmentation::UnicodeSegmentation; use crate::{ - app::{self, TableComponentState}, + app::{ + self, CellContent, SortState, TableComponentColumn, TableComponentHeader, + TableComponentState, + }, constants::{SIDE_BORDERS, TABLE_GAP_HEIGHT_LIMIT}, - data_conversion::{CellContent, TableData, TableRow}, + data_conversion::{TableData, TableRow}, }; pub struct TextTableTitle<'a> { @@ -101,8 +107,8 @@ impl<'a> TextTable<'a> { } }) } - pub fn draw_text_table( - &self, f: &mut Frame<'_, B>, draw_loc: Rect, state: &mut TableComponentState, + pub fn draw_text_table( + &self, f: &mut Frame<'_, B>, draw_loc: Rect, state: &mut TableComponentState, table_data: &TableData, ) { // TODO: This is a *really* ugly hack to get basic mode to hide the border when not selected, without shifting everything. @@ -179,7 +185,7 @@ impl<'a> TextTable<'a> { desired, max_percentage: _, } => { - *desired = std::cmp::max(column.name.len(), *data_width) as u16; + *desired = max(column.header.header_text().len(), *data_width) as u16; } app::WidthBounds::Hard(_width) => {} }); @@ -188,15 +194,9 @@ impl<'a> TextTable<'a> { } let columns = &state.columns; - let header = Row::new(columns.iter().filter_map(|c| { - if c.calculated_width == 0 { - None - } else { - Some(truncate_text(&c.name, c.calculated_width.into(), None)) - } - })) - .style(self.header_style) - .bottom_margin(table_gap); + 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), @@ -245,9 +245,60 @@ impl<'a> TextTable<'a> { } } +/// Constructs the table header. +fn build_header<'a, H: TableComponentHeader>( + columns: &'a [TableComponentColumn], sort_state: &SortState, +) -> Row<'a> { + use itertools::Either; + + const UP_ARROW: &str = "▲"; + const DOWN_ARROW: &str = "▼"; + + let iter = match sort_state { + SortState::Unsortable => Either::Left(columns.iter().filter_map(|c| { + if c.calculated_width == 0 { + None + } else { + Some(truncate_text( + c.header.header_text(), + c.calculated_width.into(), + None, + )) + } + })), + SortState::Sortable { index, order } => { + 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