(keyNames: string[], durationMs: number)
| 590 | }, |
| 591 | |
| 592 | async holdKey(keyNames: string[], durationMs: number): Promise<void> { |
| 593 | const input = getInput() |
| 594 | // Press/release each wrapped in drainRunLoop; the sleep sits outside so |
| 595 | // durationMs isn't bounded by drainRunLoop's 30s timeout. `pressed` |
| 596 | // tracks which presses landed so a mid-press throw still releases |
| 597 | // everything that was actually pressed. |
| 598 | // |
| 599 | // `orphaned` guards against a timeout-orphan race: if the press-phase |
| 600 | // drainRunLoop times out while the esc-hotkey pump-retain keeps the |
| 601 | // pump running, the orphaned lambda would continue pushing to `pressed` |
| 602 | // after finally's releasePressed snapshotted the length — leaving keys |
| 603 | // stuck. The flag stops the lambda at the next iteration. |
| 604 | const pressed: string[] = [] |
| 605 | let orphaned = false |
| 606 | try { |
| 607 | await drainRunLoop(async () => { |
| 608 | for (const k of keyNames) { |
| 609 | if (orphaned) return |
| 610 | // Bare Escape: notify the CGEventTap so it doesn't fire the |
| 611 | // abort callback for a model-synthesized press. Same as key(). |
| 612 | if (isBareEscape([k])) { |
| 613 | notifyExpectedEscape() |
| 614 | } |
| 615 | await input.key(k, 'press') |
| 616 | pressed.push(k) |
| 617 | } |
| 618 | }) |
| 619 | await sleep(durationMs) |
| 620 | } finally { |
| 621 | orphaned = true |
| 622 | await drainRunLoop(() => releasePressed(input, pressed)) |
| 623 | } |
| 624 | }, |
| 625 | |
| 626 | async type(text: string, opts: { viaClipboard: boolean }): Promise<void> { |
| 627 | // Windows/Linux: route through platform which handles HWND binding |
nothing calls this directly
no test coverage detected