@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
|
@@ -1,19 +1,76 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
const V_THUMB = '┃';
|
|
2
|
+
const H_THUMB = '▁';
|
|
3
|
+
const SCROLLBAR_COLOR = { r: 180, g: 180, b: 180 };
|
|
4
|
+
/**
|
|
5
|
+
* Render a vertical scrollbar overlay on the rightmost column.
|
|
6
|
+
*/
|
|
7
|
+
export function renderScrollbar(buffer, viewportX, viewportY, viewportWidth, viewportHeight, contentHeight, scrollTop, opacity) {
|
|
8
|
+
if (contentHeight <= viewportHeight || opacity <= 0)
|
|
5
9
|
return;
|
|
6
|
-
const col =
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
const col = viewportX + viewportWidth - 1;
|
|
11
|
+
const thumbSize = Math.max(1, Math.round(viewportHeight * (viewportHeight / contentHeight)));
|
|
12
|
+
const maxScroll = contentHeight - viewportHeight;
|
|
13
|
+
const thumbPos = Math.round((scrollTop / maxScroll) * (viewportHeight - thumbSize));
|
|
14
|
+
for (let row = 0; row < viewportHeight; row++) {
|
|
15
|
+
const y = viewportY + row;
|
|
12
16
|
const isThumb = row >= thumbPos && row < thumbPos + thumbSize;
|
|
13
|
-
buffer.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
const existing = buffer.getCell(col, y);
|
|
18
|
+
const bg = parseColor(existing?.bg ?? 'default');
|
|
19
|
+
const fg = lerpColor(bg, SCROLLBAR_COLOR, isThumb ? opacity : opacity * 0.3);
|
|
20
|
+
buffer.setCell(col, y, {
|
|
21
|
+
char: isThumb ? V_THUMB : ' ',
|
|
22
|
+
fg: isThumb ? toHex(fg) : undefined,
|
|
23
|
+
dim: false,
|
|
17
24
|
});
|
|
18
25
|
}
|
|
19
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Render a horizontal scrollbar overlay on the bottom row.
|
|
29
|
+
*/
|
|
30
|
+
export function renderHScrollbar(buffer, viewportX, viewportY, viewportWidth, viewportHeight, contentWidth, scrollLeft, opacity) {
|
|
31
|
+
if (contentWidth <= viewportWidth || opacity <= 0)
|
|
32
|
+
return;
|
|
33
|
+
const row = viewportY + viewportHeight - 1;
|
|
34
|
+
const thumbSize = Math.max(1, Math.round(viewportWidth * (viewportWidth / contentWidth)));
|
|
35
|
+
const maxScroll = contentWidth - viewportWidth;
|
|
36
|
+
const thumbPos = Math.round((scrollLeft / maxScroll) * (viewportWidth - thumbSize));
|
|
37
|
+
for (let col = 0; col < viewportWidth; col++) {
|
|
38
|
+
const x = viewportX + col;
|
|
39
|
+
const isThumb = col >= thumbPos && col < thumbPos + thumbSize;
|
|
40
|
+
const existing = buffer.getCell(x, row);
|
|
41
|
+
const bg = parseColor(existing?.bg ?? 'default');
|
|
42
|
+
const fg = lerpColor(bg, SCROLLBAR_COLOR, isThumb ? opacity : opacity * 0.3);
|
|
43
|
+
buffer.setCell(x, row, {
|
|
44
|
+
char: isThumb ? H_THUMB : ' ',
|
|
45
|
+
fg: isThumb ? toHex(fg) : undefined,
|
|
46
|
+
dim: false,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function parseColor(color) {
|
|
51
|
+
if (color.startsWith('#')) {
|
|
52
|
+
const hex = color.length === 4
|
|
53
|
+
? `#${color[1]}${color[1]}${color[2]}${color[2]}${color[3]}${color[3]}`
|
|
54
|
+
: color;
|
|
55
|
+
return {
|
|
56
|
+
r: parseInt(hex.slice(1, 3), 16),
|
|
57
|
+
g: parseInt(hex.slice(3, 5), 16),
|
|
58
|
+
b: parseInt(hex.slice(5, 7), 16),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
// Default/unknown — assume dark background
|
|
62
|
+
return { r: 13, g: 17, b: 23 };
|
|
63
|
+
}
|
|
64
|
+
function lerpColor(a, b, t) {
|
|
65
|
+
return {
|
|
66
|
+
r: Math.round(a.r + (b.r - a.r) * t),
|
|
67
|
+
g: Math.round(a.g + (b.g - a.g) * t),
|
|
68
|
+
b: Math.round(a.b + (b.b - a.b) * t),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function toHex(c) {
|
|
72
|
+
return `#${hex(c.r)}${hex(c.g)}${hex(c.b)}`;
|
|
73
|
+
}
|
|
74
|
+
function hex(n) {
|
|
75
|
+
return n.toString(16).padStart(2, '0');
|
|
76
|
+
}
|
|
@@ -53,6 +53,8 @@ export function bufferToStyledText(buffer) {
|
|
|
53
53
|
parts.push('underline');
|
|
54
54
|
if (cell.dim)
|
|
55
55
|
parts.push('dim');
|
|
56
|
+
if (cell.inverse)
|
|
57
|
+
parts.push('inverse');
|
|
56
58
|
styles.push(`[${col}:${parts.join(',')}]`);
|
|
57
59
|
}
|
|
58
60
|
}
|
|
@@ -109,7 +111,7 @@ export function bufferToSvg(buffer, options) {
|
|
|
109
111
|
function hasStyle(cell) {
|
|
110
112
|
return cell.fg !== 'default' || cell.bg !== 'default'
|
|
111
113
|
|| cell.bold || cell.italic || cell.underline
|
|
112
|
-
|| cell.strikethrough || cell.dim;
|
|
114
|
+
|| cell.strikethrough || cell.dim || cell.inverse;
|
|
113
115
|
}
|
|
114
116
|
function colorToHex(color) {
|
|
115
117
|
if (color.startsWith('#'))
|
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
import { createTermRenderer } from './index.js';
|
|
2
2
|
declare const renderer: ReturnType<typeof createTermRenderer>;
|
|
3
3
|
export default renderer;
|
|
4
|
+
/**
|
|
5
|
+
* True inside a Svelte component being rendered into svelterm's cell tree
|
|
6
|
+
* (i.e. mounted via svelterm's `run()`); false for plain browser-Svelte
|
|
7
|
+
* mounts. Components that render structurally different output per target
|
|
8
|
+
* use this to dispatch.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isTerminal(): boolean;
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
import { getContext } from 'svelte';
|
|
1
2
|
import { createTermRenderer } from './index.js';
|
|
2
3
|
const renderer = createTermRenderer();
|
|
3
4
|
export default renderer;
|
|
5
|
+
const TARGET_KEY = Symbol.for('@svelterm/target');
|
|
6
|
+
/**
|
|
7
|
+
* True inside a Svelte component being rendered into svelterm's cell tree
|
|
8
|
+
* (i.e. mounted via svelterm's `run()`); false for plain browser-Svelte
|
|
9
|
+
* mounts. Components that render structurally different output per target
|
|
10
|
+
* use this to dispatch.
|
|
11
|
+
*/
|
|
12
|
+
export function isTerminal() {
|
|
13
|
+
return getContext(TARGET_KEY) === 'terminal';
|
|
14
|
+
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { createRenderer as svelteCreateRenderer } from 'svelte/renderer';
|
|
2
2
|
import { TermNode } from './node.js';
|
|
3
|
-
|
|
3
|
+
type TermNodes = {
|
|
4
|
+
fragment: TermNode;
|
|
5
|
+
element: TermNode;
|
|
6
|
+
text: TermNode;
|
|
7
|
+
comment: TermNode;
|
|
8
|
+
};
|
|
9
|
+
export declare function createTermRenderer(): ReturnType<typeof svelteCreateRenderer<TermNodes>>;
|
|
4
10
|
/**
|
|
5
11
|
* Keep the custom renderer active globally so Svelte's effects
|
|
6
12
|
* use our renderer methods (setText, setAttribute, etc.) instead
|
|
@@ -8,4 +14,4 @@ export declare function createTermRenderer(): ReturnType<typeof svelteCreateRend
|
|
|
8
14
|
*
|
|
9
15
|
* Call this AFTER renderer.render() which pops the renderer.
|
|
10
16
|
*/
|
|
11
|
-
export { TermNode } from './node.js';
|
|
17
|
+
export { TermNode, SvtRegionNode } from './node.js';
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { createRenderer as svelteCreateRenderer } from 'svelte/renderer';
|
|
2
|
-
import { TermNode } from './node.js';
|
|
2
|
+
import { TermNode, SvtRegionNode } from './node.js';
|
|
3
3
|
export function createTermRenderer() {
|
|
4
4
|
return svelteCreateRenderer({
|
|
5
5
|
createFragment() {
|
|
6
6
|
return new TermNode('fragment');
|
|
7
7
|
},
|
|
8
8
|
createElement(name) {
|
|
9
|
+
if (name === 'svt-region')
|
|
10
|
+
return new SvtRegionNode();
|
|
9
11
|
return new TermNode('element', name);
|
|
10
12
|
},
|
|
11
13
|
createTextNode(data) {
|
|
@@ -113,4 +115,4 @@ export function createTermRenderer() {
|
|
|
113
115
|
*
|
|
114
116
|
* Call this AFTER renderer.render() which pops the renderer.
|
|
115
117
|
*/
|
|
116
|
-
export { TermNode } from './node.js';
|
|
118
|
+
export { TermNode, SvtRegionNode } from './node.js';
|
|
@@ -2,11 +2,26 @@ import type { ResolvedStyle } from '../css/compute.js';
|
|
|
2
2
|
import type { LayoutBox } from '../layout/engine.js';
|
|
3
3
|
import type { RenderContext } from '../render/context.js';
|
|
4
4
|
import { TextBuffer } from '../components/text-buffer.js';
|
|
5
|
+
import type { Cell } from '../render/buffer.js';
|
|
5
6
|
export type NodeType = 'element' | 'text' | 'comment' | 'fragment';
|
|
7
|
+
export interface CursorScreenPos {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
/** Full-paint generation that wrote this position (see render/generation). */
|
|
11
|
+
generation?: number;
|
|
12
|
+
/** False when the cursor falls outside the node's content viewport
|
|
13
|
+
* (scrolled off, clipped by an ancestor). The emitter hides the
|
|
14
|
+
* cursor in that case. */
|
|
15
|
+
inViewport: boolean;
|
|
16
|
+
}
|
|
6
17
|
export interface RenderCache {
|
|
7
18
|
resolvedStyle: ResolvedStyle | null;
|
|
8
19
|
layoutBox: LayoutBox | null;
|
|
9
20
|
classAttr: string;
|
|
21
|
+
/** Last known screen position of the node's text cursor. Set by
|
|
22
|
+
* paint when the node owns a cursor (focused input/textarea);
|
|
23
|
+
* consumed by the post-paint cursor emitter. */
|
|
24
|
+
cursorScreen: CursorScreenPos | null;
|
|
10
25
|
}
|
|
11
26
|
export declare class TermNode {
|
|
12
27
|
readonly id: number;
|
|
@@ -16,20 +31,47 @@ export declare class TermNode {
|
|
|
16
31
|
ctx: RenderContext | null;
|
|
17
32
|
parent: TermNode | null;
|
|
18
33
|
children: TermNode[];
|
|
34
|
+
/**
|
|
35
|
+
* Synthetic ::before/::after boxes. Renderer-owned: kept out of
|
|
36
|
+
* `children` so selectors (:empty, :nth-*), focus walks and Svelte's
|
|
37
|
+
* anchor bookkeeping never see them; layout and paint include them
|
|
38
|
+
* via childrenWithPseudos().
|
|
39
|
+
*/
|
|
40
|
+
pseudoBefore: TermNode | null;
|
|
41
|
+
pseudoAfter: TermNode | null;
|
|
19
42
|
attributes: Map<string, string>;
|
|
20
43
|
listeners: Map<string, Set<(...args: any[]) => void>>;
|
|
21
44
|
scrollTop: number;
|
|
22
45
|
scrollLeft: number;
|
|
46
|
+
scrollbarVisibleUntil: number;
|
|
47
|
+
hScrollbarVisibleUntil: number;
|
|
23
48
|
textBuffer: TextBuffer | null;
|
|
24
49
|
cache: RenderCache;
|
|
25
50
|
/** DOM compatibility — Svelte's effects set nodeValue directly when renderer is not pushed */
|
|
26
51
|
get nodeValue(): string | null;
|
|
27
52
|
set nodeValue(value: string | null);
|
|
53
|
+
/** DOM compatibility — form control value as a property */
|
|
54
|
+
get value(): string;
|
|
55
|
+
set value(newValue: string);
|
|
56
|
+
/** DOM compatibility — checkbox/radio state as a property */
|
|
57
|
+
get checked(): boolean;
|
|
58
|
+
set checked(value: boolean);
|
|
28
59
|
/** DOM compatibility — Svelte may also use textContent */
|
|
29
60
|
get textContent(): string;
|
|
30
61
|
set textContent(value: string);
|
|
31
62
|
constructor(nodeType: NodeType, tagOrText?: string);
|
|
32
63
|
get classes(): Set<string>;
|
|
64
|
+
/** DOM compat — returns the root of the tree */
|
|
65
|
+
getRootNode(): TermNode;
|
|
66
|
+
/** DOM compat — append_styles uses querySelector */
|
|
67
|
+
querySelector(selector: string): TermNode | null;
|
|
68
|
+
private findById;
|
|
69
|
+
/** DOM compat — append_styles checks for .host on the root */
|
|
70
|
+
get host(): undefined;
|
|
71
|
+
/** DOM compat — append_styles accesses .head */
|
|
72
|
+
get head(): TermNode;
|
|
73
|
+
/** DOM compat — append_styles accesses .ownerDocument */
|
|
74
|
+
get ownerDocument(): TermNode;
|
|
33
75
|
getFirstChild(): TermNode | null;
|
|
34
76
|
getLastChild(): TermNode | null;
|
|
35
77
|
getNextSibling(): TermNode | null;
|
|
@@ -37,8 +79,75 @@ export declare class TermNode {
|
|
|
37
79
|
removeChild(node: TermNode): void;
|
|
38
80
|
remove(): void;
|
|
39
81
|
collectText(): string;
|
|
82
|
+
/**
|
|
83
|
+
* Screen-space position of this node's text cursor, set by paint.
|
|
84
|
+
* Returns null unless the node currently owns a cursor (e.g. a
|
|
85
|
+
* focused input/textarea repainted this frame). Consumed by the
|
|
86
|
+
* post-paint cursor emitter.
|
|
87
|
+
*/
|
|
88
|
+
getCursorScreenPos(): CursorScreenPos | null;
|
|
40
89
|
invalidateStyle(): void;
|
|
41
90
|
invalidateLayout(): void;
|
|
42
91
|
invalidateAll(): void;
|
|
43
92
|
cleanup(): void;
|
|
44
93
|
}
|
|
94
|
+
/** The children layout and paint should flow: ::before, real children, ::after. */
|
|
95
|
+
export declare function childrenWithPseudos(node: TermNode): TermNode[];
|
|
96
|
+
/**
|
|
97
|
+
* HTML boolean attribute semantics, with one renderer-specific twist:
|
|
98
|
+
* Svelte passes attribute values through `String(value)`, so a literal
|
|
99
|
+
* "false" means the author set `disabled={false}` and expects it off.
|
|
100
|
+
*/
|
|
101
|
+
export declare function hasBooleanAttribute(node: TermNode, name: string): boolean;
|
|
102
|
+
/**
|
|
103
|
+
* A layout-participating region whose contents are filled by an external
|
|
104
|
+
* source. The consumer (typically `EmbeddedTerminal`) registers a
|
|
105
|
+
* cell-source function via `setCellSource`, and the paint phase calls
|
|
106
|
+
* it for each cell of the region's allocated box.
|
|
107
|
+
*
|
|
108
|
+
* Local coordinates: the cell-source function receives `(col, row)`
|
|
109
|
+
* relative to the region's own top-left, NOT the surrounding buffer.
|
|
110
|
+
*
|
|
111
|
+
* Resize: when the layout-allocated cell dimensions change between
|
|
112
|
+
* paints, a `resize` event fires with `{ cols, rows }` so the consumer
|
|
113
|
+
* can resize the upstream Terminal / stream to match.
|
|
114
|
+
*/
|
|
115
|
+
export interface RegionCursor {
|
|
116
|
+
col: number;
|
|
117
|
+
row: number;
|
|
118
|
+
visible: boolean;
|
|
119
|
+
}
|
|
120
|
+
export declare class SvtRegionNode extends TermNode {
|
|
121
|
+
private cellSource;
|
|
122
|
+
private lastCols;
|
|
123
|
+
private lastRows;
|
|
124
|
+
private cursor;
|
|
125
|
+
/** Last allocated screen origin. Set by paintRegion so the post-diff
|
|
126
|
+
* cursor emitter can translate region-local coords to absolute. */
|
|
127
|
+
lastBoxX: number;
|
|
128
|
+
lastBoxY: number;
|
|
129
|
+
constructor();
|
|
130
|
+
setCellSource(fn: (col: number, row: number) => Cell): void;
|
|
131
|
+
getCellSource(): ((col: number, row: number) => Cell) | null;
|
|
132
|
+
/**
|
|
133
|
+
* Called by the paint phase with the region's currently-allocated
|
|
134
|
+
* dimensions. Fires `resize` if they've changed since the last call.
|
|
135
|
+
* Returns the (possibly newly-fired) dimensions.
|
|
136
|
+
*/
|
|
137
|
+
notifyAllocatedSize(cols: number, rows: number, fire: (cols: number, rows: number) => void): void;
|
|
138
|
+
/**
|
|
139
|
+
* Position (and show/hide) a cursor inside the region. Coordinates are
|
|
140
|
+
* region-local; the post-paint cursor emitter translates them to
|
|
141
|
+
* screen coordinates using the region's last allocated box. Pass
|
|
142
|
+
* `null` to suppress cursor emission for this region.
|
|
143
|
+
*/
|
|
144
|
+
setCursor(cursor: RegionCursor | null): void;
|
|
145
|
+
getCursor(): RegionCursor | null;
|
|
146
|
+
/**
|
|
147
|
+
* Mark the region as needing a repaint. Consumers call this when
|
|
148
|
+
* their cell source's output has changed (e.g. after their upstream
|
|
149
|
+
* Terminal has consumed new bytes from a stream). Schedules a paint
|
|
150
|
+
* via the render context and re-runs the paint pipeline.
|
|
151
|
+
*/
|
|
152
|
+
markDirty(): void;
|
|
153
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { paintGeneration } from '../render/generation.js';
|
|
1
2
|
let nextId = 1;
|
|
2
3
|
export class TermNode {
|
|
3
4
|
id;
|
|
@@ -7,12 +8,22 @@ export class TermNode {
|
|
|
7
8
|
ctx = null;
|
|
8
9
|
parent = null;
|
|
9
10
|
children = [];
|
|
11
|
+
/**
|
|
12
|
+
* Synthetic ::before/::after boxes. Renderer-owned: kept out of
|
|
13
|
+
* `children` so selectors (:empty, :nth-*), focus walks and Svelte's
|
|
14
|
+
* anchor bookkeeping never see them; layout and paint include them
|
|
15
|
+
* via childrenWithPseudos().
|
|
16
|
+
*/
|
|
17
|
+
pseudoBefore = null;
|
|
18
|
+
pseudoAfter = null;
|
|
10
19
|
attributes = new Map();
|
|
11
20
|
listeners = new Map();
|
|
12
21
|
scrollTop = 0;
|
|
13
22
|
scrollLeft = 0;
|
|
23
|
+
scrollbarVisibleUntil = 0;
|
|
24
|
+
hScrollbarVisibleUntil = 0;
|
|
14
25
|
textBuffer = null;
|
|
15
|
-
cache = { resolvedStyle: null, layoutBox: null, classAttr: '' };
|
|
26
|
+
cache = { resolvedStyle: null, layoutBox: null, classAttr: '', cursorScreen: null };
|
|
16
27
|
/** DOM compatibility — Svelte's effects set nodeValue directly when renderer is not pushed */
|
|
17
28
|
get nodeValue() {
|
|
18
29
|
if (this.nodeType === 'text' || this.nodeType === 'comment')
|
|
@@ -30,6 +41,34 @@ export class TermNode {
|
|
|
30
41
|
}
|
|
31
42
|
}
|
|
32
43
|
}
|
|
44
|
+
/** DOM compatibility — form control value as a property */
|
|
45
|
+
get value() {
|
|
46
|
+
return this.attributes.get('value') ?? '';
|
|
47
|
+
}
|
|
48
|
+
set value(newValue) {
|
|
49
|
+
if (this.ctx)
|
|
50
|
+
this.ctx.onSetAttribute(this, 'value', newValue);
|
|
51
|
+
else
|
|
52
|
+
this.attributes.set('value', newValue);
|
|
53
|
+
}
|
|
54
|
+
/** DOM compatibility — checkbox/radio state as a property */
|
|
55
|
+
get checked() {
|
|
56
|
+
return hasBooleanAttribute(this, 'checked');
|
|
57
|
+
}
|
|
58
|
+
set checked(value) {
|
|
59
|
+
if (value) {
|
|
60
|
+
if (this.ctx)
|
|
61
|
+
this.ctx.onSetAttribute(this, 'checked', 'true');
|
|
62
|
+
else
|
|
63
|
+
this.attributes.set('checked', 'true');
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
if (this.ctx)
|
|
67
|
+
this.ctx.onRemoveAttribute(this, 'checked');
|
|
68
|
+
else
|
|
69
|
+
this.attributes.delete('checked');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
33
72
|
/** DOM compatibility — Svelte may also use textContent */
|
|
34
73
|
get textContent() {
|
|
35
74
|
return this.collectText();
|
|
@@ -60,6 +99,38 @@ export class TermNode {
|
|
|
60
99
|
return new Set();
|
|
61
100
|
return new Set(raw.split(/\s+/).filter(Boolean));
|
|
62
101
|
}
|
|
102
|
+
/** DOM compat — returns the root of the tree */
|
|
103
|
+
getRootNode() {
|
|
104
|
+
let node = this;
|
|
105
|
+
while (node.parent)
|
|
106
|
+
node = node.parent;
|
|
107
|
+
return node;
|
|
108
|
+
}
|
|
109
|
+
/** DOM compat — append_styles uses querySelector */
|
|
110
|
+
querySelector(selector) {
|
|
111
|
+
// Simple #id selector support for append_styles
|
|
112
|
+
if (selector.startsWith('#')) {
|
|
113
|
+
const id = selector.slice(1);
|
|
114
|
+
return this.findById(id);
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
findById(id) {
|
|
119
|
+
if (this.attributes.get('id') === id)
|
|
120
|
+
return this;
|
|
121
|
+
for (const child of this.children) {
|
|
122
|
+
const found = child.findById(id);
|
|
123
|
+
if (found)
|
|
124
|
+
return found;
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
/** DOM compat — append_styles checks for .host on the root */
|
|
129
|
+
get host() { return undefined; }
|
|
130
|
+
/** DOM compat — append_styles accesses .head */
|
|
131
|
+
get head() { return this; }
|
|
132
|
+
/** DOM compat — append_styles accesses .ownerDocument */
|
|
133
|
+
get ownerDocument() { return this.getRootNode(); }
|
|
63
134
|
getFirstChild() {
|
|
64
135
|
return this.children[0] ?? null;
|
|
65
136
|
}
|
|
@@ -120,6 +191,23 @@ export class TermNode {
|
|
|
120
191
|
return '';
|
|
121
192
|
return this.children.map(c => c.collectText()).join('');
|
|
122
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* Screen-space position of this node's text cursor, set by paint.
|
|
196
|
+
* Returns null unless the node currently owns a cursor (e.g. a
|
|
197
|
+
* focused input/textarea repainted this frame). Consumed by the
|
|
198
|
+
* post-paint cursor emitter.
|
|
199
|
+
*/
|
|
200
|
+
getCursorScreenPos() {
|
|
201
|
+
const pos = this.cache.cursorScreen;
|
|
202
|
+
if (!pos)
|
|
203
|
+
return null;
|
|
204
|
+
// A position stamped by an older full paint is stale — the node
|
|
205
|
+
// was culled offscreen this frame, so it no longer owns a cursor.
|
|
206
|
+
// Unstamped positions (set directly by embedders/tests) stay valid.
|
|
207
|
+
if (pos.generation !== undefined && pos.generation !== paintGeneration())
|
|
208
|
+
return null;
|
|
209
|
+
return pos;
|
|
210
|
+
}
|
|
123
211
|
invalidateStyle() {
|
|
124
212
|
this.cache.resolvedStyle = null;
|
|
125
213
|
this.cache.classAttr = '';
|
|
@@ -139,6 +227,82 @@ export class TermNode {
|
|
|
139
227
|
}
|
|
140
228
|
}
|
|
141
229
|
}
|
|
230
|
+
/** The children layout and paint should flow: ::before, real children, ::after. */
|
|
231
|
+
export function childrenWithPseudos(node) {
|
|
232
|
+
if (!node.pseudoBefore && !node.pseudoAfter)
|
|
233
|
+
return node.children;
|
|
234
|
+
const flowed = [];
|
|
235
|
+
if (node.pseudoBefore)
|
|
236
|
+
flowed.push(node.pseudoBefore);
|
|
237
|
+
flowed.push(...node.children);
|
|
238
|
+
if (node.pseudoAfter)
|
|
239
|
+
flowed.push(node.pseudoAfter);
|
|
240
|
+
return flowed;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* HTML boolean attribute semantics, with one renderer-specific twist:
|
|
244
|
+
* Svelte passes attribute values through `String(value)`, so a literal
|
|
245
|
+
* "false" means the author set `disabled={false}` and expects it off.
|
|
246
|
+
*/
|
|
247
|
+
export function hasBooleanAttribute(node, name) {
|
|
248
|
+
const value = node.attributes.get(name);
|
|
249
|
+
return value !== undefined && value !== 'false';
|
|
250
|
+
}
|
|
251
|
+
export class SvtRegionNode extends TermNode {
|
|
252
|
+
cellSource = null;
|
|
253
|
+
lastCols = -1;
|
|
254
|
+
lastRows = -1;
|
|
255
|
+
cursor = null;
|
|
256
|
+
/** Last allocated screen origin. Set by paintRegion so the post-diff
|
|
257
|
+
* cursor emitter can translate region-local coords to absolute. */
|
|
258
|
+
lastBoxX = 0;
|
|
259
|
+
lastBoxY = 0;
|
|
260
|
+
constructor() {
|
|
261
|
+
super('element', 'svt-region');
|
|
262
|
+
}
|
|
263
|
+
setCellSource(fn) {
|
|
264
|
+
this.cellSource = fn;
|
|
265
|
+
}
|
|
266
|
+
getCellSource() {
|
|
267
|
+
return this.cellSource;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Called by the paint phase with the region's currently-allocated
|
|
271
|
+
* dimensions. Fires `resize` if they've changed since the last call.
|
|
272
|
+
* Returns the (possibly newly-fired) dimensions.
|
|
273
|
+
*/
|
|
274
|
+
notifyAllocatedSize(cols, rows, fire) {
|
|
275
|
+
if (cols === this.lastCols && rows === this.lastRows)
|
|
276
|
+
return;
|
|
277
|
+
this.lastCols = cols;
|
|
278
|
+
this.lastRows = rows;
|
|
279
|
+
fire(cols, rows);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Position (and show/hide) a cursor inside the region. Coordinates are
|
|
283
|
+
* region-local; the post-paint cursor emitter translates them to
|
|
284
|
+
* screen coordinates using the region's last allocated box. Pass
|
|
285
|
+
* `null` to suppress cursor emission for this region.
|
|
286
|
+
*/
|
|
287
|
+
setCursor(cursor) {
|
|
288
|
+
this.cursor = cursor;
|
|
289
|
+
}
|
|
290
|
+
getCursor() {
|
|
291
|
+
return this.cursor;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Mark the region as needing a repaint. Consumers call this when
|
|
295
|
+
* their cell source's output has changed (e.g. after their upstream
|
|
296
|
+
* Terminal has consumed new bytes from a stream). Schedules a paint
|
|
297
|
+
* via the render context and re-runs the paint pipeline.
|
|
298
|
+
*/
|
|
299
|
+
markDirty() {
|
|
300
|
+
if (!this.ctx)
|
|
301
|
+
return;
|
|
302
|
+
this.ctx.queue.enqueuePaintOnly(this);
|
|
303
|
+
this.ctx.onScheduleRender?.();
|
|
304
|
+
}
|
|
305
|
+
}
|
|
142
306
|
function propagateCtx(node, ctx) {
|
|
143
307
|
node.ctx = ctx;
|
|
144
308
|
for (const child of node.children) {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal capability detection: colour depth from the environment and
|
|
3
|
+
* XTVERSION, synchronized-output support via DECRQM. Queries run through
|
|
4
|
+
* the StdinRouter with timeouts, so unresponsive terminals just get
|
|
5
|
+
* conservative defaults.
|
|
6
|
+
*/
|
|
7
|
+
import type { StdinRouter } from './stdin-router.js';
|
|
8
|
+
export type ColorDepth = 'truecolor' | '256' | '16' | 'mono';
|
|
9
|
+
export interface TerminalCapabilities {
|
|
10
|
+
colorDepth: ColorDepth;
|
|
11
|
+
/** DEC 2026 begin/end synchronized update. */
|
|
12
|
+
syncOutput: boolean;
|
|
13
|
+
/** Kitty graphics protocol (crisp images instead of half-blocks). */
|
|
14
|
+
graphics: boolean;
|
|
15
|
+
/** Terminal name and version from XTVERSION, when it answered. */
|
|
16
|
+
terminal: string | null;
|
|
17
|
+
}
|
|
18
|
+
export declare function resolveColorDepth(env: Record<string, string | undefined>, xtversion?: string | null): ColorDepth;
|
|
19
|
+
/** Match a DCS > | <text> ST reply to XTVERSION, returning the text. */
|
|
20
|
+
export declare function matchXTVERSION(data: string): string | null;
|
|
21
|
+
/** Match a DECRQM reply for the given mode. */
|
|
22
|
+
export declare function matchDECRQM(mode: number): (data: string) => string | null;
|
|
23
|
+
/** Match a CPR (cursor position report) reply: CSI row ; col R. */
|
|
24
|
+
export declare function matchCPR(data: string): string | null;
|
|
25
|
+
/** The 1-based row from a CPR reply, or null. */
|
|
26
|
+
export declare function parseCPRRow(reply: string | null): number | null;
|
|
27
|
+
/** Whether a DECRQM reply says the mode is recognised (set or reset). */
|
|
28
|
+
export declare function decrqmSupported(reply: string | null): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Query the live terminal. COLORTERM/NO_COLOR decide colour depth without
|
|
31
|
+
* a query; otherwise XTVERSION identifies known-truecolor terminals.
|
|
32
|
+
*/
|
|
33
|
+
export declare function detectCapabilities(router: StdinRouter, env?: Record<string, string | undefined>): Promise<TerminalCapabilities>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal capability detection: colour depth from the environment and
|
|
3
|
+
* XTVERSION, synchronized-output support via DECRQM. Queries run through
|
|
4
|
+
* the StdinRouter with timeouts, so unresponsive terminals just get
|
|
5
|
+
* conservative defaults.
|
|
6
|
+
*/
|
|
7
|
+
import { graphicsSupported } from '../render/kitty-graphics.js';
|
|
8
|
+
/** Terminals that render truecolor but predate COLORTERM adoption. */
|
|
9
|
+
const TRUECOLOR_TERMINALS = /iterm|kitty|wezterm|ghostty|alacritty|contour|rio|vscode/i;
|
|
10
|
+
export function resolveColorDepth(env, xtversion) {
|
|
11
|
+
if (env.NO_COLOR !== undefined && env.NO_COLOR !== '')
|
|
12
|
+
return 'mono';
|
|
13
|
+
const colorterm = env.COLORTERM ?? '';
|
|
14
|
+
if (colorterm === 'truecolor' || colorterm === '24bit')
|
|
15
|
+
return 'truecolor';
|
|
16
|
+
if (xtversion && TRUECOLOR_TERMINALS.test(xtversion))
|
|
17
|
+
return 'truecolor';
|
|
18
|
+
if ((env.TERM ?? '').includes('256color'))
|
|
19
|
+
return '256';
|
|
20
|
+
return '16';
|
|
21
|
+
}
|
|
22
|
+
/** Match a DCS > | <text> ST reply to XTVERSION, returning the text. */
|
|
23
|
+
export function matchXTVERSION(data) {
|
|
24
|
+
const match = /\x1bP>\|([^\x1b]*)\x1b\\/.exec(data);
|
|
25
|
+
return match ? match[1] : null;
|
|
26
|
+
}
|
|
27
|
+
/** Match a DECRQM reply for the given mode. */
|
|
28
|
+
export function matchDECRQM(mode) {
|
|
29
|
+
const pattern = new RegExp(`\\x1b\\[\\?${mode};\\d\\$y`);
|
|
30
|
+
return data => pattern.exec(data)?.[0] ?? null;
|
|
31
|
+
}
|
|
32
|
+
/** Match a CPR (cursor position report) reply: CSI row ; col R. */
|
|
33
|
+
export function matchCPR(data) {
|
|
34
|
+
return /\x1b\[\d+;\d+R/.exec(data)?.[0] ?? null;
|
|
35
|
+
}
|
|
36
|
+
/** The 1-based row from a CPR reply, or null. */
|
|
37
|
+
export function parseCPRRow(reply) {
|
|
38
|
+
if (!reply)
|
|
39
|
+
return null;
|
|
40
|
+
const row = /\x1b\[(\d+);/.exec(reply)?.[1];
|
|
41
|
+
return row ? parseInt(row, 10) : null;
|
|
42
|
+
}
|
|
43
|
+
/** Whether a DECRQM reply says the mode is recognised (set or reset). */
|
|
44
|
+
export function decrqmSupported(reply) {
|
|
45
|
+
if (!reply)
|
|
46
|
+
return false;
|
|
47
|
+
const value = /;(\d)\$y/.exec(reply)?.[1];
|
|
48
|
+
return value === '1' || value === '2';
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Query the live terminal. COLORTERM/NO_COLOR decide colour depth without
|
|
52
|
+
* a query; otherwise XTVERSION identifies known-truecolor terminals.
|
|
53
|
+
*/
|
|
54
|
+
export async function detectCapabilities(router, env = process.env) {
|
|
55
|
+
const [xtversion, decrqm] = await Promise.all([
|
|
56
|
+
// Identifies known-truecolor terminals AND graphics support
|
|
57
|
+
router.query('\x1b[>0q', matchXTVERSION, 150),
|
|
58
|
+
router.query('\x1b[?2026$p', matchDECRQM(2026), 150),
|
|
59
|
+
]);
|
|
60
|
+
return {
|
|
61
|
+
colorDepth: resolveColorDepth(env, xtversion),
|
|
62
|
+
syncOutput: decrqmSupported(decrqm),
|
|
63
|
+
graphics: graphicsSupported(xtversion),
|
|
64
|
+
terminal: xtversion,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clipboard writes. OSC 52 travels in-band (works over ssh and through
|
|
3
|
+
* multiplexers that pass it on); a platform tool runs as well when one
|
|
4
|
+
* exists, covering terminals with OSC 52 disabled. Failures are silent —
|
|
5
|
+
* copying is best-effort by nature here.
|
|
6
|
+
*/
|
|
7
|
+
export declare function osc52Copy(text: string): string;
|
|
8
|
+
/** Write text to the clipboard: OSC 52 through `write`, plus a platform tool. */
|
|
9
|
+
export declare function copyToClipboard(text: string, write: (data: string) => void): void;
|