@@ -35,6 +35,7 @@ export class SelectionManager {
3535 private terminal : Terminal ;
3636 private renderer : CanvasRenderer ;
3737 private wasmTerm : GhosttyTerminal ;
38+ private textarea : HTMLTextAreaElement ;
3839
3940 // Selection state
4041 private selectionStart : { col : number ; row : number } | null = null ;
@@ -49,11 +50,18 @@ export class SelectionManager {
4950
5051 // Store bound event handlers for cleanup
5152 private boundMouseUpHandler : ( ( e : MouseEvent ) => void ) | null = null ;
52-
53- constructor ( terminal : Terminal , renderer : CanvasRenderer , wasmTerm : GhosttyTerminal ) {
53+ private boundContextMenuHandler : ( ( e : MouseEvent ) => void ) | null = null ;
54+
55+ constructor (
56+ terminal : Terminal ,
57+ renderer : CanvasRenderer ,
58+ wasmTerm : GhosttyTerminal ,
59+ textarea : HTMLTextAreaElement
60+ ) {
5461 this . terminal = terminal ;
5562 this . renderer = renderer ;
5663 this . wasmTerm = wasmTerm ;
64+ this . textarea = textarea ;
5765
5866 // Attach mouse event listeners
5967 this . attachEventListeners ( ) ;
@@ -253,6 +261,13 @@ export class SelectionManager {
253261 this . boundMouseUpHandler = null ;
254262 }
255263
264+ // Clean up context menu event listener
265+ if ( this . boundContextMenuHandler ) {
266+ const canvas = this . renderer . getCanvas ( ) ;
267+ canvas . removeEventListener ( 'contextmenu' , this . boundContextMenuHandler ) ;
268+ this . boundContextMenuHandler = null ;
269+ }
270+
256271 // Canvas event listeners will be cleaned up when canvas is removed from DOM
257272 }
258273
@@ -345,17 +360,67 @@ export class SelectionManager {
345360 }
346361 } ) ;
347362
348- // Right-click (context menu) - copy selection if exists
349- canvas . addEventListener ( 'contextmenu' , ( e : MouseEvent ) => {
350- e . preventDefault ( ) ; // Prevent default context menu
351-
363+ // Right-click (context menu) - position textarea to show browser's native menu
364+ // This allows Copy/Paste options to appear in the context menu
365+ this . boundContextMenuHandler = ( e : MouseEvent ) => {
366+ // Position textarea at mouse cursor
367+ const canvas = this . renderer . getCanvas ( ) ;
368+ const rect = canvas . getBoundingClientRect ( ) ;
369+
370+ this . textarea . style . position = 'fixed' ;
371+ this . textarea . style . left = `${ e . clientX } px` ;
372+ this . textarea . style . top = `${ e . clientY } px` ;
373+ this . textarea . style . width = '1px' ;
374+ this . textarea . style . height = '1px' ;
375+ this . textarea . style . zIndex = '1000' ;
376+ this . textarea . style . opacity = '0' ;
377+
378+ // Enable pointer events temporarily so context menu targets the textarea
379+ this . textarea . style . pointerEvents = 'auto' ;
380+
381+ // If there's a selection, populate textarea with it and select the text
352382 if ( this . hasSelection ( ) ) {
353383 const text = this . getSelection ( ) ;
354- if ( text ) {
355- this . copyToClipboard ( text ) ;
356- }
384+ this . textarea . value = text ;
385+ this . textarea . select ( ) ;
386+ this . textarea . setSelectionRange ( 0 , text . length ) ;
387+ } else {
388+ // No selection - clear textarea but still show menu (for paste)
389+ this . textarea . value = '' ;
357390 }
358- } ) ;
391+
392+ // Focus the textarea so the context menu appears on it
393+ this . textarea . focus ( ) ;
394+
395+ // After a short delay, restore the textarea to its hidden state
396+ // This allows the context menu to appear first
397+ setTimeout ( ( ) => {
398+ // Listen for when the context menu closes (user clicks away or selects an option)
399+ const resetTextarea = ( ) => {
400+ this . textarea . style . pointerEvents = 'none' ;
401+ this . textarea . style . zIndex = '-10' ;
402+ this . textarea . style . width = '0' ;
403+ this . textarea . style . height = '0' ;
404+ this . textarea . style . left = '0' ;
405+ this . textarea . style . top = '0' ;
406+ this . textarea . value = '' ;
407+
408+ // Remove the one-time listeners
409+ document . removeEventListener ( 'click' , resetTextarea ) ;
410+ document . removeEventListener ( 'contextmenu' , resetTextarea ) ;
411+ this . textarea . removeEventListener ( 'blur' , resetTextarea ) ;
412+ } ;
413+
414+ // Reset on any of these events (menu closed)
415+ document . addEventListener ( 'click' , resetTextarea , { once : true } ) ;
416+ document . addEventListener ( 'contextmenu' , resetTextarea , { once : true } ) ;
417+ this . textarea . addEventListener ( 'blur' , resetTextarea , { once : true } ) ;
418+ } , 10 ) ;
419+
420+ // Don't prevent default - let browser show the context menu on the textarea
421+ } ;
422+
423+ canvas . addEventListener ( 'contextmenu' , this . boundContextMenuHandler ) ;
359424 }
360425
361426 /**
0 commit comments