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.
Files changed (76) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +3 -2
  3. package/dist/Screen/Screen.d.mts +40 -0
  4. package/dist/Screen/Screen.d.mts.map +1 -1
  5. package/dist/Screen/Screen.mjs +148 -1
  6. package/dist/Screen/Screen.mjs.map +1 -1
  7. package/dist/Screen/Window.d.mts +50 -1
  8. package/dist/Screen/Window.d.mts.map +1 -1
  9. package/dist/Screen/Window.mjs +176 -19
  10. package/dist/Screen/Window.mjs.map +1 -1
  11. package/dist/Screen/WindowManager.d.mts.map +1 -1
  12. package/dist/Screen/WindowManager.mjs +11 -0
  13. package/dist/Screen/WindowManager.mjs.map +1 -1
  14. package/dist/Screen/controls/BarChart.d.mts.map +1 -1
  15. package/dist/Screen/controls/BarChart.mjs +3 -0
  16. package/dist/Screen/controls/BarChart.mjs.map +1 -1
  17. package/dist/Screen/controls/Checkbox.d.mts.map +1 -1
  18. package/dist/Screen/controls/Checkbox.mjs +4 -0
  19. package/dist/Screen/controls/Checkbox.mjs.map +1 -1
  20. package/dist/Screen/controls/LineChart.d.mts.map +1 -1
  21. package/dist/Screen/controls/LineChart.mjs +3 -0
  22. package/dist/Screen/controls/LineChart.mjs.map +1 -1
  23. package/dist/Screen/controls/ListBox.d.mts.map +1 -1
  24. package/dist/Screen/controls/ListBox.mjs +9 -0
  25. package/dist/Screen/controls/ListBox.mjs.map +1 -1
  26. package/dist/Screen/controls/ProgressBar.d.mts.map +1 -1
  27. package/dist/Screen/controls/ProgressBar.mjs +2 -0
  28. package/dist/Screen/controls/ProgressBar.mjs.map +1 -1
  29. package/dist/Screen/controls/ProgressBarV.d.mts.map +1 -1
  30. package/dist/Screen/controls/ProgressBarV.mjs +2 -0
  31. package/dist/Screen/controls/ProgressBarV.mjs.map +1 -1
  32. package/dist/Screen/controls/Radio.d.mts.map +1 -1
  33. package/dist/Screen/controls/Radio.mjs +7 -1
  34. package/dist/Screen/controls/Radio.mjs.map +1 -1
  35. package/dist/Screen/controls/Sparkline.d.mts.map +1 -1
  36. package/dist/Screen/controls/Sparkline.mjs +3 -0
  37. package/dist/Screen/controls/Sparkline.mjs.map +1 -1
  38. package/dist/Screen/controls/Spinner.d.mts.map +1 -1
  39. package/dist/Screen/controls/Spinner.mjs +8 -0
  40. package/dist/Screen/controls/Spinner.mjs.map +1 -1
  41. package/dist/Screen/controls/StatusLED.d.mts.map +1 -1
  42. package/dist/Screen/controls/StatusLED.mjs +3 -0
  43. package/dist/Screen/controls/StatusLED.mjs.map +1 -1
  44. package/dist/Screen/controls/Tabs.d.mts.map +1 -1
  45. package/dist/Screen/controls/Tabs.mjs +2 -0
  46. package/dist/Screen/controls/Tabs.mjs.map +1 -1
  47. package/dist/Screen/controls/TextArea.d.mts.map +1 -1
  48. package/dist/Screen/controls/TextArea.mjs +11 -0
  49. package/dist/Screen/controls/TextArea.mjs.map +1 -1
  50. package/dist/Screen/controls/TextBox.d.mts.map +1 -1
  51. package/dist/Screen/controls/TextBox.mjs +13 -0
  52. package/dist/Screen/controls/TextBox.mjs.map +1 -1
  53. package/dist/Screen/types.d.mts +26 -0
  54. package/dist/Screen/types.d.mts.map +1 -1
  55. package/dist/index.d.mts +1 -1
  56. package/dist/index.d.mts.map +1 -1
  57. package/package.json +1 -1
  58. package/src/Screen/Screen.mts +148 -2
  59. package/src/Screen/Window.mts +165 -16
  60. package/src/Screen/WindowManager.mts +11 -0
  61. package/src/Screen/controls/BarChart.mts +3 -0
  62. package/src/Screen/controls/Checkbox.mts +3 -0
  63. package/src/Screen/controls/LineChart.mts +3 -0
  64. package/src/Screen/controls/ListBox.mts +8 -0
  65. package/src/Screen/controls/ProgressBar.mts +2 -0
  66. package/src/Screen/controls/ProgressBarV.mts +2 -0
  67. package/src/Screen/controls/Radio.mts +6 -1
  68. package/src/Screen/controls/Sparkline.mts +3 -0
  69. package/src/Screen/controls/Spinner.mts +6 -0
  70. package/src/Screen/controls/StatusLED.mts +2 -0
  71. package/src/Screen/controls/Tabs.mts +2 -0
  72. package/src/Screen/controls/TextArea.mts +10 -0
  73. package/src/Screen/controls/TextBox.mts +12 -0
  74. package/src/Screen/types.mts +27 -0
  75. package/src/demo.mts +51 -0
  76. 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
 
@@ -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
 
package/src/index.mts CHANGED
@@ -74,6 +74,7 @@ export type {
74
74
  TerminalSize,
75
75
  ScreenOptions,
76
76
  ScreenFrameStats,
77
+ DirtyRect,
77
78
  AxisSpec,
78
79
  DimSpec,
79
80
  FlexBasis,