@bindtty/renderer-terminal 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -0
- package/dist/ansi.d.ts +2 -0
- package/dist/ansi.js +90 -0
- package/dist/diff.d.ts +2 -0
- package/dist/diff.js +140 -0
- package/dist/frame.d.ts +13 -0
- package/dist/frame.js +227 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +5 -0
- package/dist/paint.d.ts +7 -0
- package/dist/paint.js +240 -0
- package/dist/renderer.d.ts +2 -0
- package/dist/renderer.js +18 -0
- package/dist/style.d.ts +9 -0
- package/dist/style.js +87 -0
- package/dist/types.d.ts +40 -0
- package/dist/types.js +1 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# @bindtty/renderer-terminal
|
|
2
|
+
|
|
3
|
+
Terminal renderer package for BindTTY.
|
|
4
|
+
|
|
5
|
+
Renders `LayoutNode` trees into ANSI terminal output:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
LayoutNode → Frame → cell diff → ANSI patch string
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- `createTerminalRenderer()` — stateful renderer with previous frame cache
|
|
14
|
+
- `renderer.render(layoutTree, options)` — paint → diff → ANSI
|
|
15
|
+
- `renderer.reset()` — clear previous frame (for resize/clear screen)
|
|
16
|
+
- Default focused inverse style with `focusStyle: "none"` opt-out
|
|
17
|
+
- Cell-level diff for minimal ANSI output
|
|
18
|
+
- Wide-cell frame model for CJK, common emoji, and combining marks
|
|
19
|
+
- Placeholder cells for wide grapheme continuation columns
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { createTerminalRenderer } from "@bindtty/renderer-terminal";
|
|
25
|
+
|
|
26
|
+
const renderer = createTerminalRenderer();
|
|
27
|
+
const ansi = renderer.render(layoutTree, {
|
|
28
|
+
viewport: { width: 80, height: 24 },
|
|
29
|
+
isFocused: (mounted) => interaction.isFocused(mounted)
|
|
30
|
+
});
|
|
31
|
+
stdout.write(ansi);
|
|
32
|
+
```
|
package/dist/ansi.d.ts
ADDED
package/dist/ansi.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const RESET = "\x1b[0m";
|
|
2
|
+
const foregroundColors = new Map([
|
|
3
|
+
["black", 30],
|
|
4
|
+
["red", 31],
|
|
5
|
+
["green", 32],
|
|
6
|
+
["yellow", 33],
|
|
7
|
+
["blue", 34],
|
|
8
|
+
["magenta", 35],
|
|
9
|
+
["cyan", 36],
|
|
10
|
+
["white", 37],
|
|
11
|
+
["gray", 90],
|
|
12
|
+
["brightBlack", 90],
|
|
13
|
+
["brightRed", 91],
|
|
14
|
+
["brightGreen", 92],
|
|
15
|
+
["brightYellow", 93],
|
|
16
|
+
["brightBlue", 94],
|
|
17
|
+
["brightMagenta", 95],
|
|
18
|
+
["brightCyan", 96],
|
|
19
|
+
["brightWhite", 97]
|
|
20
|
+
]);
|
|
21
|
+
const backgroundColors = new Map([
|
|
22
|
+
["black", 40],
|
|
23
|
+
["red", 41],
|
|
24
|
+
["green", 42],
|
|
25
|
+
["yellow", 43],
|
|
26
|
+
["blue", 44],
|
|
27
|
+
["magenta", 45],
|
|
28
|
+
["cyan", 46],
|
|
29
|
+
["white", 47],
|
|
30
|
+
["gray", 100],
|
|
31
|
+
["brightBlack", 100],
|
|
32
|
+
["brightRed", 101],
|
|
33
|
+
["brightGreen", 102],
|
|
34
|
+
["brightYellow", 103],
|
|
35
|
+
["brightBlue", 104],
|
|
36
|
+
["brightMagenta", 105],
|
|
37
|
+
["brightCyan", 106],
|
|
38
|
+
["brightWhite", 107]
|
|
39
|
+
]);
|
|
40
|
+
export function encodeAnsiPatch(patch) {
|
|
41
|
+
if (patch.changes.length === 0) {
|
|
42
|
+
return "";
|
|
43
|
+
}
|
|
44
|
+
let output = "";
|
|
45
|
+
for (const change of [...patch.changes].sort((left, right) => left.y - right.y || left.x - right.x)) {
|
|
46
|
+
if (change.cell.width === 0) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
output += moveCursor(change.x, change.y);
|
|
50
|
+
output += RESET;
|
|
51
|
+
output += encodeStyle(change.cell.style);
|
|
52
|
+
output += change.cell.char;
|
|
53
|
+
}
|
|
54
|
+
return output.length === 0 ? "" : output + RESET;
|
|
55
|
+
}
|
|
56
|
+
function moveCursor(x, y) {
|
|
57
|
+
return `\x1b[${y + 1};${x + 1}H`;
|
|
58
|
+
}
|
|
59
|
+
function encodeStyle(style) {
|
|
60
|
+
const codes = [];
|
|
61
|
+
if (style.bold === true) {
|
|
62
|
+
codes.push(1);
|
|
63
|
+
}
|
|
64
|
+
if (style.dim === true) {
|
|
65
|
+
codes.push(2);
|
|
66
|
+
}
|
|
67
|
+
if (style.italic === true) {
|
|
68
|
+
codes.push(3);
|
|
69
|
+
}
|
|
70
|
+
if (style.underline === true) {
|
|
71
|
+
codes.push(4);
|
|
72
|
+
}
|
|
73
|
+
if (style.inverse === true) {
|
|
74
|
+
codes.push(7);
|
|
75
|
+
}
|
|
76
|
+
if (style.foreground !== undefined) {
|
|
77
|
+
codes.push(readColorCode(foregroundColors, style.foreground, "foreground"));
|
|
78
|
+
}
|
|
79
|
+
if (style.background !== undefined) {
|
|
80
|
+
codes.push(readColorCode(backgroundColors, style.background, "background"));
|
|
81
|
+
}
|
|
82
|
+
return codes.length === 0 ? "" : `\x1b[${codes.join(";")}m`;
|
|
83
|
+
}
|
|
84
|
+
function readColorCode(colors, color, kind) {
|
|
85
|
+
const code = colors.get(color);
|
|
86
|
+
if (code === undefined) {
|
|
87
|
+
throw new Error(`Unsupported ${kind} color: ${color}`);
|
|
88
|
+
}
|
|
89
|
+
return code;
|
|
90
|
+
}
|
package/dist/diff.d.ts
ADDED
package/dist/diff.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
export function diffFrames(previous, next) {
|
|
2
|
+
if (previous === null ||
|
|
3
|
+
previous.width !== next.width ||
|
|
4
|
+
previous.height !== next.height) {
|
|
5
|
+
return createFullFramePatch(next);
|
|
6
|
+
}
|
|
7
|
+
const dirtyCells = new Set();
|
|
8
|
+
for (let y = 0; y < next.height; y += 1) {
|
|
9
|
+
for (let x = 0; x < next.width; x += 1) {
|
|
10
|
+
const index = y * next.width + x;
|
|
11
|
+
const previousCell = previous.cells[index];
|
|
12
|
+
const nextCell = next.cells[index];
|
|
13
|
+
if (nextCell && (!previousCell || !cellsEqual(previousCell, nextCell))) {
|
|
14
|
+
markChangedCell(dirtyCells, previous, next, x, y);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const dirtyCoordinates = Array.from(dirtyCells)
|
|
19
|
+
.map(parseCellKey)
|
|
20
|
+
.sort((left, right) => left.y - right.y || left.x - right.x);
|
|
21
|
+
if (!hasVisibleChange(dirtyCoordinates, previous, next)) {
|
|
22
|
+
return {
|
|
23
|
+
width: next.width,
|
|
24
|
+
height: next.height,
|
|
25
|
+
changes: []
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const changes = dirtyCoordinates
|
|
29
|
+
.flatMap(({ x, y }) => {
|
|
30
|
+
const cell = next.cells[y * next.width + x];
|
|
31
|
+
return cell
|
|
32
|
+
? [{
|
|
33
|
+
x,
|
|
34
|
+
y,
|
|
35
|
+
cell: cloneCell(cell)
|
|
36
|
+
}]
|
|
37
|
+
: [];
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
width: next.width,
|
|
41
|
+
height: next.height,
|
|
42
|
+
changes
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function hasVisibleChange(coordinates, previous, next) {
|
|
46
|
+
return coordinates.some(({ x, y }) => {
|
|
47
|
+
const index = y * next.width + x;
|
|
48
|
+
const nextCell = next.cells[index];
|
|
49
|
+
const previousCell = previous.cells[index];
|
|
50
|
+
return (nextCell !== undefined &&
|
|
51
|
+
normalizeWidth(nextCell) !== 0 &&
|
|
52
|
+
(!previousCell || !cellsEqual(previousCell, nextCell)));
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function createFullFramePatch(frame) {
|
|
56
|
+
const changes = [];
|
|
57
|
+
for (let y = 0; y < frame.height; y += 1) {
|
|
58
|
+
for (let x = 0; x < frame.width; x += 1) {
|
|
59
|
+
const cell = frame.cells[y * frame.width + x];
|
|
60
|
+
if (cell) {
|
|
61
|
+
changes.push({
|
|
62
|
+
x,
|
|
63
|
+
y,
|
|
64
|
+
cell: cloneCell(cell)
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
width: frame.width,
|
|
71
|
+
height: frame.height,
|
|
72
|
+
changes
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function markChangedCell(dirtyCells, previous, next, x, y) {
|
|
76
|
+
mark(dirtyCells, next, x, y);
|
|
77
|
+
markWideRange(dirtyCells, previous, x, y);
|
|
78
|
+
markWideRange(dirtyCells, next, x, y);
|
|
79
|
+
}
|
|
80
|
+
function markWideRange(dirtyCells, frame, x, y) {
|
|
81
|
+
const cell = frame.cells[y * frame.width + x];
|
|
82
|
+
if (!cell) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (cell.width === 2) {
|
|
86
|
+
mark(dirtyCells, frame, x, y);
|
|
87
|
+
mark(dirtyCells, frame, x + 1, y);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (cell.width === 0) {
|
|
91
|
+
mark(dirtyCells, frame, x - 1, y);
|
|
92
|
+
mark(dirtyCells, frame, x, y);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function mark(dirtyCells, frame, x, y) {
|
|
96
|
+
if (Number.isInteger(x) &&
|
|
97
|
+
Number.isInteger(y) &&
|
|
98
|
+
x >= 0 &&
|
|
99
|
+
y >= 0 &&
|
|
100
|
+
x < frame.width &&
|
|
101
|
+
y < frame.height) {
|
|
102
|
+
dirtyCells.add(`${y}:${x}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function parseCellKey(key) {
|
|
106
|
+
const [y, x] = key.split(":").map(Number);
|
|
107
|
+
return {
|
|
108
|
+
x: x ?? 0,
|
|
109
|
+
y: y ?? 0
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function cellsEqual(left, right) {
|
|
113
|
+
return (left.char === right.char &&
|
|
114
|
+
normalizeWidth(left) === normalizeWidth(right) &&
|
|
115
|
+
stylesEqual(left.style, right.style));
|
|
116
|
+
}
|
|
117
|
+
function stylesEqual(left, right) {
|
|
118
|
+
return (left.foreground === right.foreground &&
|
|
119
|
+
left.background === right.background &&
|
|
120
|
+
normalizeBoolean(left.bold) === normalizeBoolean(right.bold) &&
|
|
121
|
+
normalizeBoolean(left.dim) === normalizeBoolean(right.dim) &&
|
|
122
|
+
normalizeBoolean(left.italic) === normalizeBoolean(right.italic) &&
|
|
123
|
+
normalizeBoolean(left.underline) === normalizeBoolean(right.underline) &&
|
|
124
|
+
normalizeBoolean(left.inverse) === normalizeBoolean(right.inverse));
|
|
125
|
+
}
|
|
126
|
+
function normalizeBoolean(value) {
|
|
127
|
+
return value === true;
|
|
128
|
+
}
|
|
129
|
+
function cloneCell(cell) {
|
|
130
|
+
return {
|
|
131
|
+
char: cell.char,
|
|
132
|
+
style: { ...cell.style },
|
|
133
|
+
width: normalizeWidth(cell)
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function normalizeWidth(cell) {
|
|
137
|
+
return cell.width === 0 || cell.width === 1 || cell.width === 2
|
|
138
|
+
? cell.width
|
|
139
|
+
: 1;
|
|
140
|
+
}
|
package/dist/frame.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Cell, CellStyle, Frame } from "./types.js";
|
|
2
|
+
export declare function createFrame(width: number, height: number): Frame;
|
|
3
|
+
export declare function getCell(frame: Frame, x: number, y: number): Cell | undefined;
|
|
4
|
+
export declare function setCell(frame: Frame, x: number, y: number, cell: Cell): boolean;
|
|
5
|
+
export declare function transformCellStyle(frame: Frame, x: number, y: number, transform: (style: CellStyle) => CellStyle): boolean;
|
|
6
|
+
export declare function writeText(frame: Frame, x: number, y: number, text: string, style?: CellStyle): number;
|
|
7
|
+
export declare function frameToLines(frame: Frame): string[];
|
|
8
|
+
export declare function frameToDebugLines(frame: Frame): string[];
|
|
9
|
+
export declare function createBlankCell(style?: CellStyle): Cell;
|
|
10
|
+
export declare function createTextCell(text: string, width: 1 | 2, style?: CellStyle): Cell;
|
|
11
|
+
export declare function createPlaceholderCell(style?: CellStyle): Cell;
|
|
12
|
+
export declare function isPlaceholderCell(cell: Cell): boolean;
|
|
13
|
+
export declare function isWideLeadingCell(cell: Cell): boolean;
|
package/dist/frame.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { segmentText } from "@bindtty/text";
|
|
2
|
+
export function createFrame(width, height) {
|
|
3
|
+
const normalizedWidth = toNonNegativeInteger(width);
|
|
4
|
+
const normalizedHeight = toNonNegativeInteger(height);
|
|
5
|
+
const cellCount = normalizedWidth * normalizedHeight;
|
|
6
|
+
const cells = [];
|
|
7
|
+
for (let index = 0; index < cellCount; index += 1) {
|
|
8
|
+
cells.push(createBlankCell());
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
width: normalizedWidth,
|
|
12
|
+
height: normalizedHeight,
|
|
13
|
+
cells
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function getCell(frame, x, y) {
|
|
17
|
+
if (!isInsideFrame(frame, x, y)) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
return frame.cells[getCellIndex(frame, x, y)];
|
|
21
|
+
}
|
|
22
|
+
export function setCell(frame, x, y, cell) {
|
|
23
|
+
if (!isInsideFrame(frame, x, y)) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
const clonedCell = cloneCell(cell);
|
|
27
|
+
const width = clonedCell.width ?? 1;
|
|
28
|
+
if (width === 0) {
|
|
29
|
+
throw new Error("Invalid placeholder cell: use wide text writes to create placeholders");
|
|
30
|
+
}
|
|
31
|
+
if (width > 1 && x + width > frame.width) {
|
|
32
|
+
throw new Error("Invalid wide cell: cell width exceeds frame bounds");
|
|
33
|
+
}
|
|
34
|
+
clearCellsForWrite(frame, x, y, width);
|
|
35
|
+
setCellRaw(frame, x, y, clonedCell);
|
|
36
|
+
for (let offset = 1; offset < width; offset += 1) {
|
|
37
|
+
setCellRaw(frame, x + offset, y, createPlaceholderCell(clonedCell.style));
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
export function transformCellStyle(frame, x, y, transform) {
|
|
42
|
+
const cell = getCell(frame, x, y);
|
|
43
|
+
if (!cell) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
setCellRaw(frame, x, y, {
|
|
47
|
+
char: cell.char,
|
|
48
|
+
style: transform(cell.style),
|
|
49
|
+
width: cell.width
|
|
50
|
+
});
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
export function writeText(frame, x, y, text, style = {}) {
|
|
54
|
+
const firstLine = text.split("\n", 1)[0] ?? "";
|
|
55
|
+
let written = 0;
|
|
56
|
+
let cursorX = x;
|
|
57
|
+
for (const segment of segmentText(firstLine)) {
|
|
58
|
+
if (segment.width <= 0) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (canDrawWholeSegment(frame, cursorX, y, segment.width)) {
|
|
62
|
+
clearCellsForWrite(frame, cursorX, y, segment.width);
|
|
63
|
+
setCellRaw(frame, cursorX, y, {
|
|
64
|
+
char: segment.text,
|
|
65
|
+
style,
|
|
66
|
+
width: segment.width
|
|
67
|
+
});
|
|
68
|
+
for (let offset = 1; offset < segment.width; offset += 1) {
|
|
69
|
+
setCellRaw(frame, cursorX + offset, y, createPlaceholderCell(style));
|
|
70
|
+
}
|
|
71
|
+
written += segment.width;
|
|
72
|
+
}
|
|
73
|
+
cursorX += segment.width;
|
|
74
|
+
}
|
|
75
|
+
return written;
|
|
76
|
+
}
|
|
77
|
+
export function frameToLines(frame) {
|
|
78
|
+
const lines = [];
|
|
79
|
+
for (let y = 0; y < frame.height; y += 1) {
|
|
80
|
+
let line = "";
|
|
81
|
+
for (let x = 0; x < frame.width; x += 1) {
|
|
82
|
+
const cell = getCell(frame, x, y);
|
|
83
|
+
line += cell?.width === 0 ? "" : cell?.char ?? " ";
|
|
84
|
+
}
|
|
85
|
+
lines.push(line);
|
|
86
|
+
}
|
|
87
|
+
return lines;
|
|
88
|
+
}
|
|
89
|
+
export function frameToDebugLines(frame) {
|
|
90
|
+
const lines = [];
|
|
91
|
+
for (let y = 0; y < frame.height; y += 1) {
|
|
92
|
+
let line = "";
|
|
93
|
+
for (let x = 0; x < frame.width; x += 1) {
|
|
94
|
+
const cell = getCell(frame, x, y);
|
|
95
|
+
line += cell?.width === 0 ? "·" : cell?.char ?? " ";
|
|
96
|
+
}
|
|
97
|
+
lines.push(line);
|
|
98
|
+
}
|
|
99
|
+
return lines;
|
|
100
|
+
}
|
|
101
|
+
export function createBlankCell(style = {}) {
|
|
102
|
+
return {
|
|
103
|
+
char: " ",
|
|
104
|
+
style: cloneStyle(style),
|
|
105
|
+
width: 1
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export function createTextCell(text, width, style = {}) {
|
|
109
|
+
assertValidCell(text, width);
|
|
110
|
+
return {
|
|
111
|
+
char: text,
|
|
112
|
+
style: cloneStyle(style),
|
|
113
|
+
width
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
export function createPlaceholderCell(style = {}) {
|
|
117
|
+
return {
|
|
118
|
+
char: "",
|
|
119
|
+
style: cloneStyle(style),
|
|
120
|
+
width: 0
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export function isPlaceholderCell(cell) {
|
|
124
|
+
return cell.width === 0;
|
|
125
|
+
}
|
|
126
|
+
export function isWideLeadingCell(cell) {
|
|
127
|
+
return cell.width === 2;
|
|
128
|
+
}
|
|
129
|
+
function cloneCell(cell) {
|
|
130
|
+
const width = normalizeWidth(cell);
|
|
131
|
+
const char = width === 0 ? cell.char : normalizeChar(cell.char);
|
|
132
|
+
assertValidCell(char, width);
|
|
133
|
+
return {
|
|
134
|
+
char: width === 0 ? "" : char,
|
|
135
|
+
style: cloneStyle(cell.style),
|
|
136
|
+
width
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function cloneStyle(style) {
|
|
140
|
+
return { ...style };
|
|
141
|
+
}
|
|
142
|
+
function setCellRaw(frame, x, y, cell) {
|
|
143
|
+
if (!isInsideFrame(frame, x, y)) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
const clonedCell = cloneCell(cell);
|
|
147
|
+
const width = clonedCell.width ?? 1;
|
|
148
|
+
if (width > 1 && x + width > frame.width) {
|
|
149
|
+
throw new Error("Invalid wide cell: cell width exceeds frame bounds");
|
|
150
|
+
}
|
|
151
|
+
frame.cells[getCellIndex(frame, x, y)] = clonedCell;
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
function normalizeChar(char) {
|
|
155
|
+
return char.length > 0 ? char : " ";
|
|
156
|
+
}
|
|
157
|
+
function normalizeWidth(cell) {
|
|
158
|
+
if (cell.width === 0 || cell.width === 1 || cell.width === 2) {
|
|
159
|
+
return cell.width;
|
|
160
|
+
}
|
|
161
|
+
return 1;
|
|
162
|
+
}
|
|
163
|
+
function assertValidCell(char, width) {
|
|
164
|
+
if (width === 0) {
|
|
165
|
+
if (char !== "") {
|
|
166
|
+
throw new Error("Invalid placeholder cell: char must be empty");
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const segments = segmentText(char);
|
|
171
|
+
if (segments.length !== 1) {
|
|
172
|
+
throw new Error("Invalid text cell: char must be a single grapheme");
|
|
173
|
+
}
|
|
174
|
+
const [segment] = segments;
|
|
175
|
+
if (!segment || segment.width !== width) {
|
|
176
|
+
throw new Error(`Invalid text cell: char display width ${segment?.width ?? 0} does not match cell width ${width}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function canDrawWholeSegment(frame, x, y, width) {
|
|
180
|
+
return (Number.isInteger(x) &&
|
|
181
|
+
Number.isInteger(y) &&
|
|
182
|
+
y >= 0 &&
|
|
183
|
+
y < frame.height &&
|
|
184
|
+
x >= 0 &&
|
|
185
|
+
x + width <= frame.width);
|
|
186
|
+
}
|
|
187
|
+
function clearCellsForWrite(frame, x, y, width) {
|
|
188
|
+
for (let col = x; col < x + width; col += 1) {
|
|
189
|
+
clearWideCellAt(frame, col, y);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function clearWideCellAt(frame, x, y) {
|
|
193
|
+
const cell = getCell(frame, x, y);
|
|
194
|
+
if (!cell) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (cell.width === 2) {
|
|
198
|
+
setCellRaw(frame, x, y, createBlankCell());
|
|
199
|
+
setCellRaw(frame, x + 1, y, createBlankCell());
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (cell.width === 0) {
|
|
203
|
+
const leadingX = findWideLeadingCell(frame, x, y);
|
|
204
|
+
if (leadingX !== null) {
|
|
205
|
+
setCellRaw(frame, leadingX, y, createBlankCell());
|
|
206
|
+
setCellRaw(frame, leadingX + 1, y, createBlankCell());
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function findWideLeadingCell(frame, x, y) {
|
|
211
|
+
const previous = getCell(frame, x - 1, y);
|
|
212
|
+
return previous?.width === 2 ? x - 1 : null;
|
|
213
|
+
}
|
|
214
|
+
function getCellIndex(frame, x, y) {
|
|
215
|
+
return y * frame.width + x;
|
|
216
|
+
}
|
|
217
|
+
function isInsideFrame(frame, x, y) {
|
|
218
|
+
return (Number.isInteger(x) &&
|
|
219
|
+
Number.isInteger(y) &&
|
|
220
|
+
x >= 0 &&
|
|
221
|
+
y >= 0 &&
|
|
222
|
+
x < frame.width &&
|
|
223
|
+
y < frame.height);
|
|
224
|
+
}
|
|
225
|
+
function toNonNegativeInteger(value) {
|
|
226
|
+
return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
|
|
227
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { createBlankCell, createFrame, createPlaceholderCell, createTextCell, frameToDebugLines, frameToLines, getCell, isPlaceholderCell, isWideLeadingCell, setCell, writeText } from "./frame.js";
|
|
2
|
+
export { encodeAnsiPatch } from "./ansi.js";
|
|
3
|
+
export { diffFrames } from "./diff.js";
|
|
4
|
+
export { paintLayout } from "./paint.js";
|
|
5
|
+
export { createTerminalRenderer } from "./renderer.js";
|
|
6
|
+
export type { Cell, CellChange, CellStyle, Frame, FramePatch, RenderOptions, TerminalRenderer } from "./types.js";
|
|
7
|
+
export type { PaintOptions } from "./paint.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createBlankCell, createFrame, createPlaceholderCell, createTextCell, frameToDebugLines, frameToLines, getCell, isPlaceholderCell, isWideLeadingCell, setCell, writeText } from "./frame.js";
|
|
2
|
+
export { encodeAnsiPatch } from "./ansi.js";
|
|
3
|
+
export { diffFrames } from "./diff.js";
|
|
4
|
+
export { paintLayout } from "./paint.js";
|
|
5
|
+
export { createTerminalRenderer } from "./renderer.js";
|
package/dist/paint.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { LayoutNode, LayoutViewport } from "@bindtty/layout";
|
|
2
|
+
import type { Frame } from "./types.js";
|
|
3
|
+
export interface PaintOptions {
|
|
4
|
+
viewport: LayoutViewport;
|
|
5
|
+
isFocused?: (mounted: LayoutNode["mounted"]) => boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function paintLayout(root: LayoutNode | null, options: PaintOptions): Frame;
|
package/dist/paint.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { layoutText, readTextWrapMode, segmentText } from "@bindtty/text";
|
|
2
|
+
import { createFrame, getCell, setCell, transformCellStyle } from "./frame.js";
|
|
3
|
+
import { readPaintStyle, toBorderCellStyle, toCellStyle } from "./style.js";
|
|
4
|
+
const BORDER = {
|
|
5
|
+
topLeft: "┌",
|
|
6
|
+
topRight: "┐",
|
|
7
|
+
bottomLeft: "└",
|
|
8
|
+
bottomRight: "┘",
|
|
9
|
+
horizontal: "─",
|
|
10
|
+
vertical: "│"
|
|
11
|
+
};
|
|
12
|
+
export function paintLayout(root, options) {
|
|
13
|
+
const frame = createFrame(options.viewport.width, options.viewport.height);
|
|
14
|
+
if (root) {
|
|
15
|
+
paintNode(frame, root, options, {
|
|
16
|
+
clip: {
|
|
17
|
+
x: 0,
|
|
18
|
+
y: 0,
|
|
19
|
+
width: options.viewport.width,
|
|
20
|
+
height: options.viewport.height
|
|
21
|
+
},
|
|
22
|
+
offsetX: 0,
|
|
23
|
+
offsetY: 0
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return frame;
|
|
27
|
+
}
|
|
28
|
+
function paintNode(frame, node, options, context) {
|
|
29
|
+
const mounted = node.mounted;
|
|
30
|
+
const childContext = createChildContext(node, context);
|
|
31
|
+
if (mounted.kind !== "element") {
|
|
32
|
+
paintChildren(frame, node, options, childContext);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
switch (mounted.tag) {
|
|
36
|
+
case "screen":
|
|
37
|
+
case "vstack":
|
|
38
|
+
case "hstack":
|
|
39
|
+
paintChildren(frame, node, options, childContext);
|
|
40
|
+
paintFocusedState(frame, node, options, context);
|
|
41
|
+
return;
|
|
42
|
+
case "box":
|
|
43
|
+
paintBox(frame, node, mounted, context);
|
|
44
|
+
paintChildren(frame, node, options, childContext);
|
|
45
|
+
paintFocusedState(frame, node, options, context);
|
|
46
|
+
return;
|
|
47
|
+
case "text":
|
|
48
|
+
paintText(frame, node, mounted, context);
|
|
49
|
+
paintFocusedState(frame, node, options, context);
|
|
50
|
+
return;
|
|
51
|
+
case "spacer":
|
|
52
|
+
paintFocusedState(frame, node, options, context);
|
|
53
|
+
return;
|
|
54
|
+
case "button":
|
|
55
|
+
case "input":
|
|
56
|
+
throw new Error(`Unsupported paint element: ${mounted.tag}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function paintChildren(frame, node, options, context) {
|
|
60
|
+
for (const child of node.children) {
|
|
61
|
+
paintNode(frame, child, options, context);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function paintText(frame, node, mounted, context) {
|
|
65
|
+
if (node.rect.width <= 0 || node.rect.height <= 0) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const value = mounted.props.value;
|
|
69
|
+
const text = value === null || value === undefined ? "" : String(value);
|
|
70
|
+
const wrap = readTextWrapMode(mounted.props.wrap);
|
|
71
|
+
const textLayout = layoutText(text, {
|
|
72
|
+
width: node.rect.width,
|
|
73
|
+
wrap
|
|
74
|
+
});
|
|
75
|
+
const lines = textLayout.lines.slice(0, node.rect.height);
|
|
76
|
+
const style = toCellStyle(readPaintStyle(mounted.props));
|
|
77
|
+
for (let row = 0; row < lines.length; row += 1) {
|
|
78
|
+
writeTextClipped(frame, node.rect.x + context.offsetX, node.rect.y + row + context.offsetY, lines[row] ?? "", node.rect.width, style, context);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function paintBox(frame, node, mounted, context) {
|
|
82
|
+
const style = readPaintStyle(mounted.props);
|
|
83
|
+
if (style.background !== undefined) {
|
|
84
|
+
fillRect(frame, offsetRect(node.rect, context), {
|
|
85
|
+
background: style.background
|
|
86
|
+
}, context);
|
|
87
|
+
}
|
|
88
|
+
if (shouldPaintBorder(style.border)) {
|
|
89
|
+
paintBorder(frame, offsetRect(node.rect, context), toBorderCellStyle(style), context);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function fillRect(frame, rect, style, context) {
|
|
93
|
+
for (let y = rect.y; y < rect.y + rect.height; y += 1) {
|
|
94
|
+
for (let x = rect.x; x < rect.x + rect.width; x += 1) {
|
|
95
|
+
setCellClipped(frame, x, y, {
|
|
96
|
+
char: " ",
|
|
97
|
+
style,
|
|
98
|
+
width: 1
|
|
99
|
+
}, context);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function paintBorder(frame, rect, style, context) {
|
|
104
|
+
const { x, y, width, height } = rect;
|
|
105
|
+
if (width <= 0 || height <= 0) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (width === 1) {
|
|
109
|
+
for (let row = y; row < y + height; row += 1) {
|
|
110
|
+
paintChar(frame, x, row, BORDER.vertical, style, context);
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (height === 1) {
|
|
115
|
+
for (let col = x; col < x + width; col += 1) {
|
|
116
|
+
paintChar(frame, col, y, BORDER.horizontal, style, context);
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
paintChar(frame, x, y, BORDER.topLeft, style, context);
|
|
121
|
+
paintChar(frame, x + width - 1, y, BORDER.topRight, style, context);
|
|
122
|
+
paintChar(frame, x, y + height - 1, BORDER.bottomLeft, style, context);
|
|
123
|
+
paintChar(frame, x + width - 1, y + height - 1, BORDER.bottomRight, style, context);
|
|
124
|
+
for (let col = x + 1; col < x + width - 1; col += 1) {
|
|
125
|
+
paintChar(frame, col, y, BORDER.horizontal, style, context);
|
|
126
|
+
paintChar(frame, col, y + height - 1, BORDER.horizontal, style, context);
|
|
127
|
+
}
|
|
128
|
+
for (let row = y + 1; row < y + height - 1; row += 1) {
|
|
129
|
+
paintChar(frame, x, row, BORDER.vertical, style, context);
|
|
130
|
+
paintChar(frame, x + width - 1, row, BORDER.vertical, style, context);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function paintChar(frame, x, y, char, style, context) {
|
|
134
|
+
setCellClipped(frame, x, y, {
|
|
135
|
+
char,
|
|
136
|
+
style,
|
|
137
|
+
width: 1
|
|
138
|
+
}, context);
|
|
139
|
+
}
|
|
140
|
+
function shouldPaintBorder(border) {
|
|
141
|
+
if (typeof border === "number") {
|
|
142
|
+
return border > 0;
|
|
143
|
+
}
|
|
144
|
+
return border === true;
|
|
145
|
+
}
|
|
146
|
+
function paintFocusedState(frame, node, options, context) {
|
|
147
|
+
if (options.isFocused?.(node.mounted) !== true) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (node.mounted.kind === "element" &&
|
|
151
|
+
readPaintStyle(node.mounted.props).focusStyle === "none") {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const { x, y, width, height } = offsetRect(node.rect, context);
|
|
155
|
+
for (let row = y; row < y + height; row += 1) {
|
|
156
|
+
for (let col = x; col < x + width; col += 1) {
|
|
157
|
+
if (!isInsideRect(context.clip, col, row)) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const cell = getCell(frame, col, row);
|
|
161
|
+
if (cell) {
|
|
162
|
+
transformCellStyle(frame, col, row, (style) => ({
|
|
163
|
+
...style,
|
|
164
|
+
inverse: true
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function writeTextClipped(frame, x, y, text, maxWidth, style, context) {
|
|
171
|
+
let written = 0;
|
|
172
|
+
let cursorX = x;
|
|
173
|
+
let usedWidth = 0;
|
|
174
|
+
for (const segment of segmentText(text)) {
|
|
175
|
+
if (segment.width <= 0) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (usedWidth + segment.width > maxWidth) {
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
if (canDrawWholeSegment(cursorX, y, segment.width, context.clip)) {
|
|
182
|
+
setCellClipped(frame, cursorX, y, {
|
|
183
|
+
char: segment.text,
|
|
184
|
+
style,
|
|
185
|
+
width: segment.width
|
|
186
|
+
}, context);
|
|
187
|
+
written += segment.width;
|
|
188
|
+
}
|
|
189
|
+
cursorX += segment.width;
|
|
190
|
+
usedWidth += segment.width;
|
|
191
|
+
}
|
|
192
|
+
return written;
|
|
193
|
+
}
|
|
194
|
+
function canDrawWholeSegment(x, y, width, clip) {
|
|
195
|
+
return (y >= clip.y &&
|
|
196
|
+
y < clip.y + clip.height &&
|
|
197
|
+
x >= clip.x &&
|
|
198
|
+
x + width <= clip.x + clip.width);
|
|
199
|
+
}
|
|
200
|
+
function setCellClipped(frame, x, y, cell, context) {
|
|
201
|
+
if (!isInsideRect(context.clip, x, y)) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
return setCell(frame, x, y, cell);
|
|
205
|
+
}
|
|
206
|
+
function createChildContext(node, context) {
|
|
207
|
+
const nodeClip = node.clip ? offsetRect(node.clip, context) : context.clip;
|
|
208
|
+
const clip = intersectRects(context.clip, nodeClip);
|
|
209
|
+
return {
|
|
210
|
+
clip,
|
|
211
|
+
offsetX: context.offsetX - (node.scrollOffset?.x ?? 0),
|
|
212
|
+
offsetY: context.offsetY - (node.scrollOffset?.y ?? 0)
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function offsetRect(rect, context) {
|
|
216
|
+
return {
|
|
217
|
+
x: rect.x + context.offsetX,
|
|
218
|
+
y: rect.y + context.offsetY,
|
|
219
|
+
width: rect.width,
|
|
220
|
+
height: rect.height
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function intersectRects(first, second) {
|
|
224
|
+
const x = Math.max(first.x, second.x);
|
|
225
|
+
const y = Math.max(first.y, second.y);
|
|
226
|
+
const right = Math.min(first.x + first.width, second.x + second.width);
|
|
227
|
+
const bottom = Math.min(first.y + first.height, second.y + second.height);
|
|
228
|
+
return {
|
|
229
|
+
x,
|
|
230
|
+
y,
|
|
231
|
+
width: Math.max(0, right - x),
|
|
232
|
+
height: Math.max(0, bottom - y)
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function isInsideRect(rect, x, y) {
|
|
236
|
+
return (x >= rect.x &&
|
|
237
|
+
y >= rect.y &&
|
|
238
|
+
x < rect.x + rect.width &&
|
|
239
|
+
y < rect.y + rect.height);
|
|
240
|
+
}
|
package/dist/renderer.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { encodeAnsiPatch } from "./ansi.js";
|
|
2
|
+
import { diffFrames } from "./diff.js";
|
|
3
|
+
import { paintLayout } from "./paint.js";
|
|
4
|
+
export function createTerminalRenderer() {
|
|
5
|
+
let previousFrame = null;
|
|
6
|
+
return {
|
|
7
|
+
render(root, options) {
|
|
8
|
+
const nextFrame = paintLayout(root, options);
|
|
9
|
+
const patch = diffFrames(previousFrame, nextFrame);
|
|
10
|
+
const ansi = encodeAnsiPatch(patch);
|
|
11
|
+
previousFrame = nextFrame;
|
|
12
|
+
return ansi;
|
|
13
|
+
},
|
|
14
|
+
reset() {
|
|
15
|
+
previousFrame = null;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
package/dist/style.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CellStyle } from "./types.js";
|
|
2
|
+
export interface PaintStyle extends CellStyle {
|
|
3
|
+
border?: boolean | number;
|
|
4
|
+
borderColor?: string;
|
|
5
|
+
focusStyle?: "inverse" | "none";
|
|
6
|
+
}
|
|
7
|
+
export declare function readPaintStyle(props: Record<string, unknown>): PaintStyle;
|
|
8
|
+
export declare function toCellStyle(style: PaintStyle): CellStyle;
|
|
9
|
+
export declare function toBorderCellStyle(style: PaintStyle): CellStyle;
|
package/dist/style.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const kebabStyleAliases = new Map([
|
|
2
|
+
["border-color", "borderColor"]
|
|
3
|
+
]);
|
|
4
|
+
export function readPaintStyle(props) {
|
|
5
|
+
const normalized = normalizePaintProps(props);
|
|
6
|
+
const style = {};
|
|
7
|
+
const color = readString(normalized, "color");
|
|
8
|
+
const foreground = readString(normalized, "foreground");
|
|
9
|
+
if (color !== undefined && foreground !== undefined) {
|
|
10
|
+
throw new Error("Duplicate paint prop: foreground / color");
|
|
11
|
+
}
|
|
12
|
+
if (color !== undefined) {
|
|
13
|
+
style.foreground = color;
|
|
14
|
+
}
|
|
15
|
+
if (foreground !== undefined) {
|
|
16
|
+
style.foreground = foreground;
|
|
17
|
+
}
|
|
18
|
+
setStringStyle(style, normalized, "background");
|
|
19
|
+
setStringStyle(style, normalized, "borderColor");
|
|
20
|
+
setBooleanStyle(style, normalized, "bold");
|
|
21
|
+
setBooleanStyle(style, normalized, "dim");
|
|
22
|
+
setBooleanStyle(style, normalized, "italic");
|
|
23
|
+
setBooleanStyle(style, normalized, "underline");
|
|
24
|
+
setBooleanStyle(style, normalized, "inverse");
|
|
25
|
+
const focusStyle = normalized.focusStyle;
|
|
26
|
+
if (focusStyle === "inverse" || focusStyle === "none") {
|
|
27
|
+
style.focusStyle = focusStyle;
|
|
28
|
+
}
|
|
29
|
+
const border = normalized.border;
|
|
30
|
+
if (typeof border === "boolean" || typeof border === "number") {
|
|
31
|
+
style.border = border;
|
|
32
|
+
}
|
|
33
|
+
return style;
|
|
34
|
+
}
|
|
35
|
+
export function toCellStyle(style) {
|
|
36
|
+
const cellStyle = {};
|
|
37
|
+
setDefinedString(cellStyle, "foreground", style.foreground);
|
|
38
|
+
setDefinedString(cellStyle, "background", style.background);
|
|
39
|
+
setTrueBoolean(cellStyle, "bold", style.bold);
|
|
40
|
+
setTrueBoolean(cellStyle, "dim", style.dim);
|
|
41
|
+
setTrueBoolean(cellStyle, "italic", style.italic);
|
|
42
|
+
setTrueBoolean(cellStyle, "underline", style.underline);
|
|
43
|
+
setTrueBoolean(cellStyle, "inverse", style.inverse);
|
|
44
|
+
return cellStyle;
|
|
45
|
+
}
|
|
46
|
+
export function toBorderCellStyle(style) {
|
|
47
|
+
return {
|
|
48
|
+
...toCellStyle(style),
|
|
49
|
+
...(style.borderColor === undefined ? {} : { foreground: style.borderColor })
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function normalizePaintProps(props) {
|
|
53
|
+
const normalized = {};
|
|
54
|
+
for (const [name, value] of Object.entries(props)) {
|
|
55
|
+
const canonicalName = kebabStyleAliases.get(name) ?? name;
|
|
56
|
+
if (canonicalName in normalized && canonicalName !== name) {
|
|
57
|
+
throw new Error(`Duplicate paint prop: ${canonicalName} / ${name}`);
|
|
58
|
+
}
|
|
59
|
+
normalized[canonicalName] = value;
|
|
60
|
+
}
|
|
61
|
+
return normalized;
|
|
62
|
+
}
|
|
63
|
+
function readString(props, name) {
|
|
64
|
+
const value = props[name];
|
|
65
|
+
return typeof value === "string" ? value : undefined;
|
|
66
|
+
}
|
|
67
|
+
function setStringStyle(style, props, name) {
|
|
68
|
+
const value = readString(props, name);
|
|
69
|
+
if (value !== undefined) {
|
|
70
|
+
style[name] = value;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function setBooleanStyle(style, props, name) {
|
|
74
|
+
if (props[name] === true) {
|
|
75
|
+
style[name] = true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function setDefinedString(style, name, value) {
|
|
79
|
+
if (value !== undefined) {
|
|
80
|
+
style[name] = value;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function setTrueBoolean(style, name, value) {
|
|
84
|
+
if (value === true) {
|
|
85
|
+
style[name] = true;
|
|
86
|
+
}
|
|
87
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface Frame {
|
|
2
|
+
width: number;
|
|
3
|
+
height: number;
|
|
4
|
+
cells: Cell[];
|
|
5
|
+
}
|
|
6
|
+
export interface Cell {
|
|
7
|
+
char: string;
|
|
8
|
+
style: CellStyle;
|
|
9
|
+
width?: 0 | 1 | 2;
|
|
10
|
+
}
|
|
11
|
+
export interface CellStyle {
|
|
12
|
+
foreground?: string;
|
|
13
|
+
background?: string;
|
|
14
|
+
bold?: boolean;
|
|
15
|
+
dim?: boolean;
|
|
16
|
+
italic?: boolean;
|
|
17
|
+
underline?: boolean;
|
|
18
|
+
inverse?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface FramePatch {
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
changes: CellChange[];
|
|
24
|
+
}
|
|
25
|
+
export interface CellChange {
|
|
26
|
+
x: number;
|
|
27
|
+
y: number;
|
|
28
|
+
cell: Cell;
|
|
29
|
+
}
|
|
30
|
+
export interface RenderOptions {
|
|
31
|
+
viewport: {
|
|
32
|
+
width: number;
|
|
33
|
+
height: number;
|
|
34
|
+
};
|
|
35
|
+
isFocused?: (mounted: import("@bindtty/vnode").MountedNode) => boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface TerminalRenderer {
|
|
38
|
+
render(root: import("@bindtty/layout").LayoutNode | null, options: RenderOptions): string;
|
|
39
|
+
reset(): void;
|
|
40
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bindtty/renderer-terminal",
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Terminal renderer package for BindTTY.",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -p tsconfig.json",
|
|
20
|
+
"prepublishOnly": "npm run build",
|
|
21
|
+
"test": "npm run build --workspace @bindtty/text && npm run build --workspace @bindtty/layout && npm run build && tsc -p test/tsconfig.json && node --test test/dist/*.test.js"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@bindtty/layout": "0.1.0-alpha.1",
|
|
25
|
+
"@bindtty/text": "0.1.0-alpha.1"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@bindtty/signal": "0.1.0-alpha.1",
|
|
29
|
+
"@bindtty/vnode": "0.1.0-alpha.1",
|
|
30
|
+
"@types/node": "^22.0.0",
|
|
31
|
+
"typescript": "^5.5.0"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/lithdoo/BindTTY.git",
|
|
43
|
+
"directory": "packages/renderer-terminal"
|
|
44
|
+
},
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/lithdoo/BindTTY/issues"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://github.com/lithdoo/BindTTY/tree/main/packages/renderer-terminal#readme"
|
|
49
|
+
}
|