take4-console 0.15.1 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +360 -0
- package/dist/Screen/InterfaceBuilder.d.mts +15 -4
- package/dist/Screen/InterfaceBuilder.d.mts.map +1 -1
- package/dist/Screen/InterfaceBuilder.mjs +104 -8
- package/dist/Screen/InterfaceBuilder.mjs.map +1 -1
- package/dist/Screen/Pos.d.mts +12 -0
- package/dist/Screen/Pos.d.mts.map +1 -1
- package/dist/Screen/Pos.mjs +23 -1
- package/dist/Screen/Pos.mjs.map +1 -1
- package/dist/Screen/Screen.d.mts +77 -3
- package/dist/Screen/Screen.d.mts.map +1 -1
- package/dist/Screen/Screen.mjs +168 -3
- package/dist/Screen/Screen.mjs.map +1 -1
- package/dist/Screen/Size.d.mts +49 -6
- package/dist/Screen/Size.d.mts.map +1 -1
- package/dist/Screen/Size.mjs +81 -7
- package/dist/Screen/Size.mjs.map +1 -1
- package/dist/Screen/Window.d.mts +131 -20
- package/dist/Screen/Window.d.mts.map +1 -1
- package/dist/Screen/Window.mjs +474 -57
- package/dist/Screen/Window.mjs.map +1 -1
- package/dist/Screen/WindowManager.d.mts +85 -5
- package/dist/Screen/WindowManager.d.mts.map +1 -1
- package/dist/Screen/WindowManager.mjs +279 -26
- package/dist/Screen/WindowManager.mjs.map +1 -1
- package/dist/Screen/controls/ListBox.d.mts +34 -12
- package/dist/Screen/controls/ListBox.d.mts.map +1 -1
- package/dist/Screen/controls/ListBox.mjs +127 -25
- package/dist/Screen/controls/ListBox.mjs.map +1 -1
- package/dist/Screen/controls/TextArea.d.mts +15 -1
- package/dist/Screen/controls/TextArea.d.mts.map +1 -1
- package/dist/Screen/controls/TextArea.mjs +74 -1
- package/dist/Screen/controls/TextArea.mjs.map +1 -1
- package/dist/Screen/controls/TextBox.d.mts +13 -1
- package/dist/Screen/controls/TextBox.d.mts.map +1 -1
- package/dist/Screen/controls/TextBox.mjs +36 -1
- package/dist/Screen/controls/TextBox.mjs.map +1 -1
- package/dist/Screen/textWidth.d.mts +13 -0
- package/dist/Screen/textWidth.d.mts.map +1 -0
- package/dist/Screen/textWidth.mjs +188 -0
- package/dist/Screen/textWidth.mjs.map +1 -0
- package/dist/Screen/types.d.mts +336 -20
- package/dist/Screen/types.d.mts.map +1 -1
- package/dist/Screen/types.mjs.map +1 -1
- package/dist/index.d.mts +3 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/Screen/InterfaceBuilder.mts +116 -20
- package/src/Screen/Pos.mts +24 -1
- package/src/Screen/Screen.mts +192 -4
- package/src/Screen/Size.mts +97 -12
- package/src/Screen/Window.mts +463 -63
- package/src/Screen/WindowManager.mts +301 -29
- package/src/Screen/controls/ListBox.mts +151 -32
- package/src/Screen/controls/TextArea.mts +82 -1
- package/src/Screen/controls/TextBox.mts +40 -1
- package/src/Screen/textWidth.mts +186 -0
- package/src/Screen/types.mts +328 -23
- package/src/demo.mts +232 -20
- package/src/index.mts +23 -3
- package/src/layout.yaml +56 -24
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,365 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.25.0] – 2026-04-18
|
|
4
|
+
|
|
5
|
+
### Added — backlog P0-10 (InterfaceBuilder: register custom types)
|
|
6
|
+
- **`InterfaceBuilder#registerType(name, factory)`** — rejestruje niestandardową
|
|
7
|
+
fabrykę kontrolki adresowalną przez `type: <name>` w YAML. Fabryka otrzymuje
|
|
8
|
+
surowy `YamlWindowDef` oraz `CustomTypeContext` z wstępnie zresolvowanym
|
|
9
|
+
`wp: WindowProperties`, aktywnym `registry: StyleRegistry` oraz pomocniczym
|
|
10
|
+
`resolveCallback(id)` przekierowującym do wcześniej zarejestrowanych
|
|
11
|
+
callbacków. Próba rejestracji pod nazwą wbudowanego typu rzuca wyjątkiem,
|
|
12
|
+
żeby uniknąć cichego shadowingu.
|
|
13
|
+
- **`YamlWindowDef.props`** — wolne pole `Record<string, unknown>` forwardowane
|
|
14
|
+
w całości do fabryki; wbudowane typy je ignorują.
|
|
15
|
+
- **Auto-focus registration** — jeśli fabryka zwróci `Window` z metodą
|
|
16
|
+
`handleKey()`, builder automatycznie rejestruje go w `WindowManager` razem
|
|
17
|
+
z łańcuchem rodziców, tak samo jak wbudowane `Button`/`TextBox`/itd.
|
|
18
|
+
- **Eksporty**: nowe typy `CustomTypeContext` oraz `CustomTypeFactory` są
|
|
19
|
+
publicznie dostępne z `take4-console`.
|
|
20
|
+
- **Demo**: w `src/demo.mts` rejestrowany jest custom typ `badge` (klasa
|
|
21
|
+
`Badge extends Window` malująca etykietę w render()), a `src/layout.yaml`
|
|
22
|
+
używa go w pasku nagłówka (`id: buildBadge`, `props: { text, color }`).
|
|
23
|
+
|
|
24
|
+
## [0.24.0] – 2026-04-18
|
|
25
|
+
|
|
26
|
+
### Added — backlog P0-3 (flex layout / auto-sizing)
|
|
27
|
+
- **`WindowProperties.layout: 'absolute' | 'row' | 'column' | 'grid'`** — nowy,
|
|
28
|
+
per-okienny tryb layoutu dla bezpośrednich dzieci. Domyślnie `'absolute'`,
|
|
29
|
+
czyli dotychczasowe zachowanie (`Pos`/`Size` rozwiązuje każde dziecko
|
|
30
|
+
niezależnie). `'row'` i `'column'` uruchamiają silnik flex (rozdzielenie osi
|
|
31
|
+
głównej pro-rata `grow`, skurcz `shrink`, wyrównanie cross-axis), `'grid'`
|
|
32
|
+
rozkłada dzieci w równe komórki wiersz-po-wierszu.
|
|
33
|
+
- **Nowe pola `WindowProperties`**: `gap` (odstęp między dziećmi w cellach),
|
|
34
|
+
`padding` (uniform number | `[v, h]` tuple | per-side record; wpływa na
|
|
35
|
+
`getInnerSize()` / `getInnerOffset()` — stackuje na border inset),
|
|
36
|
+
`gridColumns` (liczba kolumn dla `grid`), `alignItems`
|
|
37
|
+
(`start | center | end | stretch`, domyślnie `'stretch'`) oraz
|
|
38
|
+
`justifyContent` (`start | center | end | space-between | space-around`).
|
|
39
|
+
- **`Pos.flex(order?)` / `Pos#getFlexOrder()`** — marker pozycji flex. Silnik
|
|
40
|
+
layoutu parenta ustawia finalne `child.x / child.y`; `order` sortuje dzieci
|
|
41
|
+
niezależnie od kolejności `addChild` (stabilne ties → kolejność wstawiania).
|
|
42
|
+
- **`Size.flex(grow?, shrink?, basis?)` / `Size.content()`** oraz osobne
|
|
43
|
+
fabryki `flex()` i `content()` — `Size.flex(...)` zaznacza oba axes jako
|
|
44
|
+
flex (silnik decyduje który jest main vs cross na podstawie parenta),
|
|
45
|
+
`Size.content()` używa aktualnych wymiarów regionu dziecka jako naturalnego
|
|
46
|
+
rozmiaru. Dla mieszanych osi: `new Size(flex(), 10)` / `new Size(content(),
|
|
47
|
+
pct(50))`. `Size.isAbsolute()` zwraca `false` dla flex/content — `resolve()`
|
|
48
|
+
daje bezpieczny fallback (basis dla flex, 1 dla content) dopóki silnik nie
|
|
49
|
+
nadpisze wartości. Nowe gettery `getWidthSpec()` / `getHeightSpec()`.
|
|
50
|
+
- **Silnik layoutu w `Window`** — `addChild` i `setSize` (→ `reflowChildren`)
|
|
51
|
+
delegują do `runLayout()`. Absolute path zachowany 1:1 dla back-compat;
|
|
52
|
+
row / column liczą basis, rozdzielają `remainder` przez `grow` (całkowite,
|
|
53
|
+
reszta od truncation trafia do ostatniego flex-a), skracają przy ujemnym
|
|
54
|
+
remainderze przez `shrink`, aplikują stretch/align cross-axis i — gdy nic
|
|
55
|
+
nie zjada slack'u — uruchamiają `justifyContent`. `grid` liczy równe komórki
|
|
56
|
+
`(inner - gap * (cols|rows - 1)) / (cols|rows)`. Niewidoczne dzieci są
|
|
57
|
+
pomijane, więc `setVisible(false)` wyjmuje je ze stacka.
|
|
58
|
+
- **YAML (InterfaceBuilder)** — wspiera `pos: flex` / `pos: { flex: N }`,
|
|
59
|
+
`size: flex` / `size: content` / `size: { flex: { grow, shrink, basis } }`,
|
|
60
|
+
a także per-axis `size: { width: content, height: { flex: { grow: 2 } } }`.
|
|
61
|
+
Nowe pola `layout`, `gap`, `padding`, `gridColumns`, `alignItems`,
|
|
62
|
+
`justifyContent` na każdej definicji okna.
|
|
63
|
+
- **`Window#blitChild`** czyta teraz `child.x / child.y` zamiast re-solve'ować
|
|
64
|
+
`posSpec`, więc absolute i flex lecą tym samym kodem kompozycji.
|
|
65
|
+
|
|
66
|
+
### Demo
|
|
67
|
+
- `src/layout.yaml` — dolny pasek akcji (Delete / Cancel / Save) przekonwertowany
|
|
68
|
+
z trzech absolutnych pozycji na kontener `buttonRow` z `layout: row`,
|
|
69
|
+
`gap: 1`, `alignItems: stretch` i dzieckiem-spacerem `{ size: flex }` między
|
|
70
|
+
"Delete" a grupą "Cancel / Save" — ten sam wygląd co wcześniej, zapisany
|
|
71
|
+
deklaratywnie, automatycznie re-flow przy SIGWINCH.
|
|
72
|
+
|
|
73
|
+
## [0.23.0] – 2026-04-16
|
|
74
|
+
|
|
75
|
+
### Added — backlog P0-5 (WindowManager.pause / resume)
|
|
76
|
+
- **`WindowManager.pause(options?)` / `resume(options?)` / `isPaused()`** —
|
|
77
|
+
tymczasowo zwalnia kontrolę nad terminalem bez niszczenia focus tree,
|
|
78
|
+
rejestracji kontrolek ani stosu dialogów. `pause()` odłącza listener
|
|
79
|
+
stdin, wyłącza raw mode + mouse tracking, pokazuje kursor i opcjonalnie
|
|
80
|
+
(`{ leaveAltScreen: true }`) wychodzi z alt-screen buffer. `resume()`
|
|
81
|
+
re-enter-uje alt-screen (jeśli pause go zamknął), re-enable mouse,
|
|
82
|
+
ukrywa kursor z powrotem, re-attach stdin + raw mode i re-renderuje
|
|
83
|
+
klatkę (chyba że `{ rerender: false }`). Obie metody są idempotentne.
|
|
84
|
+
Typowe użycie: `pause({ leaveAltScreen: true })` → `spawnSync('$EDITOR')`
|
|
85
|
+
→ `resume()`.
|
|
86
|
+
- **`WindowManager.stop()`** rozpoznaje stan pauzy i nie powtarza teardownu
|
|
87
|
+
stdin / mouse / raw mode, które pause już zrobiła — unika podwójnego
|
|
88
|
+
`stdin.off('data', …)`. Reszta semantyki stop bez zmian.
|
|
89
|
+
|
|
90
|
+
### Demo
|
|
91
|
+
- `src/demo.mts` — `bindKey('ctrl+e')` pauzuje TUI (wychodzi z alt-screen),
|
|
92
|
+
drukuje prompt `--- paused … Press Enter to return ---`, blokuje na
|
|
93
|
+
`bash -c 'read -r _'` w podpowłoce i po `Enter` wywołuje `resume()`.
|
|
94
|
+
Focus, helpMode i historia sparkline-ów są zachowane przez cały cykl.
|
|
95
|
+
- Status bar dostaje `Ctrl+E` w obu trybach (help / normal).
|
|
96
|
+
|
|
97
|
+
## [0.22.0] – 2026-04-16
|
|
98
|
+
|
|
99
|
+
### Added — backlog P0-8 (Window.setVisible)
|
|
100
|
+
- **`Window.setVisible(visible: boolean)` / `Window.isVisible(): boolean`** —
|
|
101
|
+
ortogonalny do `disabled` przełącznik widoczności. Okna konstruują się
|
|
102
|
+
jako `visible: true`; `setVisible(false)` zamienia `render()` w no-op
|
|
103
|
+
(żadna faza `paintBackground → blitContent → paintBorder → children` się
|
|
104
|
+
nie odpala), a przy przeglądzie dzieci rodzic **pomija** ukryte dziecko,
|
|
105
|
+
więc jego dotychczasowy region nie jest blitowany — widoczne jest tło
|
|
106
|
+
rodzica. `getCell` na ukrytym oknie rzuca wyjątek; `setVisible(true)`
|
|
107
|
+
przywraca okno z nienaruszoną zawartością `content` (hide/show to
|
|
108
|
+
logiczne ukrycie, a nie kasowanie buforu).
|
|
109
|
+
- **`WindowManager` respektuje widoczność** — nowy prywatny helper
|
|
110
|
+
`isFocusable(control)` = `!disabled && visible`. Tab / Shift-Tab,
|
|
111
|
+
`setFocus()`, auto-init focusa oraz mouse click hit-test pomijają
|
|
112
|
+
niewidoczne kontrolki tak samo, jak od dawna pomijają disabled.
|
|
113
|
+
|
|
114
|
+
### Demo
|
|
115
|
+
- `src/demo.mts` — `bindKey('v')` toggle'uje widoczność panelu
|
|
116
|
+
`chartsPanel` (Tabs). Status bar dostaje skrót `v` w trybie normal
|
|
117
|
+
i help, żeby feature był używalny bez sięgania do docs.
|
|
118
|
+
|
|
119
|
+
## [0.21.0] – 2026-04-16
|
|
120
|
+
|
|
121
|
+
### Added — backlog P0-6 (onChange / onSubmit / onKeyDown w TextBox + TextArea)
|
|
122
|
+
- **`TextBoxProperties` / `TextAreaProperties`** rozszerzone o
|
|
123
|
+
`onChange(value)`, `onSubmit(value)` i `onKeyDown(key)` (pre-dispatch
|
|
124
|
+
hook z semantyką `boolean | void` — return `true` = consumed,
|
|
125
|
+
identycznie jak `WindowManagerOptions.onKey` z P0-4).
|
|
126
|
+
- **`TextAreaProperties.insertTabAsSpaces`** (domyślnie `0`) — gdy
|
|
127
|
+
`> 0`, Tab wstawia N spacji zamiast cyklować focus; Shift-Tab
|
|
128
|
+
**zawsze** cykluje focus, dzięki czemu user ma kontrolowany escape
|
|
129
|
+
z multi-line input-a.
|
|
130
|
+
- **`TextAreaProperties.ctrlDDeletesForward`** (domyślnie `false`) —
|
|
131
|
+
opt-in forward-delete pod `\x04`.
|
|
132
|
+
- **`Focusable.capturesTab?()`** — opcjonalny hook sprawdzany przez
|
|
133
|
+
`WindowManager`. `TextArea` implementuje go tak, by zwracał `true`
|
|
134
|
+
wtedy i tylko wtedy gdy `insertTabAsSpaces > 0`.
|
|
135
|
+
- **Settery**: `TextBox.setOnChange/SetOnSubmit/SetOnKeyDown`,
|
|
136
|
+
`TextArea.setOnChange/SetOnSubmit/SetOnKeyDown` — do wstrzykiwania
|
|
137
|
+
callbacków po konstrukcji (np. przy YAML-buildowanych kontrolkach).
|
|
138
|
+
- **YAML / `InterfaceBuilder`** — nowe pola `YamlWindowDef`:
|
|
139
|
+
`onSubmit`, `onKeyDown`, `insertTabAsSpaces`, `ctrlDDeletesForward`.
|
|
140
|
+
Callbacki wiązane przez istniejące `ib.registerCallback(id, fn)`.
|
|
141
|
+
|
|
142
|
+
### Changed
|
|
143
|
+
- **`TextBox.handleKey`**: Enter (`\r` / `\n` / `'enter'`) nie
|
|
144
|
+
wstawia znaku — odpala `onSubmit(value)`.
|
|
145
|
+
- **`TextArea.handleKey`**: alias `'ctrl+enter'` odpala `onSubmit`;
|
|
146
|
+
plain Enter bez zmian (wstawia newline).
|
|
147
|
+
- **`WindowManager.handleInput`**: Tab najpierw pyta focused control
|
|
148
|
+
o `capturesTab()` — gdy odpowiedź to `true`, `handleKey('\t')`
|
|
149
|
+
dostaje klawisz zamiast standardowego `moveFocus(+1)`.
|
|
150
|
+
|
|
151
|
+
### Demo
|
|
152
|
+
- `layout.yaml` — `tbUsername` wiąże `onSubmit: usernameSubmitted`.
|
|
153
|
+
- `src/demo.mts` — registerCallback `usernameSubmitted` dopisuje
|
|
154
|
+
event do log-u po Enter w polu username.
|
|
155
|
+
|
|
156
|
+
## [0.20.0] – 2026-04-16
|
|
157
|
+
|
|
158
|
+
### Added — backlog P0-4 (onKey preventDefault + kolejność)
|
|
159
|
+
- **`WindowManagerOptions.onKey`** — nowa sygnatura
|
|
160
|
+
`(key: string, ctx: KeyContext) => boolean | void`. Zwrot `true`
|
|
161
|
+
*konsumuje* klawisz: pomija exit-key check, Tab/Shift-Tab navigation
|
|
162
|
+
i dispatch do focused control. Zwrot `void` / `false` zachowuje
|
|
163
|
+
dotychczasowe pass-through — istniejący kod działa bez zmian.
|
|
164
|
+
- **`KeyContext`** — nowy typ publiczny: `{ focusedControl, inDialog,
|
|
165
|
+
dialogDepth }`. Snapshot wzięty przed wywołaniem global handlerów,
|
|
166
|
+
więc widzą spójny widok focus / dialog stack.
|
|
167
|
+
- **`WindowManager.bindKey(keySpec, handler)` / `unbindKey(keySpec, handler?)`**
|
|
168
|
+
— register-based alternative dla global shortcut-ów. `bindKey` zwraca
|
|
169
|
+
unbind-funkcję. `keySpec` akceptuje raw strings, nazwy (`enter`,
|
|
170
|
+
`space`, `esc`, strzałki, `pageup/down`, `home/end`, …) oraz
|
|
171
|
+
`ctrl+<letter>`. Wiele handlerów pod jeden klawisz — fire w kolejności
|
|
172
|
+
rejestracji aż pierwszy zwróci `true`.
|
|
173
|
+
- **`KeyBindHandler`** — typ publiczny: `(ctx: KeyContext) => boolean | void`.
|
|
174
|
+
|
|
175
|
+
### Changed
|
|
176
|
+
- **Kolejność dispatchu** w `WindowManager.handleInput` — global
|
|
177
|
+
handlery (`bindKey` → `onKey`) **przed** exit-key check / Tab /
|
|
178
|
+
dispatch. Dzięki temu global shortcut może zablokować `q` exit
|
|
179
|
+
(np. confirmation dialog), a TextBox nie "połyka" `?` / `:` / itp.
|
|
180
|
+
- **Demo (`src/demo.mts`)** — `bindKey('?')` toggluje help mode
|
|
181
|
+
w status barze; `bindKey('ctrl+r')` wymusza `tick()` out-of-band.
|
|
182
|
+
Status bar stale pokazuje skrót `?` zamiast tylko `q`.
|
|
183
|
+
|
|
184
|
+
## [0.19.0] – 2026-04-16
|
|
185
|
+
|
|
186
|
+
### Added — backlog P0-11 (Screen alt-screen + hide-cursor opcja)
|
|
187
|
+
- **`ScreenOptions`** — nowy interfejs publiczny: `altScreen?`, `hideCursor?`,
|
|
188
|
+
`targetFps?`. `new Screen({ altScreen: true, hideCursor: true })` sam
|
|
189
|
+
wchodzi w alt-screen buffer i ukrywa kursor — koniec boilerplate'u dla
|
|
190
|
+
konsumentów-bez-WindowManagera.
|
|
191
|
+
- **`Screen.enterAltScreen` / `exitAltScreen` / `hideHardwareCursor` /
|
|
192
|
+
`showHardwareCursor`** + odpowiadające `is…` queries. Wszystkie idempotentne;
|
|
193
|
+
pozwalają zewnętrznemu kodowi (w tym `WindowManager`-owi) dzielić ten sam
|
|
194
|
+
state machine.
|
|
195
|
+
- **`Screen.dispose`** — przywraca każdą zmianę w stanie terminala i
|
|
196
|
+
odpina listenery sygnałów. Idempotentne. Połączone z `process.on('exit')`,
|
|
197
|
+
więc cleanup last-chance odpalą się też przy `process.exit()` /
|
|
198
|
+
naturalnym end-of-loop.
|
|
199
|
+
- **`Screen.getTargetFps`** — soft cap z `ScreenOptions` zwracany do inspekcji
|
|
200
|
+
(pełny enforcement w P2-47).
|
|
201
|
+
|
|
202
|
+
### Added — backlog P0-12 (SIGWINCH autoresize + event)
|
|
203
|
+
- **`Screen` event API**: `screen.on('resize', listener)` i
|
|
204
|
+
`screen.on('frame', listener)` (typed overloads). Pod spodem
|
|
205
|
+
composed `EventEmitter`, więc Window class hierarchy zostaje płaska.
|
|
206
|
+
- **`Screen.resize(width?, height?)`** — pobiera nowe wymiary z
|
|
207
|
+
`process.stdout` (lub z explicit args), reflowuje dzieci procentowe,
|
|
208
|
+
emituje `'resize'`. Wywoływane automatycznie przez SIGWINCH handler.
|
|
209
|
+
- **`render()`** mierzy wall-clock i emituje `'frame'` z `{ ms }`
|
|
210
|
+
po każdym wywołaniu — daje hak do telemetrii FPS / animation loopów.
|
|
211
|
+
- **`Window.setSize(width, height)`** — publiczny wrapper na
|
|
212
|
+
`resizeRegions` (zmienione z `private` na `protected`). Reflowuje
|
|
213
|
+
dzieci procentowe pod nowy parent area.
|
|
214
|
+
|
|
215
|
+
### Changed
|
|
216
|
+
- **`WindowManager.run/stop`** — używa `screen.enterAltScreen` /
|
|
217
|
+
`hideHardwareCursor` zamiast pisać escape'y wprost. Tracking ownership
|
|
218
|
+
przez `ownsAltScreen` / `ownsCursor`: jeżeli `Screen` ustawił stan
|
|
219
|
+
w konstruktorze (`ScreenOptions`), `stop()` go nie wycofuje — alt-screen
|
|
220
|
+
przeżywa restart WM-a aż do `Screen.dispose()`.
|
|
221
|
+
- **Demo (`src/demo.mts`)** — `new Screen({ altScreen: true, hideCursor: true })`,
|
|
222
|
+
`screen.on('resize', …)` aktualizujący status bar i wykonujący `screen.render()`,
|
|
223
|
+
`screen.dispose()` w `onExit` callbacku WindowManagera. Status bar wydzielony
|
|
224
|
+
jako `redrawStatusBar()` żeby etykieta `${width}×${height}` mogła być
|
|
225
|
+
przerysowana po resize.
|
|
226
|
+
- **`Screen` listener cap** — statyczny licznik aktywnych instancji bumpuje
|
|
227
|
+
`process.setMaxListeners(10 + n*2)`, dispose dekrementuje. Eliminuje
|
|
228
|
+
`MaxListenersExceededWarning` w suite-ach testowych z wieloma `new Screen()`.
|
|
229
|
+
|
|
230
|
+
### Tests
|
|
231
|
+
- 9 nowych testów w `tests/Screen.test.mts` (ScreenOptions, dispose,
|
|
232
|
+
resize, frame event) + `afterEach(dispose)`.
|
|
233
|
+
- 1 nowy test w `tests/WindowManager.test.mts` (lifecycle: brak podwójnego
|
|
234
|
+
toggle dla alt-screen i kursora) + `afterEach(dispose)`.
|
|
235
|
+
- Pełna suita: 499 testów, wszystkie zielone.
|
|
236
|
+
|
|
237
|
+
### Docs
|
|
238
|
+
- Dodano `doc/p0-11-and-p0-12-screen-lifecycle.md` — pełny opis API,
|
|
239
|
+
cyklu życia, interakcji z WindowManager, backwards compatibility i
|
|
240
|
+
zmienionych plików.
|
|
241
|
+
|
|
242
|
+
## [0.18.0] – 2026-04-16
|
|
243
|
+
|
|
244
|
+
### Added — backlog P0-2 (rich text / multi-style writeText)
|
|
245
|
+
- **`Window.writeText`** akceptuje teraz `WriteTextInput = string | WriteTextSegment[]`.
|
|
246
|
+
Segmenty są renderowane inline — kursor „płynie" przez kolejne segmenty bez
|
|
247
|
+
resetowania X, więc info-bary, history rows czy command completion nie
|
|
248
|
+
wymagają ręcznego liczenia pozycji. Każdy segment może podać własny
|
|
249
|
+
`style: StyleId` (mergowany z base) albo `attrs: CellAttributes` (rejestrowane
|
|
250
|
+
ad hoc w `StyleRegistry`). Pusta tablica jest no-opem.
|
|
251
|
+
- **`Window.writeMarkup(template, options?)`** — mini-markup `{name}…{/}` dla
|
|
252
|
+
nazwanych stylów z `StyleRegistry`. Tagi wspierają zagnieżdżanie
|
|
253
|
+
(wewnętrzny styl jest mergowany na zewnętrzny), `{/}` zamyka najbliższy
|
|
254
|
+
otwarty tag, `{{` / `}}` to escape literalnych nawiasów klamrowych, nieznane
|
|
255
|
+
nazwy nie zmieniają stylu. Template kompilowany jest do `WriteTextSegment[]`
|
|
256
|
+
i puszczany przez `writeText`, więc layout/width/clipping idą tą samą ścieżką.
|
|
257
|
+
- **`WriteTextSegment`**, **`WriteTextInput`** — nowe typy publiczne w
|
|
258
|
+
`src/Screen/types.mts`, eksportowane z `take4-console` barrel-a.
|
|
259
|
+
|
|
260
|
+
### Changed
|
|
261
|
+
- **`Window.writeText` pętla renderująca**: zachowana (East-Asian width,
|
|
262
|
+
zero-width skip, sentinel `''` dla wide chars, clipping). Dodano zewnętrzną
|
|
263
|
+
pętlę po segmentach i jednorazowe policzenie bazowego stylu.
|
|
264
|
+
- **Demo (`src/demo.mts`)** — nagłówek ekranu używa `writeMarkup` z nazwanymi
|
|
265
|
+
stylami `hdr:app` / `hdr:mode` / `hdr:sep` (rejestrowane przez
|
|
266
|
+
`Screen.setBuiltinStyle`). Status bar przeszedł na segmentowy `writeText`
|
|
267
|
+
z inline'owymi skrótami, separatorem i sekcją dim pokazującą emoji/CJK
|
|
268
|
+
(double-width) — cała linia jednym wywołaniem.
|
|
269
|
+
|
|
270
|
+
### Docs
|
|
271
|
+
- Dodano `doc/p0-2-rich-text-writetext.md` — pełny opis API, algorytmu,
|
|
272
|
+
gramatyki markup-u i kompatybilności wstecznej.
|
|
273
|
+
|
|
274
|
+
## [0.17.0] – 2026-04-16
|
|
275
|
+
|
|
276
|
+
### Added — backlog P0-7 (text measurement z East-Asian width)
|
|
277
|
+
- **`src/Screen/textWidth.mts`** — nowy moduł z funkcjami `charWidth(cp)`,
|
|
278
|
+
`stringWidth(str)`, `setPuaWidth(1|2)`, `getPuaWidth()`. Tabela szerokości
|
|
279
|
+
oparta o Unicode East Asian Width (kategorie W i F) plus zero-width
|
|
280
|
+
control / combining / format / variation selectors / ZWJ / BOM.
|
|
281
|
+
- **`Window.getTextWidth(text)`** — zwraca display width tekstu w komórkach
|
|
282
|
+
terminala, odpowiednik `stringWidth` dostępny na każdym Window/kontrolce.
|
|
283
|
+
- **`Window.writeText`** uwzględnia szerokość znaków:
|
|
284
|
+
- znaki szerokie (CJK, emoji, double-width NerdFonts) zajmują dwie kolejne
|
|
285
|
+
komórki — `''` w komórce kontynuacyjnej jako sentinel;
|
|
286
|
+
- kursor wewnątrz pętli zaawansowuje się o `charWidth(ch)`;
|
|
287
|
+
- znaki o szerokości 0 (combining, control) są pomijane;
|
|
288
|
+
- wide znaki, których prawa połówka wykraczałaby poza inner-width, są
|
|
289
|
+
pomijane w całości (zachowanie alignmentu).
|
|
290
|
+
- **`Screen.render`** pomija continuation cells (`ch === ''`), dzięki czemu
|
|
291
|
+
ANSI output nie emituje stub'a — terminal po wide znaku już ma kursor
|
|
292
|
+
zaawansowany o 2 kolumny.
|
|
293
|
+
- **Konfigurowalna szerokość PUA** (NerdFonts) przez `setPuaWidth(1|2)` —
|
|
294
|
+
domyślnie `1` (większość patched fontów ma single-cell glyphs).
|
|
295
|
+
- Eksportowane z `take4-console` barrel-a: `charWidth`, `stringWidth`,
|
|
296
|
+
`setPuaWidth`, `getPuaWidth`.
|
|
297
|
+
|
|
298
|
+
### Changed
|
|
299
|
+
- **Demo (`src/demo.mts`)** — kilka templates eventów ma double-width
|
|
300
|
+
emoji (`'cache warmed 🚀'`, `'job completed 💾'`); demo prezentuje, że
|
|
301
|
+
wide glyphs nie psują wyrównania w `ListBox` (przy emoji surrogate-pair,
|
|
302
|
+
gdzie `length=2 = displayWidth=2`).
|
|
303
|
+
|
|
304
|
+
### Added — backlog P0-9 (rozszerzenie BorderStyle)
|
|
305
|
+
- **`BorderStyle`** rozszerzony o cztery nowe warianty:
|
|
306
|
+
- `'thick'` — heavy box-drawing (`━┃┏┓┗┛`),
|
|
307
|
+
- `'dashed'` — dashed lines z light corners (`╌╎┌┐└┘`),
|
|
308
|
+
- `'ascii'` — fallback ASCII (`-|+`),
|
|
309
|
+
- `'none'` — jawny placeholder równoważny brakowi ramki (bez insetów).
|
|
310
|
+
- **`BorderChars`** — nowy interfejs publiczny opisujący komplet glifów
|
|
311
|
+
(krawędzie, narożniki, opcjonalne T-junctions i cross dla
|
|
312
|
+
przyszłych kontrolek typu Table).
|
|
313
|
+
- **`WindowBorder.chars?: Partial<BorderChars>`** — per-glyph override
|
|
314
|
+
aplikowany na bazowy zestaw wybranego `style`. Pozwala podmienić tylko
|
|
315
|
+
wybrane znaki (np. narożniki) bez redefiniowania całej tabeli.
|
|
316
|
+
- Eksportowane z `take4-console` barrel-a: typ `BorderChars`.
|
|
317
|
+
|
|
318
|
+
### Changed — backlog P0-9
|
|
319
|
+
- **`Window.borderInset`** traktuje `style: 'none'` jako brak ramki —
|
|
320
|
+
`getInnerOffset()` / `getInnerSize()` nie odejmują 1 cell-a po stronach.
|
|
321
|
+
- **`Window.paintBorder`** mergeuje `border.chars` na bazowy zestaw
|
|
322
|
+
(`{ ...baseChars, ...border.chars }`) i przerywa się natychmiast dla
|
|
323
|
+
`'none'`.
|
|
324
|
+
- **Internal `BORDER_CHARS`** używa pełnych nazw glifów z `BorderChars`
|
|
325
|
+
(`horizontal`/`vertical`/`topLeft`/…) zamiast wcześniejszych skrótów
|
|
326
|
+
(`h`/`v`/`tl`/…). Pole jest prywatne — bez wpływu na konsumentów.
|
|
327
|
+
- **Demo (`src/layout.yaml`)** — `monitorPanel` używa `style: thick`,
|
|
328
|
+
`eventsPanel` używa `style: dashed`, `leftPanel` demonstruje override
|
|
329
|
+
pojedynczych glifów (`chars: { topLeft: '◆', topRight: '◆' }`).
|
|
330
|
+
|
|
331
|
+
### Docs
|
|
332
|
+
- Dodano `doc/p0-7-text-width.md` — pełny opis API, algorytmu sentinela
|
|
333
|
+
continuation cell, tabel Unicode i kompatybilności wstecznej.
|
|
334
|
+
- Dodano `doc/p0-9-border-styles.md` — opis nowych stylów, tabela
|
|
335
|
+
glifów, semantyka `'none'` i przykłady override `chars`.
|
|
336
|
+
|
|
337
|
+
## [0.16.0] – 2026-04-16
|
|
338
|
+
|
|
339
|
+
### Added — backlog P0-1 (custom per-row rendering w ListBox)
|
|
340
|
+
- **`ListBox<T>` jest teraz generyczny** — element listy może być dowolnym typem
|
|
341
|
+
(domyślnie `string`, co zachowuje wsteczną kompatybilność). `ListBoxProperties<T>`,
|
|
342
|
+
`setItems(items: T[])`, `getSelectedItem(): T | undefined`, `onChange(idx, item: T)`.
|
|
343
|
+
- **`renderItem(item, ctx): string | ListBoxRowSegment[]`** — opcjonalny per-row
|
|
344
|
+
renderer. Obsługuje segmenty z wyrównaniem `left` / `right` / `fill`; style
|
|
345
|
+
segmentów są mergowane ze stylem bazowym wiersza (tło selekcji pozostaje widoczne).
|
|
346
|
+
- **`rowHeight`** — wysokość slotu w komórkach (domyślnie 1). `handleKey` PgUp/PgDn
|
|
347
|
+
uwzględnia liczbę widocznych slotów, nie surowych wierszy.
|
|
348
|
+
- **`keyFn`** — stały klucz per-item, dostępny przez `getItemKey(item)`; przechowywany
|
|
349
|
+
pod kątem późniejszej reconciliation w `setItems()`.
|
|
350
|
+
- **`setRenderItem(fn)`** — podmiana renderera po konstrukcji.
|
|
351
|
+
- Nowe typy w `src/Screen/types.mts`: `ListBoxRenderContext`, `ListBoxRowSegment`,
|
|
352
|
+
`ListBoxRowSegments`. Eksportowane z `take4-console` barrel-a.
|
|
353
|
+
|
|
354
|
+
### Changed
|
|
355
|
+
- **Demo (`src/demo.mts` + `src/layout.yaml`)** — lista zdarzeń (`eventsList`) to teraz
|
|
356
|
+
`ListBox<EventRow>` z customowym rendererem pokazującym kolorowe ikony (✓/⚠/✖),
|
|
357
|
+
wyciemniony timestamp i licznik wyrównany do prawej krawędzi.
|
|
358
|
+
|
|
359
|
+
### Docs
|
|
360
|
+
- Dodano `doc/p0-1-listbox-custom-render.md` — pełny opis API, algorytmu
|
|
361
|
+
renderowania segmentów i kompatybilności wstecznej.
|
|
362
|
+
|
|
3
363
|
## [0.15.1] – 2026-04-12
|
|
4
364
|
|
|
5
365
|
### Fixed
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CustomTypeFactory } from './types.mjs';
|
|
1
2
|
import { Window } from './Window.mjs';
|
|
2
3
|
import { Screen } from './Screen.mjs';
|
|
3
4
|
import { WindowManager } from './WindowManager.mjs';
|
|
@@ -5,9 +6,11 @@ import { WindowManager } from './WindowManager.mjs';
|
|
|
5
6
|
*
|
|
6
7
|
* Usage:
|
|
7
8
|
* 1. Create an InterfaceBuilder and call registerCallback() for any onPress/onChange IDs.
|
|
8
|
-
* 2.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* 2. Optionally call registerType(name, factory) to teach the builder about
|
|
10
|
+
* user-defined controls addressable via `type: <name>` in YAML.
|
|
11
|
+
* 3. Call build(yamlText, screen) or buildFromFile(path, screen).
|
|
12
|
+
* 4. Optionally pass a WindowManager to automatically register all focusable controls.
|
|
13
|
+
* 5. The returned Map<string, Window> gives access to windows by their YAML id.
|
|
11
14
|
*
|
|
12
15
|
* YAML schema:
|
|
13
16
|
* windows:
|
|
@@ -22,10 +25,18 @@ import { WindowManager } from './WindowManager.mjs';
|
|
|
22
25
|
*/
|
|
23
26
|
export declare class InterfaceBuilder {
|
|
24
27
|
private callbacks;
|
|
25
|
-
|
|
28
|
+
private customTypes;
|
|
29
|
+
/** Creates an InterfaceBuilder with empty callback and custom-type registries. */
|
|
26
30
|
constructor();
|
|
27
31
|
/** Registers a named callback for use with onPress or onChange in YAML definitions. */
|
|
28
32
|
registerCallback(id: string, fn: (...args: unknown[]) => void): void;
|
|
33
|
+
/** Registers a custom control factory addressable by `type: <name>` in YAML.
|
|
34
|
+
* The factory receives the raw YAML node plus a context with pre-resolved
|
|
35
|
+
* `wp` (WindowProperties), the style registry, and a callback resolver.
|
|
36
|
+
* Custom names cannot override a built-in type tag. If the returned window
|
|
37
|
+
* implements Focusable, it is auto-registered with WindowManager (when one
|
|
38
|
+
* is supplied to `build()`). */
|
|
39
|
+
registerType(name: string, factory: CustomTypeFactory): void;
|
|
29
40
|
/** Builds the UI from a YAML string, adds all top-level windows to Screen,
|
|
30
41
|
* and registers focusable controls with WindowManager if provided.
|
|
31
42
|
* Styles defined in the `styles:` section are registered before any windows are built.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InterfaceBuilder.d.mts","sourceRoot":"","sources":["../../src/Screen/InterfaceBuilder.mts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"InterfaceBuilder.d.mts","sourceRoot":"","sources":["../../src/Screen/InterfaceBuilder.mts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAUV,iBAAiB,EAElB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAiIpD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,SAAS,CAA8C;IAC/D,OAAO,CAAC,WAAW,CAAiC;IAEpD,kFAAkF;;IAMlF,uFAAuF;IAChF,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,GAAG,IAAI;IAI3E;;;;;qCAKiC;IAC1B,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,IAAI;IAOnE;;;4EAGwE;IACjE,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,aAAa,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;IAmCvF,+DAA+D;IAClD,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAO9G;;yEAEqE;IACrE,OAAO,CAAC,SAAS;IAqPjB,8EAA8E;IAC9E,OAAO,CAAC,WAAW;CAOpB"}
|
|
@@ -2,7 +2,7 @@ import { parse } from 'yaml';
|
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { Window } from './Window.mjs';
|
|
4
4
|
import { Pos, pct } from './Pos.mjs';
|
|
5
|
-
import { Size } from './Size.mjs';
|
|
5
|
+
import { Size, flex, content } from './Size.mjs';
|
|
6
6
|
import { getRegistry } from './RegistryHolder.mjs';
|
|
7
7
|
import { Button } from './controls/Button.mjs';
|
|
8
8
|
import { TextBox } from './controls/TextBox.mjs';
|
|
@@ -18,6 +18,21 @@ import { ListBox } from './controls/ListBox.mjs';
|
|
|
18
18
|
import { Tabs } from './controls/Tabs.mjs';
|
|
19
19
|
import { Sparkline } from './controls/Sparkline.mjs';
|
|
20
20
|
import { Spinner } from './controls/Spinner.mjs';
|
|
21
|
+
// ── Internal helpers ──────────────────────────────────────────────────────────
|
|
22
|
+
/** Reserved `type:` tags owned by the built-in widget switch — custom factories
|
|
23
|
+
* cannot override these. */
|
|
24
|
+
/** Structural check: returns true when the window opts into keyboard input by
|
|
25
|
+
* exposing `handleKey` (on top of the focus helpers inherited from Window).
|
|
26
|
+
* Used to auto-register custom-type controls with WindowManager. */
|
|
27
|
+
function isFocusable(win) {
|
|
28
|
+
const w = win;
|
|
29
|
+
return typeof w.handleKey === 'function';
|
|
30
|
+
}
|
|
31
|
+
const BUILTIN_TYPE_NAMES = new Set([
|
|
32
|
+
'window', 'button', 'textbox', 'textarea', 'checkbox', 'radio',
|
|
33
|
+
'statusled', 'progressbar', 'progressbarv', 'linechart', 'barchart',
|
|
34
|
+
'listbox', 'tabs', 'sparkline', 'spinner',
|
|
35
|
+
]);
|
|
21
36
|
/** Converts a YamlAxisValue ("N%", or a number) to a number or Pct instance. */
|
|
22
37
|
function parseAxisValue(v) {
|
|
23
38
|
if (typeof v === 'string') {
|
|
@@ -42,6 +57,8 @@ function parsePos(spec) {
|
|
|
42
57
|
return Pos.bottomLeft();
|
|
43
58
|
if (spec === 'bottomRight')
|
|
44
59
|
return Pos.bottomRight();
|
|
60
|
+
if (spec === 'flex')
|
|
61
|
+
return Pos.flex();
|
|
45
62
|
if (typeof spec === 'object') {
|
|
46
63
|
if ('preset' in spec) {
|
|
47
64
|
const off = spec.offset !== undefined ? parseAxisValue(spec.offset) : 0;
|
|
@@ -52,23 +69,52 @@ function parsePos(spec) {
|
|
|
52
69
|
case 'bottom': return Pos.bottom(off);
|
|
53
70
|
}
|
|
54
71
|
}
|
|
72
|
+
if ('flex' in spec) {
|
|
73
|
+
return Pos.flex(spec.flex ?? 0);
|
|
74
|
+
}
|
|
55
75
|
if ('x' in spec && 'y' in spec) {
|
|
56
76
|
return new Pos(parseAxisValue(spec.x), parseAxisValue(spec.y));
|
|
57
77
|
}
|
|
58
78
|
}
|
|
59
79
|
throw new Error(`Invalid pos spec: ${JSON.stringify(spec)}`);
|
|
60
80
|
}
|
|
81
|
+
/** Converts a single axis value within a Size spec — extends the plain-axis
|
|
82
|
+
* parser with the 'flex' / 'content' shorthands and the `{ flex: {...} }` /
|
|
83
|
+
* `{ content: true }` objects so each axis can be chosen independently. */
|
|
84
|
+
function parseDimValue(v) {
|
|
85
|
+
if (v === 'flex')
|
|
86
|
+
return flex();
|
|
87
|
+
if (v === 'content')
|
|
88
|
+
return content();
|
|
89
|
+
if (typeof v === 'object' && v !== null) {
|
|
90
|
+
if ('flex' in v) {
|
|
91
|
+
const basis = v.flex.basis !== undefined ? parseAxisValue(v.flex.basis) : 0;
|
|
92
|
+
return flex(v.flex.grow ?? 1, v.flex.shrink ?? 1, basis);
|
|
93
|
+
}
|
|
94
|
+
if ('content' in v && v.content === true)
|
|
95
|
+
return content();
|
|
96
|
+
}
|
|
97
|
+
return parseAxisValue(v);
|
|
98
|
+
}
|
|
61
99
|
/** Converts a YamlSizeSpec to a Size instance. */
|
|
62
100
|
function parseSize(spec) {
|
|
63
101
|
if (spec === 'fill')
|
|
64
102
|
return Size.fill();
|
|
103
|
+
if (spec === 'flex')
|
|
104
|
+
return Size.flex();
|
|
105
|
+
if (spec === 'content')
|
|
106
|
+
return Size.content();
|
|
65
107
|
if (typeof spec === 'object') {
|
|
66
108
|
if ('fillWidth' in spec)
|
|
67
109
|
return Size.fillWidth(parseAxisValue(spec.fillWidth));
|
|
68
110
|
if ('fillHeight' in spec)
|
|
69
111
|
return Size.fillHeight(parseAxisValue(spec.fillHeight));
|
|
112
|
+
if ('flex' in spec) {
|
|
113
|
+
const basis = spec.flex.basis !== undefined ? parseAxisValue(spec.flex.basis) : 0;
|
|
114
|
+
return Size.flex(spec.flex.grow ?? 1, spec.flex.shrink ?? 1, basis);
|
|
115
|
+
}
|
|
70
116
|
if ('width' in spec && 'height' in spec) {
|
|
71
|
-
return new Size(
|
|
117
|
+
return new Size(parseDimValue(spec.width), parseDimValue(spec.height));
|
|
72
118
|
}
|
|
73
119
|
}
|
|
74
120
|
throw new Error(`Invalid size spec: ${JSON.stringify(spec)}`);
|
|
@@ -88,9 +134,11 @@ function resolveBackground(bg) {
|
|
|
88
134
|
*
|
|
89
135
|
* Usage:
|
|
90
136
|
* 1. Create an InterfaceBuilder and call registerCallback() for any onPress/onChange IDs.
|
|
91
|
-
* 2.
|
|
92
|
-
*
|
|
93
|
-
*
|
|
137
|
+
* 2. Optionally call registerType(name, factory) to teach the builder about
|
|
138
|
+
* user-defined controls addressable via `type: <name>` in YAML.
|
|
139
|
+
* 3. Call build(yamlText, screen) or buildFromFile(path, screen).
|
|
140
|
+
* 4. Optionally pass a WindowManager to automatically register all focusable controls.
|
|
141
|
+
* 5. The returned Map<string, Window> gives access to windows by their YAML id.
|
|
94
142
|
*
|
|
95
143
|
* YAML schema:
|
|
96
144
|
* windows:
|
|
@@ -105,14 +153,28 @@ function resolveBackground(bg) {
|
|
|
105
153
|
*/
|
|
106
154
|
export class InterfaceBuilder {
|
|
107
155
|
callbacks;
|
|
108
|
-
|
|
156
|
+
customTypes;
|
|
157
|
+
/** Creates an InterfaceBuilder with empty callback and custom-type registries. */
|
|
109
158
|
constructor() {
|
|
110
159
|
this.callbacks = new Map();
|
|
160
|
+
this.customTypes = new Map();
|
|
111
161
|
}
|
|
112
162
|
/** Registers a named callback for use with onPress or onChange in YAML definitions. */
|
|
113
163
|
registerCallback(id, fn) {
|
|
114
164
|
this.callbacks.set(id, fn);
|
|
115
165
|
}
|
|
166
|
+
/** Registers a custom control factory addressable by `type: <name>` in YAML.
|
|
167
|
+
* The factory receives the raw YAML node plus a context with pre-resolved
|
|
168
|
+
* `wp` (WindowProperties), the style registry, and a callback resolver.
|
|
169
|
+
* Custom names cannot override a built-in type tag. If the returned window
|
|
170
|
+
* implements Focusable, it is auto-registered with WindowManager (when one
|
|
171
|
+
* is supplied to `build()`). */
|
|
172
|
+
registerType(name, factory) {
|
|
173
|
+
if (BUILTIN_TYPE_NAMES.has(name)) {
|
|
174
|
+
throw new Error(`Cannot register custom type "${name}": name is reserved by a built-in type.`);
|
|
175
|
+
}
|
|
176
|
+
this.customTypes.set(name, factory);
|
|
177
|
+
}
|
|
116
178
|
/** Builds the UI from a YAML string, adds all top-level windows to Screen,
|
|
117
179
|
* and registers focusable controls with WindowManager if provided.
|
|
118
180
|
* Styles defined in the `styles:` section are registered before any windows are built.
|
|
@@ -168,6 +230,12 @@ export class InterfaceBuilder {
|
|
|
168
230
|
focused: def.focused,
|
|
169
231
|
disabled: def.disabled,
|
|
170
232
|
label: def.label,
|
|
233
|
+
layout: def.layout,
|
|
234
|
+
gap: def.gap,
|
|
235
|
+
padding: def.padding,
|
|
236
|
+
gridColumns: def.gridColumns,
|
|
237
|
+
alignItems: def.alignItems,
|
|
238
|
+
justifyContent: def.justifyContent,
|
|
171
239
|
};
|
|
172
240
|
let win;
|
|
173
241
|
switch (def.type ?? 'window') {
|
|
@@ -183,9 +251,15 @@ export class InterfaceBuilder {
|
|
|
183
251
|
}
|
|
184
252
|
case 'textbox': {
|
|
185
253
|
wp.size = wp.size ?? this.requireSize(def);
|
|
254
|
+
const tbChange = def.onChange ? this.callbacks.get(def.onChange) : undefined;
|
|
255
|
+
const tbSubmit = def.onSubmit ? this.callbacks.get(def.onSubmit) : undefined;
|
|
256
|
+
const tbKeyDown = def.onKeyDown ? this.callbacks.get(def.onKeyDown) : undefined;
|
|
186
257
|
const tb = new TextBox(wp, {
|
|
187
258
|
value: def.value,
|
|
188
259
|
placeholder: def.placeholder,
|
|
260
|
+
onChange: tbChange,
|
|
261
|
+
onSubmit: tbSubmit,
|
|
262
|
+
onKeyDown: tbKeyDown,
|
|
189
263
|
});
|
|
190
264
|
pending.push({ control: tb, parents: [...parentChain] });
|
|
191
265
|
win = tb;
|
|
@@ -193,9 +267,17 @@ export class InterfaceBuilder {
|
|
|
193
267
|
}
|
|
194
268
|
case 'textarea': {
|
|
195
269
|
wp.size = wp.size ?? this.requireSize(def);
|
|
270
|
+
const taChange = def.onChange ? this.callbacks.get(def.onChange) : undefined;
|
|
271
|
+
const taSubmit = def.onSubmit ? this.callbacks.get(def.onSubmit) : undefined;
|
|
272
|
+
const taKeyDown = def.onKeyDown ? this.callbacks.get(def.onKeyDown) : undefined;
|
|
196
273
|
const ta = new TextArea(wp, {
|
|
197
274
|
value: def.value,
|
|
198
275
|
placeholder: def.placeholder,
|
|
276
|
+
onChange: taChange,
|
|
277
|
+
onSubmit: taSubmit,
|
|
278
|
+
onKeyDown: taKeyDown,
|
|
279
|
+
insertTabAsSpaces: def.insertTabAsSpaces,
|
|
280
|
+
ctrlDDeletesForward: def.ctrlDDeletesForward,
|
|
199
281
|
});
|
|
200
282
|
pending.push({ control: ta, parents: [...parentChain] });
|
|
201
283
|
win = ta;
|
|
@@ -320,8 +402,22 @@ export class InterfaceBuilder {
|
|
|
320
402
|
break;
|
|
321
403
|
}
|
|
322
404
|
default: {
|
|
323
|
-
|
|
324
|
-
|
|
405
|
+
const customFactory = def.type ? this.customTypes.get(def.type) : undefined;
|
|
406
|
+
if (customFactory) {
|
|
407
|
+
const ctx = {
|
|
408
|
+
wp,
|
|
409
|
+
registry: getRegistry(),
|
|
410
|
+
resolveCallback: (id) => (id ? this.callbacks.get(id) : undefined),
|
|
411
|
+
};
|
|
412
|
+
win = customFactory(def, ctx);
|
|
413
|
+
if (isFocusable(win)) {
|
|
414
|
+
pending.push({ control: win, parents: [...parentChain] });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
wp.size = wp.size ?? this.requireSize(def);
|
|
419
|
+
win = new Window(wp);
|
|
420
|
+
}
|
|
325
421
|
break;
|
|
326
422
|
}
|
|
327
423
|
}
|