@aurigax-ai/ghostty-web 0.3.3 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/ghostty.ts +5 -0
- package/lib/interfaces.ts +2 -0
- package/lib/terminal.ts +83 -10
- package/package.json +1 -1
package/lib/ghostty.ts
CHANGED
|
@@ -864,6 +864,11 @@ export class GhosttyTerminal {
|
|
|
864
864
|
return this.trackLineStart(POINT_ACTIVE, activeY);
|
|
865
865
|
}
|
|
866
866
|
|
|
867
|
+
/** Tracks the start of an absolute line (scrollback first). */
|
|
868
|
+
trackScreenRow(screenY: number): TrackedRow | null {
|
|
869
|
+
return this.trackLineStart(POINT_SCREEN, screenY);
|
|
870
|
+
}
|
|
871
|
+
|
|
867
872
|
/**
|
|
868
873
|
* Whether the terminal may have discarded rows since the last call, which is
|
|
869
874
|
* the only way a tracked row loses its line: libghostty-vt prunes scrollback
|
package/lib/interfaces.ts
CHANGED
|
@@ -38,8 +38,10 @@ export interface ITerminalOptions {
|
|
|
38
38
|
// Phase 1 additions
|
|
39
39
|
convertEol?: boolean; // Convert \n to \r\n (default: false)
|
|
40
40
|
disableStdin?: boolean; // Disable keyboard input (default: false)
|
|
41
|
+
scrollOnUserInput?: boolean; // Typing or pasting scrolls to the bottom (default: true)
|
|
41
42
|
|
|
42
43
|
// Scrolling options
|
|
44
|
+
scrollbar?: boolean; // Draw the built-in scrollbar and let it be dragged (default: true)
|
|
43
45
|
smoothScrollDuration?: number; // Duration in ms for smooth scroll animation (default: 100, 0 = instant)
|
|
44
46
|
|
|
45
47
|
// Internal: Ghostty WASM instance (optional, for test isolation)
|
package/lib/terminal.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
type GhosttyTerminalConfig,
|
|
27
27
|
type MouseTrackingMode,
|
|
28
28
|
type SemanticPromptEvent,
|
|
29
|
+
type TrackedRow,
|
|
29
30
|
} from './ghostty';
|
|
30
31
|
import { getGhostty } from './index';
|
|
31
32
|
import { InputHandler, type MouseTrackingConfig } from './input-handler';
|
|
@@ -60,6 +61,12 @@ import { WebglRenderer } from './webgl-renderer';
|
|
|
60
61
|
// Terminal Class
|
|
61
62
|
// ============================================================================
|
|
62
63
|
|
|
64
|
+
interface ViewportAnchor {
|
|
65
|
+
row: TrackedRow;
|
|
66
|
+
viewportY: number;
|
|
67
|
+
scrollback: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
63
70
|
export class Terminal implements ITerminalCore {
|
|
64
71
|
// Public properties (xterm.js compatibility)
|
|
65
72
|
public cols: number;
|
|
@@ -202,6 +209,8 @@ export class Terminal implements ITerminalCore {
|
|
|
202
209
|
renderer: options.renderer ?? 'webgl',
|
|
203
210
|
convertEol: options.convertEol ?? false,
|
|
204
211
|
disableStdin: options.disableStdin ?? false,
|
|
212
|
+
scrollOnUserInput: options.scrollOnUserInput ?? true,
|
|
213
|
+
scrollbar: options.scrollbar ?? true,
|
|
205
214
|
smoothScrollDuration: options.smoothScrollDuration ?? 100, // Default: 100ms smooth scroll
|
|
206
215
|
fontWeight: options.fontWeight ?? 'normal',
|
|
207
216
|
fontWeightBold: options.fontWeightBold ?? 'bold',
|
|
@@ -291,6 +300,10 @@ export class Terminal implements ITerminalCore {
|
|
|
291
300
|
this.wasmTerm?.configure({ scrollbackLimit: this.options.scrollback });
|
|
292
301
|
break;
|
|
293
302
|
|
|
303
|
+
case 'scrollbar':
|
|
304
|
+
if (!this.options.scrollbar) this.dropScrollbar();
|
|
305
|
+
break;
|
|
306
|
+
|
|
294
307
|
case 'fontSize':
|
|
295
308
|
if (this.renderer) {
|
|
296
309
|
this.renderer.setFontSize(this.options.fontSize);
|
|
@@ -510,6 +523,7 @@ export class Terminal implements ITerminalCore {
|
|
|
510
523
|
}
|
|
511
524
|
// Clear selection when user types
|
|
512
525
|
this.selectionManager?.clearSelection();
|
|
526
|
+
this.followUserInput();
|
|
513
527
|
this.awaitingEcho = true;
|
|
514
528
|
// Input handler fires data events
|
|
515
529
|
this.dataEmitter.fire(data);
|
|
@@ -723,6 +737,8 @@ export class Terminal implements ITerminalCore {
|
|
|
723
737
|
// preserve selection when new data arrives. Selection is cleared by user actions
|
|
724
738
|
// like clicking or typing, not by incoming data.
|
|
725
739
|
|
|
740
|
+
const anchor = this.anchorViewport();
|
|
741
|
+
|
|
726
742
|
// Write directly to WASM terminal (handles VT parsing internally)
|
|
727
743
|
this.wasmTerm!.write(data);
|
|
728
744
|
|
|
@@ -733,10 +749,7 @@ export class Terminal implements ITerminalCore {
|
|
|
733
749
|
// Invalidate link cache (content changed)
|
|
734
750
|
this.linkDetector?.invalidateCache();
|
|
735
751
|
|
|
736
|
-
|
|
737
|
-
if (this.viewportY !== 0) {
|
|
738
|
-
this.scrollToBottom();
|
|
739
|
-
}
|
|
752
|
+
this.restoreViewport(anchor);
|
|
740
753
|
|
|
741
754
|
this.afterWrite();
|
|
742
755
|
|
|
@@ -755,6 +768,40 @@ export class Terminal implements ITerminalCore {
|
|
|
755
768
|
// Render will happen on next animation frame
|
|
756
769
|
}
|
|
757
770
|
|
|
771
|
+
private anchorViewport(): ViewportAnchor | null {
|
|
772
|
+
if (this.viewportY <= 0 || !this.wasmTerm) return null;
|
|
773
|
+
if (this.scrollAnimationFrame !== undefined && this.targetViewportY === 0) return null;
|
|
774
|
+
const scrollback = this.wasmTerm.getScrollbackLength();
|
|
775
|
+
const row = this.wasmTerm.trackScreenRow(scrollback - Math.floor(this.viewportY));
|
|
776
|
+
if (!row) return null;
|
|
777
|
+
return { row, viewportY: this.viewportY, scrollback };
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
private restoreViewport(anchor: ViewportAnchor | null): void {
|
|
781
|
+
if (!anchor) return;
|
|
782
|
+
const line = anchor.row.line();
|
|
783
|
+
anchor.row.free();
|
|
784
|
+
const scrollback = this.getScrollbackLength();
|
|
785
|
+
const fraction = anchor.viewportY - Math.floor(anchor.viewportY);
|
|
786
|
+
const viewportY =
|
|
787
|
+
line === null ? scrollback : Math.max(0, Math.min(scrollback, scrollback - line + fraction));
|
|
788
|
+
const shift = viewportY - anchor.viewportY;
|
|
789
|
+
this.viewportY = viewportY;
|
|
790
|
+
this.targetViewportY =
|
|
791
|
+
this.scrollAnimationFrame === undefined
|
|
792
|
+
? viewportY
|
|
793
|
+
: Math.max(0, Math.min(scrollback, this.targetViewportY + shift));
|
|
794
|
+
if (Math.round(scrollback - viewportY) !== Math.round(anchor.scrollback - anchor.viewportY)) {
|
|
795
|
+
this.fireScroll();
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
private followUserInput(): void {
|
|
800
|
+
if (this.options.scrollOnUserInput) {
|
|
801
|
+
this.scrollToBottom();
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
758
805
|
/**
|
|
759
806
|
* Write data with newline
|
|
760
807
|
*/
|
|
@@ -782,6 +829,7 @@ export class Terminal implements ITerminalCore {
|
|
|
782
829
|
return;
|
|
783
830
|
}
|
|
784
831
|
|
|
832
|
+
this.followUserInput();
|
|
785
833
|
this.awaitingEcho = true;
|
|
786
834
|
|
|
787
835
|
// Check if terminal has bracketed paste mode enabled
|
|
@@ -809,6 +857,7 @@ export class Terminal implements ITerminalCore {
|
|
|
809
857
|
}
|
|
810
858
|
|
|
811
859
|
if (wasUserInput) {
|
|
860
|
+
this.followUserInput();
|
|
812
861
|
this.awaitingEcho = true;
|
|
813
862
|
// Trigger onData event as if user typed it
|
|
814
863
|
this.dataEmitter.fire(data);
|
|
@@ -839,8 +888,11 @@ export class Terminal implements ITerminalCore {
|
|
|
839
888
|
this.cols = cols;
|
|
840
889
|
this.rows = rows;
|
|
841
890
|
|
|
891
|
+
const anchor = this.anchorViewport();
|
|
892
|
+
|
|
842
893
|
// Resize WASM terminal (may reallocate buffers, invalidating TypedArray views)
|
|
843
894
|
this.wasmTerm!.resize(cols, rows);
|
|
895
|
+
this.restoreViewport(anchor);
|
|
844
896
|
this.refreshMarkers(false);
|
|
845
897
|
|
|
846
898
|
// Resize renderer
|
|
@@ -1104,6 +1156,7 @@ export class Terminal implements ITerminalCore {
|
|
|
1104
1156
|
*/
|
|
1105
1157
|
public scrollToBottom(): void {
|
|
1106
1158
|
this.wake();
|
|
1159
|
+
this.stopScrollAnimation();
|
|
1107
1160
|
if (this.viewportY !== 0) {
|
|
1108
1161
|
this.viewportY = 0;
|
|
1109
1162
|
this.fireScroll();
|
|
@@ -1114,6 +1167,15 @@ export class Terminal implements ITerminalCore {
|
|
|
1114
1167
|
}
|
|
1115
1168
|
}
|
|
1116
1169
|
|
|
1170
|
+
private stopScrollAnimation(): void {
|
|
1171
|
+
if (this.scrollAnimationFrame !== undefined) {
|
|
1172
|
+
cancelAnimationFrame(this.scrollAnimationFrame);
|
|
1173
|
+
}
|
|
1174
|
+
this.scrollAnimationFrame = undefined;
|
|
1175
|
+
this.scrollAnimationStartTime = undefined;
|
|
1176
|
+
this.targetViewportY = 0;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1117
1179
|
/** Scrolls so that absolute buffer line `line` (0 = oldest scrollback line) is at the top, like xterm.js. */
|
|
1118
1180
|
public scrollToLine(line: number): void {
|
|
1119
1181
|
const scrollbackLength = this.getScrollbackLength();
|
|
@@ -1250,11 +1312,7 @@ export class Terminal implements ITerminalCore {
|
|
|
1250
1312
|
this.cancelRenderLoop();
|
|
1251
1313
|
this.writeQueue.length = 0;
|
|
1252
1314
|
|
|
1253
|
-
|
|
1254
|
-
if (this.scrollAnimationFrame) {
|
|
1255
|
-
cancelAnimationFrame(this.scrollAnimationFrame);
|
|
1256
|
-
this.scrollAnimationFrame = undefined;
|
|
1257
|
-
}
|
|
1315
|
+
this.stopScrollAnimation();
|
|
1258
1316
|
|
|
1259
1317
|
// Clear mouse move throttle timeout
|
|
1260
1318
|
if (this.mouseMoveThrottleTimeout) {
|
|
@@ -1774,7 +1832,7 @@ export class Terminal implements ITerminalCore {
|
|
|
1774
1832
|
if (!this.canvas || !this.renderer || !this.wasmTerm) return;
|
|
1775
1833
|
|
|
1776
1834
|
const scrollbackLength = this.wasmTerm.getScrollbackLength();
|
|
1777
|
-
if (scrollbackLength === 0) return;
|
|
1835
|
+
if (scrollbackLength === 0 || !this.options.scrollbar) return;
|
|
1778
1836
|
|
|
1779
1837
|
const rect = this.canvas.getBoundingClientRect();
|
|
1780
1838
|
const mouseX = e.clientX - rect.left;
|
|
@@ -1885,6 +1943,7 @@ export class Terminal implements ITerminalCore {
|
|
|
1885
1943
|
* Show scrollbar with fade-in and schedule auto-hide
|
|
1886
1944
|
*/
|
|
1887
1945
|
private showScrollbar(): void {
|
|
1946
|
+
if (!this.options.scrollbar) return;
|
|
1888
1947
|
this.wake();
|
|
1889
1948
|
// Clear any existing hide timeout
|
|
1890
1949
|
if (this.scrollbarHideTimeout) {
|
|
@@ -1910,6 +1969,19 @@ export class Terminal implements ITerminalCore {
|
|
|
1910
1969
|
}
|
|
1911
1970
|
}
|
|
1912
1971
|
|
|
1972
|
+
private dropScrollbar(): void {
|
|
1973
|
+
this.wake();
|
|
1974
|
+
if (this.scrollbarHideTimeout) {
|
|
1975
|
+
window.clearTimeout(this.scrollbarHideTimeout);
|
|
1976
|
+
this.scrollbarHideTimeout = undefined;
|
|
1977
|
+
}
|
|
1978
|
+
this.isDraggingScrollbar = false;
|
|
1979
|
+
this.scrollbarDragStart = null;
|
|
1980
|
+
this.scrollbarVisible = false;
|
|
1981
|
+
this.scrollbarOpacity = 0;
|
|
1982
|
+
this.draw(true, 0);
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1913
1985
|
/**
|
|
1914
1986
|
* Hide scrollbar with fade-out
|
|
1915
1987
|
*/
|
|
@@ -1932,6 +2004,7 @@ export class Terminal implements ITerminalCore {
|
|
|
1932
2004
|
this.wake();
|
|
1933
2005
|
const startTime = Date.now();
|
|
1934
2006
|
const animate = () => {
|
|
2007
|
+
if (!this.options.scrollbar) return;
|
|
1935
2008
|
const elapsed = Date.now() - startTime;
|
|
1936
2009
|
const progress = Math.min(elapsed / this.SCROLLBAR_FADE_DURATION_MS, 1);
|
|
1937
2010
|
this.scrollbarOpacity = progress;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aurigax-ai/ghostty-web",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "Ghostty's terminal engine (libghostty-vt) in the browser: WebAssembly build, an xterm.js-shaped API and WebGL and canvas renderers. A hard fork of coder/ghostty-web.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.ts",
|