diff options
27 files changed, 4811 insertions, 2393 deletions
diff --git a/.travis.yml b/.travis.yml index fed4dac1..38288797 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ os: jobs: allow_failures: - rust: nightly - - env: TARGET=x86_64-pc-windows-gnu # Seems to cause problems. TODO: Add test for it, but keep allow fail. + - env: TARGET=x86_64-pc-windows-gnu # Seems to cause problems. fast_finish: true branches: only: @@ -31,7 +31,7 @@ fern = "0.6.0" futures = "0.3.4" heim = "0.0.10" log = "0.4.8" -regex = "1.3.4" +regex = "1.3" sysinfo = "0.11" toml = "0.5.6" tui = {version = "0.8", features = ["crossterm"], default-features = false } diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 00000000..59155b42 --- /dev/null +++ b/clippy.toml @@ -0,0 +1 @@ +cognitive-complexity-threshold = 35
\ No newline at end of file diff --git a/docs/config.md b/docs/config.md index e18db307..d625739a 100644 --- a/docs/config.md +++ b/docs/config.md @@ -8,7 +8,6 @@ One use of a config file is to set boot flags to execute without having to state - These options are generally the same as the long names as other flags (ex: `case_sensitive = true`). - Note that if a flag and an option conflict, the flag has higher precedence (ex: if the `-c` and `temperature_type = kelvin` both exist, the Celsius temperature type is ultimately chosen). - For temperature type, use `temperature_type = "kelvin|k|celsius|c|fahrenheit|f"`. -- For default widgets, use `default_widget = "cpu_default|memory_default|disk_default|temperature_default|network_default|process_default"`. ## Colours @@ -36,6 +35,45 @@ Supported named colours are one of the following: `Reset, Black, Red, Green, Yel Note some colours may not be compatible with the terminal you are using. For example, macOS's default Terminal does not play nice with many colours. +## Layout + +As of 0.3.0, bottom supports custom layouts. Layouts are in the TOML specification, and are arranged by row -> column -> row. For example, the default layout: + +```toml +[[row]] + ratio=30 + [[row.child]] + type="cpu" +[[row]] + ratio=40 + [[row.child]] + ratio=4 + type="mem" + [[row.child]] + ratio=3 + [[row.child.child]] + type="temp" + [[row.child.child]] + type="disk" +[[row]] + ratio=30 + [[row.child]] + type="net" + [[row.child]] + type="proc" + default=true +``` + +Valid types are: + +- `cpu` +- `mem` +- `proc` +- `net` +- `temp` +- `disk` +- `empty` + ## Default config locations bottom will check specific locations by default for a config file. If no file is found, it will be created. @@ -1,5 +1,4 @@ -use std::cmp::max; -use std::time::Instant; +use std::{cmp::max, collections::HashMap, time::Instant}; use unicode_segmentation::GraphemeCursor; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; @@ -8,64 +7,20 @@ use typed_builder::*; use data_farmer::*; use data_harvester::{processes, temperature}; +use layout_manager::*; -use crate::{canvas, constants, utils::error::Result}; +use crate::{ + canvas, constants, + utils::error::{BottomError, Result}, +}; pub mod data_farmer; pub mod data_harvester; +pub mod layout_manager; mod process_killer; const MAX_SEARCH_LENGTH: usize = 200; -#[derive(Debug, Clone, Copy)] -pub enum WidgetPosition { - Cpu, - CpuLegend, - Mem, - Disk, - Temp, - Network, - NetworkLegend, - Process, - ProcessSearch, - BasicCpu, - BasicMem, - BasicNet, -} - -impl WidgetPosition { - pub fn is_widget_table(self) -> bool { - match self { - WidgetPosition::Disk - | WidgetPosition::Process - | WidgetPosition::ProcessSearch - | WidgetPosition::Temp - | WidgetPosition::CpuLegend => true, - _ => false, - } - } - - pub fn is_widget_graph(self) -> bool { - match self { - WidgetPosition::Cpu | WidgetPosition::Network | WidgetPosition::Mem => true, - _ => false, - } - } - - pub fn get_pretty_name(self) -> String { - use WidgetPosition::*; - match self { - Cpu | BasicCpu | CpuLegend => "CPU", - Mem | BasicMem => "Memory", - Disk => "Disks", - Temp => "Temperature", - Network | BasicNet | NetworkLegend => "Network", - Process | ProcessSearch => "Processes", - } - .to_string() - } -} - #[derive(Debug)] pub enum ScrollDirection { // UP means scrolling up --- this usually DECREMENTS @@ -74,6 +29,12 @@ pub enum ScrollDirection { DOWN, } +impl Default for ScrollDirection { + fn default() -> Self { + ScrollDirection::DOWN + } +} + #[derive(Debug)] pub enum CursorDirection { LEFT, @@ -85,28 +46,52 @@ pub enum CursorDirection { pub struct AppScrollWidgetState { pub current_scroll_position: u64, pub previous_scroll_position: u64, + pub scroll_direction: ScrollDirection, } -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, +#[derive(Default)] +pub struct AppDeleteDialogState { + pub is_showing_dd: bool, + pub is_on_yes: bool, // Defaults to "No" } -impl Default for AppScrollState { +pub enum AppHelpCategory { + General, + Process, + Search, +} + +pub struct AppHelpDialogState { + pub is_showing_help: bool, + pub current_category: AppHelpCategory, +} + +impl Default for AppHelpDialogState { 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(), + 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. +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 use_basic_mode: bool, + pub default_time_value: u64, + pub time_interval: u64, + pub hide_time: bool, + pub autohide_time: bool, +} + /// AppSearchState deals with generic searching (I might do this in the future). pub struct AppSearchState { pub is_enabled: bool, @@ -186,138 +171,299 @@ impl ProcessSearchState { } } -#[derive(Default)] -pub struct AppDeleteDialogState { - pub is_showing_dd: bool, - pub is_on_yes: bool, // Defaults to "No" +pub struct ProcWidgetState { + pub process_search_state: ProcessSearchState, + pub is_grouped: bool, + pub scroll_state: AppScrollWidgetState, + pub process_sorting_type: processes::ProcessSorting, + pub process_sorting_reverse: bool, } -pub enum AppHelpCategory { - General, - Process, - Search, -} +impl ProcWidgetState { + pub fn init( + is_case_sensitive: bool, is_match_whole_word: bool, is_use_regex: bool, is_grouped: bool, + ) -> Self { + let mut process_search_state = ProcessSearchState::default(); + if is_case_sensitive { + // By default it's off + process_search_state.search_toggle_ignore_case(); + } + if is_match_whole_word { + process_search_state.search_toggle_whole_word(); + } + if is_use_regex { + process_search_state.search_toggle_regex(); + } -pub struct AppHelpDialogState { - pub is_showing_help: bool, - pub current_category: AppHelpCategory, -} + ProcWidgetState { + process_search_state, + is_grouped, + scroll_state: AppScrollWidgetState::default(), + process_sorting_type: processes::ProcessSorting::CPU, + process_sorting_reverse: true, + } + } -impl Default for AppHelpDialogState { - fn default() -> Self { - AppHelpDialogState { - is_showing_help: false, - current_category: AppHelpCategory::General, + 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 + } + + pub fn is_search_enabled(&self) -> bool { + self.process_search_state.search_state.is_enabled + } + + pub fn get_current_search_query(&self) -> &String { + &self.process_search_state.search_state.current_search_query + } + + pub fn update_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.scroll_state.previous_scroll_position = 0; + self.scroll_state.current_scroll_position = 0; + } + + pub fn clear_search(&mut self) { + self.process_search_state.search_state.reset(); + } + + pub fn search_walk_forward(&mut self, start_position: usize) { + self.process_search_state + .search_state + .grapheme_cursor + .next_boundary( + &self.process_search_state.search_state.current_search_query[start_position..], + start_position, + ) + .unwrap(); + } + + pub fn search_walk_back(&mut self, start_position: usize) { + self.process_search_state + .search_state + .grapheme_cursor + .prev_boundary( + &self.process_search_state.search_state.current_search_query[..start_position], + 0, + ) + .unwrap(); } } -/// AppConfigFields is meant to cover basic fields that would normally be set -/// by config files or launch options. -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 use_basic_mode: bool, - pub default_time_value: u64, - pub time_interval: u64, - pub hide_time: bool, - pub autohide_time: bool, +pub struct ProcState { + pub widget_states: HashMap<u64, ProcWidgetState>, + pub force_update: Option<u64>, + pub force_update_all: bool, } -/// Network specific -pub struct NetState { - pub is_showing_tray: bool, - pub is_showing_rx: bool, - pub is_showing_tx: bool, - pub zoom_level: f64, +impl ProcState { + pub fn init(widget_states: HashMap<u64, ProcWidgetState>) -> Self { + ProcState { + widget_states, + force_update: None, + force_update_all: false, + } + } +} + +pub struct NetWidgetState { pub current_display_time: u64, - pub force_update: bool, pub autohide_timer: Option<Instant>, } -impl NetState { +impl NetWidgetState { pub fn init(current_display_time: u64, autohide_timer: Option<Instant>) -> Self { - NetState { - is_showing_tray: false, - is_showing_rx: true, - is_showing_tx: true, - zoom_level: 100.0, + NetWidgetState { current_display_time, - force_update: false, autohide_timer, } } } -/// CPU specific -pub struct CpuState { +pub struct NetState { + pub force_update: Option<u64>, + pub widget_states: HashMap<u64, NetWidgetState>, +} + +impl NetState { + pub fn init(widget_states: HashMap<u64, NetWidgetState>) -> Self { + NetState { + force_update: None, + widget_states, + } + } +} + +pub struct CpuWidgetState { + pub current_display_time: u64, + pub is_legend_hidden: bool, pub is_showing_tray: bool, - pub zoom_level: f64, pub core_show_vec: Vec<bool>, - pub num_cpus_shown: u64, - pub current_display_time: u64, - pub force_update: bool, + pub num_cpus_shown: usize, pub autohide_timer: Option<Instant>, + pub scroll_state: AppScrollWidgetState, } -impl CpuState { +impl CpuWidgetState { pub fn init(current_display_time: u64, autohide_timer: Option<Instant>) -> Self { - CpuState { + CpuWidgetState { + current_display_time, + is_legend_hidden: false, is_showing_tray: false, - zoom_level: 100.0, core_show_vec: Vec::new(), num_cpus_shown: 0, - current_display_time, - force_update: false, autohide_timer, + scroll_state: AppScrollWidgetState::default(), } } } -/// 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 struct CpuState { + pub force_update: Option<u64>, + pub widget_states: HashMap<u64, CpuWidgetState>, + pub num_cpus_total: usize, +} + +impl CpuState { + pub fn init(widget_states: HashMap<u64, CpuWidgetState>) -> Self { + CpuState { + force_update: None, + widget_states, + num_cpus_total: 0, + } + } +} + +pub struct MemWidgetState { pub current_display_time: u64, - pub force_update: bool, pub autohide_timer: Option<Instant>, } -impl MemState { +impl MemWidgetState { pub fn init(current_display_time: u64, autohide_timer: Option<Instant>) -> Self { - MemState { - is_showing_tray: false, - is_showing_ram: true, - is_showing_swap: true, - zoom_level: 100.0, + MemWidgetState { current_display_time, - force_update: false, autohide_timer, } } } -#[derive(TypedBuilder)] -pub struct App { - #[builder(default=processes::ProcessSorting::CPU, setter(skip))] - pub process_sorting_type: processes::ProcessSorting, +pub struct MemState { + pub force_update: Option<u64>, + pub widget_states: HashMap<u64, MemWidgetState>, +} - #[builder(default = true, setter(skip))] - pub process_sorting_reverse: bool, +impl MemState { + pub fn init(widget_states: HashMap<u64, MemWidgetState>) -> Self { + MemState { + force_update: None, + widget_states, + } + } +} - #[builder(default = false, setter(skip))] - pub force_update_processes: bool, +pub struct TempWidgetState { + pub scroll_state: AppScrollWidgetState, +} - #[builder(default, setter(skip))] - pub app_scroll_positions: AppScrollState, +impl TempWidgetState { + pub fn init() -> Self { + TempWidgetState { + scroll_state: AppScrollWidgetState::default(), + } + } +} + +pub struct TempState { + pub widget_states: HashMap<u64, TempWidgetState>, +} +impl TempState { + pub fn init(widget_states: HashMap<u64, TempWidgetState>) -> Self { + TempState { widget_states } + } +} + +pub struct DiskWidgetState { + pub scroll_state: AppScrollWidgetState, +} + +impl DiskWidgetState { + pub fn init() -> Self { + DiskWidgetState { + scroll_state: AppScrollWidgetState::default(), + } + } +} + +pub struct DiskState { + pub widget_states: HashMap<u64, DiskWidgetState>, +} + +impl DiskState { + pub fn init(widget_states: HashMap<u64, DiskWidgetState>) -> Self { + DiskState { widget_states } + } +} + +pub struct BasicTableWidgetState { + // Since this is intended (currently) to only be used for ONE widget, that's + // how it's going to be written. If we want to allow for multiple of these, + // then we can expand outwards with a normal BasicTableState and a hashmap + pub currently_displayed_widget_type: BottomWidgetType, + pub currently_displayed_widget_id: u64, + pub widget_id: i64, +} + +#[derive(TypedBuilder)] +pub struct App { #[builder(default = false, setter(skip))] awaiting_second_char: bool, @@ -339,16 +485,10 @@ pub struct App { #[builder(default, setter(skip))] pub canvas_data: canvas::DisplayableData, - #[builder(default = false)] - enable_grouping: bool, - #[builder(default, setter(skip))] pub data_collection: DataCollection, #[builder(default, setter(skip))] - pub process_search_state: ProcessSearchState, - - #[builder(default, setter(skip))] pub delete_dialog_state: AppDeleteDialogState, #[builder(default, setter(skip))] @@ -363,10 +503,15 @@ pub struct App { pub cpu_state: CpuState, pub mem_state: MemState, pub net_state: NetState, + pub proc_state: ProcState, + pub temp_state: TempState, + pub disk_state: DiskState, + + pub basic_table_widget_state: Option<BasicTableWidgetState>, pub app_config_fields: AppConfigFields, - pub current_widget_selected: WidgetPosition, - pub previous_basic_table_selected: WidgetPosition, + pub widget_map: HashMap<u64, BottomWidget>, + pub current_widget: BottomWidget, } impl App { @@ -378,9 +523,22 @@ impl App { self.help_dialog_state.is_showing_help = false; self.delete_dialog_state.is_showing_dd = false; - // Close search and reset it - self.process_search_state.search_state.reset(); - self.force_update_processes = true; + // Close all searches and reset it + self.proc_state + .widget_states + .values_mut() + .for_each(|state| { + state.process_search_state.search_state.reset(); + }); + self.proc_state.force_update_all = true; + + // Reset all CPU filter states + self.cpu_state.widget_states.values_mut().for_each(|state| { + for show_vec_state in &mut state.core_show_vec { + *show_vec_state = true; + } + state.num_cpus_shown = state.core_show_vec.len(); + }); // Clear current delete list self.to_delete_process_list = None; @@ -408,59 +566,136 @@ impl App { self.to_delete_process_list = None; self.dd_err = None; } else if self.is_filtering_or_searching() { - match self.current_widget_selected { - WidgetPosition::Cpu | WidgetPosition::CpuLegend => { - self.cpu_state.is_showing_tray = false; - if self - .app_scroll_positions - .cpu_scroll_state - .current_scroll_position - >= self.cpu_state.num_cpus_shown + match self.current_widget.widget_type { + BottomWidgetType::Cpu => { + if let Some(cpu_widget_state) = self + .cpu_state + .widget_states + .get_mut(&self.current_widget.widget_id) { - let new_position = max(0, self.cpu_state.num_cpus_shown as i64 - 1) as u64; - self.app_scroll_positions - .cpu_scroll_state - .current_scroll_position = new_position; - self.app_scroll_positions - .cpu_scroll_state - .previous_scroll_position = 0; + cpu_widget_state.is_showing_tray = false; + if cpu_widget_state.scroll_state.current_scroll_position + >= cpu_widget_state.num_cpus_shown as u64 + { + let new_position = + max(0, cpu_widget_state.num_cpus_shown as i64 - 1) as u64; + cpu_widget_state.scroll_state.current_scroll_position = new_position; + cpu_widget_state.scroll_state.previous_scroll_position = 0; + } } } - 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; + BottomWidgetType::CpuLegend => { + if let Some(cpu_widget_state) = self + .cpu_state + .widget_states + .get_mut(&(self.current_widget.widget_id - 1)) + { + cpu_widget_state.is_showing_tray = false; + if cpu_widget_state.scroll_state.current_scroll_position + >= cpu_widget_state.num_cpus_shown as u64 + { + let new_position = + max(0, cpu_widget_state.num_cpus_shown as i64 - 1) as u64; + cpu_widget_state.scroll_state.current_scroll_position = new_position; + cpu_widget_state.scroll_state.previous_scroll_position = 0; + } } } - WidgetPosition::Mem => { - self.mem_state.is_showing_tray = false; + BottomWidgetType::Proc => { + if let Some(current_proc_state) = self + .proc_state + .widget_states + .get_mut(&self.current_widget.widget_id) + { + if current_proc_state.is_search_enabled() { + current_proc_state + .process_search_state + .search_state + .is_enabled = false; + } + } } - WidgetPosition::Network => { - self.net_state.is_showing_tray = false; + BottomWidgetType::ProcSearch => { + if let Some(current_proc_state) = self + .proc_state + .widget_states + .get_mut(&(self.current_widget.widget_id - 1)) + { + if current_proc_state.is_search_enabled() { + current_proc_state + .process_search_state + .search_state + .is_enabled = false; + self.move_widget_selection_up(); + } + } } _ => {} } } else if self.is_expanded { self.is_expanded = false; self.is_resized = true; - if self.app_config_fields.use_basic_mode { - self.current_widget_selected = match self.current_widget_selected { - WidgetPosition::Cpu | WidgetPosition::CpuLegend => WidgetPosition::BasicCpu, - WidgetPosition::Mem => WidgetPosition::BasicMem, - WidgetPosition::Network => WidgetPosition::BasicNet, - _ => self.current_widget_selected, - } - } } } + pub fn is_in_search_widget(&self) -> bool { + matches!( + self.current_widget.widget_type, + BottomWidgetType::ProcSearch + ) + } + fn is_filtering_or_searching(&self) -> bool { - match self.current_widget_selected { - WidgetPosition::Cpu | WidgetPosition::CpuLegend => 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 + match self.current_widget.widget_type { + BottomWidgetType::Cpu => { + if let Some(cpu_widget_state) = self + .cpu_state + .widget_states + .get(&self.current_widget.widget_id) + { + cpu_widget_state.is_showing_tray + } else { + false + } + } + BottomWidgetType::CpuLegend => { + if let Some(cpu_widget_state) = self + .cpu_state + .widget_states + .get(&(self.current_widget.widget_id - 1)) + { + cpu_widget_state.is_showing_tray + } else { + false + } + } + BottomWidgetType::Proc => { + if let Some(proc_widget_state) = self + .proc_state + .widget_states + .get(&self.current_widget.widget_id) + { + proc_widget_state + .process_search_state + .search_state + .is_enabled + } else { + false + } + } + BottomWidgetType::ProcSearch => { + if let Some(proc_widget_state) = self + .proc_state + .widget_states + .get(&(self.current_widget.widget_id - 1)) + { + proc_widget_state + .process_search_state + .search_state + .is_enabled + } else { + false + } } _ => false, } @@ -475,208 +710,210 @@ impl App { self.help_dialog_state.is_showing_help || self.delete_dialog_state.is_showing_dd } - pub fn toggle_grouping(&mut self) { + pub fn on_tab(&mut self) { // Disallow usage whilst in a dialog and only in processes - if !self.is_in_dialog() { |