@flowtty/core 1.0.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 ADDED
@@ -0,0 +1,35 @@
1
+ # @flowtty/core
2
+
3
+ The framework-free core of [flowtty](https://github.com/mellonis/flowtty) — a library for building terminal apps in React.
4
+
5
+ This package has **no React and no Node dependency** in its main surface. It is the shared data model every adapter (React, future Svelte) and every backend (TTY, inline, test) depends on:
6
+
7
+ - `Buffer`, `Cell`, `Style`, `Key` — the cell-grid model a renderer paints into.
8
+ - `Backend` — the interface between a framework adapter and its output (a real TTY, an inline live region, an in-memory test surface).
9
+ - Pure utilities — `wrap`, `splitVisualLines`, `windowAround`, and the input/state reducers.
10
+
11
+ Most app authors don't install this directly — they use [`@flowtty/react`](https://github.com/mellonis/flowtty/tree/master/packages/react), which re-exports the core types you need. Install `@flowtty/core` directly when **writing an adapter or a backend**.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install @flowtty/core
17
+ ```
18
+
19
+ ## Subpaths
20
+
21
+ ```ts
22
+ import { type Backend, type Buffer } from '@flowtty/core'; // public data model
23
+ import { /* Instance, paint, layout */ } from '@flowtty/core/host'; // adapter-facing internals
24
+ import { TestBackend } from '@flowtty/core/testing'; // headless test surface
25
+ ```
26
+
27
+ - `.` — the framework-free public surface (types + pure utils). No React, no Node.
28
+ - `./host` — the primitives a framework adapter wires its component lifecycle onto (`Instance` tree, paint pipeline, layout).
29
+ - `./testing` — `TestBackend`, an in-memory backend that captures frames and injects keys for unit tests.
30
+
31
+ ## See also
32
+
33
+ - [`@flowtty/react`](https://github.com/mellonis/flowtty/tree/master/packages/react) — the React adapter.
34
+ - [`@flowtty/tty-backend`](https://github.com/mellonis/flowtty/tree/master/packages/tty-backend) — the canonical TTY backend.
35
+ - [flowtty on GitHub](https://github.com/mellonis/flowtty) — full docs and examples.
@@ -0,0 +1,74 @@
1
+ import { B as Buffer } from './cells-CaXEx4lH.js';
2
+
3
+ interface Key {
4
+ /**
5
+ * Canonical name of the key. For printable ASCII characters this is the
6
+ * character itself ('a', '!', ' '). For named keys: 'return', 'escape',
7
+ * 'tab', 'backspace', 'delete', 'up', 'down', 'left', 'right', 'home',
8
+ * 'end', 'pageup', 'pagedown'.
9
+ */
10
+ name: string;
11
+ /** Raw byte sequence as received from the source (empty for synthetic keys). */
12
+ sequence: string;
13
+ ctrl: boolean;
14
+ meta: boolean;
15
+ shift: boolean;
16
+ }
17
+
18
+ interface Backend {
19
+ size(): {
20
+ width: number;
21
+ height: number;
22
+ };
23
+ draw(buffer: Buffer): void;
24
+ /**
25
+ * Subscribe to raw key events. Returns an unsubscribe function.
26
+ * Backends without an input source omit this method.
27
+ */
28
+ onKey?(handler: (key: Key) => void): () => void;
29
+ /**
30
+ * Subscribe to terminal-resize events. Returns an unsubscribe function.
31
+ * Handlers are called AFTER `size()` reflects the new dimensions.
32
+ * Backends with fixed dimensions (e.g. the test backend) omit this method.
33
+ */
34
+ onResize?(handler: () => void): () => void;
35
+ dispose?(): void;
36
+ /**
37
+ * Append plain (already-styled, ANSI-ready) lines ABOVE the live region.
38
+ * The lines scroll naturally into the terminal's scrollback. Backends
39
+ * that own the whole screen (alt-screen TTY) or are headless (TestBackend)
40
+ * may omit this; components like <Static> check for presence at runtime
41
+ * and degrade gracefully when absent.
42
+ */
43
+ printStatic?(lines: string[]): void;
44
+ /**
45
+ * Whether this backend owns the entire render area — i.e. components can
46
+ * use the full `size()` for layout and overlay larger panels (Menu cascade,
47
+ * full-screen DialogHost) on top.
48
+ *
49
+ * true — TtyBackend (alt-screen), TestBackend (full buffer)
50
+ * false — @flowtty/inline-tty-backend (only the live region is yours;
51
+ * scrollback above is append-only and out of layout control)
52
+ *
53
+ * Defaults to `true` when omitted — preserves the behavior of existing
54
+ * backends that don't declare the flag. Inline-style backends MUST set
55
+ * it to `false` so capability-sensitive components (e.g. <Menu>) can
56
+ * refuse to render rather than produce broken overflow UI.
57
+ */
58
+ fullScreen?: boolean;
59
+ /**
60
+ * Whether this backend can emit OSC 8 terminal hyperlinks (clickable links).
61
+ *
62
+ * true — TTY backends (alt-screen + inline) wrap linked cells in the
63
+ * OSC 8 escape, so a `<Link>` is clickable in supporting terminals.
64
+ * omitted — treated as false. The headless TestBackend and any output that
65
+ * /false can't render clickable links leave them out, so `<Link>` should
66
+ * degrade to styled text plus a visible URL.
67
+ *
68
+ * Feature-detected like `fullScreen`: components read it (via useBackend())
69
+ * rather than assuming a capability.
70
+ */
71
+ hyperlinks?: boolean;
72
+ }
73
+
74
+ export type { Backend as B, Key as K };
@@ -0,0 +1,31 @@
1
+ interface Style {
2
+ fg?: string;
3
+ bg?: string;
4
+ bold?: boolean;
5
+ dim?: boolean;
6
+ underline?: boolean;
7
+ inverse?: boolean;
8
+ strikethrough?: boolean;
9
+ /**
10
+ * Target URL for an OSC 8 terminal hyperlink. Backends that can emit
11
+ * clickable links (TTY / inline TTY) wrap this cell's char in the hyperlink
12
+ * escape; backends that can't (headless test surface) ignore it. Carried in
13
+ * Style so it threads through the same paint + diff path as visual attrs.
14
+ */
15
+ link?: string;
16
+ }
17
+ interface Cell {
18
+ char: string;
19
+ style: Style;
20
+ }
21
+ declare class Buffer {
22
+ readonly width: number;
23
+ readonly height: number;
24
+ private readonly cells;
25
+ constructor(width: number, height: number);
26
+ set(x: number, y: number, char: string, style?: Style): void;
27
+ get(x: number, y: number): Cell;
28
+ toString(): string;
29
+ }
30
+
31
+ export { Buffer as B, type Cell as C, type Style as S };
@@ -0,0 +1,106 @@
1
+ // src/cells.ts
2
+ var Buffer = class {
3
+ width;
4
+ height;
5
+ cells;
6
+ constructor(width, height) {
7
+ this.width = Math.max(0, width);
8
+ this.height = Math.max(0, height);
9
+ this.cells = Array.from({ length: this.width * this.height }, () => ({
10
+ char: " ",
11
+ style: {}
12
+ }));
13
+ }
14
+ set(x, y, char, style = {}) {
15
+ if (x < 0 || y < 0 || x >= this.width || y >= this.height) return;
16
+ this.cells[y * this.width + x] = { char, style };
17
+ }
18
+ // Out-of-bounds reads return a fresh blank cell (mirrors set()'s no-op).
19
+ get(x, y) {
20
+ return this.cells[y * this.width + x] ?? { char: " ", style: {} };
21
+ }
22
+ // Plain-text frame. Trailing ASCII spaces are trimmed (cosmetic), but NBSP
23
+ // (U+00A0) and other content are preserved — NBSP-safety is a flowtty value.
24
+ toString() {
25
+ const lines = [];
26
+ for (let y = 0; y < this.height; y++) {
27
+ let line = "";
28
+ for (let x = 0; x < this.width; x++) line += this.get(x, y).char;
29
+ lines.push(line.replace(/ +$/u, ""));
30
+ }
31
+ return lines.join("\n").replace(/\n+$/u, "");
32
+ }
33
+ };
34
+
35
+ // src/host/borders.ts
36
+ var DEFAULT_BORDER_STYLE = "round";
37
+ var GRID_CHARS = {
38
+ single: { h: "\u2500", v: "\u2502", tl: "\u250C", tr: "\u2510", bl: "\u2514", br: "\u2518", tDown: "\u252C", tUp: "\u2534", tRight: "\u251C", tLeft: "\u2524", cross: "\u253C" },
39
+ round: { h: "\u2500", v: "\u2502", tl: "\u256D", tr: "\u256E", bl: "\u2570", br: "\u256F", tDown: "\u252C", tUp: "\u2534", tRight: "\u251C", tLeft: "\u2524", cross: "\u253C" },
40
+ double: { h: "\u2550", v: "\u2551", tl: "\u2554", tr: "\u2557", bl: "\u255A", br: "\u255D", tDown: "\u2566", tUp: "\u2569", tRight: "\u2560", tLeft: "\u2563", cross: "\u256C" },
41
+ bold: { h: "\u2501", v: "\u2503", tl: "\u250F", tr: "\u2513", bl: "\u2517", br: "\u251B", tDown: "\u2533", tUp: "\u253B", tRight: "\u2523", tLeft: "\u252B", cross: "\u254B" },
42
+ classic: { h: "-", v: "|", tl: "+", tr: "+", bl: "+", br: "+", tDown: "+", tUp: "+", tRight: "+", tLeft: "+", cross: "+" }
43
+ };
44
+ var BORDER_CHARS = GRID_CHARS;
45
+
46
+ // src/wrap.ts
47
+ var ELLIPSIS = "\u2026";
48
+ function wrapText(text, width, mode) {
49
+ if (width < 0) width = 0;
50
+ const out = [];
51
+ for (const source of text.split("\n")) {
52
+ if (mode === "none") {
53
+ out.push(source);
54
+ continue;
55
+ }
56
+ if (mode === "truncate") {
57
+ out.push(truncateLine(source, width));
58
+ continue;
59
+ }
60
+ wrapLine(source, width, out);
61
+ }
62
+ if (out.length === 0) out.push("");
63
+ return out;
64
+ }
65
+ function truncateLine(line, width) {
66
+ if (width <= 0) return "";
67
+ const chars = [...line];
68
+ if (chars.length <= width) return line;
69
+ if (width === 1) return ELLIPSIS;
70
+ return chars.slice(0, width - 1).join("") + ELLIPSIS;
71
+ }
72
+ function wrapLine(line, width, out) {
73
+ if (width === 0) {
74
+ out.push("");
75
+ return;
76
+ }
77
+ if (line === "") {
78
+ out.push("");
79
+ return;
80
+ }
81
+ let current = "";
82
+ for (const word of line.split(" ")) {
83
+ const candidate = current ? current + " " + word : word;
84
+ if ([...candidate].length <= width) {
85
+ current = candidate;
86
+ continue;
87
+ }
88
+ if (current) {
89
+ out.push(current);
90
+ current = "";
91
+ }
92
+ if ([...word].length > width) {
93
+ let remainder = [...word];
94
+ while (remainder.length > width) {
95
+ out.push(remainder.slice(0, width).join(""));
96
+ remainder = remainder.slice(width);
97
+ }
98
+ current = remainder.join("");
99
+ } else {
100
+ current = word;
101
+ }
102
+ }
103
+ if (current) out.push(current);
104
+ }
105
+
106
+ export { BORDER_CHARS, Buffer, DEFAULT_BORDER_STYLE, GRID_CHARS, wrapText };
@@ -0,0 +1,8 @@
1
+ import { C as Container } from '../host-DttBG-OZ.js';
2
+ export { B as BORDER_CHARS, H as HostType, I as Instance, R as Rect, T as TextInstance, Y as Yoga, e as YogaNode, f as appendChild, g as applyProps, h as computeLayout, i as createInstance, j as createTextInstance, k as getYoga, l as insertBefore, m as layoutOf, n as measureText, o as ownText, r as refreshMeasure, p as removeChild } from '../host-DttBG-OZ.js';
3
+ import { B as Buffer } from '../cells-CaXEx4lH.js';
4
+ import 'yoga-layout/load';
5
+
6
+ declare function paint(container: Container, width: number, height: number): Buffer;
7
+
8
+ export { Container, paint };
@@ -0,0 +1,357 @@
1
+ import { wrapText, Buffer, BORDER_CHARS } from '../chunk-D6HW2BNM.js';
2
+ export { BORDER_CHARS } from '../chunk-D6HW2BNM.js';
3
+ import { loadYoga, FlexDirection, PositionType, Edge, Gutter, Display, Wrap, Justify, Align, MeasureMode } from 'yoga-layout/load';
4
+
5
+ var yogaPromise = null;
6
+ function getYoga() {
7
+ yogaPromise ??= loadYoga();
8
+ return yogaPromise;
9
+ }
10
+
11
+ // src/host/host.ts
12
+ function createInstance(type, props, Yoga) {
13
+ const node = Yoga.Node.create();
14
+ const inst = { type: "box", props, yogaNode: node, children: [] };
15
+ applyProps(inst, props);
16
+ return inst;
17
+ }
18
+ function createTextInstance(text, _Yoga) {
19
+ return { type: "text", text };
20
+ }
21
+ function applyProps(inst, props, _Yoga) {
22
+ inst.props = props;
23
+ const n = inst.yogaNode;
24
+ if (typeof props.width === "number") n.setWidth(props.width);
25
+ else if (typeof props.width === "string" && props.width.endsWith("%")) {
26
+ n.setWidthPercent(parseFloat(props.width));
27
+ } else n.setWidthAuto();
28
+ if (typeof props.height === "number") n.setHeight(props.height);
29
+ else if (typeof props.height === "string" && props.height.endsWith("%")) {
30
+ n.setHeightPercent(parseFloat(props.height));
31
+ } else n.setHeightAuto();
32
+ n.setFlexDirection(
33
+ props.flexDirection === "row" ? FlexDirection.Row : FlexDirection.Column
34
+ );
35
+ n.setPositionType(props.position === "absolute" ? PositionType.Absolute : PositionType.Static);
36
+ if (props.top !== void 0) n.setPosition(Edge.Top, props.top);
37
+ if (props.left !== void 0) n.setPosition(Edge.Left, props.left);
38
+ if (props.right !== void 0) n.setPosition(Edge.Right, props.right);
39
+ if (props.bottom !== void 0) n.setPosition(Edge.Bottom, props.bottom);
40
+ const borderWidth = props.border ? 1 : 0;
41
+ n.setBorder(Edge.Top, borderWidth);
42
+ n.setBorder(Edge.Right, borderWidth);
43
+ n.setBorder(Edge.Bottom, borderWidth);
44
+ n.setBorder(Edge.Left, borderWidth);
45
+ const padTop = props.paddingTop ?? props.paddingY ?? props.padding ?? 0;
46
+ const padRight = props.paddingRight ?? props.paddingX ?? props.padding ?? 0;
47
+ const padBottom = props.paddingBottom ?? props.paddingY ?? props.padding ?? 0;
48
+ const padLeft = props.paddingLeft ?? props.paddingX ?? props.padding ?? 0;
49
+ n.setPadding(Edge.Top, padTop);
50
+ n.setPadding(Edge.Right, padRight);
51
+ n.setPadding(Edge.Bottom, padBottom);
52
+ n.setPadding(Edge.Left, padLeft);
53
+ const marTop = props.marginTop ?? props.marginY ?? props.margin ?? 0;
54
+ const marRight = props.marginRight ?? props.marginX ?? props.margin ?? 0;
55
+ const marBottom = props.marginBottom ?? props.marginY ?? props.margin ?? 0;
56
+ const marLeft = props.marginLeft ?? props.marginX ?? props.margin ?? 0;
57
+ n.setMargin(Edge.Top, marTop);
58
+ n.setMargin(Edge.Right, marRight);
59
+ n.setMargin(Edge.Bottom, marBottom);
60
+ n.setMargin(Edge.Left, marLeft);
61
+ const rGap = props.rowGap ?? props.gap ?? 0;
62
+ const cGap = props.columnGap ?? props.gap ?? 0;
63
+ n.setGap(Gutter.Row, rGap);
64
+ n.setGap(Gutter.Column, cGap);
65
+ n.setFlexGrow(props.flexGrow ?? 0);
66
+ n.setFlexShrink(props.flexShrink ?? 0);
67
+ if (typeof props.flexBasis === "number") {
68
+ n.setFlexBasis(props.flexBasis);
69
+ } else if (typeof props.flexBasis === "string" && props.flexBasis.endsWith("%")) {
70
+ n.setFlexBasisPercent(parseFloat(props.flexBasis));
71
+ } else {
72
+ n.setFlexBasisAuto();
73
+ }
74
+ n.setMinWidth(props.minWidth);
75
+ n.setMaxWidth(props.maxWidth);
76
+ n.setMinHeight(props.minHeight);
77
+ n.setMaxHeight(props.maxHeight);
78
+ n.setAspectRatio(props.aspectRatio);
79
+ n.setDisplay(props.display === "none" ? Display.None : Display.Flex);
80
+ n.setFlexWrap(wrapMap(props.flexWrap));
81
+ n.setAlignContent(acMap(props.alignContent));
82
+ n.setJustifyContent(jcMap(props.justifyContent));
83
+ n.setAlignItems(aiMap(props.alignItems));
84
+ }
85
+ function wrapMap(v) {
86
+ switch (v) {
87
+ case "wrap":
88
+ return Wrap.Wrap;
89
+ case "wrap-reverse":
90
+ return Wrap.WrapReverse;
91
+ default:
92
+ return Wrap.NoWrap;
93
+ }
94
+ }
95
+ function jcMap(v) {
96
+ switch (v) {
97
+ case "center":
98
+ return Justify.Center;
99
+ case "flex-end":
100
+ return Justify.FlexEnd;
101
+ case "space-between":
102
+ return Justify.SpaceBetween;
103
+ case "space-around":
104
+ return Justify.SpaceAround;
105
+ case "space-evenly":
106
+ return Justify.SpaceEvenly;
107
+ default:
108
+ return Justify.FlexStart;
109
+ }
110
+ }
111
+ function aiMap(v) {
112
+ switch (v) {
113
+ case "center":
114
+ return Align.Center;
115
+ case "flex-end":
116
+ return Align.FlexEnd;
117
+ case "flex-start":
118
+ return Align.FlexStart;
119
+ // Default 'stretch' (matches React Native; deviates from CSS flex-start).
120
+ // Rationale: most TUI use cases want children to fill cross-axis (e.g. a
121
+ // column dialog wants its TextInput/buttons to span the dialog width); CSS's
122
+ // flex-start default forces content-sizing which causes percentage children
123
+ // to collapse and onLayout-driven scroll to misreport viewport widths.
124
+ default:
125
+ return Align.Stretch;
126
+ }
127
+ }
128
+ function acMap(v) {
129
+ switch (v) {
130
+ case "flex-end":
131
+ return Align.FlexEnd;
132
+ case "center":
133
+ return Align.Center;
134
+ case "space-between":
135
+ return Align.SpaceBetween;
136
+ case "space-around":
137
+ return Align.SpaceAround;
138
+ case "space-evenly":
139
+ return Align.SpaceEvenly;
140
+ case "stretch":
141
+ return Align.Stretch;
142
+ default:
143
+ return Align.FlexStart;
144
+ }
145
+ }
146
+ function measureText(text) {
147
+ const lines = text.split("\n");
148
+ const width = lines.reduce((m, l) => Math.max(m, [...l].length), 0);
149
+ return { width, height: lines.length };
150
+ }
151
+ function ownText(inst) {
152
+ return inst.children.filter((c) => c.type === "text").map((c) => c.text).join("");
153
+ }
154
+ function refreshMeasure(inst, _Yoga) {
155
+ const hasText = inst.children.some((c) => c.type === "text");
156
+ const hasBox = inst.children.some((c) => c.type === "box");
157
+ if (hasText && !hasBox) {
158
+ const text = ownText(inst);
159
+ const mode = inst.props.wrap ?? "none";
160
+ inst.yogaNode.setMeasureFunc((width, widthMode) => {
161
+ if (mode !== "none" && (widthMode === MeasureMode.Exactly || widthMode === MeasureMode.AtMost) && Number.isFinite(width)) {
162
+ const cap = Math.max(0, Math.floor(width));
163
+ const lines = wrapText(text, cap, mode);
164
+ const longest = lines.reduce((m, l) => Math.max(m, [...l].length), 0);
165
+ return { width: longest, height: lines.length };
166
+ }
167
+ return measureText(text);
168
+ });
169
+ inst.yogaNode.markDirty();
170
+ } else {
171
+ inst.yogaNode.setMeasureFunc(null);
172
+ }
173
+ }
174
+ function appendChild(parent, child, Yoga) {
175
+ parent.children.push(child);
176
+ if (child.type === "box") {
177
+ parent.yogaNode.setMeasureFunc(null);
178
+ parent.yogaNode.insertChild(child.yogaNode, parent.yogaNode.getChildCount());
179
+ } else {
180
+ child.parent = parent;
181
+ }
182
+ refreshMeasure(parent);
183
+ }
184
+ function removeChild(parent, child, Yoga) {
185
+ const i = parent.children.indexOf(child);
186
+ if (i >= 0) parent.children.splice(i, 1);
187
+ if (child.type === "box") {
188
+ parent.yogaNode.removeChild(child.yogaNode);
189
+ child.yogaNode.freeRecursive();
190
+ } else {
191
+ child.parent = void 0;
192
+ }
193
+ refreshMeasure(parent);
194
+ }
195
+ function insertBefore(parent, child, before, Yoga) {
196
+ const i = parent.children.indexOf(before);
197
+ parent.children.splice(i < 0 ? parent.children.length : i, 0, child);
198
+ if (child.type === "box") {
199
+ const boxIndex = parent.children.filter((c) => c.type === "box").indexOf(child);
200
+ parent.yogaNode.setMeasureFunc(null);
201
+ parent.yogaNode.insertChild(child.yogaNode, boxIndex);
202
+ } else {
203
+ child.parent = parent;
204
+ }
205
+ refreshMeasure(parent);
206
+ }
207
+
208
+ // src/host/layout.ts
209
+ function computeLayout(container, width, height) {
210
+ for (const root of container.children) {
211
+ root.yogaNode.calculateLayout(width, height);
212
+ }
213
+ }
214
+ function layoutOf(inst, offsetX = 0, offsetY = 0) {
215
+ const n = inst.yogaNode;
216
+ return {
217
+ left: offsetX + n.getComputedLeft(),
218
+ top: offsetY + n.getComputedTop(),
219
+ width: n.getComputedWidth(),
220
+ height: n.getComputedHeight()
221
+ };
222
+ }
223
+
224
+ // src/host/paint.ts
225
+ function paint(container, width, height) {
226
+ const buffer = new Buffer(width, height);
227
+ for (const root of container.children) paintInstance(root, buffer, 0, 0);
228
+ return buffer;
229
+ }
230
+ function textStyleOf(inst) {
231
+ const p = inst.props;
232
+ const style = {};
233
+ if (p.color !== void 0) style.fg = p.color;
234
+ if (p.bold) style.bold = true;
235
+ if (p.dim) style.dim = true;
236
+ if (p.underline) style.underline = true;
237
+ if (p.inverse) style.inverse = true;
238
+ if (p.strikethrough) style.strikethrough = true;
239
+ if (p.link !== void 0) style.link = p.link;
240
+ if (p.backgroundColor !== void 0) style.bg = p.backgroundColor;
241
+ return style;
242
+ }
243
+ function setClipped(buffer, x, y, char, style, clip) {
244
+ if (clip !== null) {
245
+ if (x < clip.left || y < clip.top || x >= clip.left + clip.width || y >= clip.top + clip.height) return;
246
+ }
247
+ buffer.set(x, y, char, style);
248
+ }
249
+ function intersectRects(a, b) {
250
+ if (a === null) return b;
251
+ const left = Math.max(a.left, b.left);
252
+ const top = Math.max(a.top, b.top);
253
+ const right = Math.min(a.left + a.width, b.left + b.width);
254
+ const bottom = Math.min(a.top + a.height, b.top + b.height);
255
+ if (right <= left || bottom <= top) return { left, top, width: 0, height: 0 };
256
+ return { left, top, width: right - left, height: bottom - top };
257
+ }
258
+ function paintBorder(inst, buffer, box, clip) {
259
+ const style = inst.props.border;
260
+ if (!style) return;
261
+ if (box.width < 2 || box.height < 2) return;
262
+ const chars = BORDER_CHARS[style];
263
+ const cellStyle = {};
264
+ if (inst.props.borderColor !== void 0) cellStyle.fg = inst.props.borderColor;
265
+ const x0 = box.left;
266
+ const y0 = box.top;
267
+ const x1 = box.left + box.width - 1;
268
+ const y1 = box.top + box.height - 1;
269
+ setClipped(buffer, x0, y0, chars.tl, cellStyle, clip);
270
+ setClipped(buffer, x1, y0, chars.tr, cellStyle, clip);
271
+ setClipped(buffer, x0, y1, chars.bl, cellStyle, clip);
272
+ setClipped(buffer, x1, y1, chars.br, cellStyle, clip);
273
+ for (let x = x0 + 1; x < x1; x++) {
274
+ setClipped(buffer, x, y0, chars.h, cellStyle, clip);
275
+ setClipped(buffer, x, y1, chars.h, cellStyle, clip);
276
+ }
277
+ for (let y = y0 + 1; y < y1; y++) {
278
+ setClipped(buffer, x0, y, chars.v, cellStyle, clip);
279
+ setClipped(buffer, x1, y, chars.v, cellStyle, clip);
280
+ }
281
+ const title = inst.props.borderTitle;
282
+ if (title && title !== "") {
283
+ const avail = box.width - 4;
284
+ if (avail >= 1) {
285
+ const raw = ` ${title} `;
286
+ const titleChars = [...raw];
287
+ const drawN = Math.min(titleChars.length, avail);
288
+ for (let i = 0; i < drawN; i++) {
289
+ const ch = i === drawN - 1 && titleChars.length > avail ? "\u2026" : titleChars[i];
290
+ setClipped(buffer, x0 + 2 + i, y0, ch, cellStyle, clip);
291
+ }
292
+ }
293
+ }
294
+ }
295
+ function contentRectOf(inst, box) {
296
+ const n = inst.yogaNode;
297
+ const padT = n.getComputedPadding(Edge.Top) + n.getComputedBorder(Edge.Top);
298
+ const padR = n.getComputedPadding(Edge.Right) + n.getComputedBorder(Edge.Right);
299
+ const padB = n.getComputedPadding(Edge.Bottom) + n.getComputedBorder(Edge.Bottom);
300
+ const padL = n.getComputedPadding(Edge.Left) + n.getComputedBorder(Edge.Left);
301
+ return {
302
+ left: box.left + padL,
303
+ top: box.top + padT,
304
+ width: Math.max(0, box.width - padL - padR),
305
+ height: Math.max(0, box.height - padT - padB)
306
+ };
307
+ }
308
+ function paintInstance(inst, buffer, offsetX, offsetY, inheritedBg = void 0, clip = null) {
309
+ if (inst.props.display === "none") return;
310
+ const box = layoutOf(inst, offsetX, offsetY);
311
+ inst.props.onLayout?.(box);
312
+ const ownBg = inst.props.backgroundColor;
313
+ const effectiveBg = ownBg ?? inheritedBg;
314
+ if (ownBg !== void 0) {
315
+ const fillStyle = ownBg === "default" ? {} : { bg: ownBg };
316
+ for (let y = box.top; y < box.top + box.height; y++) {
317
+ for (let x = box.left; x < box.left + box.width; x++) {
318
+ setClipped(buffer, x, y, " ", fillStyle, clip);
319
+ }
320
+ }
321
+ }
322
+ paintBorder(inst, buffer, box, clip);
323
+ const text = ownText(inst);
324
+ if (text) {
325
+ const content = contentRectOf(inst, box);
326
+ const mode = inst.props.wrap ?? "none";
327
+ const lines = mode === "none" ? text.split("\n") : wrapText(text, content.width, mode);
328
+ const textStyle = textStyleOf(inst);
329
+ if (textStyle.bg === void 0 && effectiveBg !== void 0) {
330
+ textStyle.bg = effectiveBg;
331
+ }
332
+ for (let row = 0; row < lines.length; row++) {
333
+ if (row >= content.height) break;
334
+ const chars = [...lines[row] ?? ""];
335
+ for (let col = 0; col < chars.length; col++) {
336
+ if (col >= content.width) break;
337
+ const ch = chars[col];
338
+ const safe = ch.charCodeAt(0) < 32 ? " " : ch;
339
+ setClipped(buffer, content.left + col, content.top + row, safe, textStyle, clip);
340
+ }
341
+ }
342
+ }
343
+ const childClip = inst.props.overflow === "hidden" ? intersectRects(clip, contentRectOf(inst, box)) : clip;
344
+ const stackFlow = [];
345
+ const absolutes = [];
346
+ for (const child of inst.children) {
347
+ if (child.type !== "box") continue;
348
+ (child.props.position === "absolute" ? absolutes : stackFlow).push(child);
349
+ }
350
+ const byZ = (a, b) => (a.props.zIndex ?? 0) - (b.props.zIndex ?? 0);
351
+ stackFlow.sort(byZ);
352
+ absolutes.sort(byZ);
353
+ for (const child of stackFlow) paintInstance(child, buffer, box.left, box.top, effectiveBg, childClip);
354
+ for (const child of absolutes) paintInstance(child, buffer, box.left, box.top, effectiveBg, childClip);
355
+ }
356
+
357
+ export { appendChild, applyProps, computeLayout, createInstance, createTextInstance, getYoga, insertBefore, layoutOf, measureText, ownText, paint, refreshMeasure, removeChild };