Post real modifier key presses around typed keys in hands

This commit is contained in:
Ian Gulliver
2026-10-22 20:55:42 -07:00
parent 5fc8a88a82
commit 1361b67bed
+52 -21
View File
@@ -12,10 +12,20 @@ let keyCodes: [String: CGKeyCode] = [
"forwarddelete": 117, "end": 119, "pagedown": 121, "left": 123, "right": 124, "down": 125, "up": 126, "forwarddelete": 117, "end": 119, "pagedown": 121, "left": 123, "right": 124, "down": 125, "up": 126,
] ]
let modifierFlags: [String: CGEventFlags] = [ struct Modifier: Equatable {
"cmd": .maskCommand, "command": .maskCommand, "shift": .maskShift, "alt": .maskAlternate, let code: CGKeyCode
"opt": .maskAlternate, "option": .maskAlternate, "ctrl": .maskControl, "control": .maskControl, let flag: CGEventFlags
"fn": .maskSecondaryFn, }
// 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 { func pause(_ ms: Int) async throws {
@@ -29,15 +39,15 @@ let shifted: [Character: String] = [
let named: [Character: String] = [" ": "space", "\n": "return", "\t": "tab"] 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] { if let name = named[ch], let c = keyCodes[name] {
return (c, []) return (c, [])
} }
if let base = shifted[ch], let c = keyCodes[base] { if let base = shifted[ch], let c = keyCodes[base] {
return (c, .maskShift) return (c, [shiftKey])
} }
if ch.isUppercase, let c = keyCodes[ch.lowercased()] { if ch.isUppercase, let c = keyCodes[ch.lowercased()] {
return (c, .maskShift) return (c, [shiftKey])
} }
if let c = keyCodes[String(ch)] { if let c = keyCodes[String(ch)] {
return (c, []) return (c, [])
@@ -45,6 +55,30 @@ func keystroke(_ ch: Character) -> (CGKeyCode, CGEventFlags)? {
return nil 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 { func lockInput() async throws -> Int32 {
let fd = open(NSTemporaryDirectory() + "byre-hands-input.lock", O_CREAT | O_RDWR, 0o600) let fd = open(NSTemporaryDirectory() + "byre-hands-input.lock", O_CREAT | O_RDWR, 0o600)
if fd < 0 { if fd < 0 {
@@ -132,13 +166,12 @@ func scroll(_ p: CGPoint, dx: Int32, dy: Int32) {
func type(_ s: String) async throws { func type(_ s: String) async throws {
for ch in s { for ch in s {
if let (code, mods) = keystroke(ch) {
press(code, mods)
try await pause(15)
continue
}
for down in [true, false] { 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 units = Array(String(ch).utf16)
let e = CGEvent(keyboardEventSource: nil, virtualKey: 0, keyDown: down) let e = CGEvent(keyboardEventSource: nil, virtualKey: 0, keyDown: down)
e?.keyboardSetUnicodeString(stringLength: units.count, unicodeString: units) e?.keyboardSetUnicodeString(stringLength: units.count, unicodeString: units)
@@ -149,11 +182,13 @@ func type(_ s: String) async throws {
} }
func key(_ combo: String) throws { func key(_ combo: String) throws {
var flags: CGEventFlags = [] var mods: [Modifier] = []
var code: CGKeyCode? var code: CGKeyCode?
for part in combo.lowercased().split(separator: "+").map(String.init) { for part in combo.lowercased().split(separator: "+").map(String.init) {
if let f = modifierFlags[part] { if let m = modifiers[part] {
flags.insert(f) if !mods.contains(m) {
mods.append(m)
}
continue continue
} }
guard let c = keyCodes[part] else { guard let c = keyCodes[part] else {
@@ -164,9 +199,5 @@ func key(_ combo: String) throws {
guard let code else { guard let code else {
throw ToolError("no key in \(combo.debugDescription)") throw ToolError("no key in \(combo.debugDescription)")
} }
for down in [true, false] { press(code, mods)
let e = CGEvent(keyboardEventSource: nil, virtualKey: code, keyDown: down)
e?.flags = flags
post(e)
}
} }