mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-26 03:03:04 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96020ab773 |
@@ -50,9 +50,9 @@ import {
|
|||||||
import {
|
import {
|
||||||
TERMINAL_GESTURE_LONG_PRESS_MS,
|
TERMINAL_GESTURE_LONG_PRESS_MS,
|
||||||
TERMINAL_GESTURE_TAP_TOLERANCE_PX,
|
TERMINAL_GESTURE_TAP_TOLERANCE_PX,
|
||||||
advanceTerminalScrollGesture,
|
|
||||||
classifyTerminalGestureIntent,
|
classifyTerminalGestureIntent,
|
||||||
resolveTerminalGestureReleaseAction,
|
routeTerminalGestureRelease,
|
||||||
|
routeTerminalScrollGesture,
|
||||||
type TerminalGestureIntent,
|
type TerminalGestureIntent,
|
||||||
} from "../terminal/native-renderer/terminal-selection-gesture";
|
} from "../terminal/native-renderer/terminal-selection-gesture";
|
||||||
import { resolveNativeTerminalMountAction } from "../terminal/native-renderer/terminal-mount-policy";
|
import { resolveNativeTerminalMountAction } from "../terminal/native-renderer/terminal-mount-policy";
|
||||||
@@ -95,6 +95,7 @@ interface NativeTerminalPanState {
|
|||||||
didNavigate: boolean;
|
didNavigate: boolean;
|
||||||
movedBeyondTapTolerance: boolean;
|
movedBeyondTapTolerance: boolean;
|
||||||
intent: TerminalGestureIntent | null;
|
intent: TerminalGestureIntent | null;
|
||||||
|
origin: { x: number; y: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NativeTerminalSelectionPress {
|
interface NativeTerminalSelectionPress {
|
||||||
@@ -214,6 +215,7 @@ function NativeTerminalEmulator({
|
|||||||
didNavigate: false,
|
didNavigate: false,
|
||||||
movedBeyondTapTolerance: false,
|
movedBeyondTapTolerance: false,
|
||||||
intent: null,
|
intent: null,
|
||||||
|
origin: { x: 0, y: 0 },
|
||||||
});
|
});
|
||||||
const inputModeTrackerRef = useRef(new TerminalInputModeTracker());
|
const inputModeTrackerRef = useRef(new TerminalInputModeTracker());
|
||||||
const lastInputModeStateRef = useRef<TerminalInputModeState>({
|
const lastInputModeStateRef = useRef<TerminalInputModeState>({
|
||||||
@@ -775,31 +777,33 @@ function NativeTerminalEmulator({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handlePanResponderScroll = useCallback(
|
const handlePanResponderScroll = useCallback(
|
||||||
(currentDy: number) => {
|
(gesture: PanResponderGestureState) => {
|
||||||
const cellHeight = metricsRef.current?.cellHeight;
|
const metrics = metricsRef.current;
|
||||||
if (!cellHeight || cellHeight <= 0) {
|
const viewport = resolveSelectionViewport();
|
||||||
|
if (!metrics || metrics.cellHeight <= 0 || !viewport) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const panState = panStateRef.current;
|
const panState = panStateRef.current;
|
||||||
const update = advanceTerminalScrollGesture({
|
const { state, route } = routeTerminalScrollGesture({
|
||||||
state: panState,
|
state: panState,
|
||||||
currentDy,
|
currentDy: gesture.dy,
|
||||||
cellHeight,
|
point: { x: panState.origin.x + gesture.dx, y: panState.origin.y + gesture.dy },
|
||||||
|
metrics,
|
||||||
|
viewport,
|
||||||
|
inputMode: inputModeTrackerRef.current,
|
||||||
});
|
});
|
||||||
panState.lastDy = update.state.lastDy;
|
panState.lastDy = state.lastDy;
|
||||||
panState.rowRemainder = update.state.rowRemainder;
|
panState.rowRemainder = state.rowRemainder;
|
||||||
panState.didScroll = update.state.didScroll;
|
panState.didScroll = state.didScroll;
|
||||||
const { rowDelta } = update;
|
if (route.kind === "scrollback") {
|
||||||
if (rowDelta === 0) {
|
applyScroll(route.direction, route.rows);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (rowDelta > 0) {
|
if (route.kind === "input") {
|
||||||
applyScroll("up", rowDelta);
|
callbacksRef.current.onInput?.(route.data);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
applyScroll("down", Math.abs(rowDelta));
|
|
||||||
},
|
},
|
||||||
[applyScroll],
|
[applyScroll, resolveSelectionViewport],
|
||||||
);
|
);
|
||||||
|
|
||||||
const panResponder = useMemo(
|
const panResponder = useMemo(
|
||||||
@@ -828,6 +832,10 @@ function NativeTerminalEmulator({
|
|||||||
return intent === "scroll" || intent === "select";
|
return intent === "scroll" || intent === "select";
|
||||||
},
|
},
|
||||||
onPanResponderGrant: (event) => {
|
onPanResponderGrant: (event) => {
|
||||||
|
const point = {
|
||||||
|
x: event.nativeEvent.locationX,
|
||||||
|
y: event.nativeEvent.locationY,
|
||||||
|
};
|
||||||
panStateRef.current = {
|
panStateRef.current = {
|
||||||
lastDy: 0,
|
lastDy: 0,
|
||||||
rowRemainder: 0,
|
rowRemainder: 0,
|
||||||
@@ -835,10 +843,7 @@ function NativeTerminalEmulator({
|
|||||||
didNavigate: false,
|
didNavigate: false,
|
||||||
movedBeyondTapTolerance: false,
|
movedBeyondTapTolerance: false,
|
||||||
intent: null,
|
intent: null,
|
||||||
};
|
origin: point,
|
||||||
const point = {
|
|
||||||
x: event.nativeEvent.locationX,
|
|
||||||
y: event.nativeEvent.locationY,
|
|
||||||
};
|
};
|
||||||
const pressStatus = selectionRangeRef.current ? "selecting" : "pressing";
|
const pressStatus = selectionRangeRef.current ? "selecting" : "pressing";
|
||||||
selectionPressRef.current = {
|
selectionPressRef.current = {
|
||||||
@@ -875,7 +880,7 @@ function NativeTerminalEmulator({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (panState.intent === "scroll") {
|
if (panState.intent === "scroll") {
|
||||||
handlePanResponderScroll(gesture.dy);
|
handlePanResponderScroll(gesture);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (panState.didNavigate) {
|
if (panState.didNavigate) {
|
||||||
@@ -903,7 +908,7 @@ function NativeTerminalEmulator({
|
|||||||
|
|
||||||
if (intent === "scroll") {
|
if (intent === "scroll") {
|
||||||
selectionPressRef.current = null;
|
selectionPressRef.current = null;
|
||||||
handlePanResponderScroll(gesture.dy);
|
handlePanResponderScroll(gesture);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -932,8 +937,9 @@ function NativeTerminalEmulator({
|
|||||||
didNavigate: false,
|
didNavigate: false,
|
||||||
movedBeyondTapTolerance: false,
|
movedBeyondTapTolerance: false,
|
||||||
intent: null,
|
intent: null,
|
||||||
|
origin: { x: 0, y: 0 },
|
||||||
};
|
};
|
||||||
const releaseAction = resolveTerminalGestureReleaseAction({
|
const { action: releaseAction, mouseInput } = routeTerminalGestureRelease({
|
||||||
status: selectionPress?.status ?? "idle",
|
status: selectionPress?.status ?? "idle",
|
||||||
startedWithSelection: selectionPress?.startedWithSelection,
|
startedWithSelection: selectionPress?.startedWithSelection,
|
||||||
didScroll,
|
didScroll,
|
||||||
@@ -942,11 +948,18 @@ function NativeTerminalEmulator({
|
|||||||
pressDurationMs: selectionPress ? Date.now() - selectionPress.startedAt : 0,
|
pressDurationMs: selectionPress ? Date.now() - selectionPress.startedAt : 0,
|
||||||
longPressMs: TERMINAL_GESTURE_LONG_PRESS_MS,
|
longPressMs: TERMINAL_GESTURE_LONG_PRESS_MS,
|
||||||
scrollMode: terminalScreenRef.current?.scroll.mode ?? "following",
|
scrollMode: terminalScreenRef.current?.scroll.mode ?? "following",
|
||||||
|
point: { x: selectionPress?.startX ?? 0, y: selectionPress?.startY ?? 0 },
|
||||||
|
metrics: metricsRef.current,
|
||||||
|
viewport: resolveSelectionViewport(),
|
||||||
|
inputMode: inputModeTrackerRef.current,
|
||||||
});
|
});
|
||||||
if (releaseAction === "select" && selectionPress) {
|
if (releaseAction === "select" && selectionPress) {
|
||||||
beginSelectionAtPoint({ x: selectionPress.startX, y: selectionPress.startY });
|
beginSelectionAtPoint({ x: selectionPress.startX, y: selectionPress.startY });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (mouseInput !== null) {
|
||||||
|
callbacksRef.current.onInput?.(mouseInput);
|
||||||
|
}
|
||||||
if (releaseAction === "focus" || releaseAction === "clear") {
|
if (releaseAction === "focus" || releaseAction === "clear") {
|
||||||
focusTerminalFromTap();
|
focusTerminalFromTap();
|
||||||
}
|
}
|
||||||
@@ -961,6 +974,7 @@ function NativeTerminalEmulator({
|
|||||||
didNavigate: false,
|
didNavigate: false,
|
||||||
movedBeyondTapTolerance: false,
|
movedBeyondTapTolerance: false,
|
||||||
intent: null,
|
intent: null,
|
||||||
|
origin: { x: 0, y: 0 },
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -969,6 +983,7 @@ function NativeTerminalEmulator({
|
|||||||
clearSelectionLongPressTimeout,
|
clearSelectionLongPressTimeout,
|
||||||
focusTerminalFromTap,
|
focusTerminalFromTap,
|
||||||
handlePanResponderScroll,
|
handlePanResponderScroll,
|
||||||
|
resolveSelectionViewport,
|
||||||
updateSelectionAtPoint,
|
updateSelectionAtPoint,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { TerminalInputModeTracker } from "@getpaseo/protocol/terminal-input-mode";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
TERMINAL_GESTURE_LONG_PRESS_MS,
|
TERMINAL_GESTURE_LONG_PRESS_MS,
|
||||||
@@ -7,8 +8,64 @@ import {
|
|||||||
advanceTerminalScrollGesture,
|
advanceTerminalScrollGesture,
|
||||||
classifyTerminalGestureIntent,
|
classifyTerminalGestureIntent,
|
||||||
resolveTerminalGestureReleaseAction,
|
resolveTerminalGestureReleaseAction,
|
||||||
|
routeTerminalGestureRelease,
|
||||||
|
routeTerminalScrollGesture,
|
||||||
|
type TerminalGestureReleaseRouteInput,
|
||||||
|
type TerminalScrollGestureRoute,
|
||||||
} from "./terminal-selection-gesture";
|
} from "./terminal-selection-gesture";
|
||||||
|
|
||||||
|
const PI_FULLSCREEN_MOUSE_ENABLE =
|
||||||
|
"\x1b[?1049h\x1b[?1000h\x1b[?1002h\x1b[?1003h\x1b[?1004h\x1b[?1006h";
|
||||||
|
const PI_FULLSCREEN_MOUSE_DISABLE = "\x1b[?1006l\x1b[?1004l\x1b[?1003l\x1b[?1002l\x1b[?1000l";
|
||||||
|
|
||||||
|
function trackerFromOutput(output: string): TerminalInputModeTracker {
|
||||||
|
const tracker = new TerminalInputModeTracker();
|
||||||
|
tracker.feed(output);
|
||||||
|
return tracker;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragRoutes(input: {
|
||||||
|
inputMode: TerminalInputModeTracker;
|
||||||
|
dys: number[];
|
||||||
|
point?: { x: number; y: number };
|
||||||
|
}): TerminalScrollGestureRoute[] {
|
||||||
|
let state = { lastDy: 0, rowRemainder: 0, didScroll: false };
|
||||||
|
const routes: TerminalScrollGestureRoute[] = [];
|
||||||
|
for (const currentDy of input.dys) {
|
||||||
|
const update = routeTerminalScrollGesture({
|
||||||
|
state,
|
||||||
|
currentDy,
|
||||||
|
point: input.point ?? { x: 25, y: 50 },
|
||||||
|
metrics: { cellWidth: 8, cellHeight: 14 },
|
||||||
|
viewport: { rows: 30, cols: 40 },
|
||||||
|
inputMode: input.inputMode,
|
||||||
|
});
|
||||||
|
state = update.state;
|
||||||
|
routes.push(update.route);
|
||||||
|
}
|
||||||
|
return routes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tapRelease(
|
||||||
|
overrides: Partial<TerminalGestureReleaseRouteInput> = {},
|
||||||
|
): ReturnType<typeof routeTerminalGestureRelease> {
|
||||||
|
return routeTerminalGestureRelease({
|
||||||
|
status: "pressing",
|
||||||
|
startedWithSelection: false,
|
||||||
|
didScroll: false,
|
||||||
|
didNavigate: false,
|
||||||
|
movedBeyondTapTolerance: false,
|
||||||
|
pressDurationMs: 80,
|
||||||
|
longPressMs: TERMINAL_GESTURE_LONG_PRESS_MS,
|
||||||
|
scrollMode: "following",
|
||||||
|
point: { x: 25, y: 50 },
|
||||||
|
metrics: { cellWidth: 8, cellHeight: 14 },
|
||||||
|
viewport: { rows: 30, cols: 40 },
|
||||||
|
inputMode: trackerFromOutput(PI_FULLSCREEN_MOUSE_ENABLE),
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
describe("native terminal gesture policy", () => {
|
describe("native terminal gesture policy", () => {
|
||||||
describe("advanceTerminalScrollGesture", () => {
|
describe("advanceTerminalScrollGesture", () => {
|
||||||
it("keeps consuming a slow drag after scroll intent arms below one row", () => {
|
it("keeps consuming a slow drag after scroll intent arms below one row", () => {
|
||||||
@@ -351,4 +408,143 @@ describe("native terminal gesture policy", () => {
|
|||||||
).toEqual("none");
|
).toEqual("none");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe("routeTerminalScrollGesture", () => {
|
||||||
|
it("sends SGR wheel reports at the touched cell while a fullscreen app tracks the mouse", () => {
|
||||||
|
const inputMode = trackerFromOutput(PI_FULLSCREEN_MOUSE_ENABLE);
|
||||||
|
|
||||||
|
expect(dragRoutes({ inputMode, dys: [13, 28, 42, 14] })).toEqual([
|
||||||
|
{ kind: "none" },
|
||||||
|
{ kind: "input", data: "\x1b[<64;4;4M\x1b[<64;4;4M" },
|
||||||
|
{ kind: "input", data: "\x1b[<64;4;4M" },
|
||||||
|
{ kind: "input", data: "\x1b[<65;4;4M\x1b[<65;4;4M" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps ordinary shell drags in local scrollback", () => {
|
||||||
|
const inputMode = trackerFromOutput("\x1b[?2004h$ ls\r\n");
|
||||||
|
|
||||||
|
expect(dragRoutes({ inputMode, dys: [13, 28, 14] })).toEqual([
|
||||||
|
{ kind: "none" },
|
||||||
|
{ kind: "scrollback", direction: "up", rows: 2 },
|
||||||
|
{ kind: "scrollback", direction: "down", rows: 1 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns drags to local scrollback after the app disables mouse tracking", () => {
|
||||||
|
const inputMode = trackerFromOutput(PI_FULLSCREEN_MOUSE_ENABLE);
|
||||||
|
inputMode.feed(PI_FULLSCREEN_MOUSE_DISABLE);
|
||||||
|
|
||||||
|
expect(dragRoutes({ inputMode, dys: [28] })).toEqual([
|
||||||
|
{ kind: "scrollback", direction: "up", rows: 2 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps forwarding wheel input after a reconnect replays the daemon preamble", () => {
|
||||||
|
const daemon = trackerFromOutput(PI_FULLSCREEN_MOUSE_ENABLE);
|
||||||
|
const client = trackerFromOutput(PI_FULLSCREEN_MOUSE_ENABLE);
|
||||||
|
client.reset();
|
||||||
|
client.feed(daemon.getPreamble());
|
||||||
|
|
||||||
|
expect(dragRoutes({ inputMode: client, dys: [14] })).toEqual([
|
||||||
|
{ kind: "input", data: "\x1b[<64;4;4M" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps pixel reports integral at fractional cell boundaries", () => {
|
||||||
|
const inputMode = trackerFromOutput("\x1b[?1003h\x1b[?1016h");
|
||||||
|
const { route } = routeTerminalScrollGesture({
|
||||||
|
state: { lastDy: 0, rowRemainder: 0, didScroll: false },
|
||||||
|
currentDy: 15,
|
||||||
|
point: { x: 999, y: 999 },
|
||||||
|
metrics: { cellWidth: 8.25, cellHeight: 14.25 },
|
||||||
|
viewport: { rows: 3, cols: 3 },
|
||||||
|
inputMode,
|
||||||
|
});
|
||||||
|
expect(route).toEqual({ kind: "input", data: "\x1b[<64;25;43M" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clamps wheel reports to the visible grid", () => {
|
||||||
|
const inputMode = trackerFromOutput(PI_FULLSCREEN_MOUSE_ENABLE);
|
||||||
|
|
||||||
|
expect(dragRoutes({ inputMode, dys: [-14], point: { x: 999, y: -5 } })).toEqual([
|
||||||
|
{ kind: "input", data: "\x1b[<65;40;1M" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("routeTerminalGestureRelease", () => {
|
||||||
|
it("clicks the tapped cell and still focuses while a fullscreen app tracks the mouse", () => {
|
||||||
|
expect(tapRelease()).toEqual({
|
||||||
|
action: "focus",
|
||||||
|
mouseInput: "\x1b[<0;4;4M\x1b[<0;4;4m",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("only focuses on an ordinary shell tap", () => {
|
||||||
|
expect(tapRelease({ inputMode: trackerFromOutput("\x1b[?2004h$ ls\r\n") })).toEqual({
|
||||||
|
action: "focus",
|
||||||
|
mouseInput: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends only the press for X10 tracking", () => {
|
||||||
|
expect(tapRelease({ inputMode: trackerFromOutput("\x1b[?9h") })).toEqual({
|
||||||
|
action: "focus",
|
||||||
|
mouseInput: "\x1b[M $$",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps a long press as local selection without clicking", () => {
|
||||||
|
expect(tapRelease({ pressDurationMs: TERMINAL_GESTURE_LONG_PRESS_MS + 50 })).toEqual({
|
||||||
|
action: "select",
|
||||||
|
mouseInput: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["moved beyond tap tolerance", { movedBeyondTapTolerance: true }],
|
||||||
|
["scrolled", { didScroll: true }],
|
||||||
|
])("never clicks after a drag that %s", (_name, overrides) => {
|
||||||
|
expect(tapRelease(overrides)).toEqual({ action: "none", mouseInput: null });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clears an existing selection without clicking", () => {
|
||||||
|
expect(tapRelease({ status: "selecting", startedWithSelection: true })).toEqual({
|
||||||
|
action: "clear",
|
||||||
|
mouseInput: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not send live mouse input from a scrolled-back viewport", () => {
|
||||||
|
expect(tapRelease({ scrollMode: "scrolled" })).toEqual({
|
||||||
|
action: "focus",
|
||||||
|
mouseInput: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not click before the grid is measured", () => {
|
||||||
|
expect(tapRelease({ metrics: null, viewport: null })).toEqual({
|
||||||
|
action: "focus",
|
||||||
|
mouseInput: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clamps clicks to the visible grid", () => {
|
||||||
|
expect(tapRelease({ point: { x: 999, y: -5 } })).toEqual({
|
||||||
|
action: "focus",
|
||||||
|
mouseInput: "\x1b[<0;40;1M\x1b[<0;40;1m",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps pixel clicks integral at fractional cell boundaries", () => {
|
||||||
|
expect(
|
||||||
|
tapRelease({
|
||||||
|
inputMode: trackerFromOutput("\x1b[?1000h\x1b[?1016h"),
|
||||||
|
point: { x: 999, y: 999 },
|
||||||
|
metrics: { cellWidth: 8.25, cellHeight: 14.25 },
|
||||||
|
viewport: { rows: 3, cols: 3 },
|
||||||
|
}),
|
||||||
|
).toEqual({ action: "focus", mouseInput: "\x1b[<0;25;43M\x1b[<0;25;43m" });
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
import type {
|
||||||
|
TerminalInputModeTracker,
|
||||||
|
TerminalMousePosition,
|
||||||
|
} from "@getpaseo/protocol/terminal-input-mode";
|
||||||
|
import type { TerminalGridCellMetrics } from "./terminal-grid-metrics";
|
||||||
|
|
||||||
export type TerminalGestureStatus = "idle" | "pressing" | "selecting";
|
export type TerminalGestureStatus = "idle" | "pressing" | "selecting";
|
||||||
export type TerminalGestureIntent = "tap" | "pending" | "select" | "scroll";
|
export type TerminalGestureIntent = "tap" | "pending" | "select" | "scroll";
|
||||||
export type TerminalGestureScrollMode = "following" | "scrolled";
|
export type TerminalGestureScrollMode = "following" | "scrolled";
|
||||||
@@ -33,8 +39,39 @@ export interface TerminalScrollGestureUpdate {
|
|||||||
rowDelta: number;
|
rowDelta: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TerminalScrollGestureRoute =
|
||||||
|
| { kind: "none" }
|
||||||
|
| { kind: "scrollback"; direction: "up" | "down"; rows: number }
|
||||||
|
| { kind: "input"; data: string };
|
||||||
|
|
||||||
|
export interface TerminalScrollGestureRouteInput {
|
||||||
|
state: TerminalScrollGestureState;
|
||||||
|
currentDy: number;
|
||||||
|
point: { x: number; y: number };
|
||||||
|
metrics: TerminalGridCellMetrics;
|
||||||
|
viewport: { rows: number; cols: number };
|
||||||
|
inputMode: Pick<TerminalInputModeTracker, "encodeWheel">;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TerminalScrollGestureRouteUpdate {
|
||||||
|
state: TerminalScrollGestureState;
|
||||||
|
route: TerminalScrollGestureRoute;
|
||||||
|
}
|
||||||
|
|
||||||
export type TerminalGestureAction = "none" | "focus" | "select" | "clear";
|
export type TerminalGestureAction = "none" | "focus" | "select" | "clear";
|
||||||
|
|
||||||
|
export interface TerminalGestureReleaseRouteInput extends TerminalGestureReleaseInput {
|
||||||
|
point: { x: number; y: number };
|
||||||
|
metrics: TerminalGridCellMetrics | null;
|
||||||
|
viewport: { rows: number; cols: number } | null;
|
||||||
|
inputMode: Pick<TerminalInputModeTracker, "encodeLeftClick">;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TerminalGestureReleaseRoute {
|
||||||
|
action: TerminalGestureAction;
|
||||||
|
mouseInput: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export const TERMINAL_GESTURE_TAP_TOLERANCE_PX = 8;
|
export const TERMINAL_GESTURE_TAP_TOLERANCE_PX = 8;
|
||||||
export const TERMINAL_GESTURE_LONG_PRESS_MS = 450;
|
export const TERMINAL_GESTURE_LONG_PRESS_MS = 450;
|
||||||
export const TERMINAL_GESTURE_VERTICAL_SCROLL_THRESHOLD_PX = 12;
|
export const TERMINAL_GESTURE_VERTICAL_SCROLL_THRESHOLD_PX = 12;
|
||||||
@@ -78,6 +115,60 @@ export function advanceTerminalScrollGesture(input: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clamp(value: number, min: number, max: number): number {
|
||||||
|
return Math.min(max, Math.max(min, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Clamps a touch point to the visible grid; pixel reports stay integral at fractional cell sizes. */
|
||||||
|
function resolveTerminalMousePosition(input: {
|
||||||
|
point: { x: number; y: number };
|
||||||
|
metrics: TerminalGridCellMetrics;
|
||||||
|
viewport: { rows: number; cols: number };
|
||||||
|
}): TerminalMousePosition {
|
||||||
|
const { cellWidth, cellHeight } = input.metrics;
|
||||||
|
const pixelX = clamp(
|
||||||
|
Math.floor(input.point.x),
|
||||||
|
0,
|
||||||
|
Math.ceil(input.viewport.cols * cellWidth) - 1,
|
||||||
|
);
|
||||||
|
const pixelY = clamp(
|
||||||
|
Math.floor(input.point.y),
|
||||||
|
0,
|
||||||
|
Math.ceil(input.viewport.rows * cellHeight) - 1,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
col: Math.floor(pixelX / cellWidth) + 1,
|
||||||
|
row: Math.floor(pixelY / cellHeight) + 1,
|
||||||
|
pixelX: pixelX + 1,
|
||||||
|
pixelY: pixelY + 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A drag scrolls local scrollback unless the foreground app asked for wheel reports. */
|
||||||
|
export function routeTerminalScrollGesture(
|
||||||
|
input: TerminalScrollGestureRouteInput,
|
||||||
|
): TerminalScrollGestureRouteUpdate {
|
||||||
|
const { state, rowDelta } = advanceTerminalScrollGesture({
|
||||||
|
state: input.state,
|
||||||
|
currentDy: input.currentDy,
|
||||||
|
cellHeight: input.metrics.cellHeight,
|
||||||
|
});
|
||||||
|
if (rowDelta === 0) {
|
||||||
|
return { state, route: { kind: "none" } };
|
||||||
|
}
|
||||||
|
|
||||||
|
const direction = rowDelta > 0 ? "up" : "down";
|
||||||
|
const rows = Math.abs(rowDelta);
|
||||||
|
const report = input.inputMode.encodeWheel({
|
||||||
|
direction,
|
||||||
|
...resolveTerminalMousePosition(input),
|
||||||
|
});
|
||||||
|
if (report === null) {
|
||||||
|
return { state, route: { kind: "scrollback", direction, rows } };
|
||||||
|
}
|
||||||
|
return { state, route: { kind: "input", data: report.repeat(rows) } };
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveTerminalGestureReleaseAction(
|
export function resolveTerminalGestureReleaseAction(
|
||||||
input: TerminalGestureReleaseInput,
|
input: TerminalGestureReleaseInput,
|
||||||
): TerminalGestureAction {
|
): TerminalGestureAction {
|
||||||
@@ -106,3 +197,19 @@ export function resolveTerminalGestureReleaseAction(
|
|||||||
|
|
||||||
return "focus";
|
return "focus";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only a short tap on the live screen clicks; long press, drags and selection clears stay local.
|
||||||
|
* The caller sends the click before focusing, so the keyboard resize cannot move the target cell.
|
||||||
|
*/
|
||||||
|
export function routeTerminalGestureRelease(
|
||||||
|
input: TerminalGestureReleaseRouteInput,
|
||||||
|
): TerminalGestureReleaseRoute {
|
||||||
|
const action = resolveTerminalGestureReleaseAction(input);
|
||||||
|
const { metrics, viewport } = input;
|
||||||
|
if (action !== "focus" || input.scrollMode !== "following" || !metrics || !viewport) {
|
||||||
|
return { action, mouseInput: null };
|
||||||
|
}
|
||||||
|
const position = resolveTerminalMousePosition({ point: input.point, metrics, viewport });
|
||||||
|
return { action, mouseInput: input.inputMode.encodeLeftClick(position) };
|
||||||
|
}
|
||||||
|
|||||||
@@ -118,6 +118,111 @@ describe("TerminalInputModeTracker", () => {
|
|||||||
expect(tracker.getPreamble()).toBe("");
|
expect(tracker.getPreamble()).toBe("");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const PI_FULLSCREEN_MOUSE_ENABLE =
|
||||||
|
"\x1b[?1049h\x1b[?1000h\x1b[?1002h\x1b[?1003h\x1b[?1004h\x1b[?1006h";
|
||||||
|
const PI_FULLSCREEN_MOUSE_DISABLE = "\x1b[?1006l\x1b[?1004l\x1b[?1003l\x1b[?1002l\x1b[?1000l";
|
||||||
|
const wheelUpAt3x4 = { direction: "up" as const, col: 3, row: 4, pixelX: 30, pixelY: 80 };
|
||||||
|
|
||||||
|
it("encodes SGR wheel reports while a fullscreen app tracks the mouse", () => {
|
||||||
|
const tracker = new TerminalInputModeTracker();
|
||||||
|
expect(tracker.encodeWheel(wheelUpAt3x4)).toBeNull();
|
||||||
|
|
||||||
|
expect(tracker.feed(PI_FULLSCREEN_MOUSE_ENABLE).changed).toBe(true);
|
||||||
|
|
||||||
|
expect(tracker.encodeWheel(wheelUpAt3x4)).toBe("\x1b[<64;3;4M");
|
||||||
|
expect(tracker.encodeWheel({ ...wheelUpAt3x4, direction: "down" })).toBe("\x1b[<65;3;4M");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stops encoding wheel reports once the app disables mouse tracking", () => {
|
||||||
|
const tracker = new TerminalInputModeTracker();
|
||||||
|
tracker.feed(PI_FULLSCREEN_MOUSE_ENABLE);
|
||||||
|
|
||||||
|
expect(tracker.feed(PI_FULLSCREEN_MOUSE_DISABLE).changed).toBe(true);
|
||||||
|
|
||||||
|
expect(tracker.encodeWheel(wheelUpAt3x4)).toBeNull();
|
||||||
|
expect(tracker.getPreamble()).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("replays mouse tracking so a reconnected client keeps forwarding wheel input", () => {
|
||||||
|
const daemon = new TerminalInputModeTracker();
|
||||||
|
daemon.feed(PI_FULLSCREEN_MOUSE_ENABLE);
|
||||||
|
const client = new TerminalInputModeTracker();
|
||||||
|
client.feed(PI_FULLSCREEN_MOUSE_ENABLE);
|
||||||
|
|
||||||
|
client.reset();
|
||||||
|
expect(client.encodeWheel(wheelUpAt3x4)).toBeNull();
|
||||||
|
expect(daemon.getPreamble()).toBe("\x1b[?1003h\x1b[?1006h");
|
||||||
|
client.feed(daemon.getPreamble());
|
||||||
|
|
||||||
|
expect(client.encodeWheel(wheelUpAt3x4)).toBe("\x1b[<64;3;4M");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("encodes SGR pixel wheel reports with pixel coordinates", () => {
|
||||||
|
const tracker = new TerminalInputModeTracker();
|
||||||
|
tracker.feed("\x1b[?1000h\x1b[?1016h");
|
||||||
|
|
||||||
|
expect(tracker.encodeWheel(wheelUpAt3x4)).toBe("\x1b[<64;30;80M");
|
||||||
|
expect(tracker.getPreamble()).toBe("\x1b[?1000h\x1b[?1016h");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("encodes default wheel reports only while every byte stays ASCII", () => {
|
||||||
|
const tracker = new TerminalInputModeTracker();
|
||||||
|
tracker.feed("\x1b[?1000h");
|
||||||
|
|
||||||
|
expect(tracker.encodeWheel(wheelUpAt3x4)).toBe("\x1b[M`#$");
|
||||||
|
expect(tracker.encodeWheel({ ...wheelUpAt3x4, col: 96 })).toBeNull();
|
||||||
|
expect(tracker.encodeWheel({ ...wheelUpAt3x4, row: 96 })).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not encode wheel reports for X10 press-only tracking", () => {
|
||||||
|
const tracker = new TerminalInputModeTracker();
|
||||||
|
tracker.feed("\x1b[?9h\x1b[?1006h");
|
||||||
|
|
||||||
|
expect(tracker.encodeWheel(wheelUpAt3x4)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
const tapAt3x4 = { col: 3, row: 4, pixelX: 30, pixelY: 80 };
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["no tracking", "", null],
|
||||||
|
["SGR press/release", PI_FULLSCREEN_MOUSE_ENABLE, "\x1b[<0;3;4M\x1b[<0;3;4m"],
|
||||||
|
["SGR pixel press/release", "\x1b[?1000h\x1b[?1016h", "\x1b[<0;30;80M\x1b[<0;30;80m"],
|
||||||
|
["default press and button-3 release", "\x1b[?1000h", "\x1b[M #$\x1b[M##$"],
|
||||||
|
["X10 default press only", "\x1b[?9h", "\x1b[M #$"],
|
||||||
|
["X10 SGR press only", "\x1b[?9h\x1b[?1006h", "\x1b[<0;3;4M"],
|
||||||
|
[
|
||||||
|
"tracking disabled again",
|
||||||
|
`${PI_FULLSCREEN_MOUSE_ENABLE}${PI_FULLSCREEN_MOUSE_DISABLE}`,
|
||||||
|
null,
|
||||||
|
],
|
||||||
|
])("encodes a left click with %s", (_name, output, expected) => {
|
||||||
|
const tracker = new TerminalInputModeTracker();
|
||||||
|
tracker.feed(output);
|
||||||
|
|
||||||
|
expect(tracker.encodeLeftClick(tapAt3x4)).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not encode a default left click that leaves ASCII", () => {
|
||||||
|
const tracker = new TerminalInputModeTracker();
|
||||||
|
tracker.feed("\x1b[?1000h");
|
||||||
|
|
||||||
|
expect(tracker.encodeLeftClick({ ...tapAt3x4, col: 96 })).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([["\x1bc"], ["\x1b", "c"]])(
|
||||||
|
"clears every tracked mode on a full reset split as %j",
|
||||||
|
(...chunks) => {
|
||||||
|
const tracker = new TerminalInputModeTracker();
|
||||||
|
tracker.feed(`\x1b[>7u\x1b[?2004h${PI_FULLSCREEN_MOUSE_ENABLE}`);
|
||||||
|
|
||||||
|
const results = chunks.map((chunk) => tracker.feed(chunk));
|
||||||
|
|
||||||
|
expect(results.at(-1)).toEqual({ changed: true, responses: [] });
|
||||||
|
expect(tracker.getPreamble()).toBe("");
|
||||||
|
expect(tracker.encodeWheel(wheelUpAt3x4)).toBeNull();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it("ignores encoded key input sequences", () => {
|
it("ignores encoded key input sequences", () => {
|
||||||
const tracker = new TerminalInputModeTracker();
|
const tracker = new TerminalInputModeTracker();
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,17 @@ export interface TerminalInputModeState {
|
|||||||
bracketedPaste?: boolean;
|
bracketedPaste?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TerminalMousePosition {
|
||||||
|
col: number;
|
||||||
|
row: number;
|
||||||
|
pixelX: number;
|
||||||
|
pixelY: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TerminalWheelInput extends TerminalMousePosition {
|
||||||
|
direction: "up" | "down";
|
||||||
|
}
|
||||||
|
|
||||||
export const DEFAULT_TERMINAL_INPUT_MODE_STATE: TerminalInputModeState = {
|
export const DEFAULT_TERMINAL_INPUT_MODE_STATE: TerminalInputModeState = {
|
||||||
kittyKeyboardFlags: 0,
|
kittyKeyboardFlags: 0,
|
||||||
win32InputMode: false,
|
win32InputMode: false,
|
||||||
@@ -21,8 +32,20 @@ const ESC = String.fromCharCode(0x1b);
|
|||||||
const APPLICATION_CURSOR_KEYS_MODE = 1;
|
const APPLICATION_CURSOR_KEYS_MODE = 1;
|
||||||
const WIN32_INPUT_MODE = 9001;
|
const WIN32_INPUT_MODE = 9001;
|
||||||
const BRACKETED_PASTE_MODE = 2004;
|
const BRACKETED_PASTE_MODE = 2004;
|
||||||
const CSI_INPUT_MODE_SEQUENCE = new RegExp(
|
const X10_MOUSE_TRACKING_MODE = 9;
|
||||||
`${ESC}\\[(?:([<>=?]?)([0-9;]*)u|\\?([0-9;]*)([hl]))`,
|
const MOUSE_TRACKING_MODES = new Set([X10_MOUSE_TRACKING_MODE, 1000, 1002, 1003]);
|
||||||
|
const SGR_MOUSE_ENCODING_MODE = 1006;
|
||||||
|
const SGR_PIXELS_MOUSE_ENCODING_MODE = 1016;
|
||||||
|
const MOUSE_ENCODING_MODES = new Set([SGR_MOUSE_ENCODING_MODE, SGR_PIXELS_MOUSE_ENCODING_MODE]);
|
||||||
|
const LEFT_BUTTON = 0;
|
||||||
|
const DEFAULT_RELEASE_BUTTON = 3;
|
||||||
|
const WHEEL_UP_BUTTON = 64;
|
||||||
|
const WHEEL_DOWN_BUTTON = 65;
|
||||||
|
const DEFAULT_MOUSE_ENCODING_OFFSET = 32;
|
||||||
|
const MAX_ASCII_CODE = 127;
|
||||||
|
const FULL_RESET = `${ESC}c`;
|
||||||
|
const INPUT_MODE_SEQUENCE = new RegExp(
|
||||||
|
`${ESC}(?:c|\\[(?:([<>=?]?)([0-9;]*)u|\\?([0-9;]*)([hl])))`,
|
||||||
"g",
|
"g",
|
||||||
);
|
);
|
||||||
const INCOMPLETE_CSI_INPUT_MODE_SEQUENCE = new RegExp(`${ESC}\\[[<>=?]?[0-9;]*$`);
|
const INCOMPLETE_CSI_INPUT_MODE_SEQUENCE = new RegExp(`${ESC}\\[[<>=?]?[0-9;]*$`);
|
||||||
@@ -74,6 +97,8 @@ export class TerminalInputModeTracker {
|
|||||||
private win32InputMode = false;
|
private win32InputMode = false;
|
||||||
private applicationCursorKeys = false;
|
private applicationCursorKeys = false;
|
||||||
private bracketedPaste = false;
|
private bracketedPaste = false;
|
||||||
|
private mouseTrackingMode = 0;
|
||||||
|
private mouseEncodingMode = 0;
|
||||||
private readonly kittyKeyboardStack: number[] = [];
|
private readonly kittyKeyboardStack: number[] = [];
|
||||||
private pending = "";
|
private pending = "";
|
||||||
|
|
||||||
@@ -89,13 +114,19 @@ export class TerminalInputModeTracker {
|
|||||||
const responses: string[] = [];
|
const responses: string[] = [];
|
||||||
let consumedUntil = 0;
|
let consumedUntil = 0;
|
||||||
|
|
||||||
CSI_INPUT_MODE_SEQUENCE.lastIndex = 0;
|
INPUT_MODE_SEQUENCE.lastIndex = 0;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
const match = CSI_INPUT_MODE_SEQUENCE.exec(text);
|
const match = INPUT_MODE_SEQUENCE.exec(text);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
consumedUntil = CSI_INPUT_MODE_SEQUENCE.lastIndex;
|
consumedUntil = INPUT_MODE_SEQUENCE.lastIndex;
|
||||||
|
|
||||||
|
if (match[0] === FULL_RESET) {
|
||||||
|
changed = this.getPreamble().length > 0 || changed;
|
||||||
|
this.reset();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (match[4]) {
|
if (match[4]) {
|
||||||
changed = this.applyPrivateModeSequence(match[3] ?? "", match[4]) || changed;
|
changed = this.applyPrivateModeSequence(match[3] ?? "", match[4]) || changed;
|
||||||
@@ -108,10 +139,10 @@ export class TerminalInputModeTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tail = text.slice(consumedUntil);
|
const tail = text.slice(consumedUntil);
|
||||||
const pendingStart = tail.lastIndexOf(`${ESC}[`);
|
const pendingStart = tail.lastIndexOf(ESC);
|
||||||
if (pendingStart >= 0) {
|
if (pendingStart >= 0) {
|
||||||
const pending = tail.slice(pendingStart);
|
const pending = tail.slice(pendingStart);
|
||||||
if (INCOMPLETE_CSI_INPUT_MODE_SEQUENCE.test(pending)) {
|
if (pending === ESC || INCOMPLETE_CSI_INPUT_MODE_SEQUENCE.test(pending)) {
|
||||||
this.pending = pending;
|
this.pending = pending;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,6 +155,8 @@ export class TerminalInputModeTracker {
|
|||||||
this.win32InputMode = false;
|
this.win32InputMode = false;
|
||||||
this.applicationCursorKeys = false;
|
this.applicationCursorKeys = false;
|
||||||
this.bracketedPaste = false;
|
this.bracketedPaste = false;
|
||||||
|
this.mouseTrackingMode = 0;
|
||||||
|
this.mouseEncodingMode = 0;
|
||||||
this.kittyKeyboardStack.length = 0;
|
this.kittyKeyboardStack.length = 0;
|
||||||
this.pending = "";
|
this.pending = "";
|
||||||
}
|
}
|
||||||
@@ -159,9 +192,64 @@ export class TerminalInputModeTracker {
|
|||||||
if (this.bracketedPaste) {
|
if (this.bracketedPaste) {
|
||||||
parts.push("\x1b[?2004h");
|
parts.push("\x1b[?2004h");
|
||||||
}
|
}
|
||||||
|
// The alternate screen is never replayed here: re-entering it would clear the restored screen.
|
||||||
|
if (this.mouseTrackingMode > 0) {
|
||||||
|
parts.push(`\x1b[?${this.mouseTrackingMode}h`);
|
||||||
|
}
|
||||||
|
if (this.mouseEncodingMode > 0) {
|
||||||
|
parts.push(`\x1b[?${this.mouseEncodingMode}h`);
|
||||||
|
}
|
||||||
return parts.join("");
|
return parts.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns the wheel report the foreground app requested, or null when it does not track the wheel. */
|
||||||
|
encodeWheel(input: TerminalWheelInput): string | null {
|
||||||
|
const isWheelReported =
|
||||||
|
this.mouseTrackingMode > 0 && this.mouseTrackingMode !== X10_MOUSE_TRACKING_MODE;
|
||||||
|
if (!isWheelReported) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const button = input.direction === "up" ? WHEEL_UP_BUTTON : WHEEL_DOWN_BUTTON;
|
||||||
|
return this.encodeMouseReport(button, input, "M");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a left press and release, press only under X10, or null when the app does not track the mouse. */
|
||||||
|
encodeLeftClick(position: TerminalMousePosition): string | null {
|
||||||
|
if (this.mouseTrackingMode === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const press = this.encodeMouseReport(LEFT_BUTTON, position, "M");
|
||||||
|
if (press === null || this.mouseTrackingMode === X10_MOUSE_TRACKING_MODE) {
|
||||||
|
return press;
|
||||||
|
}
|
||||||
|
const isSgr = this.mouseEncodingMode !== 0;
|
||||||
|
const release = isSgr
|
||||||
|
? this.encodeMouseReport(LEFT_BUTTON, position, "m")
|
||||||
|
: this.encodeMouseReport(DEFAULT_RELEASE_BUTTON, position, "M");
|
||||||
|
return release === null ? null : `${press}${release}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private encodeMouseReport(
|
||||||
|
button: number,
|
||||||
|
position: TerminalMousePosition,
|
||||||
|
sgrFinal: "M" | "m",
|
||||||
|
): string | null {
|
||||||
|
if (this.mouseEncodingMode === SGR_PIXELS_MOUSE_ENCODING_MODE) {
|
||||||
|
return `\x1b[<${button};${position.pixelX};${position.pixelY}${sgrFinal}`;
|
||||||
|
}
|
||||||
|
if (this.mouseEncodingMode === SGR_MOUSE_ENCODING_MODE) {
|
||||||
|
return `\x1b[<${button};${position.col};${position.row}${sgrFinal}`;
|
||||||
|
}
|
||||||
|
// Input travels as UTF-8 text, so a default-encoded byte above ASCII would be re-encoded.
|
||||||
|
const codes = [button, position.col, position.row].map(
|
||||||
|
(value) => value + DEFAULT_MOUSE_ENCODING_OFFSET,
|
||||||
|
);
|
||||||
|
if (codes.some((code) => code > MAX_ASCII_CODE)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return `\x1b[M${String.fromCharCode(...codes)}`;
|
||||||
|
}
|
||||||
|
|
||||||
private applyKittyKeyboardSequence(
|
private applyKittyKeyboardSequence(
|
||||||
prefix: string,
|
prefix: string,
|
||||||
params: string,
|
params: string,
|
||||||
@@ -205,6 +293,18 @@ export class TerminalInputModeTracker {
|
|||||||
const modes = parsePrivateModeParams(params);
|
const modes = parsePrivateModeParams(params);
|
||||||
let changed = false;
|
let changed = false;
|
||||||
|
|
||||||
|
for (const mode of modes) {
|
||||||
|
const next = final === "h" ? mode : 0;
|
||||||
|
if (MOUSE_TRACKING_MODES.has(mode)) {
|
||||||
|
changed = this.mouseTrackingMode !== next || changed;
|
||||||
|
this.mouseTrackingMode = next;
|
||||||
|
}
|
||||||
|
if (MOUSE_ENCODING_MODES.has(mode)) {
|
||||||
|
changed = this.mouseEncodingMode !== next || changed;
|
||||||
|
this.mouseEncodingMode = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (modes.has(WIN32_INPUT_MODE)) {
|
if (modes.has(WIN32_INPUT_MODE)) {
|
||||||
const previous = this.win32InputMode;
|
const previous = this.win32InputMode;
|
||||||
this.win32InputMode = final === "h";
|
this.win32InputMode = final === "h";
|
||||||
|
|||||||
Reference in New Issue
Block a user