fix(input): type printable characters with key events
This commit is contained in:
parent
b6df8b09e5
commit
fbb2a577c9
@ -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.
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user