@cplieger/web-terminal-engine 2.3.2 → 2.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/jsr.json +1 -1
- package/package.json +4 -4
- package/src/mouse.ts +21 -0
- package/src/scroll.ts +43 -8
package/jsr.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/web-terminal-engine",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"typecheck": "tsc -p tsconfig.json",
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
"@typescript/native": "npm:typescript@7.0.2",
|
|
16
16
|
"@vitest/coverage-v8": "4.1.10",
|
|
17
17
|
"esbuild": "0.28.1",
|
|
18
|
-
"eslint": "10.
|
|
19
|
-
"fast-check": "4.
|
|
18
|
+
"eslint": "10.7.0",
|
|
19
|
+
"fast-check": "4.9.0",
|
|
20
20
|
"happy-dom": "20.10.6",
|
|
21
21
|
"pngjs": "7.0.0",
|
|
22
|
-
"prettier": "3.9.
|
|
22
|
+
"prettier": "3.9.5",
|
|
23
23
|
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
24
24
|
"typescript-eslint": "8.63.0",
|
|
25
25
|
"vitest": "4.1.10"
|
package/src/mouse.ts
CHANGED
|
@@ -85,6 +85,13 @@ export interface MouseInputHandler {
|
|
|
85
85
|
|
|
86
86
|
let handler: MouseInputHandler | null = null;
|
|
87
87
|
let lastButton = -1;
|
|
88
|
+
// True while a Shift-initiated press is in flight. Shift+click/drag is the
|
|
89
|
+
// xterm convention for "bypass application mouse tracking": the gesture is
|
|
90
|
+
// reserved for the browser's native text selection instead of being reported
|
|
91
|
+
// to (and preventDefault-ed away from) the TUI. Tracked from press to release
|
|
92
|
+
// so the move/up of a bypassed drag stay bypassed even if Shift is lifted
|
|
93
|
+
// mid-drag.
|
|
94
|
+
let shiftBypass = false;
|
|
88
95
|
|
|
89
96
|
/**
|
|
90
97
|
* Initialize the mouse module by attaching pointer/wheel/focus listeners to
|
|
@@ -138,6 +145,12 @@ function onMouseDown(e: MouseEvent): void {
|
|
|
138
145
|
if (mode === 0 || !(isMouseSGR() || isMousePixels()) || !handler) {
|
|
139
146
|
return;
|
|
140
147
|
}
|
|
148
|
+
if (e.shiftKey) {
|
|
149
|
+
// Shift+press: leave the whole gesture to the browser (native selection).
|
|
150
|
+
shiftBypass = true;
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
shiftBypass = false;
|
|
141
154
|
const pos = pixelToCell(e);
|
|
142
155
|
if (!pos) {
|
|
143
156
|
return;
|
|
@@ -153,6 +166,11 @@ function onMouseUp(e: MouseEvent): void {
|
|
|
153
166
|
if (mode === 0 || !(isMouseSGR() || isMousePixels()) || !handler) {
|
|
154
167
|
return;
|
|
155
168
|
}
|
|
169
|
+
if (shiftBypass) {
|
|
170
|
+
// Release of a Shift-initiated (native-selection) gesture: report nothing.
|
|
171
|
+
shiftBypass = false;
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
156
174
|
const pos = pixelToCell(e);
|
|
157
175
|
if (!pos) {
|
|
158
176
|
return;
|
|
@@ -169,6 +187,9 @@ function onMouseMove(e: MouseEvent): void {
|
|
|
169
187
|
if (mode === 0 || !(isMouseSGR() || isMousePixels()) || !handler) {
|
|
170
188
|
return;
|
|
171
189
|
}
|
|
190
|
+
if (shiftBypass && e.buttons) {
|
|
191
|
+
return; // a Shift-initiated drag is a native selection; don't report it
|
|
192
|
+
}
|
|
172
193
|
// mode 1000: no motion events
|
|
173
194
|
// mode 1002: motion only while button held (drag)
|
|
174
195
|
// mode 1003: all motion events
|
package/src/scroll.ts
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
//
|
|
3
3
|
// One piece of state: `following`. The user is "following" when the viewport is
|
|
4
4
|
// at (or within a small tolerance of) the bottom; otherwise they have scrolled
|
|
5
|
-
// up to read and are "holding". The state is derived purely from scroll
|
|
6
|
-
// position
|
|
7
|
-
// timer, and no programmatic-vs-user flag. That heuristic soup (a
|
|
8
|
-
// tolerance, a 150ms debounce, a 60-second touch window) was the source
|
|
9
|
-
// view-jumping and scroll-interruption bugs (the legacy heuristic-soup
|
|
5
|
+
// up to read and are "holding". The state is derived purely from the scroll
|
|
6
|
+
// events — position plus movement direction — with no debounce window, no
|
|
7
|
+
// suppress timer, and no programmatic-vs-user flag. That heuristic soup (a
|
|
8
|
+
// 100px tolerance, a 150ms debounce, a 60-second touch window) was the source
|
|
9
|
+
// of the view-jumping and scroll-interruption bugs (the legacy heuristic-soup
|
|
10
10
|
// failure mode; see the #web-terminal-engine steering doc, "Design rationale").
|
|
11
11
|
//
|
|
12
12
|
// The renderer calls stickToBottom() once after each flush: if following, pin
|
|
@@ -15,13 +15,32 @@
|
|
|
15
15
|
// Appending content at the bottom does not fire a scroll event, so following
|
|
16
16
|
// stays true across new output and the post-flush pin lands correctly. Pinning
|
|
17
17
|
// to the bottom produces a scroll event whose recomputation yields
|
|
18
|
-
// following=true again (no churn
|
|
19
|
-
//
|
|
18
|
+
// following=true again (no churn; the pin only ever moves DOWN). Scrolling
|
|
19
|
+
// back to within the tolerance of the bottom re-engages following.
|
|
20
|
+
//
|
|
21
|
+
// Disengaging is direction-based, not tolerance-based: ANY upward movement
|
|
22
|
+
// that leaves a real gap below flips to holding at once. A tolerance-only
|
|
23
|
+
// rule lost a race under heavy streaming — the renderer flushes (and pins)
|
|
24
|
+
// every frame, so each frame's upward scroll increment restarted from the
|
|
25
|
+
// bottom the previous pin reset it to, and unless a single frame's delta
|
|
26
|
+
// exceeded the tolerance the user was yanked back down every few milliseconds
|
|
27
|
+
// (the "scroll up fights me during output" bug). Per the HTML event loop the
|
|
28
|
+
// scroll steps run before that frame's rAF flush, so the first upward tick
|
|
29
|
+
// flips holding before the next pin can fire. The nothing-below guard keeps
|
|
30
|
+
// one non-user case following: when content SHRINKS (top-row eviction, a
|
|
31
|
+
// clear), the browser clamps scrollTop DOWN to the new maximum — an upward
|
|
32
|
+
// move that lands exactly at the bottom, which must not break auto-follow.
|
|
20
33
|
|
|
21
34
|
const BOTTOM_TOLERANCE_PX = 24;
|
|
35
|
+
// An upward move only disengages follow when a real gap is left below it.
|
|
36
|
+
// Bigger than 0 to absorb fractional-layout rounding in the shrink-clamp case
|
|
37
|
+
// (a clamp lands at the bottom, but scrollTop can be subpixel-off); far below
|
|
38
|
+
// any real one-frame user scroll increment.
|
|
39
|
+
const CLAMP_EPSILON_PX = 1;
|
|
22
40
|
|
|
23
41
|
let scrollEl: HTMLElement | null = null;
|
|
24
42
|
let following = true;
|
|
43
|
+
let lastScrollTop = 0;
|
|
25
44
|
let onFollowChange: ((scrolledUp: boolean) => void) | null = null;
|
|
26
45
|
let scrollHandler: (() => void) | null = null;
|
|
27
46
|
|
|
@@ -65,8 +84,24 @@ export function init(opts: {
|
|
|
65
84
|
scrollEl = opts.scrollEl;
|
|
66
85
|
onFollowChange = opts.onUserScrollChange ?? null;
|
|
67
86
|
following = true;
|
|
87
|
+
lastScrollTop = scrollEl.scrollTop;
|
|
68
88
|
scrollHandler = () => {
|
|
69
|
-
|
|
89
|
+
if (!scrollEl) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const top = scrollEl.scrollTop;
|
|
93
|
+
const movedUp = top < lastScrollTop;
|
|
94
|
+
lastScrollTop = top;
|
|
95
|
+
// Upward movement with content still below = the user pulling away from
|
|
96
|
+
// the live tail: hold immediately, however small the move (see the header
|
|
97
|
+
// comment for why the tolerance alone lost the streaming race). An upward
|
|
98
|
+
// move that lands AT the bottom is the browser clamping after a content
|
|
99
|
+
// shrink, not the user; everything else derives from position as before.
|
|
100
|
+
if (movedUp && distanceFromBottom() > CLAMP_EPSILON_PX) {
|
|
101
|
+
setFollowing(false);
|
|
102
|
+
} else {
|
|
103
|
+
setFollowing(atBottom());
|
|
104
|
+
}
|
|
70
105
|
};
|
|
71
106
|
scrollEl.addEventListener("scroll", scrollHandler, { passive: true });
|
|
72
107
|
}
|