summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorClement Tsang <34804052+ClementTsang@users.noreply.github.com>2020-09-28 19:50:21 -0400
committerGitHub <noreply@github.com>2020-09-28 19:50:21 -0400
commit57e87d88d09e6282770a2315977fe43ef52958b4 (patch)
tree12af86e5aebad78d067caf9a93d5e15efc4f5dc9
parent7eff79395db3efe9914d3e5ddcd6df7a1fdc649d (diff)
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.
-rw-r--r--.travis.yml1
-rw-r--r--.vscode/settings.json210
-rw-r--r--Cargo.toml1
-rw-r--r--src/app.rs141
-rw-r--r--src/bin/main.rs4
-rw-r--r--src/constants.rs7
-rw-r--r--src/options.rs150
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<bool> = 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<bool> = 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<bool> = None;
if let Some(proc_widget_state) = self
.proc_state
.widget_states
@@ -499,7 +584,47 @@ 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();
}
}
@@ -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<IgnoreList>,
}
-#[derive(Clone, Default, Deserialize, Serialize)]
+impl Config {
+ pub fn get_config_as_bytes(&self) -> anyhow::Result<Vec<u8>> {
+ let mut config_string: Vec<Cow<'_, str>> = 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<bool>,
+
+ #[builder(default, setter(strip_option))]
pub dot_marker: Option<bool>,
+
+ #[builder(default, setter(strip_option))]
pub temperature_type: Option<String>,
+
+ #[builder(default, setter(strip_option))]
pub rate: Option<u64>,
+
+ #[builder(default, setter(strip_option))]
pub left_legend: Option<bool>,
+
+ #[builder(default, setter(strip_option))]
pub current_usage: Option<bool>,
+
+ #[builder(default, setter(strip_option))]
pub group_processes: Option<bool>,
+
+ #[builder(default, setter(strip_option))]
pub case_sensitive: Option<bool>,
+
+ #[builder(default, setter(strip_option))]
pub whole_word: Option<bool>,
+
+ #[builder(default, setter(strip_option))]
pub regex: Option<bool>,
+
+ #[builder(default, setter(strip_option))]
pub default_widget: Option<String>,
+
+ #[builder(default, setter(strip_option))]
pub basic: Option<bool>,
+
+ #[builder(default, setter(strip_option))]
pub default_time_value: Option<u64>,
+
+ #[builder(default, setter(strip_option))]
pub time_delta: Option<u64>,
+
+ #[builder(default, setter(strip_option))]
pub autohide_time: Option<bool>,
+
+ #[builder(default, setter(strip_option))]
pub hide_time: Option<bool>,
+
+ #[builder(default, setter(strip_option))]
pub default_widget_type: Option<String>,
+
+ #[builder(default, setter(strip_option))]
pub default_widget_count: Option<u64>,
+
+ #[builder(default, setter(strip_option))]
pub use_old_network_legend: Option<bool>,
+
+ #[builder(default, setter(strip_option))]
pub hide_table_gap: Option<bool>,
+
+ #[builder(default, setter(strip_option))]
pub battery: Option<bool>,
+
+ #[builder(default, setter(strip_option))]
pub disable_click: Option<bool>,
+
+ #[builder(default, setter(strip_option))]
pub no_write: Option<bool>,
+
+ // 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<HashMap<u64, bool>>,
+
+ #[builder(default, setter(strip_option))]
+ pub search_case_enabled_widgets: Option<Vec<WidgetIdEnabled>>,
+
+ #[builder(default, setter(strip_option))]
+ #[serde(skip)]
+ pub search_whole_word_enabled_widgets_map: Option<HashMap<u64, bool>>,
+
+ #[builder(default, setter(strip_option))]
+ pub search_whole_word_enabled_widgets: Option<Vec<WidgetIdEnabled>>,
+
+ #[builder(default, setter(strip_option))]
+ #[serde(skip)]
+ pub search_regex_enabled_widgets_map: Option<HashMap<u64, bool>>,
+
+ #[builder(default, setter(strip_option))]
+ pub search_regex_enabled_widgets: Option<Vec<WidgetIdEnabled>>,
+}
+
+#[derive(Clone, Default, Debug, Deserialize, Serialize)]
+pub struct WidgetIdEnabled {
+ id: u64,
+ enabled: bool,
+}
+
+impl WidgetIdEnabled {
+ pub fn create_from_hashmap(hashmap: &HashMap<u64, bool>) -> Vec<WidgetIdEnabled> {
+ 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<BottomWidgetType>,
config_path: Option<PathBuf>,
) -> Result<App> {
@@ -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 {