diff --git a/packages/cli/src/ui/AppContainer.test.tsx b/packages/cli/src/ui/AppContainer.test.tsx index 78e9b875aa..cee6e918d7 100644 --- a/packages/cli/src/ui/AppContainer.test.tsx +++ b/packages/cli/src/ui/AppContainer.test.tsx @@ -1450,6 +1450,7 @@ describe('AppContainer State Management', () => { meta: false, shift: false, paste: false, + insertable: false, sequence: '\x13', }); }); @@ -1476,6 +1477,7 @@ describe('AppContainer State Management', () => { meta: false, shift: false, paste: false, + insertable: false, sequence: '\x13', }); }); @@ -1490,6 +1492,7 @@ describe('AppContainer State Management', () => { meta: false, shift: false, paste: false, + insertable: true, sequence: 'a', }); }); @@ -1510,6 +1513,7 @@ describe('AppContainer State Management', () => { meta: false, shift: false, paste: false, + insertable: false, sequence: '\x13', }); }); @@ -1525,6 +1529,7 @@ describe('AppContainer State Management', () => { meta: false, shift: false, paste: false, + insertable: true, sequence: 'a', }); }); diff --git a/packages/cli/src/ui/components/ModelDialog.test.tsx b/packages/cli/src/ui/components/ModelDialog.test.tsx index 2ec9b21c32..5c8338c604 100644 --- a/packages/cli/src/ui/components/ModelDialog.test.tsx +++ b/packages/cli/src/ui/components/ModelDialog.test.tsx @@ -188,6 +188,7 @@ describe('', () => { meta: false, shift: false, paste: false, + insertable: false, sequence: '', }); expect(props.onClose).toHaveBeenCalledTimes(1); @@ -198,6 +199,7 @@ describe('', () => { meta: false, shift: false, paste: false, + insertable: true, sequence: '', }); expect(props.onClose).toHaveBeenCalledTimes(1); diff --git a/packages/cli/src/ui/components/shared/text-buffer.test.ts b/packages/cli/src/ui/components/shared/text-buffer.test.ts index 5ff088a3f0..02bdd9d533 100644 --- a/packages/cli/src/ui/components/shared/text-buffer.test.ts +++ b/packages/cli/src/ui/components/shared/text-buffer.test.ts @@ -977,6 +977,7 @@ describe('useTextBuffer', () => { meta: false, shift: false, paste: false, + insertable: true, sequence: 'h', }), ); @@ -987,6 +988,7 @@ describe('useTextBuffer', () => { meta: false, shift: false, paste: false, + insertable: true, sequence: 'i', }), ); @@ -1004,6 +1006,7 @@ describe('useTextBuffer', () => { meta: false, shift: false, paste: false, + insertable: true, sequence: '\r', }), ); @@ -1021,6 +1024,7 @@ describe('useTextBuffer', () => { meta: false, shift: false, paste: false, + insertable: false, sequence: '\t', }), ); @@ -1038,6 +1042,7 @@ describe('useTextBuffer', () => { meta: false, shift: true, paste: false, + insertable: false, sequence: '\u001b[9;2u', }), ); @@ -1060,6 +1065,7 @@ describe('useTextBuffer', () => { meta: false, shift: false, paste: false, + insertable: false, sequence: '\x7f', }), ); @@ -1084,6 +1090,7 @@ describe('useTextBuffer', () => { meta: false, shift: false, paste: false, + insertable: false, sequence: '\x7f', }); result.current.handleInput({ @@ -1092,6 +1099,7 @@ describe('useTextBuffer', () => { meta: false, shift: false, paste: false, + insertable: false, sequence: '\x7f', }); result.current.handleInput({ @@ -1100,6 +1108,7 @@ describe('useTextBuffer', () => { meta: false, shift: false, paste: false, + insertable: false, sequence: '\x7f', }); }); @@ -1159,6 +1168,7 @@ describe('useTextBuffer', () => { meta: false, shift: false, paste: false, + insertable: false, sequence: '\x1b[D', }), ); // cursor [0,1] @@ -1170,6 +1180,7 @@ describe('useTextBuffer', () => { meta: false, shift: false, paste: false, + insertable: false, sequence: '\x1b[C', }), ); // cursor [0,2] @@ -1189,6 +1200,7 @@ describe('useTextBuffer', () => { meta: false, shift: false, paste: false, + insertable: true, sequence: textWithAnsi, }), ); @@ -1206,6 +1218,7 @@ describe('useTextBuffer', () => { meta: false, shift: true, paste: false, + insertable: true, sequence: '\r', }), ); // Simulates Shift+Enter in VSCode terminal @@ -1410,6 +1423,7 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots meta: false, shift: false, paste: false, + insertable: true, sequence, }); @@ -1468,6 +1482,7 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots meta: false, shift: false, paste: false, + insertable: true, sequence: largeTextWithUnsafe, }), ); @@ -1502,6 +1517,7 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots meta: false, shift: false, paste: false, + insertable: true, sequence: largeTextWithAnsi, }), ); @@ -1526,6 +1542,7 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots meta: false, shift: false, paste: false, + insertable: true, sequence: emojis, }), ); @@ -1717,12 +1734,35 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots meta: false, shift: false, paste: false, + insertable: true, sequence: '\r', }), ); expect(getBufferState(result).lines).toEqual(['']); }); + it('should not print anything for function keys when singleLine is true', () => { + const { result } = renderHook(() => + useTextBuffer({ + viewport, + isValidPath: () => false, + singleLine: true, + }), + ); + act(() => + result.current.handleInput({ + name: 'f1', + ctrl: false, + meta: false, + shift: false, + paste: false, + insertable: false, + sequence: '\u001bOP', + }), + ); + expect(getBufferState(result).lines).toEqual(['']); + }); + it('should strip newlines from pasted text when singleLine is true', () => { const { result } = renderHook(() => useTextBuffer({ diff --git a/packages/cli/src/ui/components/shared/text-buffer.ts b/packages/cli/src/ui/components/shared/text-buffer.ts index b44485cb0c..70f30abc03 100644 --- a/packages/cli/src/ui/components/shared/text-buffer.ts +++ b/packages/cli/src/ui/components/shared/text-buffer.ts @@ -17,6 +17,7 @@ import { stripUnsafeCharacters, getCachedStringWidth, } from '../../utils/textUtils.js'; +import type { Key } from '../../contexts/KeypressContext.js'; import type { VimAction } from './vim-buffer-actions.js'; import { handleVimAction } from './vim-buffer-actions.js'; @@ -1907,14 +1908,7 @@ export function useTextBuffer({ ); const handleInput = useCallback( - (key: { - name: string; - ctrl: boolean; - meta: boolean; - shift: boolean; - paste: boolean; - sequence: string; - }): void => { + (key: Key): void => { const { sequence: input } = key; if (key.paste) { @@ -1964,7 +1958,7 @@ export function useTextBuffer({ else if (key.name === 'delete' || (key.ctrl && key.name === 'd')) del(); else if (key.ctrl && !key.shift && key.name === 'z') undo(); else if (key.ctrl && key.shift && key.name === 'z') redo(); - else if (input && !key.ctrl && !key.meta && key.name !== 'tab') { + else if (key.insertable) { insert(input, { paste: key.paste }); } }, @@ -2266,14 +2260,7 @@ export interface TextBuffer { /** * High level "handleInput" – receives what Ink gives us. */ - handleInput: (key: { - name: string; - ctrl: boolean; - meta: boolean; - shift: boolean; - paste: boolean; - sequence: string; - }) => void; + handleInput: (key: Key) => void; /** * Opens the current buffer contents in the user's preferred terminal text * editor ($VISUAL or $EDITOR, falling back to "vi"). The method blocks diff --git a/packages/cli/src/ui/contexts/KeypressContext.tsx b/packages/cli/src/ui/contexts/KeypressContext.tsx index a48231a463..1d1fcbbcde 100644 --- a/packages/cli/src/ui/contexts/KeypressContext.tsx +++ b/packages/cli/src/ui/contexts/KeypressContext.tsx @@ -224,6 +224,7 @@ function bufferPaste( meta: false, shift: false, paste: true, + insertable: true, sequence: buffer, }); } @@ -273,6 +274,7 @@ function* emitKeys( let meta = false; let shift = false; let code = undefined; + let insertable = false; if (ch === ESC) { escaped = true; @@ -455,6 +457,7 @@ function* emitKeys( } else if (ch === ' ') { name = 'space'; meta = escaped; + insertable = true; } else if (!escaped && ch <= '\x1a') { // ctrl+letter name = String.fromCharCode(ch.charCodeAt(0) + 'a'.charCodeAt(0) - 1); @@ -464,6 +467,7 @@ function* emitKeys( name = ch.toLowerCase(); shift = /^[A-Z]$/.exec(ch) !== null; meta = escaped; + insertable = true; } else if (MAC_ALT_KEY_CHARACTER_MAP[ch] && process.platform === 'darwin') { name = MAC_ALT_KEY_CHARACTER_MAP[ch]; meta = true; @@ -479,12 +483,16 @@ function* emitKeys( meta, shift, paste: false, + insertable: false, sequence: ESC, }); } else if (escaped) { // Escape sequence timeout name = ch.length ? undefined : 'escape'; meta = true; + } else { + // Any other character is considered printable. + insertable = true; } if ( @@ -497,6 +505,7 @@ function* emitKeys( meta, shift, paste: false, + insertable, sequence, }); } @@ -510,6 +519,7 @@ export interface Key { meta: boolean; shift: boolean; paste: boolean; + insertable: boolean; sequence: string; } diff --git a/packages/cli/src/ui/hooks/useKeypress.test.tsx b/packages/cli/src/ui/hooks/useKeypress.test.tsx index f849a86a25..6a063f35fc 100644 --- a/packages/cli/src/ui/hooks/useKeypress.test.tsx +++ b/packages/cli/src/ui/hooks/useKeypress.test.tsx @@ -38,7 +38,7 @@ class MockStdin extends EventEmitter { } } -describe(`useKeypress with useKitty=%s`, () => { +describe(`useKeypress`, () => { let stdin: MockStdin; const mockSetRawMode = vi.fn(); const onKeypress = vi.fn(); @@ -144,6 +144,7 @@ describe(`useKeypress with useKitty=%s`, () => { meta: false, shift: false, paste: true, + insertable: true, sequence: pasteText, }); }); @@ -281,6 +282,7 @@ describe(`useKeypress with useKitty=%s`, () => { meta: false, shift: false, paste: true, + insertable: true, sequence: pasteText, }); }); diff --git a/packages/cli/src/ui/hooks/useSelectionList.test.tsx b/packages/cli/src/ui/hooks/useSelectionList.test.tsx index 7964bf591f..b4b203e72d 100644 --- a/packages/cli/src/ui/hooks/useSelectionList.test.tsx +++ b/packages/cli/src/ui/hooks/useSelectionList.test.tsx @@ -62,6 +62,7 @@ describe('useSelectionList', () => { meta: false, shift: options.shift ?? false, paste: false, + insertable: false, }; activeKeypressHandler(key); } else { @@ -331,6 +332,7 @@ describe('useSelectionList', () => { meta: false, shift: false, paste: false, + insertable: true, }; handler(key); }; @@ -380,6 +382,7 @@ describe('useSelectionList', () => { meta: false, shift: false, paste: false, + insertable: false, }; handler(key); }; diff --git a/packages/cli/src/ui/hooks/vim.test.tsx b/packages/cli/src/ui/hooks/vim.test.tsx index d3cec9cc13..3f6ae1f90b 100644 --- a/packages/cli/src/ui/hooks/vim.test.tsx +++ b/packages/cli/src/ui/hooks/vim.test.tsx @@ -40,6 +40,7 @@ const createKey = (partial: Partial): Key => ({ meta: partial.meta || false, shift: partial.shift || false, paste: partial.paste || false, + insertable: partial.insertable || false, ...partial, }); diff --git a/packages/cli/src/ui/hooks/vim.ts b/packages/cli/src/ui/hooks/vim.ts index d8d5ab38d2..dc3344a201 100644 --- a/packages/cli/src/ui/hooks/vim.ts +++ b/packages/cli/src/ui/hooks/vim.ts @@ -313,6 +313,7 @@ export function useVim(buffer: TextBuffer, onSubmit?: (value: string) => void) { meta: key.meta || false, shift: key.shift || false, paste: key.paste || false, + insertable: key.insertable || false, }), [], ); diff --git a/packages/cli/src/ui/keyMatchers.test.ts b/packages/cli/src/ui/keyMatchers.test.ts index baaa88dbff..be028f5c02 100644 --- a/packages/cli/src/ui/keyMatchers.test.ts +++ b/packages/cli/src/ui/keyMatchers.test.ts @@ -17,6 +17,7 @@ describe('keyMatchers', () => { meta: false, shift: false, paste: false, + insertable: false, sequence: name, ...mods, });