@aurigax-ai/ghostty-web 0.3.2 → 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
@@ -192,7 +192,6 @@ export class InputHandler {
192
192
  private mousedownListener: ((e: MouseEvent) => void) | null = null;
193
193
  private mouseupListener: ((e: MouseEvent) => void) | null = null;
194
194
  private mousemoveListener: ((e: MouseEvent) => void) | null = null;
195
- private wheelListener: ((e: WheelEvent) => void) | null = null;
196
195
  private isComposing = false;
197
196
  private isDisposed = false;
198
197
  /**
@@ -319,9 +318,6 @@ export class InputHandler {
319
318
 
320
319
  this.mousemoveListener = this.handleMouseMove.bind(this);
321
320
  this.container.addEventListener('mousemove', this.mousemoveListener);
322
-
323
- this.wheelListener = this.handleWheel.bind(this);
324
- this.container.addEventListener('wheel', this.wheelListener, { passive: false });
325
321
  }
326
322
 
327
323
  /**
@@ -925,7 +921,7 @@ export class InputHandler {
925
921
  /**
926
922
  * Handle wheel event (scroll)
927
923
  */
928
- private handleWheel(event: WheelEvent): void {
924
+ handleWheel(event: WheelEvent): void {
929
925
  if (this.isDisposed) return;
930
926
  if (!this.mouseConfig?.hasMouseTracking()) return;
931
927
 
@@ -1129,11 +1125,6 @@ export class InputHandler {
1129
1125
  this.mousemoveListener = null;
1130
1126
  }
1131
1127
 
1132
- if (this.wheelListener) {
1133
- this.container.removeEventListener('wheel', this.wheelListener);
1134
- this.wheelListener = null;
1135
- }
1136
-
1137
1128
  this.isDisposed = true;
1138
1129
  }
1139
1130
 
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) {
@@ -1709,6 +1762,11 @@ export class Terminal implements ITerminalCore {
1709
1762
  return;
1710
1763
  }
1711
1764
 
1765
+ if (this.wasmTerm?.hasMouseTracking()) {
1766
+ this.inputHandler?.handleWheel(e);
1767
+ return;
1768
+ }
1769
+
1712
1770
  // Check if in alternate screen mode (vim, less, htop, etc.)
1713
1771
  const isAltScreen = this.wasmTerm?.isAlternateScreen() ?? false;
1714
1772
 
@@ -1717,7 +1775,8 @@ export class Terminal implements ITerminalCore {
1717
1775
  // Applications like vim handle scrolling internally
1718
1776
  // Standard: ~3 arrow presses per wheel "click"
1719
1777
  const direction = e.deltaY > 0 ? 'down' : 'up';
1720
- const count = Math.min(Math.abs(Math.round(e.deltaY / 33)), 5); // Cap at 5
1778
+ const lines = e.deltaMode === WheelEvent.DOM_DELTA_LINE ? e.deltaY : e.deltaY / 33;
1779
+ const count = Math.min(Math.abs(Math.round(lines)), 5); // Cap at 5
1721
1780
 
1722
1781
  for (let i = 0; i < count; i++) {
1723
1782
  if (direction === 'up') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aurigax-ai/ghostty-web",
3
- "version": "0.3.2",
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",