@@ -6,6 +6,7 @@ use crate::mouse_gestures::db::{
66} ;
77use crate :: mouse_gestures:: engine:: { DirMode , GestureTracker } ;
88use crate :: mouse_gestures:: service:: MouseGestureConfig ;
9+ use crate :: plugin:: Plugin ;
910use eframe:: egui;
1011
1112#[ derive( Debug , Clone , Copy ) ]
@@ -122,6 +123,125 @@ fn stroke_points_in_rect(stroke: &[[i16; 2]], rect: egui::Rect) -> Vec<egui::Pos
122123 . collect ( )
123124}
124125
126+ const AUTO_LABEL_PREFIX : & str = "Gesture" ;
127+ const AUTO_LABEL_TOKEN_MAX : usize = 24 ;
128+
129+ fn normalize_recorded_tokens_for_label ( tokens : & str ) -> String {
130+ tokens
131+ . chars ( )
132+ . filter ( |c| c. is_ascii_alphanumeric ( ) )
133+ . map ( |c| c. to_ascii_uppercase ( ) )
134+ . take ( AUTO_LABEL_TOKEN_MAX )
135+ . collect ( )
136+ }
137+
138+ fn label_from_recorded_tokens ( tokens : & str ) -> Option < String > {
139+ let normalized = normalize_recorded_tokens_for_label ( tokens) ;
140+ if normalized. is_empty ( ) {
141+ None
142+ } else {
143+ Some ( format ! ( "{AUTO_LABEL_PREFIX} {normalized}" ) )
144+ }
145+ }
146+
147+ fn is_default_generated_label ( label : & str ) -> bool {
148+ let trimmed = label. trim ( ) ;
149+ if trimmed. is_empty ( ) {
150+ return true ;
151+ }
152+
153+ let Some ( rest) = trimmed. strip_prefix ( AUTO_LABEL_PREFIX ) else {
154+ return false ;
155+ } ;
156+ let rest = rest. trim ( ) ;
157+ if rest. is_empty ( ) {
158+ return true ;
159+ }
160+
161+ rest. chars ( ) . all ( |c| c. is_ascii_digit ( ) ) || rest == normalize_recorded_tokens_for_label ( rest)
162+ }
163+
164+ fn list_query_prefix_for_plugin ( plugin_name : & str ) -> Option < & ' static str > {
165+ match plugin_name {
166+ "folders" => Some ( "f list" ) ,
167+ "bookmarks" => Some ( "bm list" ) ,
168+ "clipboard" => Some ( "cb list" ) ,
169+ "emoji" => Some ( "emoji list" ) ,
170+ "notes" => Some ( "note list" ) ,
171+ _ => None ,
172+ }
173+ }
174+
175+ fn resolve_action_source ( plugin : & dyn Plugin , filter : & str ) -> Vec < crate :: actions:: Action > {
176+ if let Some ( prefix) = list_query_prefix_for_plugin ( plugin. name ( ) ) {
177+ let query = if filter. trim ( ) . is_empty ( ) {
178+ prefix. to_string ( )
179+ } else {
180+ format ! ( "{prefix} {}" , filter. trim( ) )
181+ } ;
182+ let actions = plugin. search ( & query) ;
183+ if !actions. is_empty ( ) {
184+ return actions;
185+ }
186+ }
187+ plugin. commands ( )
188+ }
189+
190+ fn append_query_args ( query : & str , add_args : & str ) -> String {
191+ let extra = add_args. trim ( ) ;
192+ if extra. is_empty ( ) {
193+ return query. to_string ( ) ;
194+ }
195+ if query. ends_with ( ' ' ) {
196+ format ! ( "{query}{extra}" )
197+ } else {
198+ format ! ( "{query} {extra}" )
199+ }
200+ }
201+
202+ fn apply_recording_to_entry (
203+ entry : & mut GestureEntry ,
204+ token_buffer : & mut String ,
205+ recorded_tokens : & str ,
206+ normalized_stroke : Vec < [ i16 ; 2 ] > ,
207+ ) {
208+ entry. tokens = recorded_tokens. to_string ( ) ;
209+ * token_buffer = entry. tokens . clone ( ) ;
210+ entry. stroke = normalized_stroke;
211+
212+ if is_default_generated_label ( & entry. label ) {
213+ if let Some ( auto_label) = label_from_recorded_tokens ( recorded_tokens) {
214+ entry. label = auto_label;
215+ }
216+ }
217+ }
218+
219+ fn apply_action_pick ( editor : & mut BindingEditor , act : & crate :: actions:: Action , add_args : & str ) {
220+ if let Some ( query) = act. action . strip_prefix ( "query:" ) {
221+ editor. kind = match editor. kind {
222+ BindingKind :: SetQueryAndShow => BindingKind :: SetQueryAndShow ,
223+ BindingKind :: SetQueryAndExecute => BindingKind :: SetQueryAndExecute ,
224+ _ => BindingKind :: SetQuery ,
225+ } ;
226+ editor. action = append_query_args ( query, add_args) ;
227+ editor. args . clear ( ) ;
228+ } else if act. action == "launcher:toggle" {
229+ editor. kind = BindingKind :: ToggleLauncher ;
230+ editor. action . clear ( ) ;
231+ editor. args . clear ( ) ;
232+ } else {
233+ editor. kind = BindingKind :: Execute ;
234+ editor. action = act. action . clone ( ) ;
235+ editor. args = if add_args. trim ( ) . is_empty ( ) {
236+ act. args . clone ( ) . unwrap_or_default ( )
237+ } else {
238+ add_args. trim ( ) . to_string ( )
239+ } ;
240+ }
241+ editor. label = act. label . clone ( ) ;
242+ editor. add_args . clear ( ) ;
243+ }
244+
125245pub struct GestureRecorder {
126246 config : RecorderConfig ,
127247 tracker : GestureTracker ,
@@ -691,43 +811,16 @@ impl MgGesturesDialog {
691811 }
692812 if ui. button ( format ! ( "{} - {}" , act. label, act. desc) ) . clicked ( )
693813 {
694- editor. label = act. label . clone ( ) ;
695- if let Some ( query) = act. action . strip_prefix ( "query:" ) {
696- editor. kind = match editor. kind {
697- BindingKind :: SetQueryAndShow => {
698- BindingKind :: SetQueryAndShow
699- }
700- BindingKind :: SetQueryAndExecute => {
701- BindingKind :: SetQueryAndExecute
702- }
703- _ => BindingKind :: SetQuery ,
704- } ;
705- editor. action = query. to_string ( ) ;
706- editor. args . clear ( ) ;
707- } else if act. action == "launcher:toggle" {
708- editor. kind = BindingKind :: ToggleLauncher ;
709- editor. action . clear ( ) ;
710- editor. args . clear ( ) ;
711- } else {
712- editor. kind = BindingKind :: Execute ;
713- editor. action = act. action . clone ( ) ;
714- editor. args = act. args . clone ( ) . unwrap_or_default ( ) ;
715- }
716- editor. add_args . clear ( ) ;
814+ apply_action_pick ( editor, act, "" ) ;
717815 }
718816 }
719817 } ) ;
720818 } else if let Some ( plugin) =
721819 app. plugins . iter ( ) . find ( |p| p. name ( ) == editor. add_plugin )
722820 {
723821 let filter = editor. add_filter . trim ( ) . to_lowercase ( ) ;
724- let mut actions = if plugin. name ( ) == "folders" {
725- plugin. search ( & format ! ( "f list {}" , editor. add_filter) )
726- } else if plugin. name ( ) == "bookmarks" {
727- plugin. search ( & format ! ( "bm list {}" , editor. add_filter) )
728- } else {
729- plugin. commands ( )
730- } ;
822+ let mut actions =
823+ resolve_action_source ( plugin. as_ref ( ) , & editor. add_filter ) ;
731824 egui:: ScrollArea :: vertical ( )
732825 . id_source ( "mg_binding_action_list" )
733826 . max_height ( 160.0 )
@@ -742,41 +835,8 @@ impl MgGesturesDialog {
742835 }
743836 if ui. button ( format ! ( "{} - {}" , act. label, act. desc) ) . clicked ( )
744837 {
745- let args = if editor. add_args . trim ( ) . is_empty ( ) {
746- None
747- } else {
748- Some ( editor. add_args . clone ( ) )
749- } ;
750-
751- if let Some ( query) = act. action . strip_prefix ( "query:" ) {
752- let mut query = query. to_string ( ) ;
753- if let Some ( ref a) = args {
754- query. push_str ( a) ;
755- }
756- editor. kind = match editor. kind {
757- BindingKind :: SetQueryAndShow => {
758- BindingKind :: SetQueryAndShow
759- }
760- BindingKind :: SetQueryAndExecute => {
761- BindingKind :: SetQueryAndExecute
762- }
763- _ => BindingKind :: SetQuery ,
764- } ;
765- editor. action = query;
766- editor. args . clear ( ) ;
767- } else if act. action == "launcher:toggle" {
768- editor. kind = BindingKind :: ToggleLauncher ;
769- editor. action . clear ( ) ;
770- editor. args . clear ( ) ;
771- } else {
772- editor. kind = BindingKind :: Execute ;
773- editor. action = act. action . clone ( ) ;
774- editor. args = args. unwrap_or_else ( || {
775- act. args . clone ( ) . unwrap_or_default ( )
776- } ) ;
777- }
778- editor. label = act. label . clone ( ) ;
779- editor. add_args . clear ( ) ;
838+ let add_args = editor. add_args . clone ( ) ;
839+ apply_action_pick ( editor, & act, & add_args) ;
780840 }
781841 }
782842 } ) ;
@@ -1022,9 +1082,14 @@ impl MgGesturesDialog {
10221082 ) ) ;
10231083 ui. horizontal ( |ui| {
10241084 if ui. button ( "Use Recording" ) . clicked ( ) {
1025- entry. tokens = recorded_tokens. clone ( ) ;
1026- self . token_buffer = entry. tokens . clone ( ) ;
1027- entry. stroke = self . recorder . normalized_stroke ( ) ;
1085+ let normalized_stroke =
1086+ self . recorder . normalized_stroke ( ) ;
1087+ apply_recording_to_entry (
1088+ entry,
1089+ & mut self . token_buffer ,
1090+ & recorded_tokens,
1091+ normalized_stroke,
1092+ ) ;
10281093 self . recorder . reset ( ) ;
10291094 save_now = true ;
10301095 }
@@ -1067,9 +1132,14 @@ impl MgGesturesDialog {
10671132 if response. drag_stopped ( ) {
10681133 let recorded_tokens = self . recorder . tokens_string ( ) ;
10691134 if !recorded_tokens. is_empty ( ) {
1070- entry. tokens = recorded_tokens. clone ( ) ;
1071- self . token_buffer = entry. tokens . clone ( ) ;
1072- entry. stroke = self . recorder . normalized_stroke ( ) ;
1135+ let normalized_stroke =
1136+ self . recorder . normalized_stroke ( ) ;
1137+ apply_recording_to_entry (
1138+ entry,
1139+ & mut self . token_buffer ,
1140+ & recorded_tokens,
1141+ normalized_stroke,
1142+ ) ;
10731143 save_now = true ;
10741144 }
10751145 //Clear the live drawing so the saved preview is shown
@@ -1207,4 +1277,89 @@ mod tests {
12071277 assert_eq ! ( dlg. selected_idx, Some ( 1 ) ) ;
12081278 assert_eq ! ( dlg. rename_idx, Some ( 1 ) ) ;
12091279 }
1280+
1281+ #[ derive( Clone ) ]
1282+ struct MockPlugin {
1283+ name : String ,
1284+ search_results : Vec < crate :: actions:: Action > ,
1285+ command_results : Vec < crate :: actions:: Action > ,
1286+ }
1287+
1288+ impl crate :: plugin:: Plugin for MockPlugin {
1289+ fn search ( & self , _query : & str ) -> Vec < crate :: actions:: Action > {
1290+ self . search_results . clone ( )
1291+ }
1292+
1293+ fn name ( & self ) -> & str {
1294+ & self . name
1295+ }
1296+
1297+ fn description ( & self ) -> & str {
1298+ "mock"
1299+ }
1300+
1301+ fn capabilities ( & self ) -> & [ & str ] {
1302+ & [ ]
1303+ }
1304+
1305+ fn commands ( & self ) -> Vec < crate :: actions:: Action > {
1306+ self . command_results . clone ( )
1307+ }
1308+ }
1309+
1310+ #[ test]
1311+ fn auto_label_is_applied_only_for_default_generated_labels ( ) {
1312+ let mut entry = gesture ( "Gesture 3" , "" ) ;
1313+
1314+ let mut token_buffer = String :: new ( ) ;
1315+ apply_recording_to_entry ( & mut entry, & mut token_buffer, "udlr" , Vec :: new ( ) ) ;
1316+
1317+ assert_eq ! ( entry. tokens, "udlr" ) ;
1318+ assert_eq ! ( entry. label, "Gesture UDLR" ) ;
1319+ }
1320+
1321+ #[ test]
1322+ fn customized_label_is_preserved_when_new_recording_is_applied ( ) {
1323+ let mut entry = gesture ( "My Favorite Gesture" , "UD" ) ;
1324+
1325+ let mut token_buffer = String :: new ( ) ;
1326+ apply_recording_to_entry ( & mut entry, & mut token_buffer, "LR" , Vec :: new ( ) ) ;
1327+
1328+ assert_eq ! ( entry. tokens, "LR" ) ;
1329+ assert_eq ! ( entry. label, "My Favorite Gesture" ) ;
1330+ }
1331+
1332+ #[ test]
1333+ fn resolve_action_source_prefers_mapped_list_search_and_falls_back_to_commands ( ) {
1334+ let list_action = crate :: actions:: Action {
1335+ label : "Clipboard Entry" . into ( ) ,
1336+ desc : "Clipboard" . into ( ) ,
1337+ action : "clipboard:copy:1" . into ( ) ,
1338+ args : None ,
1339+ } ;
1340+ let command_action = crate :: actions:: Action {
1341+ label : "cb list" . into ( ) ,
1342+ desc : "Clipboard" . into ( ) ,
1343+ action : "query:cb list" . into ( ) ,
1344+ args : None ,
1345+ } ;
1346+
1347+ let mapped_plugin = MockPlugin {
1348+ name : "clipboard" . into ( ) ,
1349+ search_results : vec ! [ list_action. clone( ) ] ,
1350+ command_results : vec ! [ command_action. clone( ) ] ,
1351+ } ;
1352+ let mapped_actions = resolve_action_source ( & mapped_plugin, "abc" ) ;
1353+ assert_eq ! ( mapped_actions. len( ) , 1 ) ;
1354+ assert_eq ! ( mapped_actions[ 0 ] . action, "clipboard:copy:1" ) ;
1355+
1356+ let fallback_plugin = MockPlugin {
1357+ name : "custom" . into ( ) ,
1358+ search_results : vec ! [ list_action] ,
1359+ command_results : vec ! [ command_action. clone( ) ] ,
1360+ } ;
1361+ let fallback_actions = resolve_action_source ( & fallback_plugin, "abc" ) ;
1362+ assert_eq ! ( fallback_actions. len( ) , 1 ) ;
1363+ assert_eq ! ( fallback_actions[ 0 ] . action, command_action. action) ;
1364+ }
12101365}
0 commit comments