From 57e87d88d09e6282770a2315977fe43ef52958b4 Mon Sep 17 00:00:00 2001 From: Clement Tsang <34804052+ClementTsang@users.noreply.github.com> Date: Mon, 28 Sep 2020 19:50:21 -0400 Subject: [PATCH] feature: Add persistent search settings (#257) Adds persistent search settings across runs, by saving to the config file. Each process widget keeps track of it's *own* behaviour. The previous flags/options are now for *global* behaviour. The following new behaviour is: - Relevant flags: `--case_sensitive`, `--whole_word`, and `--regex`, will *override* the current widget's default behaviour. - Relevant options: `case_sensitive`, `whole_word`, and `regex`, will also *override* the current widget's default behaviour. As per before, if you set, say, `--case_sensitive`and `case_sensitive=true`, the flag always overrides. Documentation updates will be done in #248. --- .travis.yml | 1 + .vscode/settings.json | 210 +++++++++++++++++++++--------------------- Cargo.toml | 1 - src/app.rs | 141 ++++++++++++++++++++++++++-- src/bin/main.rs | 4 +- src/constants.rs | 7 +- src/options.rs | 150 +++++++++++++++++++++++++++++- 7 files changed, 391 insertions(+), 123 deletions(-) diff --git a/.travis.yml b/.travis.yml index a0c714ae..310e387d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -113,6 +113,7 @@ script: cargo test --verbose --target $TARGET fi +# FIXME: [TRAVIS] Probably want to update this with the new build targets and all. before_deploy: - | echo "Test whether installing works. This is mostly just a sanity check."; diff --git a/.vscode/settings.json b/.vscode/settings.json index 65c086b8..da6cd6b9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,106 +1,106 @@ { - "cSpell.words": [ - "Artem", - "COPR", - "DWORD", - "Deque", - "EINVAL", - "EPERM", - "ESRCH", - "GIBI", - "GIBIBYTE", - "GIGA", - "KIBI", - "MEBI", - "MEBIBYTE", - "MSRV", - "Mahmoud", - "Marcin", - "Mousebindings", - "Nonexhaustive", - "PKGBUILD", - "PKGBUILDs", - "Polishchuk", - "Qudsi", - "SIGTERM", - "TEBI", - "TERA", - "Tebibytes", - "Toolset", - "Ungrouped", - "WASD", - "Wojnarowski", - "aarch", - "andys", - "armhf", - "armv", - "atim", - "autohide", - "choco", - "cmdline", - "commandline", - "concat", - "crossterm", - "curr", - "czvf", - "denylist", - "fedoracentos", - "fpath", - "fract", - "gnueabihf", - "gotop", - "gotop's", - "gtop", - "haase", - "heim", - "hjkl", - "htop", - "indexmap", - "keybinds", - "libc", - "markdownlint", - "memb", - "minwindef", - "musl", - "n'th", - "noheader", - "ntdef", - "nuget", - "nvme", - "paren", - "pids", - "pmem", - "ppid", - "prepush", - "processthreadsapi", - "regexes", - "rsplitn", - "rustfmt", - "shilangyu", - "softirq", - "splitn", - "statm", - "stime", - "subwidget", - "sysconf", - "sysinfo", - "tebibyte", - "tokei", - "twrite", - "usage", - "use", - "use curr usage", - "utime", - "virt", - "vsize", - "whitespaces", - "wifi", - "winapi", - "winget", - "winnt", - "wixtoolset", - "xargs", - "xzvf", - "ytop" - ] -} \ No newline at end of file + "cSpell.words": [ + "Artem", + "COPR", + "DWORD", + "Deque", + "EINVAL", + "EPERM", + "ESRCH", + "GIBI", + "GIBIBYTE", + "GIGA", + "KIBI", + "MEBI", + "MEBIBYTE", + "MSRV", + "Mahmoud", + "Marcin", + "Mousebindings", + "Nonexhaustive", + "PKGBUILD", + "PKGBUILDs", + "Polishchuk", + "Qudsi", + "SIGTERM", + "TEBI", + "TERA", + "Tebibytes", + "Toolset", + "Ungrouped", + "WASD", + "Wojnarowski", + "aarch", + "andys", + "armhf", + "armv", + "atim", + "autohide", + "choco", + "cmdline", + "commandline", + "concat", + "crossterm", + "curr", + "czvf", + "denylist", + "fedoracentos", + "fpath", + "fract", + "gnueabihf", + "gotop", + "gotop's", + "gtop", + "haase", + "heim", + "hjkl", + "htop", + "indexmap", + "keybinds", + "libc", + "markdownlint", + "memb", + "minwindef", + "musl", + "n'th", + "noheader", + "ntdef", + "nuget", + "nvme", + "paren", + "pids", + "pmem", + "ppid", + "prepush", + "processthreadsapi", + "regexes", + "rsplitn", + "rustfmt", + "shilangyu", + "softirq", + "splitn", + "statm", + "stime", + "subwidget", + "sysconf", + "sysinfo", + "tebibyte", + "tokei", + "twrite", + "usage", + "use", + "use curr usage", + "utime", + "virt", + "vsize", + "whitespaces", + "wifi", + "winapi", + "winget", + "winnt", + "wixtoolset", + "xargs", + "xzvf", + "ytop" + ] +} diff --git a/Cargo.toml b/Cargo.toml index e46ef7b0..7014597e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,6 @@ sysinfo = "0.15.1" thiserror = "1.0.20" toml = "0.5.6" tui = {version = "0.12.0", features = ["crossterm"], default-features = false } -# tui = {version = "0.11.0", features = ["crossterm"], default-features = false, path="../tui-rs" } typed-builder = "0.7.0" unicode-segmentation = "1.6.0" unicode-width = "0.1" diff --git a/src/app.rs b/src/app.rs index 3eda03ad..bad2803e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -13,6 +13,8 @@ pub use states::*; use crate::{ canvas, constants, options::Config, + options::ConfigFlags, + options::WidgetIdEnabled, utils::error::{BottomError, Result}, Pid, }; @@ -105,6 +107,9 @@ pub struct App { #[builder(default = false, setter(skip))] pub is_config_open: bool, + #[builder(default = false, setter(skip))] + pub did_config_fail_to_save: bool, + pub cpu_state: CpuState, pub mem_state: MemState, pub net_state: NetState, @@ -179,7 +184,7 @@ impl App { self.is_force_redraw = true; } else if self.is_config_open { - self.close_config(); + self.close_config_screen(); } else { match self.current_widget.widget_type { BottomWidgetType::Proc => { @@ -454,6 +459,7 @@ impl App { pub fn toggle_ignore_case(&mut self) { let is_in_search_widget = self.is_in_search_widget(); + let mut is_case_sensitive: Option = None; if let Some(proc_widget_state) = self .proc_state .widget_states @@ -466,13 +472,49 @@ impl App { proc_widget_state.update_query(); self.proc_state.force_update = Some(self.current_widget.widget_id - 1); - // Also toggle it in the config file. + // Remember, it's the opposite (ignoring case is case "in"sensitive) + is_case_sensitive = Some(!proc_widget_state.process_search_state.is_ignoring_case); } } + + // Also toggle it in the config file if we actually changed it. + if let Some(is_ignoring_case) = is_case_sensitive { + if let Some(flags) = &mut self.config.flags { + if let Some(map) = &mut flags.search_case_enabled_widgets_map { + // Just update the map. + let mapping = map.entry(self.current_widget.widget_id - 1).or_default(); + *mapping = is_ignoring_case; + + flags.search_case_enabled_widgets = + Some(WidgetIdEnabled::create_from_hashmap(&map)); + } else { + // Map doesn't exist yet... initialize ourselves. + let mut map = HashMap::default(); + map.insert(self.current_widget.widget_id - 1, is_ignoring_case); + flags.search_case_enabled_widgets = + Some(WidgetIdEnabled::create_from_hashmap(&map)); + flags.search_case_enabled_widgets_map = Some(map); + } + } else { + // Must initialize it ourselves... + let mut map = HashMap::default(); + map.insert(self.current_widget.widget_id - 1, is_ignoring_case); + + self.config.flags = Some( + ConfigFlags::builder() + .search_case_enabled_widgets(WidgetIdEnabled::create_from_hashmap(&map)) + .search_case_enabled_widgets_map(map) + .build(), + ); + } + + self.did_config_fail_to_save = self.update_config_file().is_err(); + } } pub fn toggle_search_whole_word(&mut self) { let is_in_search_widget = self.is_in_search_widget(); + let mut is_searching_whole_word: Option = None; if let Some(proc_widget_state) = self .proc_state .widget_states @@ -484,12 +526,55 @@ impl App { .search_toggle_whole_word(); proc_widget_state.update_query(); self.proc_state.force_update = Some(self.current_widget.widget_id - 1); + + is_searching_whole_word = Some( + proc_widget_state + .process_search_state + .is_searching_whole_word, + ); } } + + // Also toggle it in the config file if we actually changed it. + if let Some(is_searching_whole_word) = is_searching_whole_word { + if let Some(flags) = &mut self.config.flags { + if let Some(map) = &mut flags.search_whole_word_enabled_widgets_map { + // Just update the map. + let mapping = map.entry(self.current_widget.widget_id - 1).or_default(); + *mapping = is_searching_whole_word; + + flags.search_whole_word_enabled_widgets = + Some(WidgetIdEnabled::create_from_hashmap(&map)); + } else { + // Map doesn't exist yet... initialize ourselves. + let mut map = HashMap::default(); + map.insert(self.current_widget.widget_id - 1, is_searching_whole_word); + flags.search_whole_word_enabled_widgets = + Some(WidgetIdEnabled::create_from_hashmap(&map)); + flags.search_whole_word_enabled_widgets_map = Some(map); + } + } else { + // Must initialize it ourselves... + let mut map = HashMap::default(); + map.insert(self.current_widget.widget_id - 1, is_searching_whole_word); + + self.config.flags = Some( + ConfigFlags::builder() + .search_whole_word_enabled_widgets(WidgetIdEnabled::create_from_hashmap( + &map, + )) + .search_whole_word_enabled_widgets_map(map) + .build(), + ); + } + + self.did_config_fail_to_save = self.update_config_file().is_err(); + } } pub fn toggle_search_regex(&mut self) { let is_in_search_widget = self.is_in_search_widget(); + let mut is_searching_with_regex: Option = None; if let Some(proc_widget_state) = self .proc_state .widget_states @@ -499,8 +584,48 @@ impl App { proc_widget_state.process_search_state.search_toggle_regex(); proc_widget_state.update_query(); self.proc_state.force_update = Some(self.current_widget.widget_id - 1); + + is_searching_with_regex = Some( + proc_widget_state + .process_search_state + .is_searching_with_regex, + ); } } + + // Also toggle it in the config file if we actually changed it. + if let Some(is_searching_whole_word) = is_searching_with_regex { + if let Some(flags) = &mut self.config.flags { + if let Some(map) = &mut flags.search_regex_enabled_widgets_map { + // Just update the map. + let mapping = map.entry(self.current_widget.widget_id - 1).or_default(); + *mapping = is_searching_whole_word; + + flags.search_regex_enabled_widgets = + Some(WidgetIdEnabled::create_from_hashmap(&map)); + } else { + // Map doesn't exist yet... initialize ourselves. + let mut map = HashMap::default(); + map.insert(self.current_widget.widget_id - 1, is_searching_whole_word); + flags.search_regex_enabled_widgets = + Some(WidgetIdEnabled::create_from_hashmap(&map)); + flags.search_regex_enabled_widgets_map = Some(map); + } + } else { + // Must initialize it ourselves... + let mut map = HashMap::default(); + map.insert(self.current_widget.widget_id - 1, is_searching_whole_word); + + self.config.flags = Some( + ConfigFlags::builder() + .search_regex_enabled_widgets(WidgetIdEnabled::create_from_hashmap(&map)) + .search_regex_enabled_widgets_map(map) + .build(), + ); + } + + self.did_config_fail_to_save = self.update_config_file().is_err(); + } } pub fn toggle_tree_mode(&mut self) { @@ -1265,28 +1390,28 @@ impl App { pub fn on_space(&mut self) {} - pub fn open_config(&mut self) { + pub fn open_config_screen(&mut self) { self.is_config_open = true; self.is_force_redraw = true; } - pub fn close_config(&mut self) { + pub fn close_config_screen(&mut self) { self.is_config_open = false; self.is_force_redraw = true; } /// Call this whenever the config value is updated! - #[allow(dead_code)] //FIXME: Remove this fn update_config_file(&mut self) -> anyhow::Result<()> { if self.app_config_fields.no_write { + debug!("No write enabled. Config will not be written."); // Don't write! // FIXME: [CONFIG] This should be made VERY clear to the user... make a thing saying "it will not write due to no_write option" Ok(()) } else if let Some(config_path) = &self.config_path { // Update - std::fs::File::open(config_path)? - .write_all(toml::to_string(&self.config)?.as_bytes())?; - + // debug!("Updating config file - writing to: {:?}", config_path); + std::fs::File::create(config_path)? + .write_all(self.config.get_config_as_bytes()?.as_ref())?; Ok(()) } else { // FIXME: [CONFIG] Put an actual error message? diff --git a/src/bin/main.rs b/src/bin/main.rs index 37056e78..25ed880e 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -34,7 +34,7 @@ fn main() -> Result<()> { let config_path = read_config(matches.value_of("config_location")) .context("Unable to access the given config file location.")?; - let config: Config = create_or_get_config(&config_path) + let mut config: Config = create_or_get_config(&config_path) .context("Unable to properly parse or create the config file.")?; // Get widget layout separately @@ -45,7 +45,7 @@ fn main() -> Result<()> { // Create "app" struct, which will control most of the program and store settings/state let mut app = build_app( &matches, - &config, + &mut config, &widget_layout, default_widget_id, &default_widget_type_option, diff --git a/src/constants.rs b/src/constants.rs index 40f8e646..8bc6f11d 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -246,7 +246,8 @@ pub const DEFAULT_BATTERY_LAYOUT: &str = r##" pub const DEFAULT_CONFIG_FILE_PATH: &str = "bottom/bottom.toml"; pub const CONFIG_TOP_HEAD: &str = r##"# This is bottom's config file. Values in this config file will change when changed in the -# interface. You can also manually change these values. +# interface. You can also manually change these values. Be aware that contents of this file will be overwritten if something is +# changed in the application; you can disable writing via the --no_write flag or no_write config option. "##; @@ -272,6 +273,6 @@ pub const CONFIG_LAYOUT_HEAD: &str = r##" # All layout components have a ratio value - if this is not set, then it defaults to 1. "##; -pub const CONFIG_DIVIDER: &str = r##" -######################################################################### +pub const CONFIG_FILTER_HEAD: &str = r##" +# These options represent disabled entries for the temperature and disk widgets. "##; diff --git a/src/options.rs b/src/options.rs index 1efd10c9..0f85a611 100644 --- a/src/options.rs +++ b/src/options.rs @@ -1,6 +1,6 @@ use regex::Regex; use serde::{Deserialize, Serialize}; -use std::time::Instant; +use std::{borrow::Cow, time::Instant}; use std::{ collections::{HashMap, HashSet}, path::PathBuf, @@ -12,6 +12,8 @@ use crate::{ utils::error::{self, BottomError}, }; +use typed_builder::*; + use layout_options::*; pub mod layout_options; @@ -27,31 +29,129 @@ pub struct Config { pub temp_filter: Option, } -#[derive(Clone, Default, Deserialize, Serialize)] +impl Config { + pub fn get_config_as_bytes(&self) -> anyhow::Result> { + let mut config_string: Vec> = Vec::default(); + + // Top level + config_string.push(CONFIG_TOP_HEAD.into()); + config_string.push(toml::to_string_pretty(self)?.into()); + + Ok(config_string.concat().as_bytes().to_vec()) + } +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize, TypedBuilder)] pub struct ConfigFlags { + #[builder(default, setter(strip_option))] pub hide_avg_cpu: Option, + + #[builder(default, setter(strip_option))] pub dot_marker: Option, + + #[builder(default, setter(strip_option))] pub temperature_type: Option, + + #[builder(default, setter(strip_option))] pub rate: Option, + + #[builder(default, setter(strip_option))] pub left_legend: Option, + + #[builder(default, setter(strip_option))] pub current_usage: Option, + + #[builder(default, setter(strip_option))] pub group_processes: Option, + + #[builder(default, setter(strip_option))] pub case_sensitive: Option, + + #[builder(default, setter(strip_option))] pub whole_word: Option, + + #[builder(default, setter(strip_option))] pub regex: Option, + + #[builder(default, setter(strip_option))] pub default_widget: Option, + + #[builder(default, setter(strip_option))] pub basic: Option, + + #[builder(default, setter(strip_option))] pub default_time_value: Option, + + #[builder(default, setter(strip_option))] pub time_delta: Option, + + #[builder(default, setter(strip_option))] pub autohide_time: Option, + + #[builder(default, setter(strip_option))] pub hide_time: Option, + + #[builder(default, setter(strip_option))] pub default_widget_type: Option, + + #[builder(default, setter(strip_option))] pub default_widget_count: Option, + + #[builder(default, setter(strip_option))] pub use_old_network_legend: Option, + + #[builder(default, setter(strip_option))] pub hide_table_gap: Option, + + #[builder(default, setter(strip_option))] pub battery: Option, + + #[builder(default, setter(strip_option))] pub disable_click: Option, + + #[builder(default, setter(strip_option))] pub no_write: Option, + + // This is a huge hack to enable hashmap functionality WITHOUT being able to serializing the field. + // Basically, keep a hashmap in the struct, and convert to a vector every time. + #[builder(default, setter(strip_option))] + #[serde(skip)] + pub search_case_enabled_widgets_map: Option>, + + #[builder(default, setter(strip_option))] + pub search_case_enabled_widgets: Option>, + + #[builder(default, setter(strip_option))] + #[serde(skip)] + pub search_whole_word_enabled_widgets_map: Option>, + + #[builder(default, setter(strip_option))] + pub search_whole_word_enabled_widgets: Option>, + + #[builder(default, setter(strip_option))] + #[serde(skip)] + pub search_regex_enabled_widgets_map: Option>, + + #[builder(default, setter(strip_option))] + pub search_regex_enabled_widgets: Option>, +} + +#[derive(Clone, Default, Debug, Deserialize, Serialize)] +pub struct WidgetIdEnabled { + id: u64, + enabled: bool, +} + +impl WidgetIdEnabled { + pub fn create_from_hashmap(hashmap: &HashMap) -> Vec { + hashmap + .iter() + .map(|(id, enabled)| WidgetIdEnabled { + id: *id, + enabled: *enabled, + }) + .collect() + } } #[derive(Clone, Default, Deserialize, Serialize)] @@ -85,7 +185,7 @@ pub struct IgnoreList { } pub fn build_app( - matches: &clap::ArgMatches<'static>, config: &Config, widget_layout: &BottomLayout, + matches: &clap::ArgMatches<'static>, config: &mut Config, widget_layout: &BottomLayout, default_widget_id: u64, default_widget_type_option: &Option, config_path: Option, ) -> Result { @@ -271,6 +371,48 @@ pub fn build_app( let temp_filter = get_ignore_list(&config.temp_filter).context("Update 'temp_filter' in your config file")?; + // One more thing - we have to update the search settings of our proc_state_map, and create the hashmaps if needed! + // Note that if you change your layout, this might not actually match properly... not sure if/where we should deal with that... + if let Some(flags) = &mut config.flags { + if flags.case_sensitive.is_none() && !matches.is_present("case_sensitive") { + if let Some(search_case_enabled_widgets) = &flags.search_case_enabled_widgets { + let mapping = HashMap::new(); + for widget in search_case_enabled_widgets { + if let Some(proc_widget) = proc_state_map.get_mut(&widget.id) { + proc_widget.process_search_state.is_ignoring_case = !widget.enabled; + } + } + flags.search_case_enabled_widgets_map = Some(mapping); + } + } + + if flags.whole_word.is_none() && !matches.is_present("whole_word") { + if let Some(search_whole_word_enabled_widgets) = + &flags.search_whole_word_enabled_widgets + { + let mapping = HashMap::new(); + for widget in search_whole_word_enabled_widgets { + if let Some(proc_widget) = proc_state_map.get_mut(&widget.id) { + proc_widget.process_search_state.is_searching_whole_word = widget.enabled; + } + } + flags.search_whole_word_enabled_widgets_map = Some(mapping); + } + } + + if flags.regex.is_none() && !matches.is_present("regex") { + if let Some(search_regex_enabled_widgets) = &flags.search_regex_enabled_widgets { + let mapping = HashMap::new(); + for widget in search_regex_enabled_widgets { + if let Some(proc_widget) = proc_state_map.get_mut(&widget.id) { + proc_widget.process_search_state.is_searching_with_regex = widget.enabled; + } + } + flags.search_regex_enabled_widgets_map = Some(mapping); + } + } + } + Ok(App::builder() .app_config_fields(app_config_fields) .cpu_state(CpuState::init(cpu_state_map)) @@ -281,7 +423,7 @@ pub fn build_app( .temp_state(TempState::init(temp_state_map)) .battery_state(BatteryState::init(battery_state_map)) .basic_table_widget_state(basic_table_widget_state) - .current_widget(widget_map.get(&initial_widget_id).unwrap().clone()) // FIXME: [UNWRAP] - many of the unwraps are fine (like this one) but do a once-over and/or switch to expect? + .current_widget(widget_map.get(&initial_widget_id).unwrap().clone()) // TODO: [UNWRAP] - many of the unwraps are fine (like this one) but do a once-over and/or switch to expect? .widget_map(widget_map) .used_widgets(used_widgets) .filters(DataFilters {