From 3efa62fa327f0aa2357a670fd46a36eb29a2be9c Mon Sep 17 00:00:00 2001 From: multiplex55 <6619098+multiplex55@users.noreply.github.com> Date: Thu, 12 Feb 2026 18:14:52 -0500 Subject: [PATCH 1/2] Improve gesture auto-labeling and action source expansion --- src/gui/mouse_gestures_dialog.rs | 283 +++++++++++++++++++++++-------- 1 file changed, 212 insertions(+), 71 deletions(-) diff --git a/src/gui/mouse_gestures_dialog.rs b/src/gui/mouse_gestures_dialog.rs index b25da0a2..1deb1600 100644 --- a/src/gui/mouse_gestures_dialog.rs +++ b/src/gui/mouse_gestures_dialog.rs @@ -6,6 +6,7 @@ use crate::mouse_gestures::db::{ }; use crate::mouse_gestures::engine::{DirMode, GestureTracker}; use crate::mouse_gestures::service::MouseGestureConfig; +use crate::plugin::Plugin; use eframe::egui; #[derive(Debug, Clone, Copy)] @@ -122,6 +123,108 @@ fn stroke_points_in_rect(stroke: &[[i16; 2]], rect: egui::Rect) -> Vec String { + tokens + .chars() + .filter(|c| c.is_ascii_alphanumeric()) + .map(|c| c.to_ascii_uppercase()) + .take(AUTO_LABEL_TOKEN_MAX) + .collect() +} + +fn label_from_recorded_tokens(tokens: &str) -> Option { + let normalized = normalize_recorded_tokens_for_label(tokens); + if normalized.is_empty() { + None + } else { + Some(format!("{AUTO_LABEL_PREFIX} {normalized}")) + } +} + +fn is_default_generated_label(label: &str) -> bool { + let trimmed = label.trim(); + if trimmed.is_empty() { + return true; + } + + let Some(rest) = trimmed.strip_prefix(AUTO_LABEL_PREFIX) else { + return false; + }; + let rest = rest.trim(); + if rest.is_empty() { + return true; + } + + rest.chars().all(|c| c.is_ascii_digit()) || rest == normalize_recorded_tokens_for_label(rest) +} + +fn list_query_prefix_for_plugin(plugin_name: &str) -> Option<&'static str> { + match plugin_name { + "folders" => Some("f list"), + "bookmarks" => Some("bm list"), + "clipboard" => Some("cb list"), + "emoji" => Some("emoji list"), + "notes" => Some("note list"), + _ => None, + } +} + +fn resolve_action_source(plugin: &dyn Plugin, filter: &str) -> Vec { + if let Some(prefix) = list_query_prefix_for_plugin(plugin.name()) { + let query = if filter.trim().is_empty() { + prefix.to_string() + } else { + format!("{prefix} {}", filter.trim()) + }; + let actions = plugin.search(&query); + if !actions.is_empty() { + return actions; + } + } + plugin.commands() +} + +fn append_query_args(query: &str, add_args: &str) -> String { + let extra = add_args.trim(); + if extra.is_empty() { + return query.to_string(); + } + if query.ends_with(' ') { + format!("{query}{extra}") + } else { + format!("{query} {extra}") + } +} + +fn apply_action_pick(editor: &mut BindingEditor, act: &crate::actions::Action, add_args: &str) { + if let Some(query) = act.action.strip_prefix("query:") { + editor.kind = match editor.kind { + BindingKind::SetQueryAndShow => BindingKind::SetQueryAndShow, + BindingKind::SetQueryAndExecute => BindingKind::SetQueryAndExecute, + _ => BindingKind::SetQuery, + }; + editor.action = append_query_args(query, add_args); + editor.args.clear(); + } else if act.action == "launcher:toggle" { + editor.kind = BindingKind::ToggleLauncher; + editor.action.clear(); + editor.args.clear(); + } else { + editor.kind = BindingKind::Execute; + editor.action = act.action.clone(); + editor.args = if add_args.trim().is_empty() { + act.args.clone().unwrap_or_default() + } else { + add_args.trim().to_string() + }; + } + editor.label = act.label.clone(); + editor.add_args.clear(); +} + pub struct GestureRecorder { config: RecorderConfig, tracker: GestureTracker, @@ -315,6 +418,23 @@ impl Default for MgGesturesDialog { } impl MgGesturesDialog { + fn apply_recording_to_entry( + &mut self, + entry: &mut GestureEntry, + recorded_tokens: &str, + ) -> bool { + entry.tokens = recorded_tokens.to_string(); + self.token_buffer = entry.tokens.clone(); + entry.stroke = self.recorder.normalized_stroke(); + + if is_default_generated_label(&entry.label) + && let Some(auto_label) = label_from_recorded_tokens(recorded_tokens) + { + entry.label = auto_label; + } + true + } + /// Returns gesture indices sorted by label (case-insensitive) for display purposes. fn sorted_gesture_indices(&self) -> Vec { let mut indices: Vec = (0..self.db.gestures.len()).collect(); @@ -691,29 +811,7 @@ impl MgGesturesDialog { } if ui.button(format!("{} - {}", act.label, act.desc)).clicked() { - editor.label = act.label.clone(); - if let Some(query) = act.action.strip_prefix("query:") { - editor.kind = match editor.kind { - BindingKind::SetQueryAndShow => { - BindingKind::SetQueryAndShow - } - BindingKind::SetQueryAndExecute => { - BindingKind::SetQueryAndExecute - } - _ => BindingKind::SetQuery, - }; - editor.action = query.to_string(); - editor.args.clear(); - } else if act.action == "launcher:toggle" { - editor.kind = BindingKind::ToggleLauncher; - editor.action.clear(); - editor.args.clear(); - } else { - editor.kind = BindingKind::Execute; - editor.action = act.action.clone(); - editor.args = act.args.clone().unwrap_or_default(); - } - editor.add_args.clear(); + apply_action_pick(editor, act, ""); } } }); @@ -721,13 +819,8 @@ impl MgGesturesDialog { app.plugins.iter().find(|p| p.name() == editor.add_plugin) { let filter = editor.add_filter.trim().to_lowercase(); - let mut actions = if plugin.name() == "folders" { - plugin.search(&format!("f list {}", editor.add_filter)) - } else if plugin.name() == "bookmarks" { - plugin.search(&format!("bm list {}", editor.add_filter)) - } else { - plugin.commands() - }; + let mut actions = + resolve_action_source(plugin.as_ref(), &editor.add_filter); egui::ScrollArea::vertical() .id_source("mg_binding_action_list") .max_height(160.0) @@ -742,41 +835,8 @@ impl MgGesturesDialog { } if ui.button(format!("{} - {}", act.label, act.desc)).clicked() { - let args = if editor.add_args.trim().is_empty() { - None - } else { - Some(editor.add_args.clone()) - }; - - if let Some(query) = act.action.strip_prefix("query:") { - let mut query = query.to_string(); - if let Some(ref a) = args { - query.push_str(a); - } - editor.kind = match editor.kind { - BindingKind::SetQueryAndShow => { - BindingKind::SetQueryAndShow - } - BindingKind::SetQueryAndExecute => { - BindingKind::SetQueryAndExecute - } - _ => BindingKind::SetQuery, - }; - editor.action = query; - editor.args.clear(); - } else if act.action == "launcher:toggle" { - editor.kind = BindingKind::ToggleLauncher; - editor.action.clear(); - editor.args.clear(); - } else { - editor.kind = BindingKind::Execute; - editor.action = act.action.clone(); - editor.args = args.unwrap_or_else(|| { - act.args.clone().unwrap_or_default() - }); - } - editor.label = act.label.clone(); - editor.add_args.clear(); + let add_args = editor.add_args.clone(); + apply_action_pick(editor, &act, &add_args); } } }); @@ -1022,9 +1082,7 @@ impl MgGesturesDialog { )); ui.horizontal(|ui| { if ui.button("Use Recording").clicked() { - entry.tokens = recorded_tokens.clone(); - self.token_buffer = entry.tokens.clone(); - entry.stroke = self.recorder.normalized_stroke(); + self.apply_recording_to_entry(entry, &recorded_tokens); self.recorder.reset(); save_now = true; } @@ -1067,9 +1125,7 @@ impl MgGesturesDialog { if response.drag_stopped() { let recorded_tokens = self.recorder.tokens_string(); if !recorded_tokens.is_empty() { - entry.tokens = recorded_tokens.clone(); - self.token_buffer = entry.tokens.clone(); - entry.stroke = self.recorder.normalized_stroke(); + self.apply_recording_to_entry(entry, &recorded_tokens); save_now = true; } //Clear the live drawing so the saved preview is shown @@ -1207,4 +1263,89 @@ mod tests { assert_eq!(dlg.selected_idx, Some(1)); assert_eq!(dlg.rename_idx, Some(1)); } + + #[derive(Clone)] + struct MockPlugin { + name: String, + search_results: Vec, + command_results: Vec, + } + + impl crate::plugin::Plugin for MockPlugin { + fn search(&self, _query: &str) -> Vec { + self.search_results.clone() + } + + fn name(&self) -> &str { + &self.name + } + + fn description(&self) -> &str { + "mock" + } + + fn capabilities(&self) -> &[&str] { + &[] + } + + fn commands(&self) -> Vec { + self.command_results.clone() + } + } + + #[test] + fn auto_label_is_applied_only_for_default_generated_labels() { + let mut dlg = MgGesturesDialog::default(); + let mut entry = gesture("Gesture 3", ""); + + dlg.apply_recording_to_entry(&mut entry, "udlr"); + + assert_eq!(entry.tokens, "udlr"); + assert_eq!(entry.label, "Gesture UDLR"); + } + + #[test] + fn customized_label_is_preserved_when_new_recording_is_applied() { + let mut dlg = MgGesturesDialog::default(); + let mut entry = gesture("My Favorite Gesture", "UD"); + + dlg.apply_recording_to_entry(&mut entry, "LR"); + + assert_eq!(entry.tokens, "LR"); + assert_eq!(entry.label, "My Favorite Gesture"); + } + + #[test] + fn resolve_action_source_prefers_mapped_list_search_and_falls_back_to_commands() { + let list_action = crate::actions::Action { + label: "Clipboard Entry".into(), + desc: "Clipboard".into(), + action: "clipboard:copy:1".into(), + args: None, + }; + let command_action = crate::actions::Action { + label: "cb list".into(), + desc: "Clipboard".into(), + action: "query:cb list".into(), + args: None, + }; + + let mapped_plugin = MockPlugin { + name: "clipboard".into(), + search_results: vec![list_action.clone()], + command_results: vec![command_action.clone()], + }; + let mapped_actions = resolve_action_source(&mapped_plugin, "abc"); + assert_eq!(mapped_actions.len(), 1); + assert_eq!(mapped_actions[0].action, "clipboard:copy:1"); + + let fallback_plugin = MockPlugin { + name: "custom".into(), + search_results: vec![list_action], + command_results: vec![command_action.clone()], + }; + let fallback_actions = resolve_action_source(&fallback_plugin, "abc"); + assert_eq!(fallback_actions.len(), 1); + assert_eq!(fallback_actions[0].action, command_action.action); + } } From c06b07cede1a1e910a5d8a1ca19ccee36644ee7e Mon Sep 17 00:00:00 2001 From: multiplex55 <6619098+multiplex55@users.noreply.github.com> Date: Thu, 12 Feb 2026 18:22:49 -0500 Subject: [PATCH 2/2] Fix mouse gesture dialog borrow and edition compatibility --- src/gui/mouse_gestures_dialog.rs | 60 ++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/src/gui/mouse_gestures_dialog.rs b/src/gui/mouse_gestures_dialog.rs index 1deb1600..03fc6c50 100644 --- a/src/gui/mouse_gestures_dialog.rs +++ b/src/gui/mouse_gestures_dialog.rs @@ -199,6 +199,23 @@ fn append_query_args(query: &str, add_args: &str) -> String { } } +fn apply_recording_to_entry( + entry: &mut GestureEntry, + token_buffer: &mut String, + recorded_tokens: &str, + normalized_stroke: Vec<[i16; 2]>, +) { + entry.tokens = recorded_tokens.to_string(); + *token_buffer = entry.tokens.clone(); + entry.stroke = normalized_stroke; + + if is_default_generated_label(&entry.label) { + if let Some(auto_label) = label_from_recorded_tokens(recorded_tokens) { + entry.label = auto_label; + } + } +} + fn apply_action_pick(editor: &mut BindingEditor, act: &crate::actions::Action, add_args: &str) { if let Some(query) = act.action.strip_prefix("query:") { editor.kind = match editor.kind { @@ -418,23 +435,6 @@ impl Default for MgGesturesDialog { } impl MgGesturesDialog { - fn apply_recording_to_entry( - &mut self, - entry: &mut GestureEntry, - recorded_tokens: &str, - ) -> bool { - entry.tokens = recorded_tokens.to_string(); - self.token_buffer = entry.tokens.clone(); - entry.stroke = self.recorder.normalized_stroke(); - - if is_default_generated_label(&entry.label) - && let Some(auto_label) = label_from_recorded_tokens(recorded_tokens) - { - entry.label = auto_label; - } - true - } - /// Returns gesture indices sorted by label (case-insensitive) for display purposes. fn sorted_gesture_indices(&self) -> Vec { let mut indices: Vec = (0..self.db.gestures.len()).collect(); @@ -1082,7 +1082,14 @@ impl MgGesturesDialog { )); ui.horizontal(|ui| { if ui.button("Use Recording").clicked() { - self.apply_recording_to_entry(entry, &recorded_tokens); + let normalized_stroke = + self.recorder.normalized_stroke(); + apply_recording_to_entry( + entry, + &mut self.token_buffer, + &recorded_tokens, + normalized_stroke, + ); self.recorder.reset(); save_now = true; } @@ -1125,7 +1132,14 @@ impl MgGesturesDialog { if response.drag_stopped() { let recorded_tokens = self.recorder.tokens_string(); if !recorded_tokens.is_empty() { - self.apply_recording_to_entry(entry, &recorded_tokens); + let normalized_stroke = + self.recorder.normalized_stroke(); + apply_recording_to_entry( + entry, + &mut self.token_buffer, + &recorded_tokens, + normalized_stroke, + ); save_now = true; } //Clear the live drawing so the saved preview is shown @@ -1295,10 +1309,10 @@ mod tests { #[test] fn auto_label_is_applied_only_for_default_generated_labels() { - let mut dlg = MgGesturesDialog::default(); let mut entry = gesture("Gesture 3", ""); - dlg.apply_recording_to_entry(&mut entry, "udlr"); + let mut token_buffer = String::new(); + apply_recording_to_entry(&mut entry, &mut token_buffer, "udlr", Vec::new()); assert_eq!(entry.tokens, "udlr"); assert_eq!(entry.label, "Gesture UDLR"); @@ -1306,10 +1320,10 @@ mod tests { #[test] fn customized_label_is_preserved_when_new_recording_is_applied() { - let mut dlg = MgGesturesDialog::default(); let mut entry = gesture("My Favorite Gesture", "UD"); - dlg.apply_recording_to_entry(&mut entry, "LR"); + let mut token_buffer = String::new(); + apply_recording_to_entry(&mut entry, &mut token_buffer, "LR", Vec::new()); assert_eq!(entry.tokens, "LR"); assert_eq!(entry.label, "My Favorite Gesture");