@aurigax-ai/ghostty-web 0.3.3 → 0.3.4

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,6 +38,7 @@ 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
43
44
  smoothScrollDuration?: number; // Duration in ms for smooth scroll animation (default: 100, 0 = instant)
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,7 @@ 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,
205
213
  smoothScrollDuration: options.smoothScrollDuration ?? 100, // Default: 100ms smooth scroll
206
214
  fontWeight: options.fontWeight ?? 'normal',
207
215
  fontWeightBold: options.fontWeightBold ?? 'bold',
@@ -510,6 +518,7 @@ export class Terminal implements ITerminalCore {
510
518
  }
511
519
  // Clear selection when user types
512
520
  this.selectionManager?.clearSelection();
521
+ this.followUserInput();
513
522
  this.awaitingEcho = true;
514
523
  // Input handler fires data events
515
524
  this.dataEmitter.fire(data);
@@ -723,6 +732,8 @@ export class Terminal implements ITerminalCore {
723
732
  // preserve selection when new data arrives. Selection is cleared by user actions
724
733
  // like clicking or typing, not by incoming data.
725
734
 
735
+ const anchor = this.anchorViewport();
736
+
726
737
  // Write directly to WASM terminal (handles VT parsing internally)
727
738
  this.wasmTerm!.write(data);
728
739
 
@@ -733,10 +744,7 @@ export class Terminal implements ITerminalCore {
733
744
  // Invalidate link cache (content changed)
734
745
  this.linkDetector?.invalidateCache();
735
746
 
736
- // Phase 2: Auto-scroll to bottom on new output (xterm.js behavior)
737
- if (this.viewportY !== 0) {
738
- this.scrollToBottom();
739
- }
747
+ this.restoreViewport(anchor);
740
748
 
741
749
  this.afterWrite();
742
750
 
@@ -755,6 +763,40 @@ export class Terminal implements ITerminalCore {
755
763
  // Render will happen on next animation frame
756
764
  }
757
765
 
766
+ private anchorViewport(): ViewportAnchor | null {
767
+ if (this.viewportY <= 0 || !this.wasmTerm) return null;
768
+ if (this.scrollAnimationFrame !== undefined && this.targetViewportY === 0) return null;
769
+ const scrollback = this.wasmTerm.getScrollbackLength();
770
+ const row = this.wasmTerm.trackScreenRow(scrollback - Math.floor(this.viewportY));
771
+ if (!row) return null;
772
+ return { row, viewportY: this.viewportY, scrollback };
773
+ }
774
+
775
+ private restoreViewport(anchor: ViewportAnchor | null): void {
776
+ if (!anchor) return;
777
+ const line = anchor.row.line();
778
+ anchor.row.free();
779
+ const scrollback = this.getScrollbackLength();
780
+ const fraction = anchor.viewportY - Math.floor(anchor.viewportY);
781
+ const viewportY =
782
+ line === null ? scrollback : Math.max(0, Math.min(scrollback, scrollback - line + fraction));
783
+ const shift = viewportY - anchor.viewportY;
784
+ this.viewportY = viewportY;
785
+ this.targetViewportY =
786
+ this.scrollAnimationFrame === undefined
787
+ ? viewportY
788
+ : Math.max(0, Math.min(scrollback, this.targetViewportY + shift));
789
+ if (Math.round(scrollback - viewportY) !== Math.round(anchor.scrollback - anchor.viewportY)) {
790
+ this.fireScroll();
791
+ }
792
+ }
793
+
794
+ private followUserInput(): void {
795
+ if (this.options.scrollOnUserInput) {
796
+ this.scrollToBottom();
797
+ }
798
+ }
799
+
758
800
  /**
759
801
  * Write data with newline
760
802
  */
@@ -782,6 +824,7 @@ export class Terminal implements ITerminalCore {
782
824
  return;
783
825
  }
784
826
 
827
+ this.followUserInput();
785
828
  this.awaitingEcho = true;
786
829
 
787
830
  // Check if terminal has bracketed paste mode enabled
@@ -809,6 +852,7 @@ export class Terminal implements ITerminalCore {
809
852
  }
810
853
 
811
854
  if (wasUserInput) {
855
+ this.followUserInput();
812
856
  this.awaitingEcho = true;
813
857
  // Trigger onData event as if user typed it
814
858
  this.dataEmitter.fire(data);
@@ -839,8 +883,11 @@ export class Terminal implements ITerminalCore {
839
883
  this.cols = cols;
840
884
  this.rows = rows;
841
885
 
886
+ const anchor = this.anchorViewport();
887
+
842
888
  // Resize WASM terminal (may reallocate buffers, invalidating TypedArray views)
843
889
  this.wasmTerm!.resize(cols, rows);
890
+ this.restoreViewport(anchor);
844
891
  this.refreshMarkers(false);
845
892
 
846
893
  // Resize renderer
@@ -1104,6 +1151,7 @@ export class Terminal implements ITerminalCore {
1104
1151
  */
1105
1152
  public scrollToBottom(): void {
1106
1153
  this.wake();
1154
+ this.stopScrollAnimation();
1107
1155
  if (this.viewportY !== 0) {
1108
1156
  this.viewportY = 0;
1109
1157
  this.fireScroll();
@@ -1114,6 +1162,15 @@ export class Terminal implements ITerminalCore {
1114
1162
  }
1115
1163
  }
1116
1164
 
1165
+ private stopScrollAnimation(): void {
1166
+ if (this.scrollAnimationFrame !== undefined) {
1167
+ cancelAnimationFrame(this.scrollAnimationFrame);
1168
+ }
1169
+ this.scrollAnimationFrame = undefined;
1170
+ this.scrollAnimationStartTime = undefined;
1171
+ this.targetViewportY = 0;
1172
+ }
1173
+
1117
1174
  /** Scrolls so that absolute buffer line `line` (0 = oldest scrollback line) is at the top, like xterm.js. */
1118
1175
  public scrollToLine(line: number): void {
1119
1176
  const scrollbackLength = this.getScrollbackLength();
@@ -1250,11 +1307,7 @@ export class Terminal implements ITerminalCore {
1250
1307
  this.cancelRenderLoop();
1251
1308
  this.writeQueue.length = 0;
1252
1309
 
1253
- // Stop smooth scroll animation
1254
- if (this.scrollAnimationFrame) {
1255
- cancelAnimationFrame(this.scrollAnimationFrame);
1256
- this.scrollAnimationFrame = undefined;
1257
- }
1310
+ this.stopScrollAnimation();
1258
1311
 
1259
1312
  // Clear mouse move throttle timeout
1260
1313
  if (this.mouseMoveThrottleTimeout) {
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.4",
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",