Skip to content

Commit 4e13414

Browse files
committed
feat(frontend): enhance MudSocketAdapter with hooks for message transformation and lifecycle management
1 parent 2527d7a commit 4e13414

File tree

1 file changed

+177
-3
lines changed

1 file changed

+177
-3
lines changed

frontend/src/app/core/mud/components/mud-client/mud-client.component.ts

Lines changed: 177 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ import { MudService } from '../../services/mud.service';
1717
import { SecureString } from '@mudlet3/frontend/shared';
1818

1919
type SocketListener = EventListener;
20+
type MudSocketAdapterHooks = {
21+
transformMessage?: (data: string) => string;
22+
beforeMessage?: (data: string) => void;
23+
afterMessage?: (data: string) => void;
24+
};
2025

2126
class 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

Comments
 (0)