take4-console 0.30.0 → 0.31.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 +60 -0
- package/README.md +3 -2
- package/dist/Screen/Screen.d.mts +40 -0
- package/dist/Screen/Screen.d.mts.map +1 -1
- package/dist/Screen/Screen.mjs +148 -1
- package/dist/Screen/Screen.mjs.map +1 -1
- package/dist/Screen/Window.d.mts +50 -1
- package/dist/Screen/Window.d.mts.map +1 -1
- package/dist/Screen/Window.mjs +176 -19
- package/dist/Screen/Window.mjs.map +1 -1
- package/dist/Screen/WindowManager.d.mts.map +1 -1
- package/dist/Screen/WindowManager.mjs +11 -0
- package/dist/Screen/WindowManager.mjs.map +1 -1
- package/dist/Screen/controls/BarChart.d.mts.map +1 -1
- package/dist/Screen/controls/BarChart.mjs +3 -0
- package/dist/Screen/controls/BarChart.mjs.map +1 -1
- package/dist/Screen/controls/Checkbox.d.mts.map +1 -1
- package/dist/Screen/controls/Checkbox.mjs +4 -0
- package/dist/Screen/controls/Checkbox.mjs.map +1 -1
- package/dist/Screen/controls/LineChart.d.mts.map +1 -1
- package/dist/Screen/controls/LineChart.mjs +3 -0
- package/dist/Screen/controls/LineChart.mjs.map +1 -1
- package/dist/Screen/controls/ListBox.d.mts.map +1 -1
- package/dist/Screen/controls/ListBox.mjs +9 -0
- package/dist/Screen/controls/ListBox.mjs.map +1 -1
- package/dist/Screen/controls/ProgressBar.d.mts.map +1 -1
- package/dist/Screen/controls/ProgressBar.mjs +2 -0
- package/dist/Screen/controls/ProgressBar.mjs.map +1 -1
- package/dist/Screen/controls/ProgressBarV.d.mts.map +1 -1
- package/dist/Screen/controls/ProgressBarV.mjs +2 -0
- package/dist/Screen/controls/ProgressBarV.mjs.map +1 -1
- package/dist/Screen/controls/Radio.d.mts.map +1 -1
- package/dist/Screen/controls/Radio.mjs +7 -1
- package/dist/Screen/controls/Radio.mjs.map +1 -1
- package/dist/Screen/controls/Sparkline.d.mts.map +1 -1
- package/dist/Screen/controls/Sparkline.mjs +3 -0
- package/dist/Screen/controls/Sparkline.mjs.map +1 -1
- package/dist/Screen/controls/Spinner.d.mts.map +1 -1
- package/dist/Screen/controls/Spinner.mjs +8 -0
- package/dist/Screen/controls/Spinner.mjs.map +1 -1
- package/dist/Screen/controls/StatusLED.d.mts.map +1 -1
- package/dist/Screen/controls/StatusLED.mjs +3 -0
- package/dist/Screen/controls/StatusLED.mjs.map +1 -1
- package/dist/Screen/controls/Tabs.d.mts.map +1 -1
- package/dist/Screen/controls/Tabs.mjs +2 -0
- package/dist/Screen/controls/Tabs.mjs.map +1 -1
- package/dist/Screen/controls/TextArea.d.mts.map +1 -1
- package/dist/Screen/controls/TextArea.mjs +11 -0
- package/dist/Screen/controls/TextArea.mjs.map +1 -1
- package/dist/Screen/controls/TextBox.d.mts.map +1 -1
- package/dist/Screen/controls/TextBox.mjs +13 -0
- package/dist/Screen/controls/TextBox.mjs.map +1 -1
- package/dist/Screen/types.d.mts +26 -0
- package/dist/Screen/types.d.mts.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.mts.map +1 -1
- package/package.json +1 -1
- package/src/Screen/Screen.mts +148 -2
- package/src/Screen/Window.mts +165 -16
- package/src/Screen/WindowManager.mts +11 -0
- package/src/Screen/controls/BarChart.mts +3 -0
- package/src/Screen/controls/Checkbox.mts +3 -0
- package/src/Screen/controls/LineChart.mts +3 -0
- package/src/Screen/controls/ListBox.mts +8 -0
- package/src/Screen/controls/ProgressBar.mts +2 -0
- package/src/Screen/controls/ProgressBarV.mts +2 -0
- package/src/Screen/controls/Radio.mts +6 -1
- package/src/Screen/controls/Sparkline.mts +3 -0
- package/src/Screen/controls/Spinner.mts +6 -0
- package/src/Screen/controls/StatusLED.mts +2 -0
- package/src/Screen/controls/Tabs.mts +2 -0
- package/src/Screen/controls/TextArea.mts +10 -0
- package/src/Screen/controls/TextBox.mts +12 -0
- package/src/Screen/types.mts +27 -0
- package/src/demo.mts +51 -0
- package/src/index.mts +1 -0
|
@@ -31,6 +31,7 @@ export class Sparkline extends Window {
|
|
|
31
31
|
/** Sets the data series. Call render() afterwards. */
|
|
32
32
|
public setData(data: number[]): void {
|
|
33
33
|
this.data = data;
|
|
34
|
+
this.markDirty();
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
/** Returns the current data series. */
|
|
@@ -41,6 +42,7 @@ export class Sparkline extends Window {
|
|
|
41
42
|
/** Sets the minimum value. Pass undefined to derive from data. Call render() afterwards. */
|
|
42
43
|
public setMin(min: number | undefined): void {
|
|
43
44
|
this.minValue = min;
|
|
45
|
+
this.markDirty();
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
/** Returns the configured minimum value, or undefined if derived from data. */
|
|
@@ -51,6 +53,7 @@ export class Sparkline extends Window {
|
|
|
51
53
|
/** Sets the maximum value. Pass undefined to derive from data. Call render() afterwards. */
|
|
52
54
|
public setMax(max: number | undefined): void {
|
|
53
55
|
this.maxValue = max;
|
|
56
|
+
this.markDirty();
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
/** Returns the configured maximum value, or undefined if derived from data. */
|
|
@@ -52,12 +52,14 @@ export class Spinner extends Window {
|
|
|
52
52
|
public step(): void {
|
|
53
53
|
if (!this.running || this.frames.length === 0) return;
|
|
54
54
|
this.frame = (this.frame + 1) % this.frames.length;
|
|
55
|
+
this.markDirty();
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
/** Sets the current frame index directly. Wraps into the valid range. */
|
|
58
59
|
public setFrame(frame: number): void {
|
|
59
60
|
if (this.frames.length === 0) return;
|
|
60
61
|
this.frame = ((frame % this.frames.length) + this.frames.length) % this.frames.length;
|
|
62
|
+
this.markDirty();
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
/** Returns the current frame index. */
|
|
@@ -67,12 +69,16 @@ export class Spinner extends Window {
|
|
|
67
69
|
|
|
68
70
|
/** Starts or resumes the animation. step() will advance frames again. */
|
|
69
71
|
public start(): void {
|
|
72
|
+
if (this.running) return;
|
|
70
73
|
this.running = true;
|
|
74
|
+
this.markDirty();
|
|
71
75
|
}
|
|
72
76
|
|
|
73
77
|
/** Pauses the animation. step() becomes a no-op; the current frame stays visible. */
|
|
74
78
|
public stop(): void {
|
|
79
|
+
if (!this.running) return;
|
|
75
80
|
this.running = false;
|
|
81
|
+
this.markDirty();
|
|
76
82
|
}
|
|
77
83
|
|
|
78
84
|
/** Returns whether the spinner is currently animating. */
|
|
@@ -38,7 +38,9 @@ export class StatusLED extends Window {
|
|
|
38
38
|
|
|
39
39
|
/** Sets the current LED state. Call render() afterwards to update the display. */
|
|
40
40
|
public setState(state: 'ok' | 'warn' | 'error' | 'off'): void {
|
|
41
|
+
if (this.state === state) return;
|
|
41
42
|
this.state = state;
|
|
43
|
+
this.markDirty();
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
/** Returns the current LED state. */
|
|
@@ -48,6 +48,7 @@ export class Tabs extends Window {
|
|
|
48
48
|
public setTitles(titles: string[]): void {
|
|
49
49
|
this.titles = titles;
|
|
50
50
|
this.activeIndex = Math.max(0, Math.min(titles.length - 1, this.activeIndex));
|
|
51
|
+
this.markDirty();
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
/** Returns the current tab titles. */
|
|
@@ -64,6 +65,7 @@ export class Tabs extends Window {
|
|
|
64
65
|
const clamped = Math.max(0, Math.min(this.titles.length - 1, index));
|
|
65
66
|
if (clamped !== this.activeIndex) {
|
|
66
67
|
this.activeIndex = clamped;
|
|
68
|
+
this.markDirty();
|
|
67
69
|
this.onChange?.(clamped, this.titles[clamped]);
|
|
68
70
|
}
|
|
69
71
|
}
|
|
@@ -82,6 +82,7 @@ export class TextArea extends Window {
|
|
|
82
82
|
this.cursor.x = Math.min(this.cursor.x, this.lines[this.cursor.y].length);
|
|
83
83
|
this.selectionAnchor = null;
|
|
84
84
|
this.clampScroll();
|
|
85
|
+
this.markDirty();
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
/** Returns the normalized selection range `{ start, end }` in 2-D
|
|
@@ -120,6 +121,7 @@ export class TextArea extends Window {
|
|
|
120
121
|
this.selectionAnchor = (a.x === c.x && a.y === c.y) ? null : a;
|
|
121
122
|
this.cursor = c;
|
|
122
123
|
this.clampScroll();
|
|
124
|
+
this.markDirty();
|
|
123
125
|
}
|
|
124
126
|
|
|
125
127
|
/** Selects every character in the buffer. No-op when the buffer holds a
|
|
@@ -134,11 +136,14 @@ export class TextArea extends Window {
|
|
|
134
136
|
this.selectionAnchor = { x: 0, y: 0 };
|
|
135
137
|
this.cursor = { x: lastX, y: lastY };
|
|
136
138
|
this.clampScroll();
|
|
139
|
+
this.markDirty();
|
|
137
140
|
}
|
|
138
141
|
|
|
139
142
|
/** Drops any active selection without moving the cursor. */
|
|
140
143
|
public clearSelection(): void {
|
|
144
|
+
if (this.selectionAnchor === null) return;
|
|
141
145
|
this.selectionAnchor = null;
|
|
146
|
+
this.markDirty();
|
|
142
147
|
}
|
|
143
148
|
|
|
144
149
|
/** Clamps a 2-D position so both components land inside the buffer. */
|
|
@@ -182,6 +187,7 @@ export class TextArea extends Window {
|
|
|
182
187
|
this.cursor.x = Math.max(0, Math.min(pos.x, this.lines[this.cursor.y].length));
|
|
183
188
|
this.selectionAnchor = null;
|
|
184
189
|
this.clampScroll();
|
|
190
|
+
this.markDirty();
|
|
185
191
|
}
|
|
186
192
|
|
|
187
193
|
/** Returns a copy of the current cursor position. */
|
|
@@ -363,6 +369,10 @@ export class TextArea extends Window {
|
|
|
363
369
|
private finishKey(before: string): void {
|
|
364
370
|
this.clampScroll();
|
|
365
371
|
this.virtualCursor.resetPhase();
|
|
372
|
+
// Every key dispatch may have moved the cursor, changed the
|
|
373
|
+
// selection, or edited the buffer — flag the window dirty so the
|
|
374
|
+
// next Screen.render() re-emits the composed state.
|
|
375
|
+
this.markDirty();
|
|
366
376
|
if (this.getValue() !== before) this.onChange?.(this.getValue());
|
|
367
377
|
}
|
|
368
378
|
|
|
@@ -75,6 +75,7 @@ export class TextBox extends Window {
|
|
|
75
75
|
this.cursor = Math.min(this.cursor, value.length);
|
|
76
76
|
this.selectionAnchor = null;
|
|
77
77
|
this.clampScroll();
|
|
78
|
+
this.markDirty();
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
/** Returns the normalized selection range `{ start, end }` (half-open)
|
|
@@ -103,6 +104,7 @@ export class TextBox extends Window {
|
|
|
103
104
|
this.selectionAnchor = a === c ? null : a;
|
|
104
105
|
this.cursor = c;
|
|
105
106
|
this.clampScroll();
|
|
107
|
+
this.markDirty();
|
|
106
108
|
}
|
|
107
109
|
|
|
108
110
|
/** Selects every character in the current value. No-op when the value is empty. */
|
|
@@ -114,11 +116,14 @@ export class TextBox extends Window {
|
|
|
114
116
|
this.selectionAnchor = 0;
|
|
115
117
|
this.cursor = this.value.length;
|
|
116
118
|
this.clampScroll();
|
|
119
|
+
this.markDirty();
|
|
117
120
|
}
|
|
118
121
|
|
|
119
122
|
/** Drops any active selection without moving the cursor. */
|
|
120
123
|
public clearSelection(): void {
|
|
124
|
+
if (this.selectionAnchor === null) return;
|
|
121
125
|
this.selectionAnchor = null;
|
|
126
|
+
this.markDirty();
|
|
122
127
|
}
|
|
123
128
|
|
|
124
129
|
/** Replaces the onChange callback (passing undefined clears it). */
|
|
@@ -148,6 +153,7 @@ export class TextBox extends Window {
|
|
|
148
153
|
this.cursor = Math.max(0, Math.min(pos, this.value.length));
|
|
149
154
|
this.selectionAnchor = null;
|
|
150
155
|
this.clampScroll();
|
|
156
|
+
this.markDirty();
|
|
151
157
|
}
|
|
152
158
|
|
|
153
159
|
/** Returns the current cursor character index. */
|
|
@@ -256,6 +262,12 @@ export class TextBox extends Window {
|
|
|
256
262
|
private finishKey(before: string): void {
|
|
257
263
|
this.clampScroll();
|
|
258
264
|
this.virtualCursor.resetPhase();
|
|
265
|
+
// Every key dispatch is a potential content / selection / cursor
|
|
266
|
+
// change — flag the window dirty unconditionally so the next
|
|
267
|
+
// Screen.render() re-emits it. The per-key equality checks in
|
|
268
|
+
// individual branches are not worth the code; the bounding box is
|
|
269
|
+
// cheap and we're going to emit at most the whole window.
|
|
270
|
+
this.markDirty();
|
|
259
271
|
if (this.value !== before) this.onChange?.(this.value);
|
|
260
272
|
}
|
|
261
273
|
|
package/src/Screen/types.mts
CHANGED
|
@@ -110,12 +110,39 @@ export interface ScreenOptions {
|
|
|
110
110
|
* for inspection; full enforcement (frame coalescing) lands with backlog
|
|
111
111
|
* item P2-47. Default: undefined (uncapped). */
|
|
112
112
|
targetFps?: number;
|
|
113
|
+
/** Enables damage-region tracking: `Screen.render()` collects dirty rects
|
|
114
|
+
* propagated bottom-up from each mutated window and emits ANSI sequences
|
|
115
|
+
* only for the changed cells. A frame with no dirty rects is skipped
|
|
116
|
+
* entirely (no stdout traffic). The first frame and every frame after
|
|
117
|
+
* `resize()` / `invalidate()` fall back to a full repaint. Default:
|
|
118
|
+
* `true`. Set to `false` for the pre-0.31 behaviour (always emit the
|
|
119
|
+
* full buffer), useful for debugging or terminals that mis-handle
|
|
120
|
+
* partial cursor jumps. */
|
|
121
|
+
damageTracking?: boolean;
|
|
113
122
|
}
|
|
114
123
|
|
|
115
124
|
/** Statistics emitted by the Screen 'frame' event after each render() call. */
|
|
116
125
|
export interface ScreenFrameStats {
|
|
117
126
|
/** Wall-clock duration of the render() call in milliseconds. */
|
|
118
127
|
ms: number;
|
|
128
|
+
/** Number of cells emitted to stdout during this frame (0 when the frame
|
|
129
|
+
* was skipped because nothing was dirty). Populated only when damage
|
|
130
|
+
* tracking is enabled; undefined otherwise. */
|
|
131
|
+
cellsEmitted?: number;
|
|
132
|
+
/** True when the emit path was a full-screen repaint (first frame,
|
|
133
|
+
* post-resize, or damage tracking disabled); false when only dirty runs
|
|
134
|
+
* were emitted. Undefined when the frame was skipped entirely. */
|
|
135
|
+
fullRepaint?: boolean;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Rectangle expressed in the owning Window's local coordinate space (for
|
|
139
|
+
* dirty regions on a Window) or in Screen coordinates (when collected by
|
|
140
|
+
* `Screen.render()` during damage tracking). */
|
|
141
|
+
export interface DirtyRect {
|
|
142
|
+
x: number;
|
|
143
|
+
y: number;
|
|
144
|
+
w: number;
|
|
145
|
+
h: number;
|
|
119
146
|
}
|
|
120
147
|
|
|
121
148
|
/** Box-drawing character style for window borders.
|
package/src/demo.mts
CHANGED
|
@@ -15,6 +15,8 @@ import { ListBox } from './Screen/controls/ListBox.mjs';
|
|
|
15
15
|
import { Tabs } from './Screen/controls/Tabs.mjs';
|
|
16
16
|
import { TextBox } from './Screen/controls/TextBox.mjs';
|
|
17
17
|
import { Window } from './Screen/Window.mjs';
|
|
18
|
+
import { Pos } from './Screen/Pos.mjs';
|
|
19
|
+
import { Size } from './Screen/Size.mjs';
|
|
18
20
|
import type { ListBoxRowSegments, WindowProperties, StyleId } from './Screen/types.mjs';
|
|
19
21
|
|
|
20
22
|
/** Custom Window subclass registered with InterfaceBuilder via
|
|
@@ -439,6 +441,22 @@ const main = async (): Promise<void> => {
|
|
|
439
441
|
return true;
|
|
440
442
|
});
|
|
441
443
|
|
|
444
|
+
// P2-59 demo: Ctrl+D toggles damage tracking at runtime and posts a
|
|
445
|
+
// sticky toast with the new state so the effect is visible. When
|
|
446
|
+
// tracking is on, the 'frame' event reports only the dirty cells each
|
|
447
|
+
// frame (watch the toast subtitle); when it's off, every frame is a
|
|
448
|
+
// full repaint as before.
|
|
449
|
+
wm.bindKey('ctrl+d', () => {
|
|
450
|
+
const next = !screen.isDamageTrackingEnabled();
|
|
451
|
+
screen.setDamageTracking(next);
|
|
452
|
+
screen.toast(
|
|
453
|
+
next ? ' Damage tracking ON — only dirty cells are emitted '
|
|
454
|
+
: ' Damage tracking OFF — every frame is a full repaint ',
|
|
455
|
+
{ position: 'top-center', duration: 2500 },
|
|
456
|
+
);
|
|
457
|
+
return true;
|
|
458
|
+
});
|
|
459
|
+
|
|
442
460
|
// Virtual cursor: animated caret in TextBox/TextArea. The `tbEmail`
|
|
443
461
|
// TextBox opts into slow blink via layout.yaml; here we flip the
|
|
444
462
|
// `tbUsername` TextBox into the `irregular` mode so the two
|
|
@@ -457,6 +475,39 @@ const main = async (): Promise<void> => {
|
|
|
457
475
|
}
|
|
458
476
|
wm.enableCursorBlink(80);
|
|
459
477
|
|
|
478
|
+
// P2-59 showcase: five independent spinners with different styles,
|
|
479
|
+
// cadences, and positions, each driven by its own `setInterval`.
|
|
480
|
+
// With damage tracking ON, every tick emits only that spinner's 1–2
|
|
481
|
+
// cells (check the 'frame' event `cellsEmitted`); with tracking OFF
|
|
482
|
+
// (Ctrl+D) each tick repaints the whole screen. Placed along the
|
|
483
|
+
// bottom edge so they don't overlap existing widgets.
|
|
484
|
+
const spinnerStyles: Array<'braille' | 'dots' | 'line' | 'circle' | 'arrow'> =
|
|
485
|
+
['braille', 'dots', 'line', 'circle', 'arrow'];
|
|
486
|
+
const spinnerCadences = [120, 180, 240, 320, 420];
|
|
487
|
+
const spinnerTimers: NodeJS.Timeout[] = [];
|
|
488
|
+
const { width: screenW, height: screenH } = screen.getSize();
|
|
489
|
+
for (let i = 0; i < spinnerStyles.length; i++) {
|
|
490
|
+
const sp = new Spinner(
|
|
491
|
+
{ pos: new Pos(screenW - 12 - i * 3, screenH - 2), size: new Size(2, 1) },
|
|
492
|
+
{ style: spinnerStyles[i], color: 75 + i * 10 },
|
|
493
|
+
);
|
|
494
|
+
screen.addChild(sp);
|
|
495
|
+
const timer = setInterval(() => {
|
|
496
|
+
sp.step();
|
|
497
|
+
screen.render();
|
|
498
|
+
}, spinnerCadences[i]!);
|
|
499
|
+
if (typeof timer === 'object' && timer !== null && 'unref' in timer
|
|
500
|
+
&& typeof timer.unref === 'function') timer.unref();
|
|
501
|
+
spinnerTimers.push(timer);
|
|
502
|
+
}
|
|
503
|
+
// Ensure the spinner timers are torn down alongside the main demo
|
|
504
|
+
// timer when the user quits (the existing onExit handler already
|
|
505
|
+
// clears `demoTimer`, so extend it with ours).
|
|
506
|
+
const originalOnExit = () => {
|
|
507
|
+
for (const t of spinnerTimers) clearInterval(t);
|
|
508
|
+
};
|
|
509
|
+
process.once('exit', originalOnExit);
|
|
510
|
+
|
|
460
511
|
wm.run();
|
|
461
512
|
};
|
|
462
513
|
|