diff --git a/src/main/input/command-executor.test.ts b/src/main/input/command-executor.test.ts index 9eede59..8855ed0 100644 --- a/src/main/input/command-executor.test.ts +++ b/src/main/input/command-executor.test.ts @@ -121,7 +121,9 @@ describe('DesktopCommandExecutor', () => { const { adapter, executor, overlay } = createExecutor(); await executor.execute(command('keyboard.key', { key: 'Enter' })); + await executor.execute(command('keyboard.key', { key: 'F12' })); await executor.execute(command('keyboard.shortcut', { keys: ['Ctrl', 'C'] })); + await executor.execute(command('keyboard.shortcut', { keys: ['Ctrl', 'F5'] })); await executor.execute(command('keyboard.typeText', { text: 'Hello' })); await executor.execute(command('media.control', { action: 'playPause' })); await executor.execute(command('window.control', { action: 'switchNext' })); @@ -130,14 +132,16 @@ describe('DesktopCommandExecutor', () => { expect(pingResult).toEqual({ ok: true }); expect(adapter.calls).toEqual([ { method: 'pressKey', args: ['Enter'] }, + { method: 'pressKey', args: ['F12'] }, { method: 'pressShortcut', args: [['Ctrl', 'C']] }, + { method: 'pressShortcut', args: [['Ctrl', 'F5']] }, { method: 'typeText', args: ['Hello'] }, { method: 'mediaControl', args: ['playPause'] }, { method: 'controlWindow', args: ['switchNext'] } ]); expect(overlay.events).toHaveLength(0); expect(overlay.activeCount).toBe(0); - expect(overlay.hideCount).toBe(6); + expect(overlay.hideCount).toBe(8); }); it('executes non-movement no-response commands without coalescing', async () => { @@ -145,13 +149,13 @@ describe('DesktopCommandExecutor', () => { await executor.execute(command('mouse.click', { button: 'left' }, { responseMode: 'none' })); await executor.execute(command('mouse.scroll', { dx: 0, dy: -3 }, { responseMode: 'none' })); - await executor.execute(command('keyboard.key', { key: 'Enter' }, { responseMode: 'none' })); + await executor.execute(command('keyboard.key', { key: 'F12' }, { responseMode: 'none' })); await executor.execute(command('window.control', { action: 'switchNext' }, { responseMode: 'none' })); expect(adapter.calls).toEqual([ { method: 'clickMouse', args: ['left'] }, { method: 'scrollMouse', args: [{ dx: 0, dy: -3 }] }, - { method: 'pressKey', args: ['Enter'] }, + { method: 'pressKey', args: ['F12'] }, { method: 'controlWindow', args: ['switchNext'] } ]); expect(overlay.events).toEqual(['click']); diff --git a/src/main/input/libnut-win32-adapter.test.ts b/src/main/input/libnut-win32-adapter.test.ts index 722ba76..9ff089e 100644 --- a/src/main/input/libnut-win32-adapter.test.ts +++ b/src/main/input/libnut-win32-adapter.test.ts @@ -1,5 +1,10 @@ import { describe, expect, it } from 'vitest'; -import { calculateNativeScrollDelta, calculateScaledMouseTarget, toLibnutMouseToggle } from './libnut-win32-adapter'; +import { + calculateNativeScrollDelta, + calculateScaledMouseTarget, + toLibnutKeyboardKey, + toLibnutMouseToggle +} from './libnut-win32-adapter'; import { createWindowControlScript, toWindowsWindowControlStrategy @@ -90,6 +95,17 @@ describe('toLibnutMouseToggle', () => { }); }); +describe('toLibnutKeyboardKey', () => { + it('maps navigation keys to libnut key values', () => { + expect(toLibnutKeyboardKey('PageUp')).toBe('pageup'); + }); + + it('maps function keys to libnut key values', () => { + expect(toLibnutKeyboardKey('F1')).toBe('f1'); + expect(toLibnutKeyboardKey('F12')).toBe('f12'); + }); +}); + describe('toWindowsWindowControlStrategy', () => { it('leaves app switching on the known-good libnut path', () => { expect(toWindowsWindowControlStrategy('switchNext')).toBeNull(); diff --git a/src/main/input/libnut-win32-adapter.ts b/src/main/input/libnut-win32-adapter.ts index 69fdbf5..1d8f409 100644 --- a/src/main/input/libnut-win32-adapter.ts +++ b/src/main/input/libnut-win32-adapter.ts @@ -53,17 +53,17 @@ export class LibnutWin32InputAdapter implements DesktopInputAdapter { } async pressKey(key: KeyboardKey): Promise { - keyTap(toLibnutKey(key)); + keyTap(toLibnutKeyboardKey(key)); } async pressShortcut(keys: ShortcutKey[]): Promise { if (keys.length === 1) { - keyTap(toLibnutKey(keys[0])); + keyTap(toLibnutKeyboardKey(keys[0])); return; } const [key, ...modifiers] = [...keys].reverse(); - keyTap(toLibnutKey(key), modifiers.map(toLibnutKey)); + keyTap(toLibnutKeyboardKey(key), modifiers.map(toLibnutKeyboardKey)); } async typeText(text: string): Promise { @@ -150,7 +150,7 @@ function toLibnutMouseButton(button: MouseButton): string { } } -function toLibnutKey(key: KeyboardKey | ShortcutKey): string { +export function toLibnutKeyboardKey(key: KeyboardKey | ShortcutKey): string { switch (key) { case 'Backspace': return 'backspace'; @@ -180,6 +180,19 @@ function toLibnutKey(key: KeyboardKey | ShortcutKey): string { return 'pageup'; case 'PageDown': return 'pagedown'; + case 'F1': + case 'F2': + case 'F3': + case 'F4': + case 'F5': + case 'F6': + case 'F7': + case 'F8': + case 'F9': + case 'F10': + case 'F11': + case 'F12': + return key.toLowerCase(); case 'Ctrl': return 'control'; case 'Alt': diff --git a/src/shared/protocol.test.ts b/src/shared/protocol.test.ts index c55e33f..3f4bd73 100644 --- a/src/shared/protocol.test.ts +++ b/src/shared/protocol.test.ts @@ -33,6 +33,9 @@ describe('protocol request validation', () => { { type: 'mouse.dragEnd', payload: { button: 'left' } }, { type: 'keyboard.key', payload: { key: 'Enter' } }, { type: 'keyboard.shortcut', payload: { keys: ['Ctrl', 'C'] } }, + { type: 'keyboard.key', payload: { key: 'F1' } }, + { type: 'keyboard.key', payload: { key: 'F12' } }, + { type: 'keyboard.shortcut', payload: { keys: ['Ctrl', 'F5'] } }, { type: 'keyboard.typeText', payload: { text: 'Hello' } }, { type: 'media.control', payload: { action: 'playPause' } }, { type: 'window.control', payload: { action: 'switchNext' } }, @@ -196,6 +199,10 @@ describe('protocol request validation', () => { ok: false, error: 'invalid_payload' }); + expect(validateProtocolRequest({ ...baseCommand, type: 'keyboard.key', payload: { key: 'F13' } })).toMatchObject({ + ok: false, + error: 'invalid_payload' + }); expect(validateProtocolRequest({ ...baseCommand, type: 'keyboard.typeText', payload: { text: 'x'.repeat(2_001) } })).toMatchObject({ ok: false, error: 'invalid_payload' diff --git a/src/shared/protocol.ts b/src/shared/protocol.ts index c0e20f4..fa8178c 100644 --- a/src/shared/protocol.ts +++ b/src/shared/protocol.ts @@ -31,7 +31,19 @@ export type KeyboardKey = | 'Home' | 'End' | 'PageUp' - | 'PageDown'; + | 'PageDown' + | 'F1' + | 'F2' + | 'F3' + | 'F4' + | 'F5' + | 'F6' + | 'F7' + | 'F8' + | 'F9' + | 'F10' + | 'F11' + | 'F12'; export type ShortcutKey = | KeyboardKey @@ -241,7 +253,19 @@ const keyboardKeys = new Set([ 'Home', 'End', 'PageUp', - 'PageDown' + 'PageDown', + 'F1', + 'F2', + 'F3', + 'F4', + 'F5', + 'F6', + 'F7', + 'F8', + 'F9', + 'F10', + 'F11', + 'F12' ]); const shortcutKeys = new Set([ ...keyboardKeys,