Browse / github.com/storytold/gridcraft / patch
feat(ui): add Tools › Customize Keyboard dialog and custom shortcut keymap
[Feature Request] Tools › Customize Keyboard dialog and custom shortcut keymap ### Summary Users need the ability to customize keyboard shortcuts to match their workflow and muscle memory. On macOS, this feature is standard in the top application menu bar under `Tools > Customize Keyboard...`. Currently, GridCraft defines built-in keyboard shortcuts in `crates/ui-egui/src/keymap.rs`, but does not provide an in-app dialog or keymap configuration to view and reassign shortcuts to ribbon/menu commands. ### Proposed Solution 1. In `Tools` menu (and via command/dialog `dialog:customizeKeyboard`): - Add a `Customize Keyboard…` item. 2. In the Customize Keyboard dialog:
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- piyushgoel2808 (imported from pull request #2581, open)
- Base
f86aec12dabc744b5f75e1033f87bfe685e78481- Size
- +540 −3 in 6 files
- Review
- pending
- Shared
- 2026-10-28
- Id
e645e129eccc6a4643817d9f
How it went
No other operator has reported using it yet.
Diff
raw · apps/gridcraft/src/native_menu.rs crates/ui-egui/src/dialogs.rs crates/ui-egui/src/grid.rs crates/ui-egui/src/keymap.rs crates/ui-egui/src/lib.rs crates/ui-egui/tests/it/keyboard_shortcuts.rs
@@ -143,6 +143,7 @@ fn tree() -> Vec<(&'static str, Vec<Entry>)> { ("Evaluate Formula…", "dialog:evaluateFormula", None), ("-", "", None), ("Record Actions", "automate.recordActions", None),+ ("Customize Keyboard…", "dialog:customizeKeyboard", None), ("Command Palette…", "dialog:commandSearch", Some("CMD+SHIFT+P")), ], ),@@ -620,6 +620,16 @@ impl Dialog { d } "solverResults" => Dialog::custom("solverResults", "Solver Results", params.clone()),+ "customizeKeyboard" => Dialog::custom(+ "customizeKeyboard",+ "Customize Keyboard",+ json!({+ "category": "Home",+ "command": "home.bold",+ "current": "",+ "pressed": "",+ }),+ ), "subtotal" => Dialog::form( "subtotal", "Subtotal",@@ -1262,7 +1272,7 @@ pub fn show(app: &mut SheetApp, ctx: &egui::Context) { let width = match d.name.as_str() { "formatCells" => 560.0, "about" => 660.0,- "insertFunction" | "commandSearch" | "nameManager" | "manageRules" | "journal" | "agents" | "macros" => 520.0,+ "insertFunction" | "commandSearch" | "nameManager" | "manageRules" | "journal" | "agents" | "macros" | "customizeKeyboard" => 520.0, "recommendedCharts" => 600.0, "analysisToolPak" => 420.0, "solver" => 460.0,@@ -1307,6 +1317,7 @@ pub fn show(app: &mut SheetApp, ctx: &egui::Context) { "solver" => solver(ui, &mut d, &mut confirm, &mut open), "solverResults" => solver_results(app, ui, &mut d, &mut open), "spelling" => spelling(app, ui, &mut d, &mut open),+ "customizeKeyboard" => customize_keyboard(app, ui, &mut d, &mut open), "agents" => { ui.label("GridCraft is fully drivable by agents. Every menu item, button and gesture is a command:"); ui.add_space(4.0);@@ -3308,6 +3319,170 @@ fn spelling(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, open: &mut bo }); } +/// Dialog allowing users to customize keyboard shortcuts for ribbon and menu commands.+fn customize_keyboard(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, open: &mut bool) {+ let categories = ["Home", "Insert", "Page Layout", "Formulas", "Data", "Review", "View", "Automate", "All Commands"];+ let cat = d.values.get("category").and_then(Json::as_str).unwrap_or("Home").to_string();+ let mut pressed = d.values.get("pressed").and_then(Json::as_str).unwrap_or("").to_string();++ ui.horizontal_top(|ui| {+ // Left Column: Categories+ ui.vertical(|ui| {+ ui.label(egui::RichText::new("Categories:").strong());+ egui::ScrollArea::vertical().id_salt("cat_scroll").max_height(160.0).min_scrolled_width(140.0).show(ui, |ui| {+ for &c in &categories {+ let selected = cat == c;+ if ui.selectable_label(selected, c).clicked() {+ d.values.insert("category".into(), json!(c));+ d.values.insert("command".into(), json!(""));+ }+ }+ });+ });++ ui.add_space(8.0);++ // Refresh category and current_cmd after category interaction+ let cat = d.values.get("category").and_then(Json::as_str).unwrap_or("Home").to_string();+ let current_cmd = d.values.get("command").and_then(Json::as_str).unwrap_or("").to_string();++ // Right Column: Commands+ ui.vertical(|ui| {+ ui.label(egui::RichText::new("Commands:").strong());+ let all_specs = gridcraft_engine::command_specs();+ let filtered: Vec<&gridcraft_engine::cmd::CommandSpec> =+ all_specs.iter().filter(|s| if cat == "All Commands" { true } else { s.menu.first().is_some_and(|m| *m == cat) }).collect();++ let active_cmd = if current_cmd.is_empty() { filtered.first().map(|s| s.id).unwrap_or("") } else { ¤t_cmd };++ egui::ScrollArea::vertical().id_salt("cmd_scroll").max_height(160.0).min_scrolled_width(220.0).show(ui, |ui| {+ for s in &filtered {+ let selected = active_cmd == s.id;+ if ui.selectable_label(selected, s.label).clicked() {+ d.values.insert("command".into(), json!(s.id));+ }+ }+ });++ if current_cmd.is_empty() && !active_cmd.is_empty() {+ d.values.insert("command".into(), json!(active_cmd));+ }+ });+ });++ ui.separator();++ let sel_id = d.values.get("command").and_then(Json::as_str).unwrap_or("").to_string();+ let sel_spec = gridcraft_engine::find_command(&sel_id);++ // Current shortcuts for this command+ let mut current_keys: Vec<String> = Vec::new();+ if let Some(spec) = sel_spec+ && let Some(sc) = spec.shortcut+ && !app.ui.shortcuts.get(sc).is_some_and(|t| t != &sel_id)+ {+ current_keys.push(sc.to_string());+ }+ for (k, target) in &app.ui.shortcuts {+ if target == &sel_id && !current_keys.contains(k) {+ current_keys.push(k.clone());+ }+ }++ ui.horizontal(|ui| {+ ui.vertical(|ui| {+ ui.label("Current keys:");+ if current_keys.is_empty() {+ ui.weak("[None]");+ } else {+ for k in ¤t_keys {+ ui.monospace(k);+ }+ }+ });++ ui.add_space(20.0);++ ui.vertical(|ui| {+ let mac = ui.ctx().os() == egui::os::OperatingSystem::Mac;+ ui.label("Press new keyboard shortcut:");+ let r = ui.add(egui::TextEdit::singleline(&mut pressed).hint_text("e.g. Control+Option+E, Ctrl+Shift+F"));+ if r.changed() {+ d.values.insert("pressed".into(), json!(pressed.clone()));+ }++ if r.has_focus() {+ ui.input(|i| {+ for e in &i.events {+ if let egui::Event::Key { key, pressed: true, modifiers, .. } = e+ && let Some(combo) = crate::keymap::canonical_combo(*key, *modifiers, mac)+ {+ d.values.insert("pressed".into(), json!(combo));+ }+ }+ });+ }++ let p_val = d.values.get("pressed").and_then(Json::as_str).unwrap_or("");+ let norm_p_val = crate::keymap::normalize_for_platform(p_val, mac);+ ui.horizontal(|ui| {+ ui.label("Currently assigned to:");+ if p_val.is_empty() {+ ui.weak("[unassigned]");+ } else if let Some(assigned_to) = norm_p_val.as_ref().and_then(|norm| app.ui.shortcuts.get(norm)) {+ let lbl = gridcraft_engine::find_command(assigned_to).map(|s| s.label).unwrap_or(assigned_to);+ ui.label(egui::RichText::new(lbl).strong());+ } else {+ let mut found = None;+ for spec in gridcraft_engine::command_specs() {+ if let Some(sc) = spec.shortcut+ && (sc.eq_ignore_ascii_case(p_val) || norm_p_val.as_deref().is_some_and(|n| sc.eq_ignore_ascii_case(n)))+ && !app.ui.shortcuts.contains_key(sc)+ {+ found = Some(spec.label);+ break;+ }+ }+ if let Some(lbl) = found {+ ui.label(egui::RichText::new(lbl).strong());+ } else {+ ui.weak("[unassigned]");+ }+ }+ });+ });+ });++ ui.add_space(8.0);+ ui.horizontal(|ui| {+ let mac = ui.ctx().os() == egui::os::OperatingSystem::Mac;+ let p_val = d.values.get("pressed").and_then(Json::as_str).unwrap_or("").to_string();+ let can_assign = !sel_id.is_empty() && !p_val.is_empty();+ if ui.add_enabled(can_assign, egui::Button::new("Assign")).clicked()+ && let Some(norm) = crate::keymap::normalize_for_platform(&p_val, mac)+ {+ app.ui.shortcuts.insert(norm, sel_id.clone());+ d.values.insert("pressed".into(), json!(""));+ }+ let can_remove = current_keys.iter().any(|k| app.ui.shortcuts.get(k) == Some(&sel_id));+ if ui.add_enabled(can_remove, egui::Button::new("Remove")).clicked() {+ for k in ¤t_keys {+ if app.ui.shortcuts.get(k) == Some(&sel_id) {+ app.ui.shortcuts.remove(k);+ }+ }+ }+ if ui.add_enabled(!app.ui.shortcuts.is_empty(), egui::Button::new("Reset All…")).clicked() {+ app.ui.shortcuts.clear();+ }+ ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {+ if ui.button("OK").clicked() {+ *open = false;+ }+ });+ });+}+ #[cfg(test)] mod data_validation_tests { use super::*;@@ -3409,4 +3584,15 @@ mod tests { assert_eq!(d.values["type"], "list"); assert!(d.values.get("formula1").is_none()); }++ #[test]+ fn customize_keyboard_opens_with_default_category_and_command() {+ let mut session = gridcraft_engine::Session::default();+ session.new_workbook();+ let mut app = SheetApp::new(session, Default::default());+ let d = Dialog::open(&mut app, "customizeKeyboard", json!({})).unwrap();+ assert_eq!(d.title, "Customize Keyboard");+ assert_eq!(d.values["category"], "Home");+ assert_eq!(d.values["command"], "home.bold");+ } }@@ -3037,10 +3037,38 @@ fn keyboard(app: &mut SheetApp, ctx: &egui::Context, resp: &egui::Response, geo: for ev in events { match ev { egui::Event::Copy => {- app.copy_to_clipboard(ctx, "edit.copy");+ let target = if ctx.input(|i| i.key_down(egui::Key::Insert)) {+ app.ui.shortcuts.get("Ctrl+Insert").cloned()+ } else if ctx.input(|i| i.key_down(egui::Key::C)) || !ctx.input(|i| i.modifiers.ctrl || i.modifiers.command) {+ let mac = ctx.os() == egui::os::OperatingSystem::Mac;+ let copy_combo = if mac { "Cmd+C" } else { "Ctrl+C" };+ app.ui.shortcuts.get(copy_combo).cloned()+ } else {+ None+ };+ if let Some(target) = target {+ crate::keymap::dispatch_target(app, ctx, &target);+ shortcut_ran = true;+ } else {+ app.copy_to_clipboard(ctx, "edit.copy");+ } } egui::Event::Cut => {- app.copy_to_clipboard(ctx, "edit.cut");+ let target = if ctx.input(|i| i.key_down(egui::Key::Delete)) {+ app.ui.shortcuts.get("Shift+Delete").cloned()+ } else if ctx.input(|i| i.key_down(egui::Key::X)) || !ctx.input(|i| i.modifiers.ctrl || i.modifiers.command) {+ let mac = ctx.os() == egui::os::OperatingSystem::Mac;+ let cut_combo = if mac { "Cmd+X" } else { "Ctrl+X" };+ app.ui.shortcuts.get(cut_combo).cloned()+ } else {+ None+ };+ if let Some(target) = target {+ crate::keymap::dispatch_target(app, ctx, &target);+ shortcut_ran = true;+ } else {+ app.copy_to_clipboard(ctx, "edit.cut");+ } } egui::Event::Paste(text) => app.paste_external(text), egui::Event::Text(_) if shortcut_pressed || shortcut_ran => {}@@ -178,12 +178,253 @@ pub(crate) fn altgr_text(ctx: &egui::Context, m: Modifiers, mac: bool) -> bool { !mac && m.alt && m.ctrl && ctx.input(|i| i.events.iter().any(|e| matches!(e, egui::Event::Text(_)))) } +/// Canonical name for an egui Key.+pub fn key_name(k: Key) -> Option<&'static str> {+ Some(match k {+ Key::ArrowLeft => "Left",+ Key::ArrowRight => "Right",+ Key::ArrowUp => "Up",+ Key::ArrowDown => "Down",+ Key::Home => "Home",+ Key::End => "End",+ Key::PageUp => "PageUp",+ Key::PageDown => "PageDown",+ Key::Enter => "Enter",+ Key::Tab => "Tab",+ Key::Backspace => "Backspace",+ Key::Delete => "Delete",+ Key::Escape => "Escape",+ Key::Space => "Space",+ Key::Insert => "Insert",+ Key::Equals | Key::Plus => "=",+ Key::Minus => "-",+ Key::Period => ".",+ Key::Comma => ",",+ Key::OpenBracket => "[",+ Key::CloseBracket => "]",+ Key::Semicolon => ";",+ Key::Quote => "'",+ Key::Backtick => "`",+ Key::Num0 => "0",+ Key::Num1 => "1",+ Key::Num2 => "2",+ Key::Num3 => "3",+ Key::Num4 => "4",+ Key::Num5 => "5",+ Key::Num6 => "6",+ Key::Num7 => "7",+ Key::Num8 => "8",+ Key::Num9 => "9",+ Key::F1 => "F1",+ Key::F2 => "F2",+ Key::F3 => "F3",+ Key::F4 => "F4",+ Key::F5 => "F5",+ Key::F6 => "F6",+ Key::F7 => "F7",+ Key::F8 => "F8",+ Key::F9 => "F9",+ Key::F10 => "F10",+ Key::F11 => "F11",+ Key::F12 => "F12",+ Key::A => "A",+ Key::B => "B",+ Key::C => "C",+ Key::D => "D",+ Key::E => "E",+ Key::F => "F",+ Key::G => "G",+ Key::H => "H",+ Key::I => "I",+ Key::J => "J",+ Key::K => "K",+ Key::L => "L",+ Key::M => "M",+ Key::N => "N",+ Key::O => "O",+ Key::P => "P",+ Key::Q => "Q",+ Key::R => "R",+ Key::S => "S",+ Key::T => "T",+ Key::U => "U",+ Key::V => "V",+ Key::W => "W",+ Key::X => "X",+ Key::Y => "Y",+ Key::Z => "Z",+ _ => return None,+ })+}++/// Canonical representation of a key combination, e.g. "Ctrl+Shift+E" or "Cmd+Option+E".+pub fn canonical_combo(key: Key, m: Modifiers, mac: bool) -> Option<String> {+ let name = key_name(key)?;+ let is_function_key =+ matches!(key, Key::F1 | Key::F2 | Key::F3 | Key::F4 | Key::F5 | Key::F6 | Key::F7 | Key::F8 | Key::F9 | Key::F10 | Key::F11 | Key::F12);+ if !m.ctrl && !m.command && !m.alt && !m.shift && !is_function_key {+ return None;+ }+ let mut s = String::new();+ if mac {+ if m.command || m.mac_cmd {+ s.push_str("Cmd+");+ }+ if m.ctrl {+ s.push_str("Ctrl+");+ }+ if m.alt {+ s.push_str("Option+");+ }+ if m.shift {+ s.push_str("Shift+");+ }+ } else {+ if m.ctrl || m.command {+ s.push_str("Ctrl+");+ }+ if m.alt {+ s.push_str("Alt+");+ }+ if m.shift {+ s.push_str("Shift+");+ }+ }+ s.push_str(name);+ Some(s)+}++fn canonical_key_name(s: &str) -> Option<String> {+ if s.len() == 1 {+ let c = s.chars().next()?.to_ascii_uppercase();+ if c.is_ascii_alphanumeric() || matches!(c, '=' | '-' | '.' | ',' | '[' | ']' | ';' | '\'' | '`') {+ return Some(c.to_string());+ }+ return None;+ }+ if let Some(f) = s.strip_prefix(['f', 'F'])+ && let Ok(n) = f.parse::<u8>()+ && (1..=12).contains(&n)+ {+ return Some(format!("F{n}"));+ }+ match s.to_ascii_lowercase().as_str() {+ "left" | "arrowleft" => Some("Left".into()),+ "right" | "arrowright" => Some("Right".into()),+ "up" | "arrowup" => Some("Up".into()),+ "down" | "arrowdown" => Some("Down".into()),+ "home" => Some("Home".into()),+ "end" => Some("End".into()),+ "pageup" | "pgup" => Some("PageUp".into()),+ "pagedown" | "pgdn" => Some("PageDown".into()),+ "enter" | "return" => Some("Enter".into()),+ "tab" => Some("Tab".into()),+ "backspace" => Some("Backspace".into()),+ "delete" | "del" => Some("Delete".into()),+ "escape" | "esc" => Some("Escape".into()),+ "space" => Some("Space".into()),+ "insert" | "ins" => Some("Insert".into()),+ _ => None,+ }+}++/// Normalizes any user-entered shortcut string for the specified platform.+pub fn normalize_for_platform(s: &str, mac: bool) -> Option<String> {+ let s = s.trim();+ if s.is_empty() {+ return None;+ }+ let parts: Vec<&str> = s.split('+').map(str::trim).collect();+ if parts.is_empty() {+ return None;+ }+ let mut ctrl = false;+ let mut alt = false;+ let mut shift = false;+ let mut cmd = false;+ let key_part = parts.last()?;+ for p in &parts[..parts.len() - 1] {+ match p.to_ascii_lowercase().as_str() {+ "ctrl" | "control" => ctrl = true,+ "alt" | "option" | "opt" => alt = true,+ "shift" => shift = true,+ "cmd" | "command" | "mod" => cmd = true,+ _ => return None,+ }+ }+ let key_name = canonical_key_name(key_part)?;+ let is_function_key = key_name.starts_with('F') && key_name[1..].parse::<u8>().is_ok();+ if !ctrl && !alt && !shift && !cmd && !is_function_key {+ return None;+ }+ let mut out = String::new();+ if mac {+ if cmd {+ out.push_str("Cmd+");+ }+ if ctrl {+ out.push_str("Ctrl+");+ }+ if alt {+ out.push_str("Option+");+ }+ if shift {+ out.push_str("Shift+");+ }+ } else {+ if cmd || ctrl {+ out.push_str("Ctrl+");+ }+ if alt {+ out.push_str("Alt+");+ }+ if shift {+ out.push_str("Shift+");+ }+ }+ out.push_str(&key_name);+ Some(out)+}++/// Normalizes any user-entered shortcut string (case-insensitively, e.g. "ctrl+shift+f" -> "Ctrl+Shift+F").+pub fn normalize(s: &str) -> Option<String> {+ normalize_for_platform(s, cfg!(target_os = "macos"))+}++pub(crate) fn dispatch_target(app: &mut SheetApp, ctx: &egui::Context, target: &str) {+ if let Some(dialog_name) = target.strip_prefix("dialog:") {+ app.open_dialog(dialog_name, json!({}));+ } else if matches!(target, "edit.copy" | "edit.cut") {+ app.copy_to_clipboard(ctx, target);+ } else if target == "view.zoom" {+ app.open_dialog("zoom", json!({}));+ } else {+ let params = match target {+ "home.borders" => {+ let preset = if app.grid.last_border.is_empty() { "all" } else { &app.grid.last_border };+ json!({"preset": preset})+ }+ _ => json!({}),+ };+ app.run_or_alert(target, params);+ }+ app.grid.ensure_visible = true;+}+ /// Handles a key press in the grid. Returns whether it was a shortcut. pub(crate) fn dispatch(app: &mut SheetApp, ctx: &egui::Context, key: Key, physical: Option<Key>, m: Modifiers, geo: &Geo, sh: &Sheet) -> bool { let mac = ctx.os() == egui::os::OperatingSystem::Mac; if altgr_text(ctx, m, mac) { return false; }+ // Check custom shortcuts before built-in shortcuts+ if let Some(combo) = canonical_combo(key, m, mac)+ && let Some(target) =+ app.ui.shortcuts.get(&combo).or_else(|| normalize_for_platform(&combo, mac).as_ref().and_then(|nc| app.ui.shortcuts.get(nc))).cloned()+ {+ dispatch_target(app, ctx, &target);+ return true;+ } let Some(shortcut) = lookup(key, physical, m, mac) else { return false }; let (active, anchor) = app.session.active().map(|d| (d.selection.active, d.selection.anchor)).unwrap_or_default(); match shortcut {@@ -267,3 +508,35 @@ fn extend(app: &mut SheetApp, anchor: CellRef, active: CellRef, to: CellRef) { app.grid.ensure_visible = true; app.grid.reveal_moving_end = true; }++#[cfg(test)]+mod tests {+ use super::*;++ #[test]+ fn canonical_combo_formats_keys() {+ let mac_cmd = Modifiers { mac_cmd: true, command: true, ..Modifiers::NONE };+ let ctrl_shift = Modifiers { ctrl: true, shift: true, command: true, ..Modifiers::NONE };+ assert_eq!(canonical_combo(Key::B, mac_cmd, true), Some("Cmd+B".into()));+ assert_eq!(canonical_combo(Key::K, ctrl_shift, false), Some("Ctrl+Shift+K".into()));+ assert_eq!(canonical_combo(Key::F5, Modifiers::NONE, false), Some("F5".into()));+ assert_eq!(canonical_combo(Key::F, Modifiers::NONE, false), None);+ }++ #[test]+ fn normalize_shortcuts() {+ assert_eq!(normalize_for_platform("ctrl+shift+k", false), Some("Ctrl+Shift+K".into()));+ assert_eq!(normalize_for_platform("cmd+b", false), Some("Ctrl+B".into()));+ assert_eq!(normalize_for_platform("cmd+b", true), Some("Cmd+B".into()));+ assert_eq!(normalize_for_platform("alt+k", true), Some("Option+K".into()));+ assert_eq!(normalize_for_platform("option+k", false), Some("Alt+K".into()));+ assert_eq!(normalize_for_platform("mod+shift+m", true), Some("Cmd+Shift+M".into()));+ assert_eq!(normalize_for_platform("ctrl + alt + f1", false), Some("Ctrl+Alt+F1".into()));+ assert_eq!(normalize_for_platform("ctrl+pageup", false), Some("Ctrl+PageUp".into()));+ assert_eq!(normalize_for_platform("ctrl+pagedown", false), Some("Ctrl+PageDown".into()));+ assert_eq!(normalize_for_platform("ctrl+whatever", false), None);+ assert_eq!(normalize_for_platform("ctrl+f13", false), None);+ assert_eq!(normalize_for_platform("f", false), None);+ assert_eq!(normalize_for_platform("f2", false), Some("F2".into()));+ }+}@@ -81,6 +81,9 @@ pub struct UiState { /// [`INTERFACE_SCALE_MIN`]..=[`INTERFACE_SCALE_MAX`]; a malformed saved value reads as 100. #[serde(deserialize_with = "lenient_interface_scale")] pub interface_scale: u16,+ /// Custom keyboard shortcut bindings (canonical shortcut -> command id).+ #[serde(default)]+ pub shortcuts: std::collections::HashMap<String, String>, } pub const INTERFACE_SCALE_MIN: u16 = 75;@@ -113,6 +116,7 @@ impl Default for UiState { keytip_invoke: None, language: i18n::Language::En, interface_scale: 100,+ shortcuts: std::collections::HashMap::new(), } } }@@ -306,3 +306,48 @@ fn ctrl_alt_letter_that_typed_text_is_not_select_all() { assert_eq!(h.state().session.active().unwrap().selection.current().a1(), "B2"); assert_eq!(h.state().editor.as_ref().unwrap().text, "\u{e4}"); }++#[test]+fn custom_shortcut_triggers_command_and_dialog() {+ let mut h = harness(book("A1:B2"));+ let mac = h.ctx.os() == egui::os::OperatingSystem::Mac;+ let combo = if mac { "Cmd+Shift+K" } else { "Ctrl+Shift+K" };+ let mods = if mac { Modifiers { mac_cmd: true, command: true, shift: true, ..Modifiers::NONE } } else { CTRL_SHIFT };+ h.state_mut().ui.shortcuts.insert(combo.into(), "dialog:customizeKeyboard".into());+ key(&mut h, Key::K, mods);+ assert_eq!(h.state().dialog.as_ref().map(|d| d.name.as_str()), Some("customizeKeyboard"));+ h.state_mut().dialog = None;++ // Configurable commands like home.borders receive default parameters+ let border_combo = if mac { "Cmd+Shift+B" } else { "Ctrl+Shift+B" };+ h.state_mut().ui.shortcuts.insert(border_combo.into(), "home.borders".into());+ key(&mut h, Key::B, mods);+ assert!(style(&h, "B2").borders.bottom.style != BorderStyle::None);++ // view.zoom opens the zoom dialog without error+ let zoom_combo = if mac { "Cmd+Shift+Z" } else { "Ctrl+Shift+Z" };+ h.state_mut().ui.shortcuts.insert(zoom_combo.into(), "view.zoom".into());+ key(&mut h, Key::Z, mods);+ assert_eq!(h.state().dialog.as_ref().map(|d| d.name.as_str()), Some("zoom"));+ h.state_mut().dialog = None;++ // Native copy event checks custom shortcuts before clipboard fallback+ let copy_combo = if mac { "Cmd+C" } else { "Ctrl+C" };+ h.state_mut().ui.shortcuts.insert(copy_combo.into(), "dialog:customizeKeyboard".into());+ h.input_mut().events.push(Event::Copy);+ h.run_steps(1);+ assert_eq!(h.state().dialog.as_ref().map(|d| d.name.as_str()), Some("customizeKeyboard"));+ h.state_mut().dialog = None;++ // Alternate clipboard keys (Insert held) stay on clipboard path, not triggering Cmd/Ctrl+C target+ h.input_mut().events.push(Event::Key {+ key: Key::Insert,+ physical_key: Some(Key::Insert),+ pressed: true,+ repeat: false,+ modifiers: Modifiers::CTRL,+ });+ h.input_mut().events.push(Event::Copy);+ h.run_steps(1);+ assert_eq!(h.state().dialog.as_ref().map(|d| d.name.as_str()), None);+}