Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions crates/openlogi-core/src/binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,8 @@ impl KeyCombo {
pub const MOD_CTRL: u8 = 1 << 2;
/// Bit for the ⌥ Option/Alt modifier in [`Self::modifiers`].
pub const MOD_OPTION: u8 = 1 << 3;
/// Bit for the macOS Fn/Globe modifier in [`Self::modifiers`].
pub const MOD_FN: u8 = 1 << 4;

/// Build the human-readable label from the modifier bitmask + key code.
/// Falls back to `"⌘key 0xNN"` when the key code isn't one of the
Expand All @@ -410,6 +412,9 @@ impl KeyCombo {
return self.display.clone();
}
let mut out = String::new();
if self.modifiers & Self::MOD_FN != 0 {
out.push_str("fn");
}
if self.modifiers & Self::MOD_CTRL != 0 {
out.push('⌃');
}
Expand Down Expand Up @@ -442,6 +447,12 @@ impl KeyCombo {
0x22 => out.push('I'),
0x1F => out.push('O'),
0x23 => out.push('P'),
0x74 => out.push_str("Page Up"),
0x79 => out.push_str("Page Down"),
0x7B => out.push('←'),
0x7C => out.push('→'),
0x7D => out.push('↓'),
0x7E => out.push('↑'),
_ => {
use std::fmt::Write as _;
let _ = write!(out, "key 0x{:02X}", self.key_code);
Expand Down Expand Up @@ -1005,6 +1016,40 @@ mod tests {
assert_eq!(combo.rendered_label(), "⇧⌘P");
}

#[test]
fn key_combo_rendered_label_includes_fn_and_navigation_key() {
let combo = KeyCombo {
modifiers: KeyCombo::MOD_FN,
key_code: 0x7D, // Down Arrow
display: String::new(),
};
assert_eq!(combo.rendered_label(), "fn↓");
}

#[test]
fn key_combo_rendered_label_places_fn_before_other_modifiers() {
let combo = KeyCombo {
modifiers: KeyCombo::MOD_FN
| KeyCombo::MOD_CTRL
| KeyCombo::MOD_OPTION
| KeyCombo::MOD_SHIFT
| KeyCombo::MOD_CMD,
key_code: 0x7D, // Down Arrow
display: String::new(),
};
assert_eq!(combo.rendered_label(), "fn⌃⌥⇧⌘↓");
}

#[test]
fn fn_custom_shortcut_roundtrips_toml() {
let action = Action::CustomShortcut(KeyCombo {
modifiers: KeyCombo::MOD_FN,
key_code: 0x7E, // Up Arrow
display: String::new(),
});
assert_eq!(roundtrip(&action), action);
}

// ── Category tests ────────────────────────────────────────────────────────

#[test]
Expand Down
2 changes: 1 addition & 1 deletion crates/openlogi-inject/examples/inject_action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ fn print_usage() {
ScrollUp ScrollDown HorizontalScrollLeft HorizontalScrollRight\n\
CustomShortcut:<mod_hex>:<key_hex>\n\
\n\
CustomShortcut modifier bits: 0x01=Cmd/Ctrl 0x02=Shift 0x04=Ctrl 0x08=Option/Alt\n\
CustomShortcut modifier bits: 0x01=Cmd/Ctrl 0x02=Shift 0x04=Ctrl 0x08=Option/Alt 0x10=Fn (macOS)\n\
CustomShortcut key_hex: macOS kVK_* code (e.g. 0x08=C, 0x09=V, 0x7E=Up)\n\
\n\
Examples:\n\
Expand Down
120 changes: 103 additions & 17 deletions crates/openlogi-inject/src/inject/macos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use core_graphics::event::{
use core_graphics::event_source::{CGEventSource, CGEventSourceStateID};
use core_graphics::geometry::CGPoint;

use openlogi_core::binding::Action;
use openlogi_core::binding::{Action, KeyCombo};

// NX_KEYTYPE_* constants from <IOKit/hidsystem/ev_keymap.h>.
const NX_KEYTYPE_SOUND_UP: i32 = 0;
Expand All @@ -31,11 +31,56 @@ const VK_W: u16 = 0x0D;
const VK_X: u16 = 0x07;
const VK_Z: u16 = 0x06;
const VK_TAB: u16 = 0x30;
const VK_HOME: u16 = 0x73;
const VK_PAGE_UP: u16 = 0x74;
const VK_END: u16 = 0x77;
const VK_PAGE_DOWN: u16 = 0x79;
const VK_LEFT_ARROW: u16 = 0x7B;
const VK_RIGHT_ARROW: u16 = 0x7C;
const VK_DOWN_ARROW: u16 = 0x7D;
const VK_UP_ARROW: u16 = 0x7E;

/// Translate a stored shortcut into the virtual key and flags macOS expects.
///
/// macOS reports Fn+Arrow as the corresponding Home/End/Page Up/Page Down
/// virtual key with the SecondaryFn flag, rather than as a flagged arrow-key
/// event. Reproduce both parts of that representation so apps receive native
/// navigation behavior instead of treating the event like an ordinary arrow.
fn custom_shortcut_event(combo: &KeyCombo) -> (u16, CGEventFlags) {
let mut flags = CGEventFlags::CGEventFlagNull;
if combo.modifiers & KeyCombo::MOD_CMD != 0 {
flags |= CGEventFlags::CGEventFlagCommand;
}
if combo.modifiers & KeyCombo::MOD_SHIFT != 0 {
flags |= CGEventFlags::CGEventFlagShift;
}
if combo.modifiers & KeyCombo::MOD_CTRL != 0 {
flags |= CGEventFlags::CGEventFlagControl;
}
if combo.modifiers & KeyCombo::MOD_OPTION != 0 {
flags |= CGEventFlags::CGEventFlagAlternate;
}
if combo.modifiers & KeyCombo::MOD_FN != 0
|| matches!(combo.key_code, VK_PAGE_UP | VK_PAGE_DOWN)
{
flags |= CGEventFlags::CGEventFlagSecondaryFn;
}
let key_code = if combo.modifiers & KeyCombo::MOD_FN != 0 {
match combo.key_code {
VK_LEFT_ARROW => VK_HOME,
VK_RIGHT_ARROW => VK_END,
VK_UP_ARROW => VK_PAGE_UP,
VK_DOWN_ARROW => VK_PAGE_DOWN,
key_code => key_code,
}
} else {
combo.key_code
};
Comment thread
greptile-apps[bot] marked this conversation as resolved.
(key_code, flags)
}

/// macOS implementation: dispatch to the appropriate event helper.
pub(super) fn execute(action: &Action) {
use openlogi_core::binding::KeyCombo;

// Modifier bit shorthands.
let cmd = CGEventFlags::CGEventFlagCommand;
let shift = CGEventFlags::CGEventFlagShift;
Expand Down Expand Up @@ -131,20 +176,8 @@ pub(super) fn execute(action: &Action) {
);
return;
}
let mut flags = CGEventFlags::CGEventFlagNull;
if combo.modifiers & KeyCombo::MOD_CMD != 0 {
flags |= CGEventFlags::CGEventFlagCommand;
}
if combo.modifiers & KeyCombo::MOD_SHIFT != 0 {
flags |= CGEventFlags::CGEventFlagShift;
}
if combo.modifiers & KeyCombo::MOD_CTRL != 0 {
flags |= CGEventFlags::CGEventFlagControl;
}
if combo.modifiers & KeyCombo::MOD_OPTION != 0 {
flags |= CGEventFlags::CGEventFlagAlternate;
}
post_key(combo.key_code, flags);
let (key_code, flags) = custom_shortcut_event(combo);
post_key(key_code, flags);
}
}
}
Expand Down Expand Up @@ -433,6 +466,59 @@ mod dock {
}
}

#[cfg(test)]
mod tests {
use openlogi_core::binding::KeyCombo;

use super::custom_shortcut_event;

#[test]
fn fn_arrows_use_navigation_keycodes_and_the_secondary_fn_flag() {
for (arrow, navigation_key) in [(0x7B, 0x73), (0x7C, 0x77), (0x7E, 0x74), (0x7D, 0x79)] {
let combo = KeyCombo {
modifiers: KeyCombo::MOD_FN,
key_code: arrow,
display: String::new(),
};

let (key_code, flags) = custom_shortcut_event(&combo);

assert_eq!(key_code, navigation_key);
assert!(flags.contains(core_graphics::event::CGEventFlags::CGEventFlagSecondaryFn));
}
}

#[test]
fn fn_modifier_preserves_non_arrow_keycodes() {
let combo = KeyCombo {
modifiers: KeyCombo::MOD_FN,
key_code: 0x23, // kVK_ANSI_P
display: String::new(),
};

let (key_code, flags) = custom_shortcut_event(&combo);

assert_eq!(key_code, 0x23);
assert!(flags.contains(core_graphics::event::CGEventFlags::CGEventFlagSecondaryFn));
}

#[test]
fn page_keys_preserve_the_secondary_fn_flag_used_by_macos() {
for key_code in [0x74, 0x79] {
let combo = KeyCombo {
modifiers: 0,
key_code,
display: String::new(),
};

let (actual_key_code, flags) = custom_shortcut_event(&combo);

assert_eq!(actual_key_code, key_code);
assert!(flags.contains(core_graphics::event::CGEventFlags::CGEventFlagSecondaryFn));
}
}
}

/// macOS Space switching actions.
///
/// Use the system symbolic hotkey records for "Move left a space" (79) and
Expand Down
14 changes: 14 additions & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,17 @@ brightness = 80
Action names are the catalog's variant names (`LeftClick`, `MouseBack`,
`Copy`, `PlayPause`, `CycleDpiPresets`, …). Custom keyboard shortcuts are
currently hand-authored as a `CustomShortcut` table in the TOML file.

The modifier mask uses `1` for Command, `2` for Shift, `4` for Control, `8`
for Option, and `16` for Fn/Globe on macOS. For example, these bindings send
Fn+Down and Fn+Up:

```toml
[devices.2b042.bindings]
Back = { CustomShortcut = { modifiers = 16, key_code = 125, display = "fn↓" } }
Forward = { CustomShortcut = { modifiers = 16, key_code = 126, display = "fn↑" } }
```

macOS Page Down (`key_code = 121`) and Page Up (`key_code = 116`) shortcuts
also preserve the SecondaryFn event flag automatically, matching the events
produced by the corresponding physical Fn+arrow combinations.