@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 +5 -0
- package/lib/input-handler.ts +1 -10
- package/lib/interfaces.ts +1 -0
- package/lib/terminal.ts +69 -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/input-handler.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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",
|