From aaf39ae8370cbf11c9e7d0551b69fba3827bcf5c Mon Sep 17 00:00:00 2001 From: Fahad Sheikh Date: Tue, 15 Sep 2026 18:03:31 +0500 Subject: [PATCH 1/3] feat(gui): add custom-shortcut Tap/Hold recorder to the button picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Action::CustomShortcut and Action::HoldShortcut already existed end-to-end in openlogi-core (parsing, IPC, agent runtime) but had no way to reach them for a physical button binding — the picker's catalog deliberately excludes both, and the only recorder in the app lived inside the Actions Ring slot editor. Button bindings (mouse buttons, the 5-direction gesture map used by HapticPanel/GestureButton) could only pick from the fixed catalog. Add a "Custom shortcut" section to the shared action_library() picker (used by button, inherited-gesture, and gesture-direction inspectors alike), with a Tap/Hold choice and a free-text KeyCombo input mirroring the Ring editor's existing recorder. Commits through the same PickFn every other row already uses, so no new state-commit plumbing. Comes up in practice pairing a haptic-pad button with a third-party hold-a-shortcut launcher (e.g. Droppy's Ring) — currently only reachable by hand-editing config.toml. --- .../src/features/mouse/inspector.rs | 92 +++++++++++++++++-- .../src/features/mouse/view.rs | 36 ++++++-- .../src/features/mouse/view/tests.rs | 2 + crates/openlogi-ui/locales/be.toml | 4 + crates/openlogi-ui/locales/da.toml | 4 + crates/openlogi-ui/locales/de.toml | 4 + crates/openlogi-ui/locales/el.toml | 4 + crates/openlogi-ui/locales/en.toml | 4 + crates/openlogi-ui/locales/es.toml | 4 + crates/openlogi-ui/locales/fi.toml | 4 + crates/openlogi-ui/locales/fr.toml | 4 + crates/openlogi-ui/locales/it.toml | 4 + crates/openlogi-ui/locales/ja.toml | 4 + crates/openlogi-ui/locales/ko.toml | 4 + crates/openlogi-ui/locales/nb.toml | 4 + crates/openlogi-ui/locales/nl.toml | 4 + crates/openlogi-ui/locales/pl.toml | 4 + crates/openlogi-ui/locales/pt-BR.toml | 4 + crates/openlogi-ui/locales/pt-PT.toml | 4 + crates/openlogi-ui/locales/ru.toml | 4 + crates/openlogi-ui/locales/sv.toml | 4 + crates/openlogi-ui/locales/tr.toml | 4 + crates/openlogi-ui/locales/uk.toml | 4 + crates/openlogi-ui/locales/zh-CN.toml | 4 + crates/openlogi-ui/locales/zh-HK.toml | 4 + crates/openlogi-ui/locales/zh-TW.toml | 4 + 26 files changed, 209 insertions(+), 13 deletions(-) diff --git a/crates/openlogi-desktop/src/features/mouse/inspector.rs b/crates/openlogi-desktop/src/features/mouse/inspector.rs index 58c6a7736..73a71ca4d 100644 --- a/crates/openlogi-desktop/src/features/mouse/inspector.rs +++ b/crates/openlogi-desktop/src/features/mouse/inspector.rs @@ -12,7 +12,7 @@ use gpui_component::{ Disableable as _, Icon, IconName, Selectable as _, Sizable as _, button::Button, h_flex, input::InputState, scroll::ScrollableElement as _, v_flex, }; -use openlogi_core::binding::{Action, ButtonId, GestureDirection, default_binding}; +use openlogi_core::binding::{Action, ButtonId, GestureDirection, KeyCombo, default_binding}; use super::hotspots::MouseControlId; use super::thumbwheel::ThumbwheelPreset; @@ -44,12 +44,16 @@ pub(super) struct BindingInspectorData<'a> { struct ActionPickerContext<'a> { open: bool, search: &'a Entity, + shortcut_input: &'a Entity, + shortcut_hold: bool, view: &'a Entity, } pub(super) fn binding_inspector( data: BindingInspectorData<'_>, action_search: &Entity, + shortcut_input: &Entity, + shortcut_hold: bool, view: &Entity, cx: &Context, ) -> gpui::Div { @@ -57,6 +61,8 @@ pub(super) fn binding_inspector( let picker = ActionPickerContext { open: data.action_picker_open, search: action_search, + shortcut_input, + shortcut_hold, view, }; let body = match data.selected { @@ -223,7 +229,7 @@ fn button_inspector( panel.child(action_library( "inspector-action", Some(&action), - picker.search, + picker, &on_pick, pal, cx, @@ -276,7 +282,7 @@ fn inherited_gesture_inspector( panel.child(action_library( "inspector-gesture-override", None, - picker.search, + picker, &on_pick, pal, cx, @@ -337,7 +343,7 @@ fn gesture_inspector( panel.child(action_library( "inspector-gesture-action", Some(¤t), - picker.search, + picker, &on_pick, pal, cx, @@ -646,18 +652,18 @@ fn selection_card( fn action_library( id_prefix: &'static str, current: Option<&Action>, - action_search: &Entity, + picker: ActionPickerContext<'_>, on_pick: &PickFn, pal: Palette, cx: &Context, ) -> impl IntoElement { - let query = action_search.read(cx).value(); + let query = picker.search.read(cx).value(); let rows = action_rows_matching(id_prefix, current, &query, on_pick, pal); v_flex() .gap_2() .pt_1() .child(editor_section(tr!("actions.actions"), pal)) - .child(control_input(action_search).cleanable(true)) + .child(control_input(picker.search).cleanable(true)) .child( v_flex() .gap_0p5() @@ -672,6 +678,78 @@ fn action_library( }) .children(rows), ) + .child(custom_shortcut_editor(picker, on_pick, pal)) +} + +/// Free-text `KeyCombo` recorder shared by every `action_library` call site. +/// Mirrors `action_ring/editor.rs::shortcut_editor`, plus a Hold/Tap choice a +/// ring wedge (selected by releasing over it) has no physical use for. +fn custom_shortcut_editor( + picker: ActionPickerContext<'_>, + on_pick: &PickFn, + pal: Palette, +) -> impl IntoElement { + let hold = picker.shortcut_hold; + let view_tap = picker.view.clone(); + let view_hold = picker.view.clone(); + let submit_input = picker.shortcut_input.clone(); + let on_pick = on_pick.clone(); + + v_flex() + .gap_1() + .child(editor_section(tr!("actions.custom_shortcut"), pal)) + .child( + h_flex() + .gap_2() + .child( + control_button("shortcut-tap") + .selected(!hold) + .label(tr!("actions.shortcut_tap")) + .on_click(move |_, _, cx| { + view_tap.update(cx, |v, cx| { + v.set_shortcut_hold(false); + cx.notify(); + }); + }), + ) + .child( + control_button("shortcut-hold") + .selected(hold) + .label(tr!("actions.shortcut_hold")) + .on_click(move |_, _, cx| { + view_hold.update(cx, |v, cx| { + v.set_shortcut_hold(true); + cx.notify(); + }); + }), + ), + ) + .child( + h_flex() + .gap_2() + .child( + div() + .flex_1() + .min_w_0() + .child(control_input(picker.shortcut_input).cleanable(true)), + ) + .child( + Button::new("shortcut-add") + .compact() + .label(tr!("common.add")) + .on_click(move |_, window, cx| { + let text = submit_input.read(cx).value().to_string(); + if let Ok(combo) = text.parse::() { + let action = if hold { + Action::HoldShortcut(combo) + } else { + Action::CustomShortcut(combo) + }; + (on_pick)(action, window, cx); + } + }), + ), + ) } fn gesture_action( diff --git a/crates/openlogi-desktop/src/features/mouse/view.rs b/crates/openlogi-desktop/src/features/mouse/view.rs index 700e2e9a7..34692f669 100644 --- a/crates/openlogi-desktop/src/features/mouse/view.rs +++ b/crates/openlogi-desktop/src/features/mouse/view.rs @@ -130,6 +130,8 @@ pub struct MouseModelView { gesture_active_dir: Option, action_picker_open: bool, action_search: Entity, + shortcut_input: Entity, + shortcut_hold: bool, _state_obs: Subscription, } @@ -144,6 +146,8 @@ impl MouseModelView { } }) .detach(); + let shortcut_input = cx + .new(|cx| InputState::new(window, cx).placeholder(tr!("actions.shortcut_placeholder"))); let state_obs = AppState::repaint_on(cx, |event| { matches!( event, @@ -160,6 +164,8 @@ impl MouseModelView { gesture_active_dir: None, action_picker_open: false, action_search, + shortcut_input, + shortcut_hold: false, _state_obs: state_obs, } } @@ -179,6 +185,27 @@ impl MouseModelView { self.action_picker_open = false; } + pub(super) fn set_shortcut_hold(&mut self, hold: bool) { + self.shortcut_hold = hold; + } + + /// Re-derive every input's placeholder from the current locale, so a live + /// language switch doesn't leave one stuck with the text it was built with. + fn localize_inputs(&self, window: &mut Window, cx: &mut Context) { + crate::ui::components::localize_placeholder( + &self.action_search, + tr!("actions.search_actions"), + window, + cx, + ); + crate::ui::components::localize_placeholder( + &self.shortcut_input, + tr!("actions.shortcut_placeholder"), + window, + cx, + ); + } + fn reset_for_device(&mut self, device_key: Option) { if self.current_device_key == device_key { return; @@ -223,12 +250,7 @@ fn set_control_hovered( impl Render for MouseModelView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - crate::ui::components::localize_placeholder( - &self.action_search, - tr!("actions.search_actions"), - window, - cx, - ); + self.localize_inputs(window, cx); let (empty_bindings, empty_gesture_maps) = (BTreeMap::new(), BTreeMap::new()); let MouseWorkspaceData { device_key, @@ -321,6 +343,8 @@ impl Render for MouseModelView { overridden, }, &self.action_search, + &self.shortcut_input, + self.shortcut_hold, &view, cx, ); diff --git a/crates/openlogi-desktop/src/features/mouse/view/tests.rs b/crates/openlogi-desktop/src/features/mouse/view/tests.rs index c8563666e..6e34f5a7b 100644 --- a/crates/openlogi-desktop/src/features/mouse/view/tests.rs +++ b/crates/openlogi-desktop/src/features/mouse/view/tests.rs @@ -95,6 +95,8 @@ fn a_selected_gesture_can_render_in_the_binding_inspector(cx: &mut TestAppContex overridden: None, }, &view.action_search, + &view.shortcut_input, + view.shortcut_hold, &entity, cx, ); diff --git a/crates/openlogi-ui/locales/be.toml b/crates/openlogi-ui/locales/be.toml index be9106cf6..08fef5947 100644 --- a/crates/openlogi-ui/locales/be.toml +++ b/crates/openlogi-ui/locales/be.toml @@ -256,6 +256,10 @@ bind_control_in_app = "Прывязаць %{name} у %{app}" unbound = "Не прывязана" five_directions = "5 кірункаў" hold_shortcut = "Утрымліваць %{chord}" +custom_shortcut = "Custom shortcut" +shortcut_placeholder = "e.g. Cmd+Shift+P" +shortcut_hold = "Hold" +shortcut_tap = "Tap" gesture_named = "Жэст %{name}" left_click = "Левы націск" right_click = "Правы націск" diff --git a/crates/openlogi-ui/locales/da.toml b/crates/openlogi-ui/locales/da.toml index 6010544ca..31f3c7536 100644 --- a/crates/openlogi-ui/locales/da.toml +++ b/crates/openlogi-ui/locales/da.toml @@ -256,6 +256,10 @@ bind_control_in_app = "Tildel %{name} i %{app}" unbound = "Ikke tildelt" five_directions = "5 retninger" hold_shortcut = "Hold %{chord} nede" +custom_shortcut = "Custom shortcut" +shortcut_placeholder = "e.g. Cmd+Shift+P" +shortcut_hold = "Hold" +shortcut_tap = "Tap" gesture_named = "Bevægelse %{name}" left_click = "Venstreklik" right_click = "Højreklik" diff --git a/crates/openlogi-ui/locales/de.toml b/crates/openlogi-ui/locales/de.toml index fa02c66fe..efabbfca9 100644 --- a/crates/openlogi-ui/locales/de.toml +++ b/crates/openlogi-ui/locales/de.toml @@ -256,6 +256,10 @@ bind_control_in_app = "%{name} in %{app} zuweisen" unbound = "Nicht zugewiesen" five_directions = "5 Richtungen" hold_shortcut = "%{chord} halten" +custom_shortcut = "Custom shortcut" +shortcut_placeholder = "e.g. Cmd+Shift+P" +shortcut_hold = "Hold" +shortcut_tap = "Tap" gesture_named = "Geste %{name}" left_click = "Linksklick" right_click = "Rechtsklick" diff --git a/crates/openlogi-ui/locales/el.toml b/crates/openlogi-ui/locales/el.toml index 6585a985e..01b487218 100644 --- a/crates/openlogi-ui/locales/el.toml +++ b/crates/openlogi-ui/locales/el.toml @@ -256,6 +256,10 @@ bind_control_in_app = "Αντιστοίχιση %{name} στο %{app}" unbound = "Χωρίς αντιστοίχιση" five_directions = "5 κατευθύνσεις" hold_shortcut = "Κράτημα %{chord}" +custom_shortcut = "Custom shortcut" +shortcut_placeholder = "e.g. Cmd+Shift+P" +shortcut_hold = "Hold" +shortcut_tap = "Tap" gesture_named = "Χειρονομία %{name}" left_click = "Αριστερό κλικ" right_click = "Δεξί κλικ" diff --git a/crates/openlogi-ui/locales/en.toml b/crates/openlogi-ui/locales/en.toml index bce28cf46..ef2173468 100644 --- a/crates/openlogi-ui/locales/en.toml +++ b/crates/openlogi-ui/locales/en.toml @@ -256,6 +256,10 @@ bind_control_in_app = "Bind %{name} in %{app}" unbound = "Unbound" five_directions = "5 directions" hold_shortcut = "Hold %{chord}" +custom_shortcut = "Custom shortcut" +shortcut_placeholder = "e.g. Cmd+Shift+P" +shortcut_hold = "Hold" +shortcut_tap = "Tap" gesture_named = "Gesture %{name}" left_click = "Left Click" right_click = "Right Click" diff --git a/crates/openlogi-ui/locales/es.toml b/crates/openlogi-ui/locales/es.toml index 5f72c763e..a797bcbe2 100644 --- a/crates/openlogi-ui/locales/es.toml +++ b/crates/openlogi-ui/locales/es.toml @@ -256,6 +256,10 @@ bind_control_in_app = "Asignar %{name} en %{app}" unbound = "Sin asignar" five_directions = "5 direcciones" hold_shortcut = "Mantener pulsado %{chord}" +custom_shortcut = "Custom shortcut" +shortcut_placeholder = "e.g. Cmd+Shift+P" +shortcut_hold = "Hold" +shortcut_tap = "Tap" gesture_named = "Gesto %{name}" left_click = "Clic izquierdo" right_click = "Clic derecho" diff --git a/crates/openlogi-ui/locales/fi.toml b/crates/openlogi-ui/locales/fi.toml index 70eaa9616..5173ebeb2 100644 --- a/crates/openlogi-ui/locales/fi.toml +++ b/crates/openlogi-ui/locales/fi.toml @@ -256,6 +256,10 @@ bind_control_in_app = "Määritä %{name} sovelluksessa %{app}" unbound = "Ei määritetty" five_directions = "5 suuntaa" hold_shortcut = "Pidä %{chord}" +custom_shortcut = "Custom shortcut" +shortcut_placeholder = "e.g. Cmd+Shift+P" +shortcut_hold = "Hold" +shortcut_tap = "Tap" gesture_named = "Ele %{name}" left_click = "Vasen napsautus" right_click = "Oikea napsautus" diff --git a/crates/openlogi-ui/locales/fr.toml b/crates/openlogi-ui/locales/fr.toml index f4eafafb8..6679465f0 100644 --- a/crates/openlogi-ui/locales/fr.toml +++ b/crates/openlogi-ui/locales/fr.toml @@ -256,6 +256,10 @@ bind_control_in_app = "Attribuer %{name} dans %{app}" unbound = "Non attribué" five_directions = "5 directions" hold_shortcut = "Maintenir %{chord}" +custom_shortcut = "Custom shortcut" +shortcut_placeholder = "e.g. Cmd+Shift+P" +shortcut_hold = "Hold" +shortcut_tap = "Tap" gesture_named = "Geste %{name}" left_click = "Clic gauche" right_click = "Clic droit" diff --git a/crates/openlogi-ui/locales/it.toml b/crates/openlogi-ui/locales/it.toml index 9a6e43eea..a475f8cd1 100644 --- a/crates/openlogi-ui/locales/it.toml +++ b/crates/openlogi-ui/locales/it.toml @@ -256,6 +256,10 @@ bind_control_in_app = "Associa %{name} in %{app}" unbound = "Non associato" five_directions = "5 direzioni" hold_shortcut = "Tieni premuto %{chord}" +custom_shortcut = "Custom shortcut" +shortcut_placeholder = "e.g. Cmd+Shift+P" +shortcut_hold = "Hold" +shortcut_tap = "Tap" gesture_named = "Gesto %{name}" left_click = "Click sinistro" right_click = "Click destro" diff --git a/crates/openlogi-ui/locales/ja.toml b/crates/openlogi-ui/locales/ja.toml index 0357a6877..1a820c9f5 100644 --- a/crates/openlogi-ui/locales/ja.toml +++ b/crates/openlogi-ui/locales/ja.toml @@ -256,6 +256,10 @@ bind_control_in_app = "%{app} で %{name} を割り当て" unbound = "未割り当て" five_directions = "5 方向" hold_shortcut = "%{chord} を長押し" +custom_shortcut = "Custom shortcut" +shortcut_placeholder = "e.g. Cmd+Shift+P" +shortcut_hold = "Hold" +shortcut_tap = "Tap" gesture_named = "ジェスチャー %{name}" left_click = "左クリック" right_click = "右クリック" diff --git a/crates/openlogi-ui/locales/ko.toml b/crates/openlogi-ui/locales/ko.toml index 1b72dffa7..898016c17 100644 --- a/crates/openlogi-ui/locales/ko.toml +++ b/crates/openlogi-ui/locales/ko.toml @@ -256,6 +256,10 @@ bind_control_in_app = "%{app}에서 %{name} 지정" unbound = "할당되지 않음" five_directions = "5방향" hold_shortcut = "%{chord} 길게 누르기" +custom_shortcut = "Custom shortcut" +shortcut_placeholder = "e.g. Cmd+Shift+P" +shortcut_hold = "Hold" +shortcut_tap = "Tap" gesture_named = "제스처 %{name}" left_click = "왼쪽 클릭" right_click = "오른쪽 클릭" diff --git a/crates/openlogi-ui/locales/nb.toml b/crates/openlogi-ui/locales/nb.toml index 883ff6be4..ddf1ec608 100644 --- a/crates/openlogi-ui/locales/nb.toml +++ b/crates/openlogi-ui/locales/nb.toml @@ -256,6 +256,10 @@ bind_control_in_app = "Tilordne %{name} i %{app}" unbound = "Ikke tildelt" five_directions = "5 retninger" hold_shortcut = "Hold %{chord} inne" +custom_shortcut = "Custom shortcut" +shortcut_placeholder = "e.g. Cmd+Shift+P" +shortcut_hold = "Hold" +shortcut_tap = "Tap" gesture_named = "Bevegelse %{name}" left_click = "Venstreklikk" right_click = "Høyreklikk" diff --git a/crates/openlogi-ui/locales/nl.toml b/crates/openlogi-ui/locales/nl.toml index 06ef8aaa1..90e280e94 100644 --- a/crates/openlogi-ui/locales/nl.toml +++ b/crates/openlogi-ui/locales/nl.toml @@ -256,6 +256,10 @@ bind_control_in_app = "%{name} toewijzen in %{app}" unbound = "Niet toegewezen" five_directions = "5 richtingen" hold_shortcut = "%{chord} ingedrukt houden" +custom_shortcut = "Custom shortcut" +shortcut_placeholder = "e.g. Cmd+Shift+P" +shortcut_hold = "Hold" +shortcut_tap = "Tap" gesture_named = "Gebaar %{name}" left_click = "Linkerklik" right_click = "Rechterklik" diff --git a/crates/openlogi-ui/locales/pl.toml b/crates/openlogi-ui/locales/pl.toml index a9af438fc..b82e77703 100644 --- a/crates/openlogi-ui/locales/pl.toml +++ b/crates/openlogi-ui/locales/pl.toml @@ -256,6 +256,10 @@ bind_control_in_app = "Przypisz %{name} w %{app}" unbound = "Nieprzypisane" five_directions = "5 kierunków" hold_shortcut = "Przytrzymaj %{chord}" +custom_shortcut = "Custom shortcut" +shortcut_placeholder = "e.g. Cmd+Shift+P" +shortcut_hold = "Hold" +shortcut_tap = "Tap" gesture_named = "Gest %{name}" left_click = "Lewy przycisk" right_click = "Prawy przycisk" diff --git a/crates/openlogi-ui/locales/pt-BR.toml b/crates/openlogi-ui/locales/pt-BR.toml index cedfa80c7..20ed5ccfb 100644 --- a/crates/openlogi-ui/locales/pt-BR.toml +++ b/crates/openlogi-ui/locales/pt-BR.toml @@ -256,6 +256,10 @@ bind_control_in_app = "Atribuir %{name} em %{app}" unbound = "Não atribuído" five_directions = "5 direções" hold_shortcut = "Manter %{chord}" +custom_shortcut = "Custom shortcut" +shortcut_placeholder = "e.g. Cmd+Shift+P" +shortcut_hold = "Hold" +shortcut_tap = "Tap" gesture_named = "Gesto %{name}" left_click = "Clique Esquerdo" right_click = "Clique Direito" diff --git a/crates/openlogi-ui/locales/pt-PT.toml b/crates/openlogi-ui/locales/pt-PT.toml index 5c57d5c30..7d9e21a70 100644 --- a/crates/openlogi-ui/locales/pt-PT.toml +++ b/crates/openlogi-ui/locales/pt-PT.toml @@ -256,6 +256,10 @@ bind_control_in_app = "Atribuir %{name} em %{app}" unbound = "Não atribuído" five_directions = "5 direções" hold_shortcut = "Manter %{chord}" +custom_shortcut = "Custom shortcut" +shortcut_placeholder = "e.g. Cmd+Shift+P" +shortcut_hold = "Hold" +shortcut_tap = "Tap" gesture_named = "Gesto %{name}" left_click = "Clique esquerdo" right_click = "Clique direito" diff --git a/crates/openlogi-ui/locales/ru.toml b/crates/openlogi-ui/locales/ru.toml index d4308a4fc..a8236b8f5 100644 --- a/crates/openlogi-ui/locales/ru.toml +++ b/crates/openlogi-ui/locales/ru.toml @@ -256,6 +256,10 @@ bind_control_in_app = "Назначить %{name} в %{app}" unbound = "Не назначено" five_directions = "5 направлений" hold_shortcut = "Удерживать %{chord}" +custom_shortcut = "Custom shortcut" +shortcut_placeholder = "e.g. Cmd+Shift+P" +shortcut_hold = "Hold" +shortcut_tap = "Tap" gesture_named = "Жест %{name}" left_click = "Левый щелчок" right_click = "Правый щелчок" diff --git a/crates/openlogi-ui/locales/sv.toml b/crates/openlogi-ui/locales/sv.toml index e07853409..0c957d5cb 100644 --- a/crates/openlogi-ui/locales/sv.toml +++ b/crates/openlogi-ui/locales/sv.toml @@ -256,6 +256,10 @@ bind_control_in_app = "Tilldela %{name} i %{app}" unbound = "Ej tilldelad" five_directions = "5 riktningar" hold_shortcut = "Håll %{chord} nedtryckt" +custom_shortcut = "Custom shortcut" +shortcut_placeholder = "e.g. Cmd+Shift+P" +shortcut_hold = "Hold" +shortcut_tap = "Tap" gesture_named = "Gest %{name}" left_click = "Vänsterklick" right_click = "Högerklick" diff --git a/crates/openlogi-ui/locales/tr.toml b/crates/openlogi-ui/locales/tr.toml index fc26483b3..25fa128fa 100644 --- a/crates/openlogi-ui/locales/tr.toml +++ b/crates/openlogi-ui/locales/tr.toml @@ -256,6 +256,10 @@ bind_control_in_app = "%{app} içinde %{name} ata" unbound = "Atanmamış" five_directions = "5 yön" hold_shortcut = "%{chord} basılı tut" +custom_shortcut = "Custom shortcut" +shortcut_placeholder = "e.g. Cmd+Shift+P" +shortcut_hold = "Hold" +shortcut_tap = "Tap" gesture_named = "%{name} hareketi" left_click = "Sol Tık" right_click = "Sağ Tık" diff --git a/crates/openlogi-ui/locales/uk.toml b/crates/openlogi-ui/locales/uk.toml index 1f525f809..5d82a55d6 100644 --- a/crates/openlogi-ui/locales/uk.toml +++ b/crates/openlogi-ui/locales/uk.toml @@ -256,6 +256,10 @@ bind_control_in_app = "Прив'язати %{name} в %{app}" unbound = "Не прив'язано" five_directions = "5 напрямків" hold_shortcut = "Утримувати %{chord}" +custom_shortcut = "Custom shortcut" +shortcut_placeholder = "e.g. Cmd+Shift+P" +shortcut_hold = "Hold" +shortcut_tap = "Tap" gesture_named = "Жест %{name}" left_click = "Клацання лівою" right_click = "Клацання правою" diff --git a/crates/openlogi-ui/locales/zh-CN.toml b/crates/openlogi-ui/locales/zh-CN.toml index 2f322ee8f..dd7c567f4 100644 --- a/crates/openlogi-ui/locales/zh-CN.toml +++ b/crates/openlogi-ui/locales/zh-CN.toml @@ -256,6 +256,10 @@ bind_control_in_app = "在 %{app} 中绑定 %{name}" unbound = "未绑定" five_directions = "5 个方向" hold_shortcut = "按住 %{chord}" +custom_shortcut = "Custom shortcut" +shortcut_placeholder = "e.g. Cmd+Shift+P" +shortcut_hold = "Hold" +shortcut_tap = "Tap" gesture_named = "手势 %{name}" left_click = "左键单击" right_click = "右键单击" diff --git a/crates/openlogi-ui/locales/zh-HK.toml b/crates/openlogi-ui/locales/zh-HK.toml index 462228d0b..17d120d9a 100644 --- a/crates/openlogi-ui/locales/zh-HK.toml +++ b/crates/openlogi-ui/locales/zh-HK.toml @@ -256,6 +256,10 @@ bind_control_in_app = "在 %{app} 中綁定 %{name}" unbound = "未綁定" five_directions = "5 個方向" hold_shortcut = "按住 %{chord}" +custom_shortcut = "Custom shortcut" +shortcut_placeholder = "e.g. Cmd+Shift+P" +shortcut_hold = "Hold" +shortcut_tap = "Tap" gesture_named = "手勢 %{name}" left_click = "左鍵單擊" right_click = "右鍵單擊" diff --git a/crates/openlogi-ui/locales/zh-TW.toml b/crates/openlogi-ui/locales/zh-TW.toml index 44b89cb97..98561f7e5 100644 --- a/crates/openlogi-ui/locales/zh-TW.toml +++ b/crates/openlogi-ui/locales/zh-TW.toml @@ -256,6 +256,10 @@ bind_control_in_app = "在 %{app} 中設定 %{name}" unbound = "未設定" five_directions = "5 個方向" hold_shortcut = "按住 %{chord}" +custom_shortcut = "Custom shortcut" +shortcut_placeholder = "e.g. Cmd+Shift+P" +shortcut_hold = "Hold" +shortcut_tap = "Tap" gesture_named = "手勢 %{name}" left_click = "左鍵按一下" right_click = "右鍵按一下" From 9c1dddba0988adb050eeefc74152d70396457243 Mon Sep 17 00:00:00 2001 From: Fahad Sheikh Date: Tue, 15 Sep 2026 18:20:00 +0500 Subject: [PATCH 2/3] fix(gui): reset custom-shortcut draft state and reject invalid input Two Greptile findings on the initial commit: - shortcut_input/shortcut_hold on MouseModelView were stored once and never cleared, so opening the picker for a different button or gesture direction showed the previous target's half-typed shortcut, and Add could silently commit it to the wrong binding. Cleared in the same opening-branch of selection_card's on_click that already resets action_search. - The Add button parsed KeyCombo and no-op'd silently on failure, giving no feedback for empty input, modifier-only combos, unknown keys, or malformed separators. Add is now disabled while the current input doesn't parse, requiring a live-updating subscription on shortcut_input (mirroring the one action_search already had) so the disabled state tracks each keystroke. --- .../src/features/mouse/inspector.rs | 15 ++++++++++++++- .../openlogi-desktop/src/features/mouse/view.rs | 6 ++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/crates/openlogi-desktop/src/features/mouse/inspector.rs b/crates/openlogi-desktop/src/features/mouse/inspector.rs index 73a71ca4d..6f864c3e8 100644 --- a/crates/openlogi-desktop/src/features/mouse/inspector.rs +++ b/crates/openlogi-desktop/src/features/mouse/inspector.rs @@ -584,6 +584,7 @@ fn selection_card( ) -> impl IntoElement { let toggle = picker.view.clone(); let search = picker.search.clone(); + let shortcut_input = picker.shortcut_input.clone(); let opening = !picker.open; let accessible_label = value.clone(); BaseButton::new(id) @@ -641,8 +642,12 @@ fn selection_card( .on_click(move |_, window, cx| { if opening { search.update(cx, |search, cx| search.set_value("", window, cx)); + shortcut_input.update(cx, |input, cx| input.set_value("", window, cx)); } toggle.update(cx, |view, cx| { + if opening { + view.set_shortcut_hold(false); + } view.toggle_action_picker(); cx.notify(); }); @@ -678,7 +683,7 @@ fn action_library( }) .children(rows), ) - .child(custom_shortcut_editor(picker, on_pick, pal)) + .child(custom_shortcut_editor(picker, on_pick, pal, cx)) } /// Free-text `KeyCombo` recorder shared by every `action_library` call site. @@ -688,12 +693,19 @@ fn custom_shortcut_editor( picker: ActionPickerContext<'_>, on_pick: &PickFn, pal: Palette, + cx: &Context, ) -> impl IntoElement { let hold = picker.shortcut_hold; let view_tap = picker.view.clone(); let view_hold = picker.view.clone(); let submit_input = picker.shortcut_input.clone(); let on_pick = on_pick.clone(); + let is_valid = picker + .shortcut_input + .read(cx) + .value() + .parse::() + .is_ok(); v_flex() .gap_1() @@ -737,6 +749,7 @@ fn custom_shortcut_editor( Button::new("shortcut-add") .compact() .label(tr!("common.add")) + .disabled(!is_valid) .on_click(move |_, window, cx| { let text = submit_input.read(cx).value().to_string(); if let Ok(combo) = text.parse::() { diff --git a/crates/openlogi-desktop/src/features/mouse/view.rs b/crates/openlogi-desktop/src/features/mouse/view.rs index 34692f669..2f8f10f14 100644 --- a/crates/openlogi-desktop/src/features/mouse/view.rs +++ b/crates/openlogi-desktop/src/features/mouse/view.rs @@ -148,6 +148,12 @@ impl MouseModelView { .detach(); let shortcut_input = cx .new(|cx| InputState::new(window, cx).placeholder(tr!("actions.shortcut_placeholder"))); + cx.subscribe(&shortcut_input, |_, _, event: &InputEvent, cx| { + if matches!(event, InputEvent::Change) { + cx.notify(); + } + }) + .detach(); let state_obs = AppState::repaint_on(cx, |event| { matches!( event, From 4c1f46da2a088fbc968e4a8bad5f6998a849dd7a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 10 Oct 2026 21:36:38 +0000 Subject: [PATCH 3/3] fix(gui): model shortcut mode as an enum Co-authored-by: David Budnick --- .../src/features/mouse/inspector.rs | 79 ++++++++++--------- .../src/features/mouse/view.rs | 18 ++--- .../src/features/mouse/view/tests.rs | 18 ++++- 3 files changed, 64 insertions(+), 51 deletions(-) diff --git a/crates/openlogi-desktop/src/features/mouse/inspector.rs b/crates/openlogi-desktop/src/features/mouse/inspector.rs index c7394c105..ef011157d 100644 --- a/crates/openlogi-desktop/src/features/mouse/inspector.rs +++ b/crates/openlogi-desktop/src/features/mouse/inspector.rs @@ -46,11 +46,27 @@ pub(super) struct ActionPickerContext<'a> { pub shortcut_input: &'a Entity, pub application_input: &'a Entity, pub shortcut_invalid: bool, - pub shortcut_hold: bool, + pub shortcut_mode: ShortcutMode, pub application_invalid: bool, pub view: &'a Entity, } +/// Whether the custom shortcut editor records a tapped or a held chord. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum ShortcutMode { + Tap, + Hold, +} + +impl ShortcutMode { + fn action(self, combo: openlogi_core::binding::KeyCombo) -> Action { + match self { + Self::Tap => Action::CustomShortcut(combo), + Self::Hold => Action::HoldShortcut(combo), + } + } +} + pub(super) fn binding_inspector( data: BindingInspectorData<'_>, picker: ActionPickerContext<'_>, @@ -666,7 +682,7 @@ fn action_library( id_prefix, picker.shortcut_input, picker.shortcut_invalid, - picker.shortcut_hold, + picker.shortcut_mode, picker.view, on_pick, pal, @@ -704,45 +720,37 @@ fn custom_shortcut_editor( id_prefix: &'static str, input: &Entity, invalid: bool, - hold: bool, + mode: ShortcutMode, view: &Entity, on_pick: &PickFn, pal: Palette, ) -> impl IntoElement { let submit_input = input.clone(); let on_pick = on_pick.clone(); - let view_tap = view.clone(); - let view_hold = view.clone(); + let mode_button = |option: ShortcutMode| { + let (id, label) = match option { + ShortcutMode::Tap => ("tap", tr!("actions.shortcut_tap")), + ShortcutMode::Hold => ("hold", tr!("actions.shortcut_hold")), + }; + let view = view.clone(); + control_button(format!("{id_prefix}-shortcut-{id}")) + .debug_selector(move || format!("{id_prefix}-shortcut-{id}")) + .selected(mode == option) + .label(label) + .on_click(move |_, _, cx| { + view.update(cx, |view, cx| { + view.shortcut_mode = option; + cx.notify(); + }); + }) + }; + let tap_button = mode_button(ShortcutMode::Tap); + let hold_button = mode_button(ShortcutMode::Hold); let view = view.clone(); v_flex() .gap_1() .child(editor_section(tr!("action_ring.custom_shortcut"), pal)) - .child( - h_flex() - .gap_2() - .child( - control_button(format!("{id_prefix}-shortcut-tap")) - .selected(!hold) - .label(tr!("actions.shortcut_tap")) - .on_click(move |_, _, cx| { - view_tap.update(cx, |view, cx| { - view.set_shortcut_hold(false); - cx.notify(); - }); - }), - ) - .child( - control_button(format!("{id_prefix}-shortcut-hold")) - .selected(hold) - .label(tr!("actions.shortcut_hold")) - .on_click(move |_, _, cx| { - view_hold.update(cx, |view, cx| { - view.set_shortcut_hold(true); - cx.notify(); - }); - }), - ), - ) + .child(h_flex().gap_2().child(tap_button).child(hold_button)) .child( h_flex() .gap_2() @@ -761,14 +769,7 @@ fn custom_shortcut_editor( .on_click(move |_, window, cx| { let shortcut = submit_input.read(cx).value().to_string(); match shortcut.parse::() { - Ok(combo) => { - let action = if hold { - Action::HoldShortcut(combo) - } else { - Action::CustomShortcut(combo) - }; - (on_pick)(action, window, cx); - } + Ok(combo) => (on_pick)(mode.action(combo), window, cx), Err(_) => view.update(cx, |view, cx| { view.custom_shortcut_invalid = true; cx.notify(); diff --git a/crates/openlogi-desktop/src/features/mouse/view.rs b/crates/openlogi-desktop/src/features/mouse/view.rs index 6ede1ea6b..2f03fd5f9 100644 --- a/crates/openlogi-desktop/src/features/mouse/view.rs +++ b/crates/openlogi-desktop/src/features/mouse/view.rs @@ -20,7 +20,9 @@ use super::geometry::{ default_labels, labels_from_hotspots, }; use super::hotspots::{Hotspot, MOUSE_MODEL_SIZE, MouseControlId, default_hotspots}; -use super::inspector::{ActionPickerContext, BindingInspectorData, binding_inspector}; +use super::inspector::{ + ActionPickerContext, BindingInspectorData, ShortcutMode, binding_inspector, +}; use super::leader_lines::{Geometry as LeaderGeometry, Label, paint as paint_leader_lines}; use crate::app::{glow_canvas, keyboard_glow}; use crate::features::profiles::{friendly_app_name, profile_canvas_status}; @@ -135,9 +137,7 @@ pub struct MouseModelView { /// Whether the last "Add" attempt on the corresponding custom editor /// failed to parse, so its caption can show an inline error. pub(super) custom_shortcut_invalid: bool, - /// Whether the custom shortcut editor records a held chord (Hold) rather - /// than a tapped one (Tap). - pub(super) shortcut_hold: bool, + pub(super) shortcut_mode: ShortcutMode, pub(super) custom_application_invalid: bool, _state_obs: Subscription, } @@ -197,7 +197,7 @@ impl MouseModelView { custom_shortcut_input, custom_application_input, custom_shortcut_invalid: false, - shortcut_hold: false, + shortcut_mode: ShortcutMode::Tap, custom_application_invalid: false, _state_obs: state_obs, } @@ -217,11 +217,7 @@ impl MouseModelView { .update(cx, |input, cx| input.set_value("", window, cx)); self.custom_shortcut_invalid = false; self.custom_application_invalid = false; - self.shortcut_hold = false; - } - - pub(super) fn set_shortcut_hold(&mut self, hold: bool) { - self.shortcut_hold = hold; + self.shortcut_mode = ShortcutMode::Tap; } /// Set (or clear, with `None`) the activated gesture direction. Callers must @@ -405,7 +401,7 @@ impl Render for MouseModelView { shortcut_input: &self.custom_shortcut_input, application_input: &self.custom_application_input, shortcut_invalid: self.custom_shortcut_invalid, - shortcut_hold: self.shortcut_hold, + shortcut_mode: self.shortcut_mode, application_invalid: self.custom_application_invalid, view: &view, }, diff --git a/crates/openlogi-desktop/src/features/mouse/view/tests.rs b/crates/openlogi-desktop/src/features/mouse/view/tests.rs index 742cc136e..48866934f 100644 --- a/crates/openlogi-desktop/src/features/mouse/view/tests.rs +++ b/crates/openlogi-desktop/src/features/mouse/view/tests.rs @@ -162,6 +162,20 @@ fn action_picker_renders_after_opening_through_the_inspector(cx: &mut TestAppCon }); click_picker_control(cx, "inspector-current-action"); + click_picker_control(cx, "inspector-action-shortcut-hold"); + view.read_with(cx, |view, _| { + assert_eq!(view.shortcut_mode, ShortcutMode::Hold); + }); + click_picker_control(cx, "inspector-action-custom-shortcut-input"); + cx.simulate_input("Cmd+K"); + click_picker_control(cx, "inspector-action-custom-shortcut-add"); + view.read_with(cx, |view, _| assert!(!view.action_picker_open)); + assert_forward_binding(cx, &Action::HoldShortcut("Cmd+K".parse().unwrap())); + + click_picker_control(cx, "inspector-current-action"); + view.read_with(cx, |view, _| { + assert_eq!(view.shortcut_mode, ShortcutMode::Tap); + }); cx.update(|window, cx| { view.read(cx) .action_search @@ -214,7 +228,7 @@ fn a_selected_gesture_can_render_in_the_binding_inspector(cx: &mut TestAppContex shortcut_input: &view.custom_shortcut_input, application_input: &view.custom_application_input, shortcut_invalid: view.custom_shortcut_invalid, - shortcut_hold: view.shortcut_hold, + shortcut_mode: view.shortcut_mode, application_invalid: view.custom_application_invalid, view: &entity, }, @@ -262,6 +276,7 @@ fn clearing_custom_action_drafts_resets_text_and_invalid_state(cx: &mut TestAppC .update(cx, |input, cx| input.set_value("/bin/true", window, cx)); view.custom_shortcut_invalid = true; view.custom_application_invalid = true; + view.shortcut_mode = ShortcutMode::Hold; view.clear_custom_action_drafts(window, cx); }); @@ -272,6 +287,7 @@ fn clearing_custom_action_drafts_resets_text_and_invalid_state(cx: &mut TestAppC assert_eq!(view.custom_application_input.read(cx).value(), ""); assert!(!view.custom_shortcut_invalid); assert!(!view.custom_application_invalid); + assert_eq!(view.shortcut_mode, ShortcutMode::Tap); }); drop(view); cx.update(|window, _| window.remove_window());