77 ViewChild ,
88} from '@angular/core' ;
99import { AttachAddon } from '@xterm/addon-attach' ;
10+ import { ClipboardAddon } from '@xterm/addon-clipboard' ;
1011import { FitAddon } from '@xterm/addon-fit' ;
1112import { IDisposable , Terminal } from '@xterm/xterm' ;
1213import { Subscription } from 'rxjs' ;
@@ -66,6 +67,7 @@ export class MudClientComponent implements AfterViewInit, OnDestroy {
6667 private readonly inputController : MudInputController ;
6768 private readonly promptManager : MudPromptManager ;
6869 private screenReader ?: MudScreenReaderAnnouncer ;
70+ private readonly terminalClipboardAddon = new ClipboardAddon ( ) ;
6971 private readonly terminalFitAddon = new FitAddon ( ) ;
7072 private socketAdapter ?: MudSocketAdapter ;
7173 private terminalAttachAddon ?: AttachAddon ;
@@ -89,6 +91,7 @@ export class MudClientComponent implements AfterViewInit, OnDestroy {
8991
9092 private showEchoSubscription ?: Subscription ;
9193 private linemodeSubscription ?: Subscription ;
94+ private pasteHandler ?: ( event : ClipboardEvent ) => void ;
9295 private state : MudClientState = {
9396 isEditMode : true ,
9497 showEcho : true ,
@@ -172,6 +175,7 @@ export class MudClientComponent implements AfterViewInit, OnDestroy {
172175 this . applyResponsiveFontSize ( window . innerWidth ) ;
173176
174177 this . terminal . open ( this . terminalRef . nativeElement ) ;
178+ this . terminal . loadAddon ( this . terminalClipboardAddon ) ;
175179 this . terminal . loadAddon ( this . terminalFitAddon ) ;
176180 this . terminal . loadAddon ( this . terminalAttachAddon ) ;
177181 this . terminal . focus ( ) ;
@@ -187,6 +191,9 @@ export class MudClientComponent implements AfterViewInit, OnDestroy {
187191 this . helperTextarea . setAttribute ( 'aria-label' , 'Eingabe' ) ;
188192 }
189193
194+ // Set up paste handler on terminal container (for native paste events)
195+ this . setupPasteHandler ( this . terminalRef . nativeElement ) ;
196+
190197 this . terminalDisposables . push (
191198 this . terminal . onData ( ( data ) => this . handleInput ( data ) ) ,
192199 this . terminal . onBell ( ( ) => this . playBell ( ) ) ,
@@ -227,10 +234,22 @@ export class MudClientComponent implements AfterViewInit, OnDestroy {
227234 this . handleVisibilityChange ,
228235 ) ;
229236
237+ // Unregister paste handler
238+ if ( this . pasteHandler ) {
239+ if ( this . terminalRef ?. nativeElement ) {
240+ this . terminalRef . nativeElement . removeEventListener (
241+ 'paste' ,
242+ this . pasteHandler ,
243+ ) ;
244+ }
245+ document . removeEventListener ( 'paste' , this . pasteHandler ) ;
246+ }
247+
230248 this . terminalDisposables . forEach ( ( disposable ) => disposable . dispose ( ) ) ;
231249 this . showEchoSubscription ?. unsubscribe ( ) ;
232250 this . linemodeSubscription ?. unsubscribe ( ) ;
233251
252+ this . terminalClipboardAddon . dispose ( ) ;
234253 this . terminalAttachAddon ?. dispose ( ) ;
235254 this . socketAdapter ?. dispose ( ) ;
236255 this . terminal . dispose ( ) ;
@@ -343,8 +362,23 @@ export class MudClientComponent implements AfterViewInit, OnDestroy {
343362 /**
344363 * Routes terminal keystrokes either directly to the socket (when not in edit mode)
345364 * or through the {@link MudInputController}.
365+ * Special handling for Ctrl+V: intercepts clipboard content and injects it properly.
346366 */
347367 private handleInput ( data : string ) {
368+ console . log ( '[PASTE-DEBUG] Edit mode:' , this . state . isEditMode ) ;
369+ console . log ( '[PASTE-DEBUG] Data received:' , JSON . stringify ( data ) ) ;
370+ console . log ( '[PASTE-DEBUG] Data length:' , data . length ) ;
371+
372+ // Special handling for Ctrl+V (paste): xterm converts paste to \u0016 in onData()
373+ // We need to read the clipboard and inject the actual content
374+ if ( data === '\u0016' ) {
375+ console . log (
376+ '[MudClient] Ctrl+V detected, reading clipboard from native event...' ,
377+ ) ;
378+ this . handlePasteFromClipboard ( ) ;
379+ return ;
380+ }
381+
348382 // Unlock audio context on first user interaction (browser autoplay policy)
349383 if ( ! this . audioUnlocked ) {
350384 this . unlockAudio ( ) ;
@@ -595,4 +629,71 @@ export class MudClientComponent implements AfterViewInit, OnDestroy {
595629
596630 this . helperTextarea . value = `${ prompt } ${ effectiveBuffer ?? '' } ` ;
597631 }
632+
633+ /**
634+ * Sets up a paste event handler to intercept native clipboard paste operations.
635+ * This method listens on the terminal container for paste events that come
636+ * directly from the browser (Ctrl+V, right-click paste, etc.) without requiring
637+ * explicit clipboard API permissions.
638+ *
639+ * The paste event automatically includes clipboard access through event.clipboardData,
640+ * so no navigator.clipboard.readText() call is needed.
641+ */
642+ private setupPasteHandler ( element : HTMLElement ) : void {
643+ this . pasteHandler = ( event : ClipboardEvent ) => {
644+ console . log ( '[MudClient] Native paste event intercepted' ) ;
645+
646+ // Don't prevent default for now - let xterm handle the visual part
647+ // We'll just read the clipboard data and inject it properly
648+ const pastedText = event . clipboardData ?. getData ( 'text/plain' ) ;
649+
650+ console . log ( '[MudClient] Clipboard content:' , {
651+ length : pastedText ?. length ?? 0 ,
652+ preview : pastedText ?. substring ( 0 , 50 ) ,
653+ } ) ;
654+
655+ if ( pastedText ) {
656+ // Prevent the default onData behavior (which sends \u0016 only)
657+ event . preventDefault ( ) ;
658+
659+ if ( ! this . state . isEditMode ) {
660+ // In non-edit mode, send paste content directly to server
661+ this . mudService . sendMessage ( pastedText ) ;
662+ } else {
663+ // In edit mode, route through input controller for buffering and echo
664+ this . inputController . handleData ( pastedText ) ;
665+ }
666+ }
667+ } ;
668+
669+ element . addEventListener ( 'paste' , this . pasteHandler ) ;
670+ }
671+
672+ /**
673+ * Handles paste by reading clipboard content when Ctrl+V is detected.
674+ * Uses the Clipboard API which is safe to call here since it's triggered
675+ * by a user gesture (Ctrl+V keypress).
676+ */
677+ private async handlePasteFromClipboard ( ) : Promise < void > {
678+ try {
679+ const pastedText = await navigator . clipboard . readText ( ) ;
680+
681+ console . log ( '[MudClient] Clipboard content read:' , {
682+ length : pastedText . length ,
683+ preview : pastedText . substring ( 0 , 50 ) ,
684+ } ) ;
685+
686+ if ( pastedText ) {
687+ if ( ! this . state . isEditMode ) {
688+ // In non-edit mode, send paste content directly to server
689+ this . mudService . sendMessage ( pastedText ) ;
690+ } else {
691+ // In edit mode, route through input controller for buffering and echo
692+ this . inputController . handleData ( pastedText ) ;
693+ }
694+ }
695+ } catch ( err ) {
696+ console . error ( '[MudClient] Failed to read clipboard:' , err ) ;
697+ }
698+ }
598699}
0 commit comments