@@ -114,10 +114,23 @@ vi.mock("@xterm/addon-webgl", () => ({
114114 } ,
115115} ) ) ;
116116
117+ function setNavigatorPlatform ( platform : string ) {
118+ Object . defineProperty ( window . navigator , "platform" , {
119+ configurable : true ,
120+ value : platform ,
121+ } ) ;
122+ Object . defineProperty ( window . navigator , "userAgentData" , {
123+ configurable : true ,
124+ value : { platform } ,
125+ } ) ;
126+ }
127+
117128describe ( "XtermTerminal" , ( ) => {
118129 beforeEach ( ( ) => {
119130 state . lastTerminal = null ;
131+ setNavigatorPlatform ( "Linux x86_64" ) ;
120132 window . ao ! . clipboard . writeText = vi . fn ( ) . mockResolvedValue ( undefined ) ;
133+ window . ao ! . clipboard . readText = vi . fn ( ) . mockResolvedValue ( "" ) ;
121134 } ) ;
122135
123136 it ( "copies selected terminal text on the terminal copy shortcut" , ( ) => {
@@ -130,6 +143,7 @@ describe("XtermTerminal", () => {
130143 ctrlKey : false ,
131144 shiftKey : false ,
132145 preventDefault : vi . fn ( ) ,
146+ stopPropagation : vi . fn ( ) ,
133147 } as unknown as KeyboardEvent ;
134148 const allowed = state . lastTerminal ! . keyHandler ! ( event ) ;
135149
@@ -186,6 +200,7 @@ describe("XtermTerminal", () => {
186200 ctrlKey : false ,
187201 shiftKey : false ,
188202 preventDefault : vi . fn ( ) ,
203+ stopPropagation : vi . fn ( ) ,
189204 } as unknown as KeyboardEvent ;
190205 const allowed = state . lastTerminal ! . keyHandler ! ( event ) ;
191206
@@ -194,6 +209,159 @@ describe("XtermTerminal", () => {
194209 expect ( writeText ) . toHaveBeenLastCalledWith ( "retry me" ) ;
195210 } ) ;
196211
212+ it ( "leaves plain Ctrl+C as terminal input on non-Windows even when text is selected" , ( ) => {
213+ render ( < XtermTerminal theme = "dark" /> ) ;
214+ state . lastTerminal ! . selection = "selected text" ;
215+
216+ const event = {
217+ key : "c" ,
218+ metaKey : false ,
219+ ctrlKey : true ,
220+ shiftKey : false ,
221+ altKey : false ,
222+ preventDefault : vi . fn ( ) ,
223+ stopPropagation : vi . fn ( ) ,
224+ } as unknown as KeyboardEvent ;
225+ const allowed = state . lastTerminal ! . keyHandler ! ( event ) ;
226+
227+ expect ( allowed ) . toBe ( true ) ;
228+ expect ( event . preventDefault ) . not . toHaveBeenCalled ( ) ;
229+ expect ( event . stopPropagation ) . not . toHaveBeenCalled ( ) ;
230+ expect ( window . ao ! . clipboard . writeText ) . not . toHaveBeenCalled ( ) ;
231+ } ) ;
232+
233+ it ( "copies selected text with plain Ctrl+C on Windows" , ( ) => {
234+ setNavigatorPlatform ( "Win32" ) ;
235+ render ( < XtermTerminal theme = "dark" /> ) ;
236+ state . lastTerminal ! . selection = "windows copy" ;
237+
238+ const event = {
239+ key : "c" ,
240+ metaKey : false ,
241+ ctrlKey : true ,
242+ shiftKey : false ,
243+ altKey : false ,
244+ preventDefault : vi . fn ( ) ,
245+ stopPropagation : vi . fn ( ) ,
246+ } as unknown as KeyboardEvent ;
247+ const allowed = state . lastTerminal ! . keyHandler ! ( event ) ;
248+
249+ expect ( allowed ) . toBe ( false ) ;
250+ expect ( event . preventDefault ) . toHaveBeenCalled ( ) ;
251+ expect ( event . stopPropagation ) . toHaveBeenCalled ( ) ;
252+ expect ( window . ao ! . clipboard . writeText ) . toHaveBeenCalledWith ( "windows copy" ) ;
253+ } ) ;
254+
255+ it ( "leaves plain Ctrl+C as terminal input on Windows when nothing is selected" , ( ) => {
256+ setNavigatorPlatform ( "Win32" ) ;
257+ render ( < XtermTerminal theme = "dark" /> ) ;
258+ state . lastTerminal ! . selection = "" ;
259+
260+ const event = {
261+ key : "c" ,
262+ metaKey : false ,
263+ ctrlKey : true ,
264+ shiftKey : false ,
265+ altKey : false ,
266+ preventDefault : vi . fn ( ) ,
267+ stopPropagation : vi . fn ( ) ,
268+ } as unknown as KeyboardEvent ;
269+ const allowed = state . lastTerminal ! . keyHandler ! ( event ) ;
270+
271+ expect ( allowed ) . toBe ( true ) ;
272+ expect ( event . preventDefault ) . not . toHaveBeenCalled ( ) ;
273+ expect ( event . stopPropagation ) . not . toHaveBeenCalled ( ) ;
274+ expect ( window . ao ! . clipboard . writeText ) . not . toHaveBeenCalled ( ) ;
275+ } ) ;
276+
277+ it ( "pastes from the Electron clipboard on Windows/Linux paste shortcuts" , async ( ) => {
278+ const onInput = vi . fn ( ) ;
279+ window . ao ! . clipboard . readText = vi . fn ( ) . mockResolvedValue ( "hello\nworld" ) ;
280+ render ( < XtermTerminal theme = "dark" onReady = { ( terminal ) => terminal . onUserInput ( onInput ) } /> ) ;
281+
282+ const event = {
283+ key : "v" ,
284+ metaKey : false ,
285+ ctrlKey : true ,
286+ shiftKey : true ,
287+ altKey : false ,
288+ preventDefault : vi . fn ( ) ,
289+ stopPropagation : vi . fn ( ) ,
290+ } as unknown as KeyboardEvent ;
291+ const allowed = state . lastTerminal ! . keyHandler ! ( event ) ;
292+ await Promise . resolve ( ) ;
293+
294+ expect ( allowed ) . toBe ( false ) ;
295+ expect ( event . preventDefault ) . toHaveBeenCalled ( ) ;
296+ expect ( event . stopPropagation ) . toHaveBeenCalled ( ) ;
297+ expect ( window . ao ! . clipboard . readText ) . toHaveBeenCalled ( ) ;
298+ expect ( onInput ) . toHaveBeenCalledWith ( "hello\rworld" , "paste" ) ;
299+ } ) ;
300+
301+ it ( "supports classic Windows terminal copy and paste shortcuts" , async ( ) => {
302+ const onInput = vi . fn ( ) ;
303+ window . ao ! . clipboard . readText = vi . fn ( ) . mockResolvedValue ( "insert paste" ) ;
304+ render ( < XtermTerminal theme = "dark" onReady = { ( terminal ) => terminal . onUserInput ( onInput ) } /> ) ;
305+ state . lastTerminal ! . selection = "insert copy" ;
306+
307+ const copyEvent = {
308+ key : "Insert" ,
309+ metaKey : false ,
310+ ctrlKey : true ,
311+ shiftKey : false ,
312+ altKey : false ,
313+ preventDefault : vi . fn ( ) ,
314+ stopPropagation : vi . fn ( ) ,
315+ } as unknown as KeyboardEvent ;
316+ expect ( state . lastTerminal ! . keyHandler ! ( copyEvent ) ) . toBe ( false ) ;
317+ expect ( window . ao ! . clipboard . writeText ) . toHaveBeenCalledWith ( "insert copy" ) ;
318+
319+ const pasteEvent = {
320+ key : "Insert" ,
321+ metaKey : false ,
322+ ctrlKey : false ,
323+ shiftKey : true ,
324+ altKey : false ,
325+ preventDefault : vi . fn ( ) ,
326+ stopPropagation : vi . fn ( ) ,
327+ } as unknown as KeyboardEvent ;
328+ expect ( state . lastTerminal ! . keyHandler ! ( pasteEvent ) ) . toBe ( false ) ;
329+ await Promise . resolve ( ) ;
330+
331+ expect ( window . ao ! . clipboard . readText ) . toHaveBeenCalled ( ) ;
332+ expect ( onInput ) . toHaveBeenCalledWith ( "insert paste" , "paste" ) ;
333+ } ) ;
334+
335+ it . each ( [
336+ [ "Option/Alt+Left" , { key : "ArrowLeft" , altKey : true } , "\x1bb" ] ,
337+ [ "Option/Alt+Right" , { key : "ArrowRight" , altKey : true } , "\x1bf" ] ,
338+ [ "Option/Alt+Backspace" , { key : "Backspace" , altKey : true } , "\x1b\x7f" ] ,
339+ [ "Option/Alt+Delete" , { key : "Delete" , altKey : true } , "\x1bd" ] ,
340+ [ "Ctrl+Left" , { key : "ArrowLeft" , ctrlKey : true } , "\x1b[1;5D" ] ,
341+ [ "Ctrl+Right" , { key : "ArrowRight" , ctrlKey : true } , "\x1b[1;5C" ] ,
342+ [ "Ctrl+Backspace" , { key : "Backspace" , ctrlKey : true } , "\x1b\x7f" ] ,
343+ [ "Ctrl+Delete" , { key : "Delete" , ctrlKey : true } , "\x1bd" ] ,
344+ ] ) ( "normalizes %s into terminal input" , ( _name , init , expected ) => {
345+ const onInput = vi . fn ( ) ;
346+ render ( < XtermTerminal theme = "dark" onReady = { ( terminal ) => terminal . onUserInput ( onInput ) } /> ) ;
347+
348+ const event = {
349+ metaKey : false ,
350+ ctrlKey : false ,
351+ shiftKey : false ,
352+ altKey : false ,
353+ preventDefault : vi . fn ( ) ,
354+ stopPropagation : vi . fn ( ) ,
355+ ...init ,
356+ } as unknown as KeyboardEvent ;
357+ const allowed = state . lastTerminal ! . keyHandler ! ( event ) ;
358+
359+ expect ( allowed ) . toBe ( false ) ;
360+ expect ( event . preventDefault ) . toHaveBeenCalled ( ) ;
361+ expect ( event . stopPropagation ) . toHaveBeenCalled ( ) ;
362+ expect ( onInput ) . toHaveBeenCalledWith ( expected , "terminal" ) ;
363+ } ) ;
364+
197365 it ( "forwards generated xterm input data such as wheel scroll reports" , ( ) => {
198366 const onInput = vi . fn ( ) ;
199367 render ( < XtermTerminal theme = "dark" onReady = { ( terminal ) => terminal . onUserInput ( onInput ) } /> ) ;
0 commit comments