@@ -29,6 +29,14 @@ interface MockClipboardEvent {
2929 preventDefault : ( ) => void ;
3030 stopPropagation : ( ) => void ;
3131}
32+ interface MockInputEvent {
33+ type : string ;
34+ inputType : string ;
35+ data : string | null ;
36+ isComposing ?: boolean ;
37+ preventDefault : ( ) => void ;
38+ stopPropagation : ( ) => void ;
39+ }
3240
3341interface MockHTMLElement {
3442 addEventListener : ( event : string , handler : ( e : any ) => void ) => void ;
@@ -79,6 +87,18 @@ function createClipboardEvent(text: string | null): MockClipboardEvent {
7987 stopPropagation : mock ( ( ) => { } ) ,
8088 } ;
8189}
90+
91+ // Helper to create mock beforeinput event
92+ function createBeforeInputEvent ( inputType : string , data : string | null ) : MockInputEvent {
93+ return {
94+ type : 'beforeinput' ,
95+ inputType,
96+ data,
97+ isComposing : false ,
98+ preventDefault : mock ( ( ) => { } ) ,
99+ stopPropagation : mock ( ( ) => { } ) ,
100+ } ;
101+ }
82102interface MockCompositionEvent {
83103 type : string ;
84104 data : string | null ;
@@ -399,6 +419,50 @@ describe('InputHandler', () => {
399419 expect ( container . childNodes [ 0 ] ) . toBe ( elementNode ) ;
400420 expect ( dataReceived ) . toEqual ( [ '你好' ] ) ;
401421 } ) ;
422+
423+ test ( 'avoids duplicate commit when compositionend fires before beforeinput' , ( ) => {
424+ const inputElement = createMockContainer ( ) ;
425+ const handler = new InputHandler (
426+ ghostty ,
427+ container as any ,
428+ ( data ) => dataReceived . push ( data ) ,
429+ ( ) => {
430+ bellCalled = true ;
431+ } ,
432+ undefined ,
433+ undefined ,
434+ undefined ,
435+ undefined ,
436+ inputElement as any
437+ ) ;
438+
439+ container . dispatchEvent ( createCompositionEvent ( 'compositionend' , '你好' ) ) ;
440+ inputElement . dispatchEvent ( createBeforeInputEvent ( 'insertText' , '你好' ) ) ;
441+
442+ expect ( dataReceived ) . toEqual ( [ '你好' ] ) ;
443+ } ) ;
444+
445+ test ( 'avoids duplicate commit when beforeinput fires before compositionend' , ( ) => {
446+ const inputElement = createMockContainer ( ) ;
447+ const handler = new InputHandler (
448+ ghostty ,
449+ container as any ,
450+ ( data ) => dataReceived . push ( data ) ,
451+ ( ) => {
452+ bellCalled = true ;
453+ } ,
454+ undefined ,
455+ undefined ,
456+ undefined ,
457+ undefined ,
458+ inputElement as any
459+ ) ;
460+
461+ inputElement . dispatchEvent ( createBeforeInputEvent ( 'insertText' , '你好' ) ) ;
462+ container . dispatchEvent ( createCompositionEvent ( 'compositionend' , '你好' ) ) ;
463+
464+ expect ( dataReceived ) . toEqual ( [ '你好' ] ) ;
465+ } ) ;
402466 } ) ;
403467
404468 describe ( 'Control Characters' , ( ) => {
@@ -939,6 +1003,56 @@ describe('InputHandler', () => {
9391003 expect ( dataReceived [ 0 ] ) . toBe ( pasteText ) ;
9401004 } ) ;
9411005
1006+ test ( 'handles beforeinput insertFromPaste with data' , ( ) => {
1007+ const inputElement = createMockContainer ( ) ;
1008+ const handler = new InputHandler (
1009+ ghostty ,
1010+ container as any ,
1011+ ( data ) => dataReceived . push ( data ) ,
1012+ ( ) => {
1013+ bellCalled = true ;
1014+ } ,
1015+ undefined ,
1016+ undefined ,
1017+ undefined ,
1018+ undefined ,
1019+ inputElement as any
1020+ ) ;
1021+
1022+ const pasteText = 'Hello, beforeinput!' ;
1023+ const beforeInputEvent = createBeforeInputEvent ( 'insertFromPaste' , pasteText ) ;
1024+
1025+ inputElement . dispatchEvent ( beforeInputEvent ) ;
1026+
1027+ expect ( dataReceived . length ) . toBe ( 1 ) ;
1028+ expect ( dataReceived [ 0 ] ) . toBe ( pasteText ) ;
1029+ } ) ;
1030+
1031+ test ( 'uses bracketed paste for beforeinput insertFromPaste' , ( ) => {
1032+ const inputElement = createMockContainer ( ) ;
1033+ const handler = new InputHandler (
1034+ ghostty ,
1035+ container as any ,
1036+ ( data ) => dataReceived . push ( data ) ,
1037+ ( ) => {
1038+ bellCalled = true ;
1039+ } ,
1040+ undefined ,
1041+ undefined ,
1042+ ( mode ) => mode === 2004 ,
1043+ undefined ,
1044+ inputElement as any
1045+ ) ;
1046+
1047+ const pasteText = 'Bracketed paste' ;
1048+ const beforeInputEvent = createBeforeInputEvent ( 'insertFromPaste' , pasteText ) ;
1049+
1050+ inputElement . dispatchEvent ( beforeInputEvent ) ;
1051+
1052+ expect ( dataReceived . length ) . toBe ( 1 ) ;
1053+ expect ( dataReceived [ 0 ] ) . toBe ( `\x1b[200~${ pasteText } \x1b[201~` ) ;
1054+ } ) ;
1055+
9421056 test ( 'handles multi-line paste' , ( ) => {
9431057 const handler = new InputHandler (
9441058 ghostty ,
@@ -958,6 +1072,60 @@ describe('InputHandler', () => {
9581072 expect ( dataReceived [ 0 ] ) . toBe ( pasteText ) ;
9591073 } ) ;
9601074
1075+ test ( 'ignores beforeinput insertFromPaste when paste already handled' , ( ) => {
1076+ const inputElement = createMockContainer ( ) ;
1077+ const handler = new InputHandler (
1078+ ghostty ,
1079+ container as any ,
1080+ ( data ) => dataReceived . push ( data ) ,
1081+ ( ) => {
1082+ bellCalled = true ;
1083+ } ,
1084+ undefined ,
1085+ undefined ,
1086+ undefined ,
1087+ undefined ,
1088+ inputElement as any
1089+ ) ;
1090+
1091+ const pasteText = 'Hello, World!' ;
1092+ const pasteEvent = createClipboardEvent ( pasteText ) ;
1093+ const beforeInputEvent = createBeforeInputEvent ( 'insertFromPaste' , pasteText ) ;
1094+
1095+ container . dispatchEvent ( pasteEvent ) ;
1096+ inputElement . dispatchEvent ( beforeInputEvent ) ;
1097+
1098+ expect ( dataReceived . length ) . toBe ( 1 ) ;
1099+ expect ( dataReceived [ 0 ] ) . toBe ( pasteText ) ;
1100+ } ) ;
1101+
1102+ test ( 'ignores paste when beforeinput insertFromPaste already handled' , ( ) => {
1103+ const inputElement = createMockContainer ( ) ;
1104+ const handler = new InputHandler (
1105+ ghostty ,
1106+ container as any ,
1107+ ( data ) => dataReceived . push ( data ) ,
1108+ ( ) => {
1109+ bellCalled = true ;
1110+ } ,
1111+ undefined ,
1112+ undefined ,
1113+ undefined ,
1114+ undefined ,
1115+ inputElement as any
1116+ ) ;
1117+
1118+ const pasteText = 'Hello, World!' ;
1119+ const beforeInputEvent = createBeforeInputEvent ( 'insertFromPaste' , pasteText ) ;
1120+ const pasteEvent = createClipboardEvent ( pasteText ) ;
1121+
1122+ inputElement . dispatchEvent ( beforeInputEvent ) ;
1123+ container . dispatchEvent ( pasteEvent ) ;
1124+
1125+ expect ( dataReceived . length ) . toBe ( 1 ) ;
1126+ expect ( dataReceived [ 0 ] ) . toBe ( pasteText ) ;
1127+ } ) ;
1128+
9611129 test ( 'ignores paste with no clipboard data' , ( ) => {
9621130 const handler = new InputHandler (
9631131 ghostty ,
0 commit comments