From 1d94433cff51c567d4122293f05d47b10aa90b81 Mon Sep 17 00:00:00 2001 From: Owen McGirr Date: Sat, 27 Jun 2026 09:33:42 +0100 Subject: [PATCH] Expose Meta as keyboard key --- src/main/input/command-executor.test.ts | 20 +++++++++++++++++++- src/main/input/libnut-win32-adapter.test.ts | 4 ++++ src/shared/protocol.test.ts | 20 ++++++++++++++++++-- src/shared/protocol.ts | 2 ++ 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/main/input/command-executor.test.ts b/src/main/input/command-executor.test.ts index 52f07dd..ee4c47c 100644 --- a/src/main/input/command-executor.test.ts +++ b/src/main/input/command-executor.test.ts @@ -131,6 +131,7 @@ describe('DesktopCommandExecutor', () => { await executor.execute(command('keyboard.key', { key: 'Enter' })); await executor.execute(command('keyboard.key', { key: 'F12' })); + await executor.execute(command('keyboard.key', { key: 'Meta' })); 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' })); @@ -142,6 +143,7 @@ describe('DesktopCommandExecutor', () => { expect(adapter.calls).toEqual([ { method: 'pressKey', args: ['Enter'] }, { method: 'pressKey', args: ['F12'] }, + { method: 'pressKey', args: ['Meta'] }, { method: 'pressShortcut', args: [['Ctrl', 'C']] }, { method: 'pressShortcut', args: [['Ctrl', 'F5']] }, { method: 'typeText', args: ['Hello'] }, @@ -150,7 +152,7 @@ describe('DesktopCommandExecutor', () => { ]); expect(overlay.events).toHaveLength(0); expect(overlay.activeCount).toBe(0); - expect(overlay.hideCount).toBe(8); + expect(overlay.hideCount).toBe(9); }); it('executes non-movement no-response commands without coalescing', async () => { @@ -202,6 +204,22 @@ describe('DesktopCommandExecutor', () => { ]); }); + it('streams Meta as a key before closing successfully', async () => { + const { adapter, executor } = createExecutor(); + + await expect(executor.execute(command('keyboard.textStream.open', { streamId: 'stream-1' }))).resolves.toEqual({ + ok: true + }); + await expect( + executor.execute(command('keyboard.textStream.key', { streamId: 'stream-1', seq: 0, key: 'Meta' }, { responseMode: 'none' })) + ).resolves.toEqual({ ok: true }); + await expect(executor.execute(command('keyboard.textStream.close', { streamId: 'stream-1', expectedCount: 1 }))).resolves.toEqual({ + ok: true + }); + + expect(adapter.calls).toEqual([{ method: 'pressKey', args: ['Meta'] }]); + }); + it('streams ACKed text chunks before closing successfully', async () => { const { adapter, executor } = createExecutor(); diff --git a/src/main/input/libnut-win32-adapter.test.ts b/src/main/input/libnut-win32-adapter.test.ts index 87bb71a..915a13d 100644 --- a/src/main/input/libnut-win32-adapter.test.ts +++ b/src/main/input/libnut-win32-adapter.test.ts @@ -361,6 +361,10 @@ describe('toLibnutKeyboardKey', () => { expect(toLibnutKeyboardKey('F1')).toBe('f1'); expect(toLibnutKeyboardKey('F12')).toBe('f12'); }); + + it('maps Meta to the libnut command key', () => { + expect(toLibnutKeyboardKey('Meta')).toBe('command'); + }); }); describe('toWindowsWindowControlStrategy', () => { diff --git a/src/shared/protocol.test.ts b/src/shared/protocol.test.ts index 6396d9d..e62e153 100644 --- a/src/shared/protocol.test.ts +++ b/src/shared/protocol.test.ts @@ -32,6 +32,7 @@ describe('protocol request validation', () => { { type: 'mouse.dragStart', payload: { button: 'left' } }, { type: 'mouse.dragEnd', payload: { button: 'left' } }, { type: 'keyboard.key', payload: { key: 'Enter' } }, + { type: 'keyboard.key', payload: { key: 'Meta' } }, { type: 'keyboard.shortcut', payload: { keys: ['Ctrl', 'C'] } }, { type: 'keyboard.key', payload: { key: 'F1' } }, { type: 'keyboard.key', payload: { key: 'F12' } }, @@ -41,6 +42,7 @@ describe('protocol request validation', () => { { type: 'keyboard.textStream.char', payload: { streamId: 'android-stream-1', seq: 0, text: 'H' } }, { type: 'keyboard.textStream.chunk', payload: { streamId: 'android-stream-1', seq: 0, text: 'Hello' } }, { type: 'keyboard.textStream.key', payload: { streamId: 'android-stream-1', seq: 1, key: 'Enter' } }, + { type: 'keyboard.textStream.key', payload: { streamId: 'android-stream-1', seq: 2, key: 'Meta' } }, { type: 'keyboard.textStream.close', payload: { streamId: 'android-stream-1', expectedCount: 2 } }, { type: 'media.control', payload: { action: 'playPause' } }, { type: 'window.control', payload: { action: 'switchNext' } }, @@ -63,11 +65,11 @@ describe('protocol request validation', () => { 'mouse.scroll': { dx: 0, dy: -3 }, 'mouse.dragStart': { button: 'left' }, 'mouse.dragEnd': { button: 'left' }, - 'keyboard.key': { key: 'Enter' }, + 'keyboard.key': { key: 'Meta' }, 'keyboard.shortcut': { keys: ['Ctrl', 'C'] }, 'keyboard.typeText': { text: 'Hello' }, 'keyboard.textStream.char': { streamId: 'android-stream-1', seq: 0, text: 'H' }, - 'keyboard.textStream.key': { streamId: 'android-stream-1', seq: 1, key: 'Enter' }, + 'keyboard.textStream.key': { streamId: 'android-stream-1', seq: 1, key: 'Meta' }, 'media.control': { action: 'playPause' }, 'window.control': { action: 'switchNext' } }; @@ -114,6 +116,20 @@ describe('protocol request validation', () => { ).toMatchObject({ ok: false, error: 'invalid_payload' }); }); + it('accepts Meta as a keyboard key', () => { + expect(validateProtocolRequest({ ...baseCommand, type: 'keyboard.key', payload: { key: 'Meta' } })).toMatchObject({ + ok: true + }); + + expect( + validateProtocolRequest({ + ...baseCommand, + type: 'keyboard.textStream.key', + payload: { streamId: 'stream-1', seq: 0, key: 'Meta' } + }) + ).toMatchObject({ ok: true }); + }); + it('accepts pairing approval requests without command auth fields', () => { expect( validateProtocolRequest({ diff --git a/src/shared/protocol.ts b/src/shared/protocol.ts index fba14a0..0f6c6c5 100644 --- a/src/shared/protocol.ts +++ b/src/shared/protocol.ts @@ -27,6 +27,7 @@ export type KeyboardKey = | 'Escape' | 'Space' | 'Tab' + | 'Meta' | 'ArrowUp' | 'ArrowDown' | 'ArrowLeft' @@ -279,6 +280,7 @@ const keyboardKeys = new Set([ 'Escape', 'Space', 'Tab', + 'Meta', 'ArrowUp', 'ArrowDown', 'ArrowLeft',