@@ -17,6 +17,11 @@ import { MudService } from '../../services/mud.service';
1717import { SecureString } from '@mudlet3/frontend/shared' ;
1818
1919type SocketListener = EventListener ;
20+ type MudSocketAdapterHooks = {
21+ transformMessage ?: ( data : string ) => string ;
22+ beforeMessage ?: ( data : string ) => void ;
23+ afterMessage ?: ( data : string ) => void ;
24+ } ;
2025
2126class MudSocketAdapter {
2227 public binaryType : BinaryType = 'arraybuffer' ;
@@ -25,9 +30,23 @@ class MudSocketAdapter {
2530 private readonly listeners = new Map < string , Set < SocketListener > > ( ) ;
2631 private readonly subscription : Subscription ;
2732
28- constructor ( private readonly mudService : MudService ) {
33+ constructor (
34+ private readonly mudService : MudService ,
35+ private readonly hooks ?: MudSocketAdapterHooks ,
36+ ) {
2937 this . subscription = this . mudService . mudOutput$ . subscribe ( ( { data } ) => {
30- this . dispatch ( 'message' , new MessageEvent ( 'message' , { data } ) ) ;
38+ this . hooks ?. beforeMessage ?.( data ) ;
39+
40+ const transformed = this . hooks ?. transformMessage ?.( data ) ?? data ;
41+
42+ if ( transformed . length > 0 ) {
43+ this . dispatch (
44+ 'message' ,
45+ new MessageEvent ( 'message' , { data : transformed } ) ,
46+ ) ;
47+ }
48+
49+ this . hooks ?. afterMessage ?.( transformed ) ;
3150 } ) ;
3251 }
3352
@@ -88,7 +107,14 @@ export class MudClientComponent implements AfterViewInit, OnDestroy {
88107
89108 private readonly terminal : Terminal ;
90109 private readonly terminalFitAddon = new FitAddon ( ) ;
91- private readonly socketAdapter = new MudSocketAdapter ( this . mudService ) ;
110+ private readonly socketAdapter = new MudSocketAdapter (
111+ this . mudService ,
112+ {
113+ transformMessage : ( data ) => this . transformMudOutput ( data ) ,
114+ beforeMessage : ( data ) => this . beforeMudOutput ( data ) ,
115+ afterMessage : ( data ) => this . afterMudOutput ( data ) ,
116+ } ,
117+ ) ;
92118 private readonly terminalAttachAddon = new AttachAddon (
93119 this . socketAdapter as unknown as WebSocket ,
94120 { bidirectional : false } ,
@@ -107,6 +133,11 @@ export class MudClientComponent implements AfterViewInit, OnDestroy {
107133 private currentShowEcho = true ;
108134 private isEditMode = true ;
109135 private lastViewportSize ?: { columns : number ; rows : number } ;
136+ private terminalReady = false ;
137+ private editLineHidden = false ;
138+ private serverLineBuffer = '' ;
139+ private hiddenPrompt = '' ;
140+ private leadingLineBreaksToStrip = 0 ;
110141
111142 @ViewChild ( 'hostRef' , { static : true } )
112143 private readonly terminalRef ! : ElementRef < HTMLDivElement > ;
@@ -143,6 +174,7 @@ export class MudClientComponent implements AfterViewInit, OnDestroy {
143174 ) ;
144175
145176 this . resizeObs . observe ( this . terminalRef . nativeElement ) ;
177+ this . terminalReady = true ;
146178
147179 const columns = this . terminal . cols ;
148180 const rows = this . terminal . rows + 1 ;
@@ -269,6 +301,10 @@ export class MudClientComponent implements AfterViewInit, OnDestroy {
269301 this . lastInputWasCarriageReturn = false ;
270302 }
271303
304+ this . editLineHidden = false ;
305+ this . serverLineBuffer = '' ;
306+ this . hiddenPrompt = '' ;
307+ this . leadingLineBreaksToStrip = 0 ;
272308 this . updateLocalEcho ( this . currentShowEcho ) ;
273309 }
274310
@@ -315,4 +351,142 @@ export class MudClientComponent implements AfterViewInit, OnDestroy {
315351 // Default to consuming only the ESC character
316352 return 1 ;
317353 }
354+
355+ private beforeMudOutput ( _data : string ) {
356+ if (
357+ ! this . isEditMode ||
358+ ! this . terminalReady ||
359+ ! this . localEchoEnabled ||
360+ this . inputBuffer . length === 0 ||
361+ this . editLineHidden
362+ ) {
363+ return ;
364+ }
365+
366+ this . hiddenPrompt = this . serverLineBuffer ;
367+ this . serverLineBuffer = '' ;
368+ this . leadingLineBreaksToStrip = 1 ;
369+ this . terminal . write ( '\r\u001b[2K' ) ;
370+ this . editLineHidden = true ;
371+ }
372+
373+ private afterMudOutput ( data : string ) {
374+ this . trackServerLine ( data ) ;
375+
376+ if (
377+ ! this . editLineHidden ||
378+ ! this . isEditMode ||
379+ ! this . terminalReady ||
380+ ! this . localEchoEnabled ||
381+ this . inputBuffer . length === 0
382+ ) {
383+ return ;
384+ }
385+
386+ queueMicrotask ( ( ) => this . restoreEditInput ( ) ) ;
387+ }
388+
389+ private restoreEditInput ( ) {
390+ if ( ! this . editLineHidden ) {
391+ return ;
392+ }
393+
394+ if (
395+ ! this . isEditMode ||
396+ ! this . terminalReady ||
397+ ! this . localEchoEnabled ||
398+ this . inputBuffer . length === 0
399+ ) {
400+ this . editLineHidden = false ;
401+ return ;
402+ }
403+
404+ this . terminal . write ( '\r\u001b[2K' ) ;
405+
406+ const prefix =
407+ this . serverLineBuffer . length > 0 ? this . serverLineBuffer : this . hiddenPrompt ;
408+
409+ if ( prefix . length > 0 ) {
410+ this . terminal . write ( prefix ) ;
411+ }
412+
413+ this . terminal . write ( this . inputBuffer ) ;
414+ this . editLineHidden = false ;
415+ this . hiddenPrompt = '' ;
416+ this . serverLineBuffer = prefix ;
417+ this . leadingLineBreaksToStrip = 0 ;
418+ }
419+
420+ private transformMudOutput ( data : string ) : string {
421+ if ( this . leadingLineBreaksToStrip === 0 || data . length === 0 ) {
422+ return data ;
423+ }
424+
425+ let startIndex = 0 ;
426+ let remainingBreaks = this . leadingLineBreaksToStrip ;
427+
428+ while ( startIndex < data . length && remainingBreaks > 0 ) {
429+ const char = data [ startIndex ] ;
430+
431+ if ( char === '\n' ) {
432+ remainingBreaks -= 1 ;
433+ startIndex += 1 ;
434+ continue ;
435+ }
436+
437+ if ( char === '\r' ) {
438+ startIndex += 1 ;
439+ continue ;
440+ }
441+
442+ break ;
443+ }
444+
445+ this . leadingLineBreaksToStrip = remainingBreaks ;
446+
447+ if ( startIndex === 0 ) {
448+ this . leadingLineBreaksToStrip = 0 ;
449+ return data ;
450+ }
451+
452+ if ( startIndex >= data . length ) {
453+ return '' ;
454+ }
455+
456+ this . leadingLineBreaksToStrip = 0 ;
457+ return data . slice ( startIndex ) ;
458+ }
459+
460+ private trackServerLine ( data : string ) {
461+ let index = 0 ;
462+
463+ while ( index < data . length ) {
464+ const char = data [ index ] ;
465+
466+ if ( char === '\r' || char === '\n' ) {
467+ this . serverLineBuffer = '' ;
468+ index += 1 ;
469+ continue ;
470+ }
471+
472+ if ( char === '\b' || char === '\u007f' ) {
473+ this . serverLineBuffer = this . serverLineBuffer . slice ( 0 , - 1 ) ;
474+ index += 1 ;
475+ continue ;
476+ }
477+
478+ if ( char === '\u001b' ) {
479+ const consumed = this . skipEscapeSequence ( data . slice ( index ) ) ;
480+ const sequence =
481+ consumed > 0 ? data . slice ( index , index + consumed ) : char ;
482+
483+ this . serverLineBuffer += sequence ;
484+ index += Math . max ( consumed , 1 ) ;
485+ continue ;
486+ }
487+
488+ this . serverLineBuffer += char ;
489+ index += 1 ;
490+ }
491+ }
318492}
0 commit comments