fix(input): type printable characters with key events

This commit is contained in:
Peter Steinberger 2026-05-08 12:07:24 +01:00
parent b6df8b09e5
commit fbb2a577c9
No known key found for this signature in database
3 changed files with 134 additions and 0 deletions

View File

@ -5,6 +5,7 @@ All notable changes to AXorcist will be documented in this file.
## [Unreleased]
### Fixed
- Printable ASCII typing now uses physical key events before falling back to Unicode events, improving reliability in VM and headless launch paths.
- Unsupported command dispatch now returns an `unknown_command` error instead of trapping, and JSON path hint parsing no longer writes warnings to stdout for unknown attributes.
- Preserve CFRange-backed AXValue attributes such as selected text ranges instead of misclassifying raw value 4 as a boolean. Thanks @WinnCook.

View File

@ -181,6 +181,104 @@ extension Element {
/// Type a single character
@MainActor public static func typeCharacter(_ character: Character) throws {
// Physical key events survive VM/headless launch paths that can silently drop Unicode-only events.
if let stroke = self.keyboardStroke(for: character) {
try self.postKeyboardStroke(stroke)
return
}
try self.postUnicodeCharacter(character)
}
static func keyboardStroke(for character: Character) -> (keyCode: CGKeyCode, flags: CGEventFlags)? {
let string = String(character)
guard string.count == 1 else { return nil }
if let scalar = string.unicodeScalars.first,
CharacterSet.lowercaseLetters.contains(scalar),
let key = SpecialKey(rawValue: string),
let keyCode = key.keyCode
{
return (keyCode, [])
}
if let scalar = string.unicodeScalars.first,
CharacterSet.uppercaseLetters.contains(scalar),
let key = SpecialKey(rawValue: string.lowercased()),
let keyCode = key.keyCode
{
return (keyCode, .maskShift)
}
if let key = SpecialKey(rawValue: string),
let keyCode = key.keyCode
{
return (keyCode, [])
}
let shiftedSymbols: [Character: CGKeyCode] = [
"!": 18,
"@": 19,
"#": 20,
"$": 21,
"%": 23,
"^": 22,
"&": 26,
"*": 28,
"(": 25,
")": 29,
"_": 27,
"+": 24,
"{": 33,
"}": 30,
"|": 42,
":": 41,
"\"": 39,
"<": 43,
">": 47,
"?": 44,
"~": 50,
]
if let keyCode = shiftedSymbols[character] {
return (keyCode, .maskShift)
}
let symbols: [Character: CGKeyCode] = [
" ": 49,
"-": 27,
"=": 24,
"[": 33,
"]": 30,
"\\": 42,
";": 41,
"'": 39,
",": 43,
".": 47,
"/": 44,
"`": 50,
]
if let keyCode = symbols[character] {
return (keyCode, [])
}
return nil
}
private static func postKeyboardStroke(_ stroke: (keyCode: CGKeyCode, flags: CGEventFlags)) throws {
guard let keyDown = CGEvent(keyboardEventSource: nil, virtualKey: stroke.keyCode, keyDown: true),
let keyUp = CGEvent(keyboardEventSource: nil, virtualKey: stroke.keyCode, keyDown: false)
else {
throw UIAutomationError.failedToCreateEvent
}
keyDown.flags = stroke.flags
keyUp.flags = stroke.flags
keyDown.post(tap: .cghidEventTap)
Thread.sleep(forTimeInterval: 0.001)
keyUp.post(tap: .cghidEventTap)
}
private static func postUnicodeCharacter(_ character: Character) throws {
let string = String(character)
// Create keyboard event

View File

@ -18,4 +18,39 @@ struct InputDriverTests {
// If running in CI without UI, location may be nil; just assert cache mirrors result.
#expect(cache == InputDriver.currentLocation())
}
@Test("keyboardStroke maps printable ASCII to physical key events")
@MainActor
func keyboardStrokeMapsPrintableASCII() throws {
let lower = try #require(Element.keyboardStroke(for: "a"))
#expect(lower.keyCode == 0)
#expect(!lower.flags.contains(.maskShift))
let upper = try #require(Element.keyboardStroke(for: "A"))
#expect(upper.keyCode == 0)
#expect(upper.flags.contains(.maskShift))
let digit = try #require(Element.keyboardStroke(for: "1"))
#expect(digit.keyCode == 18)
#expect(!digit.flags.contains(.maskShift))
let symbol = try #require(Element.keyboardStroke(for: "!"))
#expect(symbol.keyCode == 18)
#expect(symbol.flags.contains(.maskShift))
let hyphen = try #require(Element.keyboardStroke(for: "-"))
#expect(hyphen.keyCode == 27)
#expect(!hyphen.flags.contains(.maskShift))
let underscore = try #require(Element.keyboardStroke(for: "_"))
#expect(underscore.keyCode == 27)
#expect(underscore.flags.contains(.maskShift))
}
@Test("keyboardStroke leaves non-ASCII for Unicode fallback")
@MainActor
func keyboardStrokeLeavesNonASCIIForUnicodeFallback() {
#expect(Element.keyboardStroke(for: "é") == nil)
#expect(Element.keyboardStroke(for: "🙂") == nil)
}
}