diff --git a/README.md b/README.md index 7801582a..01e0be51 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ Features of bottom include: - Maximizing of widgets of interest to take up the entire window. +- Basic mode + More details about each widget and compatibility can be found [here](./docs/widgets.md). ## Config files @@ -75,7 +77,7 @@ sudo dpkg -i bottom_0.2.2_amd64.deb ### Windows -You can get release versions via [Chocolatey](https://chocolatey.org/packages/bottom/): +You can get release versions via [Chocolatey](https://chocolatey.org/packages/bottom/) (note it may take a while to be available due to moderation/review): ```bash choco install bottom --version=0.2.1 @@ -86,10 +88,10 @@ choco install bottom --version=0.2.1 You can get release versions using Homebrew: ```bash -$ brew tap clementtsang/bottom -$ brew install bottom +brew tap clementtsang/bottom +brew install bottom # Or -$ brew install clementtsang/bottom/bottom +brew install clementtsang/bottom/bottom ``` ## Usage @@ -134,6 +136,8 @@ Run using `btm`. - `-C`, `--config` takes in a file path leading to a TOML file. If doesn't exist, creates one. +- `-b`, `--basic` will enable basic mode, removing all graphs from the main interface and condensing data. + ### Keybindings #### General diff --git a/rustfmt.toml b/rustfmt.toml index 5e1a41b9..3992c568 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -3,7 +3,6 @@ max_width = 100 newline_style = "Unix" reorder_imports = true fn_args_layout = "Compressed" -hard_tabs = true merge_derives = true reorder_modules = true tab_spaces = 4 diff --git a/sample_configs/bottom.toml b/sample_configs/bottom.toml index a6b47855..1dd183af 100644 --- a/sample_configs/bottom.toml +++ b/sample_configs/bottom.toml @@ -34,6 +34,9 @@ #default_widget = "network_default" #default_widget = "process_default" +# Basic mode +#basic = true + # These are all the components that support custom theming. Currently, it only # supports taking in a string representing a hex colour. Note that colour support diff --git a/src/app.rs b/src/app.rs index e23938ac..0fbaa784 100644 --- a/src/app.rs +++ b/src/app.rs @@ -16,1273 +16,1381 @@ const MAX_SEARCH_LENGTH: usize = 200; #[derive(Debug, Clone, Copy)] pub enum WidgetPosition { - Cpu, - Mem, - Disk, - Temp, - Network, - Process, - ProcessSearch, + Cpu, + Mem, + Disk, + Temp, + Network, + Process, + ProcessSearch, + BasicCpu, + BasicMem, + BasicNet, +} + +impl WidgetPosition { + pub fn is_widget_table(self) -> bool { + match self { + WidgetPosition::Disk + | WidgetPosition::Process + | WidgetPosition::ProcessSearch + | WidgetPosition::Temp => true, + _ => false, + } + } + + pub fn get_pretty_name(self) -> String { + match self { + WidgetPosition::Cpu | WidgetPosition::BasicCpu => "CPU", + WidgetPosition::Mem | WidgetPosition::BasicMem => "Memory", + WidgetPosition::Disk => "Disks", + WidgetPosition::Temp => "Temperature", + WidgetPosition::Network | WidgetPosition::BasicNet => "Network", + WidgetPosition::Process | WidgetPosition::ProcessSearch => "Processes", + } + .to_string() + } } #[derive(Debug)] pub enum ScrollDirection { - // UP means scrolling up --- this usually DECREMENTS - UP, - // DOWN means scrolling down --- this usually INCREMENTS - DOWN, + // UP means scrolling up --- this usually DECREMENTS + UP, + // DOWN means scrolling down --- this usually INCREMENTS + DOWN, } #[derive(Debug)] pub enum CursorDirection { - LEFT, - RIGHT, + LEFT, + RIGHT, } /// AppScrollWidgetState deals with fields for a scrollable app's current state. #[derive(Default)] pub struct AppScrollWidgetState { - pub current_scroll_position: u64, - pub previous_scroll_position: u64, + pub current_scroll_position: u64, + pub previous_scroll_position: u64, } pub struct AppScrollState { - pub scroll_direction: ScrollDirection, - pub process_scroll_state: AppScrollWidgetState, - pub disk_scroll_state: AppScrollWidgetState, - pub temp_scroll_state: AppScrollWidgetState, - pub cpu_scroll_state: AppScrollWidgetState, + pub scroll_direction: ScrollDirection, + pub process_scroll_state: AppScrollWidgetState, + pub disk_scroll_state: AppScrollWidgetState, + pub temp_scroll_state: AppScrollWidgetState, + pub cpu_scroll_state: AppScrollWidgetState, } impl Default for AppScrollState { - fn default() -> Self { - AppScrollState { - scroll_direction: ScrollDirection::DOWN, - process_scroll_state: AppScrollWidgetState::default(), - disk_scroll_state: AppScrollWidgetState::default(), - temp_scroll_state: AppScrollWidgetState::default(), - cpu_scroll_state: AppScrollWidgetState::default(), - } - } + fn default() -> Self { + AppScrollState { + scroll_direction: ScrollDirection::DOWN, + process_scroll_state: AppScrollWidgetState::default(), + disk_scroll_state: AppScrollWidgetState::default(), + temp_scroll_state: AppScrollWidgetState::default(), + cpu_scroll_state: AppScrollWidgetState::default(), + } + } } /// AppSearchState deals with generic searching (I might do this in the future). pub struct AppSearchState { - pub is_enabled: bool, - current_search_query: String, - current_regex: Option>, - pub is_blank_search: bool, - pub is_invalid_search: bool, - pub grapheme_cursor: GraphemeCursor, - pub cursor_direction: CursorDirection, - pub cursor_bar: usize, - /// This represents the position in terms of CHARACTERS, not graphemes - pub char_cursor_position: usize, + pub is_enabled: bool, + current_search_query: String, + current_regex: Option>, + pub is_blank_search: bool, + pub is_invalid_search: bool, + pub grapheme_cursor: GraphemeCursor, + pub cursor_direction: CursorDirection, + pub cursor_bar: usize, + /// This represents the position in terms of CHARACTERS, not graphemes + pub char_cursor_position: usize, } impl Default for AppSearchState { - fn default() -> Self { - AppSearchState { - is_enabled: false, - current_search_query: String::default(), - current_regex: None, - is_invalid_search: false, - is_blank_search: true, - grapheme_cursor: GraphemeCursor::new(0, 0, true), - cursor_direction: CursorDirection::RIGHT, - cursor_bar: 0, - char_cursor_position: 0, - } - } + fn default() -> Self { + AppSearchState { + is_enabled: false, + current_search_query: String::default(), + current_regex: None, + is_invalid_search: false, + is_blank_search: true, + grapheme_cursor: GraphemeCursor::new(0, 0, true), + cursor_direction: CursorDirection::RIGHT, + cursor_bar: 0, + char_cursor_position: 0, + } + } } impl AppSearchState { - /// Returns a reset but still enabled app search state - pub fn reset() -> Self { - let mut app_search_state = AppSearchState::default(); - app_search_state.is_enabled = true; - app_search_state - } + /// Returns a reset but still enabled app search state + pub fn reset() -> Self { + let mut app_search_state = AppSearchState::default(); + app_search_state.is_enabled = true; + app_search_state + } - pub fn is_invalid_or_blank_search(&self) -> bool { - self.is_blank_search || self.is_invalid_search - } + pub fn is_invalid_or_blank_search(&self) -> bool { + self.is_blank_search || self.is_invalid_search + } } /// ProcessSearchState only deals with process' search's current settings and state. pub struct ProcessSearchState { - pub search_state: AppSearchState, - pub is_searching_with_pid: bool, - pub is_ignoring_case: bool, - pub is_searching_whole_word: bool, - pub is_searching_with_regex: bool, + pub search_state: AppSearchState, + pub is_searching_with_pid: bool, + 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_searching_with_pid: false, - is_ignoring_case: true, - is_searching_whole_word: false, - is_searching_with_regex: false, - } - } + fn default() -> Self { + ProcessSearchState { + search_state: AppSearchState::default(), + is_searching_with_pid: false, + 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_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_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 fn search_toggle_regex(&mut self) { + self.is_searching_with_regex = !self.is_searching_with_regex; + } } #[derive(Default)] pub struct AppDeleteDialogState { - pub is_showing_dd: bool, - pub is_on_yes: bool, // Defaults to "No" + pub is_showing_dd: bool, + pub is_on_yes: bool, // Defaults to "No" } pub enum AppHelpCategory { - General, - Process, - Search, + General, + Process, + Search, } pub struct AppHelpDialogState { - pub is_showing_help: bool, - pub current_category: AppHelpCategory, + pub is_showing_help: bool, + pub current_category: AppHelpCategory, } impl Default for AppHelpDialogState { - fn default() -> Self { - AppHelpDialogState { - is_showing_help: false, - current_category: AppHelpCategory::General, - } - } + fn default() -> Self { + AppHelpDialogState { + is_showing_help: false, + current_category: AppHelpCategory::General, + } + } } /// AppConfigFields is meant to cover basic fields that would normally be set /// by config files or launch options. Don't need to be mutable (set and forget). pub struct AppConfigFields { - pub update_rate_in_milliseconds: u64, - pub temperature_type: temperature::TemperatureType, - pub use_dot: bool, - pub left_legend: bool, - pub show_average_cpu: bool, - pub use_current_cpu_total: bool, - pub show_disabled_data: bool, + pub update_rate_in_milliseconds: u64, + pub temperature_type: temperature::TemperatureType, + pub use_dot: bool, + pub left_legend: bool, + pub show_average_cpu: bool, + pub use_current_cpu_total: bool, + pub show_disabled_data: bool, + pub use_basic_mode: bool, } /// Network specific pub struct NetworkState { - pub is_showing_tray: bool, - pub is_showing_rx: bool, - pub is_showing_tx: bool, - pub zoom_level: f64, + pub is_showing_tray: bool, + pub is_showing_rx: bool, + pub is_showing_tx: bool, + pub zoom_level: f64, } impl Default for NetworkState { - fn default() -> Self { - NetworkState { - is_showing_tray: false, - is_showing_rx: true, - is_showing_tx: true, - zoom_level: 100.0, - } - } + fn default() -> Self { + NetworkState { + is_showing_tray: false, + is_showing_rx: true, + is_showing_tx: true, + zoom_level: 100.0, + } + } } /// CPU specific pub struct CpuState { - pub is_showing_tray: bool, - pub zoom_level: f64, - pub core_show_vec: Vec, + pub is_showing_tray: bool, + pub zoom_level: f64, + pub core_show_vec: Vec, } impl Default for CpuState { - fn default() -> Self { - CpuState { - is_showing_tray: false, - zoom_level: 100.0, - core_show_vec: Vec::new(), - } - } + fn default() -> Self { + CpuState { + is_showing_tray: false, + zoom_level: 100.0, + core_show_vec: Vec::new(), + } + } } /// Memory specific pub struct MemState { - pub is_showing_tray: bool, - pub is_showing_ram: bool, - pub is_showing_swap: bool, - pub zoom_level: f64, + pub is_showing_tray: bool, + pub is_showing_ram: bool, + pub is_showing_swap: bool, + pub zoom_level: f64, } impl Default for MemState { - fn default() -> Self { - MemState { - is_showing_tray: false, - is_showing_ram: true, - is_showing_swap: true, - zoom_level: 100.0, - } - } + fn default() -> Self { + MemState { + is_showing_tray: false, + is_showing_ram: true, + is_showing_swap: true, + zoom_level: 100.0, + } + } } pub struct App { - pub process_sorting_type: processes::ProcessSorting, - pub process_sorting_reverse: bool, - pub update_process_gui: bool, - pub app_scroll_positions: AppScrollState, - pub current_widget_selected: WidgetPosition, - pub data: data_harvester::Data, - awaiting_second_char: bool, - second_char: Option, - pub dd_err: Option, - to_delete_process_list: Option<(String, Vec)>, - pub is_frozen: bool, - last_key_press: Instant, - pub canvas_data: canvas::DisplayableData, - enable_grouping: bool, - pub data_collection: DataCollection, - pub process_search_state: ProcessSearchState, - pub delete_dialog_state: AppDeleteDialogState, - pub help_dialog_state: AppHelpDialogState, - pub app_config_fields: AppConfigFields, - pub is_expanded: bool, - pub is_resized: bool, - pub cpu_state: CpuState, - pub mem_state: MemState, - pub net_state: NetworkState, + pub process_sorting_type: processes::ProcessSorting, + pub process_sorting_reverse: bool, + pub update_process_gui: bool, + pub app_scroll_positions: AppScrollState, + pub current_widget_selected: WidgetPosition, + pub previous_basic_table_selected: WidgetPosition, + awaiting_second_char: bool, + second_char: Option, + pub dd_err: Option, + to_delete_process_list: Option<(String, Vec)>, + pub is_frozen: bool, + last_key_press: Instant, + pub canvas_data: canvas::DisplayableData, + enable_grouping: bool, + pub data_collection: DataCollection, + pub process_search_state: ProcessSearchState, + pub delete_dialog_state: AppDeleteDialogState, + pub help_dialog_state: AppHelpDialogState, + pub app_config_fields: AppConfigFields, + pub is_expanded: bool, + pub is_resized: bool, + pub cpu_state: CpuState, + pub mem_state: MemState, + pub net_state: NetworkState, } impl App { - #[allow(clippy::too_many_arguments)] - pub fn new( - show_average_cpu: bool, temperature_type: temperature::TemperatureType, - update_rate_in_milliseconds: u64, use_dot: bool, left_legend: bool, - use_current_cpu_total: bool, current_widget_selected: WidgetPosition, - show_disabled_data: bool, - ) -> App { - App { - process_sorting_type: processes::ProcessSorting::CPU, - process_sorting_reverse: true, - update_process_gui: false, - current_widget_selected, - app_scroll_positions: AppScrollState::default(), - data: data_harvester::Data::default(), - awaiting_second_char: false, - second_char: None, - dd_err: None, - to_delete_process_list: None, - is_frozen: false, - last_key_press: Instant::now(), - canvas_data: canvas::DisplayableData::default(), - enable_grouping: false, - data_collection: DataCollection::default(), - process_search_state: ProcessSearchState::default(), - delete_dialog_state: AppDeleteDialogState::default(), - help_dialog_state: AppHelpDialogState::default(), - app_config_fields: AppConfigFields { - show_average_cpu, - temperature_type, - use_dot, - update_rate_in_milliseconds, - left_legend, - use_current_cpu_total, - show_disabled_data, - }, - is_expanded: false, - is_resized: false, - cpu_state: CpuState::default(), - mem_state: MemState::default(), - net_state: NetworkState::default(), - } - } - - pub fn reset(&mut self) { - self.reset_multi_tap_keys(); - self.help_dialog_state.is_showing_help = false; - self.delete_dialog_state.is_showing_dd = false; - if self.process_search_state.search_state.is_enabled { - self.current_widget_selected = WidgetPosition::Process; - self.process_search_state.search_state.is_enabled = false; - } - self.process_search_state.search_state.current_search_query = String::new(); - self.process_search_state.is_searching_with_pid = false; - self.to_delete_process_list = None; - self.dd_err = None; - } - - pub fn on_esc(&mut self) { - self.reset_multi_tap_keys(); - if self.is_in_dialog() { - self.help_dialog_state.is_showing_help = false; - self.help_dialog_state.current_category = AppHelpCategory::General; - self.delete_dialog_state.is_showing_dd = false; - self.delete_dialog_state.is_on_yes = false; - self.to_delete_process_list = None; - self.dd_err = None; - } else if self.is_filtering_or_searching() { - match self.current_widget_selected { - WidgetPosition::Process | WidgetPosition::ProcessSearch => { - if self.process_search_state.search_state.is_enabled { - self.current_widget_selected = WidgetPosition::Process; - self.process_search_state.search_state.is_enabled = false; - } - } - WidgetPosition::Cpu => { - self.cpu_state.is_showing_tray = false; - } - WidgetPosition::Mem => { - self.mem_state.is_showing_tray = false; - } - WidgetPosition::Network => { - self.net_state.is_showing_tray = false; - } - _ => {} - } - } else if self.is_expanded { - self.is_expanded = false; - self.is_resized = true; - } - } - - fn is_filtering_or_searching(&self) -> bool { - match self.current_widget_selected { - WidgetPosition::Cpu => self.cpu_state.is_showing_tray, - WidgetPosition::Mem => self.mem_state.is_showing_tray, - WidgetPosition::Network => self.net_state.is_showing_tray, - WidgetPosition::Process | WidgetPosition::ProcessSearch => { - self.process_search_state.search_state.is_enabled - } - _ => false, - } - } - - fn reset_multi_tap_keys(&mut self) { - self.awaiting_second_char = false; - self.second_char = None; - } - - fn is_in_dialog(&self) -> bool { - self.help_dialog_state.is_showing_help || self.delete_dialog_state.is_showing_dd - } - - pub fn toggle_grouping(&mut self) { - // Disallow usage whilst in a dialog and only in processes - if !self.is_in_dialog() { - if let WidgetPosition::Process = self.current_widget_selected { - self.enable_grouping = !(self.enable_grouping); - self.update_process_gui = true; - } - } - } - - pub fn on_tab(&mut self) { - match self.current_widget_selected { - WidgetPosition::Process => { - self.toggle_grouping(); - if self.is_grouped() { - self.search_with_name(); - } else { - self.update_process_gui = true; - } - } - WidgetPosition::ProcessSearch => { - if !self.is_grouped() { - if self.process_search_state.is_searching_with_pid { - self.search_with_name(); - } else { - self.search_with_pid(); - } - } - } - _ => {} - } - } - - pub fn is_grouped(&self) -> bool { - self.enable_grouping - } - - pub fn on_space(&mut self) { - match self.current_widget_selected { - WidgetPosition::Cpu => { - let curr_posn = self - .app_scroll_positions - .cpu_scroll_state - .current_scroll_position; - if self.cpu_state.is_showing_tray - && curr_posn < self.data_collection.cpu_harvest.len() as u64 - { - self.cpu_state.core_show_vec[curr_posn as usize] = - !self.cpu_state.core_show_vec[curr_posn as usize]; - } - } - WidgetPosition::Network => {} - _ => {} - } - } - - pub fn on_slash(&mut self) { - if !self.is_in_dialog() { - match self.current_widget_selected { - WidgetPosition::Process | WidgetPosition::ProcessSearch => { - // Toggle on - self.process_search_state.search_state.is_enabled = true; - self.current_widget_selected = WidgetPosition::ProcessSearch; - if self.is_grouped() { - self.search_with_name(); - } - } - WidgetPosition::Cpu => { - self.cpu_state.is_showing_tray = true; - } - // WidgetPosition::Mem => { - // self.mem_state.is_showing_tray = true; - // } - // WidgetPosition::Network => { - // self.net_state.is_showing_tray = true; - // } - _ => {} - } - } - } - - pub fn is_searching(&self) -> bool { - self.process_search_state.search_state.is_enabled - } - - pub fn is_in_search_widget(&self) -> bool { - if let WidgetPosition::ProcessSearch = self.current_widget_selected { - true - } else { - false - } - } - - pub fn search_with_pid(&mut self) { - if !self.is_in_dialog() && self.is_searching() { - self.process_search_state.is_searching_with_pid = true; - self.update_process_gui = true; - } - } - - pub fn search_with_name(&mut self) { - if !self.is_in_dialog() && self.is_searching() { - self.process_search_state.is_searching_with_pid = false; - self.update_process_gui = true; - } - } - - pub fn get_current_search_query(&self) -> &String { - &self.process_search_state.search_state.current_search_query - } - - pub fn toggle_ignore_case(&mut self) { - self.process_search_state.search_toggle_ignore_case(); - self.update_regex(); - self.update_process_gui = true; - } - - pub fn toggle_search_whole_word(&mut self) { - self.process_search_state.search_toggle_whole_word(); - self.update_regex(); - self.update_process_gui = true; - } - - pub fn toggle_search_regex(&mut self) { - self.process_search_state.search_toggle_regex(); - self.update_regex(); - self.update_process_gui = true; - } - - pub fn update_regex(&mut self) { - if self - .process_search_state - .search_state - .current_search_query - .is_empty() - { - self.process_search_state.search_state.is_invalid_search = false; - self.process_search_state.search_state.is_blank_search = true; - } else { - let regex_string = &self.process_search_state.search_state.current_search_query; - let escaped_regex: String; - let final_regex_string = &format!( - "{}{}{}{}", - if self.process_search_state.is_searching_whole_word { - "^" - } else { - "" - }, - if self.process_search_state.is_ignoring_case { - "(?i)" - } else { - "" - }, - if !self.process_search_state.is_searching_with_regex { - escaped_regex = regex::escape(regex_string); - &escaped_regex - } else { - regex_string - }, - if self.process_search_state.is_searching_whole_word { - "$" - } else { - "" - }, - ); - - let new_regex = regex::Regex::new(final_regex_string); - self.process_search_state.search_state.is_blank_search = false; - self.process_search_state.search_state.is_invalid_search = new_regex.is_err(); - - self.process_search_state.search_state.current_regex = Some(new_regex); - } - self.app_scroll_positions - .process_scroll_state - .previous_scroll_position = 0; - self.app_scroll_positions - .process_scroll_state - .current_scroll_position = 0; - } - - pub fn get_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 - } - - /// One of two functions allowed to run while in a dialog... - pub fn on_enter(&mut self) { - if self.delete_dialog_state.is_showing_dd { - if self.delete_dialog_state.is_on_yes { - // If within dd... - if self.dd_err.is_none() { - // Also ensure that we didn't just fail a dd... - let dd_result = self.kill_highlighted_process(); - self.delete_dialog_state.is_on_yes = false; - - // Check if there was an issue... if so, inform the user. - if let Err(dd_err) = dd_result { - self.dd_err = Some(dd_err.to_string()); - } else { - self.delete_dialog_state.is_showing_dd = false; - } - } - } else { - self.delete_dialog_state.is_showing_dd = false; - } - } else if !self.is_in_dialog() { - // Pop-out mode. We ignore if in process search. - - match self.current_widget_selected { - WidgetPosition::ProcessSearch => {} - _ => { - self.is_expanded = true; - self.is_resized = true; - } - } - } - } - - pub fn on_delete(&mut self) { - match self.current_widget_selected { - WidgetPosition::Process => self.start_dd(), - WidgetPosition::ProcessSearch => { - if self.process_search_state.search_state.is_enabled - && self.get_cursor_position() - < self - .process_search_state - .search_state - .current_search_query - .len() - { - self.process_search_state - .search_state - .current_search_query - .remove(self.get_cursor_position()); - - self.process_search_state.search_state.grapheme_cursor = GraphemeCursor::new( - self.get_cursor_position(), - self.process_search_state - .search_state - .current_search_query - .len(), - true, - ); - - self.update_regex(); - self.update_process_gui = true; - } - } - _ => {} - } - } - - /// Deletes an entire word till the next space or end - #[allow(unused_variables)] - pub fn skip_word_backspace(&mut self) { - if let WidgetPosition::ProcessSearch = self.current_widget_selected { - if self.process_search_state.search_state.is_enabled {} - } - } - - pub fn clear_search(&mut self) { - if let WidgetPosition::ProcessSearch = self.current_widget_selected { - self.update_process_gui = true; - self.process_search_state.search_state = AppSearchState::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(); // TODO: [UNWRAP] unwrap in this and walk_back seem sketch - } - - 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 fn on_backspace(&mut self) { - if let WidgetPosition::ProcessSearch = self.current_widget_selected { - if self.process_search_state.search_state.is_enabled && self.get_cursor_position() > 0 { - self.search_walk_back(self.get_cursor_position()); - - let removed_char = self - .process_search_state - .search_state - .current_search_query - .remove(self.get_cursor_position()); - - self.process_search_state.search_state.grapheme_cursor = GraphemeCursor::new( - self.get_cursor_position(), - self.process_search_state - .search_state - .current_search_query - .len(), - true, - ); - - self.process_search_state.search_state.char_cursor_position -= - UnicodeWidthChar::width(removed_char).unwrap_or(0); - self.process_search_state.search_state.cursor_direction = CursorDirection::LEFT; - - self.update_regex(); - self.update_process_gui = true; - } - } - } - - pub fn get_current_regex_matcher( - &self, - ) -> &Option> { - &self.process_search_state.search_state.current_regex - } - - pub fn on_up_key(&mut self) { - if !self.is_in_dialog() { - if let WidgetPosition::ProcessSearch = self.current_widget_selected { - } else { - self.decrement_position_count(); - } - } - } - - pub fn on_down_key(&mut self) { - if !self.is_in_dialog() { - if let WidgetPosition::ProcessSearch = self.current_widget_selected { - } else { - self.increment_position_count(); - } - } - } - - pub fn on_left_key(&mut self) { - if !self.is_in_dialog() { - if let WidgetPosition::ProcessSearch = self.current_widget_selected { - let prev_cursor = self.get_cursor_position(); - self.search_walk_back(self.get_cursor_position()); - if self.get_cursor_position() < prev_cursor { - let str_slice = &self.process_search_state.search_state.current_search_query - [self.get_cursor_position()..prev_cursor]; - self.process_search_state.search_state.char_cursor_position -= - UnicodeWidthStr::width(str_slice); - self.process_search_state.search_state.cursor_direction = CursorDirection::LEFT; - } - } - } else if self.delete_dialog_state.is_showing_dd && !self.delete_dialog_state.is_on_yes { - self.delete_dialog_state.is_on_yes = true; - } - } - - pub fn on_right_key(&mut self) { - if !self.is_in_dialog() { - if let WidgetPosition::ProcessSearch = self.current_widget_selected { - let prev_cursor = self.get_cursor_position(); - self.search_walk_forward(self.get_cursor_position()); - if self.get_cursor_position() > prev_cursor { - let str_slice = &self.process_search_state.search_state.current_search_query - [prev_cursor..self.get_cursor_position()]; - self.process_search_state.search_state.char_cursor_position += - UnicodeWidthStr::width(str_slice); - self.process_search_state.search_state.cursor_direction = - CursorDirection::RIGHT; - } - } - } else if self.delete_dialog_state.is_showing_dd && self.delete_dialog_state.is_on_yes { - self.delete_dialog_state.is_on_yes = false; - } - } - - pub fn skip_cursor_beginning(&mut self) { - if !self.is_in_dialog() { - if let WidgetPosition::ProcessSearch = self.current_widget_selected { - self.process_search_state.search_state.grapheme_cursor = GraphemeCursor::new( - 0, - self.process_search_state - .search_state - .current_search_query - .len(), - true, - ); - self.process_search_state.search_state.char_cursor_position = 0; - self.process_search_state.search_state.cursor_direction = CursorDirection::LEFT; - } - } - } - - pub fn skip_cursor_end(&mut self) { - if !self.is_in_dialog() { - if let WidgetPosition::ProcessSearch = self.current_widget_selected { - self.process_search_state.search_state.grapheme_cursor = GraphemeCursor::new( - self.process_search_state - .search_state - .current_search_query - .len(), - self.process_search_state - .search_state - .current_search_query - .len(), - true, - ); - self.process_search_state.search_state.char_cursor_position = - UnicodeWidthStr::width( - self.process_search_state - .search_state - .current_search_query - .as_str(), - ); - self.process_search_state.search_state.cursor_direction = CursorDirection::RIGHT; - } - } - } - - pub fn start_dd(&mut self) { - if self - .app_scroll_positions - .process_scroll_state - .current_scroll_position - < self.canvas_data.finalized_process_data.len() as u64 - { - let current_process = if self.is_grouped() { - let group_pids = &self.canvas_data.finalized_process_data[self - .app_scroll_positions - .process_scroll_state - .current_scroll_position - as usize] - .group_pids; - - let mut ret = ("".to_string(), group_pids.clone()); - - for pid in group_pids { - if let Some(process) = self.canvas_data.process_data.get(&pid) { - ret.0 = process.name.clone(); - break; - } - } - ret - } else { - let process = self.canvas_data.finalized_process_data[self - .app_scroll_positions - .process_scroll_state - .current_scroll_position - as usize] - .clone(); - (process.name.clone(), vec![process.pid]) - }; - - self.to_delete_process_list = Some(current_process); - self.delete_dialog_state.is_showing_dd = true; - } - - self.reset_multi_tap_keys(); - } - - pub fn on_char_key(&mut self, caught_char: char) { - // Skip control code chars - if caught_char.is_control() { - return; - } - - // Forbid any char key presses when showing a dialog box... - if !self.is_in_dialog() { - let current_key_press_inst = Instant::now(); - if current_key_press_inst - .duration_since(self.last_key_press) - .as_millis() > constants::MAX_KEY_TIMEOUT_IN_MILLISECONDS - { - self.reset_multi_tap_keys(); - } - self.last_key_press = current_key_press_inst; - - if let WidgetPosition::ProcessSearch = self.current_widget_selected { - if UnicodeWidthStr::width( - self.process_search_state - .search_state - .current_search_query - .as_str(), - ) <= MAX_SEARCH_LENGTH - { - self.process_search_state - .search_state - .current_search_query - .insert(self.get_cursor_position(), caught_char); - - self.process_search_state.search_state.grapheme_cursor = GraphemeCursor::new( - self.get_cursor_position(), - self.process_search_state - .search_state - .current_search_query - .len(), - true, - ); - self.search_walk_forward(self.get_cursor_position()); - - self.process_search_state.search_state.char_cursor_position += - UnicodeWidthChar::width(caught_char).unwrap_or(0); - - self.update_regex(); - self.update_process_gui = true; - } - } else { - match caught_char { - '/' => { - self.on_slash(); - } - 'd' => { - if let WidgetPosition::Process = self.current_widget_selected { - let mut is_first_d = true; - if let Some(second_char) = self.second_char { - if self.awaiting_second_char && second_char == 'd' { - is_first_d = false; - self.awaiting_second_char = false; - self.second_char = None; - - self.start_dd(); - } - } - - if is_first_d { - self.awaiting_second_char = true; - self.second_char = Some('d'); - } - } - } - 'g' => { - let mut is_first_g = true; - if let Some(second_char) = self.second_char { - if self.awaiting_second_char && second_char == 'g' { - is_first_g = false; - self.awaiting_second_char = false; - self.second_char = None; - self.skip_to_first(); - } - } - - if is_first_g { - self.awaiting_second_char = true; - self.second_char = Some('g'); - } - } - 'G' => self.skip_to_last(), - 'k' => self.decrement_position_count(), - 'j' => self.increment_position_count(), - 'f' => { - self.is_frozen = !self.is_frozen; - } - 'c' => { - match self.process_sorting_type { - processes::ProcessSorting::CPU => { - self.process_sorting_reverse = !self.process_sorting_reverse - } - _ => { - self.process_sorting_type = processes::ProcessSorting::CPU; - self.process_sorting_reverse = true; - } - } - self.update_process_gui = true; - self.app_scroll_positions - .process_scroll_state - .current_scroll_position = 0; - } - 'm' => { - match self.process_sorting_type { - processes::ProcessSorting::MEM => { - self.process_sorting_reverse = !self.process_sorting_reverse - } - _ => { - self.process_sorting_type = processes::ProcessSorting::MEM; - self.process_sorting_reverse = true; - } - } - self.update_process_gui = true; - self.app_scroll_positions - .process_scroll_state - .current_scroll_position = 0; - } - 'p' => { - // Disable if grouping - if !self.enable_grouping { - match self.process_sorting_type { - processes::ProcessSorting::PID => { - self.process_sorting_reverse = !self.process_sorting_reverse - } - _ => { - self.process_sorting_type = processes::ProcessSorting::PID; - self.process_sorting_reverse = false; - } - } - self.update_process_gui = true; - self.app_scroll_positions - .process_scroll_state - .current_scroll_position = 0; - } - } - 'n' => { - match self.process_sorting_type { - processes::ProcessSorting::NAME => { - self.process_sorting_reverse = !self.process_sorting_reverse - } - _ => { - self.process_sorting_type = processes::ProcessSorting::NAME; - self.process_sorting_reverse = false; - } - } - self.update_process_gui = true; - self.app_scroll_positions - .process_scroll_state - .current_scroll_position = 0; - } - '?' => { - self.help_dialog_state.is_showing_help = true; - } - 'H' => self.move_widget_selection_left(), - 'L' => self.move_widget_selection_right(), - 'K' => self.move_widget_selection_up(), - 'J' => self.move_widget_selection_down(), - ' ' => self.on_space(), - _ => {} - } - - if let Some(second_char) = self.second_char { - if self.awaiting_second_char && caught_char != second_char { - self.awaiting_second_char = false; - } - } - } - } else if self.help_dialog_state.is_showing_help { - match caught_char { - '1' => self.help_dialog_state.current_category = AppHelpCategory::General, - '2' => self.help_dialog_state.current_category = AppHelpCategory::Process, - '3' => self.help_dialog_state.current_category = AppHelpCategory::Search, - _ => {} - } - } - } - - pub fn kill_highlighted_process(&mut self) -> Result<()> { - // Technically unnecessary but this is a good check... - if let WidgetPosition::Process = self.current_widget_selected { - if let Some(current_selected_processes) = &self.to_delete_process_list { - for pid in ¤t_selected_processes.1 { - process_killer::kill_process_given_pid(*pid)?; - } - } - self.to_delete_process_list = None; - } - Ok(()) - } - - pub fn get_to_delete_processes(&self) -> Option<(String, Vec)> { - self.to_delete_process_list.clone() - } - - // For now, these are hard coded --- in the future, they shouldn't be! - // - // General idea for now: - // CPU -(down)> MEM - // MEM -(down)> Network, -(right)> TEMP - // TEMP -(down)> Disk, -(left)> MEM, -(up)> CPU - // Disk -(down)> Processes, -(left)> MEM, -(up)> TEMP - // Network -(up)> MEM, -(right)> PROC - // PROC -(up)> Disk, -(down)> PROC_SEARCH, -(left)> Network - // PROC_SEARCH -(up)> PROC, -(left)> Network - pub fn move_widget_selection_left(&mut self) { - if !self.is_in_dialog() && !self.is_expanded { - self.current_widget_selected = match self.current_widget_selected { - WidgetPosition::Process => WidgetPosition::Network, - WidgetPosition::ProcessSearch => WidgetPosition::Network, - WidgetPosition::Disk => WidgetPosition::Mem, - WidgetPosition::Temp => WidgetPosition::Mem, - _ => self.current_widget_selected, - }; - } - - self.reset_multi_tap_keys(); - } - - pub fn move_widget_selection_right(&mut self) { - if !self.is_in_dialog() && !self.is_expanded { - self.current_widget_selected = match self.current_widget_selected { - WidgetPosition::Mem => WidgetPosition::Temp, - WidgetPosition::Network => WidgetPosition::Process, - _ => self.current_widget_selected, - }; - } - - self.reset_multi_tap_keys(); - } - - pub fn move_widget_selection_up(&mut self) { - if !self.is_in_dialog() && !self.is_expanded { - self.current_widget_selected = match self.current_widget_selected { - WidgetPosition::Mem => WidgetPosition::Cpu, - WidgetPosition::Network => WidgetPosition::Mem, - WidgetPosition::Process => WidgetPosition::Disk, - WidgetPosition::ProcessSearch => WidgetPosition::Process, - WidgetPosition::Temp => WidgetPosition::Cpu, - WidgetPosition::Disk => WidgetPosition::Temp, - _ => self.current_widget_selected, - }; - } else if self.is_expanded { - self.current_widget_selected = match self.current_widget_selected { - WidgetPosition::ProcessSearch => WidgetPosition::Process, - _ => self.current_widget_selected, - }; - } - - self.reset_multi_tap_keys(); - } - - pub fn move_widget_selection_down(&mut self) { - if !self.is_in_dialog() && !self.is_expanded { - self.current_widget_selected = match self.current_widget_selected { - WidgetPosition::Cpu => WidgetPosition::Mem, - WidgetPosition::Mem => WidgetPosition::Network, - WidgetPosition::Temp => WidgetPosition::Disk, - WidgetPosition::Disk => WidgetPosition::Process, - WidgetPosition::Process => { - if self.is_searching() { - WidgetPosition::ProcessSearch - } else { - WidgetPosition::Process - } - } - _ => self.current_widget_selected, - }; - } else if self.is_expanded { - self.current_widget_selected = match self.current_widget_selected { - WidgetPosition::Process => { - if self.is_searching() { - WidgetPosition::ProcessSearch - } else { - WidgetPosition::Process - } - } - _ => self.current_widget_selected, - }; - } - - self.reset_multi_tap_keys(); - } - - pub fn skip_to_first(&mut self) { - if !self.is_in_dialog() { - match self.current_widget_selected { - WidgetPosition::Process => { - self.app_scroll_positions - .process_scroll_state - .current_scroll_position = 0 - } - WidgetPosition::Temp => { - self.app_scroll_positions - .temp_scroll_state - .current_scroll_position = 0 - } - WidgetPosition::Disk => { - self.app_scroll_positions - .disk_scroll_state - .current_scroll_position = 0 - } - WidgetPosition::Cpu => { - self.app_scroll_positions - .cpu_scroll_state - .current_scroll_position = 0 - } - - _ => {} - } - self.app_scroll_positions.scroll_direction = ScrollDirection::UP; - self.reset_multi_tap_keys(); - } - } - - pub fn skip_to_last(&mut self) { - if !self.is_in_dialog() { - match self.current_widget_selected { - WidgetPosition::Process => { - self.app_scroll_positions - .process_scroll_state - .current_scroll_position = self.canvas_data.finalized_process_data.len() as u64 - 1 - } - WidgetPosition::Temp => { - self.app_scroll_positions - .temp_scroll_state - .current_scroll_position = self.canvas_data.temp_sensor_data.len() as u64 - 1 - } - WidgetPosition::Disk => { - self.app_scroll_positions - .disk_scroll_state - .current_scroll_position = self.canvas_data.disk_data.len() as u64 - 1 - } - WidgetPosition::Cpu => { - self.app_scroll_positions - .cpu_scroll_state - .current_scroll_position = self.canvas_data.cpu_data.len() as u64 - 1; - } - _ => {} - } - self.app_scroll_positions.scroll_direction = ScrollDirection::DOWN; - self.reset_multi_tap_keys(); - } - } - - pub fn decrement_position_count(&mut self) { - if !self.is_in_dialog() { - match self.current_widget_selected { - WidgetPosition::Process => self.change_process_position(-1), - WidgetPosition::Temp => self.change_temp_position(-1), - WidgetPosition::Disk => self.change_disk_position(-1), - WidgetPosition::Cpu => self.change_cpu_table_position(-1), // TODO: [PO?] Temporary, may change if we add scaling - _ => {} - } - self.app_scroll_positions.scroll_direction = ScrollDirection::UP; - self.reset_multi_tap_keys(); - } - } - - pub fn increment_position_count(&mut self) { - if !self.is_in_dialog() { - match self.current_widget_selected { - WidgetPosition::Process => self.change_process_position(1), - WidgetPosition::Temp => self.change_temp_position(1), - WidgetPosition::Disk => self.change_disk_position(1), - WidgetPosition::Cpu => self.change_cpu_table_position(1), // TODO: [PO?] Temporary, may change if we add scaling - _ => {} - } - self.app_scroll_positions.scroll_direction = ScrollDirection::DOWN; - self.reset_multi_tap_keys(); - } - } - - fn change_cpu_table_position(&mut self, num_to_change_by: i64) { - let current_posn = self - .app_scroll_positions - .cpu_scroll_state - .current_scroll_position; - - if current_posn as i64 + num_to_change_by >= 0 - && current_posn as i64 + num_to_change_by < self.canvas_data.cpu_data.len() as i64 - { - self.app_scroll_positions - .cpu_scroll_state - .current_scroll_position = (current_posn as i64 + num_to_change_by) as u64; - } - } - - fn change_process_position(&mut self, num_to_change_by: i64) { - let current_posn = self - .app_scroll_positions - .process_scroll_state - .current_scroll_position; - - if current_posn as i64 + num_to_change_by >= 0 - && current_posn as i64 + num_to_change_by - < self.canvas_data.finalized_process_data.len() as i64 - { - self.app_scroll_positions - .process_scroll_state - .current_scroll_position = (current_posn as i64 + num_to_change_by) as u64; - } - } - - fn change_temp_position(&mut self, num_to_change_by: i64) { - let current_posn = self - .app_scroll_positions - .temp_scroll_state - .current_scroll_position; - - if current_posn as i64 + num_to_change_by >= 0 - && current_posn as i64 + num_to_change_by - < self.canvas_data.temp_sensor_data.len() as i64 - { - self.app_scroll_positions - .temp_scroll_state - .current_scroll_position = (current_posn as i64 + num_to_change_by) as u64; - } - } - - fn change_disk_position(&mut self, num_to_change_by: i64) { - let current_posn = self - .app_scroll_positions - .disk_scroll_state - .current_scroll_position; - - if current_posn as i64 + num_to_change_by >= 0 - && current_posn as i64 + num_to_change_by < self.canvas_data.disk_data.len() as i64 - { - self.app_scroll_positions - .disk_scroll_state - .current_scroll_position = (current_posn as i64 + num_to_change_by) as u64; - } - } + #[allow(clippy::too_many_arguments)] + pub fn new( + show_average_cpu: bool, temperature_type: temperature::TemperatureType, + update_rate_in_milliseconds: u64, use_dot: bool, left_legend: bool, + use_current_cpu_total: bool, current_widget_selected: WidgetPosition, + show_disabled_data: bool, use_basic_mode: bool, + ) -> App { + App { + process_sorting_type: processes::ProcessSorting::CPU, + process_sorting_reverse: true, + update_process_gui: false, + current_widget_selected: if use_basic_mode { + match current_widget_selected { + WidgetPosition::Cpu => WidgetPosition::BasicCpu, + WidgetPosition::Network => WidgetPosition::BasicNet, + WidgetPosition::Mem => WidgetPosition::BasicMem, + _ => current_widget_selected, + } + } else { + current_widget_selected + }, + previous_basic_table_selected: if current_widget_selected.is_widget_table() { + current_widget_selected + } else { + WidgetPosition::Process + }, + app_scroll_positions: AppScrollState::default(), + awaiting_second_char: false, + second_char: None, + dd_err: None, + to_delete_process_list: None, + is_frozen: false, + last_key_press: Instant::now(), + canvas_data: canvas::DisplayableData::default(), + enable_grouping: false, + data_collection: DataCollection::default(), + process_search_state: ProcessSearchState::default(), + delete_dialog_state: AppDeleteDialogState::default(), + help_dialog_state: AppHelpDialogState::default(), + app_config_fields: AppConfigFields { + show_average_cpu, + temperature_type, + use_dot, + update_rate_in_milliseconds, + left_legend, + use_current_cpu_total, + show_disabled_data, + use_basic_mode, + }, + is_expanded: false, + is_resized: false, + cpu_state: CpuState::default(), + mem_state: MemState::default(), + net_state: NetworkState::default(), + } + } + + pub fn reset(&mut self) { + self.reset_multi_tap_keys(); + self.help_dialog_state.is_showing_help = false; + self.delete_dialog_state.is_showing_dd = false; + if self.process_search_state.search_state.is_enabled { + self.current_widget_selected = WidgetPosition::Process; + self.process_search_state.search_state.is_enabled = false; + } + self.process_search_state.search_state.current_search_query = String::new(); + self.process_search_state.is_searching_with_pid = false; + self.to_delete_process_list = None; + self.dd_err = None; + } + + pub fn on_esc(&mut self) { + self.reset_multi_tap_keys(); + if self.is_in_dialog() { + self.help_dialog_state.is_showing_help = false; + self.help_dialog_state.current_category = AppHelpCategory::General; + self.delete_dialog_state.is_showing_dd = false; + self.delete_dialog_state.is_on_yes = false; + self.to_delete_process_list = None; + self.dd_err = None; + } else if self.is_filtering_or_searching() { + match self.current_widget_selected { + WidgetPosition::Cpu + if self.is_expanded && self.app_config_fields.use_basic_mode => + { + self.current_widget_selected = WidgetPosition::BasicCpu; + self.cpu_state.is_showing_tray = false; + } + WidgetPosition::Process | WidgetPosition::ProcessSearch => { + if self.process_search_state.search_state.is_enabled { + self.current_widget_selected = WidgetPosition::Process; + self.process_search_state.search_state.is_enabled = false; + } + } + WidgetPosition::Cpu => { + self.cpu_state.is_showing_tray = false; + } + WidgetPosition::Mem => { + self.mem_state.is_showing_tray = false; + } + WidgetPosition::Network => { + self.net_state.is_showing_tray = false; + } + _ => {} + } + } else if self.is_expanded { + self.is_expanded = false; + self.is_resized = true; + } + } + + fn is_filtering_or_searching(&self) -> bool { + match self.current_widget_selected { + WidgetPosition::Cpu => self.cpu_state.is_showing_tray, + WidgetPosition::Mem => self.mem_state.is_showing_tray, + WidgetPosition::Network => self.net_state.is_showing_tray, + WidgetPosition::Process | WidgetPosition::ProcessSearch => { + self.process_search_state.search_state.is_enabled + } + _ => false, + } + } + + fn reset_multi_tap_keys(&mut self) { + self.awaiting_second_char = false; + self.second_char = None; + } + + fn is_in_dialog(&self) -> bool { + self.help_dialog_state.is_showing_help || self.delete_dialog_state.is_showing_dd + } + + pub fn toggle_grouping(&mut self) { + // Disallow usage whilst in a dialog and only in processes + if !self.is_in_dialog() { + if let WidgetPosition::Process = self.current_widget_selected { + self.enable_grouping = !(self.enable_grouping); + self.update_process_gui = true; + } + } + } + + pub fn on_tab(&mut self) { + match self.current_widget_selected { + WidgetPosition::Process => { + self.toggle_grouping(); + if self.is_grouped() { + self.search_with_name(); + } else { + self.update_process_gui = true; + } + } + WidgetPosition::ProcessSearch => { + if !self.is_grouped() { + if self.process_search_state.is_searching_with_pid { + self.search_with_name(); + } else { + self.search_with_pid(); + } + } + } + _ => {} + } + } + + pub fn is_grouped(&self) -> bool { + self.enable_grouping + } + + pub fn on_space(&mut self) { + match self.current_widget_selected { + WidgetPosition::Cpu => { + let curr_posn = self + .app_scroll_positions + .cpu_scroll_state + .current_scroll_position; + if self.cpu_state.is_showing_tray + && curr_posn < self.data_collection.cpu_harvest.len() as u64 + { + self.cpu_state.core_show_vec[curr_posn as usize] = + !self.cpu_state.core_show_vec[curr_posn as usize]; + } + } + WidgetPosition::Network => {} + _ => {} + } + } + + pub fn on_slash(&mut self) { + if !self.is_in_dialog() { + match self.current_widget_selected { + WidgetPosition::BasicCpu if self.is_expanded => { + self.current_widget_selected = WidgetPosition::Cpu; + self.cpu_state.is_showing_tray = true; + } + WidgetPosition::Process | WidgetPosition::ProcessSearch => { + // Toggle on + self.process_search_state.search_state.is_enabled = true; + self.current_widget_selected = WidgetPosition::ProcessSearch; + if self.is_grouped() { + self.search_with_name(); + } + } + WidgetPosition::Cpu => { + self.cpu_state.is_showing_tray = true; + } + // WidgetPosition::Mem => { + // self.mem_state.is_showing_tray = true; + // } + // WidgetPosition::Network => { + // self.net_state.is_showing_tray = true; + // } + _ => {} + } + } + } + + pub fn is_searching(&self) -> bool { + self.process_search_state.search_state.is_enabled + } + + pub fn is_in_search_widget(&self) -> bool { + if let WidgetPosition::ProcessSearch = self.current_widget_selected { + true + } else { + false + } + } + + pub fn search_with_pid(&mut self) { + if !self.is_in_dialog() && self.is_searching() { + self.process_search_state.is_searching_with_pid = true; + self.update_process_gui = true; + } + } + + pub fn search_with_name(&mut self) { + if !self.is_in_dialog() && self.is_searching() { + self.process_search_state.is_searching_with_pid = false; + self.update_process_gui = true; + } + } + + pub fn get_current_search_query(&self) -> &String { + &self.process_search_state.search_state.current_search_query + } + + pub fn toggle_ignore_case(&mut self) { + self.process_search_state.search_toggle_ignore_case(); + self.update_regex(); + self.update_process_gui = true; + } + + pub fn toggle_search_whole_word(&mut self) { + self.process_search_state.search_toggle_whole_word(); + self.update_regex(); + self.update_process_gui = true; + } + + pub fn toggle_search_regex(&mut self) { + self.process_search_state.search_toggle_regex(); + self.update_regex(); + self.update_process_gui = true; + } + + pub fn update_regex(&mut self) { + if self + .process_search_state + .search_state + .current_search_query + .is_empty() + { + self.process_search_state.search_state.is_invalid_search = false; + self.process_search_state.search_state.is_blank_search = true; + } else { + let regex_string = &self.process_search_state.search_state.current_search_query; + let escaped_regex: String; + let final_regex_string = &format!( + "{}{}{}{}", + if self.process_search_state.is_searching_whole_word { + "^" + } else { + "" + }, + if self.process_search_state.is_ignoring_case { + "(?i)" + } else { + "" + }, + if !self.process_search_state.is_searching_with_regex { + escaped_regex = regex::escape(regex_string); + &escaped_regex + } else { + regex_string + }, + if self.process_search_state.is_searching_whole_word { + "$" + } else { + "" + }, + ); + + let new_regex = regex::Regex::new(final_regex_string); + self.process_search_state.search_state.is_blank_search = false; + self.process_search_state.search_state.is_invalid_search = new_regex.is_err(); + + self.process_search_state.search_state.current_regex = Some(new_regex); + } + self.app_scroll_positions + .process_scroll_state + .previous_scroll_position = 0; + self.app_scroll_positions + .process_scroll_state + .current_scroll_position = 0; + } + + pub fn get_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 + } + + /// One of two functions allowed to run while in a dialog... + pub fn on_enter(&mut self) { + if self.delete_dialog_state.is_showing_dd { + if self.delete_dialog_state.is_on_yes { + // If within dd... + if self.dd_err.is_none() { + // Also ensure that we didn't just fail a dd... + let dd_result = self.kill_highlighted_process(); + self.delete_dialog_state.is_on_yes = false; + + // Check if there was an issue... if so, inform the user. + if let Err(dd_err) = dd_result { + self.dd_err = Some(dd_err.to_string()); + } else { + self.delete_dialog_state.is_showing_dd = false; + } + } + } else { + self.delete_dialog_state.is_showing_dd = false; + } + } else if !self.is_in_dialog() { + // Pop-out mode. We ignore if in process search. + + match self.current_widget_selected { + WidgetPosition::ProcessSearch => {} + _ => { + self.is_expanded = true; + self.is_resized = true; + } + } + } + } + + pub fn on_delete(&mut self) { + match self.current_widget_selected { + WidgetPosition::Process => self.start_dd(), + WidgetPosition::ProcessSearch => { + if self.process_search_state.search_state.is_enabled + && self.get_cursor_position() + < self + .process_search_state + .search_state + .current_search_query + .len() + { + self.process_search_state + .search_state + .current_search_query + .remove(self.get_cursor_position()); + + self.process_search_state.search_state.grapheme_cursor = GraphemeCursor::new( + self.get_cursor_position(), + self.process_search_state + .search_state + .current_search_query + .len(), + true, + ); + + self.update_regex(); + self.update_process_gui = true; + } + } + _ => {} + } + } + + /// Deletes an entire word till the next space or end + #[allow(unused_variables)] + pub fn skip_word_backspace(&mut self) { + if let WidgetPosition::ProcessSearch = self.current_widget_selected { + if self.process_search_state.search_state.is_enabled {} + } + } + + pub fn clear_search(&mut self) { + if let WidgetPosition::ProcessSearch = self.current_widget_selected { + self.update_process_gui = true; + self.process_search_state.search_state = AppSearchState::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(); // TODO: [UNWRAP] unwrap in this and walk_back seem sketch + } + + 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 fn on_backspace(&mut self) { + if let WidgetPosition::ProcessSearch = self.current_widget_selected { + if self.process_search_state.search_state.is_enabled && self.get_cursor_position() > 0 { + self.search_walk_back(self.get_cursor_position()); + + let removed_char = self + .process_search_state + .search_state + .current_search_query + .remove(self.get_cursor_position()); + + self.process_search_state.search_state.grapheme_cursor = GraphemeCursor::new( + self.get_cursor_position(), + self.process_search_state + .search_state + .current_search_query + .len(), + true, + ); + + self.process_search_state.search_state.char_cursor_position -= + UnicodeWidthChar::width(removed_char).unwrap_or(0); + self.process_search_state.search_state.cursor_direction = CursorDirection::LEFT; + + self.update_regex(); + self.update_process_gui = true; + } + } + } + + pub fn get_current_regex_matcher( + &self, + ) -> &Option> { + &self.process_search_state.search_state.current_regex + } + + pub fn on_up_key(&mut self) { + if !self.is_in_dialog() { + if let WidgetPosition::ProcessSearch = self.current_widget_selected { + } else { + self.decrement_position_count(); + } + } + } + + pub fn on_down_key(&mut self) { + if !self.is_in_dialog() { + if let WidgetPosition::ProcessSearch = self.current_widget_selected { + } else { + self.increment_position_count(); + } + } + } + + pub fn on_left_key(&mut self) { + if !self.is_in_dialog() { + if let WidgetPosition::ProcessSearch = self.current_widget_selected { + let prev_cursor = self.get_cursor_position(); + self.search_walk_back(self.get_cursor_position()); + if self.get_cursor_position() < prev_cursor { + let str_slice = &self.process_search_state.search_state.current_search_query + [self.get_cursor_position()..prev_cursor]; + self.process_search_state.search_state.char_cursor_position -= + UnicodeWidthStr::width(str_slice); + self.process_search_state.search_state.cursor_direction = CursorDirection::LEFT; + } + } + } else if self.delete_dialog_state.is_showing_dd && !self.delete_dialog_state.is_on_yes { + self.delete_dialog_state.is_on_yes = true; + } + } + + pub fn on_right_key(&mut self) { + if !self.is_in_dialog() { + if let WidgetPosition::ProcessSearch = self.current_widget_selected { + let prev_cursor = self.get_cursor_position(); + self.search_walk_forward(self.get_cursor_position()); + if self.get_cursor_position() > prev_cursor { + let str_slice = &self.process_search_state.search_state.current_search_query + [prev_cursor..self.get_cursor_position()]; + self.process_search_state.search_state.char_cursor_position += + UnicodeWidthStr::width(str_slice); + self.process_search_state.search_state.cursor_direction = + CursorDirection::RIGHT; + } + } + } else if self.delete_dialog_state.is_showing_dd && self.delete_dialog_state.is_on_yes { + self.delete_dialog_state.is_on_yes = false; + } + } + + pub fn skip_cursor_beginning(&mut self) { + if !self.is_in_dialog() { + if let WidgetPosition::ProcessSearch = self.current_widget_selected { + self.process_search_state.search_state.grapheme_cursor = GraphemeCursor::new( + 0, + self.process_search_state + .search_state + .current_search_query + .len(), + true, + ); + self.process_search_state.search_state.char_cursor_position = 0; + self.process_search_state.search_state.cursor_direction = CursorDirection::LEFT; + } + } + } + + pub fn skip_cursor_end(&mut self) { + if !self.is_in_dialog() { + if let WidgetPosition::ProcessSearch = self.current_widget_selected { + self.process_search_state.search_state.grapheme_cursor = GraphemeCursor::new( + self.process_search_state + .search_state + .current_search_query + .len(), + self.process_search_state + .search_state + .current_search_query + .len(), + true, + ); + self.process_search_state.search_state.char_cursor_position = + UnicodeWidthStr::width( + self.process_search_state + .search_state + .current_search_query + .as_str(), + ); + self.process_search_state.search_state.cursor_direction = CursorDirection::RIGHT; + } + } + } + + pub fn start_dd(&mut self) { + if self + .app_scroll_positions + .process_scroll_state + .current_scroll_position + < self.canvas_data.finalized_process_data.len() as u64 + { + let current_process = if self.is_grouped() { + let group_pids = &self.canvas_data.finalized_process_data[self + .app_scroll_positions + .process_scroll_state + .current_scroll_position + as usize] + .group_pids; + + let mut ret = ("".to_string(), group_pids.clone()); + + for pid in group_pids { + if let Some(process) = self.canvas_data.process_data.get(&pid) { + ret.0 = process.name.clone(); + break; + } + } + ret + } else { + let process = self.canvas_data.finalized_process_data[self + .app_scroll_positions + .process_scroll_state + .current_scroll_position + as usize] + .clone(); + (process.name.clone(), vec![process.pid]) + }; + + self.to_delete_process_list = Some(current_process); + self.delete_dialog_state.is_showing_dd = true; + } + + self.reset_multi_tap_keys(); + } + + pub fn on_char_key(&mut self, caught_char: char) { + // Skip control code chars + if caught_char.is_control() { + return; + } + + // Forbid any char key presses when showing a dialog box... + if !self.is_in_dialog() { + let current_key_press_inst = Instant::now(); + if current_key_press_inst + .duration_since(self.last_key_press) + .as_millis() + > constants::MAX_KEY_TIMEOUT_IN_MILLISECONDS + { + self.reset_multi_tap_keys(); + } + self.last_key_press = current_key_press_inst; + + if let WidgetPosition::ProcessSearch = self.current_widget_selected { + if UnicodeWidthStr::width( + self.process_search_state + .search_state + .current_search_query + .as_str(), + ) <= MAX_SEARCH_LENGTH + { + self.process_search_state + .search_state + .current_search_query + .insert(self.get_cursor_position(), caught_char); + + self.process_search_state.search_state.grapheme_cursor = GraphemeCursor::new( + self.get_cursor_position(), + self.process_search_state + .search_state + .current_search_query + .len(), + true, + ); + self.search_walk_forward(self.get_cursor_position()); + + self.process_search_state.search_state.char_cursor_position += + UnicodeWidthChar::width(caught_char).unwrap_or(0); + + self.update_regex(); + self.update_process_gui = true; + } + } else { + match caught_char { + '/' => { + self.on_slash(); + } + 'd' => { + if let WidgetPosition::Process = self.current_widget_selected { + let mut is_first_d = true; + if let Some(second_char) = self.second_char { + if self.awaiting_second_char && second_char == 'd' { + is_first_d = false; + self.awaiting_second_char = false; + self.second_char = None; + + self.start_dd(); + } + } + + if is_first_d { + self.awaiting_second_char = true; + self.second_char = Some('d'); + } + } + } + 'g' => { + let mut is_first_g = true; + if let Some(second_char) = self.second_char { + if self.awaiting_second_char && second_char == 'g' { + is_first_g = false; + self.awaiting_second_char = false; + self.second_char = None; + self.skip_to_first(); + } + } + + if is_first_g { + self.awaiting_second_char = true; + self.second_char = Some('g'); + } + } + 'G' => self.skip_to_last(), + 'k' => self.decrement_position_count(), + 'j' => self.increment_position_count(), + 'f' => { + self.is_frozen = !self.is_frozen; + } + 'c' => { + match self.process_sorting_type { + processes::ProcessSorting::CPU => { + self.process_sorting_reverse = !self.process_sorting_reverse + } + _ => { + self.process_sorting_type = processes::ProcessSorting::CPU; + self.process_sorting_reverse = true; + } + } + self.update_process_gui = true; + self.app_scroll_positions + .process_scroll_state + .current_scroll_position = 0; + } + 'm' => { + match self.process_sorting_type { + processes::ProcessSorting::MEM => { + self.process_sorting_reverse = !self.process_sorting_reverse + } + _ => { + self.process_sorting_type = processes::ProcessSorting::MEM; + self.process_sorting_reverse = true; + } + } + self.update_process_gui = true; + self.app_scroll_positions + .process_scroll_state + .current_scroll_position = 0; + } + 'p' => { + // Disable if grouping + if !self.enable_grouping { + match self.process_sorting_type { + processes::ProcessSorting::PID => { + self.process_sorting_reverse = !self.process_sorting_reverse + } + _ => { + self.process_sorting_type = processes::ProcessSorting::PID; + self.process_sorting_reverse = false; + } + } + self.update_process_gui = true; + self.app_scroll_positions + .process_scroll_state + .current_scroll_position = 0; + } + } + 'n' => { + match self.process_sorting_type { + processes::ProcessSorting::NAME => { + self.process_sorting_reverse = !self.process_sorting_reverse + } + _ => { + self.process_sorting_type = processes::ProcessSorting::NAME; + self.process_sorting_reverse = false; + } + } + self.update_process_gui = true; + self.app_scroll_positions + .process_scroll_state + .current_scroll_position = 0; + } + '?' => { + self.help_dialog_state.is_showing_help = true; + } + 'H' => self.move_widget_selection_left(), + 'L' => self.move_widget_selection_right(), + 'K' => self.move_widget_selection_up(), + 'J' => self.move_widget_selection_down(), + ' ' => self.on_space(), + _ => {} + } + + if let Some(second_char) = self.second_char { + if self.awaiting_second_char && caught_char != second_char { + self.awaiting_second_char = false; + } + } + } + } else if self.help_dialog_state.is_showing_help { + match caught_char { + '1' => self.help_dialog_state.current_category = AppHelpCategory::General, + '2' => self.help_dialog_state.current_category = AppHelpCategory::Process, + '3' => self.help_dialog_state.current_category = AppHelpCategory::Search, + _ => {} + } + } + } + + pub fn kill_highlighted_process(&mut self) -> Result<()> { + // Technically unnecessary but this is a good check... + if let WidgetPosition::Process = self.current_widget_selected { + if let Some(current_selected_processes) = &self.to_delete_process_list { + for pid in ¤t_selected_processes.1 { + process_killer::kill_process_given_pid(*pid)?; + } + } + self.to_delete_process_list = None; + } + Ok(()) + } + + pub fn get_to_delete_processes(&self) -> Option<(String, Vec)> { + self.to_delete_process_list.clone() + } + + // TODO: [MODULARITY] Do NOT hard code this in thu future! + // + // General idea for now: + // CPU -(down)> MEM + // MEM -(down)> Network, -(right)> TEMP + // TEMP -(down)> Disk, -(left)> MEM, -(up)> CPU + // Disk -(down)> Processes, -(left)> MEM, -(up)> TEMP + // Network -(up)> MEM, -(right)> PROC + // PROC -(up)> Disk, -(down)> PROC_SEARCH, -(left)> Network + // PROC_SEARCH -(up)> PROC, -(left)> Network + pub fn move_widget_selection_left(&mut self) { + if !self.is_in_dialog() && !self.is_expanded { + if self.app_config_fields.use_basic_mode { + self.current_widget_selected = match self.current_widget_selected { + WidgetPosition::BasicNet => WidgetPosition::BasicMem, + WidgetPosition::Process => WidgetPosition::Disk, + WidgetPosition::ProcessSearch => WidgetPosition::Disk, + WidgetPosition::Disk => WidgetPosition::Temp, + WidgetPosition::Temp => WidgetPosition::Process, + _ => self.current_widget_selected, + }; + } else { + self.current_widget_selected = match self.current_widget_selected { + WidgetPosition::Process => WidgetPosition::Network, + WidgetPosition::ProcessSearch => WidgetPosition::Network, + WidgetPosition::Disk => WidgetPosition::Mem, + WidgetPosition::Temp => WidgetPosition::Mem, + _ => self.current_widget_selected, + }; + } + } + + self.reset_multi_tap_keys(); + } + + pub fn move_widget_selection_right(&mut self) { + if !self.is_in_dialog() && !self.is_expanded { + if self.app_config_fields.use_basic_mode { + self.current_widget_selected = match self.current_widget_selected { + WidgetPosition::BasicMem => WidgetPosition::BasicNet, + WidgetPosition::Process => WidgetPosition::Temp, + WidgetPosition::ProcessSearch => WidgetPosition::Temp, + WidgetPosition::Disk => WidgetPosition::Process, + WidgetPosition::Temp => WidgetPosition::Disk, + _ => self.current_widget_selected, + }; + } else { + self.current_widget_selected = match self.current_widget_selected { + WidgetPosition::Mem => WidgetPosition::Temp, + WidgetPosition::Network => WidgetPosition::Process, + _ => self.current_widget_selected, + }; + } + } + + self.reset_multi_tap_keys(); + } + + pub fn move_widget_selection_up(&mut self) { + if !self.is_in_dialog() && !self.is_expanded { + if self.app_config_fields.use_basic_mode { + if self.current_widget_selected.is_widget_table() { + self.previous_basic_table_selected = self.current_widget_selected; + } + self.current_widget_selected = match self.current_widget_selected { + WidgetPosition::BasicMem => WidgetPosition::BasicCpu, + WidgetPosition::BasicNet => WidgetPosition::BasicCpu, + WidgetPosition::Process => WidgetPosition::BasicMem, + WidgetPosition::ProcessSearch => WidgetPosition::Process, + WidgetPosition::Temp => WidgetPosition::BasicMem, + WidgetPosition::Disk => WidgetPosition::BasicMem, + _ => self.current_widget_selected, + }; + } else { + self.current_widget_selected = match self.current_widget_selected { + WidgetPosition::Mem => WidgetPosition::Cpu, + WidgetPosition::Network => WidgetPosition::Mem, + WidgetPosition::Process => WidgetPosition::Disk, + WidgetPosition::ProcessSearch => WidgetPosition::Process, + WidgetPosition::Temp => WidgetPosition::Cpu, + WidgetPosition::Disk => WidgetPosition::Temp, + _ => self.current_widget_selected, + }; + } + } else if self.is_expanded { + self.current_widget_selected = match self.current_widget_selected { + WidgetPosition::ProcessSearch => WidgetPosition::Process, + _ => self.current_widget_selected, + }; + } + + self.reset_multi_tap_keys(); + } + + pub fn move_widget_selection_down(&mut self) { + if !self.is_in_dialog() && !self.is_expanded { + if self.app_config_fields.use_basic_mode { + self.current_widget_selected = match self.current_widget_selected { + WidgetPosition::BasicMem => self.previous_basic_table_selected, + WidgetPosition::BasicNet => self.previous_basic_table_selected, + WidgetPosition::BasicCpu => WidgetPosition::BasicMem, + WidgetPosition::Process => { + if self.is_searching() { + WidgetPosition::ProcessSearch + } else { + WidgetPosition::Process + } + } + _ => self.current_widget_selected, + }; + } else { + self.current_widget_selected = match self.current_widget_selected { + WidgetPosition::Cpu => WidgetPosition::Mem, + WidgetPosition::Mem => WidgetPosition::Network, + WidgetPosition::Temp => WidgetPosition::Disk, + WidgetPosition::Disk => WidgetPosition::Process, + WidgetPosition::Process => { + if self.is_searching() { + WidgetPosition::ProcessSearch + } else { + WidgetPosition::Process + } + } + _ => self.current_widget_selected, + }; + } + } else if self.is_expanded { + self.current_widget_selected = match self.current_widget_selected { + WidgetPosition::Process => { + if self.is_searching() { + WidgetPosition::ProcessSearch + } else { + WidgetPosition::Process + } + } + _ => self.current_widget_selected, + }; + } + + self.reset_multi_tap_keys(); + } + + pub fn skip_to_first(&mut self) { + if !self.is_in_dialog() { + match self.current_widget_selected { + WidgetPosition::Process => { + self.app_scroll_positions + .process_scroll_state + .current_scroll_position = 0 + } + WidgetPosition::Temp => { + self.app_scroll_positions + .temp_scroll_state + .current_scroll_position = 0 + } + WidgetPosition::Disk => { + self.app_scroll_positions + .disk_scroll_state + .current_scroll_position = 0 + } + WidgetPosition::Cpu => { + self.app_scroll_positions + .cpu_scroll_state + .current_scroll_position = 0 + } + + _ => {} + } + self.app_scroll_positions.scroll_direction = ScrollDirection::UP; + self.reset_multi_tap_keys(); + } + } + + pub fn skip_to_last(&mut self) { + if !self.is_in_dialog() { + match self.current_widget_selected { + WidgetPosition::Process => { + self.app_scroll_positions + .process_scroll_state + .current_scroll_position = + self.canvas_data.finalized_process_data.len() as u64 - 1 + } + WidgetPosition::Temp => { + self.app_scroll_positions + .temp_scroll_state + .current_scroll_position = + self.canvas_data.temp_sensor_data.len() as u64 - 1 + } + WidgetPosition::Disk => { + self.app_scroll_positions + .disk_scroll_state + .current_scroll_position = self.canvas_data.disk_data.len() as u64 - 1 + } + WidgetPosition::Cpu => { + self.app_scroll_positions + .cpu_scroll_state + .current_scroll_position = self.canvas_data.cpu_data.len() as u64 - 1; + } + _ => {} + } + self.app_scroll_positions.scroll_direction = ScrollDirection::DOWN; + self.reset_multi_tap_keys(); + } + } + + pub fn decrement_position_count(&mut self) { + if !self.is_in_dialog() { + match self.current_widget_selected { + WidgetPosition::Process => self.change_process_position(-1), + WidgetPosition::Temp => self.change_temp_position(-1), + WidgetPosition::Disk => self.change_disk_position(-1), + WidgetPosition::Cpu => self.change_cpu_table_position(-1), // TODO: [PO?] Temporary, may change if we add scaling + _ => {} + } + self.app_scroll_positions.scroll_direction = ScrollDirection::UP; + self.reset_multi_tap_keys(); + } + } + + pub fn increment_position_count(&mut self) { + if !self.is_in_dialog() { + match self.current_widget_selected { + WidgetPosition::Process => self.change_process_position(1), + WidgetPosition::Temp => self.change_temp_position(1), + WidgetPosition::Disk => self.change_disk_position(1), + WidgetPosition::Cpu => self.change_cpu_table_position(1), // TODO: [PO?] Temporary, may change if we add scaling + _ => {} + } + self.app_scroll_positions.scroll_direction = ScrollDirection::DOWN; + self.reset_multi_tap_keys(); + } + } + + fn change_cpu_table_position(&mut self, num_to_change_by: i64) { + let current_posn = self + .app_scroll_positions + .cpu_scroll_state + .current_scroll_position; + + if current_posn as i64 + num_to_change_by >= 0 + && current_posn as i64 + num_to_change_by < self.canvas_data.cpu_data.len() as i64 + { + self.app_scroll_positions + .cpu_scroll_state + .current_scroll_position = (current_posn as i64 + num_to_change_by) as u64; + } + } + + fn change_process_position(&mut self, num_to_change_by: i64) { + let current_posn = self + .app_scroll_positions + .process_scroll_state + .current_scroll_position; + + if current_posn as i64 + num_to_change_by >= 0 + && current_posn as i64 + num_to_change_by + < self.canvas_data.finalized_process_data.len() as i64 + { + self.app_scroll_positions + .process_scroll_state + .current_scroll_position = (current_posn as i64 + num_to_change_by) as u64; + } + } + + fn change_temp_position(&mut self, num_to_change_by: i64) { + let current_posn = self + .app_scroll_positions + .temp_scroll_state + .current_scroll_position; + + if current_posn as i64 + num_to_change_by >= 0 + && current_posn as i64 + num_to_change_by + < self.canvas_data.temp_sensor_data.len() as i64 + { + self.app_scroll_positions + .temp_scroll_state + .current_scroll_position = (current_posn as i64 + num_to_change_by) as u64; + } + } + + fn change_disk_position(&mut self, num_to_change_by: i64) { + let current_posn = self + .app_scroll_positions + .disk_scroll_state + .current_scroll_position; + + if current_posn as i64 + num_to_change_by >= 0 + && current_posn as i64 + num_to_change_by < self.canvas_data.disk_data.len() as i64 + { + self.app_scroll_positions + .disk_scroll_state + .current_scroll_position = (current_posn as i64 + num_to_change_by) as u64; + } + } } diff --git a/src/app/data_farmer.rs b/src/app/data_farmer.rs index 6552a8ec..e8e98160 100644 --- a/src/app/data_farmer.rs +++ b/src/app/data_farmer.rs @@ -23,14 +23,14 @@ pub type JoinedDataPoints = (Value, Vec<(TimeOffset, Value)>); #[derive(Debug, Default)] pub struct TimedData { - pub rx_data: JoinedDataPoints, - pub tx_data: JoinedDataPoints, - pub cpu_data: Vec, - pub mem_data: JoinedDataPoints, - pub swap_data: JoinedDataPoints, - // Unused for now - // pub io_data : JoinedDataPoints - // pub temp_data: JoinedDataPoints, + pub rx_data: JoinedDataPoints, + pub tx_data: JoinedDataPoints, + pub cpu_data: Vec, + pub mem_data: JoinedDataPoints, + pub swap_data: JoinedDataPoints, + // Unused for now + // pub io_data : JoinedDataPoints + // pub temp_data: JoinedDataPoints, } /// AppCollection represents the pooled data stored within the main app @@ -44,253 +44,253 @@ pub struct TimedData { /// not the data collector. #[derive(Debug)] pub struct DataCollection { - pub current_instant: Instant, - pub timed_data_vec: Vec<(Instant, TimedData)>, - pub network_harvest: network::NetworkHarvest, - pub memory_harvest: mem::MemHarvest, - pub swap_harvest: mem::MemHarvest, - pub cpu_harvest: cpu::CPUHarvest, - pub process_harvest: Vec, - pub disk_harvest: Vec, - pub io_harvest: disks::IOHarvest, - pub io_labels: Vec<(u64, u64)>, - io_prev: Vec<(u64, u64)>, - pub temp_harvest: Vec, + pub current_instant: Instant, + pub timed_data_vec: Vec<(Instant, TimedData)>, + pub network_harvest: network::NetworkHarvest, + pub memory_harvest: mem::MemHarvest, + pub swap_harvest: mem::MemHarvest, + pub cpu_harvest: cpu::CPUHarvest, + pub process_harvest: Vec, + pub disk_harvest: Vec, + pub io_harvest: disks::IOHarvest, + pub io_labels: Vec<(u64, u64)>, + io_prev: Vec<(u64, u64)>, + pub temp_harvest: Vec, } impl Default for DataCollection { - fn default() -> Self { - DataCollection { - current_instant: Instant::now(), - timed_data_vec: Vec::default(), - network_harvest: network::NetworkHarvest::default(), - memory_harvest: mem::MemHarvest::default(), - swap_harvest: mem::MemHarvest::default(), - cpu_harvest: cpu::CPUHarvest::default(), - process_harvest: Vec::default(), - disk_harvest: Vec::default(), - io_harvest: disks::IOHarvest::default(), - io_labels: Vec::default(), - io_prev: Vec::default(), - temp_harvest: Vec::default(), - } - } + fn default() -> Self { + DataCollection { + current_instant: Instant::now(), + timed_data_vec: Vec::default(), + network_harvest: network::NetworkHarvest::default(), + memory_harvest: mem::MemHarvest::default(), + swap_harvest: mem::MemHarvest::default(), + cpu_harvest: cpu::CPUHarvest::default(), + process_harvest: Vec::default(), + disk_harvest: Vec::default(), + io_harvest: disks::IOHarvest::default(), + io_labels: Vec::default(), + io_prev: Vec::default(), + temp_harvest: Vec::default(), + } + } } impl DataCollection { - pub fn clean_data(&mut self, max_time_millis: u128) { - let current_time = Instant::now(); + pub fn clean_data(&mut self, max_time_millis: u128) { + let current_time = Instant::now(); - let mut remove_index = 0; - for entry in &self.timed_data_vec { - if current_time.duration_since(entry.0).as_millis() >= max_time_millis { - remove_index += 1; - } else { - break; - } - } + let mut remove_index = 0; + for entry in &self.timed_data_vec { + if current_time.duration_since(entry.0).as_millis() >= max_time_millis { + remove_index += 1; + } else { + break; + } + } - self.timed_data_vec.drain(0..remove_index); - } + self.timed_data_vec.drain(0..remove_index); + } - pub fn eat_data(&mut self, harvested_data: &Data) { - let harvested_time = harvested_data.last_collection_time; - let mut new_entry = TimedData::default(); + pub fn eat_data(&mut self, harvested_data: &Data) { + let harvested_time = harvested_data.last_collection_time; + let mut new_entry = TimedData::default(); - // Network - self.eat_network(&harvested_data, harvested_time, &mut new_entry); + // Network + self.eat_network(&harvested_data, harvested_time, &mut new_entry); - // Memory and Swap - self.eat_memory_and_swap(&harvested_data, harvested_time, &mut new_entry); + // Memory and Swap + self.eat_memory_and_swap(&harvested_data, harvested_time, &mut new_entry); - // CPU - self.eat_cpu(&harvested_data, harvested_time, &mut new_entry); + // CPU + self.eat_cpu(&harvested_data, harvested_time, &mut new_entry); - // Temp - self.eat_temp(&harvested_data); + // Temp + self.eat_temp(&harvested_data); - // Disks - self.eat_disks(&harvested_data, harvested_time); + // Disks + self.eat_disks(&harvested_data, harvested_time); - // Processes - self.eat_proc(&harvested_data); + // Processes + self.eat_proc(&harvested_data); - // And we're done eating. Update time and push the new entry! - self.current_instant = harvested_time; - self.timed_data_vec.push((harvested_time, new_entry)); - } + // And we're done eating. Update time and push the new entry! + self.current_instant = harvested_time; + self.timed_data_vec.push((harvested_time, new_entry)); + } - fn eat_memory_and_swap( - &mut self, harvested_data: &Data, harvested_time: Instant, new_entry: &mut TimedData, - ) { - // Memory - let mem_percent = harvested_data.memory.mem_used_in_mb as f64 - / harvested_data.memory.mem_total_in_mb as f64 - * 100.0; - let mem_joining_pts = if let Some((time, last_pt)) = self.timed_data_vec.last() { - generate_joining_points(*time, last_pt.mem_data.0, harvested_time, mem_percent) - } else { - Vec::new() - }; - let mem_pt = (mem_percent, mem_joining_pts); - new_entry.mem_data = mem_pt; + fn eat_memory_and_swap( + &mut self, harvested_data: &Data, harvested_time: Instant, new_entry: &mut TimedData, + ) { + // Memory + let mem_percent = harvested_data.memory.mem_used_in_mb as f64 + / harvested_data.memory.mem_total_in_mb as f64 + * 100.0; + let mem_joining_pts = if let Some((time, last_pt)) = self.timed_data_vec.last() { + generate_joining_points(*time, last_pt.mem_data.0, harvested_time, mem_percent) + } else { + Vec::new() + }; + let mem_pt = (mem_percent, mem_joining_pts); + new_entry.mem_data = mem_pt; - // Swap - if harvested_data.swap.mem_total_in_mb > 0 { - let swap_percent = harvested_data.swap.mem_used_in_mb as f64 - / harvested_data.swap.mem_total_in_mb as f64 - * 100.0; - let swap_joining_pt = if let Some((time, last_pt)) = self.timed_data_vec.last() { - generate_joining_points(*time, last_pt.swap_data.0, harvested_time, swap_percent) - } else { - Vec::new() - }; - let swap_pt = (swap_percent, swap_joining_pt); - new_entry.swap_data = swap_pt; - } + // Swap + if harvested_data.swap.mem_total_in_mb > 0 { + let swap_percent = harvested_data.swap.mem_used_in_mb as f64 + / harvested_data.swap.mem_total_in_mb as f64 + * 100.0; + let swap_joining_pt = if let Some((time, last_pt)) = self.timed_data_vec.last() { + generate_joining_points(*time, last_pt.swap_data.0, harvested_time, swap_percent) + } else { + Vec::new() + }; + let swap_pt = (swap_percent, swap_joining_pt); + new_entry.swap_data = swap_pt; + } - // In addition copy over latest data for easy reference - self.memory_harvest = harvested_data.memory.clone(); - self.swap_harvest = harvested_data.swap.clone(); - } + // In addition copy over latest data for easy reference + self.memory_harvest = harvested_data.memory.clone(); + self.swap_harvest = harvested_data.swap.clone(); + } - fn eat_network( - &mut self, harvested_data: &Data, harvested_time: Instant, new_entry: &mut TimedData, - ) { - // RX - let logged_rx_val = if harvested_data.network.rx as f64 > 0.0 { - (harvested_data.network.rx as f64).log(2.0) - } else { - 0.0 - }; + fn eat_network( + &mut self, harvested_data: &Data, harvested_time: Instant, new_entry: &mut TimedData, + ) { + // RX + let logged_rx_val = if harvested_data.network.rx as f64 > 0.0 { + (harvested_data.network.rx as f64).log(2.0) + } else { + 0.0 + }; - let rx_joining_pts = if let Some((time, last_pt)) = self.timed_data_vec.last() { - generate_joining_points(*time, last_pt.rx_data.0, harvested_time, logged_rx_val) - } else { - Vec::new() - }; - let rx_pt = (logged_rx_val, rx_joining_pts); - new_entry.rx_data = rx_pt; + let rx_joining_pts = if let Some((time, last_pt)) = self.timed_data_vec.last() { + generate_joining_points(*time, last_pt.rx_data.0, harvested_time, logged_rx_val) + } else { + Vec::new() + }; + let rx_pt = (logged_rx_val, rx_joining_pts); + new_entry.rx_data = rx_pt; - // TX - let logged_tx_val = if harvested_data.network.tx as f64 > 0.0 { - (harvested_data.network.tx as f64).log(2.0) - } else { - 0.0 - }; + // TX + let logged_tx_val = if harvested_data.network.tx as f64 > 0.0 { + (harvested_data.network.tx as f64).log(2.0) + } else { + 0.0 + }; - let tx_joining_pts = if let Some((time, last_pt)) = self.timed_data_vec.last() { - generate_joining_points(*time, last_pt.tx_data.0, harvested_time, logged_tx_val) - } else { - Vec::new() - }; - let tx_pt = (logged_tx_val, tx_joining_pts); - new_entry.tx_data = tx_pt; + let tx_joining_pts = if let Some((time, last_pt)) = self.timed_data_vec.last() { + generate_joining_points(*time, last_pt.tx_data.0, harvested_time, logged_tx_val) + } else { + Vec::new() + }; + let tx_pt = (logged_tx_val, tx_joining_pts); + new_entry.tx_data = tx_pt; - // In addition copy over latest data for easy reference - self.network_harvest = harvested_data.network.clone(); - } + // In addition copy over latest data for easy reference + self.network_harvest = harvested_data.network.clone(); + } - fn eat_cpu( - &mut self, harvested_data: &Data, harvested_time: Instant, new_entry: &mut TimedData, - ) { - // Note this only pre-calculates the data points - the names will be - // within the local copy of cpu_harvest. Since it's all sequential - // it probably doesn't matter anyways. - for (itx, cpu) in harvested_data.cpu.iter().enumerate() { - let cpu_joining_pts = if let Some((time, last_pt)) = self.timed_data_vec.last() { - generate_joining_points( - *time, - last_pt.cpu_data[itx].0, - harvested_time, - cpu.cpu_usage, - ) - } else { - Vec::new() - }; + fn eat_cpu( + &mut self, harvested_data: &Data, harvested_time: Instant, new_entry: &mut TimedData, + ) { + // Note this only pre-calculates the data points - the names will be + // within the local copy of cpu_harvest. Since it's all sequential + // it probably doesn't matter anyways. + for (itx, cpu) in harvested_data.cpu.iter().enumerate() { + let cpu_joining_pts = if let Some((time, last_pt)) = self.timed_data_vec.last() { + generate_joining_points( + *time, + last_pt.cpu_data[itx].0, + harvested_time, + cpu.cpu_usage, + ) + } else { + Vec::new() + }; - let cpu_pt = (cpu.cpu_usage, cpu_joining_pts); - new_entry.cpu_data.push(cpu_pt); - } + let cpu_pt = (cpu.cpu_usage, cpu_joining_pts); + new_entry.cpu_data.push(cpu_pt); + } - self.cpu_harvest = harvested_data.cpu.clone(); - } + self.cpu_harvest = harvested_data.cpu.clone(); + } - fn eat_temp(&mut self, harvested_data: &Data) { - // TODO: [PO] To implement - self.temp_harvest = harvested_data.temperature_sensors.clone(); - } + fn eat_temp(&mut self, harvested_data: &Data) { + // TODO: [PO] To implement + self.temp_harvest = harvested_data.temperature_sensors.clone(); + } - fn eat_disks(&mut self, harvested_data: &Data, harvested_time: Instant) { - // TODO: [PO] To implement + fn eat_disks(&mut self, harvested_data: &Data, harvested_time: Instant) { + // TODO: [PO] To implement - let time_since_last_harvest = harvested_time - .duration_since(self.current_instant) - .as_secs_f64(); + let time_since_last_harvest = harvested_time + .duration_since(self.current_instant) + .as_secs_f64(); - for (itx, device) in harvested_data.disks.iter().enumerate() { - if let Some(trim) = device.name.split('/').last() { - let io_device = harvested_data.io.get(trim); - if let Some(io) = io_device { - let io_r_pt = io.read_bytes; - let io_w_pt = io.write_bytes; + for (itx, device) in harvested_data.disks.iter().enumerate() { + if let Some(trim) = device.name.split('/').last() { + let io_device = harvested_data.io.get(trim); + if let Some(io) = io_device { + let io_r_pt = io.read_bytes; + let io_w_pt = io.write_bytes; - if self.io_labels.len() <= itx { - self.io_prev.push((io_r_pt, io_w_pt)); - self.io_labels.push((0, 0)); - } else { - let r_rate = ((io_r_pt - self.io_prev[itx].0) as f64 - / time_since_last_harvest) - .round() as u64; - let w_rate = ((io_w_pt - self.io_prev[itx].1) as f64 - / time_since_last_harvest) - .round() as u64; + if self.io_labels.len() <= itx { + self.io_prev.push((io_r_pt, io_w_pt)); + self.io_labels.push((0, 0)); + } else { + let r_rate = ((io_r_pt - self.io_prev[itx].0) as f64 + / time_since_last_harvest) + .round() as u64; + let w_rate = ((io_w_pt - self.io_prev[itx].1) as f64 + / time_since_last_harvest) + .round() as u64; - self.io_labels[itx] = (r_rate, w_rate); - self.io_prev[itx] = (io_r_pt, io_w_pt); - } - } - } - } + self.io_labels[itx] = (r_rate, w_rate); + self.io_prev[itx] = (io_r_pt, io_w_pt); + } + } + } + } - self.disk_harvest = harvested_data.disks.clone(); - self.io_harvest = harvested_data.io.clone(); - } + self.disk_harvest = harvested_data.disks.clone(); + self.io_harvest = harvested_data.io.clone(); + } - fn eat_proc(&mut self, harvested_data: &Data) { - self.process_harvest = harvested_data.list_of_processes.clone(); - } + fn eat_proc(&mut self, harvested_data: &Data) { + self.process_harvest = harvested_data.list_of_processes.clone(); + } } pub fn generate_joining_points( - start_x: Instant, start_y: f64, end_x: Instant, end_y: f64, + start_x: Instant, start_y: f64, end_x: Instant, end_y: f64, ) -> Vec<(TimeOffset, Value)> { - let mut points: Vec<(TimeOffset, Value)> = Vec::new(); + let mut points: Vec<(TimeOffset, Value)> = Vec::new(); - // Convert time floats first: - let tmp_time_diff = (end_x).duration_since(start_x).as_millis() as f64; - let time_difference = if tmp_time_diff == 0.0 { - 0.001 - } else { - tmp_time_diff - }; - let value_difference = end_y - start_y; + // Convert time floats first: + let tmp_time_diff = (end_x).duration_since(start_x).as_millis() as f64; + let time_difference = if tmp_time_diff == 0.0 { + 0.001 + } else { + tmp_time_diff + }; + let value_difference = end_y - start_y; - // Let's generate... about this many points! - let num_points = std::cmp::min( - std::cmp::max( - (value_difference.abs() / time_difference * 2000.0) as u64, - 50, - ), - 2000, - ); + // Let's generate... about this many points! + let num_points = std::cmp::min( + std::cmp::max( + (value_difference.abs() / time_difference * 2000.0) as u64, + 50, + ), + 2000, + ); - for itx in (0..num_points).step_by(2) { - points.push(( - time_difference - (itx as f64 / num_points as f64 * time_difference), - start_y + (itx as f64 / num_points as f64 * value_difference), - )); - } + for itx in (0..num_points).step_by(2) { + points.push(( + time_difference - (itx as f64 / num_points as f64 * time_difference), + start_y + (itx as f64 / num_points as f64 * value_difference), + )); + } - points + points } diff --git a/src/app/data_harvester.rs b/src/app/data_harvester.rs index 80d46a9e..c825b884 100644 --- a/src/app/data_harvester.rs +++ b/src/app/data_harvester.rs @@ -13,176 +13,176 @@ pub mod temperature; #[derive(Clone, Debug)] pub struct Data { - pub cpu: cpu::CPUHarvest, - pub memory: mem::MemHarvest, - pub swap: mem::MemHarvest, - pub temperature_sensors: Vec, - pub network: network::NetworkHarvest, - pub list_of_processes: Vec, - pub disks: Vec, - pub io: disks::IOHarvest, - pub last_collection_time: Instant, + pub cpu: cpu::CPUHarvest, + pub memory: mem::MemHarvest, + pub swap: mem::MemHarvest, + pub temperature_sensors: Vec, + pub network: network::NetworkHarvest, + pub list_of_processes: Vec, + pub disks: Vec, + pub io: disks::IOHarvest, + pub last_collection_time: Instant, } impl Default for Data { - fn default() -> Self { - Data { - cpu: cpu::CPUHarvest::default(), - memory: mem::MemHarvest::default(), - swap: mem::MemHarvest::default(), - temperature_sensors: Vec::default(), - list_of_processes: Vec::default(), - disks: Vec::default(), - io: disks::IOHarvest::default(), - network: network::NetworkHarvest::default(), - last_collection_time: Instant::now(), - } - } + fn default() -> Self { + Data { + cpu: cpu::CPUHarvest::default(), + memory: mem::MemHarvest::default(), + swap: mem::MemHarvest::default(), + temperature_sensors: Vec::default(), + list_of_processes: Vec::default(), + disks: Vec::default(), + io: disks::IOHarvest::default(), + network: network::NetworkHarvest::default(), + last_collection_time: Instant::now(), + } + } } impl Data { - pub fn first_run_cleanup(&mut self) { - self.io = disks::IOHarvest::default(); - self.temperature_sensors = Vec::new(); - self.list_of_processes = Vec::new(); - self.disks = Vec::new(); + pub fn first_run_cleanup(&mut self) { + self.io = disks::IOHarvest::default(); + self.temperature_sensors = Vec::new(); + self.list_of_processes = Vec::new(); + self.disks = Vec::new(); - self.network.first_run_cleanup(); - self.memory = mem::MemHarvest::default(); - self.swap = mem::MemHarvest::default(); - self.cpu = cpu::CPUHarvest::default(); - } + self.network.first_run_cleanup(); + self.memory = mem::MemHarvest::default(); + self.swap = mem::MemHarvest::default(); + self.cpu = cpu::CPUHarvest::default(); + } } pub struct DataState { - pub data: Data, - sys: System, - prev_pid_stats: HashMap, - prev_idle: f64, - prev_non_idle: f64, - mem_total_kb: u64, - temperature_type: temperature::TemperatureType, - use_current_cpu_total: bool, - last_collection_time: Instant, - total_rx: u64, - total_tx: u64, + pub data: Data, + sys: System, + prev_pid_stats: HashMap, + prev_idle: f64, + prev_non_idle: f64, + mem_total_kb: u64, + temperature_type: temperature::TemperatureType, + use_current_cpu_total: bool, + last_collection_time: Instant, + total_rx: u64, + total_tx: u64, } impl Default for DataState { - fn default() -> Self { - DataState { - data: Data::default(), - sys: System::new_all(), - prev_pid_stats: HashMap::new(), - prev_idle: 0_f64, - prev_non_idle: 0_f64, - mem_total_kb: 0, - temperature_type: temperature::TemperatureType::Celsius, - use_current_cpu_total: false, - last_collection_time: Instant::now(), - total_rx: 0, - total_tx: 0, - } - } + fn default() -> Self { + DataState { + data: Data::default(), + sys: System::new_all(), + prev_pid_stats: HashMap::new(), + prev_idle: 0_f64, + prev_non_idle: 0_f64, + mem_total_kb: 0, + temperature_type: temperature::TemperatureType::Celsius, + use_current_cpu_total: false, + last_collection_time: Instant::now(), + total_rx: 0, + total_tx: 0, + } + } } impl DataState { - pub fn set_temperature_type(&mut self, temperature_type: temperature::TemperatureType) { - self.temperature_type = temperature_type; - } + pub fn set_temperature_type(&mut self, temperature_type: temperature::TemperatureType) { + self.temperature_type = temperature_type; + } - pub fn set_use_current_cpu_total(&mut self, use_current_cpu_total: bool) { - self.use_current_cpu_total = use_current_cpu_total; - } + pub fn set_use_current_cpu_total(&mut self, use_current_cpu_total: bool) { + self.use_current_cpu_total = use_current_cpu_total; + } - pub fn init(&mut self) { - self.mem_total_kb = self.sys.get_total_memory(); - futures::executor::block_on(self.update_data()); - std::thread::sleep(std::time::Duration::from_millis(250)); - self.data.first_run_cleanup(); - } + pub fn init(&mut self) { + self.mem_total_kb = self.sys.get_total_memory(); + futures::executor::block_on(self.update_data()); + std::thread::sleep(std::time::Duration::from_millis(250)); + self.data.first_run_cleanup(); + } - pub async fn update_data(&mut self) { - self.sys.refresh_system(); + pub async fn update_data(&mut self) { + self.sys.refresh_system(); - if cfg!(not(target_os = "linux")) { - self.sys.refresh_processes(); - self.sys.refresh_components(); - } - if cfg!(target_os = "windows") { - self.sys.refresh_networks(); - } + if cfg!(not(target_os = "linux")) { + self.sys.refresh_processes(); + self.sys.refresh_components(); + } + if cfg!(target_os = "windows") { + self.sys.refresh_networks(); + } - let current_instant = std::time::Instant::now(); + let current_instant = std::time::Instant::now(); - // CPU - self.data.cpu = cpu::get_cpu_data_list(&self.sys); + // CPU + self.data.cpu = cpu::get_cpu_data_list(&self.sys); - // Processes. This is the longest part of the harvesting process... changing this might be - // good in the future. What was tried already: - // * Splitting the internal part into multiple scoped threads (dropped by ~.01 seconds, but upped usage) - if let Ok(process_list) = processes::get_sorted_processes_list( - &self.sys, - &mut self.prev_idle, - &mut self.prev_non_idle, - &mut self.prev_pid_stats, - self.use_current_cpu_total, - self.mem_total_kb, - current_instant, - ) { - self.data.list_of_processes = process_list; - } + // Processes. This is the longest part of the harvesting process... changing this might be + // good in the future. What was tried already: + // * Splitting the internal part into multiple scoped threads (dropped by ~.01 seconds, but upped usage) + if let Ok(process_list) = processes::get_sorted_processes_list( + &self.sys, + &mut self.prev_idle, + &mut self.prev_non_idle, + &mut self.prev_pid_stats, + self.use_current_cpu_total, + self.mem_total_kb, + current_instant, + ) { + self.data.list_of_processes = process_list; + } - // ASYNC - let network_data_fut = network::get_network_data( - &self.sys, - self.last_collection_time, - &mut self.total_rx, - &mut self.total_tx, - current_instant, - ); + // ASYNC + let network_data_fut = network::get_network_data( + &self.sys, + self.last_collection_time, + &mut self.total_rx, + &mut self.total_tx, + current_instant, + ); - let mem_data_fut = mem::get_mem_data_list(); - let swap_data_fut = mem::get_swap_data_list(); - let disk_data_fut = disks::get_disk_usage_list(); - let disk_io_usage_fut = disks::get_io_usage_list(false); - let temp_data_fut = temperature::get_temperature_data(&self.sys, &self.temperature_type); + let mem_data_fut = mem::get_mem_data_list(); + let swap_data_fut = mem::get_swap_data_list(); + let disk_data_fut = disks::get_disk_usage_list(); + let disk_io_usage_fut = disks::get_io_usage_list(false); + let temp_data_fut = temperature::get_temperature_data(&self.sys, &self.temperature_type); - let (net_data, mem_res, swap_res, disk_res, io_res, temp_res) = join!( - network_data_fut, - mem_data_fut, - swap_data_fut, - disk_data_fut, - disk_io_usage_fut, - temp_data_fut - ); + let (net_data, mem_res, swap_res, disk_res, io_res, temp_res) = join!( + network_data_fut, + mem_data_fut, + swap_data_fut, + disk_data_fut, + disk_io_usage_fut, + temp_data_fut + ); - // After async - self.data.network = net_data; - self.total_rx = self.data.network.total_rx; - self.total_tx = self.data.network.total_tx; + // After async + self.data.network = net_data; + self.total_rx = self.data.network.total_rx; + self.total_tx = self.data.network.total_tx; - if let Ok(memory) = mem_res { - self.data.memory = memory; - } + if let Ok(memory) = mem_res { + self.data.memory = memory; + } - if let Ok(swap) = swap_res { - self.data.swap = swap; - } + if let Ok(swap) = swap_res { + self.data.swap = swap; + } - if let Ok(disks) = disk_res { - self.data.disks = disks; - } - if let Ok(io) = io_res { - self.data.io = io; - } + if let Ok(disks) = disk_res { + self.data.disks = disks; + } + if let Ok(io) = io_res { + self.data.io = io; + } - if let Ok(temp) = temp_res { - self.data.temperature_sensors = temp; - } + if let Ok(temp) = temp_res { + self.data.temperature_sensors = temp; + } - // Update time - self.data.last_collection_time = current_instant; - self.last_collection_time = current_instant; - } + // Update time + self.data.last_collection_time = current_instant; + self.last_collection_time = current_instant; + } } diff --git a/src/app/data_harvester/cpu.rs b/src/app/data_harvester/cpu.rs index c1f2dee2..d9c7ee0a 100644 --- a/src/app/data_harvester/cpu.rs +++ b/src/app/data_harvester/cpu.rs @@ -2,26 +2,26 @@ use sysinfo::{ProcessorExt, System, SystemExt}; #[derive(Default, Debug, Clone)] pub struct CPUData { - pub cpu_name: String, - pub cpu_usage: f64, + pub cpu_name: String, + pub cpu_usage: f64, } pub type CPUHarvest = Vec; pub fn get_cpu_data_list(sys: &System) -> CPUHarvest { - let cpu_data = sys.get_processors(); - let avg_cpu_usage = sys.get_global_processor_info().get_cpu_usage(); - let mut cpu_vec = vec![CPUData { - cpu_name: "AVG".to_string(), - cpu_usage: avg_cpu_usage as f64, - }]; + let cpu_data = sys.get_processors(); + let avg_cpu_usage = sys.get_global_processor_info().get_cpu_usage(); + let mut cpu_vec = vec![CPUData { + cpu_name: "AVG".to_string(), + cpu_usage: avg_cpu_usage as f64, + }]; - for cpu in cpu_data { - cpu_vec.push(CPUData { - cpu_name: cpu.get_name().to_uppercase(), - cpu_usage: f64::from(cpu.get_cpu_usage()), - }); - } + for cpu in cpu_data { + cpu_vec.push(CPUData { + cpu_name: cpu.get_name().to_uppercase(), + cpu_usage: f64::from(cpu.get_cpu_usage()), + }); + } - cpu_vec + cpu_vec } diff --git a/src/app/data_harvester/disks.rs b/src/app/data_harvester/disks.rs index 7a7f52e2..e8401645 100644 --- a/src/app/data_harvester/disks.rs +++ b/src/app/data_harvester/disks.rs @@ -3,83 +3,83 @@ use heim::units::information; #[derive(Debug, Clone, Default)] pub struct DiskHarvest { - pub name: String, - pub mount_point: String, - pub free_space: u64, - pub used_space: u64, - pub total_space: u64, + pub name: String, + pub mount_point: String, + pub free_space: u64, + pub used_space: u64, + pub total_space: u64, } #[derive(Clone, Debug)] pub struct IOData { - pub read_bytes: u64, - pub write_bytes: u64, + pub read_bytes: u64, + pub write_bytes: u64, } pub type IOHarvest = std::collections::HashMap; pub async fn get_io_usage_list(get_physical: bool) -> crate::utils::error::Result { - let mut io_hash: std::collections::HashMap = std::collections::HashMap::new(); - if get_physical { - let mut physical_counter_stream = heim::disk::io_counters_physical(); - while let Some(io) = physical_counter_stream.next().await { - let io = io?; - let mount_point = io.device_name().to_str().unwrap_or("Name Unavailable"); - io_hash.insert( - mount_point.to_string(), - IOData { - read_bytes: io.read_bytes().get::(), - write_bytes: io.write_bytes().get::(), - }, - ); - } - } else { - let mut counter_stream = heim::disk::io_counters(); - while let Some(io) = counter_stream.next().await { - let io = io?; - let mount_point = io.device_name().to_str().unwrap_or("Name Unavailable"); - io_hash.insert( - mount_point.to_string(), - IOData { - read_bytes: io.read_bytes().get::(), - write_bytes: io.write_bytes().get::(), - }, - ); - } - } + let mut io_hash: std::collections::HashMap = std::collections::HashMap::new(); + if get_physical { + let mut physical_counter_stream = heim::disk::io_counters_physical(); + while let Some(io) = physical_counter_stream.next().await { + let io = io?; + let mount_point = io.device_name().to_str().unwrap_or("Name Unavailable"); + io_hash.insert( + mount_point.to_string(), + IOData { + read_bytes: io.read_bytes().get::(), + write_bytes: io.write_bytes().get::(), + }, + ); + } + } else { + let mut counter_stream = heim::disk::io_counters(); + while let Some(io) = counter_stream.next().await { + let io = io?; + let mount_point = io.device_name().to_str().unwrap_or("Name Unavailable"); + io_hash.insert( + mount_point.to_string(), + IOData { + read_bytes: io.read_bytes().get::(), + write_bytes: io.write_bytes().get::(), + }, + ); + } + } - Ok(io_hash) + Ok(io_hash) } pub async fn get_disk_usage_list() -> crate::utils::error::Result> { - let mut vec_disks: Vec = Vec::new(); - let mut partitions_stream = heim::disk::partitions_physical(); + let mut vec_disks: Vec = Vec::new(); + let mut partitions_stream = heim::disk::partitions_physical(); - while let Some(part) = partitions_stream.next().await { - if let Ok(part) = part { - let partition = part; - let usage = heim::disk::usage(partition.mount_point().to_path_buf()).await?; + while let Some(part) = partitions_stream.next().await { + if let Ok(part) = part { + let partition = part; + let usage = heim::disk::usage(partition.mount_point().to_path_buf()).await?; - vec_disks.push(DiskHarvest { - free_space: usage.free().get::(), - used_space: usage.used().get::(), - total_space: usage.total().get::(), - mount_point: (partition - .mount_point() - .to_str() - .unwrap_or("Name Unavailable")) - .to_string(), - name: (partition - .device() - .unwrap_or_else(|| std::ffi::OsStr::new("Name Unavailable")) - .to_str() - .unwrap_or("Name Unavailable")) - .to_string(), - }); - } - } + vec_disks.push(DiskHarvest { + free_space: usage.free().get::(), + used_space: usage.used().get::(), + total_space: usage.total().get::(), + mount_point: (partition + .mount_point() + .to_str() + .unwrap_or("Name Unavailable")) + .to_string(), + name: (partition + .device() + .unwrap_or_else(|| std::ffi::OsStr::new("Name Unavailable")) + .to_str() + .unwrap_or("Name Unavailable")) + .to_string(), + }); + } + } - vec_disks.sort_by(|a, b| a.name.cmp(&b.name)); + vec_disks.sort_by(|a, b| a.name.cmp(&b.name)); - Ok(vec_disks) + Ok(vec_disks) } diff --git a/src/app/data_harvester/mem.rs b/src/app/data_harvester/mem.rs index 4bd3a0db..d9a9993a 100644 --- a/src/app/data_harvester/mem.rs +++ b/src/app/data_harvester/mem.rs @@ -2,34 +2,34 @@ use heim::units::information; #[derive(Debug, Clone)] pub struct MemHarvest { - pub mem_total_in_mb: u64, - pub mem_used_in_mb: u64, + pub mem_total_in_mb: u64, + pub mem_used_in_mb: u64, } impl Default for MemHarvest { - fn default() -> Self { - MemHarvest { - mem_total_in_mb: 0, - mem_used_in_mb: 0, - } - } + fn default() -> Self { + MemHarvest { + mem_total_in_mb: 0, + mem_used_in_mb: 0, + } + } } pub async fn get_mem_data_list() -> crate::utils::error::Result { - let memory = heim::memory::memory().await?; + let memory = heim::memory::memory().await?; - Ok(MemHarvest { - mem_total_in_mb: memory.total().get::(), - mem_used_in_mb: memory.total().get::() - - memory.available().get::(), - }) + Ok(MemHarvest { + mem_total_in_mb: memory.total().get::(), + mem_used_in_mb: memory.total().get::() + - memory.available().get::(), + }) } pub async fn get_swap_data_list() -> crate::utils::error::Result { - let memory = heim::memory::swap().await?; + let memory = heim::memory::swap().await?; - Ok(MemHarvest { - mem_total_in_mb: memory.total().get::(), - mem_used_in_mb: memory.used().get::(), - }) + Ok(MemHarvest { + mem_total_in_mb: memory.total().get::(), + mem_used_in_mb: memory.used().get::(), + }) } diff --git a/src/app/data_harvester/network.rs b/src/app/data_harvester/network.rs index 9a40d5f7..2c2dba7d 100644 --- a/src/app/data_harvester/network.rs +++ b/src/app/data_harvester/network.rs @@ -7,53 +7,53 @@ use sysinfo::{NetworkExt, System, SystemExt}; #[derive(Default, Clone, Debug)] pub struct NetworkHarvest { - pub rx: u64, - pub tx: u64, - pub total_rx: u64, - pub total_tx: u64, + pub rx: u64, + pub tx: u64, + pub total_rx: u64, + pub total_tx: u64, } impl NetworkHarvest { - pub fn first_run_cleanup(&mut self) { - self.rx = 0; - self.tx = 0; - } + pub fn first_run_cleanup(&mut self) { + self.rx = 0; + self.tx = 0; + } } pub async fn get_network_data( - sys: &System, prev_net_access_time: Instant, prev_net_rx: &mut u64, prev_net_tx: &mut u64, - curr_time: Instant, + sys: &System, prev_net_access_time: Instant, prev_net_rx: &mut u64, prev_net_tx: &mut u64, + curr_time: Instant, ) -> NetworkHarvest { - let mut io_data = net::io_counters(); - let mut total_rx: u64 = 0; - let mut total_tx: u64 = 0; + let mut io_data = net::io_counters(); + let mut total_rx: u64 = 0; + let mut total_tx: u64 = 0; - if cfg!(target_os = "windows") { - let networks = sys.get_networks(); - for (_, network) in networks { - total_rx += network.get_total_income(); - total_tx += network.get_total_outcome(); - } - } else { - while let Some(io) = io_data.next().await { - if let Ok(io) = io { - total_rx += io.bytes_recv().get::(); - total_tx += io.bytes_sent().get::(); - } - } - } + if cfg!(target_os = "windows") { + let networks = sys.get_networks(); + for (_, network) in networks { + total_rx += network.get_total_income(); + total_tx += network.get_total_outcome(); + } + } else { + while let Some(io) = io_data.next().await { + if let Ok(io) = io { + total_rx += io.bytes_recv().get::(); + total_tx += io.bytes_sent().get::(); + } + } + } - let elapsed_time = curr_time.duration_since(prev_net_access_time).as_secs_f64(); + let elapsed_time = curr_time.duration_since(prev_net_access_time).as_secs_f64(); - let rx = ((total_rx - *prev_net_rx) as f64 / elapsed_time) as u64; - let tx = ((total_tx - *prev_net_tx) as f64 / elapsed_time) as u64; + let rx = ((total_rx - *prev_net_rx) as f64 / elapsed_time) as u64; + let tx = ((total_tx - *prev_net_tx) as f64 / elapsed_time) as u64; - *prev_net_rx = total_rx; - *prev_net_tx = total_tx; - NetworkHarvest { - rx, - tx, - total_rx, - total_tx, - } + *prev_net_rx = total_rx; + *prev_net_tx = total_tx; + NetworkHarvest { + rx, + tx, + total_rx, + total_tx, + } } diff --git a/src/app/data_harvester/processes.rs b/src/app/data_harvester/processes.rs index 027781eb..3c374bad 100644 --- a/src/app/data_harvester/processes.rs +++ b/src/app/data_harvester/processes.rs @@ -1,7 +1,7 @@ use std::{ - collections::{hash_map::RandomState, HashMap}, - process::Command, - time::Instant, + collections::{hash_map::RandomState, HashMap}, + process::Command, + time::Instant, }; use sysinfo::{ProcessExt, ProcessorExt, System, SystemExt}; @@ -10,277 +10,277 @@ use crate::utils::error; #[derive(Clone)] pub enum ProcessSorting { - CPU, - MEM, - PID, - NAME, + CPU, + MEM, + PID, + NAME, } impl Default for ProcessSorting { - fn default() -> Self { - ProcessSorting::CPU - } + fn default() -> Self { + ProcessSorting::CPU + } } #[derive(Debug, Clone, Default)] pub struct ProcessHarvest { - pub pid: u32, - pub cpu_usage_percent: f64, - pub mem_usage_percent: f64, - pub name: String, + pub pid: u32, + pub cpu_usage_percent: f64, + pub mem_usage_percent: f64, + pub name: String, } fn cpu_usage_calculation( - prev_idle: &mut f64, prev_non_idle: &mut f64, + prev_idle: &mut f64, prev_non_idle: &mut f64, ) -> error::Result<(f64, f64)> { - // From SO answer: https://stackoverflow.com/a/23376195 - let mut path = std::path::PathBuf::new(); - path.push("/proc"); - path.push("stat"); + // From SO answer: https://stackoverflow.com/a/23376195 + let mut path = std::path::PathBuf::new(); + path.push("/proc"); + path.push("stat"); - let stat_results = std::fs::read_to_string(path)?; - let first_line: &str; + let stat_results = std::fs::read_to_string(path)?; + let first_line: &str; - let split_results = stat_results.split('\n').collect::>(); - if split_results.is_empty() { - return Err(error::BottomError::InvalidIO(format!( - "Unable to properly split the stat results; saw {} values, expected at least 1 value.", - split_results.len() - ))); - } else { - first_line = split_results[0]; - } + let split_results = stat_results.split('\n').collect::>(); + if split_results.is_empty() { + return Err(error::BottomError::InvalidIO(format!( + "Unable to properly split the stat results; saw {} values, expected at least 1 value.", + split_results.len() + ))); + } else { + first_line = split_results[0]; + } - let val = first_line.split_whitespace().collect::>(); + let val = first_line.split_whitespace().collect::>(); - // SC in case that the parsing will fail due to length: - if val.len() <= 10 { - return Err(error::BottomError::InvalidIO(format!( - "CPU parsing will fail due to too short of a return value; saw {} values, expected 10 values.", - val.len() - ))); - } + // SC in case that the parsing will fail due to length: + if val.len() <= 10 { + return Err(error::BottomError::InvalidIO(format!( + "CPU parsing will fail due to too short of a return value; saw {} values, expected 10 values.", + val.len() + ))); + } - let user: f64 = val[1].parse::<_>().unwrap_or(0_f64); - let nice: f64 = val[2].parse::<_>().unwrap_or(0_f64); - let system: f64 = val[3].parse::<_>().unwrap_or(0_f64); - let idle: f64 = val[4].parse::<_>().unwrap_or(0_f64); - let iowait: f64 = val[5].parse::<_>().unwrap_or(0_f64); - let irq: f64 = val[6].parse::<_>().unwrap_or(0_f64); - let softirq: f64 = val[7].parse::<_>().unwrap_or(0_f64); - let steal: f64 = val[8].parse::<_>().unwrap_or(0_f64); - let guest: f64 = val[9].parse::<_>().unwrap_or(0_f64); + let user: f64 = val[1].parse::<_>().unwrap_or(0_f64); + let nice: f64 = val[2].parse::<_>().unwrap_or(0_f64); + let system: f64 = val[3].parse::<_>().unwrap_or(0_f64); + let idle: f64 = val[4].parse::<_>().unwrap_or(0_f64); + let iowait: f64 = val[5].parse::<_>().unwrap_or(0_f64); + let irq: f64 = val[6].parse::<_>().unwrap_or(0_f64); + let softirq: f64 = val[7].parse::<_>().unwrap_or(0_f64); + let steal: f64 = val[8].parse::<_>().unwrap_or(0_f64); + let guest: f64 = val[9].parse::<_>().unwrap_or(0_f64); - let idle = idle + iowait; - let non_idle = user + nice + system + irq + softirq + steal + guest; + let idle = idle + iowait; + let non_idle = user + nice + system + irq + softirq + steal + guest; - let total = idle + non_idle; - let prev_total = *prev_idle + *prev_non_idle; + let total = idle + non_idle; + let prev_total = *prev_idle + *prev_non_idle; - let total_delta: f64 = total - prev_total; - let idle_delta: f64 = idle - *prev_idle; + let total_delta: f64 = total - prev_total; + let idle_delta: f64 = idle - *prev_idle; - //debug!("Vangelis function: CPU PERCENT: {}", (total_delta - idle_delta) / total_delta * 100_f64); + //debug!("Vangelis function: CPU PERCENT: {}", (total_delta - idle_delta) / total_delta * 100_f64); - *prev_idle = idle; - *prev_non_idle = non_idle; + *prev_idle = idle; + *prev_non_idle = non_idle; - let result = if total_delta - idle_delta != 0_f64 { - total_delta - idle_delta - } else { - 1_f64 - }; + let result = if total_delta - idle_delta != 0_f64 { + total_delta - idle_delta + } else { + 1_f64 + }; - let cpu_percentage = if total_delta != 0_f64 { - result / total_delta - } else { - 0_f64 - }; + let cpu_percentage = if total_delta != 0_f64 { + result / total_delta + } else { + 0_f64 + }; - Ok((result, cpu_percentage)) + Ok((result, cpu_percentage)) } fn get_process_cpu_stats(pid: u32) -> std::io::Result { - let mut path = std::path::PathBuf::new(); - path.push("/proc"); - path.push(&pid.to_string()); - path.push("stat"); + let mut path = std::path::PathBuf::new(); + path.push("/proc"); + path.push(&pid.to_string()); + path.push("stat"); - let stat_results = std::fs::read_to_string(path)?; - let val = stat_results.split_whitespace().collect::>(); - let utime = val[13].parse::().unwrap_or(0_f64); - let stime = val[14].parse::().unwrap_or(0_f64); + let stat_results = std::fs::read_to_string(path)?; + let val = stat_results.split_whitespace().collect::>(); + let utime = val[13].parse::().unwrap_or(0_f64); + let stime = val[14].parse::().unwrap_or(0_f64); - //debug!("PID: {}, utime: {}, stime: {}", pid, utime, stime); + //debug!("PID: {}, utime: {}, stime: {}", pid, utime, stime); - Ok(utime + stime) // This seems to match top... + Ok(utime + stime) // This seems to match top... } /// Note that cpu_fraction should be represented WITHOUT the \times 100 factor! fn linux_cpu_usage( - pid: u32, cpu_usage: f64, cpu_fraction: f64, - prev_pid_stats: &HashMap, - new_pid_stats: &mut HashMap, use_current_cpu_total: bool, - curr_time: Instant, + pid: u32, cpu_usage: f64, cpu_fraction: f64, + prev_pid_stats: &HashMap, + new_pid_stats: &mut HashMap, use_current_cpu_total: bool, + curr_time: Instant, ) -> std::io::Result { - // Based heavily on https://stackoverflow.com/a/23376195 and https://stackoverflow.com/a/1424556 - let before_proc_val: f64 = if prev_pid_stats.contains_key(&pid.to_string()) { - prev_pid_stats - .get(&pid.to_string()) - .unwrap_or(&(0_f64, curr_time)) - .0 - } else { - 0_f64 - }; - let after_proc_val = get_process_cpu_stats(pid)?; + // Based heavily on https://stackoverflow.com/a/23376195 and https://stackoverflow.com/a/1424556 + let before_proc_val: f64 = if prev_pid_stats.contains_key(&pid.to_string()) { + prev_pid_stats + .get(&pid.to_string()) + .unwrap_or(&(0_f64, curr_time)) + .0 + } else { + 0_f64 + }; + let after_proc_val = get_process_cpu_stats(pid)?; - /*debug!( - "PID - {} - Before: {}, After: {}, CPU: {}, Percentage: {}", - pid, - before_proc_val, - after_proc_val, - cpu_usage, - (after_proc_val - before_proc_val) / cpu_usage * 100_f64 - );*/ + /*debug!( + "PID - {} - Before: {}, After: {}, CPU: {}, Percentage: {}", + pid, + before_proc_val, + after_proc_val, + cpu_usage, + (after_proc_val - before_proc_val) / cpu_usage * 100_f64 + );*/ - new_pid_stats.insert(pid.to_string(), (after_proc_val, curr_time)); + new_pid_stats.insert(pid.to_string(), (after_proc_val, curr_time)); - if use_current_cpu_total { - Ok((after_proc_val - before_proc_val) / cpu_usage * 100_f64) - } else { - Ok((after_proc_val - before_proc_val) / cpu_usage * 100_f64 * cpu_fraction) - } + if use_current_cpu_total { + Ok((after_proc_val - before_proc_val) / cpu_usage * 100_f64) + } else { + Ok((after_proc_val - before_proc_val) / cpu_usage * 100_f64 * cpu_fraction) + } } fn convert_ps( - process: &str, cpu_usage: f64, cpu_fraction: f64, - prev_pid_stats: &HashMap, - new_pid_stats: &mut HashMap, use_current_cpu_total: bool, - curr_time: Instant, + process: &str, cpu_usage: f64, cpu_fraction: f64, + prev_pid_stats: &HashMap, + new_pid_stats: &mut HashMap, use_current_cpu_total: bool, + curr_time: Instant, ) -> std::io::Result { - if process.trim().to_string().is_empty() { - return Ok(ProcessHarvest { - pid: 0, - name: "".to_string(), - mem_usage_percent: 0.0, - cpu_usage_percent: 0.0, - }); - } + if process.trim().to_string().is_empty() { + return Ok(ProcessHarvest { + pid: 0, + name: "".to_string(), + mem_usage_percent: 0.0, + cpu_usage_percent: 0.0, + }); + } - let pid = (&process[..11]) - .trim() - .to_string() - .parse::() - .unwrap_or(0); - let name = (&process[11..61]).trim().to_string(); - let mem_usage_percent = (&process[62..]) - .trim() - .to_string() - .parse::() - .unwrap_or(0_f64); + let pid = (&process[..11]) + .trim() + .to_string() + .parse::() + .unwrap_or(0); + let name = (&process[11..61]).trim().to_string(); + let mem_usage_percent = (&process[62..]) + .trim() + .to_string() + .parse::() + .unwrap_or(0_f64); - let cpu_usage_percent = linux_cpu_usage( - pid, - cpu_usage, - cpu_fraction, - prev_pid_stats, - new_pid_stats, - use_current_cpu_total, - curr_time, - )?; - Ok(ProcessHarvest { - pid, - name, - mem_usage_percent, - cpu_usage_percent, - }) + let cpu_usage_percent = linux_cpu_usage( + pid, + cpu_usage, + cpu_fraction, + prev_pid_stats, + new_pid_stats, + use_current_cpu_total, + curr_time, + )?; + Ok(ProcessHarvest { + pid, + name, + mem_usage_percent, + cpu_usage_percent, + }) } pub fn get_sorted_processes_list( - sys: &System, prev_idle: &mut f64, prev_non_idle: &mut f64, - prev_pid_stats: &mut HashMap, use_current_cpu_total: bool, - mem_total_kb: u64, curr_time: Instant, + sys: &System, prev_idle: &mut f64, prev_non_idle: &mut f64, + prev_pid_stats: &mut HashMap, use_current_cpu_total: bool, + mem_total_kb: u64, curr_time: Instant, ) -> crate::utils::error::Result> { - let mut process_vector: Vec = Vec::new(); + let mut process_vector: Vec = Vec::new(); - if cfg!(target_os = "linux") { - let ps_result = Command::new("ps") - .args(&["-axo", "pid:10,comm:50,%mem:5", "--noheader"]) - .output()?; - let ps_stdout = String::from_utf8_lossy(&ps_result.stdout); - let split_string = ps_stdout.split('\n'); - let cpu_calc = cpu_usage_calculation(prev_idle, prev_non_idle); - if let Ok((cpu_usage, cpu_fraction)) = cpu_calc { - let process_stream = split_string.collect::>(); + if cfg!(target_os = "linux") { + let ps_result = Command::new("ps") + .args(&["-axo", "pid:10,comm:50,%mem:5", "--noheader"]) + .output()?; + let ps_stdout = String::from_utf8_lossy(&ps_result.stdout); + let split_string = ps_stdout.split('\n'); + let cpu_calc = cpu_usage_calculation(prev_idle, prev_non_idle); + if let Ok((cpu_usage, cpu_fraction)) = cpu_calc { + let process_stream = split_string.collect::>(); - let mut new_pid_stats: HashMap = HashMap::new(); + let mut new_pid_stats: HashMap = HashMap::new(); - for process in process_stream { - if let Ok(process_object) = convert_ps( - process, - cpu_usage, - cpu_fraction, - &prev_pid_stats, - &mut new_pid_stats, - use_current_cpu_total, - curr_time, - ) { - if !process_object.name.is_empty() { - process_vector.push(process_object); - } - } - } + for process in process_stream { + if let Ok(process_object) = convert_ps( + process, + cpu_usage, + cpu_fraction, + &prev_pid_stats, + &mut new_pid_stats, + use_current_cpu_total, + curr_time, + ) { + if !process_object.name.is_empty() { + process_vector.push(process_object); + } + } + } - *prev_pid_stats = new_pid_stats; - } else { - error!("Unable to properly parse CPU data in Linux."); - error!("Result: {:?}", cpu_calc.err()); - } - } else { - let process_hashmap = sys.get_processes(); - let cpu_usage = sys.get_global_processor_info().get_cpu_usage() as f64 / 100.0; - let num_cpus = sys.get_processors().len() as f64; - for process_val in process_hashmap.values() { - let name = if process_val.name().is_empty() { - let process_cmd = process_val.cmd(); - if process_cmd.len() > 1 { - process_cmd[0].clone() - } else { - let process_exe = process_val.exe().file_stem(); - if let Some(exe) = process_exe { - let process_exe_opt = exe.to_str(); - if let Some(exe_name) = process_exe_opt { - exe_name.to_string() - } else { - "".to_string() - } - } else { - "".to_string() - } - } - } else { - process_val.name().to_string() - }; + *prev_pid_stats = new_pid_stats; + } else { + error!("Unable to properly parse CPU data in Linux."); + error!("Result: {:?}", cpu_calc.err()); + } + } else { + let process_hashmap = sys.get_processes(); + let cpu_usage = sys.get_global_processor_info().get_cpu_usage() as f64 / 100.0; + let num_cpus = sys.get_processors().len() as f64; + for process_val in process_hashmap.values() { + let name = if process_val.name().is_empty() { + let process_cmd = process_val.cmd(); + if process_cmd.len() > 1 { + process_cmd[0].clone() + } else { + let process_exe = process_val.exe().file_stem(); + if let Some(exe) = process_exe { + let process_exe_opt = exe.to_str(); + if let Some(exe_name) = process_exe_opt { + exe_name.to_string() + } else { + "".to_string() + } + } else { + "".to_string() + } + } + } else { + process_val.name().to_string() + }; - let pcu = if cfg!(target_os = "windows") { - process_val.cpu_usage() as f64 - } else { - process_val.cpu_usage() as f64 / num_cpus - }; - let process_cpu_usage = if use_current_cpu_total { - pcu / cpu_usage - } else { - pcu - }; + let pcu = if cfg!(target_os = "windows") { + process_val.cpu_usage() as f64 + } else { + process_val.cpu_usage() as f64 / num_cpus + }; + let process_cpu_usage = if use_current_cpu_total { + pcu / cpu_usage + } else { + pcu + }; - process_vector.push(ProcessHarvest { - pid: process_val.pid() as u32, - name, - mem_usage_percent: process_val.memory() as f64 * 100.0 / mem_total_kb as f64, - cpu_usage_percent: process_cpu_usage, - }); - } - } + process_vector.push(ProcessHarvest { + pid: process_val.pid() as u32, + name, + mem_usage_percent: process_val.memory() as f64 * 100.0 / mem_total_kb as f64, + cpu_usage_percent: process_cpu_usage, + }); + } + } - Ok(process_vector) + Ok(process_vector) } diff --git a/src/app/data_harvester/temperature.rs b/src/app/data_harvester/temperature.rs index 66d47e1e..de3ce9d9 100644 --- a/src/app/data_harvester/temperature.rs +++ b/src/app/data_harvester/temperature.rs @@ -6,93 +6,93 @@ use sysinfo::{ComponentExt, System, SystemExt}; #[derive(Default, Debug, Clone)] pub struct TempHarvest { - pub component_name: String, - pub temperature: f32, + pub component_name: String, + pub temperature: f32, } #[derive(Clone, Debug)] pub enum TemperatureType { - Celsius, - Kelvin, - Fahrenheit, + Celsius, + Kelvin, + Fahrenheit, } impl Default for TemperatureType { - fn default() -> Self { - TemperatureType::Celsius - } + fn default() -> Self { + TemperatureType::Celsius + } } pub async fn get_temperature_data( - sys: &System, temp_type: &TemperatureType, + sys: &System, temp_type: &TemperatureType, ) -> crate::utils::error::Result> { - let mut temperature_vec: Vec = Vec::new(); + let mut temperature_vec: Vec = Vec::new(); - if cfg!(target_os = "linux") { - let mut sensor_data = heim::sensors::temperatures(); - while let Some(sensor) = sensor_data.next().await { - if let Ok(sensor) = sensor { - temperature_vec.push(TempHarvest { - component_name: sensor.unit().to_string(), - temperature: match temp_type { - TemperatureType::Celsius => sensor - .current() - .get::( - ), - TemperatureType::Kelvin => { - sensor.current().get::() - } - TemperatureType::Fahrenheit => sensor - .current() - .get::( - ), - }, - }); - } - } - } else { - let sensor_data = sys.get_components(); - for component in sensor_data { - temperature_vec.push(TempHarvest { - component_name: component.get_label().to_string(), - temperature: match temp_type { - TemperatureType::Celsius => component.get_temperature(), - TemperatureType::Kelvin => { - convert_celsius_to_kelvin(component.get_temperature()) - } - TemperatureType::Fahrenheit => { - convert_celsius_to_fahrenheit(component.get_temperature()) - } - }, - }); - } - } + if cfg!(target_os = "linux") { + let mut sensor_data = heim::sensors::temperatures(); + while let Some(sensor) = sensor_data.next().await { + if let Ok(sensor) = sensor { + temperature_vec.push(TempHarvest { + component_name: sensor.unit().to_string(), + temperature: match temp_type { + TemperatureType::Celsius => sensor + .current() + .get::( + ), + TemperatureType::Kelvin => { + sensor.current().get::() + } + TemperatureType::Fahrenheit => sensor + .current() + .get::( + ), + }, + }); + } + } + } else { + let sensor_data = sys.get_components(); + for component in sensor_data { + temperature_vec.push(TempHarvest { + component_name: component.get_label().to_string(), + temperature: match temp_type { + TemperatureType::Celsius => component.get_temperature(), + TemperatureType::Kelvin => { + convert_celsius_to_kelvin(component.get_temperature()) + } + TemperatureType::Fahrenheit => { + convert_celsius_to_fahrenheit(component.get_temperature()) + } + }, + }); + } + } - // By default, sort temperature, then by alphabetically! + // By default, sort temperature, then by alphabetically! - // Note we sort in reverse here; we want greater temps to be higher priority. - temperature_vec.sort_by(|a, b| match a.temperature.partial_cmp(&b.temperature) { - Some(x) => match x { - Ordering::Less => Ordering::Greater, - Ordering::Greater => Ordering::Less, - Ordering::Equal => Ordering::Equal, - }, - None => Ordering::Equal, - }); + // Note we sort in reverse here; we want greater temps to be higher priority. + temperature_vec.sort_by(|a, b| match a.temperature.partial_cmp(&b.temperature) { + Some(x) => match x { + Ordering::Less => Ordering::Greater, + Ordering::Greater => Ordering::Less, + Ordering::Equal => Ordering::Equal, + }, + None => Ordering::Equal, + }); - temperature_vec.sort_by(|a, b| { - a.component_name - .partial_cmp(&b.component_name) - .unwrap_or(Ordering::Equal) - }); + temperature_vec.sort_by(|a, b| { + a.component_name + .partial_cmp(&b.component_name) + .unwrap_or(Ordering::Equal) + }); - Ok(temperature_vec) + Ok(temperature_vec) } fn convert_celsius_to_kelvin(celsius: f32) -> f32 { - celsius + 273.15 + celsius + 273.15 } fn convert_celsius_to_fahrenheit(celsius: f32) -> f32 { - (celsius * (9.0 / 5.0)) + 32.0 + (celsius * (9.0 / 5.0)) + 32.0 } diff --git a/src/app/process_killer.rs b/src/app/process_killer.rs index d2089aad..60b19ee9 100644 --- a/src/app/process_killer.rs +++ b/src/app/process_killer.rs @@ -3,11 +3,11 @@ use std::process::Command; // Copied from SO: https://stackoverflow.com/a/55231715 #[cfg(target_os = "windows")] use winapi::{ - shared::{minwindef::DWORD, ntdef::HANDLE}, - um::{ - processthreadsapi::{OpenProcess, TerminateProcess}, - winnt::{PROCESS_QUERY_INFORMATION, PROCESS_TERMINATE}, - }, + shared::{minwindef::DWORD, ntdef::HANDLE}, + um::{ + processthreadsapi::{OpenProcess, TerminateProcess}, + winnt::{PROCESS_QUERY_INFORMATION, PROCESS_TERMINATE}, + }, }; /// This file is meant to house (OS specific) implementations on how to kill processes. @@ -18,36 +18,36 @@ struct Process(HANDLE); #[cfg(target_os = "windows")] impl Process { - fn open(pid: DWORD) -> Result { - let pc = unsafe { OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_TERMINATE, 0, pid) }; - if pc.is_null() { - return Err("!OpenProcess".to_string()); - } - Ok(Process(pc)) - } + fn open(pid: DWORD) -> Result { + let pc = unsafe { OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_TERMINATE, 0, pid) }; + if pc.is_null() { + return Err("!OpenProcess".to_string()); + } + Ok(Process(pc)) + } - fn kill(self) -> Result<(), String> { - unsafe { TerminateProcess(self.0, 1) }; - Ok(()) - } + fn kill(self) -> Result<(), String> { + unsafe { TerminateProcess(self.0, 1) }; + Ok(()) + } } /// Kills a process, given a PID. pub fn kill_process_given_pid(pid: u32) -> crate::utils::error::Result<()> { - if cfg!(target_os = "linux") || cfg!(target_os = "macos") { - Command::new("kill").arg(pid.to_string()).output()?; - } else if cfg!(target_os = "windows") { - #[cfg(target_os = "windows")] - { - let process = Process::open(pid as DWORD)?; - process.kill()?; - } - } else { - return Err(BottomError::GenericError( - "Sorry, support operating systems outside the main three are not implemented yet!" - .to_string(), - )); - } + if cfg!(target_os = "linux") || cfg!(target_os = "macos") { + Command::new("kill").arg(pid.to_string()).output()?; + } else if cfg!(target_os = "windows") { + #[cfg(target_os = "windows")] + { + let process = Process::open(pid as DWORD)?; + process.kill()?; + } + } else { + return Err(BottomError::GenericError( + "Sorry, support operating systems outside the main three are not implemented yet!" + .to_string(), + )); + } - Ok(()) + Ok(()) } diff --git a/src/canvas.rs b/src/canvas.rs index 64d325fe..48cad2ce 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -1,13 +1,13 @@ -use std::cmp::max; +use std::cmp::{max, min}; use std::collections::HashMap; use tui::{ - backend, - layout::{Alignment, Constraint, Direction, Layout, Rect}, - style::{Color, Style}, - terminal::Frame, - widgets::{Axis, Block, Borders, Chart, Dataset, Marker, Paragraph, Row, Table, Text, Widget}, - Terminal, + backend::Backend, + layout::{Alignment, Constraint, Direction, Layout, Rect}, + style::{Color, Style}, + terminal::Frame, + widgets::{Axis, Block, Borders, Chart, Dataset, Marker, Paragraph, Row, Table, Text, Widget}, + Terminal, }; use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; @@ -16,10 +16,10 @@ use canvas_colours::*; use drawing_utils::*; use crate::{ - app::{self, data_harvester::processes::ProcessHarvest, WidgetPosition}, - constants::*, - data_conversion::{ConvertedCpuData, ConvertedProcessData}, - utils::error, + app::{self, data_harvester::processes::ProcessHarvest, WidgetPosition}, + constants::*, + data_conversion::{ConvertedCpuData, ConvertedProcessData}, + utils::error, }; mod canvas_colours; @@ -35,1460 +35,1855 @@ const NETWORK_HEADERS: [&str; 4] = ["RX", "TX", "Total RX", "Total TX"]; const FORCE_MIN_THRESHOLD: usize = 5; lazy_static! { - static ref DEFAULT_TEXT_STYLE: Style = Style::default().fg(Color::Gray); - static ref DEFAULT_HEADER_STYLE: Style = Style::default().fg(Color::LightBlue); - static ref DISK_HEADERS_LENS: Vec = DISK_HEADERS - .iter() - .map(|entry| max(FORCE_MIN_THRESHOLD, entry.len())) - .collect::>(); - static ref CPU_LEGEND_HEADER_LENS: Vec = CPU_LEGEND_HEADER - .iter() - .map(|entry| max(FORCE_MIN_THRESHOLD, entry.len())) - .collect::>(); - static ref CPU_SELECT_LEGEND_HEADER_LENS: Vec = CPU_SELECT_LEGEND_HEADER - .iter() - .map(|entry| max(FORCE_MIN_THRESHOLD, entry.len())) - .collect::>(); - static ref TEMP_HEADERS_LENS: Vec = TEMP_HEADERS - .iter() - .map(|entry| max(FORCE_MIN_THRESHOLD, entry.len())) - .collect::>(); - static ref MEM_HEADERS_LENS: Vec = MEM_HEADERS - .iter() - .map(|entry| max(FORCE_MIN_THRESHOLD, entry.len())) - .collect::>(); - static ref NETWORK_HEADERS_LENS: Vec = NETWORK_HEADERS - .iter() - .map(|entry| max(FORCE_MIN_THRESHOLD, entry.len())) - .collect::>(); + static ref SIDE_BORDERS: Borders = Borders::from_bits_truncate(20); + static ref DEFAULT_TEXT_STYLE: Style = Style::default().fg(Color::Gray); + static ref DEFAULT_HEADER_STYLE: Style = Style::default().fg(Color::LightBlue); + static ref DISK_HEADERS_LENS: Vec = DISK_HEADERS + .iter() + .map(|entry| max(FORCE_MIN_THRESHOLD, entry.len())) + .collect::>(); + static ref CPU_LEGEND_HEADER_LENS: Vec = CPU_LEGEND_HEADER + .iter() + .map(|entry| max(FORCE_MIN_THRESHOLD, entry.len())) + .collect::>(); + static ref CPU_SELECT_LEGEND_HEADER_LENS: Vec = CPU_SELECT_LEGEND_HEADER + .iter() + .map(|entry| max(FORCE_MIN_THRESHOLD, entry.len())) + .collect::>(); + static ref TEMP_HEADERS_LENS: Vec = TEMP_HEADERS + .iter() + .map(|entry| max(FORCE_MIN_THRESHOLD, entry.len())) + .collect::>(); + static ref MEM_HEADERS_LENS: Vec = MEM_HEADERS + .iter() + .map(|entry| max(FORCE_MIN_THRESHOLD, entry.len())) + .collect::>(); + static ref NETWORK_HEADERS_LENS: Vec = NETWORK_HEADERS + .iter() + .map(|entry| max(FORCE_MIN_THRESHOLD, entry.len())) + .collect::>(); } #[derive(Default)] pub struct DisplayableData { - pub rx_display: String, - pub tx_display: String, - pub total_rx_display: String, - pub total_tx_display: String, - pub network_data_rx: Vec<(f64, f64)>, - pub network_data_tx: Vec<(f64, f64)>, - pub disk_data: Vec>, - pub temp_sensor_data: Vec>, - pub process_data: HashMap, // Not the final value - pub grouped_process_data: Vec, // Not the final value - pub finalized_process_data: Vec, // What's actually displayed - pub mem_label: String, - pub swap_label: String, - pub mem_data: Vec<(f64, f64)>, - pub swap_data: Vec<(f64, f64)>, - pub cpu_data: Vec, + pub rx_display: String, + pub tx_display: String, + pub total_rx_display: String, + pub total_tx_display: String, + pub network_data_rx: Vec<(f64, f64)>, + pub network_data_tx: Vec<(f64, f64)>, + pub disk_data: Vec>, + pub temp_sensor_data: Vec>, + pub process_data: HashMap, + // Not the final value + pub grouped_process_data: Vec, + // Not the final value + pub finalized_process_data: Vec, + // What's actually displayed + pub mem_label: String, + pub swap_label: String, + pub mem_data: Vec<(f64, f64)>, + pub swap_data: Vec<(f64, f64)>, + pub cpu_data: Vec, } #[allow(dead_code)] #[derive(Default)] /// Handles the canvas' state. TODO: [OPT] implement this. pub struct Painter { - height: u16, - width: u16, - vertical_dialog_chunk: Vec, - middle_dialog_chunk: Vec, - vertical_chunks: Vec, - middle_chunks: Vec, - middle_divided_chunk_2: Vec, - bottom_chunks: Vec, - cpu_chunk: Vec, - network_chunk: Vec, - pub colours: CanvasColours, - pub styled_general_help_text: Vec>, - pub styled_process_help_text: Vec>, - pub styled_search_help_text: Vec>, - is_mac_os: bool, + height: u16, + width: u16, + vertical_dialog_chunk: Vec, + middle_dialog_chunk: Vec, + vertical_chunks: Vec, + middle_chunks: Vec, + middle_divided_chunk_2: Vec, + bottom_chunks: Vec, + cpu_chunk: Vec, + network_chunk: Vec, + pub colours: CanvasColours, + pub styled_general_help_text: Vec>, + pub styled_process_help_text: Vec>, + pub styled_search_help_text: Vec>, + is_mac_os: bool, } impl Painter { - /// Must be run once before drawing, but after setting colours. - /// This is to set some remaining styles and text. - /// This bypasses some logic checks (size > 2, for example) but this - /// assumes that you, the programmer, are sane and do not do stupid things. - /// RIGHT? - pub fn initialize(&mut self) { - self.is_mac_os = cfg!(target_os = "macos"); - - self.styled_general_help_text.push(Text::Styled( - GENERAL_HELP_TEXT[0].into(), - self.colours.table_header_style, - )); - self.styled_general_help_text.extend( - GENERAL_HELP_TEXT[1..] - .iter() - .map(|&text| Text::Styled(text.into(), self.colours.text_style)) - .collect::>(), - ); - - self.styled_process_help_text.push(Text::Styled( - PROCESS_HELP_TEXT[0].into(), - self.colours.table_header_style, - )); - self.styled_process_help_text.extend( - PROCESS_HELP_TEXT[1..] - .iter() - .map(|&text| Text::Styled(text.into(), self.colours.text_style)) - .collect::>(), - ); - - self.styled_search_help_text.push(Text::Styled( - SEARCH_HELP_TEXT[0].into(), - self.colours.table_header_style, - )); - self.styled_search_help_text.extend( - SEARCH_HELP_TEXT[1..] - .iter() - .map(|&text| Text::Styled(text.into(), self.colours.text_style)) - .collect::>(), - ); - } - - // TODO: [REFACTOR] We should clean this up tbh - // TODO: [FEATURE] Auto-resizing dialog sizes. - #[allow(clippy::cognitive_complexity)] - pub fn draw_data( - &mut self, terminal: &mut Terminal, app_state: &mut app::App, - ) -> error::Result<()> { - let terminal_size = terminal.size()?; - let current_height = terminal_size.height; - let current_width = terminal_size.width; - - if self.height == 0 && self.width == 0 { - self.height = current_height; - self.width = current_width; - } else if self.height != current_height || self.width != current_width { - app_state.is_resized = true; - } - - terminal.autoresize()?; - terminal.draw(|mut f| { - if app_state.help_dialog_state.is_showing_help { - // Only for the help - let vertical_dialog_chunk = Layout::default() - .direction(Direction::Vertical) - .margin(1) - .constraints( - [ - Constraint::Percentage(20), - Constraint::Percentage(60), - Constraint::Percentage(20), - ] - .as_ref(), - ) - .split(f.size()); - - let middle_dialog_chunk = Layout::default() - .direction(Direction::Horizontal) - .margin(0) - .constraints( - [ - Constraint::Percentage(20), - Constraint::Percentage(60), - Constraint::Percentage(20), - ] - .as_ref(), - ) - .split(vertical_dialog_chunk[1]); - - const HELP_BASE: &str = - " Help ── 1: General ─── 2: Processes ─── 3: Search ─── Esc to close "; - let repeat_num = max( - 0, - middle_dialog_chunk[1].width as i32 - HELP_BASE.chars().count() as i32 - 2, - ); - let help_title = format!( - " Help ─{}─ 1: General ─── 2: Processes ─── 3: Search ─── Esc to close ", - "─".repeat(repeat_num as usize) - ); - - Paragraph::new( - match app_state.help_dialog_state.current_category { - app::AppHelpCategory::General => &self.styled_general_help_text, - app::AppHelpCategory::Process => &self.styled_process_help_text, - app::AppHelpCategory::Search => &self.styled_search_help_text, - } - .iter(), - ) - .block( - Block::default() - .title(&help_title) - .title_style(self.colours.border_style) - .style(self.colours.border_style) - .borders(Borders::ALL) - .border_style(self.colours.border_style), - ) - .style(self.colours.text_style) - .alignment(Alignment::Left) - .wrap(true) - .render(&mut f, middle_dialog_chunk[1]); - } else if app_state.delete_dialog_state.is_showing_dd { - let vertical_dialog_chunk = Layout::default() - .direction(Direction::Vertical) - .margin(1) - .constraints( - [ - Constraint::Percentage(35), - Constraint::Percentage(30), - Constraint::Percentage(35), - ] - .as_ref(), - ) - .split(f.size()); - - let middle_dialog_chunk = Layout::default() - .direction(Direction::Horizontal) - .margin(0) - .constraints( - [ - Constraint::Percentage(25), - Constraint::Percentage(50), - Constraint::Percentage(25), - ] - .as_ref(), - ) - .split(vertical_dialog_chunk[1]); - - if let Some(dd_err) = &app_state.dd_err { - let dd_text = [Text::raw(format!( - "\nFailure to properly kill the process - {}", - dd_err - ))]; - - const ERROR_BASE: &str = " Error ── Esc to close "; - let repeat_num = max( - 0, - middle_dialog_chunk[1].width as i32 - ERROR_BASE.chars().count() as i32 - 2, - ); - let error_title = - format!(" Error ─{}─ Esc to close ", "─".repeat(repeat_num as usize)); - - Paragraph::new(dd_text.iter()) - .block( - Block::default() - .title(&error_title) - .title_style(self.colours.border_style) - .style(self.colours.border_style) - .borders(Borders::ALL) - .border_style(self.colours.border_style), - ) - .style(self.colours.text_style) - .alignment(Alignment::Center) - .wrap(true) - .render(&mut f, middle_dialog_chunk[1]); - } else if let Some(to_kill_processes) = app_state.get_to_delete_processes() { - if let Some(first_pid) = to_kill_processes.1.first() { - let dd_text = vec![ - if app_state.is_grouped() { - if to_kill_processes.1.len() != 1 { - Text::raw(format!( - "\nAre you sure you want to kill {} processes with the name {}?", - to_kill_processes.1.len(), to_kill_processes.0 - )) - } else { - Text::raw(format!( - "\nAre you sure you want to kill {} process with the name {}?", - to_kill_processes.1.len(), to_kill_processes.0 - )) - } - } else { - Text::raw(format!( - "\nAre you sure you want to kill process {} with PID {}?", - to_kill_processes.0, first_pid - )) - }, - Text::raw("\nNote that if bottom is frozen, it must be unfrozen for changes to be shown.\n\n\n"), - if app_state.delete_dialog_state.is_on_yes { - Text::styled("Yes", self.colours.currently_selected_text_style) - } else { - Text::raw("Yes") - }, - Text::raw(" "), - if app_state.delete_dialog_state.is_on_yes { - Text::raw("No") - } else { - Text::styled("No", self.colours.currently_selected_text_style) - }, - ]; - - const DD_BASE: &str = " Confirm Kill Process ── Esc to close "; - let repeat_num = max( - 0, - middle_dialog_chunk[1].width as i32 - - DD_BASE.chars().count() as i32 - 2, - ); - let dd_title = format!( - " Confirm Kill Process ─{}─ Esc to close ", - "─".repeat(repeat_num as usize) - ); - - Paragraph::new(dd_text.iter()) - .block( - Block::default() - .title(&dd_title) - .title_style(self.colours.border_style) - .style(self.colours.border_style) - .borders(Borders::ALL) - .border_style(self.colours.border_style), - ) - .style(self.colours.text_style) - .alignment(Alignment::Center) - .wrap(true) - .render(&mut f, middle_dialog_chunk[1]); - } else { - // This is a bit nasty, but it works well... I guess. - app_state.delete_dialog_state.is_showing_dd = false; - } - } else { - // This is a bit nasty, but it works well... I guess. - app_state.delete_dialog_state.is_showing_dd = false; - } - } else if app_state.is_expanded { - // TODO: [REF] we should combine this with normal drawing tbh - - let rect = Layout::default() - .margin(1) - .constraints([Constraint::Percentage(100)].as_ref()) - .split(f.size()); - match &app_state.current_widget_selected { - WidgetPosition::Cpu => { - let cpu_chunk = Layout::default() - .direction(Direction::Horizontal) - .margin(0) - .constraints( - if app_state.app_config_fields.left_legend { - [Constraint::Percentage(15), Constraint::Percentage(85)] - } else { - [Constraint::Percentage(85), Constraint::Percentage(15)] - } - .as_ref(), - ) - .split(rect[0]); - - let legend_index = if app_state.app_config_fields.left_legend { - 0 - } else { - 1 - }; - let graph_index = if app_state.app_config_fields.left_legend { - 1 - } else { - 0 - }; - - self.draw_cpu_graph(&mut f, &app_state, cpu_chunk[graph_index]); - self.draw_cpu_legend(&mut f, app_state, cpu_chunk[legend_index]); - } - WidgetPosition::Mem => { - self.draw_memory_graph(&mut f, &app_state, rect[0]); - } - WidgetPosition::Disk => { - self.draw_disk_table(&mut f, app_state, rect[0]); - } - WidgetPosition::Temp => { - self.draw_temp_table(&mut f, app_state, rect[0]); - } - WidgetPosition::Network => { - self.draw_network_graph(&mut f, &app_state, rect[0]); - } - WidgetPosition::Process | WidgetPosition::ProcessSearch => { - if app_state.is_searching() { - let processes_chunk = Layout::default() - .direction(Direction::Vertical) - .margin(0) - .constraints( - [Constraint::Percentage(85), Constraint::Percentage(15)] - .as_ref(), - ) - .split(rect[0]); - - self.draw_processes_table(&mut f, app_state, processes_chunk[0]); - self.draw_search_field(&mut f, app_state, processes_chunk[1]); - } else { - self.draw_processes_table(&mut f, app_state, rect[0]); - } - } - } - } else { - // TODO: [TUI] Change this back to a more even 33/33/34 when TUI releases - let vertical_chunks = Layout::default() - .direction(Direction::Vertical) - .margin(1) - .constraints( - [ - Constraint::Percentage(30), - Constraint::Percentage(37), - Constraint::Percentage(33), - ] - .as_ref(), - ) - .split(f.size()); - - let middle_chunks = Layout::default() - .direction(Direction::Horizontal) - .margin(0) - .constraints([Constraint::Percentage(60), Constraint::Percentage(40)].as_ref()) - .split(vertical_chunks[1]); - - let middle_divided_chunk_2 = Layout::default() - .direction(Direction::Vertical) - .margin(0) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) - .split(middle_chunks[1]); - - let bottom_chunks = Layout::default() - .direction(Direction::Horizontal) - .margin(0) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) - .split(vertical_chunks[2]); - - // Component specific chunks - let cpu_chunk = Layout::default() - .direction(Direction::Horizontal) - .margin(0) - .constraints( - if app_state.app_config_fields.left_legend { - [Constraint::Percentage(15), Constraint::Percentage(85)] - } else { - [Constraint::Percentage(85), Constraint::Percentage(15)] - } - .as_ref(), - ) - .split(vertical_chunks[0]); - - let network_chunk = Layout::default() - .direction(Direction::Vertical) - .margin(0) - .constraints( - if (bottom_chunks[0].height as f64 * 0.25) as u16 >= 4 { - [Constraint::Percentage(75), Constraint::Percentage(25)] - } else { - let required = if bottom_chunks[0].height < 10 { - bottom_chunks[0].height / 2 - } else { - 5 - }; - let remaining = bottom_chunks[0].height - required; - [Constraint::Length(remaining), Constraint::Length(required)] - } - .as_ref(), - ) - .split(bottom_chunks[0]); - - // Default chunk index based on left or right legend setting - let legend_index = if app_state.app_config_fields.left_legend { - 0 - } else { - 1 - }; - let graph_index = if app_state.app_config_fields.left_legend { - 1 - } else { - 0 - }; - - // Set up blocks and their components - // CPU graph + legend - self.draw_cpu_graph(&mut f, &app_state, cpu_chunk[graph_index]); - self.draw_cpu_legend(&mut f, app_state, cpu_chunk[legend_index]); - - //Memory usage graph - self.draw_memory_graph(&mut f, &app_state, middle_chunks[0]); - - // Network graph - self.draw_network_graph(&mut f, &app_state, network_chunk[0]); - self.draw_network_labels(&mut f, app_state, network_chunk[1]); - - // Temperature table - self.draw_temp_table(&mut f, app_state, middle_divided_chunk_2[0]); - - // Disk usage table - self.draw_disk_table(&mut f, app_state, middle_divided_chunk_2[1]); - - // Processes table - if app_state.is_searching() { - let processes_chunk = Layout::default() - .direction(Direction::Vertical) - .margin(0) - .constraints( - if (bottom_chunks[1].height as f64 * 0.25) as u16 >= 4 { - [Constraint::Percentage(75), Constraint::Percentage(25)] - } else { - let required = if bottom_chunks[1].height < 10 { - bottom_chunks[1].height / 2 - } else { - 5 - }; - let remaining = bottom_chunks[1].height - required; - [Constraint::Length(remaining), Constraint::Length(required)] - } - .as_ref(), - ) - .split(bottom_chunks[1]); - - self.draw_processes_table(&mut f, app_state, processes_chunk[0]); - self.draw_search_field(&mut f, app_state, processes_chunk[1]); - } else { - self.draw_processes_table(&mut f, app_state, bottom_chunks[1]); - } - } - })?; - - app_state.is_resized = false; - - Ok(()) - } - - fn draw_cpu_graph( - &self, f: &mut Frame<'_, B>, app_state: &app::App, draw_loc: Rect, - ) { - let cpu_data: &[ConvertedCpuData] = &app_state.canvas_data.cpu_data; - - // CPU usage graph - let x_axis: Axis<'_, String> = Axis::default().bounds([0.0, TIME_STARTS_FROM as f64]); - let y_axis = Axis::default() - .style(self.colours.graph_style) - .labels_style(self.colours.graph_style) - .bounds([-0.5, 100.5]) - .labels(&["0%", "100%"]); - - let dataset_vector: Vec> = cpu_data - .iter() - .enumerate() - .rev() - .filter_map(|(itx, cpu)| { - if app_state.cpu_state.core_show_vec[itx] { - Some( - Dataset::default() - .marker(if app_state.app_config_fields.use_dot { - Marker::Dot - } else { - Marker::Braille - }) - .style( - if app_state.app_config_fields.show_average_cpu && itx == 0 { - self.colours.avg_colour_style - } else { - self.colours.cpu_colour_styles - [itx % self.colours.cpu_colour_styles.len()] - }, - ) - .data(&cpu.cpu_data[..]), - ) - } else { - None - } - }) - .collect(); - - let title = if app_state.is_expanded && !app_state.cpu_state.is_showing_tray { - const TITLE_BASE: &str = " CPU ── Esc to go back "; - let repeat_num = max( - 0, - draw_loc.width as i32 - TITLE_BASE.chars().count() as i32 - 2, - ); - let result_title = - format!(" CPU ─{}─ Esc to go back ", "─".repeat(repeat_num as usize)); - - result_title - } else { - " CPU ".to_string() - }; - - Chart::default() - .block( - Block::default() - .title(&title) - .title_style(if app_state.is_expanded { - self.colours.highlighted_border_style - } else { - self.colours.widget_title_style - }) - .borders(Borders::ALL) - .border_style(match app_state.current_widget_selected { - app::WidgetPosition::Cpu => self.colours.highlighted_border_style, - _ => self.colours.border_style, - }), - ) - .x_axis(x_axis) - .y_axis(y_axis) - .datasets(&dataset_vector) - .render(f, draw_loc); - } - - fn draw_cpu_legend( - &self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, - ) { - let cpu_data: &[ConvertedCpuData] = &app_state.canvas_data.cpu_data; - - let num_rows = max(0, i64::from(draw_loc.height) - 5) as u64; - let start_position = get_start_position( - num_rows, - &app_state.app_scroll_positions.scroll_direction, - &mut app_state - .app_scroll_positions - .cpu_scroll_state - .previous_scroll_position, - app_state - .app_scroll_positions - .cpu_scroll_state - .current_scroll_position, - app_state.is_resized, - ); - - let sliced_cpu_data = &cpu_data[start_position as usize..]; - let mut stringified_cpu_data: Vec> = Vec::new(); - - for (itx, cpu) in sliced_cpu_data.iter().enumerate() { - if app_state.cpu_state.is_showing_tray { - stringified_cpu_data.push(vec![ - cpu.cpu_name.clone(), - if app_state.cpu_state.core_show_vec[itx + start_position as usize] { - "[*]".to_string() - } else { - "[ ]".to_string() - }, - ]); - } else if let Some(cpu_data) = cpu.cpu_data.last() { - if app_state.app_config_fields.show_disabled_data - || app_state.cpu_state.core_show_vec[itx] - { - stringified_cpu_data.push(vec![ - cpu.cpu_name.clone(), - format!("{:.0}%", cpu_data.1.round()), - ]); - } - } - } - - let cpu_rows = stringified_cpu_data - .iter() - .enumerate() - .map(|(itx, cpu_string_row)| { - Row::StyledData( - cpu_string_row.iter(), - match app_state.current_widget_selected { - app::WidgetPosition::Cpu => { - if itx as u64 - == app_state - .app_scroll_positions - .cpu_scroll_state - .current_scroll_position - start_position - { - self.colours.currently_selected_text_style - } else if app_state.app_config_fields.show_average_cpu && itx == 0 { - self.colours.avg_colour_style - } else { - self.colours.cpu_colour_styles[itx - + start_position as usize - % self.colours.cpu_colour_styles.len()] - } - } - _ => { - if app_state.app_config_fields.show_average_cpu && itx == 0 { - self.colours.avg_colour_style - } else { - self.colours.cpu_colour_styles[itx - + start_position as usize - % self.colours.cpu_colour_styles.len()] - } - } - }, - ) - }); - - // Calculate widths - let width = f64::from(draw_loc.width); - let width_ratios = vec![0.5, 0.5]; - - let variable_intrinsic_results = get_variable_intrinsic_widths( - width as u16, - &width_ratios, - if app_state.cpu_state.is_showing_tray { - &CPU_SELECT_LEGEND_HEADER_LENS - } else { - &CPU_LEGEND_HEADER_LENS - }, - ); - let intrinsic_widths = &(variable_intrinsic_results.0)[0..variable_intrinsic_results.1]; - - let title = if app_state.cpu_state.is_showing_tray { - const TITLE_BASE: &str = " Esc to close "; - let repeat_num = max( - 0, - draw_loc.width as i32 - TITLE_BASE.chars().count() as i32 - 2, - ); - let result_title = format!("{} Esc to close ", "─".repeat(repeat_num as usize)); - - result_title - } else { - "".to_string() - }; - - // Draw - Table::new( - if app_state.cpu_state.is_showing_tray { - CPU_SELECT_LEGEND_HEADER - } else { - CPU_LEGEND_HEADER - } - .iter(), - cpu_rows, - ) - .block( - Block::default() - .title(&title) - .title_style(if app_state.is_expanded { - self.colours.highlighted_border_style - } else { - match app_state.current_widget_selected { - app::WidgetPosition::Cpu => self.colours.highlighted_border_style, - _ => self.colours.border_style, - } - }) - .borders(Borders::ALL) - .border_style(match app_state.current_widget_selected { - app::WidgetPosition::Cpu => self.colours.highlighted_border_style, - _ => self.colours.border_style, - }), - ) - .header_style(self.colours.table_header_style) - .widths( - &(intrinsic_widths - .iter() - .map(|calculated_width| Constraint::Length(*calculated_width as u16)) - .collect::>()), - ) - .render(f, draw_loc); - } - - fn draw_memory_graph( - &self, f: &mut Frame<'_, B>, app_state: &app::App, draw_loc: Rect, - ) { - let mem_data: &[(f64, f64)] = &app_state.canvas_data.mem_data; - let swap_data: &[(f64, f64)] = &app_state.canvas_data.swap_data; - - let x_axis: Axis<'_, String> = Axis::default().bounds([0.0, TIME_STARTS_FROM as f64]); - - // Offset as the zero value isn't drawn otherwise... - let y_axis: Axis<'_, &str> = Axis::default() - .style(self.colours.graph_style) - .labels_style(self.colours.graph_style) - .bounds([-0.5, 100.5]) - .labels(&["0%", "100%"]); - - let mem_canvas_vec: Vec> = vec![ - Dataset::default() - .name(&app_state.canvas_data.mem_label) - .marker(if app_state.app_config_fields.use_dot { - Marker::Dot - } else { - Marker::Braille - }) - .style(self.colours.ram_style) - .data(&mem_data), - Dataset::default() - .name(&app_state.canvas_data.swap_label) - .marker(if app_state.app_config_fields.use_dot { - Marker::Dot - } else { - Marker::Braille - }) - .style(self.colours.swap_style) - .data(&swap_data), - ]; - - let title = if app_state.is_expanded { - const TITLE_BASE: &str = " Memory ── Esc to go back "; - let repeat_num = max( - 0, - draw_loc.width as i32 - TITLE_BASE.chars().count() as i32 - 2, - ); - let result_title = format!( - " Memory ─{}─ Esc to go back ", - "─".repeat(repeat_num as usize) - ); - - result_title - } else { - " Memory ".to_string() - }; - - Chart::default() - .block( - Block::default() - .title(&title) - .title_style(if app_state.is_expanded { - self.colours.highlighted_border_style - } else { - self.colours.widget_title_style - }) - .borders(Borders::ALL) - .border_style(match app_state.current_widget_selected { - app::WidgetPosition::Mem => self.colours.highlighted_border_style, - _ => self.colours.border_style, - }), - ) - .x_axis(x_axis) - .y_axis(y_axis) - .datasets(&mem_canvas_vec) - .render(f, draw_loc); - } - - fn draw_network_graph( - &self, f: &mut Frame<'_, B>, app_state: &app::App, draw_loc: Rect, - ) { - let network_data_rx: &[(f64, f64)] = &app_state.canvas_data.network_data_rx; - let network_data_tx: &[(f64, f64)] = &app_state.canvas_data.network_data_tx; - - let x_axis: Axis<'_, String> = Axis::default().bounds([0.0, 60_000.0]); - let y_axis: Axis<'_, &str> = Axis::default() - .style(self.colours.graph_style) - .labels_style(self.colours.graph_style) - .bounds([-0.5, 30_f64]) - .labels(&["0B", "1KiB", "1MiB", "1GiB"]); - - let title = if app_state.is_expanded { - const TITLE_BASE: &str = " Network ── Esc to go back "; - let repeat_num = max( - 0, - draw_loc.width as i32 - TITLE_BASE.chars().count() as i32 - 2, - ); - let result_title = format!( - " Network ─{}─ Esc to go back ", - "─".repeat(repeat_num as usize) - ); - - result_title - } else { - " Network ".to_string() - }; - - Chart::default() - .block( - Block::default() - .title(&title) - .title_style(if app_state.is_expanded { - self.colours.highlighted_border_style - } else { - self.colours.widget_title_style - }) - .borders(Borders::ALL) - .border_style(match app_state.current_widget_selected { - app::WidgetPosition::Network => self.colours.highlighted_border_style, - _ => self.colours.border_style, - }), - ) - .x_axis(x_axis) - .y_axis(y_axis) - .datasets(&[ - Dataset::default() - .name(&format!("RX: {:7}", app_state.canvas_data.rx_display)) - .marker(if app_state.app_config_fields.use_dot { - Marker::Dot - } else { - Marker::Braille - }) - .style(self.colours.rx_style) - .data(&network_data_rx), - Dataset::default() - .name(&format!("TX: {:7}", app_state.canvas_data.tx_display)) - .marker(if app_state.app_config_fields.use_dot { - Marker::Dot - } else { - Marker::Braille - }) - .style(self.colours.tx_style) - .data(&network_data_tx), - Dataset::default() - .name(&format!( - "Total RX: {:7}", - app_state.canvas_data.total_rx_display - )) - .style(self.colours.rx_total_style), - Dataset::default() - .name(&format!( - "Total TX: {:7}", - app_state.canvas_data.total_tx_display - )) - .style(self.colours.tx_total_style), - ]) - .render(f, draw_loc); - } - - fn draw_network_labels( - &self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, - ) { - let rx_display = &app_state.canvas_data.rx_display; - let tx_display = &app_state.canvas_data.tx_display; - let total_rx_display = &app_state.canvas_data.total_rx_display; - let total_tx_display = &app_state.canvas_data.total_tx_display; - - // Gross but I need it to work... - let total_network = vec![vec![ - rx_display, - tx_display, - total_rx_display, - total_tx_display, - ]]; - let mapped_network = total_network - .iter() - .map(|val| Row::StyledData(val.iter(), self.colours.text_style)); - - // Calculate widths - let width_ratios: Vec = vec![0.25, 0.25, 0.25, 0.25]; - let lens: &[usize] = &NETWORK_HEADERS_LENS; - let width = f64::from(draw_loc.width); - - let variable_intrinsic_results = - get_variable_intrinsic_widths(width as u16, &width_ratios, lens); - let intrinsic_widths = &(variable_intrinsic_results.0)[0..variable_intrinsic_results.1]; - - // Draw - Table::new(NETWORK_HEADERS.iter(), mapped_network) - .block(Block::default().borders(Borders::ALL).border_style( - match app_state.current_widget_selected { - app::WidgetPosition::Network => self.colours.highlighted_border_style, - _ => self.colours.border_style, - }, - )) - .header_style(self.colours.table_header_style) - .style(self.colours.text_style) - .widths( - &(intrinsic_widths - .iter() - .map(|calculated_width| Constraint::Length(*calculated_width as u16)) - .collect::>()), - ) - .render(f, draw_loc); - } - - fn draw_temp_table( - &self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, - ) { - let temp_sensor_data: &[Vec] = &app_state.canvas_data.temp_sensor_data; - - let num_rows = max(0, i64::from(draw_loc.height) - 5) as u64; - let start_position = get_start_position( - num_rows, - &app_state.app_scroll_positions.scroll_direction, - &mut app_state - .app_scroll_positions - .temp_scroll_state - .previous_scroll_position, - app_state - .app_scroll_positions - .temp_scroll_state - .current_scroll_position, - app_state.is_resized, - ); - - let sliced_vec = &temp_sensor_data[start_position as usize..]; - let mut temp_row_counter: i64 = 0; - - let temperature_rows = sliced_vec.iter().map(|temp_row| { - Row::StyledData( - temp_row.iter(), - match app_state.current_widget_selected { - app::WidgetPosition::Temp => { - if temp_row_counter as u64 - == app_state - .app_scroll_positions - .temp_scroll_state - .current_scroll_position - start_position - { - temp_row_counter = -1; - self.colours.currently_selected_text_style - } else { - if temp_row_counter >= 0 { - temp_row_counter += 1; - } - self.colours.text_style - } - } - _ => self.colours.text_style, - }, - ) - }); - - // Calculate widths - let width = f64::from(draw_loc.width); - let width_ratios = [0.5, 0.5]; - let variable_intrinsic_results = - get_variable_intrinsic_widths(width as u16, &width_ratios, &TEMP_HEADERS_LENS); - let intrinsic_widths = &(variable_intrinsic_results.0)[0..variable_intrinsic_results.1]; - - let title = if app_state.is_expanded { - const TITLE_BASE: &str = " Temperatures ── Esc to go back "; - let repeat_num = max( - 0, - draw_loc.width as i32 - TITLE_BASE.chars().count() as i32 - 2, - ); - let result_title = format!( - " Temperatures ─{}─ Esc to go back ", - "─".repeat(repeat_num as usize) - ); - - result_title - } else { - " Temperatures ".to_string() - }; - - // Draw - Table::new(TEMP_HEADERS.iter(), temperature_rows) - .block( - Block::default() - .title(&title) - .title_style(if app_state.is_expanded { - self.colours.highlighted_border_style - } else { - self.colours.widget_title_style - }) - .borders(Borders::ALL) - .border_style(match app_state.current_widget_selected { - app::WidgetPosition::Temp => self.colours.highlighted_border_style, - _ => self.colours.border_style, - }), - ) - .header_style(self.colours.table_header_style) - .widths( - &(intrinsic_widths - .iter() - .map(|calculated_width| Constraint::Length(*calculated_width as u16)) - .collect::>()), - ) - .render(f, draw_loc); - } - - fn draw_disk_table( - &self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, - ) { - let disk_data: &[Vec] = &app_state.canvas_data.disk_data; - let num_rows = max(0, i64::from(draw_loc.height) - 5) as u64; - let start_position = get_start_position( - num_rows, - &app_state.app_scroll_positions.scroll_direction, - &mut app_state - .app_scroll_positions - .disk_scroll_state - .previous_scroll_position, - app_state - .app_scroll_positions - .disk_scroll_state - .current_scroll_position, - app_state.is_resized, - ); - - let sliced_vec = &disk_data[start_position as usize..]; - let mut disk_counter: i64 = 0; - - let disk_rows = sliced_vec.iter().map(|disk| { - Row::StyledData( - disk.iter(), - match app_state.current_widget_selected { - app::WidgetPosition::Disk => { - if disk_counter as u64 - == app_state - .app_scroll_positions - .disk_scroll_state - .current_scroll_position - start_position - { - disk_counter = -1; - self.colours.currently_selected_text_style - } else { - if disk_counter >= 0 { - disk_counter += 1; - } - self.colours.text_style - } - } - _ => self.colours.text_style, - }, - ) - }); - - // Calculate widths - // TODO: [PRETTY] Ellipsis on strings? - let width = f64::from(draw_loc.width); - let width_ratios = [0.2, 0.15, 0.13, 0.13, 0.13, 0.13, 0.13]; - let variable_intrinsic_results = - get_variable_intrinsic_widths(width as u16, &width_ratios, &DISK_HEADERS_LENS); - let intrinsic_widths = &variable_intrinsic_results.0[0..variable_intrinsic_results.1]; - - let title = if app_state.is_expanded { - const TITLE_BASE: &str = " Disk ── Esc to go back "; - let repeat_num = max( - 0, - draw_loc.width as i32 - TITLE_BASE.chars().count() as i32 - 2, - ); - let result_title = format!( - " Disk ─{}─ Esc to go back ", - "─".repeat(repeat_num as usize) - ); - - result_title - } else { - " Disk ".to_string() - }; - - // Draw! - Table::new(DISK_HEADERS.iter(), disk_rows) - .block( - Block::default() - .title(&title) - .title_style(if app_state.is_expanded { - self.colours.highlighted_border_style - } else { - self.colours.widget_title_style - }) - .borders(Borders::ALL) - .border_style(match app_state.current_widget_selected { - app::WidgetPosition::Disk => self.colours.highlighted_border_style, - _ => self.colours.border_style, - }), - ) - .header_style(self.colours.table_header_style) - .widths( - &(intrinsic_widths - .iter() - .map(|calculated_width| Constraint::Length(*calculated_width as u16)) - .collect::>()), - ) - .render(f, draw_loc); - } - - fn draw_search_field( - &self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, - ) { - let width = max(0, draw_loc.width as i64 - 34) as u64; // TODO: [REFACTOR] Hard coding this is terrible. - let cursor_position = app_state.get_cursor_position(); - let char_cursor_position = app_state.get_char_cursor_position(); - - let start_position: usize = get_search_start_position( - width as usize, - &app_state.process_search_state.search_state.cursor_direction, - &mut app_state.process_search_state.search_state.cursor_bar, - char_cursor_position, - app_state.is_resized, - ); - - let query = app_state.get_current_search_query().as_str(); - let grapheme_indices = UnicodeSegmentation::grapheme_indices(query, true); - let mut current_grapheme_posn = 0; - let query_with_cursor: Vec> = - if let app::WidgetPosition::ProcessSearch = app_state.current_widget_selected { - let mut res = grapheme_indices - .filter_map(|grapheme| { - current_grapheme_posn += UnicodeWidthStr::width(grapheme.1); - - if current_grapheme_posn <= start_position { - None - } else { - let styled = if grapheme.0 == cursor_position { - Text::styled(grapheme.1, self.colours.currently_selected_text_style) - } else { - Text::styled(grapheme.1, self.colours.text_style) - }; - Some(styled) - } - }) - .collect::>(); - - if cursor_position >= query.len() { - res.push(Text::styled( - " ", - self.colours.currently_selected_text_style, - )) - } - - res - } else { - // This is easier - we just need to get a range of graphemes, rather than - // dealing with possibly inserting a cursor (as none is shown!) - grapheme_indices - .filter_map(|grapheme| { - current_grapheme_posn += UnicodeWidthStr::width(grapheme.1); - if current_grapheme_posn <= start_position { - None - } else { - let styled = Text::styled(grapheme.1, self.colours.text_style); - Some(styled) - } - }) - .collect::>() - }; - - let mut search_text = vec![if app_state.is_grouped() { - Text::styled("Search by Name: ", self.colours.table_header_style) - } else if app_state.process_search_state.is_searching_with_pid { - Text::styled( - "Search by PID (Tab for Name): ", - self.colours.table_header_style, - ) - } else { - Text::styled( - "Search by Name (Tab for PID): ", - self.colours.table_header_style, - ) - }]; - - // Text options shamelessly stolen from VS Code. - let mut option_text = vec![]; - for _ in 0..(draw_loc.height - 3) { - option_text.push(Text::raw("\n")); - } - - let case_style = if !app_state.process_search_state.is_ignoring_case { - self.colours.currently_selected_text_style - } else { - self.colours.text_style - }; - - let whole_word_style = if app_state.process_search_state.is_searching_whole_word { - self.colours.currently_selected_text_style - } else { - self.colours.text_style - }; - - let regex_style = if app_state.process_search_state.is_searching_with_regex { - self.colours.currently_selected_text_style - } else { - self.colours.text_style - }; - - let case_text = format!( - "Match Case ({})[{}]", - if self.is_mac_os { "F1" } else { "Alt+C" }, - if !app_state.process_search_state.is_ignoring_case { - "*" - } else { - " " - } - ); - - let whole_text = format!( - "Match Whole Word ({})[{}]", - if self.is_mac_os { "F2" } else { "Alt+W" }, - if app_state.process_search_state.is_searching_whole_word { - "*" - } else { - " " - } - ); - - let regex_text = format!( - "Use Regex ({})[{}]", - if self.is_mac_os { "F3" } else { "Alt+R" }, - if app_state.process_search_state.is_searching_with_regex { - "*" - } else { - " " - } - ); - - let option_row = vec![ - Text::styled(&case_text, case_style), - Text::raw(" "), - Text::styled(&whole_text, whole_word_style), - Text::raw(" "), - Text::styled(®ex_text, regex_style), - ]; - option_text.extend(option_row); - - search_text.extend(query_with_cursor); - search_text.extend(option_text); - - const TITLE_BASE: &str = " Esc to close "; - let repeat_num = max( - 0, - draw_loc.width as i32 - TITLE_BASE.chars().count() as i32 - 2, - ); - let title = format!("{} Esc to close ", "─".repeat(repeat_num as usize)); - - let current_border_style: Style = if app_state - .process_search_state - .search_state - .is_invalid_search - { - Style::default().fg(Color::Rgb(255, 0, 0)) - } else { - match app_state.current_widget_selected { - app::WidgetPosition::ProcessSearch => self.colours.highlighted_border_style, - _ => self.colours.border_style, - } - }; - - Paragraph::new(search_text.iter()) - .block( - Block::default() - .borders(Borders::ALL) - .title(&title) - .title_style(current_border_style) - .border_style(current_border_style), - ) - .style(self.colours.text_style) - .alignment(Alignment::Left) - .wrap(false) - .render(f, draw_loc); - } - - fn draw_processes_table( - &self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, - ) { - let process_data: &[ConvertedProcessData] = &app_state.canvas_data.finalized_process_data; - - // Admittedly this is kinda a hack... but we need to: - // * Scroll - // * Show/hide elements based on scroll position - // - // As such, we use a process_counter to know when we've - // hit the process we've currently scrolled to. - // We also need to move the list - we can - // do so by hiding some elements! - let num_rows = max(0, i64::from(draw_loc.height) - 5) as u64; - - let position = get_start_position( - num_rows, - &app_state.app_scroll_positions.scroll_direction, - &mut app_state - .app_scroll_positions - .process_scroll_state - .previous_scroll_position, - app_state - .app_scroll_positions - .process_scroll_state - .current_scroll_position, - app_state.is_resized, - ); - - // Sanity check - let start_position = if position >= process_data.len() as u64 { - std::cmp::max(0, process_data.len() as i64 - 1) as u64 - } else { - position - }; - - let sliced_vec = &process_data[start_position as usize..]; - let mut process_counter: i64 = 0; - - // Draw! - let process_rows = sliced_vec.iter().map(|process| { - let stringified_process_vec: Vec = vec![ - if app_state.is_grouped() { - process.group_pids.len().to_string() - } else { - process.pid.to_string() - }, - process.name.clone(), - format!("{:.1}%", process.cpu_usage), - format!("{:.1}%", process.mem_usage), - ]; - Row::StyledData( - stringified_process_vec.into_iter(), - match app_state.current_widget_selected { - app::WidgetPosition::Process => { - if process_counter as u64 - == app_state - .app_scroll_positions - .process_scroll_state - .current_scroll_position - start_position - { - process_counter = -1; - self.colours.currently_selected_text_style - } else { - if process_counter >= 0 { - process_counter += 1; - } - self.colours.text_style - } - } - _ => self.colours.text_style, - }, - ) - }); - - use app::data_harvester::processes::ProcessSorting; - let mut pid_or_name = if app_state.is_grouped() { - "Count" - } else { - "PID(p)" - } - .to_string(); - let mut name = "Name(n)".to_string(); - let mut cpu = "CPU%(c)".to_string(); - let mut mem = "Mem%(m)".to_string(); - - let direction_val = if app_state.process_sorting_reverse { - "â–¼".to_string() - } else { - "â–²".to_string() - }; - - match app_state.process_sorting_type { - ProcessSorting::CPU => cpu += &direction_val, - ProcessSorting::MEM => mem += &direction_val, - ProcessSorting::PID => pid_or_name += &direction_val, - ProcessSorting::NAME => name += &direction_val, - }; - - let process_headers = [pid_or_name, name, cpu, mem]; - let process_headers_lens: Vec = process_headers - .iter() - .map(|entry| entry.len()) - .collect::>(); - - // Calculate widths - let width = f64::from(draw_loc.width); - let width_ratios = [0.2, 0.4, 0.2, 0.2]; - let variable_intrinsic_results = - get_variable_intrinsic_widths(width as u16, &width_ratios, &process_headers_lens); - let intrinsic_widths = &(variable_intrinsic_results.0)[0..variable_intrinsic_results.1]; - - let title = - if app_state.is_expanded && !app_state.process_search_state.search_state.is_enabled { - const TITLE_BASE: &str = " Processes ── Esc to go back "; - let repeat_num = max( - 0, - draw_loc.width as i32 - TITLE_BASE.chars().count() as i32 - 2, - ); - let result_title = format!( - " Processes ─{}─ Esc to go back ", - "─".repeat(repeat_num as usize) - ); - - result_title - } else { - " Processes ".to_string() - }; - - Table::new(process_headers.iter(), process_rows) - .block( - Block::default() - .title(&title) - .title_style(if app_state.is_expanded { - match app_state.current_widget_selected { - app::WidgetPosition::Process => self.colours.highlighted_border_style, - _ => self.colours.border_style, - } - } else { - self.colours.widget_title_style - }) - .borders(Borders::ALL) - .border_style(match app_state.current_widget_selected { - app::WidgetPosition::Process => self.colours.highlighted_border_style, - _ => self.colours.border_style, - }), - ) - .header_style(self.colours.table_header_style) - .widths( - &(intrinsic_widths - .iter() - .map(|calculated_width| Constraint::Length(*calculated_width as u16)) - .collect::>()), - ) - .render(f, draw_loc); - } + /// Must be run once before drawing, but after setting colours. + /// This is to set some remaining styles and text. + /// This bypasses some logic checks (size > 2, for example) but this + /// assumes that you, the programmer, are sane and do not do stupid things. + /// RIGHT? + pub fn initialize(&mut self) { + self.is_mac_os = cfg!(target_os = "macos"); + + self.styled_general_help_text.push(Text::Styled( + GENERAL_HELP_TEXT[0].into(), + self.colours.table_header_style, + )); + self.styled_general_help_text.extend( + GENERAL_HELP_TEXT[1..] + .iter() + .map(|&text| Text::Styled(text.into(), self.colours.text_style)) + .collect::>(), + ); + + self.styled_process_help_text.push(Text::Styled( + PROCESS_HELP_TEXT[0].into(), + self.colours.table_header_style, + )); + self.styled_process_help_text.extend( + PROCESS_HELP_TEXT[1..] + .iter() + .map(|&text| Text::Styled(text.into(), self.colours.text_style)) + .collect::>(), + ); + + self.styled_search_help_text.push(Text::Styled( + SEARCH_HELP_TEXT[0].into(), + self.colours.table_header_style, + )); + self.styled_search_help_text.extend( + SEARCH_HELP_TEXT[1..] + .iter() + .map(|&text| Text::Styled(text.into(), self.colours.text_style)) + .collect::>(), + ); + } + + pub fn draw_specific_table( + &self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, draw_border: bool, + widget_selected: WidgetPosition, + ) { + match widget_selected { + WidgetPosition::Process | WidgetPosition::ProcessSearch => { + self.draw_process_and_search(f, app_state, draw_loc, draw_border) + } + WidgetPosition::Temp => self.draw_temp_table(f, app_state, draw_loc, draw_border), + WidgetPosition::Disk => self.draw_disk_table(f, app_state, draw_loc, draw_border), + _ => {} + } + } + + // TODO: [REFACTOR] We should clean this up tbh + // TODO: [FEATURE] Auto-resizing dialog sizes. + #[allow(clippy::cognitive_complexity)] + pub fn draw_data( + &mut self, terminal: &mut Terminal, app_state: &mut app::App, + ) -> error::Result<()> { + let terminal_size = terminal.size()?; + let current_height = terminal_size.height; + let current_width = terminal_size.width; + + // TODO: [OPT] we might be able to add an argument s.t. if there is + // no resize AND it's not a data update (or process refresh/search/etc.) + // then just... don't draw again! + if self.height == 0 && self.width == 0 { + self.height = current_height; + self.width = current_width; + } else if self.height != current_height || self.width != current_width { + app_state.is_resized = true; + } + + terminal.autoresize()?; + terminal.draw(|mut f| { + if app_state.help_dialog_state.is_showing_help { + // Only for the help + let vertical_dialog_chunk = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints( + [ + Constraint::Percentage(20), + Constraint::Percentage(60), + Constraint::Percentage(20), + ] + .as_ref(), + ) + .split(f.size()); + + let middle_dialog_chunk = Layout::default() + .direction(Direction::Horizontal) + .margin(0) + .constraints( + [ + Constraint::Percentage(20), + Constraint::Percentage(60), + Constraint::Percentage(20), + ] + .as_ref(), + ) + .split(vertical_dialog_chunk[1]); + + const HELP_BASE: &str = + " Help ── 1: General ─── 2: Processes ─── 3: Search ─── Esc to close "; + let repeat_num = max( + 0, + middle_dialog_chunk[1].width as i32 - HELP_BASE.chars().count() as i32 - 2, + ); + let help_title = format!( + " Help ─{}─ 1: General ─── 2: Processes ─── 3: Search ─── Esc to close ", + "─".repeat(repeat_num as usize) + ); + + Paragraph::new( + match app_state.help_dialog_state.current_category { + app::AppHelpCategory::General => &self.styled_general_help_text, + app::AppHelpCategory::Process => &self.styled_process_help_text, + app::AppHelpCategory::Search => &self.styled_search_help_text, + } + .iter(), + ) + .block( + Block::default() + .title(&help_title) + .title_style(self.colours.border_style) + .style(self.colours.border_style) + .borders(Borders::ALL) + .border_style(self.colours.border_style), + ) + .style(self.colours.text_style) + .alignment(Alignment::Left) + .wrap(true) + .render(&mut f, middle_dialog_chunk[1]); + } else if app_state.delete_dialog_state.is_showing_dd { + let vertical_dialog_chunk = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints( + [ + Constraint::Percentage(35), + Constraint::Percentage(30), + Constraint::Percentage(35), + ] + .as_ref(), + ) + .split(f.size()); + + let middle_dialog_chunk = Layout::default() + .direction(Direction::Horizontal) + .margin(0) + .constraints( + [ + Constraint::Percentage(25), + Constraint::Percentage(50), + Constraint::Percentage(25), + ] + .as_ref(), + ) + .split(vertical_dialog_chunk[1]); + + if let Some(dd_err) = &app_state.dd_err { + let dd_text = [Text::raw(format!( + "\nFailure to properly kill the process - {}", + dd_err + ))]; + + const ERROR_BASE: &str = " Error ── Esc to close "; + let repeat_num = max( + 0, + middle_dialog_chunk[1].width as i32 - ERROR_BASE.chars().count() as i32 - 2, + ); + let error_title = + format!(" Error ─{}─ Esc to close ", "─".repeat(repeat_num as usize)); + + Paragraph::new(dd_text.iter()) + .block( + Block::default() + .title(&error_title) + .title_style(self.colours.border_style) + .style(self.colours.border_style) + .borders(Borders::ALL) + .border_style(self.colours.border_style), + ) + .style(self.colours.text_style) + .alignment(Alignment::Center) + .wrap(true) + .render(&mut f, middle_dialog_chunk[1]); + } else if let Some(to_kill_processes) = app_state.get_to_delete_processes() { + if let Some(first_pid) = to_kill_processes.1.first() { + let dd_text = vec![ + if app_state.is_grouped() { + if to_kill_processes.1.len() != 1 { + Text::raw(format!( + "\nAre you sure you want to kill {} processes with the name {}?", + to_kill_processes.1.len(), to_kill_processes.0 + )) + } else { + Text::raw(format!( + "\nAre you sure you want to kill {} process with the name {}?", + to_kill_processes.1.len(), to_kill_processes.0 + )) + } + } else { + Text::raw(format!( + "\nAre you sure you want to kill process {} with PID {}?", + to_kill_processes.0, first_pid + )) + }, + Text::raw("\nNote that if bottom is frozen, it must be unfrozen for changes to be shown.\n\n\n"), + if app_state.delete_dialog_state.is_on_yes { + Text::styled("Yes", self.colours.currently_selected_text_style) + } else { + Text::raw("Yes") + }, + Text::raw(" "), + if app_state.delete_dialog_state.is_on_yes { + Text::raw("No") + } else { + Text::styled("No", self.colours.currently_selected_text_style) + }, + ]; + + const DD_BASE: &str = " Confirm Kill Process ── Esc to close "; + let repeat_num = max( + 0, + middle_dialog_chunk[1].width as i32 + - DD_BASE.chars().count() as i32 - 2, + ); + let dd_title = format!( + " Confirm Kill Process ─{}─ Esc to close ", + "─".repeat(repeat_num as usize) + ); + + Paragraph::new(dd_text.iter()) + .block( + Block::default() + .title(&dd_title) + .title_style(self.colours.border_style) + .style(self.colours.border_style) + .borders(Borders::ALL) + .border_style(self.colours.border_style), + ) + .style(self.colours.text_style) + .alignment(Alignment::Center) + .wrap(true) + .render(&mut f, middle_dialog_chunk[1]); + } else { + // This is a bit nasty, but it works well... I guess. + app_state.delete_dialog_state.is_showing_dd = false; + } + } else { + // This is a bit nasty, but it works well... I guess. + app_state.delete_dialog_state.is_showing_dd = false; + } + } else if app_state.is_expanded { + // TODO: [REF] we should combine this with normal drawing tbh + + let rect = Layout::default() + .margin(1) + .constraints([Constraint::Percentage(100)].as_ref()) + .split(f.size()); + match &app_state.current_widget_selected { + WidgetPosition::Cpu | WidgetPosition::BasicCpu => { + let cpu_chunk = Layout::default() + .direction(Direction::Horizontal) + .margin(0) + .constraints( + if app_state.app_config_fields.left_legend { + [Constraint::Percentage(15), Constraint::Percentage(85)] + } else { + [Constraint::Percentage(85), Constraint::Percentage(15)] + } + .as_ref(), + ) + .split(rect[0]); + + let legend_index = if app_state.app_config_fields.left_legend { + 0 + } else { + 1 + }; + let graph_index = if app_state.app_config_fields.left_legend { + 1 + } else { + 0 + }; + + self.draw_cpu_graph(&mut f, &app_state, cpu_chunk[graph_index]); + self.draw_cpu_legend(&mut f, app_state, cpu_chunk[legend_index]); + } + WidgetPosition::Mem | WidgetPosition::BasicMem => { + self.draw_memory_graph(&mut f, &app_state, rect[0]); + } + WidgetPosition::Disk => { + self.draw_disk_table(&mut f, app_state, rect[0], true); + } + WidgetPosition::Temp => { + self.draw_temp_table(&mut f, app_state, rect[0], true); + } + WidgetPosition::Network | WidgetPosition::BasicNet => { + self.draw_network_graph(&mut f, &app_state, rect[0]); + } + WidgetPosition::Process | WidgetPosition::ProcessSearch => { + self.draw_process_and_search(&mut f, app_state, rect[0], true); + } + } + } else if app_state.app_config_fields.use_basic_mode { + // Basic mode. This basically removes all graphs but otherwise + // the same info. + + let cpu_height = (app_state.canvas_data.cpu_data.len() / 4) as u16 + + ( + if app_state.canvas_data.cpu_data.len() % 4 == 0 { + 0 + } else { + 1 + } + ); + let vertical_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(cpu_height), + Constraint::Length(1), + Constraint::Length(2), + Constraint::Length(2), + Constraint::Min(5), + ].as_ref()) + .split(f.size()); + + let middle_chunks= Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(50), + Constraint::Percentage(50), + ].as_ref()) + .split(vertical_chunks[2]); + self.draw_basic_cpu(&mut f, app_state, vertical_chunks[0]); + self.draw_basic_memory(&mut f, app_state, middle_chunks[0]); + self.draw_basic_network(&mut f, app_state, middle_chunks[1]); + self.draw_basic_table_arrows(&mut f, app_state, vertical_chunks[3]); + if app_state.current_widget_selected.is_widget_table() { + self.draw_specific_table(&mut f, app_state, vertical_chunks[4], false, app_state.current_widget_selected); + } else { + self.draw_specific_table(&mut f, app_state, vertical_chunks[4], false, app_state.previous_basic_table_selected); + } + } else { + // TODO: [TUI] Change this back to a more even 33/33/34 when TUI releases + let vertical_chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints( + [ + Constraint::Percentage(30), + Constraint::Percentage(37), + Constraint::Percentage(33), + ] + .as_ref(), + ) + .split(f.size()); + + let middle_chunks = Layout::default() + .direction(Direction::Horizontal) + .margin(0) + .constraints([Constraint::Percentage(60), Constraint::Percentage(40)].as_ref()) + .split(vertical_chunks[1]); + + let middle_divided_chunk_2 = Layout::default() + .direction(Direction::Vertical) + .margin(0) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .split(middle_chunks[1]); + + let bottom_chunks = Layout::default() + .direction(Direction::Horizontal) + .margin(0) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .split(vertical_chunks[2]); + + // Component specific chunks + let cpu_chunk = Layout::default() + .direction(Direction::Horizontal) + .margin(0) + .constraints( + if app_state.app_config_fields.left_legend { + [Constraint::Percentage(15), Constraint::Percentage(85)] + } else { + [Constraint::Percentage(85), Constraint::Percentage(15)] + } + .as_ref(), + ) + .split(vertical_chunks[0]); + + let network_chunk = Layout::default() + .direction(Direction::Vertical) + .margin(0) + .constraints( + if (bottom_chunks[0].height as f64 * 0.25) as u16 >= 4 { + [Constraint::Percentage(75), Constraint::Percentage(25)] + } else { + let required = if bottom_chunks[0].height < 10 { + bottom_chunks[0].height / 2 + } else { + 5 + }; + let remaining = bottom_chunks[0].height - required; + [Constraint::Length(remaining), Constraint::Length(required)] + } + .as_ref(), + ) + .split(bottom_chunks[0]); + + // Default chunk index based on left or right legend setting + let legend_index = if app_state.app_config_fields.left_legend { + 0 + } else { + 1 + }; + let graph_index = if app_state.app_config_fields.left_legend { + 1 + } else { + 0 + }; + + self.draw_cpu_graph(&mut f, &app_state, cpu_chunk[graph_index]); + self.draw_cpu_legend(&mut f, app_state, cpu_chunk[legend_index]); + self.draw_memory_graph(&mut f, &app_state, middle_chunks[0]); + self.draw_network_graph(&mut f, &app_state, network_chunk[0]); + self.draw_network_labels(&mut f, app_state, network_chunk[1]); + self.draw_temp_table(&mut f, app_state, middle_divided_chunk_2[0], true); + self.draw_disk_table(&mut f, app_state, middle_divided_chunk_2[1], true); + self.draw_process_and_search(&mut f, app_state, bottom_chunks[1], true); + } + })?; + + app_state.is_resized = false; + + Ok(()) + } + + fn draw_process_and_search( + &self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, draw_border: bool, + ) { + let search_width = if draw_border { 5 } else { 3 }; + + if app_state.is_searching() { + let processes_chunk = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(0), Constraint::Length(search_width)].as_ref()) + .split(draw_loc); + + self.draw_processes_table(f, app_state, processes_chunk[0], draw_border); + self.draw_search_field(f, app_state, processes_chunk[1], draw_border); + } else { + self.draw_processes_table(f, app_state, draw_loc, draw_border); + } + } + + fn draw_cpu_graph( + &self, f: &mut Frame<'_, B>, app_state: &app::App, draw_loc: Rect, + ) { + let cpu_data: &[ConvertedCpuData] = &app_state.canvas_data.cpu_data; + + // CPU usage graph + let x_axis: Axis<'_, String> = Axis::default().bounds([0.0, TIME_STARTS_FROM as f64]); + let y_axis = Axis::default() + .style(self.colours.graph_style) + .labels_style(self.colours.graph_style) + .bounds([-0.5, 100.5]) + .labels(&["0%", "100%"]); + + let dataset_vector: Vec> = cpu_data + .iter() + .enumerate() + .rev() + .filter_map(|(itx, cpu)| { + if app_state.cpu_state.core_show_vec[itx] { + Some( + Dataset::default() + .marker(if app_state.app_config_fields.use_dot { + Marker::Dot + } else { + Marker::Braille + }) + .style( + if app_state.app_config_fields.show_average_cpu && itx == 0 { + self.colours.avg_colour_style + } else { + self.colours.cpu_colour_styles + [itx % self.colours.cpu_colour_styles.len()] + }, + ) + .data(&cpu.cpu_data[..]), + ) + } else { + None + } + }) + .collect(); + + let title = if app_state.is_expanded && !app_state.cpu_state.is_showing_tray { + const TITLE_BASE: &str = " CPU ── Esc to go back "; + let repeat_num = max( + 0, + draw_loc.width as i32 - TITLE_BASE.chars().count() as i32 - 2, + ); + let result_title = + format!(" CPU ─{}─ Esc to go back ", "─".repeat(repeat_num as usize)); + + result_title + } else { + " CPU ".to_string() + }; + + Chart::default() + .block( + Block::default() + .title(&title) + .title_style(if app_state.is_expanded { + self.colours.highlighted_border_style + } else { + self.colours.widget_title_style + }) + .borders(Borders::ALL) + .border_style(match app_state.current_widget_selected { + WidgetPosition::Cpu | WidgetPosition::BasicCpu => { + self.colours.highlighted_border_style + } + _ => self.colours.border_style, + }), + ) + .x_axis(x_axis) + .y_axis(y_axis) + .datasets(&dataset_vector) + .render(f, draw_loc); + } + + fn draw_cpu_legend( + &self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, + ) { + let cpu_data: &[ConvertedCpuData] = &app_state.canvas_data.cpu_data; + + let num_rows = max(0, i64::from(draw_loc.height) - 5) as u64; + let start_position = get_start_position( + num_rows, + &app_state.app_scroll_positions.scroll_direction, + &mut app_state + .app_scroll_positions + .cpu_scroll_state + .previous_scroll_position, + app_state + .app_scroll_positions + .cpu_scroll_state + .current_scroll_position, + app_state.is_resized, + ); + + let sliced_cpu_data = &cpu_data[start_position as usize..]; + let mut stringified_cpu_data: Vec> = Vec::new(); + + for (itx, cpu) in sliced_cpu_data.iter().enumerate() { + if app_state.cpu_state.is_showing_tray { + stringified_cpu_data.push(vec![ + cpu.cpu_name.clone(), + if app_state.cpu_state.core_show_vec[itx + start_position as usize] { + "[*]".to_string() + } else { + "[ ]".to_string() + }, + ]); + } else if let Some(cpu_data) = cpu.cpu_data.last() { + if app_state.app_config_fields.show_disabled_data + || app_state.cpu_state.core_show_vec[itx] + { + stringified_cpu_data.push(vec![ + cpu.cpu_name.clone(), + format!("{:.0}%", cpu_data.1.round()), + ]); + } + } + } + + let cpu_rows = stringified_cpu_data + .iter() + .enumerate() + .map(|(itx, cpu_string_row)| { + Row::StyledData( + cpu_string_row.iter(), + match app_state.current_widget_selected { + WidgetPosition::Cpu => { + if itx as u64 + == app_state + .app_scroll_positions + .cpu_scroll_state + .current_scroll_position + - start_position + { + self.colours.currently_selected_text_style + } else if app_state.app_config_fields.show_average_cpu && itx == 0 { + self.colours.avg_colour_style + } else { + self.colours.cpu_colour_styles[itx + + start_position as usize + % self.colours.cpu_colour_styles.len()] + } + } + _ => { + if app_state.app_config_fields.show_average_cpu && itx == 0 { + self.colours.avg_colour_style + } else { + self.colours.cpu_colour_styles[itx + + start_position as usize + % self.colours.cpu_colour_styles.len()] + } + } + }, + ) + }); + + // Calculate widths + let width = f64::from(draw_loc.width); + let width_ratios = vec![0.5, 0.5]; + + let variable_intrinsic_results = get_variable_intrinsic_widths( + width as u16, + &width_ratios, + if app_state.cpu_state.is_showing_tray { + &CPU_SELECT_LEGEND_HEADER_LENS + } else { + &CPU_LEGEND_HEADER_LENS + }, + ); + let intrinsic_widths = &(variable_intrinsic_results.0)[0..variable_intrinsic_results.1]; + + let title = if app_state.cpu_state.is_showing_tray { + const TITLE_BASE: &str = " Esc to close "; + let repeat_num = max( + 0, + draw_loc.width as i32 - TITLE_BASE.chars().count() as i32 - 2, + ); + let result_title = format!("{} Esc to close ", "─".repeat(repeat_num as usize)); + + result_title + } else { + "".to_string() + }; + + // Draw + Table::new( + if app_state.cpu_state.is_showing_tray { + CPU_SELECT_LEGEND_HEADER + } else { + CPU_LEGEND_HEADER + } + .iter(), + cpu_rows, + ) + .block( + Block::default() + .title(&title) + .title_style(if app_state.is_expanded { + self.colours.highlighted_border_style + } else { + match app_state.current_widget_selected { + WidgetPosition::Cpu => self.colours.highlighted_border_style, + _ => self.colours.border_style, + } + }) + .borders(Borders::ALL) + .border_style(match app_state.current_widget_selected { + WidgetPosition::Cpu => self.colours.highlighted_border_style, + _ => self.colours.border_style, + }), + ) + .header_style(self.colours.table_header_style) + .widths( + &(intrinsic_widths + .iter() + .map(|calculated_width| Constraint::Length(*calculated_width as u16)) + .collect::>()), + ) + .render(f, draw_loc); + } + + fn draw_memory_graph( + &self, f: &mut Frame<'_, B>, app_state: &app::App, draw_loc: Rect, + ) { + let mem_data: &[(f64, f64)] = &app_state.canvas_data.mem_data; + let swap_data: &[(f64, f64)] = &app_state.canvas_data.swap_data; + + let x_axis: Axis<'_, String> = Axis::default().bounds([0.0, TIME_STARTS_FROM as f64]); + + // Offset as the zero value isn't drawn otherwise... + let y_axis: Axis<'_, &str> = Axis::default() + .style(self.colours.graph_style) + .labels_style(self.colours.graph_style) + .bounds([-0.5, 100.5]) + .labels(&["0%", "100%"]); + + let mem_canvas_vec: Vec> = vec![ + Dataset::default() + .name(&app_state.canvas_data.mem_label) + .marker(if app_state.app_config_fields.use_dot { + Marker::Dot + } else { + Marker::Braille + }) + .style(self.colours.ram_style) + .data(&mem_data), + Dataset::default() + .name(&app_state.canvas_data.swap_label) + .marker(if app_state.app_config_fields.use_dot { + Marker::Dot + } else { + Marker::Braille + }) + .style(self.colours.swap_style) + .data(&swap_data), + ]; + + let title = if app_state.is_expanded { + const TITLE_BASE: &str = " Memory ── Esc to go back "; + let repeat_num = max( + 0, + draw_loc.width as i32 - TITLE_BASE.chars().count() as i32 - 2, + ); + let result_title = format!( + " Memory ─{}─ Esc to go back ", + "─".repeat(repeat_num as usize) + ); + + result_title + } else { + " Memory ".to_string() + }; + + Chart::default() + .block( + Block::default() + .title(&title) + .title_style(if app_state.is_expanded { + self.colours.highlighted_border_style + } else { + self.colours.widget_title_style + }) + .borders(Borders::ALL) + .border_style(match app_state.current_widget_selected { + WidgetPosition::Mem | WidgetPosition::BasicMem => { + self.colours.highlighted_border_style + } + _ => self.colours.border_style, + }), + ) + .x_axis(x_axis) + .y_axis(y_axis) + .datasets(&mem_canvas_vec) + .render(f, draw_loc); + } + + fn draw_network_graph( + &self, f: &mut Frame<'_, B>, app_state: &app::App, draw_loc: Rect, + ) { + let network_data_rx: &[(f64, f64)] = &app_state.canvas_data.network_data_rx; + let network_data_tx: &[(f64, f64)] = &app_state.canvas_data.network_data_tx; + + let x_axis: Axis<'_, String> = Axis::default().bounds([0.0, 60_000.0]); + let y_axis: Axis<'_, &str> = Axis::default() + .style(self.colours.graph_style) + .labels_style(self.colours.graph_style) + .bounds([-0.5, 30_f64]) + .labels(&["0B", "1KiB", "1MiB", "1GiB"]); + + let title = if app_state.is_expanded { + const TITLE_BASE: &str = " Network ── Esc to go back "; + let repeat_num = max( + 0, + draw_loc.width as i32 - TITLE_BASE.chars().count() as i32 - 2, + ); + let result_title = format!( + " Network ─{}─ Esc to go back ", + "─".repeat(repeat_num as usize) + ); + + result_title + } else { + " Network ".to_string() + }; + + Chart::default() + .block( + Block::default() + .title(&title) + .title_style(if app_state.is_expanded { + self.colours.highlighted_border_style + } else { + self.colours.widget_title_style + }) + .borders(Borders::ALL) + .border_style(match app_state.current_widget_selected { + WidgetPosition::Network | WidgetPosition::BasicNet => { + self.colours.highlighted_border_style + } + _ => self.colours.border_style, + }), + ) + .x_axis(x_axis) + .y_axis(y_axis) + .datasets(&[ + Dataset::default() + .name(&format!("RX: {:7}", app_state.canvas_data.rx_display)) + .marker(if app_state.app_config_fields.use_dot { + Marker::Dot + } else { + Marker::Braille + }) + .style(self.colours.rx_style) + .data(&network_data_rx), + Dataset::default() + .name(&format!("TX: {:7}", app_state.canvas_data.tx_display)) + .marker(if app_state.app_config_fields.use_dot { + Marker::Dot + } else { + Marker::Braille + }) + .style(self.colours.tx_style) + .data(&network_data_tx), + Dataset::default() + .name(&format!( + "Total RX: {:7}", + app_state.canvas_data.total_rx_display + )) + .style(self.colours.total_rx_style), + Dataset::default() + .name(&format!( + "Total TX: {:7}", + app_state.canvas_data.total_tx_display + )) + .style(self.colours.total_tx_style), + ]) + .render(f, draw_loc); + } + + fn draw_network_labels( + &self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, + ) { + let rx_display = &app_state.canvas_data.rx_display; + let tx_display = &app_state.canvas_data.tx_display; + let total_rx_display = &app_state.canvas_data.total_rx_display; + let total_tx_display = &app_state.canvas_data.total_tx_display; + + // Gross but I need it to work... + let total_network = vec![vec![ + rx_display, + tx_display, + total_rx_display, + total_tx_display, + ]]; + let mapped_network = total_network + .iter() + .map(|val| Row::StyledData(val.iter(), self.colours.text_style)); + + // Calculate widths + let width_ratios: Vec = vec![0.25, 0.25, 0.25, 0.25]; + let lens: &[usize] = &NETWORK_HEADERS_LENS; + let width = f64::from(draw_loc.width); + + let variable_intrinsic_results = + get_variable_intrinsic_widths(width as u16, &width_ratios, lens); + let intrinsic_widths = &(variable_intrinsic_results.0)[0..variable_intrinsic_results.1]; + + // Draw + Table::new(NETWORK_HEADERS.iter(), mapped_network) + .block(Block::default().borders(Borders::ALL).border_style( + match app_state.current_widget_selected { + WidgetPosition::Network => self.colours.highlighted_border_style, + _ => self.colours.border_style, + }, + )) + .header_style(self.colours.table_header_style) + .style(self.colours.text_style) + .widths( + &(intrinsic_widths + .iter() + .map(|calculated_width| Constraint::Length(*calculated_width as u16)) + .collect::>()), + ) + .render(f, draw_loc); + } + + fn draw_temp_table( + &self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, draw_border: bool, + ) { + let temp_sensor_data: &[Vec] = &app_state.canvas_data.temp_sensor_data; + + let num_rows = max(0, i64::from(draw_loc.height) - 5) as u64; + let start_position = get_start_position( + num_rows, + &app_state.app_scroll_positions.scroll_direction, + &mut app_state + .app_scroll_positions + .temp_scroll_state + .previous_scroll_position, + app_state + .app_scroll_positions + .temp_scroll_state + .current_scroll_position, + app_state.is_resized, + ); + + let sliced_vec = &temp_sensor_data[start_position as usize..]; + let mut temp_row_counter: i64 = 0; + + let temperature_rows = sliced_vec.iter().map(|temp_row| { + Row::StyledData( + temp_row.iter(), + match app_state.current_widget_selected { + WidgetPosition::Temp => { + if temp_row_counter as u64 + == app_state + .app_scroll_positions + .temp_scroll_state + .current_scroll_position + - start_position + { + temp_row_counter = -1; + self.colours.currently_selected_text_style + } else { + if temp_row_counter >= 0 { + temp_row_counter += 1; + } + self.colours.text_style + } + } + _ => self.colours.text_style, + }, + ) + }); + + // Calculate widths + let width = f64::from(draw_loc.width); + let width_ratios = [0.5, 0.5]; + let variable_intrinsic_results = + get_variable_intrinsic_widths(width as u16, &width_ratios, &TEMP_HEADERS_LENS); + let intrinsic_widths = &(variable_intrinsic_results.0)[0..variable_intrinsic_results.1]; + + let title = if app_state.is_expanded { + const TITLE_BASE: &str = " Temperatures ── Esc to go back "; + let repeat_num = max( + 0, + draw_loc.width as i32 - TITLE_BASE.chars().count() as i32 - 2, + ); + let result_title = format!( + " Temperatures ─{}─ Esc to go back ", + "─".repeat(repeat_num as usize) + ); + + result_title + } else if app_state.app_config_fields.use_basic_mode { + String::new() + } else { + " Temperatures ".to_string() + }; + + let temp_block = if draw_border { + Block::default() + .title(&title) + .title_style(if app_state.is_expanded { + match app_state.current_widget_selected { + WidgetPosition::Temp => self.colours.highlighted_border_style, + _ => self.colours.border_style, + } + } else { + self.colours.widget_title_style + }) + .borders(Borders::ALL) + .border_style(match app_state.current_widget_selected { + WidgetPosition::Temp => self.colours.highlighted_border_style, + _ => self.colours.border_style, + }) + } else { + match app_state.current_widget_selected { + WidgetPosition::Temp => Block::default() + .borders(*SIDE_BORDERS) + .border_style(self.colours.highlighted_border_style), + _ => Block::default().borders(Borders::NONE), + } + }; + + let margined_draw_loc = Layout::default() + .constraints([Constraint::Percentage(100)].as_ref()) + .horizontal_margin(match app_state.current_widget_selected { + WidgetPosition::Temp => 0, + _ if !draw_border => 1, + _ => 0, + }) + .direction(Direction::Horizontal) + .split(draw_loc); + + // Draw + Table::new(TEMP_HEADERS.iter(), temperature_rows) + .block(temp_block) + .header_style(self.colours.table_header_style) + .widths( + &(intrinsic_widths + .iter() + .map(|calculated_width| Constraint::Length(*calculated_width as u16)) + .collect::>()), + ) + .render(f, margined_draw_loc[0]); + } + + fn draw_disk_table( + &self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, draw_border: bool, + ) { + let disk_data: &[Vec] = &app_state.canvas_data.disk_data; + let num_rows = max(0, i64::from(draw_loc.height) - 5) as u64; + let start_position = get_start_position( + num_rows, + &app_state.app_scroll_positions.scroll_direction, + &mut app_state + .app_scroll_positions + .disk_scroll_state + .previous_scroll_position, + app_state + .app_scroll_positions + .disk_scroll_state + .current_scroll_position, + app_state.is_resized, + ); + + let sliced_vec = &disk_data[start_position as usize..]; + let mut disk_counter: i64 = 0; + + let disk_rows = sliced_vec.iter().map(|disk| { + Row::StyledData( + disk.iter(), + match app_state.current_widget_selected { + WidgetPosition::Disk => { + if disk_counter as u64 + == app_state + .app_scroll_positions + .disk_scroll_state + .current_scroll_position + - start_position + { + disk_counter = -1; + self.colours.currently_selected_text_style + } else { + if disk_counter >= 0 { + disk_counter += 1; + } + self.colours.text_style + } + } + _ => self.colours.text_style, + }, + ) + }); + + // Calculate widths + // TODO: [PRETTY] Ellipsis on strings? + let width = f64::from(draw_loc.width); + let width_ratios = [0.2, 0.15, 0.13, 0.13, 0.13, 0.13, 0.13]; + let variable_intrinsic_results = + get_variable_intrinsic_widths(width as u16, &width_ratios, &DISK_HEADERS_LENS); + let intrinsic_widths = &variable_intrinsic_results.0[0..variable_intrinsic_results.1]; + + let title = if app_state.is_expanded { + const TITLE_BASE: &str = " Disk ── Esc to go back "; + let repeat_num = max( + 0, + draw_loc.width as i32 - TITLE_BASE.chars().count() as i32 - 2, + ); + let result_title = format!( + " Disk ─{}─ Esc to go back ", + "─".repeat(repeat_num as usize) + ); + result_title + } else if app_state.app_config_fields.use_basic_mode { + String::new() + } else { + " Disk ".to_string() + }; + + let disk_block = if draw_border { + Block::default() + .title(&title) + .title_style(if app_state.is_expanded { + match app_state.current_widget_selected { + WidgetPosition::Disk => self.colours.highlighted_border_style, + _ => self.colours.border_style, + } + } else { + self.colours.widget_title_style + }) + .borders(Borders::ALL) + .border_style(match app_state.current_widget_selected { + WidgetPosition::Disk => self.colours.highlighted_border_style, + _ => self.colours.border_style, + }) + } else { + match app_state.current_widget_selected { + WidgetPosition::Disk => Block::default() + .borders(*SIDE_BORDERS) + .border_style(self.colours.highlighted_border_style), + _ => Block::default().borders(Borders::NONE), + } + }; + + let margined_draw_loc = Layout::default() + .constraints([Constraint::Percentage(100)].as_ref()) + .horizontal_margin(match app_state.current_widget_selected { + WidgetPosition::Disk => 0, + _ if !draw_border => 1, + _ => 0, + }) + .direction(Direction::Horizontal) + .split(draw_loc); + + // Draw! + Table::new(DISK_HEADERS.iter(), disk_rows) + .block(disk_block) + .header_style(self.colours.table_header_style) + .widths( + &(intrinsic_widths + .iter() + .map(|calculated_width| Constraint::Length(*calculated_width as u16)) + .collect::>()), + ) + .render(f, margined_draw_loc[0]); + } + + fn draw_search_field( + &self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, draw_border: bool, + ) { + let width = max(0, draw_loc.width as i64 - 34) as u64; // TODO: [REFACTOR] Hard coding this is terrible. + let cursor_position = app_state.get_cursor_position(); + let char_cursor_position = app_state.get_char_cursor_position(); + + let start_position: usize = get_search_start_position( + width as usize, + &app_state.process_search_state.search_state.cursor_direction, + &mut app_state.process_search_state.search_state.cursor_bar, + char_cursor_position, + app_state.is_resized, + ); + + let query = app_state.get_current_search_query().as_str(); + let grapheme_indices = UnicodeSegmentation::grapheme_indices(query, true); + let mut current_grapheme_posn = 0; + let query_with_cursor: Vec> = + if let WidgetPosition::ProcessSearch = app_state.current_widget_selected { + let mut res = grapheme_indices + .filter_map(|grapheme| { + current_grapheme_posn += UnicodeWidthStr::width(grapheme.1); + + if current_grapheme_posn <= start_position { + None + } else { + let styled = if grapheme.0 == cursor_position { + Text::styled(grapheme.1, self.colours.currently_selected_text_style) + } else { + Text::styled(grapheme.1, self.colours.text_style) + }; + Some(styled) + } + }) + .collect::>(); + + if cursor_position >= query.len() { + res.push(Text::styled( + " ", + self.colours.currently_selected_text_style, + )) + } + + res + } else { + // This is easier - we just need to get a range of graphemes, rather than + // dealing with possibly inserting a cursor (as none is shown!) + grapheme_indices + .filter_map(|grapheme| { + current_grapheme_posn += UnicodeWidthStr::width(grapheme.1); + if current_grapheme_posn <= start_position { + None + } else { + let styled = Text::styled(grapheme.1, self.colours.text_style); + Some(styled) + } + }) + .collect::>() + }; + + let mut search_text = vec![if app_state.is_grouped() { + Text::styled("Search by Name: ", self.colours.table_header_style) + } else if app_state.process_search_state.is_searching_with_pid { + Text::styled( + "Search by PID (Tab for Name): ", + self.colours.table_header_style, + ) + } else { + Text::styled( + "Search by Name (Tab for PID): ", + self.colours.table_header_style, + ) + }]; + + // Text options shamelessly stolen from VS Code. + let mut option_text = vec![]; + let case_style = if !app_state.process_search_state.is_ignoring_case { + self.colours.currently_selected_text_style + } else { + self.colours.text_style + }; + + let whole_word_style = if app_state.process_search_state.is_searching_whole_word { + self.colours.currently_selected_text_style + } else { + self.colours.text_style + }; + + let regex_style = if app_state.process_search_state.is_searching_with_regex { + self.colours.currently_selected_text_style + } else { + self.colours.text_style + }; + + let case_text = format!( + "Match Case ({})[{}]", + if self.is_mac_os { "F1" } else { "Alt+C" }, + if !app_state.process_search_state.is_ignoring_case { + "*" + } else { + " " + } + ); + + let whole_text = format!( + "Match Whole Word ({})[{}]", + if self.is_mac_os { "F2" } else { "Alt+W" }, + if app_state.process_search_state.is_searching_whole_word { + "*" + } else { + " " + } + ); + + let regex_text = format!( + "Use Regex ({})[{}]", + if self.is_mac_os { "F3" } else { "Alt+R" }, + if app_state.process_search_state.is_searching_with_regex { + "*" + } else { + " " + } + ); + + let option_row = vec![ + Text::raw("\n\n"), + Text::styled(&case_text, case_style), + Text::raw(" "), + Text::styled(&whole_text, whole_word_style), + Text::raw(" "), + Text::styled(®ex_text, regex_style), + ]; + option_text.extend(option_row); + + search_text.extend(query_with_cursor); + search_text.extend(option_text); + + let current_border_style: Style = if app_state + .process_search_state + .search_state + .is_invalid_search + { + Style::default().fg(Color::Rgb(255, 0, 0)) + } else { + match app_state.current_widget_selected { + WidgetPosition::ProcessSearch => self.colours.highlighted_border_style, + _ => self.colours.border_style, + } + }; + + let title = if draw_border { + const TITLE_BASE: &str = " Esc to close "; + + let repeat_num = max( + 0, + draw_loc.width as i32 - TITLE_BASE.chars().count() as i32 - 2, + ); + format!("{} Esc to close ", "─".repeat(repeat_num as usize)) + } else { + String::new() + }; + + let process_search_block = if draw_border { + Block::default() + .title(&title) + .title_style(current_border_style) + .borders(Borders::ALL) + .border_style(current_border_style) + } else { + match app_state.current_widget_selected { + WidgetPosition::ProcessSearch => Block::default() + .borders(*SIDE_BORDERS) + .border_style(current_border_style), + _ => Block::default().borders(Borders::NONE), + } + }; + + let margined_draw_loc = Layout::default() + .constraints([Constraint::Percentage(100)].as_ref()) + .horizontal_margin(match app_state.current_widget_selected { + WidgetPosition::ProcessSearch => 0, + _ if !draw_border => 1, + _ => 0, + }) + .direction(Direction::Horizontal) + .split(draw_loc); + + Paragraph::new(search_text.iter()) + .block(process_search_block) + .style(self.colours.text_style) + .alignment(Alignment::Left) + .wrap(false) + .render(f, margined_draw_loc[0]); + } + + fn draw_processes_table( + &self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, draw_border: bool, + ) { + let process_data: &[ConvertedProcessData] = &app_state.canvas_data.finalized_process_data; + + // Admittedly this is kinda a hack... but we need to: + // * Scroll + // * Show/hide elements based on scroll position + // + // As such, we use a process_counter to know when we've + // hit the process we've currently scrolled to. + // We also need to move the list - we can + // do so by hiding some elements! + let num_rows = max(0, i64::from(draw_loc.height) - 5) as u64; + + let position = get_start_position( + num_rows, + &app_state.app_scroll_positions.scroll_direction, + &mut app_state + .app_scroll_positions + .process_scroll_state + .previous_scroll_position, + app_state + .app_scroll_positions + .process_scroll_state + .current_scroll_position, + app_state.is_resized, + ); + + // Sanity check + let start_position = if position >= process_data.len() as u64 { + std::cmp::max(0, process_data.len() as i64 - 1) as u64 + } else { + position + }; + + let sliced_vec = &process_data[start_position as usize..]; + let mut process_counter: i64 = 0; + + // Draw! + let process_rows = sliced_vec.iter().map(|process| { + let stringified_process_vec: Vec = vec![ + if app_state.is_grouped() { + process.group_pids.len().to_string() + } else { + process.pid.to_string() + }, + process.name.clone(), + format!("{:.1}%", process.cpu_usage), + format!("{:.1}%", process.mem_usage), + ]; + Row::StyledData( + stringified_process_vec.into_iter(), + match app_state.current_widget_selected { + WidgetPosition::Process => { + if process_counter as u64 + == app_state + .app_scroll_positions + .process_scroll_state + .current_scroll_position + - start_position + { + process_counter = -1; + self.colours.currently_selected_text_style + } else { + if process_counter >= 0 { + process_counter += 1; + } + self.colours.text_style + } + } + _ => self.colours.text_style, + }, + ) + }); + + use app::data_harvester::processes::ProcessSorting; + let mut pid_or_name = if app_state.is_grouped() { + "Count" + } else { + "PID(p)" + } + .to_string(); + let mut name = "Name(n)".to_string(); + let mut cpu = "CPU%(c)".to_string(); + let mut mem = "Mem%(m)".to_string(); + + let direction_val = if app_state.process_sorting_reverse { + "â–¼".to_string() + } else { + "â–²".to_string() + }; + + match app_state.process_sorting_type { + ProcessSorting::CPU => cpu += &direction_val, + ProcessSorting::MEM => mem += &direction_val, + ProcessSorting::PID => pid_or_name += &direction_val, + ProcessSorting::NAME => name += &direction_val, + }; + + let process_headers = [pid_or_name, name, cpu, mem]; + let process_headers_lens: Vec = process_headers + .iter() + .map(|entry| entry.len()) + .collect::>(); + + // Calculate widths + let width = f64::from(draw_loc.width); + let width_ratios = [0.2, 0.4, 0.2, 0.2]; + let variable_intrinsic_results = + get_variable_intrinsic_widths(width as u16, &width_ratios, &process_headers_lens); + let intrinsic_widths = &(variable_intrinsic_results.0)[0..variable_intrinsic_results.1]; + + let title = if draw_border { + if app_state.is_expanded && !app_state.process_search_state.search_state.is_enabled { + const TITLE_BASE: &str = " Processes ── Esc to go back "; + let repeat_num = max( + 0, + draw_loc.width as i32 - TITLE_BASE.chars().count() as i32 - 2, + ); + let result_title = format!( + " Processes ─{}─ Esc to go back ", + "─".repeat(repeat_num as usize) + ); + + result_title + } else { + " Processes ".to_string() + } + } else { + String::default() + }; + + let process_block = if draw_border { + Block::default() + .title(&title) + .title_style(if app_state.is_expanded { + match app_state.current_widget_selected { + WidgetPosition::Process => self.colours.highlighted_border_style, + _ => self.colours.border_style, + } + } else { + self.colours.widget_title_style + }) + .borders(Borders::ALL) + .border_style(match app_state.current_widget_selected { + WidgetPosition::Process => self.colours.highlighted_border_style, + _ => self.colours.border_style, + }) + } else { + match app_state.current_widget_selected { + WidgetPosition::Process => Block::default() + .borders(*SIDE_BORDERS) + .border_style(self.colours.highlighted_border_style), + _ => Block::default().borders(Borders::NONE), + } + }; + + let margined_draw_loc = Layout::default() + .constraints([Constraint::Percentage(100)].as_ref()) + .horizontal_margin(match app_state.current_widget_selected { + WidgetPosition::Process => 0, + _ if !draw_border => 1, + _ => 0, + }) + .direction(Direction::Horizontal) + .split(draw_loc); + + Table::new(process_headers.iter(), process_rows) + .block(process_block) + .header_style(self.colours.table_header_style) + .widths( + &(intrinsic_widths + .iter() + .map(|calculated_width| Constraint::Length(*calculated_width as u16)) + .collect::>()), + ) + .render(f, margined_draw_loc[0]); + } + + fn draw_basic_cpu( + &self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, + ) { + let cpu_data: &[ConvertedCpuData] = &app_state.canvas_data.cpu_data; + + // This is a bit complicated, but basically, we want to draw SOME number + // of columns to draw all CPUs. Ideally, as well, we want to not have + // to ever scroll. + // **General logic** - count number of elements in cpu_data. Then see how + // many rows and columns we have in draw_loc (-2 on both sides for border?). + // I think what we can do is try to fit in as many in one column as possible. + // If not, then add a new column. + // Then, from this, split the row space across ALL columns. From there, generate + // the desired lengths. + + if let WidgetPosition::BasicCpu = app_state.current_widget_selected { + Block::default() + .borders(*SIDE_BORDERS) + .border_style(self.colours.highlighted_border_style) + .render(f, draw_loc); + } + + let num_cpus = cpu_data.len(); + if draw_loc.height > 0 { + let remaining_height = draw_loc.height as usize; + const REQUIRED_COLUMNS: usize = 4; + + let chunk_vec = + vec![Constraint::Percentage((100 / REQUIRED_COLUMNS) as u16); REQUIRED_COLUMNS]; + let chunks = Layout::default() + .constraints(chunk_vec.as_ref()) + .direction(Direction::Horizontal) + .split(draw_loc); + + // +9 due to 3 + 4 + 2 columns for the name & space + percentage + bar bounds + let margin_space = 2; + let remaining_width = max( + 0, + draw_loc.width as i64 + - ((9 + margin_space) * REQUIRED_COLUMNS - margin_space) as i64, + ) as usize; + + let bar_length = remaining_width / REQUIRED_COLUMNS; + + // CPU (and RAM) percent bars are, uh, "heavily" inspired from htop. + let cpu_bars = (0..num_cpus) + .map(|cpu_index| { + let use_percentage = + if let Some(cpu_usage) = cpu_data[cpu_index].cpu_data.last() { + cpu_usage.1 + } else { + 0.0 + }; + + let num_bars = calculate_basic_use_bars(use_percentage, bar_length); + format!( + "{:3}[{}{}{:3.0}%]\n", + if app_state.app_config_fields.show_average_cpu { + if cpu_index == 0 { + "AVG".to_string() + } else { + (cpu_index - 1).to_string() + } + } else { + cpu_index.to_string() + }, + "|".repeat(num_bars), + " ".repeat(bar_length - num_bars), + use_percentage.round(), + ) + }) + .collect::>(); + + let mut row_counter = num_cpus; + let mut start_index = 0; + for (itx, chunk) in chunks.iter().enumerate() { + let to_divide = REQUIRED_COLUMNS - itx; + let how_many_cpus = min( + remaining_height, + (row_counter / to_divide) + (if row_counter % to_divide == 0 { 0 } else { 1 }), + ); + row_counter -= how_many_cpus; + let end_index = min(start_index + how_many_cpus, num_cpus); + let cpu_column: Vec> = (start_index..end_index) + .map(|cpu_index| { + Text::Styled( + (&cpu_bars[cpu_index]).into(), + self.colours.cpu_colour_styles + [cpu_index as usize % self.colours.cpu_colour_styles.len()], + ) + }) + .collect::>(); + + start_index += how_many_cpus; + + let margined_loc = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(100)].as_ref()) + .horizontal_margin(1) + .split(*chunk); + + Paragraph::new(cpu_column.iter()) + .block(Block::default()) + .render(f, margined_loc[0]); + } + } + } + + fn draw_basic_memory( + &self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, + ) { + let mem_data: &[(f64, f64)] = &app_state.canvas_data.mem_data; + let swap_data: &[(f64, f64)] = &app_state.canvas_data.swap_data; + + let margined_loc = Layout::default() + .constraints([Constraint::Percentage(100)].as_ref()) + .horizontal_margin(1) + .split(draw_loc); + + if let WidgetPosition::BasicMem = app_state.current_widget_selected { + Block::default() + .borders(*SIDE_BORDERS) + .border_style(self.colours.highlighted_border_style) + .render(f, draw_loc); + } + + // +9 due to 3 + 4 + 2 + 2 columns for the name & space + percentage + bar bounds + margin spacing + let bar_length = max(0, draw_loc.width as i64 - 11) as usize; + let ram_use_percentage = if let Some(mem) = mem_data.last() { + mem.1 + } else { + 0.0 + }; + let swap_use_percentage = if let Some(swap) = swap_data.last() { + swap.1 + } else { + 0.0 + }; + let num_bars_ram = calculate_basic_use_bars(ram_use_percentage, bar_length); + let num_bars_swap = calculate_basic_use_bars(swap_use_percentage, bar_length); + let mem_label = format!( + "RAM[{}{}{:3.0}%]\n", + "|".repeat(num_bars_ram), + " ".repeat(bar_length - num_bars_ram), + ram_use_percentage.round(), + ); + let swap_label = format!( + "SWP[{}{}{:3.0}%]", + "|".repeat(num_bars_swap), + " ".repeat(bar_length - num_bars_swap), + swap_use_percentage.round(), + ); + + let mem_text: Vec> = vec![ + Text::Styled(mem_label.into(), self.colours.ram_style), + Text::Styled(swap_label.into(), self.colours.swap_style), + ]; + + Paragraph::new(mem_text.iter()) + .block(Block::default()) + .render(f, margined_loc[0]); + } + + fn draw_basic_network( + &self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, + ) { + let divided_loc = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .split(draw_loc); + + let net_loc = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(100)].as_ref()) + .horizontal_margin(1) + .split(divided_loc[0]); + + let total_loc = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(100)].as_ref()) + .horizontal_margin(1) + .split(divided_loc[1]); + + if let WidgetPosition::BasicNet = app_state.current_widget_selected { + Block::default() + .borders(*SIDE_BORDERS) + .border_style(self.colours.highlighted_border_style) + .render(f, draw_loc); + } + + let rx_label = format!("RX: {}\n", &app_state.canvas_data.rx_display); + let tx_label = format!("TX: {}", &app_state.canvas_data.tx_display); + let total_rx_label = format!("Total RX: {}\n", &app_state.canvas_data.total_rx_display); + let total_tx_label = format!("Total TX: {}", &app_state.canvas_data.total_tx_display); + + let net_text = vec![ + Text::Styled(rx_label.into(), self.colours.rx_style), + Text::Styled(tx_label.into(), self.colours.tx_style), + ]; + + let total_net_text = vec![ + Text::Styled(total_rx_label.into(), self.colours.total_rx_style), + Text::Styled(total_tx_label.into(), self.colours.total_tx_style), + ]; + + Paragraph::new(net_text.iter()) + .block(Block::default()) + .render(f, net_loc[0]); + + Paragraph::new(total_net_text.iter()) + .block(Block::default()) + .render(f, total_loc[0]); + } + + fn draw_basic_table_arrows( + &self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, + ) { + // Effectively a paragraph with a ton of spacing + + // TODO: [MODULARITY] This is hard coded. Gross. + let (left_table, right_table) = if app_state.current_widget_selected.is_widget_table() { + match app_state.current_widget_selected { + WidgetPosition::Process | WidgetPosition::ProcessSearch => { + (WidgetPosition::Temp, WidgetPosition::Disk) + } + WidgetPosition::Disk => (WidgetPosition::Process, WidgetPosition::Temp), + WidgetPosition::Temp => (WidgetPosition::Disk, WidgetPosition::Process), + _ => (WidgetPosition::Disk, WidgetPosition::Temp), + } + } else { + match app_state.previous_basic_table_selected { + WidgetPosition::Process | WidgetPosition::ProcessSearch => { + (WidgetPosition::Temp, WidgetPosition::Disk) + } + WidgetPosition::Disk => (WidgetPosition::Process, WidgetPosition::Temp), + WidgetPosition::Temp => (WidgetPosition::Disk, WidgetPosition::Process), + _ => (WidgetPosition::Disk, WidgetPosition::Temp), + } + }; + + let left_name = left_table.get_pretty_name(); + let right_name = right_table.get_pretty_name(); + + let num_spaces = max( + 0, + draw_loc.width as i64 - 2 - 4 - (left_name.len() + right_name.len()) as i64, + ) as usize; + + let arrow_text = vec![ + Text::Styled( + format!("\nâ—„ {}", right_name).into(), + self.colours.text_style, + ), + Text::Raw(" ".repeat(num_spaces).into()), + Text::Styled(format!("{} â–º", left_name).into(), self.colours.text_style), + ]; + + let margined_draw_loc = Layout::default() + .constraints([Constraint::Percentage(100)].as_ref()) + .horizontal_margin(1) + .split(draw_loc); + + Paragraph::new(arrow_text.iter()) + .block(Block::default()) + .render(f, margined_draw_loc[0]); + } } diff --git a/src/canvas/canvas_colours.rs b/src/canvas/canvas_colours.rs index 27cc228f..63c1be5e 100644 --- a/src/canvas/canvas_colours.rs +++ b/src/canvas/canvas_colours.rs @@ -7,147 +7,147 @@ use crate::{constants::*, utils::error}; mod colour_utils; pub struct CanvasColours { - pub currently_selected_text_colour: Color, - pub currently_selected_bg_colour: Color, - pub currently_selected_text_style: Style, - pub table_header_style: Style, - pub ram_style: Style, - pub swap_style: Style, - pub rx_style: Style, - pub tx_style: Style, - pub rx_total_style: Style, - pub tx_total_style: Style, - pub avg_colour_style: Style, - pub cpu_colour_styles: Vec