@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 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
- // Phase 2: Auto-scroll to bottom on new output (xterm.js behavior)
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
- // Stop smooth scroll animation
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; // No scrollbar if no scrollback
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",
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",