diff options
Diffstat (limited to 'src/app/states.rs')
-rw-r--r-- | src/app/states.rs | 841 |
1 files changed, 20 insertions, 821 deletions
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<u16>, } -/// 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), -} - -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, - } - } -} - -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<I>(name: I, alt: Option<I>, width_bounds: WidthBounds) -> Self - where - I: Into<Cow<'static, str>>, - { - 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<TableComponentColumn>, -} - -impl TableComponentState { - pub fn new(columns: Vec<TableComponentColumn>) -> 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<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 - } -} - #[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<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, 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<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: 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<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) } } @@ -941,8 +195,7 @@ pub struct CpuWidgetState { impl CpuWidgetState { pub fn init(current_display_time: u64, autohide_timer: Option<Instant>) -> Self { - const CPU_LEGEND_HEADER: [(Cow<'static, str>, Option<Cow<'static, str>>); 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<dyn Fn() -> 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<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![], - }; - 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); - } -} |