@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,94 +1,67 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { paintTextContent } from './paint-text.js';
|
|
1
|
+
import { paint } from './paint.js';
|
|
3
2
|
/**
|
|
4
|
-
* Repaint only
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* Repaint only the region affected by dirty nodes.
|
|
4
|
+
*
|
|
5
|
+
* Computes the union bounding box of all dirty nodes, clears that
|
|
6
|
+
* region, then does a full repaint of the entire tree clipped to
|
|
7
|
+
* that region. This correctly handles overlapping elements like
|
|
8
|
+
* parent borders, list markers in padding areas, and z-indexed
|
|
9
|
+
* siblings.
|
|
7
10
|
*/
|
|
8
11
|
export function paintNodes(nodes, buffer, styles, layout, root) {
|
|
12
|
+
if (nodes.size === 0)
|
|
13
|
+
return;
|
|
14
|
+
// Compute dirty region — union of all dirty nodes' current and previous boxes
|
|
15
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
9
16
|
for (const node of nodes) {
|
|
10
17
|
const box = layout.get(node.id);
|
|
11
|
-
if (
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (node.nodeType === 'text') {
|
|
17
|
-
paintTextShared(node, buffer, box, styles, layout);
|
|
18
|
+
if (box) {
|
|
19
|
+
minX = Math.min(minX, box.x);
|
|
20
|
+
minY = Math.min(minY, box.y);
|
|
21
|
+
maxX = Math.max(maxX, box.x + box.width);
|
|
22
|
+
maxY = Math.max(maxY, box.y + box.height);
|
|
18
23
|
}
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
const oldBox = node.cache.layoutBox;
|
|
25
|
+
if (oldBox) {
|
|
26
|
+
minX = Math.min(minX, oldBox.x);
|
|
27
|
+
minY = Math.min(minY, oldBox.y);
|
|
28
|
+
maxX = Math.max(maxX, oldBox.x + oldBox.width);
|
|
29
|
+
maxY = Math.max(maxY, oldBox.y + oldBox.height);
|
|
21
30
|
}
|
|
22
31
|
}
|
|
23
|
-
|
|
24
|
-
function paintElementNode(node, buffer, box, styles, layout) {
|
|
25
|
-
const style = styles.get(node.id);
|
|
26
|
-
if (!style || style.display === 'none')
|
|
32
|
+
if (minX >= maxX || minY >= maxY)
|
|
27
33
|
return;
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
// Expand dirty region to include parent borders and list marker padding
|
|
35
|
+
// that may overlap the dirty area
|
|
36
|
+
for (const node of nodes) {
|
|
37
|
+
let parent = node.parent;
|
|
38
|
+
while (parent) {
|
|
39
|
+
const parentBox = layout.get(parent.id);
|
|
40
|
+
if (parentBox) {
|
|
41
|
+
minX = Math.min(minX, parentBox.x);
|
|
42
|
+
minY = Math.min(minY, parentBox.y);
|
|
43
|
+
maxX = Math.max(maxX, parentBox.x + parentBox.width);
|
|
44
|
+
maxY = Math.max(maxY, parentBox.y + parentBox.height);
|
|
36
45
|
}
|
|
46
|
+
parent = parent.parent;
|
|
37
47
|
}
|
|
38
48
|
}
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
paintElementNode(child, buffer, childBox, styles, layout);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
function paintTextShared(node, buffer, box, styles, layout) {
|
|
57
|
-
const visuals = resolveInheritedVisuals(node, styles);
|
|
58
|
-
paintTextContent(node, buffer, box, visuals, styles, layout);
|
|
59
|
-
}
|
|
60
|
-
function clearArea(buffer, box) {
|
|
61
|
-
for (let row = box.y; row < box.y + box.height; row++) {
|
|
62
|
-
for (let col = box.x; col < box.x + box.width; col++) {
|
|
63
|
-
buffer.setCell(col, row, { char: ' ', fg: 'default', bg: 'default', bold: false, italic: false, underline: false, strikethrough: false, dim: false });
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
function resolveInheritedVisuals(node, styles) {
|
|
68
|
-
const result = { fg: 'default', bg: 'default', bold: false, italic: false, underline: false, strikethrough: false, dim: false };
|
|
69
|
-
let current = node.parent;
|
|
70
|
-
while (current) {
|
|
71
|
-
const style = styles.get(current.id);
|
|
72
|
-
if (style) {
|
|
73
|
-
if (result.fg === 'default' && style.fg !== 'default')
|
|
74
|
-
result.fg = style.fg;
|
|
75
|
-
if (result.bg === 'default' && style.bg !== 'default')
|
|
76
|
-
result.bg = style.bg;
|
|
77
|
-
if (!result.bold && style.bold)
|
|
78
|
-
result.bold = true;
|
|
79
|
-
if (!result.italic && style.italic)
|
|
80
|
-
result.italic = true;
|
|
81
|
-
if (!result.underline && style.underline)
|
|
82
|
-
result.underline = true;
|
|
83
|
-
if (!result.strikethrough && style.strikethrough)
|
|
84
|
-
result.strikethrough = true;
|
|
85
|
-
if (!result.dim && style.dim)
|
|
86
|
-
result.dim = true;
|
|
87
|
-
}
|
|
88
|
-
if (!result.hyperlink && current.tag === 'a') {
|
|
89
|
-
result.hyperlink = current.attributes.get('href');
|
|
49
|
+
// Clamp to buffer bounds
|
|
50
|
+
minX = Math.max(0, minX);
|
|
51
|
+
minY = Math.max(0, minY);
|
|
52
|
+
maxX = Math.min(buffer.width, maxX);
|
|
53
|
+
maxY = Math.min(buffer.height, maxY);
|
|
54
|
+
// Clear the dirty region
|
|
55
|
+
for (let row = minY; row < maxY; row++) {
|
|
56
|
+
for (let col = minX; col < maxX; col++) {
|
|
57
|
+
buffer.setCell(col, row, {
|
|
58
|
+
char: ' ', fg: 'default', bg: 'default',
|
|
59
|
+
bold: false, italic: false, underline: false,
|
|
60
|
+
strikethrough: false, dim: false, inverse: false,
|
|
61
|
+
});
|
|
90
62
|
}
|
|
91
|
-
current = current.parent;
|
|
92
63
|
}
|
|
93
|
-
|
|
64
|
+
// Full repaint of the entire tree, clipped to the dirty region
|
|
65
|
+
const clip = { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
66
|
+
paint(root, buffer, styles, layout, clip);
|
|
94
67
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline-mode screen driver. The app renders into the main buffer at the
|
|
3
|
+
* shell's cursor position; rows above the render origin belong to the
|
|
4
|
+
* terminal's scrollback and are never touched again. All cursor movement
|
|
5
|
+
* is relative — the origin's absolute position is unknown by design.
|
|
6
|
+
*
|
|
7
|
+
* Growth emits real newlines at the bottom row (LF scrolls where CUD
|
|
8
|
+
* cannot); shrinking erases to end of screen; archiving (`releaseTop`)
|
|
9
|
+
* just moves the comparison window down — the released rows are already
|
|
10
|
+
* on the terminal exactly as they should stay.
|
|
11
|
+
*/
|
|
12
|
+
import { CellBuffer } from './buffer.js';
|
|
13
|
+
export declare class InlineScreen {
|
|
14
|
+
/** Last painted content, padded with blanks to `physicalRows`. */
|
|
15
|
+
private prev;
|
|
16
|
+
/** Lines the zone has realised on the terminal. */
|
|
17
|
+
private physicalRows;
|
|
18
|
+
/** Rows the last buffer actually occupied (≤ physicalRows). */
|
|
19
|
+
private contentHeight;
|
|
20
|
+
/** Cursor position relative to the live-zone origin. */
|
|
21
|
+
private cursorRow;
|
|
22
|
+
/** -1 when unknown (wrap-pending after writing the last column). */
|
|
23
|
+
private cursorCol;
|
|
24
|
+
/** 1-based screen row of zone row 0, from a CPR query; null = unknown. */
|
|
25
|
+
private originRow;
|
|
26
|
+
/** ANSI that makes the terminal's live zone match `next`. */
|
|
27
|
+
render(next: CellBuffer): string;
|
|
28
|
+
/** Record where the zone starts on screen (1-based, from CPR). */
|
|
29
|
+
setOriginRow(row: number): void;
|
|
30
|
+
/** The cursor's current row within the zone (for CPR origin math). */
|
|
31
|
+
cursorZoneRow(): number;
|
|
32
|
+
/**
|
|
33
|
+
* Map a 0-based screen row (mouse coordinates) to a zone row, or
|
|
34
|
+
* null when unknown or outside the live content. Growth past the
|
|
35
|
+
* screen bottom scrolls the zone up, so the effective origin is
|
|
36
|
+
* clamped to keep the zone's bottom on screen.
|
|
37
|
+
*/
|
|
38
|
+
screenRowToZone(screenRow: number, screenHeight: number): number | null;
|
|
39
|
+
/**
|
|
40
|
+
* Forget the zone entirely — after a suspend the shell owned the
|
|
41
|
+
* screen, so the next render starts a fresh zone at the cursor.
|
|
42
|
+
*/
|
|
43
|
+
reset(): void;
|
|
44
|
+
/** Hand the top `n` rows to the terminal's scrollback. */
|
|
45
|
+
releaseTop(n: number): void;
|
|
46
|
+
/** Place the terminal cursor at live-zone coordinates. */
|
|
47
|
+
moveCursorTo(col: number, row: number): string;
|
|
48
|
+
/** Leave the cursor on a fresh line after the content. */
|
|
49
|
+
finish(): string;
|
|
50
|
+
/** Relative row movement, tracking the new position. */
|
|
51
|
+
private moveRow;
|
|
52
|
+
/** Realise new physical lines with LF so the terminal scrolls. */
|
|
53
|
+
private grow;
|
|
54
|
+
/**
|
|
55
|
+
* Blank everything below `height`. The physical lines stay realised
|
|
56
|
+
* (as empties) so regrowth reuses them instead of scrolling new ones.
|
|
57
|
+
*/
|
|
58
|
+
private eraseBelow;
|
|
59
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline-mode screen driver. The app renders into the main buffer at the
|
|
3
|
+
* shell's cursor position; rows above the render origin belong to the
|
|
4
|
+
* terminal's scrollback and are never touched again. All cursor movement
|
|
5
|
+
* is relative — the origin's absolute position is unknown by design.
|
|
6
|
+
*
|
|
7
|
+
* Growth emits real newlines at the bottom row (LF scrolls where CUD
|
|
8
|
+
* cannot); shrinking erases to end of screen; archiving (`releaseTop`)
|
|
9
|
+
* just moves the comparison window down — the released rows are already
|
|
10
|
+
* on the terminal exactly as they should stay.
|
|
11
|
+
*/
|
|
12
|
+
import { CellBuffer, cellsEqual } from './buffer.js';
|
|
13
|
+
import * as ansi from './ansi.js';
|
|
14
|
+
import { stringWidth } from '../layout/unicode.js';
|
|
15
|
+
const CSI = '\x1b[';
|
|
16
|
+
export class InlineScreen {
|
|
17
|
+
/** Last painted content, padded with blanks to `physicalRows`. */
|
|
18
|
+
prev = null;
|
|
19
|
+
/** Lines the zone has realised on the terminal. */
|
|
20
|
+
physicalRows = 0;
|
|
21
|
+
/** Rows the last buffer actually occupied (≤ physicalRows). */
|
|
22
|
+
contentHeight = 0;
|
|
23
|
+
/** Cursor position relative to the live-zone origin. */
|
|
24
|
+
cursorRow = 0;
|
|
25
|
+
/** -1 when unknown (wrap-pending after writing the last column). */
|
|
26
|
+
cursorCol = 0;
|
|
27
|
+
/** 1-based screen row of zone row 0, from a CPR query; null = unknown. */
|
|
28
|
+
originRow = null;
|
|
29
|
+
/** ANSI that makes the terminal's live zone match `next`. */
|
|
30
|
+
render(next) {
|
|
31
|
+
const parts = [];
|
|
32
|
+
if (this.prev && this.prev.width !== next.width) {
|
|
33
|
+
// Width changed: the terminal may have rewrapped our rows.
|
|
34
|
+
// Erase and repaint the whole zone in place — best effort.
|
|
35
|
+
parts.push(this.moveRow(0), '\r', `${CSI}0J`);
|
|
36
|
+
this.cursorCol = 0;
|
|
37
|
+
this.prev = null;
|
|
38
|
+
}
|
|
39
|
+
if (next.height > this.physicalRows)
|
|
40
|
+
parts.push(this.grow(next.height));
|
|
41
|
+
if (next.height < this.contentHeight)
|
|
42
|
+
parts.push(this.eraseBelow(next.height));
|
|
43
|
+
let lastStyle = null;
|
|
44
|
+
for (let row = 0; row < next.height; row++) {
|
|
45
|
+
for (let col = 0; col < next.width; col++) {
|
|
46
|
+
const cell = next.getCell(col, row);
|
|
47
|
+
const prevCell = this.prev?.getCell(col, row);
|
|
48
|
+
if (prevCell && cellsEqual(prevCell, cell))
|
|
49
|
+
continue;
|
|
50
|
+
// Continuation cell of a wide glyph — the glyph writes it
|
|
51
|
+
if (cell.char === '')
|
|
52
|
+
continue;
|
|
53
|
+
if (this.cursorRow !== row || this.cursorCol !== col) {
|
|
54
|
+
parts.push(this.moveRow(row), `${CSI}${col + 1}G`);
|
|
55
|
+
}
|
|
56
|
+
const styleCode = buildStyleCode(cell);
|
|
57
|
+
if (styleCode !== lastStyle) {
|
|
58
|
+
parts.push(ansi.resetStyle(), styleCode);
|
|
59
|
+
lastStyle = styleCode;
|
|
60
|
+
}
|
|
61
|
+
parts.push(cell.char);
|
|
62
|
+
const advance = Math.max(1, stringWidth(cell.char));
|
|
63
|
+
this.cursorCol = col + advance >= next.width ? -1 : col + advance;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (lastStyle !== null)
|
|
67
|
+
parts.push(ansi.resetStyle());
|
|
68
|
+
this.prev = padToHeight(next, this.physicalRows);
|
|
69
|
+
this.contentHeight = next.height;
|
|
70
|
+
return parts.join('');
|
|
71
|
+
}
|
|
72
|
+
/** Record where the zone starts on screen (1-based, from CPR). */
|
|
73
|
+
setOriginRow(row) {
|
|
74
|
+
this.originRow = row;
|
|
75
|
+
}
|
|
76
|
+
/** The cursor's current row within the zone (for CPR origin math). */
|
|
77
|
+
cursorZoneRow() {
|
|
78
|
+
return Math.max(0, this.cursorRow);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Map a 0-based screen row (mouse coordinates) to a zone row, or
|
|
82
|
+
* null when unknown or outside the live content. Growth past the
|
|
83
|
+
* screen bottom scrolls the zone up, so the effective origin is
|
|
84
|
+
* clamped to keep the zone's bottom on screen.
|
|
85
|
+
*/
|
|
86
|
+
screenRowToZone(screenRow, screenHeight) {
|
|
87
|
+
if (this.originRow === null)
|
|
88
|
+
return null;
|
|
89
|
+
const effectiveOrigin = Math.min(this.originRow, screenHeight - this.physicalRows + 1);
|
|
90
|
+
const zoneRow = screenRow - (effectiveOrigin - 1);
|
|
91
|
+
if (zoneRow < 0 || zoneRow >= this.contentHeight)
|
|
92
|
+
return null;
|
|
93
|
+
return zoneRow;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Forget the zone entirely — after a suspend the shell owned the
|
|
97
|
+
* screen, so the next render starts a fresh zone at the cursor.
|
|
98
|
+
*/
|
|
99
|
+
reset() {
|
|
100
|
+
this.prev = null;
|
|
101
|
+
this.physicalRows = 0;
|
|
102
|
+
this.contentHeight = 0;
|
|
103
|
+
this.cursorRow = 0;
|
|
104
|
+
this.cursorCol = 0;
|
|
105
|
+
this.originRow = null;
|
|
106
|
+
}
|
|
107
|
+
/** Hand the top `n` rows to the terminal's scrollback. */
|
|
108
|
+
releaseTop(n) {
|
|
109
|
+
if (n <= 0)
|
|
110
|
+
return;
|
|
111
|
+
const count = Math.min(n, this.contentHeight);
|
|
112
|
+
if (this.prev)
|
|
113
|
+
this.prev = dropTopRows(this.prev, count);
|
|
114
|
+
this.physicalRows -= count;
|
|
115
|
+
this.contentHeight -= count;
|
|
116
|
+
this.cursorRow -= count;
|
|
117
|
+
if (this.originRow !== null)
|
|
118
|
+
this.originRow += count;
|
|
119
|
+
}
|
|
120
|
+
/** Place the terminal cursor at live-zone coordinates. */
|
|
121
|
+
moveCursorTo(col, row) {
|
|
122
|
+
const out = this.moveRow(row) + `${CSI}${col + 1}G`;
|
|
123
|
+
this.cursorCol = col;
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
/** Leave the cursor on a fresh line after the content. */
|
|
127
|
+
finish() {
|
|
128
|
+
let out;
|
|
129
|
+
if (this.contentHeight < this.physicalRows) {
|
|
130
|
+
out = this.moveRow(this.contentHeight) + '\r';
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
out = this.moveRow(Math.max(0, this.physicalRows - 1)) + '\r\n';
|
|
134
|
+
this.cursorRow = this.physicalRows;
|
|
135
|
+
}
|
|
136
|
+
this.cursorCol = 0;
|
|
137
|
+
return out + ansi.showCursor();
|
|
138
|
+
}
|
|
139
|
+
/** Relative row movement, tracking the new position. */
|
|
140
|
+
moveRow(row) {
|
|
141
|
+
const delta = row - this.cursorRow;
|
|
142
|
+
this.cursorRow = row;
|
|
143
|
+
if (delta === 0)
|
|
144
|
+
return '';
|
|
145
|
+
return delta > 0 ? `${CSI}${delta}B` : `${CSI}${-delta}A`;
|
|
146
|
+
}
|
|
147
|
+
/** Realise new physical lines with LF so the terminal scrolls. */
|
|
148
|
+
grow(height) {
|
|
149
|
+
const parts = [];
|
|
150
|
+
if (this.physicalRows > 0)
|
|
151
|
+
parts.push(this.moveRow(this.physicalRows - 1));
|
|
152
|
+
parts.push('\r');
|
|
153
|
+
const count = this.physicalRows === 0 ? height - 1 : height - this.physicalRows;
|
|
154
|
+
for (let i = 0; i < count; i++)
|
|
155
|
+
parts.push('\n');
|
|
156
|
+
this.cursorRow = height - 1;
|
|
157
|
+
this.cursorCol = 0;
|
|
158
|
+
this.physicalRows = height;
|
|
159
|
+
this.prev = this.prev && padToHeight(this.prev, height);
|
|
160
|
+
return parts.join('');
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Blank everything below `height`. The physical lines stay realised
|
|
164
|
+
* (as empties) so regrowth reuses them instead of scrolling new ones.
|
|
165
|
+
*/
|
|
166
|
+
eraseBelow(height) {
|
|
167
|
+
const out = this.moveRow(height) + '\r' + `${CSI}0J`;
|
|
168
|
+
this.cursorCol = 0;
|
|
169
|
+
if (this.prev)
|
|
170
|
+
this.prev = blankBelow(this.prev, height);
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function padToHeight(buffer, height) {
|
|
175
|
+
if (buffer.height >= height)
|
|
176
|
+
return buffer;
|
|
177
|
+
const next = new CellBuffer(buffer.width, height);
|
|
178
|
+
copyRows(buffer, next, 0, buffer.height, 0);
|
|
179
|
+
return next;
|
|
180
|
+
}
|
|
181
|
+
function dropTopRows(buffer, count) {
|
|
182
|
+
const next = new CellBuffer(buffer.width, Math.max(0, buffer.height - count));
|
|
183
|
+
copyRows(buffer, next, count, buffer.height, -count);
|
|
184
|
+
return next;
|
|
185
|
+
}
|
|
186
|
+
function blankBelow(buffer, fromRow) {
|
|
187
|
+
const next = new CellBuffer(buffer.width, buffer.height);
|
|
188
|
+
copyRows(buffer, next, 0, Math.min(fromRow, buffer.height), 0);
|
|
189
|
+
return next;
|
|
190
|
+
}
|
|
191
|
+
function copyRows(from, to, start, end, offset) {
|
|
192
|
+
for (let row = start; row < end; row++) {
|
|
193
|
+
for (let col = 0; col < from.width; col++) {
|
|
194
|
+
const cell = from.getCell(col, row);
|
|
195
|
+
if (cell)
|
|
196
|
+
to.setCell(col, row + offset, cell);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function buildStyleCode(cell) {
|
|
201
|
+
const parts = [];
|
|
202
|
+
if (cell.fg !== 'default')
|
|
203
|
+
parts.push(ansi.fgColor(cell.fg));
|
|
204
|
+
if (cell.bg !== 'default')
|
|
205
|
+
parts.push(ansi.bgColor(cell.bg));
|
|
206
|
+
if (cell.bold)
|
|
207
|
+
parts.push(ansi.bold());
|
|
208
|
+
if (cell.dim)
|
|
209
|
+
parts.push(ansi.dim());
|
|
210
|
+
if (cell.italic)
|
|
211
|
+
parts.push(ansi.italic());
|
|
212
|
+
if (cell.underline)
|
|
213
|
+
parts.push(ansi.underline());
|
|
214
|
+
if (cell.strikethrough)
|
|
215
|
+
parts.push(ansi.strikethrough());
|
|
216
|
+
if (cell.inverse)
|
|
217
|
+
parts.push(ansi.inverse());
|
|
218
|
+
return parts.join('');
|
|
219
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kitty graphics protocol: transmit RGBA pixels and place them scaled to
|
|
3
|
+
* a cell box, for crisp `<img>` rendering on supporting terminals. The
|
|
4
|
+
* half-block path (render/image.ts) stays the default; this activates
|
|
5
|
+
* only when capability detection reports graphics support.
|
|
6
|
+
*
|
|
7
|
+
* Protocol: commands are APC sequences `ESC _ G <key=val,...> ; <base64>
|
|
8
|
+
* ESC \`. Payloads chunk into ≤4096 base64 chars with `m=1` on every
|
|
9
|
+
* segment but the last.
|
|
10
|
+
*/
|
|
11
|
+
import type { DecodedImage } from './png.js';
|
|
12
|
+
export declare function graphicsSupported(xtversion: string | null): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Transmit (but don't display) an image's pixels under an id, for later
|
|
15
|
+
* placement. Chunked so no single APC exceeds the protocol limit.
|
|
16
|
+
*/
|
|
17
|
+
export declare function transmitImage(imageId: number, image: DecodedImage): string;
|
|
18
|
+
/**
|
|
19
|
+
* Place a transmitted image at the current cursor position, scaled to
|
|
20
|
+
* `cols`×`rows` cells, without moving the cursor (`C=1`).
|
|
21
|
+
*/
|
|
22
|
+
export declare function placeImage(imageId: number, placementId: number, cols: number, rows: number): string;
|
|
23
|
+
/** Delete one placement, leaving the transmitted image data intact. */
|
|
24
|
+
export declare function deletePlacement(imageId: number, placementId: number): string;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kitty graphics protocol: transmit RGBA pixels and place them scaled to
|
|
3
|
+
* a cell box, for crisp `<img>` rendering on supporting terminals. The
|
|
4
|
+
* half-block path (render/image.ts) stays the default; this activates
|
|
5
|
+
* only when capability detection reports graphics support.
|
|
6
|
+
*
|
|
7
|
+
* Protocol: commands are APC sequences `ESC _ G <key=val,...> ; <base64>
|
|
8
|
+
* ESC \`. Payloads chunk into ≤4096 base64 chars with `m=1` on every
|
|
9
|
+
* segment but the last.
|
|
10
|
+
*/
|
|
11
|
+
const APC = '\x1b_G';
|
|
12
|
+
const ST = '\x1b\\';
|
|
13
|
+
const CHUNK = 4096;
|
|
14
|
+
/** Terminals that speak the kitty graphics protocol (by XTVERSION name). */
|
|
15
|
+
const GRAPHICS_TERMINALS = /kitty|ghostty|wezterm/i;
|
|
16
|
+
export function graphicsSupported(xtversion) {
|
|
17
|
+
return xtversion !== null && GRAPHICS_TERMINALS.test(xtversion);
|
|
18
|
+
}
|
|
19
|
+
/** Base64 of the image's RGBA bytes. */
|
|
20
|
+
function encodePayload(image) {
|
|
21
|
+
if (typeof Buffer !== 'undefined')
|
|
22
|
+
return Buffer.from(image.rgba).toString('base64');
|
|
23
|
+
let binary = '';
|
|
24
|
+
for (const byte of image.rgba)
|
|
25
|
+
binary += String.fromCharCode(byte);
|
|
26
|
+
return btoa(binary);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Transmit (but don't display) an image's pixels under an id, for later
|
|
30
|
+
* placement. Chunked so no single APC exceeds the protocol limit.
|
|
31
|
+
*/
|
|
32
|
+
export function transmitImage(imageId, image) {
|
|
33
|
+
const payload = encodePayload(image);
|
|
34
|
+
const control = `a=t,f=32,i=${imageId},s=${image.width},v=${image.height}`;
|
|
35
|
+
if (payload.length <= CHUNK) {
|
|
36
|
+
return `${APC}${control},m=0;${payload}${ST}`;
|
|
37
|
+
}
|
|
38
|
+
const parts = [];
|
|
39
|
+
for (let offset = 0; offset < payload.length; offset += CHUNK) {
|
|
40
|
+
const chunk = payload.slice(offset, offset + CHUNK);
|
|
41
|
+
const more = offset + CHUNK < payload.length ? 1 : 0;
|
|
42
|
+
// The control keys only need to appear on the first chunk
|
|
43
|
+
const head = offset === 0 ? `${control},m=${more}` : `m=${more}`;
|
|
44
|
+
parts.push(`${APC}${head};${chunk}${ST}`);
|
|
45
|
+
}
|
|
46
|
+
return parts.join('');
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Place a transmitted image at the current cursor position, scaled to
|
|
50
|
+
* `cols`×`rows` cells, without moving the cursor (`C=1`).
|
|
51
|
+
*/
|
|
52
|
+
export function placeImage(imageId, placementId, cols, rows) {
|
|
53
|
+
return `${APC}a=p,i=${imageId},p=${placementId},c=${cols},r=${rows},C=1;${ST}`;
|
|
54
|
+
}
|
|
55
|
+
/** Delete one placement, leaving the transmitted image data intact. */
|
|
56
|
+
export function deletePlacement(imageId, placementId) {
|
|
57
|
+
return `${APC}a=d,d=i,i=${imageId},p=${placementId};${ST}`;
|
|
58
|
+
}
|
|
@@ -1,42 +1,60 @@
|
|
|
1
|
-
import { wrapText, truncateText } from '../layout/text.js';
|
|
1
|
+
import { wrapText, truncateText, truncateMiddle } from '../layout/text.js';
|
|
2
|
+
import { graphemes, charWidth, stringWidth } from '../layout/unicode.js';
|
|
3
|
+
import { parseAnsiText } from './ansi-text.js';
|
|
4
|
+
import { blendColor } from '../css/color.js';
|
|
2
5
|
/**
|
|
3
6
|
* Paint a text node's content into the buffer, respecting inherited
|
|
4
7
|
* text-align, white-space, text-overflow from ancestors.
|
|
5
8
|
*/
|
|
6
9
|
export function paintTextContent(node, buffer, box, visuals, styles, layout, clip) {
|
|
7
|
-
|
|
10
|
+
let text = node.text ?? '';
|
|
8
11
|
if (!text)
|
|
9
12
|
return;
|
|
10
13
|
if (box.width === 0 && box.height === 0)
|
|
11
14
|
return;
|
|
15
|
+
// <svt-ansi> content is pre-styled — its own SGR codes win
|
|
16
|
+
if (node.parent?.tag === 'svt-ansi') {
|
|
17
|
+
paintAnsiContent(text, buffer, box, clip);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
12
20
|
// Find text properties from the ancestor that sets them
|
|
13
21
|
const alignResult = findAncestorWithBox(node, styles, layout, s => s.textAlign !== 'left' ? s.textAlign : undefined);
|
|
14
22
|
const align = alignResult?.value ?? 'left';
|
|
15
23
|
const whiteSpace = findAncestorProp(node, styles, s => s.whiteSpace !== 'normal' ? s.whiteSpace : undefined) ?? 'normal';
|
|
16
24
|
const textOverflow = findAncestorProp(node, styles, s => s.textOverflow !== 'clip' ? s.textOverflow : undefined) ?? 'clip';
|
|
25
|
+
const textTransform = findAncestorProp(node, styles, s => s.textTransform !== 'none' ? s.textTransform : undefined);
|
|
26
|
+
if (textTransform === 'uppercase')
|
|
27
|
+
text = text.toUpperCase();
|
|
28
|
+
else if (textTransform === 'lowercase')
|
|
29
|
+
text = text.toLowerCase();
|
|
30
|
+
else if (textTransform === 'capitalize')
|
|
31
|
+
text = text.replace(/\b\w/g, c => c.toUpperCase());
|
|
17
32
|
const noWrap = whiteSpace === 'nowrap';
|
|
18
|
-
const
|
|
33
|
+
const wordBreak = findAncestorProp(node, styles, s => s.wordBreak !== 'normal' ? s.wordBreak : undefined) ?? 'normal';
|
|
19
34
|
// For truncation, use the alignment container's inner width
|
|
20
35
|
const alignBox = alignResult?.box;
|
|
21
36
|
const parentBox = node.parent ? layout.get(node.parent.id) : undefined;
|
|
22
37
|
const truncWidth = alignBox ? innerWidth(alignBox, node, styles, layout) : (parentBox?.width ?? box.width);
|
|
23
38
|
// Determine text lines
|
|
24
39
|
let lines;
|
|
25
|
-
if (noWrap && ellipsis) {
|
|
40
|
+
if (noWrap && textOverflow === 'ellipsis') {
|
|
26
41
|
lines = [truncateText(text, truncWidth)];
|
|
27
42
|
}
|
|
43
|
+
else if (noWrap && textOverflow === 'ellipsis-middle') {
|
|
44
|
+
lines = [truncateMiddle(text, truncWidth)];
|
|
45
|
+
}
|
|
28
46
|
else if (noWrap) {
|
|
29
47
|
lines = [text.substring(0, truncWidth)];
|
|
30
48
|
}
|
|
31
49
|
else {
|
|
32
|
-
lines = wrapText(text, box.width > 0 ? box.width : buffer.width);
|
|
50
|
+
lines = wrapText(text, box.width > 0 ? box.width : buffer.width, wordBreak);
|
|
33
51
|
}
|
|
34
52
|
// Compute starting x with text-align
|
|
35
53
|
let startX = box.x;
|
|
36
54
|
if (align !== 'left' && alignBox) {
|
|
37
55
|
const inW = innerWidth(alignBox, node, styles, layout);
|
|
38
56
|
const inX = innerX(alignBox, node, styles, layout);
|
|
39
|
-
const textWidth = lines[0]
|
|
57
|
+
const textWidth = lines[0] ? stringWidth(lines[0]) : 0;
|
|
40
58
|
if (align === 'center') {
|
|
41
59
|
startX = inX + Math.floor((inW - textWidth) / 2);
|
|
42
60
|
}
|
|
@@ -44,24 +62,37 @@ export function paintTextContent(node, buffer, box, visuals, styles, layout, cli
|
|
|
44
62
|
startX = inX + inW - textWidth;
|
|
45
63
|
}
|
|
46
64
|
}
|
|
65
|
+
const fgHasAlpha = visuals.fg.startsWith('#') && visuals.fg.length === 9;
|
|
66
|
+
const bgHasAlpha = visuals.bg.startsWith('#') && visuals.bg.length === 9;
|
|
47
67
|
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
48
|
-
const line = lines[lineIdx];
|
|
49
68
|
const y = box.y + lineIdx;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
69
|
+
let cx = startX;
|
|
70
|
+
for (const glyph of graphemes(lines[lineIdx])) {
|
|
71
|
+
const width = Math.max(1, charWidth(glyph));
|
|
72
|
+
const cells = [{ x: cx, char: glyph }];
|
|
73
|
+
// A wide glyph owns its neighbour cell via an empty continuation
|
|
74
|
+
for (let extra = 1; extra < width; extra++)
|
|
75
|
+
cells.push({ x: cx + extra, char: '' });
|
|
76
|
+
for (const { x, char } of cells) {
|
|
77
|
+
if (clip && (x < clip.x || x >= clip.x + clip.width || y < clip.y || y >= clip.y + clip.height))
|
|
78
|
+
continue;
|
|
79
|
+
// An alpha bg was already composited by the ancestor's fill —
|
|
80
|
+
// the cell beneath holds the blended value; don't blend twice.
|
|
81
|
+
const under = buffer.getCell(x, y)?.bg ?? 'default';
|
|
82
|
+
const bg = bgHasAlpha ? under : visuals.bg;
|
|
83
|
+
buffer.setCell(x, y, {
|
|
84
|
+
char,
|
|
85
|
+
fg: fgHasAlpha ? blendColor(bg !== 'default' ? bg : under, visuals.fg) : visuals.fg,
|
|
86
|
+
bg,
|
|
87
|
+
bold: visuals.bold,
|
|
88
|
+
italic: visuals.italic,
|
|
89
|
+
underline: visuals.underline,
|
|
90
|
+
strikethrough: visuals.strikethrough,
|
|
91
|
+
dim: visuals.dim,
|
|
92
|
+
hyperlink: visuals.hyperlink,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
cx += width;
|
|
65
96
|
}
|
|
66
97
|
}
|
|
67
98
|
}
|
|
@@ -106,6 +137,21 @@ function innerWidth(alignBox, node, styles, layout) {
|
|
|
106
137
|
const inset = findBorderInset(alignBox, node, styles, layout);
|
|
107
138
|
return alignBox.width - inset * 2;
|
|
108
139
|
}
|
|
140
|
+
/** Paint <svt-ansi> content: cells carry their own SGR styling. */
|
|
141
|
+
function paintAnsiContent(text, buffer, box, clip) {
|
|
142
|
+
const lines = parseAnsiText(text);
|
|
143
|
+
for (let row = 0; row < lines.length; row++) {
|
|
144
|
+
const y = box.y + row;
|
|
145
|
+
if (clip && (y < clip.y || y >= clip.y + clip.height))
|
|
146
|
+
continue;
|
|
147
|
+
for (let col = 0; col < lines[row].length; col++) {
|
|
148
|
+
const x = box.x + col;
|
|
149
|
+
if (clip && (x < clip.x || x >= clip.x + clip.width))
|
|
150
|
+
continue;
|
|
151
|
+
buffer.setCell(x, y, lines[row][col]);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
109
155
|
/** Find the border inset of the ancestor that owns alignBox. */
|
|
110
156
|
function findBorderInset(alignBox, node, styles, layout) {
|
|
111
157
|
let current = node.parent;
|
|
@@ -2,4 +2,11 @@ import { TermNode } from '../renderer/node.js';
|
|
|
2
2
|
import { CellBuffer } from './buffer.js';
|
|
3
3
|
import { ResolvedStyle } from '../css/compute.js';
|
|
4
4
|
import { LayoutBox } from '../layout/engine.js';
|
|
5
|
-
|
|
5
|
+
interface ClipRect {
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function paint(root: TermNode, buffer: CellBuffer, styles?: Map<number, ResolvedStyle>, layout?: Map<number, LayoutBox>, damageClip?: ClipRect): void;
|
|
12
|
+
export {};
|