* xdotool-style sequence e.g. "ctrl+shift+a" → split on '+' and pass to * keys(). keys() dispatches to DispatchQueue.main — drainRunLoop pumps * CFRunLoop so it resolves. Rust's error-path cleanup (enigo_wrap.rs) * releases modifiers on each invocation, so a mid-loop throw leaves
(keySequence: string, repeat?: number)
| 453 | * nothing stuck. 8ms between iterations — 125Hz USB polling cadence. |
| 454 | */ |
| 455 | async key(keySequence: string, repeat?: number): Promise<void> { |
| 456 | const input = requireComputerUseInput() |
| 457 | const parts = keySequence.split('+').filter(p => p.length > 0) |
| 458 | // Bare-only: the CGEventTap checks event.flags.isEmpty so ctrl+escape |
| 459 | // etc. pass through without aborting. |
| 460 | const isEsc = isBareEscape(parts) |
| 461 | const n = repeat ?? 1 |
| 462 | await drainRunLoop(async () => { |
| 463 | for (let i = 0; i < n; i++) { |
| 464 | if (i > 0) { |
| 465 | await sleep(8) |
| 466 | } |
| 467 | if (isEsc) { |
| 468 | notifyExpectedEscape() |
| 469 | } |
| 470 | await input.keys(parts) |
| 471 | } |
| 472 | }) |
| 473 | }, |
| 474 | |
| 475 | async holdKey(keyNames: string[], durationMs: number): Promise<void> { |
| 476 | const input = requireComputerUseInput() |
nothing calls this directly
no test coverage detected