@@ -84,6 +84,29 @@ pub fn parse_keystroke(s: &str) -> Option<ParsedKeystroke> {
8484 } )
8585}
8686
87+ fn format_chord_string ( chord : & Chord ) -> String {
88+ chord
89+ . iter ( )
90+ . map ( |ks| {
91+ let mut parts = Vec :: new ( ) ;
92+ if ks. ctrl {
93+ parts. push ( "ctrl" ) ;
94+ }
95+ if ks. alt {
96+ parts. push ( "alt" ) ;
97+ }
98+ if ks. shift {
99+ parts. push ( "shift" ) ;
100+ }
101+ if ks. meta {
102+ parts. push ( "meta" ) ;
103+ }
104+ parts. push ( & ks. key ) ;
105+ parts. join ( "+" )
106+ } )
107+ . collect :: < Vec < _ > > ( )
108+ . join ( " " )
109+ }
87110fn normalize_key ( k : & str ) -> String {
88111 match k {
89112 "esc" | "escape" => "escape" . to_string ( ) ,
@@ -169,6 +192,8 @@ pub fn default_bindings() -> Vec<ParsedBinding> {
169192 ( "end" , "goLineEnd" , KeyContext :: Chat ) ,
170193 ( "cmd+left" , "goLineStart" , KeyContext :: Chat ) ,
171194 ( "cmd+right" , "goLineEnd" , KeyContext :: Chat ) ,
195+ ( "ctrl+left" , "moveWordBackward" , KeyContext :: Chat ) ,
196+ ( "ctrl+right" , "moveWordForward" , KeyContext :: Chat ) ,
172197
173198 // Text Editing (Emacs-style) + app shortcuts
174199 ( "ctrl+a" , "openModelPicker" , KeyContext :: Chat ) ,
@@ -300,12 +325,28 @@ pub fn default_bindings() -> Vec<ParsedBinding> {
300325 . collect ( )
301326}
302327
328+ /// Current schema version for keybindings
329+ pub const KEYBINDINGS_SCHEMA_VERSION : u32 = 1 ;
303330/// User keybindings loaded from ~/.claurst/keybindings.json
304- #[ derive( Debug , Clone , Serialize , Deserialize , Default ) ]
331+ #[ derive( Debug , Clone , Serialize , Deserialize ) ]
305332pub struct UserKeybindings {
333+ #[ serde( default = "default_schema_version" ) ]
334+ pub schema_version : u32 ,
306335 pub bindings : Vec < UserBinding > ,
307336}
308337
338+ fn default_schema_version ( ) -> u32 {
339+ KEYBINDINGS_SCHEMA_VERSION
340+ }
341+
342+ impl Default for UserKeybindings {
343+ fn default ( ) -> Self {
344+ Self {
345+ schema_version : KEYBINDINGS_SCHEMA_VERSION ,
346+ bindings : Vec :: new ( ) ,
347+ }
348+ }
349+ }
309350#[ derive( Debug , Clone , Serialize , Deserialize ) ]
310351struct JsonKeybindingConfig {
311352 #[ serde( default ) ]
@@ -357,7 +398,18 @@ impl UserKeybindings {
357398 pub fn load ( config_dir : & Path ) -> Self {
358399 let path = config_dir. join ( "keybindings.json" ) ;
359400 if let Ok ( content) = std:: fs:: read_to_string ( & path) {
360- Self :: from_json_str ( & content)
401+ let mut kb = Self :: from_json_str ( & content) ;
402+ let old_version = kb. schema_version ;
403+ kb. smart_merge_with_defaults ( ) ;
404+
405+ // Save back if schema was updated
406+ if kb. schema_version > old_version {
407+ if let Err ( e) = kb. save ( config_dir) {
408+ warn ! ( "Failed to save updated keybindings: {}" , e) ;
409+ }
410+ }
411+
412+ kb
361413 } else {
362414 Self :: default ( )
363415 }
@@ -384,7 +436,68 @@ impl UserKeybindings {
384436 } )
385437 } )
386438 . collect ( ) ;
387- Ok ( Self { bindings } )
439+ Ok ( Self {
440+ schema_version : KEYBINDINGS_SCHEMA_VERSION ,
441+ bindings,
442+ } )
443+ }
444+
445+ /// Smart merge: preserve user customizations while adding new defaults
446+ pub fn smart_merge_with_defaults ( & mut self ) {
447+ if self . schema_version >= KEYBINDINGS_SCHEMA_VERSION {
448+ return ; // Already up to date
449+ }
450+
451+ let old_version = self . schema_version ;
452+ self . schema_version = KEYBINDINGS_SCHEMA_VERSION ;
453+
454+ // Build a set of user-customized bindings (those that differ from old defaults)
455+ // and bindings user explicitly unbound
456+ let mut user_customizations: std:: collections:: HashMap < String , Option < String > > =
457+ std:: collections:: HashMap :: new ( ) ;
458+ for binding in & self . bindings {
459+ user_customizations
460+ . insert ( binding. chord . clone ( ) , binding. action . clone ( ) ) ;
461+ }
462+
463+ // Get current defaults and integrate customizations
464+ let mut merged_bindings = Vec :: new ( ) ;
465+ for default in default_bindings ( ) {
466+ let chord_str = format_chord_string ( & default. chord ) ;
467+ let context_str = format ! ( "{:?}" , default . context) ;
468+
469+ if let Some ( custom_action) = user_customizations. get ( & chord_str) {
470+ // User has customized this binding, use their version
471+ merged_bindings. push ( UserBinding {
472+ chord : chord_str. clone ( ) ,
473+ action : custom_action. clone ( ) ,
474+ context : Some ( context_str) ,
475+ } ) ;
476+ user_customizations. remove ( & chord_str) ;
477+ } else {
478+ // Use the default
479+ merged_bindings. push ( UserBinding {
480+ chord : chord_str,
481+ action : default. action . clone ( ) ,
482+ context : Some ( context_str) ,
483+ } ) ;
484+ }
485+ }
486+
487+ // Add any remaining user customizations that aren't in current defaults
488+ for ( chord, action) in user_customizations {
489+ merged_bindings. push ( UserBinding {
490+ chord,
491+ action,
492+ context : None ,
493+ } ) ;
494+ }
495+
496+ self . bindings = merged_bindings;
497+ warn ! (
498+ "Keybindings schema upgraded from v{} to v{}. User customizations preserved." ,
499+ old_version, KEYBINDINGS_SCHEMA_VERSION
500+ ) ;
388501 }
389502}
390503
0 commit comments