Skip to content

Commit 500296e

Browse files
fix(gui,inject): honor per-app ring layout and numpad Enter on Windows
Greptile: show the scoped Actions Ring layout in the editor instead of always reading default, and set KEYEVENTF_EXTENDEDKEY for HID 0x58 so keypad Enter stays distinct from main Enter when injecting shortcuts. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 0505515 commit 500296e

3 files changed

Lines changed: 96 additions & 47 deletions

File tree

‎crates/openlogi-desktop/src/features/action_ring.rs‎

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ use gpui_component::{
1414
v_flex,
1515
};
1616
use openlogi_core::binding::{
17-
Action, ActionRingEntry, ActionRingIcon, ActionRingLayout, ActionRingSlot, KeyCombo,
17+
Action, ActionRingConfig, ActionRingEntry, ActionRingIcon, ActionRingLayout, ActionRingSlot,
18+
KeyCombo,
1819
};
1920
use openlogi_ui::action_icons::RING_CANCEL_ICON;
2021

@@ -81,9 +82,7 @@ impl Focusable for ActionRingPanel {
8182
impl Render for ActionRingPanel {
8283
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
8384
let pal = theme::palette(cx);
84-
let ring = AppState::try_read(cx)
85-
.map(AppState::current_action_ring)
86-
.unwrap_or_default();
85+
let (ring, layout) = action_ring_editor_state(cx);
8786
let haptics_supported = current_device_supports_haptics(cx);
8887
let application_input = editor_input(
8988
&mut self.application_input,
@@ -122,10 +121,10 @@ impl Render for ActionRingPanel {
122121
.items_start()
123122
.justify_center()
124123
.gap_4()
125-
.child(ring_preview(&ring.default, self.selected_slot, &view, pal))
124+
.child(ring_preview(&layout, self.selected_slot, &view, pal))
126125
.child(action_library(
127126
self.selected_slot,
128-
ring.default.slots.get(&self.selected_slot),
127+
layout.slots.get(&self.selected_slot),
129128
&application_input,
130129
&shortcut_input,
131130
&self.library_scroll,
@@ -183,6 +182,21 @@ impl Render for ActionRingPanel {
183182
}
184183
}
185184

185+
fn action_ring_editor_state(cx: &Context<ActionRingPanel>) -> (ActionRingConfig, ActionRingLayout) {
186+
AppState::try_read(cx).map_or_else(
187+
|| {
188+
let ring = ActionRingConfig::default();
189+
let layout = ring.default.clone();
190+
(ring, layout)
191+
},
192+
|state| {
193+
let ring = state.current_action_ring();
194+
let layout = state.current_action_ring_layout();
195+
(ring, layout)
196+
},
197+
)
198+
}
199+
186200
fn editor_input(
187201
state: &mut Option<Entity<InputState>>,
188202
placeholder: impl Into<SharedString>,

‎crates/openlogi-inject/src/inject.rs‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,16 @@ fn hid_usage_to_windows(usage: u8) -> Option<u16> {
511511
}
512512
}
513513

514+
/// Whether a HID keyboard usage needs `KEYEVENTF_EXTENDEDKEY` on Windows.
515+
#[cfg_attr(
516+
not(target_os = "windows"),
517+
expect(clippy::allow_attributes, reason = "see hid_usage_to_windows"),
518+
allow(dead_code, reason = "called only by the Windows backend")
519+
)]
520+
fn hid_usage_extended_key(usage: u8) -> bool {
521+
usage == 0x58
522+
}
523+
514524
#[cfg(test)]
515525
mod tests {
516526
use openlogi_core::scroll::ScrollDelta;
@@ -687,7 +697,7 @@ mod tests {
687697

688698
#[test]
689699
fn hid_usages_map_across_windows_key_categories() {
690-
use super::hid_usage_to_windows;
700+
use super::{hid_usage_extended_key, hid_usage_to_windows};
691701

692702
assert_eq!(hid_usage_to_windows(0x04), Some(0x41)); // A
693703
assert_eq!(hid_usage_to_windows(0x1e), Some(0x31)); // 1
@@ -698,6 +708,8 @@ mod tests {
698708
assert_eq!(hid_usage_to_windows(0x33), Some(0xba)); // Semicolon
699709
assert_eq!(hid_usage_to_windows(0x49), Some(0x2d)); // Insert
700710
assert_eq!(hid_usage_to_windows(0x56), Some(0x6d)); // Numpad subtract
711+
assert!(!hid_usage_extended_key(0x28)); // Main Enter
712+
assert!(hid_usage_extended_key(0x58)); // Numpad Enter
701713
assert_eq!(hid_usage_to_windows(0xff), None);
702714
}
703715
}

‎crates/openlogi-inject/src/inject/windows.rs‎

Lines changed: 63 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ use std::mem::size_of;
55
use std::sync::{LazyLock, Mutex};
66

77
use windows_sys::Win32::UI::Input::KeyboardAndMouse::{
8-
INPUT, INPUT_0, INPUT_KEYBOARD, INPUT_MOUSE, KEYBDINPUT, KEYEVENTF_KEYUP, MOUSEEVENTF_HWHEEL,
9-
MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP, MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP,
10-
MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP, MOUSEEVENTF_WHEEL, MOUSEEVENTF_XDOWN,
11-
MOUSEEVENTF_XUP, MOUSEINPUT, SendInput,
8+
INPUT, INPUT_0, INPUT_KEYBOARD, INPUT_MOUSE, KEYBDINPUT, KEYEVENTF_EXTENDEDKEY, KEYEVENTF_KEYUP,
9+
MOUSEEVENTF_HWHEEL, MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP, MOUSEEVENTF_MIDDLEDOWN,
10+
MOUSEEVENTF_MIDDLEUP, MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP, MOUSEEVENTF_WHEEL,
11+
MOUSEEVENTF_XDOWN, MOUSEEVENTF_XUP, MOUSEINPUT, SendInput,
1212
};
1313

1414
use openlogi_core::binding::{
@@ -120,7 +120,7 @@ fn parse_shortcut(text: &str) -> KeyCombo {
120120
fn press_shortcut(shortcut: Shortcut) {
121121
match combo(shortcut) {
122122
Ok(combo) => post_custom_shortcut(&combo),
123-
Err(vk) => post_key(vk, &[]),
123+
Err(vk) => post_key(vk, &[], false),
124124
}
125125
}
126126

@@ -129,16 +129,16 @@ fn press_shortcut(shortcut: Shortcut) {
129129
/// synthesis (see the comment below) and is skipped.
130130
fn dispatch_native(native: NativeAction) {
131131
match native {
132-
NativeAction::MissionControl | NativeAction::AppExpose => post_key(VK_TAB, &[VK_LWIN]),
133-
NativeAction::PreviousDesktop => post_key(VK_LEFT, &[VK_LWIN, VK_CONTROL]),
134-
NativeAction::NextDesktop => post_key(VK_RIGHT, &[VK_LWIN, VK_CONTROL]),
135-
NativeAction::ShowDesktop => post_key(VK_D, &[VK_LWIN]),
136-
NativeAction::LaunchpadShow => post_key(VK_LWIN, &[]),
137-
NativeAction::LockScreen => post_key(VK_L, &[VK_LWIN]),
132+
NativeAction::MissionControl | NativeAction::AppExpose => post_key(VK_TAB, &[VK_LWIN], false),
133+
NativeAction::PreviousDesktop => post_key(VK_LEFT, &[VK_LWIN, VK_CONTROL], false),
134+
NativeAction::NextDesktop => post_key(VK_RIGHT, &[VK_LWIN, VK_CONTROL], false),
135+
NativeAction::ShowDesktop => post_key(VK_D, &[VK_LWIN], false),
136+
NativeAction::LaunchpadShow => post_key(VK_LWIN, &[], false),
137+
NativeAction::LockScreen => post_key(VK_L, &[VK_LWIN], false),
138138
// Win+Shift+S opens the snip overlay, which serves both full-screen
139139
// and region capture on Windows.
140140
NativeAction::Screenshot | NativeAction::CaptureRegion => {
141-
post_key(VK_S, &[VK_LWIN, VK_SHIFT]);
141+
post_key(VK_S, &[VK_LWIN, VK_SHIFT], false);
142142
}
143143
// Suspending reliably needs `SetSuspendState` (powrprof.dll), which
144144
// hibernates instead when hibernation is enabled — no clean win from
@@ -151,12 +151,12 @@ fn dispatch_native(native: NativeAction) {
151151

152152
fn dispatch_media(key: MediaKey) {
153153
match key {
154-
MediaKey::PlayPause => post_key(VK_MEDIA_PLAY_PAUSE, &[]),
155-
MediaKey::NextTrack => post_key(VK_MEDIA_NEXT_TRACK, &[]),
156-
MediaKey::PrevTrack => post_key(VK_MEDIA_PREV_TRACK, &[]),
157-
MediaKey::VolumeUp => post_key(VK_VOLUME_UP, &[]),
158-
MediaKey::VolumeDown => post_key(VK_VOLUME_DOWN, &[]),
159-
MediaKey::Mute => post_key(VK_VOLUME_MUTE, &[]),
154+
MediaKey::PlayPause => post_key(VK_MEDIA_PLAY_PAUSE, &[], false),
155+
MediaKey::NextTrack => post_key(VK_MEDIA_NEXT_TRACK, &[], false),
156+
MediaKey::PrevTrack => post_key(VK_MEDIA_PREV_TRACK, &[], false),
157+
MediaKey::VolumeUp => post_key(VK_VOLUME_UP, &[], false),
158+
MediaKey::VolumeDown => post_key(VK_VOLUME_DOWN, &[], false),
159+
MediaKey::Mute => post_key(VK_VOLUME_MUTE, &[], false),
160160
}
161161
}
162162

@@ -215,15 +215,15 @@ fn post_click(button: MouseButton) {
215215
send_inputs(&[mouse_input(down, data), mouse_input(up, data)]);
216216
}
217217

218-
fn post_key(vk: u16, modifiers: &[u16]) {
218+
fn post_key(vk: u16, modifiers: &[u16], extended: bool) {
219219
let mut inputs = Vec::with_capacity(modifiers.len() * 2 + 2);
220220
for modifier in modifiers {
221-
inputs.push(key_input(*modifier, false));
221+
inputs.push(key_input(*modifier, false, false));
222222
}
223-
inputs.push(key_input(vk, false));
224-
inputs.push(key_input(vk, true));
223+
inputs.push(key_input(vk, false, extended));
224+
inputs.push(key_input(vk, true, extended));
225225
for modifier in modifiers.iter().rev() {
226-
inputs.push(key_input(*modifier, true));
226+
inputs.push(key_input(*modifier, true, false));
227227
}
228228
send_inputs(&inputs);
229229
}
@@ -269,16 +269,21 @@ pub(super) fn post_scroll(delta: ScrollDelta) {
269269
}
270270

271271
fn post_custom_shortcut(combo: &KeyCombo) {
272-
let Some(vk) = super::hid_usage_to_windows(combo.key().code()) else {
272+
let usage = combo.key().code();
273+
let Some(vk) = super::hid_usage_to_windows(usage) else {
273274
tracing::warn!(
274-
usage = combo.key().code(),
275+
usage,
275276
chord = %combo.rendered_label(),
276277
"CustomShortcut key has no Windows mapping yet; press ignored"
277278
);
278279
return;
279280
};
280281

281-
post_key(vk, &combo_modifiers(combo));
282+
post_key(
283+
vk,
284+
&combo_modifiers(combo),
285+
super::hid_usage_extended_key(usage),
286+
);
282287
}
283288

284289
fn combo_modifiers(combo: &KeyCombo) -> Vec<u16> {
@@ -305,27 +310,42 @@ pub(super) fn hold_keys(keys: &[HeldKey], phase: KeyPhase) {
305310
.filter_map(|key| held_virtual_key(*key))
306311
.collect();
307312
let key_up = phase == KeyPhase::Up;
308-
let mut inputs: Vec<_> = keys.iter().map(|key| key_input(*key, key_up)).collect();
313+
let mut inputs: Vec<_> = keys
314+
.iter()
315+
.map(|key| key_input(key.vk, key_up, key.extended))
316+
.collect();
309317
if key_up {
310318
inputs.reverse();
311319
}
312320
send_inputs(&inputs);
313321
}
314322

315-
fn held_virtual_key(key: HeldKey) -> Option<u16> {
323+
struct VirtualKey {
324+
vk: u16,
325+
extended: bool,
326+
}
327+
328+
fn held_virtual_key(key: HeldKey) -> Option<VirtualKey> {
316329
match key {
317-
HeldKey::Control => Some(VK_CONTROL),
318-
HeldKey::Shift => Some(VK_SHIFT),
319-
HeldKey::Alt => Some(VK_MENU),
330+
HeldKey::Control => Some(VirtualKey {
331+
vk: VK_CONTROL,
332+
extended: false,
333+
}),
334+
HeldKey::Shift => Some(VirtualKey {
335+
vk: VK_SHIFT,
336+
extended: false,
337+
}),
338+
HeldKey::Alt => Some(VirtualKey {
339+
vk: VK_MENU,
340+
extended: false,
341+
}),
320342
HeldKey::Key(usage) => {
321-
let key = super::hid_usage_to_windows(usage.code());
322-
if key.is_none() {
323-
tracing::warn!(
324-
usage = usage.code(),
325-
"held shortcut usage has no Windows mapping — edge ignored"
326-
);
327-
}
328-
key
343+
let code = usage.code();
344+
let vk = super::hid_usage_to_windows(code)?;
345+
Some(VirtualKey {
346+
vk,
347+
extended: super::hid_usage_extended_key(code),
348+
})
329349
}
330350
}
331351
}
@@ -353,11 +373,14 @@ fn send_inputs(inputs: &[INPUT]) {
353373
}
354374
}
355375

356-
fn key_input(vk: u16, key_up: bool) -> INPUT {
376+
fn key_input(vk: u16, key_up: bool, extended: bool) -> INPUT {
357377
let mut flags = 0;
358378
if key_up {
359379
flags |= KEYEVENTF_KEYUP;
360380
}
381+
if extended {
382+
flags |= KEYEVENTF_EXTENDEDKEY;
383+
}
361384
INPUT {
362385
r#type: INPUT_KEYBOARD,
363386
Anonymous: INPUT_0 {

0 commit comments

Comments
 (0)