From 1361b67bed9ce868e0a82788f090085b923b4ce0 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Thu, 22 Oct 2026 20:55:42 -0700 Subject: [PATCH] Post real modifier key presses around typed keys in hands --- hands/Sources/byre-hands/Input.swift | 73 ++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 21 deletions(-) diff --git a/hands/Sources/byre-hands/Input.swift b/hands/Sources/byre-hands/Input.swift index a86168a..589b5f8 100644 --- a/hands/Sources/byre-hands/Input.swift +++ b/hands/Sources/byre-hands/Input.swift @@ -12,10 +12,20 @@ let keyCodes: [String: CGKeyCode] = [ "forwarddelete": 117, "end": 119, "pagedown": 121, "left": 123, "right": 124, "down": 125, "up": 126, ] -let modifierFlags: [String: CGEventFlags] = [ - "cmd": .maskCommand, "command": .maskCommand, "shift": .maskShift, "alt": .maskAlternate, - "opt": .maskAlternate, "option": .maskAlternate, "ctrl": .maskControl, "control": .maskControl, - "fn": .maskSecondaryFn, +struct Modifier: Equatable { + let code: CGKeyCode + let flag: CGEventFlags +} + +// Each left-hand key's flag also carries its device-dependent bit (NX_DEVICEL*KEYMASK), as a real keyboard's does. +let shiftKey = Modifier(code: 56, flag: CGEventFlags(rawValue: CGEventFlags.maskShift.rawValue | 0x2)) +let commandKey = Modifier(code: 55, flag: CGEventFlags(rawValue: CGEventFlags.maskCommand.rawValue | 0x8)) +let optionKey = Modifier(code: 58, flag: CGEventFlags(rawValue: CGEventFlags.maskAlternate.rawValue | 0x20)) +let controlKey = Modifier(code: 59, flag: CGEventFlags(rawValue: CGEventFlags.maskControl.rawValue | 0x1)) + +let modifiers: [String: Modifier] = [ + "cmd": commandKey, "command": commandKey, "shift": shiftKey, "alt": optionKey, "opt": optionKey, + "option": optionKey, "ctrl": controlKey, "control": controlKey, "fn": Modifier(code: 63, flag: .maskSecondaryFn), ] func pause(_ ms: Int) async throws { @@ -29,15 +39,15 @@ let shifted: [Character: String] = [ let named: [Character: String] = [" ": "space", "\n": "return", "\t": "tab"] -func keystroke(_ ch: Character) -> (CGKeyCode, CGEventFlags)? { +func keystroke(_ ch: Character) -> (CGKeyCode, [Modifier])? { if let name = named[ch], let c = keyCodes[name] { return (c, []) } if let base = shifted[ch], let c = keyCodes[base] { - return (c, .maskShift) + return (c, [shiftKey]) } if ch.isUppercase, let c = keyCodes[ch.lowercased()] { - return (c, .maskShift) + return (c, [shiftKey]) } if let c = keyCodes[String(ch)] { return (c, []) @@ -45,6 +55,30 @@ func keystroke(_ ch: Character) -> (CGKeyCode, CGEventFlags)? { return nil } +func postModifier(_ m: Modifier, down: Bool, _ flags: CGEventFlags) { + let e = CGEvent(keyboardEventSource: nil, virtualKey: m.code, keyDown: down) + e?.type = .flagsChanged + e?.flags = flags + post(e) +} + +func press(_ code: CGKeyCode, _ mods: [Modifier]) { + var flags: CGEventFlags = [] + for m in mods { + flags.insert(m.flag) + postModifier(m, down: true, flags) + } + for down in [true, false] { + let e = CGEvent(keyboardEventSource: nil, virtualKey: code, keyDown: down) + e?.flags = flags + post(e) + } + for m in mods.reversed() { + flags.remove(m.flag) + postModifier(m, down: false, flags) + } +} + func lockInput() async throws -> Int32 { let fd = open(NSTemporaryDirectory() + "byre-hands-input.lock", O_CREAT | O_RDWR, 0o600) if fd < 0 { @@ -132,13 +166,12 @@ func scroll(_ p: CGPoint, dx: Int32, dy: Int32) { func type(_ s: String) async throws { for ch in s { + if let (code, mods) = keystroke(ch) { + press(code, mods) + try await pause(15) + continue + } for down in [true, false] { - if let (code, flags) = keystroke(ch) { - let e = CGEvent(keyboardEventSource: nil, virtualKey: code, keyDown: down) - e?.flags = flags - post(e) - continue - } let units = Array(String(ch).utf16) let e = CGEvent(keyboardEventSource: nil, virtualKey: 0, keyDown: down) e?.keyboardSetUnicodeString(stringLength: units.count, unicodeString: units) @@ -149,11 +182,13 @@ func type(_ s: String) async throws { } func key(_ combo: String) throws { - var flags: CGEventFlags = [] + var mods: [Modifier] = [] var code: CGKeyCode? for part in combo.lowercased().split(separator: "+").map(String.init) { - if let f = modifierFlags[part] { - flags.insert(f) + if let m = modifiers[part] { + if !mods.contains(m) { + mods.append(m) + } continue } guard let c = keyCodes[part] else { @@ -164,9 +199,5 @@ func key(_ combo: String) throws { guard let code else { throw ToolError("no key in \(combo.debugDescription)") } - for down in [true, false] { - let e = CGEvent(keyboardEventSource: nil, virtualKey: code, keyDown: down) - e?.flags = flags - post(e) - } + press(code, mods) }