From e5677d0620901146ce3bdd4593c13d484807b7cd Mon Sep 17 00:00:00 2001 From: multiplex55 <6619098+multiplex55@users.noreply.github.com> Date: Sat, 7 Feb 2026 09:46:53 -0500 Subject: [PATCH 1/3] Add todo widget filters and persisted filter tests --- src/dashboard/widgets/todo.rs | 243 +++++++++++++++++++++++++--- src/dashboard/widgets/todo_focus.rs | 159 +++++++++++++++++- 2 files changed, 372 insertions(+), 30 deletions(-) diff --git a/src/dashboard/widgets/todo.rs b/src/dashboard/widgets/todo.rs index aacfa4ae..fff85e95 100644 --- a/src/dashboard/widgets/todo.rs +++ b/src/dashboard/widgets/todo.rs @@ -16,6 +16,20 @@ pub enum TodoSort { Alphabetical, } +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum TodoStatusFilter { + All, + Open, + Done, +} + +impl Default for TodoStatusFilter { + fn default() -> Self { + TodoStatusFilter::Open + } +} + impl Default for TodoSort { fn default() -> Self { TodoSort::Priority @@ -34,13 +48,17 @@ fn default_show_progress() -> bool { pub struct TodoWidgetConfig { #[serde(default = "default_count")] pub count: usize, - #[serde(default)] - pub show_done: bool, #[serde(default = "default_show_progress")] pub show_progress: bool, #[serde(default)] + pub status: TodoStatusFilter, + #[serde(default)] + pub min_priority: u8, + #[serde(default)] pub filter_tags: Vec, #[serde(default)] + pub show_done: bool, + #[serde(default)] pub sort: TodoSort, #[serde(default)] pub query: Option, @@ -50,9 +68,11 @@ impl Default for TodoWidgetConfig { fn default() -> Self { Self { count: default_count(), - show_done: false, show_progress: default_show_progress(), + status: TodoStatusFilter::default(), + min_priority: 0, filter_tags: Vec::new(), + show_done: false, sort: TodoSort::default(), query: None, } @@ -65,7 +85,9 @@ pub struct TodoWidget { impl TodoWidget { pub fn new(cfg: TodoWidgetConfig) -> Self { - Self { cfg } + Self { + cfg: migrate_config(cfg), + } } pub fn settings_ui( @@ -83,12 +105,32 @@ impl TodoWidget { .changed(); ui.label("todos"); }); - changed |= ui - .checkbox(&mut cfg.show_done, "Include completed") - .changed(); changed |= ui .checkbox(&mut cfg.show_progress, "Show progress bar") .changed(); + egui::ComboBox::from_label("Status") + .selected_text(match cfg.status { + TodoStatusFilter::All => "All", + TodoStatusFilter::Open => "Open", + TodoStatusFilter::Done => "Done", + }) + .show_ui(ui, |ui| { + changed |= ui + .selectable_value(&mut cfg.status, TodoStatusFilter::All, "All") + .changed(); + changed |= ui + .selectable_value(&mut cfg.status, TodoStatusFilter::Open, "Open") + .changed(); + changed |= ui + .selectable_value(&mut cfg.status, TodoStatusFilter::Done, "Done") + .changed(); + }); + ui.horizontal(|ui| { + ui.label("Min priority"); + changed |= ui + .add(egui::DragValue::new(&mut cfg.min_priority).clamp_range(0..=255)) + .changed(); + }); egui::ComboBox::from_label("Sort by") .selected_text(match cfg.sort { TodoSort::Priority => "Priority", @@ -165,6 +207,43 @@ impl TodoWidget { }) } + fn status_match(&self, entry: &TodoEntry) -> bool { + match self.cfg.status { + TodoStatusFilter::All => true, + TodoStatusFilter::Open => !entry.done, + TodoStatusFilter::Done => entry.done, + } + } + + fn priority_match(&self, entry: &TodoEntry) -> bool { + entry.priority >= self.cfg.min_priority + } + + fn entry_matches_filters(&self, entry: &TodoEntry) -> bool { + self.status_match(entry) && self.priority_match(entry) && self.tags_match(entry) + } + + fn filter_entries(&self, todos: &[TodoEntry]) -> Vec<(usize, TodoEntry)> { + todos + .iter() + .cloned() + .enumerate() + .filter(|(_, t)| self.entry_matches_filters(t)) + .collect() + } + + fn available_tags(todos: &[TodoEntry]) -> Vec { + let mut tags: Vec = todos + .iter() + .flat_map(|todo| todo.tags.iter()) + .map(|tag| tag.trim().to_string()) + .filter(|tag| !tag.is_empty()) + .collect(); + tags.sort_by_key(|tag| tag.to_lowercase()); + tags.dedup_by(|a, b| a.eq_ignore_ascii_case(b)); + tags + } + fn sort_entries(entries: &mut Vec<(usize, TodoEntry)>, sort: TodoSort) { match sort { TodoSort::Priority => { @@ -181,18 +260,67 @@ impl TodoWidget { } fn render_summary(&mut self, ui: &mut egui::Ui, todos: &[TodoEntry]) -> Option { - let filtered: Vec<&TodoEntry> = todos.iter().filter(|t| self.tags_match(t)).collect(); + let filtered: Vec<&TodoEntry> = todos + .iter() + .filter(|t| self.priority_match(t) && self.tags_match(t)) + .collect(); let done = filtered.iter().filter(|t| t.done).count(); let total = filtered.len(); let remaining = total.saturating_sub(done); let mut action = None; ui.vertical(|ui| { - let mut tags_value = self.cfg.filter_tags.join(", "); ui.horizontal(|ui| { - ui.label("Filter tags"); - if ui.text_edit_singleline(&mut tags_value).changed() { - self.cfg.filter_tags = parse_tags(&tags_value); - } + egui::ComboBox::from_id_source(ui.id().with("todo_filter_status")) + .selected_text(match self.cfg.status { + TodoStatusFilter::All => "All", + TodoStatusFilter::Open => "Open", + TodoStatusFilter::Done => "Done", + }) + .show_ui(ui, |ui| { + ui.selectable_value(&mut self.cfg.status, TodoStatusFilter::All, "All"); + ui.selectable_value(&mut self.cfg.status, TodoStatusFilter::Open, "Open"); + ui.selectable_value(&mut self.cfg.status, TodoStatusFilter::Done, "Done"); + }); + ui.add( + egui::DragValue::new(&mut self.cfg.min_priority) + .clamp_range(0..=255) + .prefix("p≥"), + ); + let available_tags = Self::available_tags(todos); + let selected = if self.cfg.filter_tags.is_empty() { + "Tag: any".to_string() + } else { + format!("Tags: {}", self.cfg.filter_tags.join(",")) + }; + egui::ComboBox::from_id_source(ui.id().with("todo_filter_tags")) + .selected_text(selected) + .show_ui(ui, |ui| { + if ui.button("Clear tags").clicked() { + self.cfg.filter_tags.clear(); + } + for tag in available_tags { + let mut enabled = self + .cfg + .filter_tags + .iter() + .any(|selected| selected.eq_ignore_ascii_case(&tag)); + if ui.checkbox(&mut enabled, &tag).changed() { + if enabled { + self.cfg.filter_tags.push(tag.clone()); + } else { + self.cfg + .filter_tags + .retain(|selected| !selected.eq_ignore_ascii_case(&tag)); + } + self.cfg + .filter_tags + .sort_by_key(|value| value.to_lowercase()); + self.cfg + .filter_tags + .dedup_by(|a, b| a.eq_ignore_ascii_case(b)); + } + } + }); }); ui.horizontal(|ui| { ui.label(format!("Todos: {done}/{total} done")); @@ -225,15 +353,7 @@ impl TodoWidget { ctx: &DashboardContext<'_>, todos: &[TodoEntry], ) -> Option { - let mut entries: Vec<(usize, TodoEntry)> = todos - .iter() - .cloned() - .enumerate() - .filter(|(_, t)| self.tags_match(t)) - .collect(); - if !self.cfg.show_done { - entries.retain(|(_, t)| !t.done); - } + let mut entries = self.filter_entries(todos); Self::sort_entries(&mut entries, self.cfg.sort); entries.truncate(self.cfg.count); @@ -306,6 +426,14 @@ fn parse_tags(raw: &str) -> Vec { .collect() } +fn migrate_config(mut cfg: TodoWidgetConfig) -> TodoWidgetConfig { + if cfg.show_done && cfg.status == TodoStatusFilter::Open { + cfg.status = TodoStatusFilter::All; + } + cfg.filter_tags = parse_tags(&cfg.filter_tags.join(",")); + cfg +} + impl Default for TodoWidget { fn default() -> Self { Self { @@ -332,4 +460,75 @@ impl Widget for TodoWidget { } action } + + fn on_config_updated(&mut self, settings: &serde_json::Value) { + if let Ok(cfg) = serde_json::from_value::(settings.clone()) { + self.cfg = migrate_config(cfg); + } + } +} + +#[cfg(test)] +mod tests { + use super::{TodoSort, TodoStatusFilter, TodoWidget, TodoWidgetConfig}; + use crate::plugins::todo::TodoEntry; + + fn sample_entries() -> Vec { + vec![ + TodoEntry { + text: "alpha".into(), + done: false, + priority: 1, + tags: vec!["work".into()], + }, + TodoEntry { + text: "beta".into(), + done: true, + priority: 4, + tags: vec!["home".into(), "urgent".into()], + }, + TodoEntry { + text: "gamma".into(), + done: false, + priority: 5, + tags: vec!["urgent".into()], + }, + ] + } + + #[test] + fn filters_combine_before_sorting() { + let mut cfg = TodoWidgetConfig::default(); + cfg.status = TodoStatusFilter::Open; + cfg.min_priority = 3; + cfg.filter_tags = vec!["urgent".into()]; + cfg.sort = TodoSort::Priority; + let widget = TodoWidget::new(cfg); + + let mut filtered = widget.filter_entries(&sample_entries()); + TodoWidget::sort_entries(&mut filtered, TodoSort::Priority); + let texts: Vec = filtered.into_iter().map(|(_, entry)| entry.text).collect(); + assert_eq!(texts, vec!["gamma"]); + } + + #[test] + fn config_serialization_persists_filter_state() { + let cfg = TodoWidgetConfig { + count: 7, + show_progress: false, + status: TodoStatusFilter::Done, + min_priority: 2, + filter_tags: vec!["urgent".into(), "home".into()], + show_done: false, + sort: TodoSort::Alphabetical, + query: Some("todo list".into()), + }; + + let json = serde_json::to_value(&cfg).expect("serialize todo config"); + let restored: TodoWidgetConfig = + serde_json::from_value(json).expect("deserialize todo config"); + assert_eq!(restored.status, TodoStatusFilter::Done); + assert_eq!(restored.min_priority, 2); + assert_eq!(restored.filter_tags, vec!["urgent", "home"]); + } } diff --git a/src/dashboard/widgets/todo_focus.rs b/src/dashboard/widgets/todo_focus.rs index f1e03b11..edde875f 100644 --- a/src/dashboard/widgets/todo_focus.rs +++ b/src/dashboard/widgets/todo_focus.rs @@ -7,8 +7,28 @@ use crate::plugins::todo::{mark_done, TodoEntry, TODO_FILE}; use eframe::egui; use serde::{Deserialize, Serialize}; +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum TodoFocusStatusFilter { + All, + Open, + Done, +} + +impl Default for TodoFocusStatusFilter { + fn default() -> Self { + TodoFocusStatusFilter::Open + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TodoFocusConfig { + #[serde(default)] + pub status: TodoFocusStatusFilter, + #[serde(default)] + pub min_priority: u8, + #[serde(default)] + pub filter_tags: Vec, #[serde(default)] pub show_done: bool, #[serde(default)] @@ -18,6 +38,9 @@ pub struct TodoFocusConfig { impl Default for TodoFocusConfig { fn default() -> Self { Self { + status: TodoFocusStatusFilter::Open, + min_priority: 0, + filter_tags: Vec::new(), show_done: false, query: Some("todo".into()), } @@ -30,7 +53,9 @@ pub struct TodoFocusWidget { impl TodoFocusWidget { pub fn new(cfg: TodoFocusConfig) -> Self { - Self { cfg } + Self { + cfg: migrate_config(cfg), + } } pub fn settings_ui( @@ -40,9 +65,37 @@ impl TodoFocusWidget { ) -> WidgetSettingsUiResult { edit_typed_settings(ui, value, ctx, |ui, cfg: &mut TodoFocusConfig, _ctx| { let mut changed = false; - changed |= ui - .checkbox(&mut cfg.show_done, "Include completed") - .changed(); + egui::ComboBox::from_label("Status") + .selected_text(match cfg.status { + TodoFocusStatusFilter::All => "All", + TodoFocusStatusFilter::Open => "Open", + TodoFocusStatusFilter::Done => "Done", + }) + .show_ui(ui, |ui| { + changed |= ui + .selectable_value(&mut cfg.status, TodoFocusStatusFilter::All, "All") + .changed(); + changed |= ui + .selectable_value(&mut cfg.status, TodoFocusStatusFilter::Open, "Open") + .changed(); + changed |= ui + .selectable_value(&mut cfg.status, TodoFocusStatusFilter::Done, "Done") + .changed(); + }); + ui.horizontal(|ui| { + ui.label("Min priority"); + changed |= ui + .add(egui::DragValue::new(&mut cfg.min_priority).clamp_range(0..=255)) + .changed(); + }); + ui.horizontal(|ui| { + ui.label("Filter tags"); + let mut tags = cfg.filter_tags.join(", "); + if ui.text_edit_singleline(&mut tags).changed() { + cfg.filter_tags = parse_tags(&tags); + changed = true; + } + }); ui.horizontal(|ui| { ui.label("Query override"); let mut query = cfg.query.clone().unwrap_or_default(); @@ -61,12 +114,45 @@ impl TodoFocusWidget { fn pick_focus(&self, entries: &[TodoEntry]) -> Option<(usize, TodoEntry)> { let mut todos: Vec<(usize, TodoEntry)> = entries.iter().cloned().enumerate().collect(); - if !self.cfg.show_done { - todos.retain(|(_, t)| !t.done); - } + todos.retain(|(_, t)| self.entry_matches_filters(t)); todos.sort_by(|a, b| b.1.priority.cmp(&a.1.priority).then_with(|| a.0.cmp(&b.0))); todos.into_iter().next() } + + fn entry_matches_filters(&self, entry: &TodoEntry) -> bool { + let status_match = match self.cfg.status { + TodoFocusStatusFilter::All => true, + TodoFocusStatusFilter::Open => !entry.done, + TodoFocusStatusFilter::Done => entry.done, + }; + let tag_match = if self.cfg.filter_tags.is_empty() { + true + } else { + self.cfg.filter_tags.iter().any(|tag| { + entry + .tags + .iter() + .any(|entry_tag| entry_tag.eq_ignore_ascii_case(tag)) + }) + }; + status_match && entry.priority >= self.cfg.min_priority && tag_match + } +} + +fn parse_tags(raw: &str) -> Vec { + raw.split(',') + .map(|tag| tag.trim()) + .filter(|tag| !tag.is_empty()) + .map(|tag| tag.to_string()) + .collect() +} + +fn migrate_config(mut cfg: TodoFocusConfig) -> TodoFocusConfig { + if cfg.show_done && cfg.status == TodoFocusStatusFilter::Open { + cfg.status = TodoFocusStatusFilter::All; + } + cfg.filter_tags = parse_tags(&cfg.filter_tags.join(",")); + cfg } impl Default for TodoFocusWidget { @@ -132,7 +218,64 @@ impl Widget for TodoFocusWidget { fn on_config_updated(&mut self, settings: &serde_json::Value) { if let Ok(cfg) = serde_json::from_value::(settings.clone()) { - self.cfg = cfg; + self.cfg = migrate_config(cfg); } } } + +#[cfg(test)] +mod tests { + use super::{TodoFocusConfig, TodoFocusStatusFilter, TodoFocusWidget}; + use crate::plugins::todo::TodoEntry; + + #[test] + fn focus_filters_by_status_priority_and_tags() { + let widget = TodoFocusWidget::new(TodoFocusConfig { + status: TodoFocusStatusFilter::Open, + min_priority: 3, + filter_tags: vec!["urgent".into()], + show_done: false, + query: None, + }); + let entries = vec![ + TodoEntry { + text: "low".into(), + done: false, + priority: 1, + tags: vec!["urgent".into()], + }, + TodoEntry { + text: "done".into(), + done: true, + priority: 5, + tags: vec!["urgent".into()], + }, + TodoEntry { + text: "focus".into(), + done: false, + priority: 4, + tags: vec!["urgent".into()], + }, + ]; + + let picked = widget.pick_focus(&entries).expect("focus todo exists"); + assert_eq!(picked.1.text, "focus"); + } + + #[test] + fn focus_config_persists_filter_state() { + let cfg = TodoFocusConfig { + status: TodoFocusStatusFilter::Done, + min_priority: 2, + filter_tags: vec!["home".into()], + show_done: false, + query: Some("todo".into()), + }; + let value = serde_json::to_value(&cfg).expect("serialize focus config"); + let restored: TodoFocusConfig = + serde_json::from_value(value).expect("deserialize focus config"); + assert_eq!(restored.status, TodoFocusStatusFilter::Done); + assert_eq!(restored.min_priority, 2); + assert_eq!(restored.filter_tags, vec!["home"]); + } +} From 467e7c610276bfc4b1715f1e681fe6fa11a58059 Mon Sep 17 00:00:00 2001 From: multiplex55 <6619098+multiplex55@users.noreply.github.com> Date: Sat, 7 Feb 2026 10:06:20 -0500 Subject: [PATCH 2/3] Stabilize preserve_command cwd-dependent tests --- tests/preserve_command.rs | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/tests/preserve_command.rs b/tests/preserve_command.rs index ea41a8ed..e623c55f 100644 --- a/tests/preserve_command.rs +++ b/tests/preserve_command.rs @@ -3,7 +3,38 @@ use multi_launcher::actions::Action; use multi_launcher::gui::LauncherApp; use multi_launcher::plugin::PluginManager; use multi_launcher::settings::Settings; +use once_cell::sync::Lazy; +use std::path::PathBuf; use std::sync::{atomic::AtomicBool, Arc}; +use std::sync::{Mutex, MutexGuard}; + +static CWD_TEST_LOCK: Lazy> = Lazy::new(|| Mutex::new(())); + +struct CurrentDirGuard { + _lock: MutexGuard<'static, ()>, + original: PathBuf, + _tmp: tempfile::TempDir, +} + +impl CurrentDirGuard { + fn new() -> Self { + let lock = CWD_TEST_LOCK.lock().expect("cwd test lock poisoned"); + let original = std::env::current_dir().expect("resolve current dir"); + let tmp = tempfile::tempdir().expect("create temp dir"); + std::env::set_current_dir(tmp.path()).expect("switch to temp dir"); + Self { + _lock: lock, + original, + _tmp: tmp, + } + } +} + +impl Drop for CurrentDirGuard { + fn drop(&mut self) { + let _ = std::env::set_current_dir(&self.original); + } +} fn new_app(ctx: &egui::Context, actions: Vec, preserve: bool) -> LauncherApp { let custom_len = actions.len(); @@ -105,8 +136,7 @@ fn todo_add_preserves_prefix() { args: None, }]; let mut app = new_app(&ctx, actions, true); - let dir = tempfile::tempdir().unwrap(); - std::env::set_current_dir(dir.path()).unwrap(); + let _cwd_guard = CurrentDirGuard::new(); app.query = "todo add test".into(); let a = app.results[0].clone(); if multi_launcher::launcher::launch_action(&a).is_ok() { @@ -129,8 +159,7 @@ fn tmp_new_preserves_prefix() { args: None, }]; let mut app = new_app(&ctx, actions, true); - let dir = tempfile::tempdir().unwrap(); - std::env::set_current_dir(dir.path()).unwrap(); + let _cwd_guard = CurrentDirGuard::new(); app.query = "tmp new".into(); if app.preserve_command { app.query = "tmp new ".into(); From 018d415e4dcdf2465012cfd595fa22e01ffaee2a Mon Sep 17 00:00:00 2001 From: multiplex55 Date: Sat, 7 Feb 2026 10:12:33 -0500 Subject: [PATCH 3/3] Code formatting and .gitignore update Add dashboard.json to .gitignore and apply small formatting/whitespace cleanups across several files for readability. Reorder/import mouse_gestures DB and usage modules in data_cache.rs, simplify and reflow long UI calls and closures in gesture_cheat_sheet.rs, mouse_gestures_dialog.rs, and plugins/mouse_gestures.rs, and tidy test formatting and imports in tests/mouse_gestures_service.rs and tests/mouse_gestures_settings.rs. These are non-functional stylistic changes to improve code consistency and maintainability. --- .gitignore | 1 + src/dashboard/data_cache.rs | 4 +-- src/dashboard/widgets/gesture_cheat_sheet.rs | 20 +++++------ src/gui/mouse_gestures_dialog.rs | 4 +-- src/plugins/mouse_gestures.rs | 38 +++++++++++--------- tests/mouse_gestures_service.rs | 15 ++++---- tests/mouse_gestures_settings.rs | 6 +++- 7 files changed, 49 insertions(+), 39 deletions(-) diff --git a/.gitignore b/.gitignore index 450f5b13..561f0b3a 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ mouse_gestures_usage.json mouse_gestures.json *.patch todo.json +dashboard.json diff --git a/src/dashboard/data_cache.rs b/src/dashboard/data_cache.rs index 6099082c..1f507729 100644 --- a/src/dashboard/data_cache.rs +++ b/src/dashboard/data_cache.rs @@ -1,4 +1,6 @@ use crate::actions::Action; +use crate::mouse_gestures::db::{load_gestures, GestureDb, GESTURES_FILE}; +use crate::mouse_gestures::usage::{load_usage, GestureUsageEntry, GESTURES_USAGE_FILE}; use crate::plugin::PluginManager; use crate::plugins::calendar::{ build_snapshot, refresh_events_from_disk, CalendarSnapshot, CALENDAR_EVENTS_FILE, @@ -8,8 +10,6 @@ use crate::plugins::fav::{load_favs, FavEntry, FAV_FILE}; use crate::plugins::note::{load_notes, Note}; use crate::plugins::snippets::{load_snippets, SnippetEntry, SNIPPETS_FILE}; use crate::plugins::todo::{load_todos, TodoEntry, TODO_FILE}; -use crate::mouse_gestures::db::{load_gestures, GestureDb, GESTURES_FILE}; -use crate::mouse_gestures::usage::{load_usage, GestureUsageEntry, GESTURES_USAGE_FILE}; use crate::{launcher, launcher::RecycleBinInfo}; use chrono::Local; use std::sync::{Arc, Mutex}; diff --git a/src/dashboard/widgets/gesture_cheat_sheet.rs b/src/dashboard/widgets/gesture_cheat_sheet.rs index 57d36d44..8fcb79de 100644 --- a/src/dashboard/widgets/gesture_cheat_sheet.rs +++ b/src/dashboard/widgets/gesture_cheat_sheet.rs @@ -56,7 +56,9 @@ impl GestureCheatSheetWidget { .add(egui::DragValue::new(&mut cfg.count).clamp_range(1..=50)) .changed(); }); - changed |= ui.checkbox(&mut cfg.show_disabled, "Show disabled").changed(); + changed |= ui + .checkbox(&mut cfg.show_disabled, "Show disabled") + .changed(); changed }, ) @@ -111,7 +113,11 @@ impl Widget for GestureCheatSheetWidget { .cloned() .map(|gesture| { let count = counts - .get(&(gesture.label.clone(), gesture.tokens.clone(), gesture.dir_mode)) + .get(&( + gesture.label.clone(), + gesture.tokens.clone(), + gesture.dir_mode, + )) .copied() .unwrap_or(0); (gesture, count) @@ -122,10 +128,7 @@ impl Widget for GestureCheatSheetWidget { rows.retain(|(gesture, _)| gesture.enabled); } - rows.sort_by(|a, b| { - b.1.cmp(&a.1) - .then_with(|| a.0.label.cmp(&b.0.label)) - }); + rows.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.label.cmp(&b.0.label))); let mut clicked = None; let count = self.cfg.count.max(1); @@ -153,10 +156,7 @@ impl Widget for GestureCheatSheetWidget { enabled, )); } - if ui - .selectable_label(false, gesture.label.clone()) - .clicked() - { + if ui.selectable_label(false, gesture.label.clone()).clicked() { clicked = Some(gesture_focus_action( &gesture.label, &gesture.tokens, diff --git a/src/gui/mouse_gestures_dialog.rs b/src/gui/mouse_gestures_dialog.rs index 7ee36318..b4f3bf86 100644 --- a/src/gui/mouse_gestures_dialog.rs +++ b/src/gui/mouse_gestures_dialog.rs @@ -328,9 +328,7 @@ impl MgGesturesDialog { .gestures .iter() .position(|gesture| { - gesture.label == label - && gesture.tokens == tokens - && gesture.dir_mode == dir_mode + gesture.label == label && gesture.tokens == tokens && gesture.dir_mode == dir_mode }) .or(self.selected_idx); self.ensure_selection(); diff --git a/src/plugins/mouse_gestures.rs b/src/plugins/mouse_gestures.rs index a4a7f4b1..6ddf5c55 100644 --- a/src/plugins/mouse_gestures.rs +++ b/src/plugins/mouse_gestures.rs @@ -632,11 +632,15 @@ impl Plugin for MouseGesturesPlugin { .changed(); }); }); - ui.small("Fallback runs when a gesture does not match; default is pass-through right-click."); + ui.small( + "Fallback runs when a gesture does not match; default is pass-through right-click.", + ); ui.separator(); ui.heading("Ignore windows (disable gestures)"); - ui.small("Gestures will be ignored when the active window title contains one of these entries."); + ui.small( + "Gestures will be ignored when the active window title contains one of these entries.", + ); let mut remove_index: Option = None; if cfg.ignore_window_titles.is_empty() { @@ -702,21 +706,23 @@ impl Plugin for MouseGesturesPlugin { if self.window_picker_titles.is_empty() { ui.label("No windows found."); } else { - egui::ScrollArea::vertical().max_height(220.0).show(ui, |ui| { - for title in self.window_picker_titles.clone() { - ui.horizontal(|ui| { - ui.label(&title); - if ui.button("Add").clicked() { - if add_ignore_window_title( - &mut cfg.ignore_window_titles, - &title, - ) { - changed = true; + egui::ScrollArea::vertical() + .max_height(220.0) + .show(ui, |ui| { + for title in self.window_picker_titles.clone() { + ui.horizontal(|ui| { + ui.label(&title); + if ui.button("Add").clicked() { + if add_ignore_window_title( + &mut cfg.ignore_window_titles, + &title, + ) { + changed = true; + } } - } - }); - } - }); + }); + } + }); } }); self.window_picker_open = open; diff --git a/tests/mouse_gestures_service.rs b/tests/mouse_gestures_service.rs index 08e63026..e9011fb6 100644 --- a/tests/mouse_gestures_service.rs +++ b/tests/mouse_gestures_service.rs @@ -1,14 +1,13 @@ +use multi_launcher::gui::register_event_sender; use multi_launcher::mouse_gestures::db::{ BindingEntry, BindingKind, GestureDb, GestureEntry, SCHEMA_VERSION, }; use multi_launcher::mouse_gestures::engine::DirMode; use multi_launcher::mouse_gestures::overlay::OverlayBackend; use multi_launcher::mouse_gestures::service::{ - should_ignore_window_title, CancelBehavior, CursorPositionProvider, HookEvent, - MockHookBackend, MouseGestureConfig, MouseGestureService, NoMatchBehavior, OverlayFactory, - RightClickBackend, + should_ignore_window_title, CancelBehavior, CursorPositionProvider, HookEvent, MockHookBackend, + MouseGestureConfig, MouseGestureService, NoMatchBehavior, OverlayFactory, RightClickBackend, }; -use multi_launcher::gui::register_event_sender; use once_cell::sync::Lazy; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; @@ -188,7 +187,10 @@ fn should_ignore_window_title_handles_matches() { let ignore = vec!["Notepad".to_string(), " firefox ".to_string()]; assert!(should_ignore_window_title(&ignore, "Notepad")); assert!(should_ignore_window_title(&ignore, "Mozilla Firefox")); - assert!(should_ignore_window_title(&ignore, "FIREFOX - Private Browsing")); + assert!(should_ignore_window_title( + &ignore, + "FIREFOX - Private Browsing" + )); assert!(!should_ignore_window_title(&ignore, "Terminal")); } @@ -616,8 +618,7 @@ fn selection_persists_across_gesture_sessions() { cursor_provider.set_position((50.0, 0.0)); sleep(Duration::from_millis(50)); - let hint_text = - wait_for_hint(&hint_state, Duration::from_millis(500)).expect("hint text"); + let hint_text = wait_for_hint(&hint_state, Duration::from_millis(500)).expect("hint text"); assert!(hint_text.contains("Secondary")); service.stop(); diff --git a/tests/mouse_gestures_settings.rs b/tests/mouse_gestures_settings.rs index 9c7e811e..4c60460e 100644 --- a/tests/mouse_gestures_settings.rs +++ b/tests/mouse_gestures_settings.rs @@ -16,7 +16,11 @@ fn normalize_ignore_window_titles_dedupes_and_trims() { assert!(changed); assert_eq!( titles, - vec!["Notepad".to_string(), "firefox".to_string(), "Terminal".to_string()] + vec![ + "Notepad".to_string(), + "firefox".to_string(), + "Terminal".to_string() + ] ); }