@aitty/browser 0.1.1 → 0.2.0
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/README.md +84 -0
- package/dist/browser.d.ts +6 -4
- package/dist/browser.js +3 -1
- package/dist/frontend/aitty-sw.js +43 -0
- package/dist/frontend/ansi-sequences.d.ts +7 -2
- package/dist/frontend/ansi-sequences.js +65 -2
- package/dist/frontend/ansi-style-tracker.d.ts +2 -6
- package/dist/frontend/ansi-style-tracker.js +11 -47
- package/dist/frontend/browser-terminal-renderer.d.ts +23 -15
- package/dist/frontend/browser-terminal-renderer.js +266 -102
- package/dist/frontend/cell-width.d.ts +1 -1
- package/dist/frontend/cell-width.js +1 -1
- package/dist/frontend/shell-controls.d.ts +24 -0
- package/dist/frontend/shell-controls.js +221 -0
- package/dist/frontend/terminal-app.d.ts +84 -20
- package/dist/frontend/terminal-app.js +2672 -278
- package/dist/frontend/terminal-config.d.ts +62 -0
- package/dist/frontend/terminal-config.js +126 -0
- package/dist/frontend/terminal-input-policies.d.ts +12 -2
- package/dist/frontend/terminal-input-policies.js +131 -49
- package/dist/frontend/terminal-scroll-anchor.js +25 -0
- package/dist/frontend/terminal-scroll-follow.js +23 -0
- package/dist/frontend/terminal-scrollback-window.js +18 -0
- package/dist/frontend/terminal-theme-protocol.d.ts +1 -1
- package/dist/frontend/terminal-theme-protocol.js +1 -1
- package/dist/frontend/terminal.css +161 -19
- package/dist/frontend/virtual-transcript-window.js +42 -0
- package/package.json +3 -3
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { TerminalKeyResolver } from "./terminal-input-policies.js";
|
|
2
|
+
import { AittyTheme } from "@aitty/protocol";
|
|
3
|
+
|
|
4
|
+
//#region packages/browser/src/frontend/terminal-config.d.ts
|
|
5
|
+
type TerminalTheme = AittyTheme;
|
|
6
|
+
type TerminalThemeTarget = "container" | "document";
|
|
7
|
+
type TerminalFontSizeRange = {
|
|
8
|
+
max?: number;
|
|
9
|
+
min?: number;
|
|
10
|
+
step?: number;
|
|
11
|
+
};
|
|
12
|
+
type TerminalAppearanceConfig = {
|
|
13
|
+
cssVars?: Record<string, string | null | undefined>;
|
|
14
|
+
fontSize?: number;
|
|
15
|
+
fontSizeRange?: TerminalFontSizeRange;
|
|
16
|
+
lineHeight?: number;
|
|
17
|
+
theme?: TerminalTheme;
|
|
18
|
+
themeTarget?: TerminalThemeTarget;
|
|
19
|
+
};
|
|
20
|
+
type TerminalBehaviorConfig = {
|
|
21
|
+
input?: {
|
|
22
|
+
resolveKey?: TerminalKeyResolver;
|
|
23
|
+
};
|
|
24
|
+
scrollbackLimit?: number;
|
|
25
|
+
};
|
|
26
|
+
type TerminalConfig = {
|
|
27
|
+
appearance?: TerminalAppearanceConfig;
|
|
28
|
+
behavior?: TerminalBehaviorConfig;
|
|
29
|
+
};
|
|
30
|
+
type TerminalConfigPatch = {
|
|
31
|
+
appearance?: TerminalAppearanceConfig;
|
|
32
|
+
behavior?: TerminalBehaviorConfig;
|
|
33
|
+
};
|
|
34
|
+
type TerminalResolvedAppearanceConfig = {
|
|
35
|
+
cssVars: Record<string, string>;
|
|
36
|
+
fontSize: number;
|
|
37
|
+
fontSizeRange: Required<TerminalFontSizeRange>;
|
|
38
|
+
lineHeight?: number;
|
|
39
|
+
theme?: TerminalTheme;
|
|
40
|
+
themeTarget: TerminalThemeTarget;
|
|
41
|
+
};
|
|
42
|
+
type TerminalResolvedBehaviorConfig = {
|
|
43
|
+
input?: {
|
|
44
|
+
resolveKey?: TerminalKeyResolver;
|
|
45
|
+
};
|
|
46
|
+
scrollbackLimit?: number;
|
|
47
|
+
};
|
|
48
|
+
type TerminalResolvedConfig = {
|
|
49
|
+
appearance: TerminalResolvedAppearanceConfig;
|
|
50
|
+
behavior: TerminalResolvedBehaviorConfig;
|
|
51
|
+
};
|
|
52
|
+
type TerminalConfigListener = (config: TerminalResolvedConfig) => void;
|
|
53
|
+
declare function normalizeTerminalConfig(config: TerminalConfig | undefined): TerminalResolvedConfig;
|
|
54
|
+
declare function mergeTerminalConfig(current: TerminalResolvedConfig, patch: TerminalConfigPatch): TerminalResolvedConfig;
|
|
55
|
+
declare function cloneTerminalConfig(config: TerminalResolvedConfig): TerminalResolvedConfig;
|
|
56
|
+
declare function areTerminalAppearancesEqual(left: TerminalResolvedAppearanceConfig, right: TerminalResolvedAppearanceConfig): boolean;
|
|
57
|
+
declare function areTerminalBehaviorsEqual(left: TerminalResolvedBehaviorConfig, right: TerminalResolvedBehaviorConfig): boolean;
|
|
58
|
+
declare function hasTerminalLayoutAppearanceChange(previous: TerminalResolvedAppearanceConfig, next: TerminalResolvedAppearanceConfig): boolean;
|
|
59
|
+
declare function normalizeThemeName(theme: TerminalTheme | undefined): string | undefined;
|
|
60
|
+
declare function normalizeThemeTarget(value: TerminalThemeTarget | string | undefined): TerminalThemeTarget;
|
|
61
|
+
//#endregion
|
|
62
|
+
export { TerminalAppearanceConfig, TerminalBehaviorConfig, TerminalConfig, TerminalConfigListener, TerminalConfigPatch, TerminalFontSizeRange, TerminalResolvedAppearanceConfig, TerminalResolvedBehaviorConfig, TerminalResolvedConfig, TerminalTheme, TerminalThemeTarget, areTerminalAppearancesEqual, areTerminalBehaviorsEqual, cloneTerminalConfig, hasTerminalLayoutAppearanceChange, mergeTerminalConfig, normalizeTerminalConfig, normalizeThemeName, normalizeThemeTarget };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { normalizeTheme } from "@aitty/protocol";
|
|
2
|
+
//#region packages/browser/src/frontend/terminal-config.ts
|
|
3
|
+
const DEFAULT_TERMINAL_FONT_SIZE = 15;
|
|
4
|
+
const DEFAULT_TERMINAL_FONT_SIZE_MAX = 24;
|
|
5
|
+
const DEFAULT_TERMINAL_FONT_SIZE_MIN = 11;
|
|
6
|
+
const DEFAULT_TERMINAL_FONT_SIZE_STEP = 1;
|
|
7
|
+
function normalizeTerminalConfig(config) {
|
|
8
|
+
const appearance = config?.appearance ?? {};
|
|
9
|
+
const fontSizeRange = normalizeTerminalFontSizeRange(appearance.fontSizeRange);
|
|
10
|
+
const fontSize = normalizeTerminalFontSize(appearance.fontSize, fontSizeRange);
|
|
11
|
+
return {
|
|
12
|
+
appearance: {
|
|
13
|
+
cssVars: normalizeTerminalCssVars(appearance.cssVars),
|
|
14
|
+
fontSize,
|
|
15
|
+
fontSizeRange,
|
|
16
|
+
lineHeight: normalizeTerminalLineHeight(appearance.lineHeight),
|
|
17
|
+
theme: normalizeThemeName(appearance.theme),
|
|
18
|
+
themeTarget: normalizeThemeTarget(appearance.themeTarget)
|
|
19
|
+
},
|
|
20
|
+
behavior: {
|
|
21
|
+
input: config?.behavior?.input,
|
|
22
|
+
scrollbackLimit: normalizeOptionalNonNegativeInteger(config?.behavior?.scrollbackLimit)
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function mergeTerminalConfig(current, patch) {
|
|
27
|
+
const nextAppearancePatch = patch.appearance ?? {};
|
|
28
|
+
const fontSizeRange = normalizeTerminalFontSizeRange(nextAppearancePatch.fontSizeRange ?? current.appearance.fontSizeRange);
|
|
29
|
+
const fontSize = normalizeTerminalFontSize(nextAppearancePatch.fontSize, fontSizeRange, current.appearance.fontSize);
|
|
30
|
+
return {
|
|
31
|
+
appearance: {
|
|
32
|
+
cssVars: normalizeTerminalCssVars({
|
|
33
|
+
...current.appearance.cssVars,
|
|
34
|
+
...nextAppearancePatch.cssVars
|
|
35
|
+
}),
|
|
36
|
+
fontSize,
|
|
37
|
+
fontSizeRange,
|
|
38
|
+
lineHeight: "lineHeight" in nextAppearancePatch ? normalizeTerminalLineHeight(nextAppearancePatch.lineHeight) : current.appearance.lineHeight,
|
|
39
|
+
theme: "theme" in nextAppearancePatch ? normalizeThemeName(nextAppearancePatch.theme) : current.appearance.theme,
|
|
40
|
+
themeTarget: current.appearance.themeTarget
|
|
41
|
+
},
|
|
42
|
+
behavior: {
|
|
43
|
+
input: patch.behavior?.input ?? current.behavior.input,
|
|
44
|
+
scrollbackLimit: "scrollbackLimit" in (patch.behavior ?? {}) ? normalizeOptionalNonNegativeInteger(patch.behavior?.scrollbackLimit) : current.behavior.scrollbackLimit
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function cloneTerminalConfig(config) {
|
|
49
|
+
return {
|
|
50
|
+
appearance: {
|
|
51
|
+
cssVars: { ...config.appearance.cssVars },
|
|
52
|
+
fontSize: config.appearance.fontSize,
|
|
53
|
+
fontSizeRange: { ...config.appearance.fontSizeRange },
|
|
54
|
+
lineHeight: config.appearance.lineHeight,
|
|
55
|
+
theme: config.appearance.theme,
|
|
56
|
+
themeTarget: config.appearance.themeTarget
|
|
57
|
+
},
|
|
58
|
+
behavior: {
|
|
59
|
+
input: config.behavior.input,
|
|
60
|
+
scrollbackLimit: config.behavior.scrollbackLimit
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function areTerminalAppearancesEqual(left, right) {
|
|
65
|
+
return left.fontSize === right.fontSize && left.lineHeight === right.lineHeight && left.theme === right.theme && left.themeTarget === right.themeTarget && areFontSizeRangesEqual(left.fontSizeRange, right.fontSizeRange) && areCssVarsEqual(left.cssVars, right.cssVars);
|
|
66
|
+
}
|
|
67
|
+
function areTerminalBehaviorsEqual(left, right) {
|
|
68
|
+
return left.input === right.input && left.scrollbackLimit === right.scrollbackLimit;
|
|
69
|
+
}
|
|
70
|
+
function hasTerminalLayoutAppearanceChange(previous, next) {
|
|
71
|
+
return previous.fontSize !== next.fontSize || previous.lineHeight !== next.lineHeight;
|
|
72
|
+
}
|
|
73
|
+
function normalizeThemeName(theme) {
|
|
74
|
+
return normalizeTheme(theme);
|
|
75
|
+
}
|
|
76
|
+
function normalizeThemeTarget(value) {
|
|
77
|
+
return value === "document" ? "document" : "container";
|
|
78
|
+
}
|
|
79
|
+
function areFontSizeRangesEqual(left, right) {
|
|
80
|
+
return left.min === right.min && left.max === right.max && left.step === right.step;
|
|
81
|
+
}
|
|
82
|
+
function areCssVarsEqual(left, right) {
|
|
83
|
+
const leftEntries = Object.entries(left);
|
|
84
|
+
const rightKeys = new Set(Object.keys(right));
|
|
85
|
+
return leftEntries.length === rightKeys.size && leftEntries.every(([key, value]) => right[key] === value);
|
|
86
|
+
}
|
|
87
|
+
function normalizeTerminalCssVars(vars) {
|
|
88
|
+
const normalized = {};
|
|
89
|
+
for (const [name, value] of Object.entries(vars ?? {})) {
|
|
90
|
+
if (!name.startsWith("--") || value === null || value === void 0) continue;
|
|
91
|
+
normalized[name] = String(value);
|
|
92
|
+
}
|
|
93
|
+
return normalized;
|
|
94
|
+
}
|
|
95
|
+
function normalizeTerminalLineHeight(value) {
|
|
96
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return;
|
|
97
|
+
return Math.round(value * 100) / 100;
|
|
98
|
+
}
|
|
99
|
+
function normalizeOptionalNonNegativeInteger(value) {
|
|
100
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return;
|
|
101
|
+
return Math.max(0, Math.floor(value));
|
|
102
|
+
}
|
|
103
|
+
function normalizeTerminalFontSizeRange(range) {
|
|
104
|
+
const min = normalizeFiniteNumber(range?.min, DEFAULT_TERMINAL_FONT_SIZE_MIN);
|
|
105
|
+
const max = normalizeFiniteNumber(range?.max, DEFAULT_TERMINAL_FONT_SIZE_MAX);
|
|
106
|
+
const low = Math.max(1, Math.min(min, max));
|
|
107
|
+
const high = Math.max(low, Math.max(min, max));
|
|
108
|
+
const step = Math.max(.1, roundTerminalFontSize(normalizeFiniteNumber(range?.step, DEFAULT_TERMINAL_FONT_SIZE_STEP)));
|
|
109
|
+
return {
|
|
110
|
+
max: roundTerminalFontSize(high),
|
|
111
|
+
min: roundTerminalFontSize(low),
|
|
112
|
+
step
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function normalizeTerminalFontSize(value, range, fallback = DEFAULT_TERMINAL_FONT_SIZE) {
|
|
116
|
+
const base = normalizeFiniteNumber(value, fallback);
|
|
117
|
+
return roundTerminalFontSize(Math.min(range.max, Math.max(range.min, base)));
|
|
118
|
+
}
|
|
119
|
+
function normalizeFiniteNumber(value, fallback) {
|
|
120
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
121
|
+
}
|
|
122
|
+
function roundTerminalFontSize(value) {
|
|
123
|
+
return Math.round(value * 10) / 10;
|
|
124
|
+
}
|
|
125
|
+
//#endregion
|
|
126
|
+
export { areTerminalAppearancesEqual, areTerminalBehaviorsEqual, cloneTerminalConfig, hasTerminalLayoutAppearanceChange, mergeTerminalConfig, normalizeTerminalConfig, normalizeThemeName, normalizeThemeTarget };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region src/frontend/terminal-input-policies.d.ts
|
|
1
|
+
//#region packages/browser/src/frontend/terminal-input-policies.d.ts
|
|
2
2
|
type TerminalInputHost = {
|
|
3
3
|
bridge?: {
|
|
4
4
|
bracketedPaste?: () => boolean;
|
|
@@ -10,15 +10,25 @@ type TerminalInputHost = {
|
|
|
10
10
|
} | null;
|
|
11
11
|
} | null | undefined;
|
|
12
12
|
type TerminalKeyResolver = (event: KeyboardEvent) => string | null | undefined;
|
|
13
|
+
type TerminalPasteShortcut = "cmd-v" | "ctrl-v";
|
|
14
|
+
type TerminalPasteShortcutResult = string | {
|
|
15
|
+
allowTerminalFallback?: boolean;
|
|
16
|
+
data?: string | null;
|
|
17
|
+
} | null | undefined;
|
|
13
18
|
type TerminalInputCallbacks = {
|
|
14
19
|
captureDocumentCtrlN?: boolean;
|
|
15
20
|
onData?: (data: string) => void;
|
|
21
|
+
onPasteShortcut?: (event?: ClipboardEvent) => Promise<TerminalPasteShortcutResult> | TerminalPasteShortcutResult;
|
|
22
|
+
pasteShortcut?: TerminalPasteShortcut;
|
|
16
23
|
resolveKey?: TerminalKeyResolver;
|
|
17
24
|
resolveDocumentKeyData?: (event: KeyboardEvent) => string | null;
|
|
18
25
|
};
|
|
19
26
|
declare function installTerminalInputPolicies(term: TerminalInputHost, callbacks?: TerminalInputCallbacks): () => void;
|
|
20
27
|
declare function isBrowserSafeShortcut(event: KeyboardEvent): boolean;
|
|
21
28
|
declare function isContainedTerminalNavigationKey(event: KeyboardEvent): boolean;
|
|
29
|
+
declare function resolveTerminalDocumentKeyData(term: TerminalInputHost, event: KeyboardEvent): string | null;
|
|
22
30
|
declare function isCompositionKeyDown(event: KeyboardEvent): boolean;
|
|
31
|
+
declare function resolvePlainInputText(event: InputEvent, textareaValue: string): string;
|
|
32
|
+
declare function resolveCommittedInputText(event: InputEvent, textareaValue: string): string;
|
|
23
33
|
//#endregion
|
|
24
|
-
export { TerminalInputCallbacks, TerminalKeyResolver, installTerminalInputPolicies, isBrowserSafeShortcut, isCompositionKeyDown, isContainedTerminalNavigationKey };
|
|
34
|
+
export { TerminalInputCallbacks, TerminalKeyResolver, TerminalPasteShortcut, TerminalPasteShortcutResult, installTerminalInputPolicies, isBrowserSafeShortcut, isCompositionKeyDown, isContainedTerminalNavigationKey, resolveCommittedInputText, resolvePlainInputText, resolveTerminalDocumentKeyData };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { getTerminalSelectionText, hasTerminalSelection } from "@aitty/wterm-dom";
|
|
2
|
+
//#region packages/browser/src/frontend/terminal-input-policies.ts
|
|
2
3
|
const RESERVED_BROWSER_SHORTCUT_KEYS = new Set(["r", "w"]);
|
|
3
4
|
const CONTAINED_TERMINAL_NAVIGATION_KEYS = new Set([
|
|
4
5
|
"ArrowUp",
|
|
@@ -98,11 +99,17 @@ const ALT_PRINTABLE_CODE_MAP = Object.freeze({
|
|
|
98
99
|
Slash: "/",
|
|
99
100
|
Space: " "
|
|
100
101
|
});
|
|
102
|
+
const PASTE_SHORTCUT_PENDING_MS = 15e3;
|
|
103
|
+
const PASTE_SHORTCUT_FALLBACK_MS = 0;
|
|
104
|
+
const CTRL_V_SEQUENCE = "";
|
|
105
|
+
const DEFAULT_PASTE_SHORTCUT = "ctrl-v";
|
|
101
106
|
function installTerminalInputPolicies(term, callbacks = {}) {
|
|
102
107
|
const root = term?.element;
|
|
103
108
|
const textarea = term?.input?.textarea;
|
|
104
109
|
const ownerDocument = root?.ownerDocument;
|
|
105
110
|
const onData = callbacks.onData ?? (() => {});
|
|
111
|
+
const onPasteShortcut = callbacks.onPasteShortcut;
|
|
112
|
+
const pasteShortcut = callbacks.pasteShortcut ?? DEFAULT_PASTE_SHORTCUT;
|
|
106
113
|
const captureDocumentCtrlN = callbacks.captureDocumentCtrlN === true;
|
|
107
114
|
const resolveKey = callbacks.resolveKey ?? (() => null);
|
|
108
115
|
const resolveDocumentKeyData = callbacks.resolveDocumentKeyData ?? (() => null);
|
|
@@ -111,11 +118,58 @@ function installTerminalInputPolicies(term, callbacks = {}) {
|
|
|
111
118
|
let pendingCompositionCommit = false;
|
|
112
119
|
let suppressedCompositionValue = "";
|
|
113
120
|
let latestCompositionValue = "";
|
|
114
|
-
let speculativePlainInputText = "";
|
|
115
121
|
let pendingSyntheticPrintableText = "";
|
|
116
122
|
let syntheticPrintableFlushQueued = false;
|
|
117
|
-
|
|
118
|
-
|
|
123
|
+
let pendingPasteShortcut = false;
|
|
124
|
+
let pendingPasteShortcutClearTimer = null;
|
|
125
|
+
let pendingPasteShortcutFallbackTimer = null;
|
|
126
|
+
let pasteShortcutGeneration = 0;
|
|
127
|
+
let consumedPasteShortcutGeneration = 0;
|
|
128
|
+
const schedulePasteShortcutFallback = (generation) => {
|
|
129
|
+
if (pendingPasteShortcutFallbackTimer !== null) ownerDocument.defaultView?.clearTimeout(pendingPasteShortcutFallbackTimer);
|
|
130
|
+
pendingPasteShortcutFallbackTimer = ownerDocument.defaultView?.setTimeout(() => {
|
|
131
|
+
pendingPasteShortcutFallbackTimer = null;
|
|
132
|
+
if (pendingPasteShortcut && pasteShortcutGeneration === generation && consumedPasteShortcutGeneration !== generation) {
|
|
133
|
+
consumedPasteShortcutGeneration = generation;
|
|
134
|
+
clearPendingPasteShortcut();
|
|
135
|
+
onData(CTRL_V_SEQUENCE);
|
|
136
|
+
}
|
|
137
|
+
}, PASTE_SHORTCUT_FALLBACK_MS) ?? null;
|
|
138
|
+
};
|
|
139
|
+
const runPasteShortcut = (generation, event) => {
|
|
140
|
+
if (!onPasteShortcut) return;
|
|
141
|
+
Promise.resolve(onPasteShortcut(event)).then((result) => {
|
|
142
|
+
const data = resolvePasteShortcutData(result);
|
|
143
|
+
if (typeof data === "string" && data.length > 0 && pendingPasteShortcut && pasteShortcutGeneration === generation && consumedPasteShortcutGeneration !== generation) {
|
|
144
|
+
consumedPasteShortcutGeneration = generation;
|
|
145
|
+
clearPendingPasteShortcut();
|
|
146
|
+
sendPasteText(data);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (pasteShortcut === "ctrl-v" && shouldAllowPasteShortcutFallback(result) && pendingPasteShortcut && pasteShortcutGeneration === generation && consumedPasteShortcutGeneration !== generation && event === void 0) schedulePasteShortcutFallback(generation);
|
|
150
|
+
}).catch(() => void 0);
|
|
151
|
+
};
|
|
152
|
+
const clearPendingPasteShortcut = () => {
|
|
153
|
+
pendingPasteShortcut = false;
|
|
154
|
+
if (pendingPasteShortcutClearTimer !== null) {
|
|
155
|
+
ownerDocument.defaultView?.clearTimeout(pendingPasteShortcutClearTimer);
|
|
156
|
+
pendingPasteShortcutClearTimer = null;
|
|
157
|
+
}
|
|
158
|
+
if (pendingPasteShortcutFallbackTimer !== null) {
|
|
159
|
+
ownerDocument.defaultView?.clearTimeout(pendingPasteShortcutFallbackTimer);
|
|
160
|
+
pendingPasteShortcutFallbackTimer = null;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
const markPendingPasteShortcut = () => {
|
|
164
|
+
clearPendingPasteShortcut();
|
|
165
|
+
pasteShortcutGeneration += 1;
|
|
166
|
+
pendingPasteShortcut = true;
|
|
167
|
+
const generation = pasteShortcutGeneration;
|
|
168
|
+
pendingPasteShortcutClearTimer = ownerDocument.defaultView?.setTimeout(() => {
|
|
169
|
+
if (pasteShortcutGeneration !== generation) return;
|
|
170
|
+
pendingPasteShortcut = false;
|
|
171
|
+
pendingPasteShortcutClearTimer = null;
|
|
172
|
+
}, PASTE_SHORTCUT_PENDING_MS) ?? null;
|
|
119
173
|
};
|
|
120
174
|
const clearPendingSyntheticPrintableText = () => {
|
|
121
175
|
pendingSyntheticPrintableText = "";
|
|
@@ -127,11 +181,9 @@ function installTerminalInputPolicies(term, callbacks = {}) {
|
|
|
127
181
|
onData(pendingText);
|
|
128
182
|
};
|
|
129
183
|
const clearPendingTextState = () => {
|
|
130
|
-
clearSpeculativePlainInputText();
|
|
131
184
|
clearPendingSyntheticPrintableText();
|
|
132
185
|
};
|
|
133
186
|
const flushPendingTextState = () => {
|
|
134
|
-
clearSpeculativePlainInputText();
|
|
135
187
|
flushPendingSyntheticPrintableText();
|
|
136
188
|
};
|
|
137
189
|
const deferSyntheticPrintableText = (text) => {
|
|
@@ -146,17 +198,18 @@ function installTerminalInputPolicies(term, callbacks = {}) {
|
|
|
146
198
|
onData(pendingText);
|
|
147
199
|
});
|
|
148
200
|
};
|
|
149
|
-
const rollBackSpeculativePlainInputText = (preeditText) => {
|
|
150
|
-
const rollbackText = resolveSpeculativeRollbackText(speculativePlainInputText, preeditText);
|
|
151
|
-
if (!rollbackText) return;
|
|
152
|
-
const rollback = "".repeat([...rollbackText].length);
|
|
153
|
-
speculativePlainInputText = speculativePlainInputText.slice(0, -rollbackText.length);
|
|
154
|
-
onData(rollback);
|
|
155
|
-
};
|
|
156
201
|
const sendText = (text) => {
|
|
157
202
|
if (!text) return;
|
|
158
203
|
onData(text);
|
|
159
204
|
};
|
|
205
|
+
const sendPasteText = (text) => {
|
|
206
|
+
if (!text) return;
|
|
207
|
+
if (term?.bridge?.bracketedPaste?.() === true) {
|
|
208
|
+
onData(`\u001b[200~${text.split("\x1B").join("")}\u001b[201~`);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
onData(text);
|
|
212
|
+
};
|
|
160
213
|
const handleDocumentKeyDownCapture = (event) => {
|
|
161
214
|
if (event.target === textarea) return;
|
|
162
215
|
const documentKeyData = captureDocumentCtrlN && !event.altKey && !event.metaKey && event.ctrlKey && event.key.toLowerCase() === "n" ? "" : resolveDocumentKeyData(event);
|
|
@@ -167,6 +220,7 @@ function installTerminalInputPolicies(term, callbacks = {}) {
|
|
|
167
220
|
onData(documentKeyData);
|
|
168
221
|
};
|
|
169
222
|
const handleKeyDownCapture = (event) => {
|
|
223
|
+
if (!isTerminalSessionInteractive(root)) return;
|
|
170
224
|
if (isBrowserSafeShortcut(event)) {
|
|
171
225
|
flushPendingTextState();
|
|
172
226
|
stopInputPropagation(event);
|
|
@@ -177,6 +231,14 @@ function installTerminalInputPolicies(term, callbacks = {}) {
|
|
|
177
231
|
stopInputPropagation(event);
|
|
178
232
|
return;
|
|
179
233
|
}
|
|
234
|
+
if (isPasteShortcut(event, pasteShortcut) && onPasteShortcut) {
|
|
235
|
+
flushPendingTextState();
|
|
236
|
+
stopInputPropagation(event);
|
|
237
|
+
markPendingPasteShortcut();
|
|
238
|
+
runPasteShortcut(pasteShortcutGeneration);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
clearPendingPasteShortcut();
|
|
180
242
|
const customKeyData = resolveKey(event);
|
|
181
243
|
if (customKeyData != null) {
|
|
182
244
|
flushPendingTextState();
|
|
@@ -223,7 +285,7 @@ function installTerminalInputPolicies(term, callbacks = {}) {
|
|
|
223
285
|
}
|
|
224
286
|
};
|
|
225
287
|
const handleCompositionStartCapture = (event) => {
|
|
226
|
-
|
|
288
|
+
clearPendingTextState();
|
|
227
289
|
composing = true;
|
|
228
290
|
pendingCompositionCommit = false;
|
|
229
291
|
suppressedCompositionValue = "";
|
|
@@ -255,10 +317,8 @@ function installTerminalInputPolicies(term, callbacks = {}) {
|
|
|
255
317
|
};
|
|
256
318
|
const handleInputCapture = (event) => {
|
|
257
319
|
if (composing || isComposingInput(event)) {
|
|
258
|
-
if (isCompositionPreeditInput(event))
|
|
259
|
-
|
|
260
|
-
rollBackSpeculativePlainInputText(textarea.value);
|
|
261
|
-
} else clearPendingTextState();
|
|
320
|
+
if (isCompositionPreeditInput(event)) clearPendingTextState();
|
|
321
|
+
else clearPendingTextState();
|
|
262
322
|
latestCompositionValue = textarea.value;
|
|
263
323
|
stopInputPropagation(event);
|
|
264
324
|
return;
|
|
@@ -313,28 +373,37 @@ function installTerminalInputPolicies(term, callbacks = {}) {
|
|
|
313
373
|
sendText(committedText);
|
|
314
374
|
return;
|
|
315
375
|
}
|
|
316
|
-
|
|
317
|
-
if (isPotentialImeSeedText(inputText)) speculativePlainInputText += inputText;
|
|
318
|
-
else clearSpeculativePlainInputText();
|
|
319
|
-
sendText(inputText);
|
|
376
|
+
sendText(resolvePlainInputText(event, value));
|
|
320
377
|
};
|
|
321
378
|
const handleCopy = (event) => {
|
|
322
|
-
const selectionText =
|
|
379
|
+
const selectionText = getTerminalSelectionText(root);
|
|
323
380
|
if (!selectionText || !event.clipboardData?.setData) return;
|
|
324
381
|
event.clipboardData.setData("text/plain", selectionText);
|
|
325
382
|
event.preventDefault();
|
|
326
383
|
};
|
|
327
384
|
const handlePaste = (event) => {
|
|
328
385
|
flushPendingTextState();
|
|
386
|
+
if (!isTerminalSessionInteractive(root)) return;
|
|
387
|
+
if (onPasteShortcut && clipboardEventHasImage(event)) {
|
|
388
|
+
if (!pendingPasteShortcut) markPendingPasteShortcut();
|
|
389
|
+
event.preventDefault();
|
|
390
|
+
stopInputPropagation(event);
|
|
391
|
+
runPasteShortcut(pasteShortcutGeneration, event);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
329
394
|
const text = event.clipboardData?.getData("text") ?? event.clipboardData?.getData("text/plain") ?? "";
|
|
330
|
-
if (!text)
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
395
|
+
if (!text) {
|
|
396
|
+
if (pendingPasteShortcut && onPasteShortcut) {
|
|
397
|
+
event.preventDefault();
|
|
398
|
+
stopInputPropagation(event);
|
|
399
|
+
runPasteShortcut(pasteShortcutGeneration, event);
|
|
400
|
+
}
|
|
335
401
|
return;
|
|
336
402
|
}
|
|
337
|
-
|
|
403
|
+
clearPendingPasteShortcut();
|
|
404
|
+
event.preventDefault();
|
|
405
|
+
stopInputPropagation(event);
|
|
406
|
+
sendPasteText(text);
|
|
338
407
|
};
|
|
339
408
|
ownerDocument.addEventListener("keydown", handleDocumentKeyDownCapture, true);
|
|
340
409
|
textarea.addEventListener("keydown", handleKeyDownCapture, true);
|
|
@@ -344,6 +413,8 @@ function installTerminalInputPolicies(term, callbacks = {}) {
|
|
|
344
413
|
textarea.addEventListener("paste", handlePaste, true);
|
|
345
414
|
ownerDocument.addEventListener("copy", handleCopy, true);
|
|
346
415
|
return () => {
|
|
416
|
+
clearPendingTextState();
|
|
417
|
+
clearPendingPasteShortcut();
|
|
347
418
|
ownerDocument.removeEventListener("keydown", handleDocumentKeyDownCapture, true);
|
|
348
419
|
textarea.removeEventListener("keydown", handleKeyDownCapture, true);
|
|
349
420
|
textarea.removeEventListener("compositionstart", handleCompositionStartCapture, true);
|
|
@@ -363,6 +434,13 @@ function isContainedTerminalNavigationKey(event) {
|
|
|
363
434
|
if (event.altKey || event.ctrlKey || event.metaKey) return false;
|
|
364
435
|
return !(event.shiftKey && (event.key === "PageUp" || event.key === "PageDown"));
|
|
365
436
|
}
|
|
437
|
+
function resolveTerminalDocumentKeyData(term, event) {
|
|
438
|
+
if (isBrowserSafeShortcut(event) || isCompositionKeyDown(event)) return null;
|
|
439
|
+
const normalizedAltSequence = resolveAltPrintableSequence(event);
|
|
440
|
+
if (normalizedAltSequence) return normalizedAltSequence;
|
|
441
|
+
if (isPlainPrintableKey(event)) return event.key;
|
|
442
|
+
return resolveTerminalKeySequence(term, event);
|
|
443
|
+
}
|
|
366
444
|
function resolveAltPrintableSequence(event) {
|
|
367
445
|
if (!event.altKey || event.ctrlKey || event.metaKey) return null;
|
|
368
446
|
const baseKey = ALT_PRINTABLE_CODE_MAP[event.code];
|
|
@@ -426,37 +504,41 @@ function resolveCommittedInputText(event, textareaValue) {
|
|
|
426
504
|
if (typeof event.data === "string" && event.data.length > 0) return event.data;
|
|
427
505
|
return textareaValue;
|
|
428
506
|
}
|
|
429
|
-
function isPotentialImeSeedText(text) {
|
|
430
|
-
return /^[0-9A-Za-z']$/.test(text);
|
|
431
|
-
}
|
|
432
|
-
function resolveSpeculativeRollbackText(sentText, preeditText) {
|
|
433
|
-
if (!sentText || !preeditText || !preeditText.startsWith(sentText)) return "";
|
|
434
|
-
return sentText;
|
|
435
|
-
}
|
|
436
507
|
function normalizeCompositionCommit(data) {
|
|
437
508
|
if (typeof data === "string" && data.length > 0) return data;
|
|
438
509
|
return "";
|
|
439
510
|
}
|
|
440
|
-
function getSelectionText(root) {
|
|
441
|
-
const selection = root.ownerDocument?.getSelection?.();
|
|
442
|
-
if (!selection || selection.isCollapsed) return "";
|
|
443
|
-
if (!nodeIsInsideRoot(root, selection.anchorNode) || !nodeIsInsideRoot(root, selection.focusNode)) return "";
|
|
444
|
-
return selection.toString();
|
|
445
|
-
}
|
|
446
511
|
function isCopyShortcutWithTerminalSelection(root, event) {
|
|
447
512
|
if (event.altKey || event.key.toLowerCase() !== "c") return false;
|
|
448
513
|
if (!(event.metaKey || event.ctrlKey)) return false;
|
|
449
|
-
return
|
|
514
|
+
return hasTerminalSelection(root);
|
|
515
|
+
}
|
|
516
|
+
function isPasteShortcut(event, shortcut) {
|
|
517
|
+
if (event.altKey || event.key.toLowerCase() !== "v") return false;
|
|
518
|
+
return shortcut === "cmd-v" ? event.metaKey && !event.ctrlKey : event.ctrlKey && !event.metaKey;
|
|
519
|
+
}
|
|
520
|
+
function clipboardEventHasImage(event) {
|
|
521
|
+
const clipboardData = event.clipboardData;
|
|
522
|
+
if (!clipboardData) return false;
|
|
523
|
+
for (const item of Array.from(clipboardData.items)) if (item.kind === "file" && item.type.startsWith("image/")) return true;
|
|
524
|
+
for (const file of Array.from(clipboardData.files)) if (file.type.startsWith("image/")) return true;
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
527
|
+
function resolvePasteShortcutData(result) {
|
|
528
|
+
if (typeof result === "string") return result;
|
|
529
|
+
if (!result || typeof result !== "object") return null;
|
|
530
|
+
return typeof result.data === "string" ? result.data : null;
|
|
531
|
+
}
|
|
532
|
+
function shouldAllowPasteShortcutFallback(result) {
|
|
533
|
+
if (!result || typeof result === "string") return true;
|
|
534
|
+
return result.allowTerminalFallback !== false;
|
|
450
535
|
}
|
|
451
|
-
function
|
|
452
|
-
|
|
453
|
-
if (node === root) return true;
|
|
454
|
-
if (node instanceof Element) return root.contains(node);
|
|
455
|
-
return node.parentElement ? root.contains(node.parentElement) : false;
|
|
536
|
+
function isTerminalSessionInteractive(root) {
|
|
537
|
+
return root.dataset?.sessionInteractive !== "false";
|
|
456
538
|
}
|
|
457
539
|
function stopInputPropagation(event) {
|
|
458
540
|
event.stopPropagation();
|
|
459
541
|
if (typeof event.stopImmediatePropagation === "function") event.stopImmediatePropagation();
|
|
460
542
|
}
|
|
461
543
|
//#endregion
|
|
462
|
-
export { installTerminalInputPolicies, isBrowserSafeShortcut, isCompositionKeyDown, isContainedTerminalNavigationKey };
|
|
544
|
+
export { installTerminalInputPolicies, isBrowserSafeShortcut, isCompositionKeyDown, isContainedTerminalNavigationKey, resolveCommittedInputText, resolvePlainInputText, resolveTerminalDocumentKeyData };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region packages/browser/src/frontend/terminal-scroll-anchor.ts
|
|
2
|
+
function resolveScrollAnchorRestoreTop(options) {
|
|
3
|
+
const maxScrollTop = normalizeScrollMetric(options.maxScrollTop);
|
|
4
|
+
if (options.stickToBottom) return maxScrollTop;
|
|
5
|
+
return clamp(normalizeOptionalScrollMetric(options.inputAnchoredScrollTop) ?? normalizeOptionalScrollMetric(options.anchoredScrollTop) ?? resolveRatioFallbackScrollTop({
|
|
6
|
+
maxScrollTop,
|
|
7
|
+
ratio: options.ratio,
|
|
8
|
+
scrollTop: options.scrollTop
|
|
9
|
+
}), 0, maxScrollTop);
|
|
10
|
+
}
|
|
11
|
+
function resolveRatioFallbackScrollTop({ maxScrollTop, ratio, scrollTop }) {
|
|
12
|
+
if (maxScrollTop <= 0) return Math.min(normalizeScrollMetric(scrollTop), maxScrollTop);
|
|
13
|
+
return Math.round(maxScrollTop * clamp(Number.isFinite(ratio) ? ratio : 0, 0, 1));
|
|
14
|
+
}
|
|
15
|
+
function normalizeOptionalScrollMetric(value) {
|
|
16
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
17
|
+
}
|
|
18
|
+
function normalizeScrollMetric(value) {
|
|
19
|
+
return Number.isFinite(value) ? Math.max(0, value) : 0;
|
|
20
|
+
}
|
|
21
|
+
function clamp(value, min, max) {
|
|
22
|
+
return Math.min(Math.max(value, min), max);
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
export { resolveScrollAnchorRestoreTop };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
function resolveScrollFollowPolicy(options) {
|
|
2
|
+
const tolerancePx = normalizeNonNegativeMetric(options.tolerancePx ?? 1);
|
|
3
|
+
const maxScrollTop = normalizeNonNegativeMetric(options.maxScrollTop);
|
|
4
|
+
const scrollTop = normalizeNonNegativeMetric(options.scrollTop);
|
|
5
|
+
const previousScrollTop = normalizeNonNegativeMetric(options.previousScrollTop);
|
|
6
|
+
if (!options.keepBottomFollow || isScrollAtBottom(scrollTop, maxScrollTop, tolerancePx)) return { action: "idle" };
|
|
7
|
+
if (scrollTop < previousScrollTop - tolerancePx) return { action: "stop-follow" };
|
|
8
|
+
if (options.programmaticScroll) return { action: "idle" };
|
|
9
|
+
return {
|
|
10
|
+
action: "snap-to-bottom",
|
|
11
|
+
passes: 3,
|
|
12
|
+
preferTranscriptFollow: true
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function isScrollAtBottom(scrollTop, maxScrollTop, tolerancePx = 1) {
|
|
16
|
+
const normalizedMaxScrollTop = normalizeNonNegativeMetric(maxScrollTop);
|
|
17
|
+
return normalizedMaxScrollTop <= 0 || normalizeNonNegativeMetric(scrollTop) >= normalizedMaxScrollTop - normalizeNonNegativeMetric(tolerancePx);
|
|
18
|
+
}
|
|
19
|
+
function normalizeNonNegativeMetric(value) {
|
|
20
|
+
return Number.isFinite(value) ? Math.max(0, Math.round(value)) : 0;
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
export { isScrollAtBottom, resolveScrollFollowPolicy };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region packages/browser/src/frontend/terminal-scrollback-window.ts
|
|
2
|
+
const DEFAULT_RENDERED_SCROLLBACK_ROW_LIMIT = 1200;
|
|
3
|
+
function resolveRenderedScrollbackWindow(options) {
|
|
4
|
+
const totalRows = Math.max(0, Math.floor(options.totalRows));
|
|
5
|
+
const limit = normalizeRenderedScrollbackLimit(options.limit);
|
|
6
|
+
const renderedRows = Math.min(totalRows, limit);
|
|
7
|
+
return {
|
|
8
|
+
firstOffset: Math.max(0, renderedRows - 1),
|
|
9
|
+
renderedRows,
|
|
10
|
+
skippedRows: Math.max(0, totalRows - renderedRows)
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function normalizeRenderedScrollbackLimit(value) {
|
|
14
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_RENDERED_SCROLLBACK_ROW_LIMIT;
|
|
15
|
+
return Math.max(1, Math.floor(value));
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { DEFAULT_RENDERED_SCROLLBACK_ROW_LIMIT, resolveRenderedScrollbackWindow };
|