take4-console 0.15.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.
Files changed (131) hide show
  1. package/CHANGELOG.md +287 -0
  2. package/LICENSE +21 -0
  3. package/README.md +986 -0
  4. package/dist/Screen/InterfaceBuilder.d.mts +43 -0
  5. package/dist/Screen/InterfaceBuilder.d.mts.map +1 -0
  6. package/dist/Screen/InterfaceBuilder.mjs +355 -0
  7. package/dist/Screen/InterfaceBuilder.mjs.map +1 -0
  8. package/dist/Screen/Pos.d.mts +52 -0
  9. package/dist/Screen/Pos.d.mts.map +1 -0
  10. package/dist/Screen/Pos.mjs +105 -0
  11. package/dist/Screen/Pos.mjs.map +1 -0
  12. package/dist/Screen/Region.d.mts +31 -0
  13. package/dist/Screen/Region.d.mts.map +1 -0
  14. package/dist/Screen/Region.mjs +73 -0
  15. package/dist/Screen/Region.mjs.map +1 -0
  16. package/dist/Screen/RegistryHolder.d.mts +6 -0
  17. package/dist/Screen/RegistryHolder.d.mts.map +1 -0
  18. package/dist/Screen/RegistryHolder.mjs +14 -0
  19. package/dist/Screen/RegistryHolder.mjs.map +1 -0
  20. package/dist/Screen/Screen.d.mts +25 -0
  21. package/dist/Screen/Screen.d.mts.map +1 -0
  22. package/dist/Screen/Screen.mjs +100 -0
  23. package/dist/Screen/Screen.mjs.map +1 -0
  24. package/dist/Screen/Size.d.mts +23 -0
  25. package/dist/Screen/Size.d.mts.map +1 -0
  26. package/dist/Screen/Size.mjs +48 -0
  27. package/dist/Screen/Size.mjs.map +1 -0
  28. package/dist/Screen/StyleRegistry.d.mts +32 -0
  29. package/dist/Screen/StyleRegistry.d.mts.map +1 -0
  30. package/dist/Screen/StyleRegistry.mjs +80 -0
  31. package/dist/Screen/StyleRegistry.mjs.map +1 -0
  32. package/dist/Screen/Window.d.mts +121 -0
  33. package/dist/Screen/Window.d.mts.map +1 -0
  34. package/dist/Screen/Window.mjs +407 -0
  35. package/dist/Screen/Window.mjs.map +1 -0
  36. package/dist/Screen/WindowManager.d.mts +86 -0
  37. package/dist/Screen/WindowManager.d.mts.map +1 -0
  38. package/dist/Screen/WindowManager.mjs +399 -0
  39. package/dist/Screen/WindowManager.mjs.map +1 -0
  40. package/dist/Screen/controls/BarChart.d.mts +29 -0
  41. package/dist/Screen/controls/BarChart.d.mts.map +1 -0
  42. package/dist/Screen/controls/BarChart.mjs +90 -0
  43. package/dist/Screen/controls/BarChart.mjs.map +1 -0
  44. package/dist/Screen/controls/Button.d.mts +16 -0
  45. package/dist/Screen/controls/Button.d.mts.map +1 -0
  46. package/dist/Screen/controls/Button.mjs +34 -0
  47. package/dist/Screen/controls/Button.mjs.map +1 -0
  48. package/dist/Screen/controls/Checkbox.d.mts +23 -0
  49. package/dist/Screen/controls/Checkbox.d.mts.map +1 -0
  50. package/dist/Screen/controls/Checkbox.mjs +55 -0
  51. package/dist/Screen/controls/Checkbox.mjs.map +1 -0
  52. package/dist/Screen/controls/LineChart.d.mts +29 -0
  53. package/dist/Screen/controls/LineChart.d.mts.map +1 -0
  54. package/dist/Screen/controls/LineChart.mjs +172 -0
  55. package/dist/Screen/controls/LineChart.mjs.map +1 -0
  56. package/dist/Screen/controls/ListBox.d.mts +34 -0
  57. package/dist/Screen/controls/ListBox.d.mts.map +1 -0
  58. package/dist/Screen/controls/ListBox.mjs +138 -0
  59. package/dist/Screen/controls/ListBox.mjs.map +1 -0
  60. package/dist/Screen/controls/ProgressBar.d.mts +26 -0
  61. package/dist/Screen/controls/ProgressBar.d.mts.map +1 -0
  62. package/dist/Screen/controls/ProgressBar.mjs +70 -0
  63. package/dist/Screen/controls/ProgressBar.mjs.map +1 -0
  64. package/dist/Screen/controls/ProgressBarV.d.mts +22 -0
  65. package/dist/Screen/controls/ProgressBarV.d.mts.map +1 -0
  66. package/dist/Screen/controls/ProgressBarV.mjs +61 -0
  67. package/dist/Screen/controls/ProgressBarV.mjs.map +1 -0
  68. package/dist/Screen/controls/Radio.d.mts +23 -0
  69. package/dist/Screen/controls/Radio.d.mts.map +1 -0
  70. package/dist/Screen/controls/Radio.mjs +55 -0
  71. package/dist/Screen/controls/Radio.mjs.map +1 -0
  72. package/dist/Screen/controls/Sparkline.d.mts +29 -0
  73. package/dist/Screen/controls/Sparkline.d.mts.map +1 -0
  74. package/dist/Screen/controls/Sparkline.mjs +82 -0
  75. package/dist/Screen/controls/Sparkline.mjs.map +1 -0
  76. package/dist/Screen/controls/Spinner.d.mts +37 -0
  77. package/dist/Screen/controls/Spinner.d.mts.map +1 -0
  78. package/dist/Screen/controls/Spinner.mjs +87 -0
  79. package/dist/Screen/controls/Spinner.mjs.map +1 -0
  80. package/dist/Screen/controls/StatusLED.d.mts +22 -0
  81. package/dist/Screen/controls/StatusLED.d.mts.map +1 -0
  82. package/dist/Screen/controls/StatusLED.mjs +51 -0
  83. package/dist/Screen/controls/StatusLED.mjs.map +1 -0
  84. package/dist/Screen/controls/Tabs.d.mts +42 -0
  85. package/dist/Screen/controls/Tabs.d.mts.map +1 -0
  86. package/dist/Screen/controls/Tabs.mjs +126 -0
  87. package/dist/Screen/controls/Tabs.mjs.map +1 -0
  88. package/dist/Screen/controls/TextArea.d.mts +41 -0
  89. package/dist/Screen/controls/TextArea.d.mts.map +1 -0
  90. package/dist/Screen/controls/TextArea.mjs +197 -0
  91. package/dist/Screen/controls/TextArea.mjs.map +1 -0
  92. package/dist/Screen/controls/TextBox.d.mts +35 -0
  93. package/dist/Screen/controls/TextBox.d.mts.map +1 -0
  94. package/dist/Screen/controls/TextBox.mjs +135 -0
  95. package/dist/Screen/controls/TextBox.mjs.map +1 -0
  96. package/dist/Screen/types.d.mts +399 -0
  97. package/dist/Screen/types.d.mts.map +1 -0
  98. package/dist/Screen/types.mjs +22 -0
  99. package/dist/Screen/types.mjs.map +1 -0
  100. package/dist/index.d.mts +26 -0
  101. package/dist/index.d.mts.map +1 -0
  102. package/dist/index.mjs +41 -0
  103. package/dist/index.mjs.map +1 -0
  104. package/package.json +72 -0
  105. package/src/Screen/InterfaceBuilder.mts +403 -0
  106. package/src/Screen/Pos.mts +119 -0
  107. package/src/Screen/Region.mts +88 -0
  108. package/src/Screen/RegistryHolder.mts +16 -0
  109. package/src/Screen/Screen.mts +103 -0
  110. package/src/Screen/Size.mts +55 -0
  111. package/src/Screen/StyleRegistry.mts +95 -0
  112. package/src/Screen/Window.mts +439 -0
  113. package/src/Screen/WindowManager.mts +472 -0
  114. package/src/Screen/controls/BarChart.mts +109 -0
  115. package/src/Screen/controls/Button.mts +40 -0
  116. package/src/Screen/controls/Checkbox.mts +66 -0
  117. package/src/Screen/controls/LineChart.mts +202 -0
  118. package/src/Screen/controls/ListBox.mts +154 -0
  119. package/src/Screen/controls/ProgressBar.mts +88 -0
  120. package/src/Screen/controls/ProgressBarV.mts +77 -0
  121. package/src/Screen/controls/Radio.mts +66 -0
  122. package/src/Screen/controls/Sparkline.mts +101 -0
  123. package/src/Screen/controls/Spinner.mts +102 -0
  124. package/src/Screen/controls/StatusLED.mts +65 -0
  125. package/src/Screen/controls/Tabs.mts +140 -0
  126. package/src/Screen/controls/TextArea.mts +194 -0
  127. package/src/Screen/controls/TextBox.mts +139 -0
  128. package/src/Screen/types.mts +416 -0
  129. package/src/demo.mts +171 -0
  130. package/src/index.mts +105 -0
  131. package/src/layout.yaml +236 -0
package/README.md ADDED
@@ -0,0 +1,986 @@
1
+ # take4-console
2
+
3
+ A terminal cell-grid rendering library for Node.js. Build rich text-mode
4
+ interfaces out of nested `Window`s with styles, borders, percentage layouts,
5
+ focus-aware keyboard/mouse input, and a YAML-driven layout builder.
6
+
7
+ Ships 15 built-in controls — buttons, text inputs, checkboxes, radios,
8
+ listboxes, tabs, status LEDs, progress bars, line/bar/sparkline charts,
9
+ spinners — and an extension API for adding your own. Pure ESM, TypeScript
10
+ first, zero runtime dependencies besides a YAML parser, one `stdout.write()`
11
+ per frame.
12
+
13
+ > **Requirements:** Node.js ≥ 18, a terminal that supports ANSI escape
14
+ > sequences and 256-colour / 24-bit colour. NerdFonts glyphs are supported
15
+ > (box-drawing, braille, block characters).
16
+
17
+ ---
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install take4-console
23
+ ```
24
+
25
+ The package is published as an **ES module** — use `import`, not `require`.
26
+ TypeScript declarations ship inside the package (`dist/index.d.mts`), so no
27
+ `@types/...` companion is needed.
28
+
29
+ ---
30
+
31
+ ## Quick start
32
+
33
+ ```typescript
34
+ import {
35
+ Screen,
36
+ WindowManager,
37
+ Button,
38
+ TextBox,
39
+ Pos,
40
+ Size,
41
+ } from 'take4-console';
42
+
43
+ // 1. Create the root screen — sized automatically to the terminal.
44
+ const screen = new Screen();
45
+ screen.fill(' ', screen.registerStyle({ background: 234 }));
46
+
47
+ // 2. Add controls. Screen registers a global StyleRegistry on construction;
48
+ // all controls created afterwards share it automatically.
49
+ const name = new TextBox(
50
+ { pos: new Pos(2, 2), size: new Size(30, 3) },
51
+ { placeholder: 'your name' },
52
+ );
53
+ screen.addChild(name);
54
+
55
+ const ok = new Button(
56
+ { pos: new Pos(2, 6), size: new Size(10, 3), label: 'OK' },
57
+ { onPress: () => console.error(`hello, ${name.getValue()}`) },
58
+ );
59
+ screen.addChild(ok);
60
+
61
+ // 3. Start the input loop. Tab cycles focus; q / Ctrl+C exits.
62
+ const wm = new WindowManager(screen, {
63
+ exitKeys: ['q', '\x03'],
64
+ onExit: () => process.exit(0),
65
+ mouse: true,
66
+ });
67
+ wm.register(name);
68
+ wm.register(ok);
69
+ wm.run();
70
+ ```
71
+
72
+ Or build the same UI declaratively from a YAML file via `InterfaceBuilder`
73
+ — see [section 7](#7-yaml--interfacebuilder-integration).
74
+
75
+ ---
76
+
77
+ ## Public API
78
+
79
+ Every symbol below is exported from the package root:
80
+
81
+ ```typescript
82
+ import {
83
+ // ── Core ───────────────────────────────────────────────────────────────
84
+ Screen, Window, Region, StyleRegistry, WindowManager,
85
+
86
+ // ── Geometry ───────────────────────────────────────────────────────────
87
+ Pos, Size, Pct, pct,
88
+
89
+ // ── Interactive controls (focusable) ───────────────────────────────────
90
+ Button, TextBox, TextArea, Checkbox, Radio, ListBox, Tabs,
91
+
92
+ // ── Read-only display controls ─────────────────────────────────────────
93
+ StatusLED, ProgressBar, ProgressBarV,
94
+ LineChart, BarChart, Sparkline, Spinner,
95
+
96
+ // ── YAML layout builder ────────────────────────────────────────────────
97
+ InterfaceBuilder,
98
+
99
+ // ── Built-in style name constants ──────────────────────────────────────
100
+ BUILTIN_WINDOW_BG,
101
+ BUILTIN_BORDER, BUILTIN_BORDER_FOCUSED, BUILTIN_BORDER_DISABLED,
102
+ BUILTIN_TEXT, BUILTIN_TEXT_FOCUSED, BUILTIN_TEXT_DISABLED,
103
+ BUILTIN_TEXT_PLACEHOLDER, BUILTIN_TEXT_CHECKED, BUILTIN_CURSOR,
104
+ } from 'take4-console';
105
+
106
+ import type {
107
+ // Primitives
108
+ Color, StyleId, Cell, CellAttributes, TerminalSize,
109
+
110
+ // Window / border
111
+ BorderStyle, WindowBorder, WindowProperties, WriteTextOptions,
112
+
113
+ // Control properties interfaces (first constructor arg)
114
+ // — WindowProperties is the base for all controls
115
+ // Control-specific options (second constructor arg)
116
+ ButtonProperties, TextBoxProperties, TextAreaProperties,
117
+ CheckboxProperties, RadioProperties,
118
+ StatusLEDProperties, ProgressBarProperties, ProgressBarVProperties,
119
+ LineChartProperties, BarChartProperties,
120
+ ListBoxProperties, TabsProperties, SparklineProperties, SpinnerProperties,
121
+
122
+ // Focus & input
123
+ Focusable, TerminalMouseEvent, WindowManagerOptions,
124
+
125
+ // YAML schema
126
+ YamlLayout, YamlWindowDef, YamlPosSpec, YamlSizeSpec,
127
+ YamlWindowType, YamlStyleDef, YamlAxisValue,
128
+ } from 'take4-console';
129
+ ```
130
+
131
+ Full control reference is in [section 9](#9-built-in-controls-reference);
132
+ style names are in [section 8](#8-built-in-style-reference); YAML layouts
133
+ in [section 7](#7-yaml--interfacebuilder-integration).
134
+
135
+ ---
136
+
137
+ ## Running the bundled demo
138
+
139
+ The repository ships a live demo that exercises every built-in control:
140
+
141
+ ```bash
142
+ git clone https://github.com/arcymag/take4-console.git
143
+ cd take4-console
144
+ npm install
145
+ npm run demo # tsx src/demo.mts
146
+ ```
147
+
148
+ Press `q` or `Ctrl+C` to exit. The demo's source (`src/demo.mts`) and layout
149
+ (`src/layout.yaml`) are good starting points for your own application.
150
+
151
+ ---
152
+
153
+ ## Table of contents
154
+
155
+ 1. [Core concepts](#1-core-concepts)
156
+ 2. [Architecture overview](#2-architecture-overview)
157
+ 3. [Style system](#3-style-system)
158
+ 4. [Implementing a custom control — step by step](#4-implementing-a-custom-control--step-by-step)
159
+ - 4.1 [Define the options interface](#41-define-the-options-interface)
160
+ - 4.2 [Create the class skeleton](#42-create-the-class-skeleton)
161
+ - 4.3 [Constructor: registry, super, styles](#43-constructor-registry-super-styles)
162
+ - 4.4 [State setters and getters](#44-state-setters-and-getters)
163
+ - 4.5 [Rendering pipeline](#45-rendering-pipeline)
164
+ - 4.6 [Dynamic borders](#46-dynamic-borders)
165
+ - 4.7 [Keyboard input — the Focusable interface](#47-keyboard-input--the-focusable-interface)
166
+ - 4.8 [Registering with WindowManager](#48-registering-with-windowmanager)
167
+ - 4.9 [Writing tests](#49-writing-tests)
168
+ 5. [Complete example 1: ProgressBar (read-only)](#5-complete-example-1-progressbar-read-only)
169
+ 6. [Complete example 2: NumberStepper (interactive)](#6-complete-example-2-numberstepper-interactive)
170
+ 7. [YAML / InterfaceBuilder integration](#7-yaml--interfacebuilder-integration)
171
+ 8. [Built-in style reference](#8-built-in-style-reference)
172
+ 9. [Built-in controls reference](#9-built-in-controls-reference)
173
+
174
+ > **Note on import paths.** Examples in sections 4–6 use **relative imports**
175
+ > (e.g. `from '../Window.mjs'`) because they show how to build a new control
176
+ > *inside this repository*. If you are extending the library from an external
177
+ > package, substitute `from 'take4-console'` instead.
178
+
179
+ ---
180
+
181
+ ## License
182
+
183
+ MIT © Jarosław Mężyk
184
+
185
+ ---
186
+
187
+ ## 1. Core concepts
188
+
189
+ | Concept | What it is |
190
+ |---|---|
191
+ | `Window` | A rectangular region with a position, size, optional border and background. Everything is a Window — layouts, dialogs, and controls. |
192
+ | `Region` | A flat `chars[]` + `styleIds[]` buffer. Every Window has two: `content` (user writes) and `region` (composited on render). |
193
+ | `StyleRegistry` | Maps integer `StyleId` → `CellAttributes`. Identical attribute objects always get the same ID (deduplication). ID 0 = empty style. |
194
+ | `StyleId` | An opaque integer. Never construct cell attributes directly — always call `registry.register(attrs)` and keep the returned ID. |
195
+ | `Screen` | A `Window` sized to the terminal. Owns the root `StyleRegistry`. Serialises its `region` into a single ANSI escape string and writes it with one `process.stdout.write()`. |
196
+ | `Pos` | Encodes position: absolute `new Pos(x, y)`, edge-relative `new Pos(-1, -1)`, percentage, or named preset (`Pos.center()`, …). |
197
+ | `Size` | Encodes size: absolute `new Size(w, h)`, percentage, or fill shorthand (`Size.fill()`, `Size.fillWidth(h)`, …). |
198
+
199
+ ---
200
+
201
+ ## 2. Architecture overview
202
+
203
+ ```
204
+ Screen (extends Window)
205
+ └─ Window ← user creates these, nests them
206
+ └─ Region ← flat cell buffer (chars[] + styleIds[])
207
+ ```
208
+
209
+ The render pipeline for every `Window.render()` call:
210
+
211
+ ```
212
+ 0. syncBorderColor() update border colour from focused/disabled state
213
+ 1. paintBackground() fill region with background style (if any)
214
+ 2. blitContent() overlay the content buffer (what the user wrote)
215
+ 3. paintBorder() draw box-drawing characters on edges
216
+ 4. child loop render each child and blit its region onto ours
217
+ ```
218
+
219
+ **Two-buffer design:**
220
+
221
+ - `content` (private): what you write with `setCell`, `writeText`, `fill`, etc. Persists across `render()` calls.
222
+ - `region` (protected): rebuilt from scratch on every `render()`. Read by `getCell()` and `blitChild`.
223
+
224
+ Write to `content` → it shows up in `region` after the next `render()`.
225
+
226
+ `writeText()` picks the text style automatically when `style` is omitted: `disabledStyleId` when disabled, `focusedStyleId` when focused, otherwise `normalStyleId`. Pass an explicit `style` only for special cells (e.g. a filled bar or a cursor highlight).
227
+
228
+ ---
229
+
230
+ ## 3. Style system
231
+
232
+ ### StyleRegistry
233
+
234
+ ```typescript
235
+ const id = registry.register({ foreground: 75, bold: true }); // returns stable integer
236
+ registry.get(id); // → { foreground: 75, bold: true }
237
+ registry.merge(baseId, overId); // spread-merge, overId wins conflicts
238
+ ```
239
+
240
+ ID 0 is always `{}` (empty / no style). Merging anything with 0 is a no-op.
241
+
242
+ ### Named styles
243
+
244
+ `Screen` pre-registers ten *built-in* named styles. Controls look them up by name and fall back to hardcoded defaults when running without a Screen (e.g. in unit tests):
245
+
246
+ ```typescript
247
+ import { BUILTIN_TEXT_FOCUSED } from './types.mjs';
248
+
249
+ const styleId = registry.getNamed(BUILTIN_TEXT_FOCUSED) // may be undefined (no Screen)
250
+ ?? registry.register({ foreground: 255, bold: true }); // hardcoded fallback
251
+ ```
252
+
253
+ Override a built-in style for the entire application:
254
+
255
+ ```typescript
256
+ screen.setBuiltinStyle(BUILTIN_BORDER_FOCUSED, { foreground: 214 }); // amber focused borders
257
+ ```
258
+
259
+ ### CellAttributes fields
260
+
261
+ | Field | Type | Effect |
262
+ |---|---|---|
263
+ | `foreground` | `Color` | Text colour (ANSI 0–255 or `'#rrggbb'`) |
264
+ | `background` | `Color` | Cell background colour |
265
+ | `bold` | `boolean` | Bold / bright |
266
+ | `dim` | `boolean` | Dimmed / half-brightness |
267
+ | `italic` | `boolean` | Italic |
268
+ | `underline` | `boolean` | Underline |
269
+ | `strikethrough` | `boolean` | Strikethrough |
270
+ | `blink` | `boolean` | Blinking |
271
+ | `inverse` | `boolean` | Swap foreground ↔ background |
272
+
273
+ ---
274
+
275
+ ## 4. Implementing a custom control — step by step
276
+
277
+ ### 4.1 Define the properties interface
278
+
279
+ Add your control-specific properties to `src/Screen/types.mts`. The first constructor argument always extends `WindowProperties` (position, size, border, background, label, focused, disabled, …). Define a separate interface for control-specific options passed as the second argument.
280
+
281
+ ```typescript
282
+ // src/Screen/types.mts
283
+
284
+ /** Control-specific options for the Gauge (second constructor argument). */
285
+ export interface GaugeProperties {
286
+ /** Current value, 0–max. Default: 0. */
287
+ value?: number;
288
+ /** Maximum value. Default: 100. */
289
+ max?: number;
290
+ /** Called when the value changes via handleKey(). */
291
+ onChange?: (value: number) => void;
292
+ }
293
+ ```
294
+
295
+ ### 4.2 Create the class skeleton
296
+
297
+ Place the file in `src/Screen/controls/MyControl.mts`. Import your properties interface and any style constants you need.
298
+
299
+ `focused`, `disabled`, `label`, `normalStyleId`, `disabledStyleId`, and `focusedStyleId` are all inherited from `Window` — do not re-declare them.
300
+
301
+ ```typescript
302
+ // src/Screen/controls/Gauge.mts
303
+
304
+ import type { GaugeProperties, WindowProperties, StyleId } from '../types.mjs';
305
+ import { Window } from '../Window.mjs';
306
+
307
+ export class Gauge extends Window {
308
+ // control-specific state
309
+ private value: number = 0;
310
+ private max: number = 100;
311
+ private onChange?: (value: number) => void;
312
+
313
+ // cached style IDs (registered after super())
314
+ private filledStyleId!: StyleId;
315
+ private emptyStyleId!: StyleId;
316
+
317
+ // ...
318
+ }
319
+ ```
320
+
321
+ ### 4.3 Constructor: WindowProperties, super, styles
322
+
323
+ **Key pattern:** forward `WindowProperties` to `super()`, adding a `defaultBorder` to set the border shape used when the caller doesn't provide one. Register your own style IDs *after* calling `super()` — `this.registry` is available immediately after.
324
+
325
+ The global `StyleRegistry` is set by `Screen` on construction (via `RegistryHolder`). All controls created after `new Screen()` share that registry automatically — no need to pass it as a parameter.
326
+
327
+ ```typescript
328
+ public constructor(wp: WindowProperties, cp?: GaugeProperties) {
329
+ // 1. Forward to Window. defaultBorder provides the shape when the caller
330
+ // omits border — Window.syncBorderColor() sets the colour automatically.
331
+ super({
332
+ ...wp,
333
+ defaultBorder: { top: true, right: true, bottom: true, left: true, style: 'single' },
334
+ });
335
+
336
+ // 2. Store control-specific state (this.* is available after super()).
337
+ this.max = Math.max(1, cp?.max ?? 100);
338
+ this.value = Math.max(0, Math.min(cp?.value ?? 0, this.max));
339
+ this.onChange = cp?.onChange;
340
+
341
+ // 3. Register extra style IDs that aren't part of the inherited set.
342
+ // normalStyleId / focusedStyleId / disabledStyleId come from Window.
343
+ this.filledStyleId = this.registry.register({ background: 75 });
344
+ this.emptyStyleId = this.registry.register({ background: 238 });
345
+ }
346
+ ```
347
+
348
+ **Rules:**
349
+ - Never pass `CellAttributes` directly to `setCell` / `writeText` / `fill`. Always convert to a `StyleId` first.
350
+ - `focused`, `disabled`, `label`, `normalStyleId`, `focusedStyleId`, and `disabledStyleId` are inherited from `Window`. Do not re-declare them.
351
+ - `Window.syncBorderColor()` updates the border colour automatically on every `render()` based on `focused`/`disabled` state. Do not call `updateBorder()` for colour-only updates.
352
+ - `writeText()` auto-picks `normalStyleId`, `focusedStyleId`, or `disabledStyleId` when you omit the `style` option — rely on this for most label rendering.
353
+
354
+ ### 4.4 State setters and getters
355
+
356
+ Every stateful field needs a setter and a getter. The setters do *not* call `render()` — the caller decides when to re-render.
357
+
358
+ `isFocused()`, `setFocused()`, `isDisabled()`, and `setDisabled()` are already implemented by `Window`. Override them only if you need additional side effects (e.g. updating a cursor position). The border colour and text style update automatically on the next `render()`.
359
+
360
+ ```typescript
361
+ /** Sets the current value (clamped to 0–max). */
362
+ public setValue(value: number): void {
363
+ this.value = Math.max(0, Math.min(value, this.max));
364
+ }
365
+
366
+ /** Returns the current value. */
367
+ public getValue(): number {
368
+ return this.value;
369
+ }
370
+ ```
371
+
372
+ ### 4.5 Rendering pipeline
373
+
374
+ Override `render()` and follow this fixed order:
375
+
376
+ ```
377
+ 1. this.clear() — reset content buffer (mandatory at top of render)
378
+ 2. draw your content — writeText / setCell / fill
379
+ 3. super.render() — triggers Window pipeline: paintBackground → blitContent → paintBorder → children
380
+ ```
381
+
382
+ **Never call `super.render()` first** — the base class's `paintBackground` and `blitContent` steps would overwrite your content.
383
+
384
+ ```typescript
385
+ public override render(): void {
386
+ // ── 1. Clear ──────────────────────────────────────────────────────────────
387
+ this.clear();
388
+
389
+ // ── 2. Draw content ───────────────────────────────────────────────────────
390
+ const { width, height } = this.getInnerSize(); // excludes border cells
391
+ const filled = Math.round((this.value / this.max) * width);
392
+
393
+ for (let x = 0; x < width; x++) {
394
+ const styleId = x < filled ? this.filledStyleId : this.emptyStyleId;
395
+ this.setCell(x, 0, ' ', styleId);
396
+ }
397
+
398
+ // Centred "value / max" label.
399
+ // No explicit style needed — writeText() auto-picks normal/focused/disabled.
400
+ const label = `${this.value}/${this.max}`;
401
+ const labelX = Math.max(0, Math.floor((width - label.length) / 2));
402
+ this.writeText(label, { x: labelX, y: 0 });
403
+
404
+ // ── 3. Composite ──────────────────────────────────────────────────────────
405
+ super.render();
406
+ }
407
+ ```
408
+
409
+ **Coordinate system in `writeText` / `setCell`:**
410
+
411
+ Coordinates passed to `writeText` are always relative to the *inner content area* (i.e. the area inside any borders). `(0, 0)` is always the top-left of the content area regardless of whether a border is present. The same applies to coordinates returned by `getInnerSize()`. Use `getInnerOffset()` only when you need to translate to absolute cell coordinates for direct `Region` manipulation.
412
+
413
+ ### 4.6 Dynamic borders
414
+
415
+ Border colour is managed automatically by `Window.syncBorderColor()`, which is called at the start of every `render()`. It uses the built-in named styles:
416
+
417
+ | State | Style constant | Default colour |
418
+ |---|---|---|
419
+ | Disabled | `BUILTIN_BORDER_DISABLED` | ANSI 238 (dark grey) |
420
+ | Focused | `BUILTIN_BORDER_FOCUSED` | ANSI 75 (blue) |
421
+ | Normal | `BUILTIN_BORDER` | ANSI 240 (grey) |
422
+
423
+ You do **not** need to call `updateBorder()` for colour changes. Simply set `defaultBorder` in the constructor — the colour updates automatically on every frame.
424
+
425
+ To use a **fixed** border colour that is never auto-updated, set the `color` field explicitly in the `border` property passed by the caller (or in `defaultBorder`):
426
+
427
+ ```typescript
428
+ // Fixed amber border — colour never changes even when focused.
429
+ super({ ...wp, defaultBorder: { top: true, right: true, bottom: true, left: true,
430
+ style: 'rounded', color: 214 } });
431
+ ```
432
+
433
+ To override the focused/disabled border colours globally:
434
+
435
+ ```typescript
436
+ screen.setBuiltinStyle(BUILTIN_BORDER_FOCUSED, { foreground: 214 }); // amber
437
+ ```
438
+
439
+ ### 4.7 Keyboard input — the Focusable interface
440
+
441
+ To integrate with `WindowManager`'s Tab-cycle and keyboard dispatch, your control must satisfy the `Focusable` interface:
442
+
443
+ ```typescript
444
+ // From types.mts:
445
+ interface Focusable {
446
+ isFocused(): boolean;
447
+ setFocused(focused: boolean): void;
448
+ isDisabled(): boolean;
449
+ handleKey?(key: string): void; // optional — omit if the control is read-only
450
+ }
451
+ ```
452
+
453
+ Your class satisfies this automatically if it has `isFocused`, `setFocused`, `isDisabled`, and optionally `handleKey`. TypeScript uses structural typing, so no explicit `implements Focusable` keyword is required (though you may add it for clarity).
454
+
455
+ `handleKey` receives raw terminal escape strings **and** human-readable aliases:
456
+
457
+ | Physical key | Raw string | Alias |
458
+ |---|---|---|
459
+ | Enter | `'\r'` or `'\n'` | `'enter'` |
460
+ | Space | `' '` | `'space'` |
461
+ | Backspace | `'\x7f'` | `'backspace'` |
462
+ | Delete | `'\x1b[3~'` | `'delete'` |
463
+ | ← | `'\x1b[D'` | `'left'` |
464
+ | → | `'\x1b[C'` | `'right'` |
465
+ | ↑ | `'\x1b[A'` | `'up'` |
466
+ | ↓ | `'\x1b[B'` | `'down'` |
467
+ | Home | `'\x1b[H'` | `'home'` |
468
+ | End | `'\x1b[F'` | `'end'` |
469
+
470
+ Example for a Gauge that increments/decrements with arrow keys:
471
+
472
+ ```typescript
473
+ public handleKey(key: string): void {
474
+ if (this.disabled) return;
475
+
476
+ let changed = false;
477
+ if (key === '\x1b[C' || key === 'right') {
478
+ this.value = Math.min(this.value + 1, this.max);
479
+ changed = true;
480
+ } else if (key === '\x1b[D' || key === 'left') {
481
+ this.value = Math.max(this.value - 1, 0);
482
+ changed = true;
483
+ }
484
+
485
+ if (changed) this.onChange?.(this.value);
486
+ }
487
+ ```
488
+
489
+ ### 4.8 Registering with WindowManager
490
+
491
+ Register your control after adding it to the window tree:
492
+
493
+ ```typescript
494
+ const screen = new Screen();
495
+ const wm = new WindowManager(screen);
496
+
497
+ // Screen sets the global registry on construction — Gauge picks it up automatically.
498
+ const gauge = new Gauge({ pos: Pos.center(), size: new Size(30, 3) }, { max: 50 });
499
+ screen.addChild(gauge);
500
+
501
+ // Register for Tab focus cycle. Pass all ancestor Windows after the control.
502
+ wm.register(gauge); // top-level control (no parent chain needed)
503
+ // — OR —
504
+ wm.register(gauge, panel, screen); // control is inside panel which is inside screen
505
+
506
+ wm.run();
507
+ ```
508
+
509
+ `WindowManager.register(control, ...parents)` uses the parent chain to compute absolute screen coordinates for mouse hit-testing. Pass parents from closest to farthest (innermost first).
510
+
511
+ ### 4.9 Writing tests
512
+
513
+ Tests run via Vitest. Import controls directly — no Screen or WindowManager needed.
514
+
515
+ Patterns to test:
516
+
517
+ ```typescript
518
+ import { describe, it, expect } from 'vitest';
519
+ import { Gauge } from '../../src/Screen/controls/Gauge.mjs';
520
+ import { Pos } from '../../src/Screen/Pos.mjs';
521
+ import { Size } from '../../src/Screen/Size.mjs';
522
+
523
+ describe('Gauge', () => {
524
+ // ── 1. Constructor / state ─────────────────────────────────────────────────
525
+ it('defaults to value 0', () => {
526
+ const g = new Gauge({ pos: new Pos(0, 0), size: new Size(20, 3) });
527
+ expect(g.getValue()).toBe(0);
528
+ });
529
+
530
+ it('clamps value to max in constructor', () => {
531
+ const g = new Gauge({ pos: new Pos(0, 0), size: new Size(20, 3) }, { value: 999, max: 50 });
532
+ expect(g.getValue()).toBe(50);
533
+ });
534
+
535
+ // ── 2. render() — visual output ───────────────────────────────────────────
536
+ it('renders a border', () => {
537
+ const g = new Gauge({ pos: new Pos(0, 0), size: new Size(10, 3) });
538
+ g.render();
539
+ expect(g.getCell(0, 0).char).toBe('┌');
540
+ expect(g.getCell(9, 2).char).toBe('┘');
541
+ });
542
+
543
+ it('filled portion has a distinct background', () => {
544
+ const g = new Gauge({ pos: new Pos(0, 0), size: new Size(12, 3) }, { value: 50, max: 100 });
545
+ g.render();
546
+ // inner width = 10 (border on both sides); 50% = 5 cells filled
547
+ const filledBg = g.getCell(1, 1).attributes.background; // first inner cell
548
+ const emptyBg = g.getCell(9, 1).attributes.background; // last inner cell
549
+ expect(filledBg).not.toBe(emptyBg);
550
+ });
551
+
552
+ // ── 3. handleKey ──────────────────────────────────────────────────────────
553
+ it('right arrow increments value', () => {
554
+ const g = new Gauge({ pos: new Pos(0, 0), size: new Size(20, 3) }, { value: 5, max: 10 });
555
+ g.handleKey('right');
556
+ expect(g.getValue()).toBe(6);
557
+ });
558
+
559
+ it('does not exceed max', () => {
560
+ const g = new Gauge({ pos: new Pos(0, 0), size: new Size(20, 3) }, { value: 10, max: 10 });
561
+ g.handleKey('right');
562
+ expect(g.getValue()).toBe(10);
563
+ });
564
+
565
+ it('disabled control ignores key presses', () => {
566
+ const g = new Gauge({ pos: new Pos(0, 0), size: new Size(20, 3), disabled: true }, { value: 5 });
567
+ g.handleKey('right');
568
+ expect(g.getValue()).toBe(5);
569
+ });
570
+
571
+ // ── 4. Focusable interface ────────────────────────────────────────────────
572
+ it('isFocused / setFocused round-trip', () => {
573
+ const g = new Gauge({ pos: new Pos(0, 0), size: new Size(20, 3) });
574
+ g.setFocused(true);
575
+ expect(g.isFocused()).toBe(true);
576
+ });
577
+
578
+ it('focused state changes border color', () => {
579
+ const g = new Gauge({ pos: new Pos(0, 0), size: new Size(10, 3) });
580
+ const gFocused = new Gauge({ pos: new Pos(0, 0), size: new Size(10, 3), focused: true });
581
+ g.render();
582
+ gFocused.render();
583
+ const normalColor = g.getCell(0, 0).attributes.foreground;
584
+ const focusedColor = gFocused.getCell(0, 0).attributes.foreground;
585
+ expect(normalColor).not.toBe(focusedColor);
586
+ });
587
+ });
588
+ ```
589
+
590
+ ---
591
+
592
+ ## 5. Complete example 1: ProgressBar (read-only)
593
+
594
+ A simple horizontal progress bar with a percentage label. No keyboard interaction.
595
+
596
+ > **Note:** a real `ProgressBar` is shipped as a built-in control in `src/Screen/controls/ProgressBar.mts` (see section 9). The walkthrough below is kept as a tutorial — it shows the minimum viable implementation of a read-only control and is a good starting point if you want to build your own.
597
+
598
+ ```typescript
599
+ // src/Screen/controls/ProgressBar.mts
600
+
601
+ import type { ProgressBarProperties, WindowProperties, StyleId } from '../types.mjs';
602
+ import { Window } from '../Window.mjs';
603
+
604
+ export class ProgressBar extends Window {
605
+ private value: number;
606
+ private max: number;
607
+ private filledStyleId: StyleId;
608
+ private emptyStyleId: StyleId;
609
+
610
+ /** Creates a ProgressBar. Recommended height: 1 (no border) or 3 (with border). */
611
+ public constructor(wp: WindowProperties, cp?: ProgressBarProperties) {
612
+ super(wp); // no defaultBorder — ProgressBar has none by default
613
+
614
+ this.max = Math.max(1, cp?.max ?? 100);
615
+ this.value = Math.max(0, Math.min(cp?.value ?? 0, this.max));
616
+
617
+ this.filledStyleId = this.registry.register({ background: cp?.fillColor ?? 75 });
618
+ this.emptyStyleId = this.registry.register({ background: 238 });
619
+ }
620
+
621
+ /** Sets the current value (clamped to 0–max). Caller must call render() afterwards. */
622
+ public setValue(value: number): void {
623
+ this.value = Math.max(0, Math.min(value, this.max));
624
+ }
625
+
626
+ /** Returns the current value. */
627
+ public getValue(): number {
628
+ return this.value;
629
+ }
630
+
631
+ public override render(): void {
632
+ this.clear();
633
+
634
+ const { width } = this.getInnerSize();
635
+ const pct = this.value / this.max;
636
+ const filled = Math.round(pct * width);
637
+
638
+ // Draw bar cells.
639
+ for (let x = 0; x < width; x++) {
640
+ this.setCell(x, 0, ' ', x < filled ? this.filledStyleId : this.emptyStyleId);
641
+ }
642
+
643
+ // Centred percentage label — writeText() auto-applies disabled style when needed.
644
+ const label = `${Math.round(pct * 100)}%`;
645
+ const labelX = Math.max(0, Math.floor((width - label.length) / 2));
646
+ this.writeText(label, { x: labelX, y: 0 });
647
+
648
+ super.render();
649
+ }
650
+ }
651
+ ```
652
+
653
+ Usage:
654
+
655
+ ```typescript
656
+ const screen = new Screen();
657
+ const bar = new ProgressBar(
658
+ { pos: Pos.center(), size: new Size(30, 1) },
659
+ { value: 42, max: 100, fillColor: 75 },
660
+ );
661
+ screen.addChild(bar);
662
+
663
+ // Later, update and re-render:
664
+ bar.setValue(75);
665
+ screen.render();
666
+ ```
667
+
668
+ ---
669
+
670
+ ## 6. Complete example 2: NumberStepper (interactive)
671
+
672
+ A control that lets the user increment/decrement a numeric value with ← / → arrow keys. Shows the full Focusable pattern with dynamic border colouring.
673
+
674
+ ```typescript
675
+ // src/Screen/controls/NumberStepper.mts
676
+
677
+ import type { NumberStepperProperties, WindowProperties } from '../types.mjs';
678
+ import { Window } from '../Window.mjs';
679
+
680
+ export interface NumberStepperProperties {
681
+ value?: number;
682
+ min?: number;
683
+ max?: number;
684
+ step?: number;
685
+ onChange?: (value: number) => void;
686
+ }
687
+
688
+ export class NumberStepper extends Window {
689
+ private value: number;
690
+ private min: number;
691
+ private max: number;
692
+ private step: number;
693
+ private onChange?: (value: number) => void;
694
+
695
+ // focused, disabled, normalStyleId, focusedStyleId, disabledStyleId — inherited from Window
696
+
697
+ /** Creates a NumberStepper. Minimum recommended size: width 10, height 3. */
698
+ public constructor(wp: WindowProperties, cp?: NumberStepperProperties) {
699
+ super({
700
+ ...wp,
701
+ defaultBorder: { top: true, right: true, bottom: true, left: true, style: 'rounded' },
702
+ });
703
+
704
+ this.min = cp?.min ?? 0;
705
+ this.max = cp?.max ?? 99;
706
+ this.step = cp?.step ?? 1;
707
+ this.value = Math.max(this.min, Math.min(cp?.value ?? this.min, this.max));
708
+ this.onChange = cp?.onChange;
709
+ }
710
+
711
+ public getValue(): number { return this.value; }
712
+
713
+ public setValue(value: number): void {
714
+ this.value = Math.max(this.min, Math.min(value, this.max));
715
+ }
716
+
717
+ public handleKey(key: string): void {
718
+ if (this.disabled) return;
719
+ let changed = false;
720
+
721
+ if (key === '\x1b[C' || key === 'right') {
722
+ this.value = Math.min(this.value + this.step, this.max);
723
+ changed = true;
724
+ } else if (key === '\x1b[D' || key === 'left') {
725
+ this.value = Math.max(this.value - this.step, this.min);
726
+ changed = true;
727
+ }
728
+
729
+ if (changed) this.onChange?.(this.value);
730
+ }
731
+
732
+ public override render(): void {
733
+ this.clear();
734
+
735
+ // Draw "◀ value ▶" centred in the inner area.
736
+ // writeText() auto-picks normal/focused/disabled style — no explicit style needed.
737
+ const { width, height } = this.getInnerSize();
738
+ const label = `◀ ${this.value} ▶`;
739
+ const labelX = Math.max(0, Math.floor((width - label.length) / 2));
740
+ const labelY = Math.floor(height / 2);
741
+ this.writeText(label, { x: labelX, y: labelY });
742
+
743
+ super.render();
744
+ }
745
+ }
746
+ ```
747
+
748
+ Compare with the old pattern: no `registry` parameter, no `BUILTIN_BORDER_*` imports, no `updateBorder()` call, no manual style selection — all handled by `Window`.
749
+
750
+ Usage with WindowManager:
751
+
752
+ ```typescript
753
+ import { Screen } from './src/Screen/Screen.mjs';
754
+ import { WindowManager } from './src/Screen/WindowManager.mjs';
755
+ import { NumberStepper } from './src/Screen/controls/NumberStepper.mjs';
756
+ import { Pos } from './src/Screen/Pos.mjs';
757
+ import { Size } from './src/Screen/Size.mjs';
758
+
759
+ const screen = new Screen();
760
+ const wm = new WindowManager(screen, { exitKeys: ['\x03'] });
761
+
762
+ const stepper = new NumberStepper(
763
+ { pos: Pos.center(), size: new Size(16, 3) },
764
+ { value: 10, min: 0, max: 99, step: 5, onChange: v => console.error(`value: ${v}`) },
765
+ );
766
+ screen.addChild(stepper);
767
+ wm.register(stepper);
768
+
769
+ wm.run();
770
+ ```
771
+
772
+ ---
773
+
774
+ ## 7. YAML / InterfaceBuilder integration
775
+
776
+ `InterfaceBuilder` (`src/Screen/InterfaceBuilder.mts`) builds a window hierarchy from a YAML description:
777
+
778
+ ```typescript
779
+ const screen = new Screen();
780
+ const wm = new WindowManager(screen);
781
+ const builder = new InterfaceBuilder();
782
+ const result = await builder.buildFromFile('layout.yaml', screen, wm);
783
+ // result is a Map<string, Window> keyed by the YAML `id` field
784
+ ```
785
+
786
+ Focusable controls are automatically registered with `WindowManager` after the full tree is built, so Tab-cycling works out of the box.
787
+
788
+ ### 7.1 Supported widget types
789
+
790
+ All eleven built-in control classes are directly instantiable from YAML via the `type:` field:
791
+
792
+ | `type:` | Class | Requires `size:`? | Key YAML fields |
793
+ |---|---|---|---|
794
+ | `window` | `Window` | yes | `background`, `border`, `active`, `content`, `children` |
795
+ | `button` | `Button` | yes | `label`, `focused`, `disabled`, `onPress` |
796
+ | `textbox` | `TextBox` | yes | `value`, `placeholder`, `focused`, `disabled`, `onChange` |
797
+ | `textarea` | `TextArea` | yes | `value`, `placeholder`, `focused`, `disabled`, `onChange` |
798
+ | `checkbox` | `Checkbox` | no (auto-sized) | `label`, `checked`, `focused`, `disabled`, `onChange` |
799
+ | `radio` | `Radio` | no (auto-sized) | `label`, `checked`, `focused`, `disabled`, `onChange` |
800
+ | `statusled` | `StatusLED` | no (auto-sized) | `state` (`ok`/`warn`/`error`/`off`), `label` |
801
+ | `progressbar` | `ProgressBar` | yes | `barValue`, `max`, `showLabel`, `fillColor`, `emptyColor` |
802
+ | `progressbarv` | `ProgressBarV` | yes | `barValue`, `max`, `fillColor`, `emptyColor` |
803
+ | `linechart` | `LineChart` | yes | `data`, `min`, `max`, `chartColor` |
804
+ | `barchart` | `BarChart` | yes | `data`, `barLabels`, `max`, `chartColor`, `barWidth` |
805
+
806
+ Custom (user-defined) control classes are not auto-discoverable. To use one with a YAML layout, either:
807
+
808
+ 1. **Place a `window` placeholder** at the desired geometry, retrieve it from the returned map, and construct your control in code at the same `pos`/`size`.
809
+ 2. **Extend `InterfaceBuilder`** and override `buildNode` to handle additional `type` values.
810
+
811
+ ### 7.2 Position, size, and background
812
+
813
+ ```yaml
814
+ pos: { x: 2, y: 5 } # absolute
815
+ pos: { x: -10, y: -3 } # edge-relative (negative = from right/bottom)
816
+ pos: { x: "50%", y: "25%" } # percentage of parent
817
+ pos: center # named preset
818
+ pos: { preset: bottom, offset: 2 }
819
+
820
+ size: { width: 30, height: 10 }
821
+ size: { width: "80%", height: "100%" }
822
+ size: fill # fill both axes
823
+ size: { fillWidth: 3 } # fill width, height = 3
824
+ size: { fillHeight: 20 } # fill height, width = 20
825
+
826
+ background: 237 # numeric StyleId (rarely used directly)
827
+ background: my-panel-bg # name of a style defined in `styles:` section
828
+ background: builtin:window-bg # name of a built-in style (see section 8)
829
+ ```
830
+
831
+ ### 7.3 Named styles
832
+
833
+ YAML layouts may declare named styles in an optional top-level `styles:` section. Entries are registered in the Screen's `StyleRegistry` **before** any window is built, so they are available as values for any `background:` field. Named styles may also **override** built-in names (e.g. `builtin:border-focused`) to re-skin every control in the application.
834
+
835
+ ```yaml
836
+ styles:
837
+ - name: panel-bg # custom name
838
+ background: 235
839
+ - name: builtin:border-focused # override the built-in focused border colour
840
+ foreground: 214 # amber
841
+
842
+ windows:
843
+ - id: sidebar
844
+ pos: topLeft
845
+ size: { width: 30, height: "100%" }
846
+ background: panel-bg # reference the custom style by name
847
+ ```
848
+
849
+ ### 7.4 Callbacks
850
+
851
+ `onPress` and `onChange` YAML fields reference **callback IDs** registered with `InterfaceBuilder.registerCallback(id, fn)` before `build()` is called:
852
+
853
+ ```typescript
854
+ const builder = new InterfaceBuilder();
855
+ builder.registerCallback('save', () => saveSettings());
856
+ builder.registerCallback('toggle', (checked: boolean) => setDarkMode(checked));
857
+ await builder.buildFromFile('layout.yaml', screen, wm);
858
+ ```
859
+
860
+ ```yaml
861
+ - id: btnSave
862
+ type: button
863
+ pos: { x: -15, y: -4 }
864
+ size: { width: 10, height: 3 }
865
+ label: "Save"
866
+ onPress: save # → fires the 'save' callback
867
+ - id: cbDark
868
+ type: checkbox
869
+ pos: { x: 1, y: 1 }
870
+ label: "Dark mode"
871
+ onChange: toggle # → fires the 'toggle' callback
872
+ ```
873
+
874
+ ### 7.5 Full example
875
+
876
+ ```yaml
877
+ styles:
878
+ - name: builtin:border-focused
879
+ foreground: 214 # amber focused borders everywhere
880
+
881
+ windows:
882
+ - id: dialog
883
+ pos: center
884
+ size: { width: "80%", height: "80%" }
885
+ background: builtin:window-bg
886
+ border: { top: true, right: true, bottom: true, left: true, style: rounded, color: 240 }
887
+ children:
888
+ - id: ledStatus
889
+ type: statusled
890
+ pos: { x: 2, y: 2 }
891
+ state: ok
892
+ label: "API online"
893
+
894
+ - id: pbCpu
895
+ type: progressbar
896
+ pos: { x: 2, y: 4 }
897
+ size: { width: 30, height: 1 }
898
+ barValue: 73
899
+ max: 100
900
+ fillColor: 75
901
+
902
+ - id: chart
903
+ type: linechart
904
+ pos: { x: 2, y: 6 }
905
+ size: { width: 60, height: 15 }
906
+ data: [5, 18, 8, 45, 22, 60, 38, 72, 50, 65]
907
+ chartColor: 75
908
+ ```
909
+
910
+ After `build()`, retrieve any window or control from the returned map and drive it at runtime:
911
+
912
+ ```typescript
913
+ const result = await builder.buildFromFile('layout.yaml', screen, wm);
914
+ const ledStatus = result.get('ledStatus') as StatusLED;
915
+ const pbCpu = result.get('pbCpu') as ProgressBar;
916
+
917
+ setInterval(() => {
918
+ pbCpu.setValue(readCpuPercent());
919
+ ledStatus.setState(isHealthy() ? 'ok' : 'warn');
920
+ screen.render();
921
+ }, 1000);
922
+ ```
923
+
924
+ ---
925
+
926
+ ## 8. Built-in style reference
927
+
928
+ All names are exported as constants from `src/Screen/types.mts`.
929
+
930
+ | Constant | Name string | Default attributes |
931
+ |---|---|---|
932
+ | `BUILTIN_WINDOW_BG` | `builtin:window-bg` | `{ background: 237 }` |
933
+ | `BUILTIN_BORDER` | `builtin:border` | `{ foreground: 240 }` |
934
+ | `BUILTIN_BORDER_FOCUSED` | `builtin:border-focused` | `{ foreground: 75 }` |
935
+ | `BUILTIN_BORDER_DISABLED` | `builtin:border-disabled` | `{ foreground: 238 }` |
936
+ | `BUILTIN_TEXT` | `builtin:text` | `{ foreground: 252 }` |
937
+ | `BUILTIN_TEXT_FOCUSED` | `builtin:text-focused` | `{ foreground: 255, bold: true }` |
938
+ | `BUILTIN_TEXT_DISABLED` | `builtin:text-disabled` | `{ foreground: 245, dim: true }` |
939
+ | `BUILTIN_TEXT_PLACEHOLDER` | `builtin:text-placeholder` | `{ foreground: 242, italic: true }` |
940
+ | `BUILTIN_TEXT_CHECKED` | `builtin:text-checked` | `{ foreground: 76, bold: true }` |
941
+ | `BUILTIN_CURSOR` | `builtin:cursor` | `{ inverse: true }` |
942
+
943
+ Override any of these for the whole application:
944
+
945
+ ```typescript
946
+ // Amber focused borders everywhere.
947
+ screen.setBuiltinStyle(BUILTIN_BORDER_FOCUSED, { foreground: 214 });
948
+ ```
949
+
950
+ Or override in YAML before building the layout:
951
+
952
+ ```yaml
953
+ styles:
954
+ - name: builtin:border-focused
955
+ foreground: 214
956
+ windows:
957
+ - ...
958
+ ```
959
+
960
+ ---
961
+
962
+ ## 9. Built-in controls reference
963
+
964
+ Every control extends `Window` and lives under `src/Screen/controls/`. Constructor signatures follow the two-argument pattern: `(wp: WindowProperties, cp?: ControlProperties)`. Controls created after `new Screen()` share the Screen's global `StyleRegistry` automatically — no registry parameter needed. Size behaviour, interactivity, and key methods are summarised below.
965
+
966
+ ### Interactive controls (Focusable — registerable with `WindowManager`)
967
+
968
+ | Class | Size | Purpose | Key methods |
969
+ |---|---|---|---|
970
+ | `Button` | manual | Clickable button with rounded border and centred label. Enter/Space triggers `onPress`. | `setLabel`, `setFocused`, `setDisabled` |
971
+ | `TextBox` | manual | Single-line text input with scrolling, cursor, and placeholder. | `setValue`/`getValue`, `setCursor`/`getCursor`, `handleKey` |
972
+ | `TextArea` | manual | Multi-line text input with 2-D cursor and scrolling. | `setValue`/`getValue`, `setCursor`/`getCursor` (2-D), `handleKey` |
973
+ | `Checkbox` | auto | `[✓]`/`[ ]` toggle auto-sized to `4 + label.length`. Space toggles. | `setChecked`/`isChecked`, `setFocused`, `setDisabled` |
974
+ | `Radio` | auto | `(●)`/`( )` single-selection auto-sized to `4 + label.length`. Space selects. | `setChecked`/`isChecked`, `setFocused`, `setDisabled` |
975
+
976
+ ### Read-only display controls (added 0.10.0)
977
+
978
+ | Class | Size | Purpose | Key methods |
979
+ |---|---|---|---|
980
+ | `StatusLED` | auto (`2 + label.length` × 1) | Coloured dot (●) with optional trailing label. States: `ok` / `warn` / `error` / `off`. | `setState`, `getState`, `setLabel` |
981
+ | `ProgressBar` | manual | Horizontal bar (`█`/`░`) with optional centred percentage label. | `setValue`/`getValue`, `setMax`/`getMax` |
982
+ | `ProgressBarV` | manual | Vertical bar (`█`/`░`) that fills from the bottom upward. | `setValue`/`getValue`, `setMax`/`getMax` |
983
+ | `LineChart` | manual | Line chart using box-drawing characters (`─`, `│`, `╭`, `╮`, `╯`, `╰`) with labelled Y-axis and X-axis. Min height 3, min width 4. | `setData`/`getData`, `setMin`/`setMax` |
984
+ | `BarChart` | manual | Vertical bar chart (`█`) with a one-row label strip at the bottom and configurable bar width. | `setData`/`getData`, `setLabels`, `setMax` |
985
+
986
+ All controls honour the built-in named style system (section 3): border colour and text style update automatically on every `render()` via `Window.syncBorderColor()` and `writeText()` auto-style. Override styles globally with `screen.setBuiltinStyle(BUILTIN_BORDER_FOCUSED, { foreground: 214 })`.