@@ -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,48 @@ 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+ inputElement as any
436+ ) ;
437+
438+ container . dispatchEvent ( createCompositionEvent ( 'compositionend' , '你好' ) ) ;
439+ inputElement . dispatchEvent ( createBeforeInputEvent ( 'insertText' , '你好' ) ) ;
440+
441+ expect ( dataReceived ) . toEqual ( [ '你好' ] ) ;
442+ } ) ;
443+
444+ test ( 'avoids duplicate commit when beforeinput fires before compositionend' , ( ) => {
445+ const inputElement = createMockContainer ( ) ;
446+ const handler = new InputHandler (
447+ ghostty ,
448+ container as any ,
449+ ( data ) => dataReceived . push ( data ) ,
450+ ( ) => {
451+ bellCalled = true ;
452+ } ,
453+ undefined ,
454+ undefined ,
455+ undefined ,
456+ inputElement as any
457+ ) ;
458+
459+ inputElement . dispatchEvent ( createBeforeInputEvent ( 'insertText' , '你好' ) ) ;
460+ container . dispatchEvent ( createCompositionEvent ( 'compositionend' , '你好' ) ) ;
461+
462+ expect ( dataReceived ) . toEqual ( [ '你好' ] ) ;
463+ } ) ;
402464 } ) ;
403465
404466 describe ( 'Control Characters' , ( ) => {
@@ -939,6 +1001,54 @@ describe('InputHandler', () => {
9391001 expect ( dataReceived [ 0 ] ) . toBe ( pasteText ) ;
9401002 } ) ;
9411003
1004+ test ( 'handles beforeinput insertFromPaste with data' , ( ) => {
1005+ const inputElement = createMockContainer ( ) ;
1006+ const handler = new InputHandler (
1007+ ghostty ,
1008+ container as any ,
1009+ ( data ) => dataReceived . push ( data ) ,
1010+ ( ) => {
1011+ bellCalled = true ;
1012+ } ,
1013+ undefined ,
1014+ undefined ,
1015+ undefined ,
1016+ inputElement as any
1017+ ) ;
1018+
1019+ const pasteText = 'Hello, beforeinput!' ;
1020+ const beforeInputEvent = createBeforeInputEvent ( 'insertFromPaste' , pasteText ) ;
1021+
1022+ inputElement . dispatchEvent ( beforeInputEvent ) ;
1023+
1024+ expect ( dataReceived . length ) . toBe ( 1 ) ;
1025+ expect ( dataReceived [ 0 ] ) . toBe ( pasteText ) ;
1026+ } ) ;
1027+
1028+ test ( 'uses bracketed paste for beforeinput insertFromPaste' , ( ) => {
1029+ const inputElement = createMockContainer ( ) ;
1030+ const handler = new InputHandler (
1031+ ghostty ,
1032+ container as any ,
1033+ ( data ) => dataReceived . push ( data ) ,
1034+ ( ) => {
1035+ bellCalled = true ;
1036+ } ,
1037+ undefined ,
1038+ undefined ,
1039+ ( mode ) => mode === 2004 ,
1040+ inputElement as any
1041+ ) ;
1042+
1043+ const pasteText = 'Bracketed paste' ;
1044+ const beforeInputEvent = createBeforeInputEvent ( 'insertFromPaste' , pasteText ) ;
1045+
1046+ inputElement . dispatchEvent ( beforeInputEvent ) ;
1047+
1048+ expect ( dataReceived . length ) . toBe ( 1 ) ;
1049+ expect ( dataReceived [ 0 ] ) . toBe ( `\x1b[200~${ pasteText } \x1b[201~` ) ;
1050+ } ) ;
1051+
9421052 test ( 'handles multi-line paste' , ( ) => {
9431053 const handler = new InputHandler (
9441054 ghostty ,
@@ -958,6 +1068,58 @@ describe('InputHandler', () => {
9581068 expect ( dataReceived [ 0 ] ) . toBe ( pasteText ) ;
9591069 } ) ;
9601070
1071+ test ( 'ignores beforeinput insertFromPaste when paste already handled' , ( ) => {
1072+ const inputElement = createMockContainer ( ) ;
1073+ const handler = new InputHandler (
1074+ ghostty ,
1075+ container as any ,
1076+ ( data ) => dataReceived . push ( data ) ,
1077+ ( ) => {
1078+ bellCalled = true ;
1079+ } ,
1080+ undefined ,
1081+ undefined ,
1082+ undefined ,
1083+ inputElement as any
1084+ ) ;
1085+
1086+ const pasteText = 'Hello, World!' ;
1087+ const pasteEvent = createClipboardEvent ( pasteText ) ;
1088+ const beforeInputEvent = createBeforeInputEvent ( 'insertFromPaste' , pasteText ) ;
1089+
1090+ container . dispatchEvent ( pasteEvent ) ;
1091+ inputElement . dispatchEvent ( beforeInputEvent ) ;
1092+
1093+ expect ( dataReceived . length ) . toBe ( 1 ) ;
1094+ expect ( dataReceived [ 0 ] ) . toBe ( pasteText ) ;
1095+ } ) ;
1096+
1097+ test ( 'ignores paste when beforeinput insertFromPaste already handled' , ( ) => {
1098+ const inputElement = createMockContainer ( ) ;
1099+ const handler = new InputHandler (
1100+ ghostty ,
1101+ container as any ,
1102+ ( data ) => dataReceived . push ( data ) ,
1103+ ( ) => {
1104+ bellCalled = true ;
1105+ } ,
1106+ undefined ,
1107+ undefined ,
1108+ undefined ,
1109+ inputElement as any
1110+ ) ;
1111+
1112+ const pasteText = 'Hello, World!' ;
1113+ const beforeInputEvent = createBeforeInputEvent ( 'insertFromPaste' , pasteText ) ;
1114+ const pasteEvent = createClipboardEvent ( pasteText ) ;
1115+
1116+ inputElement . dispatchEvent ( beforeInputEvent ) ;
1117+ container . dispatchEvent ( pasteEvent ) ;
1118+
1119+ expect ( dataReceived . length ) . toBe ( 1 ) ;
1120+ expect ( dataReceived [ 0 ] ) . toBe ( pasteText ) ;
1121+ } ) ;
1122+
9611123 test ( 'ignores paste with no clipboard data' , ( ) => {
9621124 const handler = new InputHandler (
9631125 ghostty ,
0 commit comments