11import { describe , test , expect } from 'bun:test'
22
3+ import {
4+ getKeypadPrintableSequence ,
5+ isKeypadEnter ,
6+ } from '../../utils/keypad-keys'
7+
38/**
49 * Tests for tab character cursor rendering in MultilineInput component.
510 *
@@ -13,23 +18,23 @@ import { describe, test, expect } from 'bun:test'
1318/**
1419 * Check if a key event represents printable character input (not a special key).
1520 * This mirrors the function in multiline-input.tsx for testing.
16- *
21+ *
1722 * Uses a positive heuristic based on key.name length rather than a brittle deny-list.
1823 * Special keys have descriptive multi-character names (like 'backspace', 'up', 'f1')
1924 * while regular printable characters either have no name or a single-character name.
2025 */
2126function isPrintableCharacterKey ( key : { name ?: string } ) : boolean {
2227 const name = key . name
23-
28+
2429 // No name = likely multi-byte input (Chinese, Japanese, Korean, etc.)
2530 if ( ! name ) return true
26-
31+
2732 // Single character name = regular ASCII printable (a, b, 1, $, etc.)
2833 if ( name . length === 1 ) return true
29-
34+
3035 // Special case: space key has name 'space' but is printable
3136 if ( name === 'space' ) return true
32-
37+
3338 // Multi-char name = special key (up, f1, backspace, etc.)
3439 return false
3540}
@@ -256,27 +261,42 @@ describe('MultilineInput - Chinese/IME character input', () => {
256261 meta ?: boolean
257262 option ?: boolean
258263 } ) : boolean {
264+ return getPrintableKeySequence ( key ) !== null
265+ }
266+
267+ function getPrintableKeySequence ( key : {
268+ sequence ?: string
269+ name ?: string
270+ ctrl ?: boolean
271+ meta ?: boolean
272+ option ?: boolean
273+ } ) : string | null {
259274 // Must have a sequence with at least one character
260275 if ( ! key . sequence || key . sequence . length < 1 ) {
261- return false
276+ return null
262277 }
263278
264279 // No modifier keys allowed
265280 if ( key . ctrl || key . meta || key . option ) {
266- return false
281+ return null
282+ }
283+
284+ const keypadValue = getKeypadPrintableSequence ( key )
285+ if ( keypadValue !== null ) {
286+ return keypadValue
267287 }
268288
269289 // Must not be a control character
270290 if ( CONTROL_CHAR_REGEX . test ( key . sequence ) ) {
271- return false
291+ return null
272292 }
273293
274294 // Must be a printable character key (not a special key like arrows, function keys, etc.)
275295 if ( ! isPrintableCharacterKey ( key ) ) {
276- return false
296+ return null
277297 }
278298
279- return true
299+ return key . sequence
280300 }
281301
282302 test ( 'accepts single Chinese character (你)' , ( ) => {
@@ -387,6 +407,42 @@ describe('MultilineInput - Chinese/IME character input', () => {
387407 expect ( shouldAcceptCharacterInput ( key ) ) . toBe ( true )
388408 } )
389409
410+ test ( 'accepts Kitty keyboard numpad digit names' , ( ) => {
411+ const key = {
412+ sequence : '\x1b[57400u' ,
413+ name : 'kp1' ,
414+ ctrl : false ,
415+ meta : false ,
416+ option : false ,
417+ }
418+
419+ expect ( getPrintableKeySequence ( key ) ) . toBe ( '1' )
420+ } )
421+
422+ test ( 'accepts raw application keypad digit sequences' , ( ) => {
423+ const key = {
424+ sequence : '\x1bOq' ,
425+ name : '' ,
426+ ctrl : false ,
427+ meta : false ,
428+ option : false ,
429+ }
430+
431+ expect ( getPrintableKeySequence ( key ) ) . toBe ( '1' )
432+ } )
433+
434+ test ( 'accepts raw application keypad operator sequences' , ( ) => {
435+ const key = {
436+ sequence : '\x1bOk' ,
437+ name : '' ,
438+ ctrl : false ,
439+ meta : false ,
440+ option : false ,
441+ }
442+
443+ expect ( getPrintableKeySequence ( key ) ) . toBe ( '+' )
444+ } )
445+
390446 test ( 'rejects arrow key (up)' , ( ) => {
391447 const key = {
392448 sequence : '\x1b[A' ,
@@ -625,7 +681,9 @@ describe('MultilineInput - newline keyboard shortcuts', () => {
625681 hasBackslashBeforeCursor : boolean = false ,
626682 ) : 'newline' | 'submit' | 'ignore' {
627683 const lowerKeyName = ( key . name ?? '' ) . toLowerCase ( )
628- const isEnterKey = key . name === 'return' || key . name === 'enter'
684+ const keypadEnter = isKeypadEnter ( key )
685+ const isEnterKey =
686+ key . name === 'return' || key . name === 'enter' || keypadEnter
629687 // Ctrl+J is translated by the terminal to a linefeed character (0x0a)
630688 // So we detect it by checking for name === 'linefeed' rather than ctrl + j
631689 const isCtrlJ =
@@ -651,13 +709,13 @@ describe('MultilineInput - newline keyboard shortcuts', () => {
651709 ! key . meta &&
652710 ! key . option &&
653711 ! isAltLikeModifier &&
654- ! hasEscapePrefix &&
655- key . sequence === '\r' &&
712+ ( ! hasEscapePrefix || keypadEnter ) &&
713+ ( key . sequence === '\r' || keypadEnter ) &&
656714 ! hasBackslashBeforeCursor
657715 const isShiftEnter =
658716 isEnterKey && ( Boolean ( key . shift ) || key . sequence === '\n' )
659717 const isOptionEnter =
660- isEnterKey && ( isAltLikeModifier || hasEscapePrefix )
718+ isEnterKey && ! keypadEnter && ( isAltLikeModifier || hasEscapePrefix )
661719 const isBackslashEnter = isEnterKey && hasBackslashBeforeCursor
662720
663721 const shouldInsertNewline =
@@ -900,6 +958,32 @@ describe('MultilineInput - newline keyboard shortcuts', () => {
900958 expect ( getEnterKeyAction ( key , false ) ) . toBe ( 'submit' )
901959 } )
902960
961+ test ( 'keypad Enter submits with Kitty keyboard key name' , ( ) => {
962+ const key = {
963+ name : 'kpenter' ,
964+ sequence : '\x1b[57414u' ,
965+ ctrl : false ,
966+ meta : false ,
967+ shift : false ,
968+ option : false ,
969+ }
970+
971+ expect ( getEnterKeyAction ( key , false ) ) . toBe ( 'submit' )
972+ } )
973+
974+ test ( 'keypad Enter submits with raw application keypad sequence' , ( ) => {
975+ const key = {
976+ name : '' ,
977+ sequence : '\x1bOM' ,
978+ ctrl : false ,
979+ meta : false ,
980+ shift : false ,
981+ option : false ,
982+ }
983+
984+ expect ( getEnterKeyAction ( key , false ) ) . toBe ( 'submit' )
985+ } )
986+
903987 // --- Non-Enter key tests ---
904988
905989 test ( 'Regular J key (no ctrl) is ignored' , ( ) => {
0 commit comments