Skip to content

Commit ae85bd8

Browse files
committed
feat(keyboard): schema versioning and smart keybindings merge
Added schema_version tracking to keybindings.json to enable automatic updates when new defaults are added. Smart merge preserves all user customizations while adding new keybindings from updated defaults. When a user's keybindings.json schema is outdated, it's automatically merged with current defaults and saved back. Also added ctrl+left and ctrl+right to defaults for word navigation.
1 parent 0b5947f commit ae85bd8

1 file changed

Lines changed: 116 additions & 3 deletions

File tree

src-rust/crates/core/src/keybindings.rs

Lines changed: 116 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}
87110
fn 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)]
305332
pub 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)]
310351
struct 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

Comments
 (0)