@svelterm/core 0.1.0 → 0.23.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/CHANGELOG.md +465 -0
- package/README.md +42 -29
- package/dist/src/cli/build.d.ts +13 -0
- package/dist/src/cli/build.js +119 -0
- package/dist/src/cli/bundle.d.ts +25 -0
- package/dist/src/cli/bundle.js +61 -0
- package/dist/src/cli/dev.d.ts +10 -0
- package/dist/src/cli/dev.js +152 -0
- package/dist/src/cli/devtools.d.ts +9 -0
- package/dist/src/cli/devtools.js +47 -0
- package/dist/src/cli/init.d.ts +8 -0
- package/dist/src/cli/init.js +153 -0
- package/dist/src/cli/main.d.ts +9 -0
- package/dist/src/cli/main.js +52 -0
- package/dist/src/cli/svt-bin.d.ts +2 -0
- package/dist/src/cli/svt-bin.js +6 -0
- package/dist/src/cli/svt.d.ts +14 -0
- package/dist/src/cli/svt.js +76 -0
- package/dist/src/components/text-buffer.js +8 -5
- package/dist/src/css/animation-runner.d.ts +15 -6
- package/dist/src/css/animation-runner.js +80 -29
- package/dist/src/css/animation.d.ts +12 -0
- package/dist/src/css/animation.js +21 -0
- package/dist/src/css/calc.js +4 -3
- package/dist/src/css/color.d.ts +19 -0
- package/dist/src/css/color.js +371 -62
- package/dist/src/css/compute.d.ts +31 -4
- package/dist/src/css/compute.js +273 -34
- package/dist/src/css/defaults.d.ts +1 -1
- package/dist/src/css/defaults.js +9 -0
- package/dist/src/css/easing.d.ts +9 -0
- package/dist/src/css/easing.js +95 -0
- package/dist/src/css/incremental.d.ts +1 -1
- package/dist/src/css/incremental.js +2 -2
- package/dist/src/css/interpolate.d.ts +13 -0
- package/dist/src/css/interpolate.js +41 -0
- package/dist/src/css/parser.js +59 -3
- package/dist/src/css/pseudo-elements.d.ts +9 -0
- package/dist/src/css/pseudo-elements.js +97 -0
- package/dist/src/css/selector.d.ts +17 -2
- package/dist/src/css/selector.js +128 -13
- package/dist/src/css/specificity.js +17 -6
- package/dist/src/css/values.d.ts +6 -1
- package/dist/src/css/values.js +13 -6
- package/dist/src/debug/context.d.ts +13 -0
- package/dist/src/debug/context.js +11 -0
- package/dist/src/debug/css.d.ts +12 -0
- package/dist/src/debug/css.js +28 -0
- package/dist/src/debug/dom.d.ts +17 -0
- package/dist/src/debug/dom.js +92 -0
- package/dist/src/devtools/DevTools.compiled.js +327 -0
- package/dist/src/devtools/DevTools.css.js +1 -0
- package/dist/src/devtools/client.d.ts +36 -0
- package/dist/src/devtools/client.js +76 -0
- package/dist/src/framelog.d.ts +54 -0
- package/dist/src/framelog.js +99 -0
- package/dist/src/headless.js +12 -4
- package/dist/src/index.d.ts +66 -3
- package/dist/src/index.js +610 -81
- package/dist/src/input/checkable.d.ts +8 -0
- package/dist/src/input/checkable.js +66 -0
- package/dist/src/input/details.d.ts +6 -0
- package/dist/src/input/details.js +34 -0
- package/dist/src/input/focus.d.ts +6 -0
- package/dist/src/input/focus.js +27 -9
- package/dist/src/input/keyboard.d.ts +2 -2
- package/dist/src/input/keyboard.js +32 -5
- package/dist/src/input/label.d.ts +8 -0
- package/dist/src/input/label.js +53 -0
- package/dist/src/input/modal.d.ts +9 -0
- package/dist/src/input/modal.js +28 -0
- package/dist/src/input/mouse.d.ts +2 -2
- package/dist/src/input/mouse.js +15 -2
- package/dist/src/input/select.d.ts +12 -0
- package/dist/src/input/select.js +63 -0
- package/dist/src/input/selection.d.ts +48 -0
- package/dist/src/input/selection.js +150 -0
- package/dist/src/layout/engine.d.ts +2 -0
- package/dist/src/layout/engine.js +1092 -142
- package/dist/src/layout/flex.js +4 -4
- package/dist/src/layout/size.js +3 -2
- package/dist/src/layout/text.d.ts +3 -2
- package/dist/src/layout/text.js +96 -17
- package/dist/src/layout/unicode.d.ts +20 -0
- package/dist/src/layout/unicode.js +121 -0
- package/dist/src/render/animation-clock.d.ts +57 -0
- package/dist/src/render/animation-clock.js +221 -0
- package/dist/src/render/ansi-text.d.ts +26 -0
- package/dist/src/render/ansi-text.js +131 -0
- package/dist/src/render/ansi.d.ts +18 -0
- package/dist/src/render/ansi.js +64 -19
- package/dist/src/render/border.js +166 -17
- package/dist/src/render/buffer.d.ts +1 -0
- package/dist/src/render/buffer.js +5 -2
- package/dist/src/render/clock.d.ts +35 -0
- package/dist/src/render/clock.js +67 -0
- package/dist/src/render/color-depth.d.ts +8 -0
- package/dist/src/render/color-depth.js +59 -0
- package/dist/src/render/context.d.ts +1 -0
- package/dist/src/render/context.js +17 -21
- package/dist/src/render/cursor-emit.d.ts +18 -0
- package/dist/src/render/cursor-emit.js +50 -0
- package/dist/src/render/diff.d.ts +12 -0
- package/dist/src/render/diff.js +120 -0
- package/dist/src/render/generation.d.ts +9 -0
- package/dist/src/render/generation.js +14 -0
- package/dist/src/render/graphics-layer.d.ts +27 -0
- package/dist/src/render/graphics-layer.js +86 -0
- package/dist/src/render/image.d.ts +27 -0
- package/dist/src/render/image.js +113 -0
- package/dist/src/render/incremental-paint.d.ts +7 -3
- package/dist/src/render/incremental-paint.js +52 -79
- package/dist/src/render/inline.d.ts +59 -0
- package/dist/src/render/inline.js +219 -0
- package/dist/src/render/kitty-graphics.d.ts +24 -0
- package/dist/src/render/kitty-graphics.js +58 -0
- package/dist/src/render/paint-text.js +68 -22
- package/dist/src/render/paint.d.ts +8 -1
- package/dist/src/render/paint.js +358 -31
- package/dist/src/render/png.d.ts +13 -0
- package/dist/src/render/png.js +145 -0
- package/dist/src/render/scrollbar.d.ts +8 -2
- package/dist/src/render/scrollbar.js +71 -14
- package/dist/src/render/snapshot.js +3 -1
- package/dist/src/renderer/default.d.ts +7 -0
- package/dist/src/renderer/default.js +11 -0
- package/dist/src/renderer/index.d.ts +8 -2
- package/dist/src/renderer/index.js +4 -2
- package/dist/src/renderer/node.d.ts +109 -0
- package/dist/src/renderer/node.js +165 -1
- package/dist/src/terminal/capabilities.d.ts +33 -0
- package/dist/src/terminal/capabilities.js +66 -0
- package/dist/src/terminal/clipboard.d.ts +9 -0
- package/dist/src/terminal/clipboard.js +39 -0
- package/dist/src/terminal/io.d.ts +82 -0
- package/dist/src/terminal/io.js +155 -0
- package/dist/src/terminal/screen.d.ts +3 -10
- package/dist/src/terminal/screen.js +5 -28
- package/dist/src/terminal/stdin-router.d.ts +8 -5
- package/dist/src/terminal/stdin-router.js +22 -11
- package/dist/src/utils/node-map.d.ts +24 -0
- package/dist/src/utils/node-map.js +75 -0
- package/dist/src/vite/config.d.ts +62 -0
- package/dist/src/vite/config.js +191 -0
- package/docs/compatibility.md +67 -0
- package/docs/debug/devtools.md +40 -0
- package/docs/debug/svt.md +50 -0
- package/docs/distribution.md +106 -0
- package/docs/elements.md +120 -0
- package/docs/getting-started.md +177 -0
- package/docs/guide/css.md +187 -0
- package/docs/guide/input.md +143 -0
- package/docs/guide/layout.md +171 -0
- package/docs/guide/theming.md +94 -0
- package/docs/how-it-works.md +115 -0
- package/docs/inline-mode.md +77 -0
- package/docs/layout.md +112 -0
- package/docs/motion.md +91 -0
- package/docs/reference/README.md +65 -0
- package/docs/reference/css/properties/border-corner.md +82 -0
- package/docs/reference/css/properties/border-style.md +168 -0
- package/docs/reference.md +227 -0
- package/docs/selectors.md +80 -0
- package/docs/terminal-css.md +149 -0
- package/docs/terminals.md +83 -0
- package/package.json +28 -7
package/dist/src/index.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
import { mount, unmount } from 'svelte/renderer';
|
|
1
2
|
import { TermNode } from './renderer/index.js';
|
|
3
|
+
import { hasBooleanAttribute } from './renderer/node.js';
|
|
4
|
+
import { AnimationClock } from './render/animation-clock.js';
|
|
5
|
+
import { getKeyframes } from './css/animation.js';
|
|
2
6
|
import renderer from './renderer/default.js';
|
|
3
7
|
import { CellBuffer } from './render/buffer.js';
|
|
4
8
|
import { diffBuffers } from './render/diff.js';
|
|
5
9
|
import { paint } from './render/paint.js';
|
|
6
10
|
import { parseCSS } from './css/parser.js';
|
|
11
|
+
import { DEFAULT_STYLESHEET } from './css/defaults.js';
|
|
7
12
|
import { resolveStyles, filterByMedia } from './css/compute.js';
|
|
13
|
+
import { collectVariables } from './css/variables.js';
|
|
8
14
|
import { resolveStylesIncremental } from './css/incremental.js';
|
|
9
15
|
import { computeLayout } from './layout/engine.js';
|
|
10
16
|
import { computeLayoutIncremental } from './layout/incremental.js';
|
|
@@ -16,33 +22,159 @@ import { parseMouseEvent } from './input/mouse.js';
|
|
|
16
22
|
import { hitTest } from './input/hit.js';
|
|
17
23
|
import { FocusManager } from './input/focus.js';
|
|
18
24
|
import { dispatchEvent } from './input/dispatch.js';
|
|
25
|
+
import { isCheckableInput, toggleCheckable } from './input/checkable.js';
|
|
26
|
+
import { toggleDetails } from './input/details.js';
|
|
27
|
+
import { cycleSelect } from './input/select.js';
|
|
28
|
+
import { labelledControl } from './input/label.js';
|
|
19
29
|
import { TextBuffer } from './components/text-buffer.js';
|
|
20
30
|
import { StdinRouter, matchOSC11, parseOSC11Scheme } from './terminal/stdin-router.js';
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
31
|
+
import { detectCapabilities, matchCPR, parseCPRRow } from './terminal/capabilities.js';
|
|
32
|
+
import { copyToClipboard } from './terminal/clipboard.js';
|
|
33
|
+
import { SelectionController, applySelectionOverlay } from './input/selection.js';
|
|
34
|
+
import { InlineScreen } from './render/inline.js';
|
|
35
|
+
import { GraphicsLayer } from './render/graphics-layer.js';
|
|
36
|
+
import { registerInlineHooks } from './framelog.js';
|
|
37
|
+
import { activeModal, withinSubtree } from './input/modal.js';
|
|
23
38
|
import * as ansi from './render/ansi.js';
|
|
24
|
-
import {
|
|
39
|
+
import { emitFocusCursor } from './render/cursor-emit.js';
|
|
40
|
+
import { enterFullscreen, exitFullscreen } from './terminal/screen.js';
|
|
41
|
+
import { ProcessIO } from './terminal/io.js';
|
|
42
|
+
/** Deduped so HMR re-evaluation doesn't accumulate copies. */
|
|
43
|
+
const registeredComponentCss = new Set();
|
|
44
|
+
/**
|
|
45
|
+
* Register a component's extracted CSS before run() is called. Bundled
|
|
46
|
+
* builds (`svelterm build`) and the dev-mode transform append a
|
|
47
|
+
* registration call to each compiled component so it carries its styles;
|
|
48
|
+
* run() falls back to the registry when no `css` option is given.
|
|
49
|
+
*/
|
|
50
|
+
export function registerComponentCss(css) {
|
|
51
|
+
if (css)
|
|
52
|
+
registeredComponentCss.add(css);
|
|
53
|
+
}
|
|
25
54
|
export function run(AppComponent, options) {
|
|
26
|
-
const
|
|
55
|
+
const io = options?.io ?? new ProcessIO();
|
|
56
|
+
// `fullscreen: false` without an explicit mode gets the inline
|
|
57
|
+
// renderer (what a main-buffer app actually needs). An explicit
|
|
58
|
+
// `mode` always wins — `mode: 'fullscreen', fullscreen: false` is
|
|
59
|
+
// full-viewport rendering without the alternate screen (embedded
|
|
60
|
+
// previews drive a virtual terminal that way).
|
|
61
|
+
const inline = options?.mode ? options.mode === 'inline' : options?.fullscreen === false;
|
|
62
|
+
const fullscreen = !inline && (options?.fullscreen ?? true);
|
|
27
63
|
const mouseEnabled = options?.mouse ?? true;
|
|
28
64
|
const debugEnabled = options?.debug ?? false;
|
|
29
65
|
const debugPort = options?.debugPort ?? 9444;
|
|
30
|
-
const
|
|
66
|
+
const exitOn = options?.exitOn ?? ['ctrl+c'];
|
|
67
|
+
const userCss = options?.css ?? [...registeredComponentCss].join('\n');
|
|
68
|
+
let stylesheet = parseCSS(DEFAULT_STYLESHEET + userCss);
|
|
69
|
+
// Console capture — only intercept when our IO owns the JS runtime's
|
|
70
|
+
// stdout/stderr (ProcessIO). With InProcessIO (browser, tests) console
|
|
71
|
+
// writes don't corrupt anything, so leave them alone. When intercepting,
|
|
72
|
+
// route to onConsole if provided; otherwise throw — silent suppression
|
|
73
|
+
// hides log calls and lengthens the feedback loop.
|
|
74
|
+
const onConsole = options?.onConsole;
|
|
75
|
+
const ownsStdio = io instanceof ProcessIO;
|
|
76
|
+
const levels = ['log', 'warn', 'error', 'info', 'debug'];
|
|
77
|
+
let restoreConsole = () => { };
|
|
78
|
+
if (ownsStdio) {
|
|
79
|
+
const originals = {
|
|
80
|
+
log: console.log.bind(console),
|
|
81
|
+
warn: console.warn.bind(console),
|
|
82
|
+
error: console.error.bind(console),
|
|
83
|
+
info: console.info.bind(console),
|
|
84
|
+
debug: console.debug.bind(console),
|
|
85
|
+
};
|
|
86
|
+
for (const level of levels) {
|
|
87
|
+
;
|
|
88
|
+
console[level] = (...args) => {
|
|
89
|
+
if (onConsole) {
|
|
90
|
+
onConsole({
|
|
91
|
+
level,
|
|
92
|
+
args: args.map(a => typeof a === 'string' ? a : JSON.stringify(a, null, 2) ?? String(a)),
|
|
93
|
+
timestamp: Date.now(),
|
|
94
|
+
});
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
throw new Error(`console.${level} would corrupt the terminal — pass an onConsole option to run() to route log output, or remove the call`);
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
restoreConsole = () => {
|
|
101
|
+
for (const level of levels) {
|
|
102
|
+
console[level] = originals[level];
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
31
106
|
// Render context tracks mutations and determines minimum work
|
|
32
107
|
const ctx = new RenderContext();
|
|
33
108
|
const root = new TermNode('element', 'root');
|
|
34
109
|
root.ctx = ctx;
|
|
35
|
-
// Color scheme detection — updated by polling
|
|
36
|
-
|
|
110
|
+
// Color scheme detection — updated by polling unless the host
|
|
111
|
+
// overrode it via options.colorScheme.
|
|
112
|
+
const colorSchemeOverride = options?.colorScheme;
|
|
113
|
+
let detectedScheme = colorSchemeOverride ?? 'dark';
|
|
37
114
|
// Wire schedule callback (defined below, hoisted via closure)
|
|
38
115
|
ctx.onScheduleRender = () => scheduleRender();
|
|
39
116
|
// Persisted render state
|
|
117
|
+
/** What the terminal currently shows (selection overlay included). */
|
|
40
118
|
let prevBuffer = null;
|
|
119
|
+
/** The last paint without the selection overlay — diff/extraction base. */
|
|
120
|
+
let prevClean = null;
|
|
121
|
+
const selection = new SelectionController(() => prevClean);
|
|
122
|
+
const inlineScreen = new InlineScreen();
|
|
123
|
+
if (inline) {
|
|
124
|
+
registerInlineHooks(root, { releaseTop: n => inlineScreen.releaseTop(n) });
|
|
125
|
+
}
|
|
126
|
+
/** Learn where the inline zone starts so mouse coordinates can map. */
|
|
127
|
+
const queryInlineOrigin = () => {
|
|
128
|
+
// The terminal reports where the cursor is, which is wherever the
|
|
129
|
+
// last render left it *within* the zone — snapshot that at the
|
|
130
|
+
// moment the query bytes go out (queries queue behind others).
|
|
131
|
+
let cursorRowAtQuery = 0;
|
|
132
|
+
router.query('\x1b[6n', matchCPR, 200, () => { cursorRowAtQuery = inlineScreen.cursorZoneRow(); }).then(reply => {
|
|
133
|
+
const row = parseCPRRow(reply);
|
|
134
|
+
if (row !== null)
|
|
135
|
+
inlineScreen.setOriginRow(Math.max(1, row - cursorRowAtQuery));
|
|
136
|
+
}).catch(() => { });
|
|
137
|
+
};
|
|
138
|
+
/** The buffer as displayed: the clean paint plus any selection. */
|
|
139
|
+
const overlayed = (buffer) => {
|
|
140
|
+
const range = selection.range();
|
|
141
|
+
if (!range)
|
|
142
|
+
return buffer;
|
|
143
|
+
const display = buffer.clone();
|
|
144
|
+
applySelectionOverlay(display, range);
|
|
145
|
+
return display;
|
|
146
|
+
};
|
|
147
|
+
/** Re-diff after a selection change without repainting the tree. */
|
|
148
|
+
const redrawSelection = () => {
|
|
149
|
+
if (!prevClean)
|
|
150
|
+
return;
|
|
151
|
+
const display = overlayed(prevClean);
|
|
152
|
+
const output = diffBuffers(prevBuffer, display);
|
|
153
|
+
if (output.length > 0)
|
|
154
|
+
writeOutput(output);
|
|
155
|
+
prevBuffer = display;
|
|
156
|
+
};
|
|
41
157
|
let lastStyles;
|
|
42
158
|
let lastFilteredStylesheet = null;
|
|
43
159
|
let lastLayout;
|
|
44
160
|
let renderScheduled = false;
|
|
45
161
|
let initialRegistrationDone = false;
|
|
162
|
+
// CSS animations — reapply the current keyframe and repaint while live
|
|
163
|
+
const animationClock = new AnimationClock();
|
|
164
|
+
animationClock.onFrame = () => {
|
|
165
|
+
if (!lastStyles)
|
|
166
|
+
return;
|
|
167
|
+
const dirty = animationClock.apply(lastStyles);
|
|
168
|
+
if (dirty.length === 0)
|
|
169
|
+
return;
|
|
170
|
+
for (const { node, touchesLayout } of dirty) {
|
|
171
|
+
if (touchesLayout)
|
|
172
|
+
ctx.queue.enqueueLayoutBubble(node);
|
|
173
|
+
else
|
|
174
|
+
ctx.queue.enqueuePaintOnly(node);
|
|
175
|
+
}
|
|
176
|
+
scheduleRender();
|
|
177
|
+
};
|
|
46
178
|
const scheduleRender = () => {
|
|
47
179
|
if (renderScheduled)
|
|
48
180
|
return;
|
|
@@ -54,25 +186,46 @@ export function run(AppComponent, options) {
|
|
|
54
186
|
};
|
|
55
187
|
const processQueue = () => {
|
|
56
188
|
const snap = ctx.queue.snapshot();
|
|
57
|
-
|
|
189
|
+
const dirty = snap.paintOnly.size > 0 || snap.styleResolve.size > 0
|
|
190
|
+
|| snap.layoutSubtree.size > 0 || snap.layoutBubble.size > 0;
|
|
191
|
+
if (inline) {
|
|
192
|
+
// The live area is content-sized, so any change can move
|
|
193
|
+
// everything — always render fully.
|
|
194
|
+
if (snap.fullRecompute || dirty || !lastStyles)
|
|
195
|
+
fullRender();
|
|
196
|
+
}
|
|
197
|
+
else if (snap.fullRecompute || !lastStyles || !lastLayout) {
|
|
58
198
|
// Full recompute — initial render, resize, or CSS reload
|
|
59
199
|
fullRender();
|
|
60
200
|
}
|
|
61
|
-
else if (
|
|
62
|
-
|| snap.layoutSubtree.size > 0 || snap.layoutBubble.size > 0) {
|
|
201
|
+
else if (dirty) {
|
|
63
202
|
// Incremental render
|
|
64
203
|
incrementalRender(snap);
|
|
65
204
|
}
|
|
66
205
|
};
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
206
|
+
// Synchronized output (DEC 2026) is on until detection says otherwise —
|
|
207
|
+
// terminals that don't know the mode ignore it.
|
|
208
|
+
let syncOutput = true;
|
|
209
|
+
const writeOutput = (data) => {
|
|
210
|
+
io.write(syncOutput ? ansi.beginSyncUpdate() + data + ansi.endSyncUpdate() : data);
|
|
211
|
+
};
|
|
212
|
+
// Kitty-graphics layer — off until capability detection turns it on.
|
|
213
|
+
const graphicsLayer = new GraphicsLayer();
|
|
214
|
+
let graphicsEnabled = false;
|
|
215
|
+
/** The per-frame tail written after the cell diff: cursor + images. */
|
|
216
|
+
const frameTail = () => {
|
|
217
|
+
const cursor = emitFocusCursor(root, focusManager.focused);
|
|
218
|
+
return graphicsEnabled ? cursor + graphicsLayer.render(root, lastLayout) : cursor;
|
|
219
|
+
};
|
|
220
|
+
/** Resolve styles + layout for the current terminal size. */
|
|
221
|
+
const resolveForRender = (size) => {
|
|
70
222
|
root.attributes.set('data-width', String(size.width));
|
|
71
223
|
root.attributes.set('data-height', String(size.height));
|
|
72
|
-
const buffer = new CellBuffer(size.width, size.height);
|
|
73
224
|
const media = { colorScheme: detectedScheme, displayMode: 'terminal', width: size.width, height: size.height };
|
|
74
225
|
lastFilteredStylesheet = stylesheet ? filterByMedia(stylesheet, media) : null;
|
|
75
|
-
|
|
226
|
+
// Passing `media` here threads colorScheme into computeStyle so
|
|
227
|
+
// light-dark() resolves against the active scheme.
|
|
228
|
+
lastStyles = lastFilteredStylesheet ? resolveStyles(root, lastFilteredStylesheet, media) : undefined;
|
|
76
229
|
// Ensure root style has terminal dimensions for percentage resolution
|
|
77
230
|
if (lastStyles) {
|
|
78
231
|
const rootStyle = lastStyles.get(root.id);
|
|
@@ -81,28 +234,92 @@ export function run(AppComponent, options) {
|
|
|
81
234
|
rootStyle.height = size.height;
|
|
82
235
|
}
|
|
83
236
|
}
|
|
237
|
+
if (lastStyles && lastFilteredStylesheet) {
|
|
238
|
+
animationClock.sync(root, lastStyles, getKeyframes(lastFilteredStylesheet), {
|
|
239
|
+
variables: collectVariables(root, lastFilteredStylesheet),
|
|
240
|
+
scheme: detectedScheme,
|
|
241
|
+
});
|
|
242
|
+
animationClock.syncTransitions(root, lastStyles);
|
|
243
|
+
animationClock.apply(lastStyles);
|
|
244
|
+
}
|
|
84
245
|
lastLayout = lastStyles ? computeLayout(root, lastStyles, size.width, size.height) : undefined;
|
|
85
|
-
if (lastLayout)
|
|
246
|
+
if (lastLayout) {
|
|
86
247
|
syncLayoutCache(root, lastLayout);
|
|
248
|
+
clampScrollPositions(root, lastLayout, io);
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
const fullRender = () => {
|
|
252
|
+
if (inline) {
|
|
253
|
+
inlineRender();
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const size = io.getSize();
|
|
257
|
+
const buffer = new CellBuffer(size.width, size.height);
|
|
258
|
+
resolveForRender(size);
|
|
87
259
|
paint(root, buffer, lastStyles, lastLayout);
|
|
88
|
-
|
|
260
|
+
prevClean = buffer;
|
|
261
|
+
const display = overlayed(buffer);
|
|
262
|
+
const output = diffBuffers(prevBuffer, display) + frameTail();
|
|
89
263
|
if (output.length > 0)
|
|
90
264
|
writeOutput(output);
|
|
91
|
-
prevBuffer =
|
|
265
|
+
prevBuffer = display;
|
|
92
266
|
// Register focusable elements after initial render
|
|
93
267
|
if (!initialRegistrationDone) {
|
|
94
268
|
registerFocusableNodes(root, focusManager);
|
|
95
269
|
initialRegistrationDone = true;
|
|
96
270
|
}
|
|
97
271
|
};
|
|
272
|
+
/**
|
|
273
|
+
* Inline mode always renders fully: the live area is content-sized
|
|
274
|
+
* (clamped to the terminal height — archive to keep it short) and the
|
|
275
|
+
* InlineScreen driver emits relative-movement diffs.
|
|
276
|
+
*/
|
|
277
|
+
const inlineRender = () => {
|
|
278
|
+
const size = io.getSize();
|
|
279
|
+
resolveForRender(size);
|
|
280
|
+
const rootBox = lastLayout?.get(root.id);
|
|
281
|
+
const extent = lastLayout && rootBox
|
|
282
|
+
? contentExtent(root, lastLayout, rootBox)
|
|
283
|
+
: { width: size.width, height: 1 };
|
|
284
|
+
const height = Math.max(1, Math.min(extent.height, size.height));
|
|
285
|
+
const buffer = new CellBuffer(size.width, height);
|
|
286
|
+
paint(root, buffer, lastStyles, lastLayout);
|
|
287
|
+
prevClean = buffer;
|
|
288
|
+
let output = inlineScreen.render(buffer);
|
|
289
|
+
const pos = focusManager.focused?.getCursorScreenPos();
|
|
290
|
+
if (pos && pos.inViewport && pos.y < height) {
|
|
291
|
+
output += inlineScreen.moveCursorTo(pos.x, pos.y)
|
|
292
|
+
+ ansi.setCursorShape('bar') + ansi.showCursor();
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
output += ansi.hideCursor() + ansi.resetCursorShape();
|
|
296
|
+
}
|
|
297
|
+
if (graphicsEnabled)
|
|
298
|
+
output += graphicsLayer.render(root, lastLayout);
|
|
299
|
+
if (output.length > 0)
|
|
300
|
+
writeOutput(output);
|
|
301
|
+
if (!initialRegistrationDone) {
|
|
302
|
+
registerFocusableNodes(root, focusManager);
|
|
303
|
+
initialRegistrationDone = true;
|
|
304
|
+
}
|
|
305
|
+
};
|
|
98
306
|
const incrementalRender = (snap) => {
|
|
99
|
-
const size =
|
|
307
|
+
const size = io.getSize();
|
|
100
308
|
// Mutable copies for promoted nodes during processing
|
|
101
309
|
const layoutSubtree = new Set(snap.layoutSubtree);
|
|
102
310
|
const layoutBubble = new Set(snap.layoutBubble);
|
|
103
311
|
// Step 1: Incremental style resolution
|
|
104
312
|
if (snap.styleResolve.size > 0 && lastStyles && lastFilteredStylesheet) {
|
|
105
|
-
|
|
313
|
+
const resolvedIds = new Set();
|
|
314
|
+
lastStyles = resolveStylesIncremental(root, lastFilteredStylesheet, lastStyles, snap.styleResolve, (nodeId) => { resolvedIds.add(nodeId); }, (node) => { layoutSubtree.add(node); }, detectedScheme);
|
|
315
|
+
// Newly mounted or restyled nodes may start/stop animations,
|
|
316
|
+
// and re-resolution resets styles to their base keyframe.
|
|
317
|
+
animationClock.sync(root, lastStyles, getKeyframes(lastFilteredStylesheet), {
|
|
318
|
+
variables: collectVariables(root, lastFilteredStylesheet),
|
|
319
|
+
scheme: detectedScheme,
|
|
320
|
+
});
|
|
321
|
+
animationClock.syncTransitions(root, lastStyles, resolvedIds);
|
|
322
|
+
animationClock.apply(lastStyles);
|
|
106
323
|
}
|
|
107
324
|
// Step 2: Incremental layout
|
|
108
325
|
if (layoutSubtree.size > 0 || layoutBubble.size > 0) {
|
|
@@ -125,21 +342,25 @@ export function run(AppComponent, options) {
|
|
|
125
342
|
dirtyPaintNodes.add(node);
|
|
126
343
|
}
|
|
127
344
|
const hasScroll = hasScrolledNode(root);
|
|
128
|
-
if (noLayoutChanges && !hasScroll && dirtyPaintNodes.size > 0 &&
|
|
129
|
-
const buffer =
|
|
345
|
+
if (noLayoutChanges && !hasScroll && dirtyPaintNodes.size > 0 && prevClean && lastStyles && lastLayout) {
|
|
346
|
+
const buffer = prevClean.clone();
|
|
130
347
|
paintNodes(dirtyPaintNodes, buffer, lastStyles, lastLayout, root);
|
|
131
|
-
|
|
348
|
+
prevClean = buffer;
|
|
349
|
+
const display = overlayed(buffer);
|
|
350
|
+
const output = diffBuffers(prevBuffer, display) + frameTail();
|
|
132
351
|
if (output.length > 0)
|
|
133
352
|
writeOutput(output);
|
|
134
|
-
prevBuffer =
|
|
353
|
+
prevBuffer = display;
|
|
135
354
|
}
|
|
136
355
|
else {
|
|
137
356
|
const buffer = new CellBuffer(size.width, size.height);
|
|
138
357
|
paint(root, buffer, lastStyles, lastLayout);
|
|
139
|
-
|
|
358
|
+
prevClean = buffer;
|
|
359
|
+
const display = overlayed(buffer);
|
|
360
|
+
const output = diffBuffers(prevBuffer, display) + frameTail();
|
|
140
361
|
if (output.length > 0)
|
|
141
362
|
writeOutput(output);
|
|
142
|
-
prevBuffer =
|
|
363
|
+
prevBuffer = display;
|
|
143
364
|
}
|
|
144
365
|
};
|
|
145
366
|
const focusManager = new FocusManager();
|
|
@@ -167,28 +388,56 @@ export function run(AppComponent, options) {
|
|
|
167
388
|
unregisterFocusableNodes(child, focusManager);
|
|
168
389
|
child.cleanup();
|
|
169
390
|
};
|
|
170
|
-
enableRawMode();
|
|
391
|
+
io.enableRawMode();
|
|
171
392
|
if (fullscreen)
|
|
172
|
-
enterFullscreen();
|
|
393
|
+
enterFullscreen(io);
|
|
394
|
+
// Cursor visibility is owned at the run-lifecycle level, not by
|
|
395
|
+
// enterFullscreen — non-fullscreen runs need it hidden too. The
|
|
396
|
+
// post-paint emitter takes over from here, showing the cursor only
|
|
397
|
+
// when something asks for it (focused input, region cursor).
|
|
398
|
+
io.write(ansi.hideCursor());
|
|
173
399
|
// Write mode sequences directly — sync update wrapping can interfere
|
|
174
|
-
|
|
400
|
+
io.write(ansi.enableBracketedPaste());
|
|
401
|
+
// Kitty keyboard protocol: unsupported terminals ignore the push/pop
|
|
402
|
+
io.write(ansi.pushKittyKeyboard());
|
|
175
403
|
if (mouseEnabled)
|
|
176
|
-
|
|
404
|
+
io.write(ansi.enableMouse());
|
|
177
405
|
// Single stdin router — all input flows through here
|
|
178
|
-
const router = new StdinRouter();
|
|
406
|
+
const router = new StdinRouter(io);
|
|
179
407
|
const handleKeyData = (data) => {
|
|
180
408
|
const key = parseKeyEvent(data);
|
|
181
409
|
if (!key)
|
|
182
410
|
return;
|
|
183
411
|
if (key.ctrl && key.key === 'c') {
|
|
184
412
|
doCleanup();
|
|
185
|
-
process
|
|
413
|
+
if (typeof process !== 'undefined')
|
|
414
|
+
process.exit(0);
|
|
415
|
+
return;
|
|
186
416
|
}
|
|
187
|
-
if (key.ctrl && key.key === '
|
|
417
|
+
if (key.ctrl && key.key === 'd' && exitOn.includes('ctrl+d')) {
|
|
188
418
|
doCleanup();
|
|
189
|
-
|
|
419
|
+
if (typeof process !== 'undefined')
|
|
420
|
+
process.exit(0);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (key.ctrl && key.key === 'z') {
|
|
424
|
+
suspend();
|
|
190
425
|
return;
|
|
191
426
|
}
|
|
427
|
+
// An open <dialog> captures keys: Escape closes it, Tab traps inside
|
|
428
|
+
const modal = activeModal(root);
|
|
429
|
+
focusManager.setScope(modal);
|
|
430
|
+
if (modal && key.key === 'Escape') {
|
|
431
|
+
modal.attributes.delete('open');
|
|
432
|
+
ctx.onRemoveAttribute(modal, 'open');
|
|
433
|
+
dispatchEvent(modal, 'close');
|
|
434
|
+
focusManager.setScope(null);
|
|
435
|
+
scheduleRender();
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (modal && focusManager.focused && !withinSubtree(focusManager.focused, modal)) {
|
|
439
|
+
focusManager.focusNext(); // pull focus into the modal
|
|
440
|
+
}
|
|
192
441
|
if (key.key === 'Tab' && key.shift) {
|
|
193
442
|
focusManager.focusPrevious();
|
|
194
443
|
scheduleRender();
|
|
@@ -201,6 +450,8 @@ export function run(AppComponent, options) {
|
|
|
201
450
|
}
|
|
202
451
|
if (key.key === 'Enter' && focusManager.focused) {
|
|
203
452
|
const target = focusManager.focused;
|
|
453
|
+
if (hasBooleanAttribute(target, 'disabled'))
|
|
454
|
+
return;
|
|
204
455
|
const event = dispatchEvent(target, 'click');
|
|
205
456
|
// Default action: open links in browser (unless preventDefault was called)
|
|
206
457
|
if (!event.defaultPrevented && target.tag === 'a') {
|
|
@@ -208,12 +459,35 @@ export function run(AppComponent, options) {
|
|
|
208
459
|
if (href)
|
|
209
460
|
openUrl(href);
|
|
210
461
|
}
|
|
462
|
+
if (!event.defaultPrevented && target.tag === 'summary')
|
|
463
|
+
toggleDetails(target);
|
|
464
|
+
if (!event.defaultPrevented && target.tag === 'select')
|
|
465
|
+
cycleSelect(target, 1);
|
|
466
|
+
scheduleRender();
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
// Space toggles a focused checkbox or selects a focused radio
|
|
470
|
+
if (key.key === ' ' && focusManager.focused && isCheckableInput(focusManager.focused)) {
|
|
471
|
+
toggleCheckable(focusManager.focused);
|
|
211
472
|
scheduleRender();
|
|
212
473
|
return;
|
|
213
474
|
}
|
|
475
|
+
// A focused select cycles its options (popup-less interaction)
|
|
476
|
+
if (focusManager.focused?.tag === 'select') {
|
|
477
|
+
if (key.key === 'ArrowUp') {
|
|
478
|
+
cycleSelect(focusManager.focused, -1);
|
|
479
|
+
scheduleRender();
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if (key.key === 'ArrowDown' || key.key === ' ') {
|
|
483
|
+
cycleSelect(focusManager.focused, 1);
|
|
484
|
+
scheduleRender();
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
214
488
|
// Text input for focused input/textarea
|
|
215
489
|
const focused = focusManager.focused;
|
|
216
|
-
if (focused && (focused.tag === 'input' || focused.tag === 'textarea')) {
|
|
490
|
+
if (focused && (focused.tag === 'input' || focused.tag === 'textarea') && !isCheckableInput(focused)) {
|
|
217
491
|
if (!focused.textBuffer)
|
|
218
492
|
focused.textBuffer = new TextBuffer(focused.attributes.get('value') ?? '');
|
|
219
493
|
if (focused.textBuffer.handleKey(key)) {
|
|
@@ -236,10 +510,18 @@ export function run(AppComponent, options) {
|
|
|
236
510
|
}
|
|
237
511
|
};
|
|
238
512
|
const handleMouseData = (data) => {
|
|
239
|
-
|
|
513
|
+
let mouse = parseMouseEvent(data);
|
|
240
514
|
if (!mouse)
|
|
241
515
|
return;
|
|
242
|
-
|
|
516
|
+
if (inline) {
|
|
517
|
+
// Mouse rows are screen-absolute; the zone origin comes from a
|
|
518
|
+
// CPR query. Events above the zone (shell history) are ignored.
|
|
519
|
+
const zoneRow = inlineScreen.screenRowToZone(mouse.row, io.getSize().height);
|
|
520
|
+
if (zoneRow === null)
|
|
521
|
+
return;
|
|
522
|
+
mouse = { ...mouse, row: zoneRow };
|
|
523
|
+
}
|
|
524
|
+
handleMouse(mouse, root, lastLayout, focusManager, scheduleRender, lastStyles, ctx, io, selection, redrawSelection);
|
|
243
525
|
};
|
|
244
526
|
const handlePaste = (text) => {
|
|
245
527
|
const focused = focusManager.focused;
|
|
@@ -262,27 +544,70 @@ export function run(AppComponent, options) {
|
|
|
262
544
|
}
|
|
263
545
|
};
|
|
264
546
|
router.start({ onKey: handleKeyData, onMouse: handleMouseData, onPaste: handlePaste });
|
|
265
|
-
// Debug server (opt-in)
|
|
547
|
+
// Debug server (opt-in, dynamically imported to avoid ws dependency in browser)
|
|
266
548
|
let debugServer = null;
|
|
267
549
|
let consoleDomain = null;
|
|
268
550
|
if (debugEnabled) {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
551
|
+
Promise.all([
|
|
552
|
+
import('./debug/server.js'),
|
|
553
|
+
import('./debug/console.js'),
|
|
554
|
+
import('./debug/dom.js'),
|
|
555
|
+
import('./debug/css.js'),
|
|
556
|
+
]).then(([{ DebugServer }, { ConsoleDomain }, { DomDomain }, { CssDomain }]) => {
|
|
557
|
+
debugServer = new DebugServer(debugPort);
|
|
558
|
+
consoleDomain = new ConsoleDomain(debugServer);
|
|
559
|
+
const debugCtx = {
|
|
560
|
+
root,
|
|
561
|
+
styles: () => lastStyles,
|
|
562
|
+
layout: () => lastLayout,
|
|
563
|
+
requestRender: () => { ctx.queue.setFullRecompute(); scheduleRender(); },
|
|
564
|
+
};
|
|
565
|
+
debugServer.registerDomain('Console', consoleDomain);
|
|
566
|
+
debugServer.registerDomain('DOM', new DomDomain(debugCtx));
|
|
567
|
+
debugServer.registerDomain('CSS', new CssDomain(debugCtx));
|
|
568
|
+
consoleDomain.start();
|
|
569
|
+
debugServer.start();
|
|
570
|
+
});
|
|
274
571
|
}
|
|
275
572
|
// Serialised color scheme detection via router query
|
|
276
573
|
const detectScheme = async () => {
|
|
277
574
|
const result = await router.query('\x1b]11;?\x07', matchOSC11, 200);
|
|
278
575
|
return result ? parseOSC11Scheme(result) : 'dark';
|
|
279
576
|
};
|
|
280
|
-
// Render immediately with default scheme
|
|
577
|
+
// Render immediately with default scheme. Seed a Svelte context so
|
|
578
|
+
// descendant components can detect they're rendered in the svelterm
|
|
579
|
+
// target (vs plain browser-Svelte) without resorting to globals —
|
|
580
|
+
// important for components like EmbeddedTerminal that branch their
|
|
581
|
+
// render path. Browser-Svelte mounts have no such key, so a
|
|
582
|
+
// `getContext` call there returns undefined and the component
|
|
583
|
+
// defaults to the browser path.
|
|
281
584
|
ctx.queue.setFullRecompute();
|
|
282
|
-
const
|
|
585
|
+
const app = mount(AppComponent, {
|
|
586
|
+
renderer,
|
|
587
|
+
target: root,
|
|
588
|
+
props: options?.props ?? {},
|
|
589
|
+
context: new Map([[Symbol.for('@svelterm/target'), 'terminal']]),
|
|
590
|
+
});
|
|
591
|
+
const svUnmount = () => void unmount(app);
|
|
592
|
+
// Collect CSS from injected <style> elements (css: 'injected' mode).
|
|
593
|
+
// Skipped when the host passed an explicit `css` option.
|
|
594
|
+
if (!userCss) {
|
|
595
|
+
const injectedCss = collectInjectedCss(root);
|
|
596
|
+
if (injectedCss) {
|
|
597
|
+
stylesheet = parseCSS(DEFAULT_STYLESHEET + injectedCss);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
283
600
|
scheduleRender();
|
|
284
|
-
|
|
285
|
-
|
|
601
|
+
io.onResize(() => {
|
|
602
|
+
ctx.onResize();
|
|
603
|
+
prevBuffer = null;
|
|
604
|
+
scheduleRender();
|
|
605
|
+
// Rewrap may have moved the zone — re-learn where it starts
|
|
606
|
+
if (inline && io instanceof ProcessIO)
|
|
607
|
+
queryInlineOrigin();
|
|
608
|
+
});
|
|
609
|
+
// Detect color scheme in background and re-render if different.
|
|
610
|
+
// Skipped when the host pinned a scheme via options.colorScheme.
|
|
286
611
|
let pollRunning = true;
|
|
287
612
|
const pollScheme = async () => {
|
|
288
613
|
if (!pollRunning)
|
|
@@ -291,7 +616,8 @@ export function run(AppComponent, options) {
|
|
|
291
616
|
const scheme = await detectScheme();
|
|
292
617
|
if (scheme !== detectedScheme) {
|
|
293
618
|
detectedScheme = scheme;
|
|
294
|
-
|
|
619
|
+
const size = io.getSize();
|
|
620
|
+
lastFilteredStylesheet = stylesheet ? filterByMedia(stylesheet, { colorScheme: detectedScheme, displayMode: 'terminal', width: size.width, height: size.height }) : null;
|
|
295
621
|
ctx.onResize();
|
|
296
622
|
prevBuffer = null;
|
|
297
623
|
scheduleRender();
|
|
@@ -303,61 +629,209 @@ export function run(AppComponent, options) {
|
|
|
303
629
|
if (pollRunning)
|
|
304
630
|
setTimeout(pollScheme, 1000);
|
|
305
631
|
};
|
|
306
|
-
|
|
307
|
-
|
|
632
|
+
if (!colorSchemeOverride)
|
|
633
|
+
pollScheme();
|
|
634
|
+
// Detect terminal capabilities in the background: colour depth for
|
|
635
|
+
// SGR quantization, DEC 2026 support for frame batching. Only the
|
|
636
|
+
// real terminal answers queries; embedded IO keeps the defaults.
|
|
637
|
+
if (options?.colorDepth) {
|
|
638
|
+
ansi.setColorDepth(options.colorDepth);
|
|
639
|
+
}
|
|
640
|
+
else if (io instanceof ProcessIO) {
|
|
641
|
+
if (inline)
|
|
642
|
+
queryInlineOrigin();
|
|
643
|
+
detectCapabilities(router).then(caps => {
|
|
644
|
+
syncOutput = caps.syncOutput;
|
|
645
|
+
// Crisp images on kitty-graphics terminals; half-blocks stay
|
|
646
|
+
// in the buffer as the fallback and layout truth.
|
|
647
|
+
if (caps.graphics && !graphicsEnabled) {
|
|
648
|
+
graphicsEnabled = true;
|
|
649
|
+
scheduleRender();
|
|
650
|
+
}
|
|
651
|
+
if (caps.colorDepth === ansi.getColorDepth())
|
|
652
|
+
return;
|
|
653
|
+
ansi.setColorDepth(caps.colorDepth);
|
|
654
|
+
// Full repaint so every cell re-emits at the new depth
|
|
655
|
+
ctx.onResize();
|
|
656
|
+
prevBuffer = null;
|
|
657
|
+
scheduleRender();
|
|
658
|
+
}).catch(() => { });
|
|
659
|
+
}
|
|
308
660
|
const doCleanup = () => {
|
|
309
661
|
pollRunning = false;
|
|
662
|
+
animationClock.stop();
|
|
310
663
|
router.stop();
|
|
311
664
|
consoleDomain?.stop();
|
|
312
665
|
debugServer?.stop();
|
|
313
|
-
|
|
666
|
+
restoreConsole?.();
|
|
667
|
+
if (graphicsEnabled)
|
|
668
|
+
io.write(graphicsLayer.clear());
|
|
669
|
+
svUnmount();
|
|
670
|
+
if (mouseEnabled)
|
|
671
|
+
io.write(ansi.disableMouse());
|
|
672
|
+
io.write(ansi.popKittyKeyboard());
|
|
673
|
+
io.write(ansi.disableBracketedPaste());
|
|
674
|
+
if (fullscreen)
|
|
675
|
+
exitFullscreen(io);
|
|
676
|
+
if (inline) {
|
|
677
|
+
// Leave the rendered output in place; park the prompt below it.
|
|
678
|
+
io.write(inlineScreen.finish() + ansi.resetCursorShape());
|
|
679
|
+
}
|
|
680
|
+
else {
|
|
681
|
+
// Show cursor *after* exitFullscreen so it targets the main
|
|
682
|
+
// screen, not the alt screen we're leaving behind.
|
|
683
|
+
io.write(ansi.showCursor() + ansi.resetCursorShape());
|
|
684
|
+
}
|
|
685
|
+
io.disableRawMode();
|
|
686
|
+
io.dispose();
|
|
314
687
|
};
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
return () => {
|
|
322
|
-
if (cleaned)
|
|
688
|
+
/**
|
|
689
|
+
* Ctrl+Z: release the terminal without unmounting, stop, and pick up
|
|
690
|
+
* where we left off on `fg` — modes re-enter and everything repaints.
|
|
691
|
+
*/
|
|
692
|
+
const suspend = () => {
|
|
693
|
+
if (typeof process === 'undefined')
|
|
323
694
|
return;
|
|
324
|
-
|
|
325
|
-
|
|
695
|
+
if (graphicsEnabled)
|
|
696
|
+
io.write(graphicsLayer.clear());
|
|
326
697
|
if (mouseEnabled)
|
|
327
|
-
|
|
328
|
-
|
|
698
|
+
io.write(ansi.disableMouse());
|
|
699
|
+
io.write(ansi.popKittyKeyboard() + ansi.disableBracketedPaste());
|
|
329
700
|
if (fullscreen)
|
|
330
|
-
exitFullscreen();
|
|
331
|
-
|
|
701
|
+
exitFullscreen(io);
|
|
702
|
+
if (inline)
|
|
703
|
+
inlineScreen.reset();
|
|
704
|
+
io.write(ansi.showCursor() + ansi.resetCursorShape());
|
|
705
|
+
io.disableRawMode();
|
|
706
|
+
process.kill(process.pid, 'SIGTSTP');
|
|
707
|
+
};
|
|
708
|
+
const resume = () => {
|
|
709
|
+
io.enableRawMode();
|
|
710
|
+
if (fullscreen)
|
|
711
|
+
enterFullscreen(io);
|
|
712
|
+
io.write(ansi.hideCursor() + ansi.enableBracketedPaste() + ansi.pushKittyKeyboard());
|
|
713
|
+
if (mouseEnabled)
|
|
714
|
+
io.write(ansi.enableMouse());
|
|
715
|
+
ctx.onResize();
|
|
716
|
+
prevBuffer = null;
|
|
717
|
+
prevClean = null;
|
|
718
|
+
scheduleRender();
|
|
719
|
+
if (inline && io instanceof ProcessIO)
|
|
720
|
+
queryInlineOrigin();
|
|
332
721
|
};
|
|
722
|
+
if (typeof process !== 'undefined') {
|
|
723
|
+
process.on('SIGINT', () => { doCleanup(); process.exit(0); });
|
|
724
|
+
process.on('SIGTERM', () => { doCleanup(); process.exit(0); });
|
|
725
|
+
process.on('SIGCONT', resume);
|
|
726
|
+
}
|
|
727
|
+
const setColorScheme = (scheme) => {
|
|
728
|
+
// Host has spoken — silence the OSC-11 poller so it doesn't overwrite
|
|
729
|
+
// this value on its next 1s tick.
|
|
730
|
+
pollRunning = false;
|
|
731
|
+
if (scheme === detectedScheme)
|
|
732
|
+
return;
|
|
733
|
+
detectedScheme = scheme;
|
|
734
|
+
const size = io.getSize();
|
|
735
|
+
lastFilteredStylesheet = stylesheet ? filterByMedia(stylesheet, { colorScheme: detectedScheme, displayMode: 'terminal', width: size.width, height: size.height }) : null;
|
|
736
|
+
ctx.onResize();
|
|
737
|
+
prevBuffer = null;
|
|
738
|
+
scheduleRender();
|
|
739
|
+
};
|
|
740
|
+
return { cleanup: doCleanup, setColorScheme };
|
|
333
741
|
}
|
|
334
|
-
|
|
742
|
+
const SCROLLBAR_VISIBLE_MS = 600;
|
|
743
|
+
const SCROLLBAR_FADE_MS = 400;
|
|
744
|
+
const SCROLLBAR_FADE_FRAMES = 16;
|
|
745
|
+
const SCROLLBAR_TOTAL_MS = SCROLLBAR_VISIBLE_MS + SCROLLBAR_FADE_MS;
|
|
746
|
+
let lastHoveredId = -1;
|
|
747
|
+
function handleMouse(mouse, root, layout, focusManager, scheduleRender, lastStyles, ctx, io, selection, redrawSelection) {
|
|
335
748
|
if (!layout)
|
|
336
749
|
return;
|
|
337
|
-
// Handle hover —
|
|
750
|
+
// Handle hover — only update when the hovered element changes.
|
|
751
|
+
// Dragging with the left button extends the text selection.
|
|
338
752
|
if (mouse.type === 'motion') {
|
|
753
|
+
if (mouse.button === 'left' && selection.onMotion(mouse.col, mouse.row)) {
|
|
754
|
+
redrawSelection();
|
|
755
|
+
}
|
|
339
756
|
const target = hitTest(root, layout, mouse.col, mouse.row);
|
|
340
757
|
const hoveredId = target?.id ?? -1;
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
758
|
+
if (hoveredId !== lastHoveredId) {
|
|
759
|
+
updateHover(root, hoveredId, ctx);
|
|
760
|
+
lastHoveredId = hoveredId;
|
|
761
|
+
}
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
// Releasing the left button finishes a selection and copies it
|
|
765
|
+
if (mouse.type === 'release' && mouse.button === 'left') {
|
|
766
|
+
const text = selection.onRelease();
|
|
767
|
+
if (text)
|
|
768
|
+
copyToClipboard(text, data => io.write(data));
|
|
344
769
|
return;
|
|
345
770
|
}
|
|
346
771
|
if (mouse.type !== 'press' && mouse.type !== 'scroll')
|
|
347
772
|
return;
|
|
348
773
|
if (mouse.button === 'left') {
|
|
774
|
+
const hadSelection = selection.range() !== null;
|
|
775
|
+
selection.onPress(mouse.col, mouse.row);
|
|
776
|
+
if (hadSelection || selection.range() !== null)
|
|
777
|
+
redrawSelection();
|
|
349
778
|
const target = hitTest(root, layout, mouse.col, mouse.row);
|
|
350
779
|
if (target) {
|
|
351
|
-
//
|
|
780
|
+
// Disabled interactive elements swallow the click, as in browsers
|
|
781
|
+
if (FOCUSABLE_TAGS.has(target.tag ?? '') && hasBooleanAttribute(target, 'disabled'))
|
|
782
|
+
return;
|
|
352
783
|
if (FOCUSABLE_TAGS.has(target.tag ?? '')) {
|
|
353
784
|
focusManager.focusByNode(target);
|
|
354
785
|
}
|
|
786
|
+
if (isCheckableInput(target))
|
|
787
|
+
toggleCheckable(target);
|
|
355
788
|
const event = dispatchEvent(target, 'click', mouse);
|
|
356
789
|
if (!event.defaultPrevented && target.tag === 'a') {
|
|
357
790
|
const href = target.attributes.get('href');
|
|
358
791
|
if (href)
|
|
359
792
|
openUrl(href);
|
|
360
793
|
}
|
|
794
|
+
if (!event.defaultPrevented && target.tag === 'summary')
|
|
795
|
+
toggleDetails(target);
|
|
796
|
+
if (!event.defaultPrevented && target.tag === 'select')
|
|
797
|
+
cycleSelect(target, 1);
|
|
798
|
+
// Clicking a label activates its control, as in browsers
|
|
799
|
+
if (!event.defaultPrevented) {
|
|
800
|
+
const control = labelledControl(target);
|
|
801
|
+
if (control && control !== target && !hasBooleanAttribute(control, 'disabled')) {
|
|
802
|
+
if (FOCUSABLE_TAGS.has(control.tag ?? ''))
|
|
803
|
+
focusManager.focusByNode(control);
|
|
804
|
+
if (isCheckableInput(control))
|
|
805
|
+
toggleCheckable(control);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
scheduleRender();
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
else if (mouse.button === 'scrollLeft' || mouse.button === 'scrollRight') {
|
|
812
|
+
const target = hitTest(root, layout, mouse.col, mouse.row);
|
|
813
|
+
if (target) {
|
|
814
|
+
const scrollTarget = findScrollableAncestor(target, lastStyles);
|
|
815
|
+
if (scrollTarget) {
|
|
816
|
+
const box = layout.get(scrollTarget.id);
|
|
817
|
+
if (box) {
|
|
818
|
+
const { width: contentWidth } = contentExtent(scrollTarget, layout, box);
|
|
819
|
+
const viewportWidth = scrollTarget.tag === 'root'
|
|
820
|
+
? io.getSize().width
|
|
821
|
+
: box.width;
|
|
822
|
+
const maxScroll = Math.max(0, contentWidth - viewportWidth);
|
|
823
|
+
const delta = mouse.button === 'scrollLeft' ? -1 : 1;
|
|
824
|
+
scrollTarget.scrollLeft = Math.max(0, Math.min(scrollTarget.scrollLeft + delta, maxScroll));
|
|
825
|
+
scrollTarget.hScrollbarVisibleUntil = Date.now() + SCROLLBAR_TOTAL_MS;
|
|
826
|
+
const forceRepaint = () => { ctx.queue.setFullRecompute(); scheduleRender(); };
|
|
827
|
+
const frameInterval = SCROLLBAR_FADE_MS / SCROLLBAR_FADE_FRAMES;
|
|
828
|
+
for (let i = 0; i <= SCROLLBAR_FADE_FRAMES; i++) {
|
|
829
|
+
setTimeout(forceRepaint, SCROLLBAR_VISIBLE_MS + i * frameInterval);
|
|
830
|
+
}
|
|
831
|
+
ctx.onScroll(scrollTarget);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
dispatchEvent(target, 'scroll', mouse);
|
|
361
835
|
scheduleRender();
|
|
362
836
|
}
|
|
363
837
|
}
|
|
@@ -368,13 +842,19 @@ function handleMouse(mouse, root, layout, focusManager, scheduleRender, lastStyl
|
|
|
368
842
|
if (scrollTarget) {
|
|
369
843
|
const box = layout.get(scrollTarget.id);
|
|
370
844
|
if (box) {
|
|
371
|
-
const contentHeight = scrollTarget
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
const
|
|
376
|
-
const
|
|
845
|
+
const { height: contentHeight } = contentExtent(scrollTarget, layout, box);
|
|
846
|
+
const viewportHeight = scrollTarget.tag === 'root'
|
|
847
|
+
? io.getSize().height
|
|
848
|
+
: box.height;
|
|
849
|
+
const maxScroll = Math.max(0, contentHeight - viewportHeight);
|
|
850
|
+
const delta = mouse.button === 'scrollUp' ? -1 : 1;
|
|
377
851
|
scrollTarget.scrollTop = Math.max(0, Math.min(scrollTarget.scrollTop + delta, maxScroll));
|
|
852
|
+
scrollTarget.scrollbarVisibleUntil = Date.now() + SCROLLBAR_TOTAL_MS;
|
|
853
|
+
const forceRepaint = () => { ctx.queue.setFullRecompute(); scheduleRender(); };
|
|
854
|
+
const frameInterval = SCROLLBAR_FADE_MS / SCROLLBAR_FADE_FRAMES;
|
|
855
|
+
for (let i = 0; i <= SCROLLBAR_FADE_FRAMES; i++) {
|
|
856
|
+
setTimeout(forceRepaint, SCROLLBAR_VISIBLE_MS + i * frameInterval);
|
|
857
|
+
}
|
|
378
858
|
ctx.onScroll(scrollTarget);
|
|
379
859
|
}
|
|
380
860
|
}
|
|
@@ -383,10 +863,7 @@ function handleMouse(mouse, root, layout, focusManager, scheduleRender, lastStyl
|
|
|
383
863
|
}
|
|
384
864
|
}
|
|
385
865
|
}
|
|
386
|
-
|
|
387
|
-
process.stdout.on('resize', onResize);
|
|
388
|
-
}
|
|
389
|
-
const FOCUSABLE_TAGS = new Set(['button', 'input', 'textarea', 'a', 'select']);
|
|
866
|
+
const FOCUSABLE_TAGS = new Set(['button', 'input', 'textarea', 'a', 'select', 'summary']);
|
|
390
867
|
function registerFocusableNodes(node, focusManager) {
|
|
391
868
|
if (node.nodeType === 'element' && FOCUSABLE_TAGS.has(node.tag ?? '')) {
|
|
392
869
|
focusManager.register(node);
|
|
@@ -421,6 +898,9 @@ function updateHover(node, hoveredId, ctx) {
|
|
|
421
898
|
function findScrollableAncestor(node, styles) {
|
|
422
899
|
let current = node;
|
|
423
900
|
while (current) {
|
|
901
|
+
// Root element is implicitly scrollable (it's the viewport)
|
|
902
|
+
if (current.tag === 'root')
|
|
903
|
+
return current;
|
|
424
904
|
const style = styles?.get(current.id);
|
|
425
905
|
if (style && (style.overflow === 'scroll' || style.overflow === 'auto' || style.overflow === 'hidden')) {
|
|
426
906
|
return current;
|
|
@@ -429,6 +909,42 @@ function findScrollableAncestor(node, styles) {
|
|
|
429
909
|
}
|
|
430
910
|
return null;
|
|
431
911
|
}
|
|
912
|
+
/** Clamp scroll positions on all nodes after resize/relayout. */
|
|
913
|
+
function clampScrollPositions(node, layout, io) {
|
|
914
|
+
if (node.scrollTop !== 0 || node.scrollLeft !== 0) {
|
|
915
|
+
const box = layout.get(node.id);
|
|
916
|
+
if (box) {
|
|
917
|
+
const extent = contentExtent(node, layout, box);
|
|
918
|
+
const viewH = node.tag === 'root' ? io.getSize().height : box.height;
|
|
919
|
+
const viewW = node.tag === 'root' ? io.getSize().width : box.width;
|
|
920
|
+
const maxScrollY = Math.max(0, extent.height - viewH);
|
|
921
|
+
const maxScrollX = Math.max(0, extent.width - viewW);
|
|
922
|
+
if (node.scrollTop > maxScrollY)
|
|
923
|
+
node.scrollTop = maxScrollY;
|
|
924
|
+
if (node.scrollLeft > maxScrollX)
|
|
925
|
+
node.scrollLeft = maxScrollX;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
for (const child of node.children)
|
|
929
|
+
clampScrollPositions(child, layout, io);
|
|
930
|
+
}
|
|
931
|
+
/** Find the maximum content extent of all descendants relative to the ancestor's position. */
|
|
932
|
+
function contentExtent(node, layout, ancestorBox) {
|
|
933
|
+
let maxW = 0;
|
|
934
|
+
let maxH = 0;
|
|
935
|
+
function walk(n) {
|
|
936
|
+
const box = layout.get(n.id);
|
|
937
|
+
if (box) {
|
|
938
|
+
maxW = Math.max(maxW, box.x - ancestorBox.x + box.width);
|
|
939
|
+
maxH = Math.max(maxH, box.y - ancestorBox.y + box.height);
|
|
940
|
+
}
|
|
941
|
+
for (const child of n.children)
|
|
942
|
+
walk(child);
|
|
943
|
+
}
|
|
944
|
+
for (const child of node.children)
|
|
945
|
+
walk(child);
|
|
946
|
+
return { width: maxW, height: maxH };
|
|
947
|
+
}
|
|
432
948
|
function scrollIntoView(node, layout, styles, ctx) {
|
|
433
949
|
if (!layout)
|
|
434
950
|
return;
|
|
@@ -478,8 +994,21 @@ function findFirstElement(node) {
|
|
|
478
994
|
}
|
|
479
995
|
return node;
|
|
480
996
|
}
|
|
997
|
+
function collectInjectedCss(root) {
|
|
998
|
+
const parts = [];
|
|
999
|
+
for (const child of root.children) {
|
|
1000
|
+
if (child.tag === 'style') {
|
|
1001
|
+
parts.push(child.collectText());
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
return parts.join('\n');
|
|
1005
|
+
}
|
|
481
1006
|
export { TermNode } from './renderer/node.js';
|
|
482
1007
|
export { CellBuffer } from './render/buffer.js';
|
|
483
1008
|
export { parseCSS } from './css/parser.js';
|
|
484
1009
|
export { resolveStyles } from './css/compute.js';
|
|
485
1010
|
export { StdinRouter } from './terminal/stdin-router.js';
|
|
1011
|
+
export { ProcessIO, InProcessIO } from './terminal/io.js';
|
|
1012
|
+
export { copyToClipboard, osc52Copy } from './terminal/clipboard.js';
|
|
1013
|
+
export { FrameLog, createFrameLog } from './framelog.js';
|
|
1014
|
+
export { TestClock, systemClock } from './render/clock.js';
|