Skip to content

Commit a884a3d

Browse files
sreya0xBigBoss
andcommitted
fix: prevent terminal crash on resize during high-output programs
Cancel the render loop before WASM resize to prevent accessing detached TypedArray views when buffers are reallocated. Restart render loop and flush any queued writes synchronously after resize completes. This avoids the background-tab regression from PR #114's approach of using an isResizing flag cleared via requestAnimationFrame (rAF is throttled/paused in background tabs, causing writes to queue indefinitely). Changes: - terminal.ts: Add cancelRenderLoop()/flushWriteQueue(), wrap resize() in try/catch with render loop pause/restart, add defensive writeQueue - ghostty.ts: Invalidate grapheme buffers in invalidateBuffers() alongside viewport buffers to prevent stale TypedArray access Co-authored-by: 0xBigBoss <95193764+0xBigBoss@users.noreply.github.com>
1 parent 3525675 commit a884a3d

File tree

2 files changed

+60
-22
lines changed

2 files changed

+60
-22
lines changed

lib/ghostty.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -874,5 +874,10 @@ export class GhosttyTerminal {
874874
this.viewportBufferPtr = 0;
875875
this.viewportBufferSize = 0;
876876
}
877+
if (this.graphemeBufferPtr) {
878+
this.exports.ghostty_wasm_free_u8_array(this.graphemeBufferPtr, 16 * 4);
879+
this.graphemeBufferPtr = 0;
880+
}
881+
this.graphemeBuffer = null;
877882
}
878883
}

lib/terminal.ts

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ export class Terminal implements ITerminalCore {
101101
private isOpen = false;
102102
private isDisposed = false;
103103
private animationFrameId?: number;
104+
private writeQueue: Uint8Array[] = [];
104105

105106
// Addons
106107
private addons: ITerminalAddon[] = [];
@@ -660,28 +661,42 @@ export class Terminal implements ITerminalCore {
660661
return; // No change
661662
}
662663

663-
// Update dimensions
664-
this.cols = cols;
665-
this.rows = rows;
664+
// Cancel render loop before resize to prevent accessing detached TypedArray
665+
// views while WASM reallocates buffers. We restart it after resize completes.
666+
// This avoids the background-tab regression of using an isResizing flag
667+
// cleared via requestAnimationFrame (rAF is throttled/paused in background tabs).
668+
this.cancelRenderLoop();
666669

667-
// Resize WASM terminal
668-
this.wasmTerm!.resize(cols, rows);
670+
try {
671+
// Update dimensions
672+
this.cols = cols;
673+
this.rows = rows;
674+
675+
// Resize WASM terminal (may reallocate buffers, invalidating TypedArray views)
676+
this.wasmTerm!.resize(cols, rows);
677+
678+
// Resize renderer
679+
this.renderer!.resize(cols, rows);
669680

670-
// Resize renderer
671-
this.renderer!.resize(cols, rows);
681+
// Update canvas dimensions
682+
const metrics = this.renderer!.getMetrics();
683+
this.canvas!.width = metrics.width * cols;
684+
this.canvas!.height = metrics.height * rows;
685+
this.canvas!.style.width = `${metrics.width * cols}px`;
686+
this.canvas!.style.height = `${metrics.height * rows}px`;
672687

673-
// Update canvas dimensions
674-
const metrics = this.renderer!.getMetrics();
675-
this.canvas!.width = metrics.width * cols;
676-
this.canvas!.height = metrics.height * rows;
677-
this.canvas!.style.width = `${metrics.width * cols}px`;
678-
this.canvas!.style.height = `${metrics.height * rows}px`;
688+
// Fire resize event
689+
this.resizeEmitter.fire({ cols, rows });
679690

680-
// Fire resize event
681-
this.resizeEmitter.fire({ cols, rows });
691+
// Force full render
692+
this.renderer!.render(this.wasmTerm!, true, this.viewportY, this);
693+
} catch (e) {
694+
console.error('Terminal resize failed:', e);
695+
}
682696

683-
// Force full render
684-
this.renderer!.render(this.wasmTerm!, true, this.viewportY, this);
697+
// Flush any writes that were queued during resize, then restart render loop
698+
this.flushWriteQueue();
699+
this.startRenderLoop();
685700
}
686701

687702
/**
@@ -1068,11 +1083,9 @@ export class Terminal implements ITerminalCore {
10681083
this.isDisposed = true;
10691084
this.isOpen = false;
10701085

1071-
// Stop render loop
1072-
if (this.animationFrameId) {
1073-
cancelAnimationFrame(this.animationFrameId);
1074-
this.animationFrameId = undefined;
1075-
}
1086+
// Stop render loop and clear write queue
1087+
this.cancelRenderLoop();
1088+
this.writeQueue.length = 0;
10761089

10771090
// Stop smooth scroll animation
10781091
if (this.scrollAnimationFrame) {
@@ -1112,6 +1125,26 @@ export class Terminal implements ITerminalCore {
11121125
// Private Methods
11131126
// ==========================================================================
11141127

1128+
/**
1129+
* Cancel the render loop
1130+
*/
1131+
private cancelRenderLoop(): void {
1132+
if (this.animationFrameId) {
1133+
cancelAnimationFrame(this.animationFrameId);
1134+
this.animationFrameId = undefined;
1135+
}
1136+
}
1137+
1138+
/**
1139+
* Flush any writes that were queued during resize
1140+
*/
1141+
private flushWriteQueue(): void {
1142+
while (this.writeQueue.length > 0) {
1143+
const data = this.writeQueue.shift()!;
1144+
this.wasmTerm!.write(data);
1145+
}
1146+
}
1147+
11151148
/**
11161149
* Start the render loop
11171150
*/

0 commit comments

Comments
 (0)