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
@@ -0,0 +1,202 @@
1
+ import type { LineChartProperties, WindowProperties, StyleId } from '../types.mjs';
2
+ import { Window } from '../Window.mjs';
3
+ import { getRegistry } from '../RegistryHolder.mjs';
4
+
5
+ /** Formats a Y-axis label value as a compact string. */
6
+ function formatYLabel(value: number): string {
7
+ if (Number.isInteger(value)) return String(value);
8
+ const rounded = Math.round(value * 10) / 10;
9
+ return String(rounded);
10
+ }
11
+
12
+ /** A read-only line chart that renders a data series using box-drawing characters.
13
+ * Includes a labelled Y-axis on the left and an X-axis at the bottom. */
14
+ export class LineChart extends Window {
15
+ private data: number[];
16
+ private minValue: number | undefined;
17
+ private maxValue: number | undefined;
18
+
19
+ private lineStyleId: StyleId;
20
+ private axisStyleId: StyleId;
21
+ private labelStyleId: StyleId;
22
+
23
+ /** Creates a LineChart from window properties and optional control-specific properties.
24
+ * Uses the global StyleRegistry set by the Screen constructor. */
25
+ public constructor(wp: WindowProperties, cp?: LineChartProperties) {
26
+ super(wp);
27
+
28
+ this.data = cp?.data ?? [];
29
+ this.minValue = cp?.min;
30
+ this.maxValue = cp?.max;
31
+
32
+ const reg = getRegistry();
33
+ this.lineStyleId = reg.register({ foreground: cp?.color ?? 75 });
34
+ this.axisStyleId = reg.register({ foreground: 245 });
35
+ this.labelStyleId = reg.register({ foreground: 245 });
36
+ }
37
+
38
+ /** Sets the data series. Call render() afterwards. */
39
+ public setData(data: number[]): void {
40
+ this.data = data;
41
+ }
42
+
43
+ /** Returns the current data series. */
44
+ public getData(): number[] {
45
+ return this.data;
46
+ }
47
+
48
+ /** Sets the minimum Y value. Pass undefined to derive from data. Call render() afterwards. */
49
+ public setMin(min: number | undefined): void {
50
+ this.minValue = min;
51
+ }
52
+
53
+ /** Returns the configured minimum Y value, or undefined if derived from data. */
54
+ public getMin(): number | undefined {
55
+ return this.minValue;
56
+ }
57
+
58
+ /** Sets the maximum Y value. Pass undefined to derive from data. Call render() afterwards. */
59
+ public setMax(max: number | undefined): void {
60
+ this.maxValue = max;
61
+ }
62
+
63
+ /** Returns the configured maximum Y value, or undefined if derived from data. */
64
+ public getMax(): number | undefined {
65
+ return this.maxValue;
66
+ }
67
+
68
+ public override render(): void {
69
+ this.clear();
70
+
71
+ const { width, height } = this.getInnerSize();
72
+ const { x: ox, y: oy } = this.getInnerOffset();
73
+
74
+ if (width < 4 || height < 3) {
75
+ super.render();
76
+ return;
77
+ }
78
+
79
+ // ── 1. Resolve data range ───────────────────────��───────────────────────
80
+ const dataMin = this.minValue ?? (this.data.length > 0 ? Math.min(...this.data) : 0);
81
+ const dataMax = this.maxValue ?? (this.data.length > 0 ? Math.max(...this.data) : 1);
82
+ const dataRange = dataMax - dataMin;
83
+
84
+ // ── 2. Compute Y-axis label width ────────────────────────────────────────
85
+ const numYLabels = Math.min(5, Math.max(2, height - 1));
86
+ const yLabelValues = Array.from({ length: numYLabels }, (_, k) =>
87
+ dataMin + (k / (numYLabels - 1)) * dataRange,
88
+ );
89
+ const yLabelStrs = yLabelValues.map(formatYLabel);
90
+ const maxLabelLen = Math.max(...yLabelStrs.map(s => s.length));
91
+ const yLabelWidth = maxLabelLen + 2; // space + label + '┤'
92
+
93
+ // ── 3. Plot dimensions ───────────────────────��───────────────────────────
94
+ const plotW = width - yLabelWidth;
95
+ const plotH = height - 1; // last row = X-axis
96
+
97
+ if (plotW < 1 || plotH < 1) {
98
+ super.render();
99
+ return;
100
+ }
101
+
102
+ /** Maps a value to a plot row (0 = top = dataMax). */
103
+ const valueToRow = (v: number): number => {
104
+ if (dataRange === 0) return Math.floor((plotH - 1) / 2);
105
+ const norm = Math.min(1, Math.max(0, (v - dataMin) / dataRange));
106
+ return (plotH - 1) - Math.round(norm * (plotH - 1));
107
+ };
108
+
109
+ // ── 4. Draw Y-axis ───────────────────��────────────────────────────────────
110
+ const yAxisAbsX = ox + yLabelWidth - 1; // column of '┤' / '│'
111
+
112
+ // Draw vertical line for all plot rows
113
+ for (let row = 0; row < plotH; row++) {
114
+ this.setCell(yAxisAbsX, oy + row, '│', this.axisStyleId);
115
+ }
116
+
117
+ // Overwrite with '┤' and labels at each label row
118
+ for (let k = 0; k < numYLabels; k++) {
119
+ // k=0 → bottom label (dataMin), k=numYLabels-1 → top label (dataMax)
120
+ const labelValue = yLabelValues[k];
121
+ const labelRow = valueToRow(labelValue);
122
+ const labelStr = yLabelStrs[k];
123
+ // Right-justify within maxLabelLen columns starting at ox
124
+ const padded = labelStr.padStart(maxLabelLen);
125
+ this.setCell(yAxisAbsX, oy + labelRow, '┤', this.axisStyleId);
126
+ for (let c = 0; c < padded.length; c++) {
127
+ this.setCell(ox + c, oy + labelRow, padded[c], this.labelStyleId);
128
+ }
129
+ }
130
+
131
+ // ── 5. Draw X-axis ─────────────────────────���─────────────────────────���────
132
+ const xAxisAbsY = oy + plotH;
133
+ for (let col = 0; col < plotW; col++) {
134
+ const absX = ox + yLabelWidth + col;
135
+ this.setCell(absX, xAxisAbsY, col === 0 ? '┼' : '─', this.axisStyleId);
136
+ }
137
+ // Y-axis bottom corner
138
+ this.setCell(yAxisAbsX, xAxisAbsY, '┼', this.axisStyleId);
139
+
140
+ // ── 6. Plot the line ──────────────────────────────────────────────────────
141
+ if (this.data.length === 0) {
142
+ super.render();
143
+ return;
144
+ }
145
+
146
+ // Build an interpolated row index for every plot column. With more
147
+ // columns than data points, this upsamples the series; with fewer,
148
+ // it averages between adjacent samples.
149
+ const dataRow: number[] = new Array(plotW);
150
+ if (this.data.length === 1) {
151
+ const r0 = valueToRow(this.data[0]);
152
+ for (let c = 0; c < plotW; c++) dataRow[c] = r0;
153
+ } else {
154
+ for (let c = 0; c < plotW; c++) {
155
+ const t = (c / (plotW - 1)) * (this.data.length - 1);
156
+ const i0 = Math.floor(t);
157
+ const i1 = Math.min(i0 + 1, this.data.length - 1);
158
+ const frac = t - i0;
159
+ const v = this.data[i0] * (1 - frac) + this.data[i1] * frac;
160
+ dataRow[c] = valueToRow(v);
161
+ }
162
+ }
163
+
164
+ /** Draws a single line cell in plot space. */
165
+ const plot = (col: number, row: number, ch: string): void => {
166
+ this.setCell(ox + yLabelWidth + col, oy + row, ch, this.lineStyleId);
167
+ };
168
+
169
+ // Each column either holds a flat horizontal segment or a self-contained
170
+ // vertical step bridging the previous column's row to this column's row.
171
+ // The first column has no incoming step.
172
+ plot(0, dataRow[0], '─');
173
+
174
+ for (let c = 1; c < plotW; c++) {
175
+ const inRow = dataRow[c - 1];
176
+ const outRow = dataRow[c];
177
+
178
+ if (inRow === outRow) {
179
+ plot(c, outRow, '─');
180
+ continue;
181
+ }
182
+
183
+ const top = Math.min(inRow, outRow);
184
+ const bot = Math.max(inRow, outRow);
185
+
186
+ if (outRow < inRow) {
187
+ // Going up: line enters from left at the bottom, exits right at the top.
188
+ plot(c, bot, '╯'); // LEFT + TOP
189
+ plot(c, top, '╭'); // BOTTOM + RIGHT
190
+ } else {
191
+ // Going down: line enters from left at the top, exits right at the bottom.
192
+ plot(c, top, '╮'); // LEFT + BOTTOM
193
+ plot(c, bot, '╰'); // TOP + RIGHT
194
+ }
195
+ for (let row = top + 1; row < bot; row++) {
196
+ plot(c, row, '│');
197
+ }
198
+ }
199
+
200
+ super.render();
201
+ }
202
+ }
@@ -0,0 +1,154 @@
1
+ import type { ListBoxProperties, WindowProperties, StyleId } from '../types.mjs';
2
+ import { Window } from '../Window.mjs';
3
+ import { getRegistry } from '../RegistryHolder.mjs';
4
+
5
+ /** A scrollable list of single-line items. The selected item is highlighted and may
6
+ * be moved with arrow keys, PgUp/PgDn, and Home/End. Emits onChange whenever the
7
+ * selection index changes via keyboard. Appearance follows the same built-in style
8
+ * naming scheme as the other focusable controls. */
9
+ export class ListBox extends Window {
10
+ private items: string[];
11
+ private selectedIndex: number;
12
+ private scrollTop: number;
13
+ private onChange?: (index: number, item: string) => void;
14
+ private selectedStyleId: StyleId;
15
+ private focusedSelStyle: StyleId;
16
+
17
+ /** Creates a ListBox from window properties and optional control-specific properties.
18
+ * Uses the global StyleRegistry set by the Screen constructor. */
19
+ public constructor(wp: WindowProperties, cp?: ListBoxProperties) {
20
+ super({
21
+ ...wp,
22
+ defaultBorder: { top: true, right: true, bottom: true, left: true, style: 'single' },
23
+ });
24
+
25
+ this.items = cp?.items ?? [];
26
+ this.onChange = cp?.onChange;
27
+ this.scrollTop = 0;
28
+ this.selectedIndex = cp?.selectedIndex ?? (this.items.length > 0 ? 0 : -1);
29
+
30
+ // Selected row: muted highlight when unfocused, bright inverse when focused.
31
+ const reg = getRegistry();
32
+ this.selectedStyleId = reg.register({ background: 238, foreground: 252 });
33
+ this.focusedSelStyle = reg.register({ background: 75, foreground: 231, bold: true });
34
+ }
35
+
36
+ /** Replaces the list items. Resets selection to 0 (or -1 if empty) and scrolls to top. */
37
+ public setItems(items: string[]): void {
38
+ this.items = items;
39
+ this.selectedIndex = items.length > 0 ? 0 : -1;
40
+ this.scrollTop = 0;
41
+ }
42
+
43
+ /** Returns the current list items. */
44
+ public getItems(): string[] {
45
+ return this.items;
46
+ }
47
+
48
+ /** Sets the selected index. Clamped to valid range; -1 allowed only when items is empty. */
49
+ public setSelectedIndex(index: number): void {
50
+ if (this.items.length === 0) {
51
+ this.selectedIndex = -1;
52
+ return;
53
+ }
54
+ this.selectedIndex = Math.max(0, Math.min(this.items.length - 1, index));
55
+ this.ensureVisible();
56
+ }
57
+
58
+ /** Returns the currently selected index, or -1 when the list is empty. */
59
+ public getSelectedIndex(): number {
60
+ return this.selectedIndex;
61
+ }
62
+
63
+ /** Returns the currently selected item string, or undefined when nothing is selected. */
64
+ public getSelectedItem(): string | undefined {
65
+ if (this.selectedIndex < 0 || this.selectedIndex >= this.items.length) return undefined;
66
+ return this.items[this.selectedIndex];
67
+ }
68
+
69
+ /** Processes a key press: arrow keys, Home/End, PgUp/PgDn move the selection. */
70
+ public handleKey(key: string): void {
71
+ if (this.disabled || this.items.length === 0) return;
72
+
73
+ const prev = this.selectedIndex;
74
+ const pageSz = Math.max(1, this.getInnerSize().height);
75
+
76
+ switch (key) {
77
+ case '\x1b[A': // Up arrow
78
+ this.selectedIndex = Math.max(0, this.selectedIndex - 1);
79
+ break;
80
+ case '\x1b[B': // Down arrow
81
+ this.selectedIndex = Math.min(this.items.length - 1, this.selectedIndex + 1);
82
+ break;
83
+ case '\x1b[H': // Home
84
+ case '\x1b[1~':
85
+ this.selectedIndex = 0;
86
+ break;
87
+ case '\x1b[F': // End
88
+ case '\x1b[4~':
89
+ this.selectedIndex = this.items.length - 1;
90
+ break;
91
+ case '\x1b[5~': // PgUp
92
+ this.selectedIndex = Math.max(0, this.selectedIndex - pageSz);
93
+ break;
94
+ case '\x1b[6~': // PgDn
95
+ this.selectedIndex = Math.min(this.items.length - 1, this.selectedIndex + pageSz);
96
+ break;
97
+ default:
98
+ return;
99
+ }
100
+
101
+ this.ensureVisible();
102
+ if (this.selectedIndex !== prev) {
103
+ this.onChange?.(this.selectedIndex, this.items[this.selectedIndex]);
104
+ }
105
+ }
106
+
107
+ /** Adjusts scrollTop so that the selected index remains within the visible window. */
108
+ private ensureVisible(): void {
109
+ const visibleRows = Math.max(1, this.getInnerSize().height);
110
+ if (this.selectedIndex < this.scrollTop) {
111
+ this.scrollTop = this.selectedIndex;
112
+ } else if (this.selectedIndex >= this.scrollTop + visibleRows) {
113
+ this.scrollTop = this.selectedIndex - visibleRows + 1;
114
+ }
115
+ // Clamp scrollTop so we don't leave trailing empty rows unless necessary.
116
+ const maxScroll = Math.max(0, this.items.length - visibleRows);
117
+ this.scrollTop = Math.max(0, Math.min(maxScroll, this.scrollTop));
118
+ }
119
+
120
+ /** Rebuilds the list: renders visible rows with styles. */
121
+ public override render(): void {
122
+ this.clear();
123
+
124
+ const { width, height } = this.getInnerSize();
125
+ if (width < 1 || height < 1) {
126
+ super.render();
127
+ return;
128
+ }
129
+
130
+ this.ensureVisible();
131
+
132
+ const visibleCount = Math.min(height, this.items.length - this.scrollTop);
133
+ for (let row = 0; row < visibleCount; row++) {
134
+ const itemIndex = this.scrollTop + row;
135
+ const rawText = this.items[itemIndex];
136
+ // Truncate to fit width; pad with spaces so the highlight covers the whole row.
137
+ const text = rawText.length > width ? rawText.slice(0, width) : rawText;
138
+ const padded = text + ' '.repeat(width - text.length);
139
+
140
+ let style: StyleId;
141
+ if (this.disabled) {
142
+ style = this.disabledStyleId;
143
+ } else if (itemIndex === this.selectedIndex) {
144
+ style = this.focused ? this.focusedSelStyle : this.selectedStyleId;
145
+ } else {
146
+ style = this.normalStyleId;
147
+ }
148
+
149
+ this.writeText(padded, { x: 0, y: row, style });
150
+ }
151
+
152
+ super.render();
153
+ }
154
+ }
@@ -0,0 +1,88 @@
1
+ import type { ProgressBarProperties, WindowProperties, StyleId } from '../types.mjs';
2
+ import { Window } from '../Window.mjs';
3
+ import { getRegistry } from '../RegistryHolder.mjs';
4
+
5
+ /** Character used for the filled portion of the bar. */
6
+ const CHAR_FILL = '█';
7
+ /** Character used for the empty portion of the bar. */
8
+ const CHAR_EMPTY = '░';
9
+
10
+ /** A horizontal read-only progress bar that fills left-to-right using block characters.
11
+ * Displays an optional centred percentage label over the bar. */
12
+ export class ProgressBar extends Window {
13
+ private value: number;
14
+ private max: number;
15
+ private showLabel: boolean;
16
+
17
+ private fillStyleId: StyleId;
18
+ private emptyStyleId: StyleId;
19
+ private labelStyleId: StyleId;
20
+
21
+ /** Creates a ProgressBar from window properties and optional control-specific properties.
22
+ * Recommended height: 1 (no border) or 3 (with border).
23
+ * Uses the global StyleRegistry set by the Screen constructor. */
24
+ public constructor(wp: WindowProperties, cp?: ProgressBarProperties) {
25
+ super(wp);
26
+
27
+ this.max = Math.max(1, cp?.max ?? 100);
28
+ this.value = Math.max(0, Math.min(cp?.value ?? 0, this.max));
29
+ this.showLabel = cp?.showLabel ?? true;
30
+
31
+ const reg = getRegistry();
32
+ this.fillStyleId = reg.register({ background: cp?.fillColor ?? 75 });
33
+ this.emptyStyleId = reg.register({ background: cp?.emptyColor ?? 237 });
34
+ this.labelStyleId = reg.register({ foreground: 255, bold: true });
35
+ }
36
+
37
+ /** Sets the current value (clamped to 0–max). Call render() afterwards. */
38
+ public setValue(value: number): void {
39
+ this.value = Math.max(0, Math.min(value, this.max));
40
+ }
41
+
42
+ /** Returns the current value. */
43
+ public getValue(): number {
44
+ return this.value;
45
+ }
46
+
47
+ /** Sets the maximum value (minimum 1). Call render() afterwards. */
48
+ public setMax(max: number): void {
49
+ this.max = Math.max(1, max);
50
+ this.value = Math.min(this.value, this.max);
51
+ }
52
+
53
+ /** Returns the maximum value. */
54
+ public getMax(): number {
55
+ return this.max;
56
+ }
57
+
58
+ public override render(): void {
59
+ this.clear();
60
+
61
+ const { width, height } = this.getInnerSize();
62
+ const { x: ox, y: oy } = this.getInnerOffset();
63
+
64
+ if (width < 1 || height < 1) {
65
+ super.render();
66
+ return;
67
+ }
68
+
69
+ const ratio = this.max > 0 ? Math.min(1, Math.max(0, this.value / this.max)) : 0;
70
+ const filledCols = Math.round(ratio * width);
71
+ const barRow = Math.floor(height / 2);
72
+
73
+ for (let x = 0; x < width; x++) {
74
+ const isFilled = x < filledCols;
75
+ this.setCell(ox + x, oy + barRow, isFilled ? CHAR_FILL : CHAR_EMPTY,
76
+ isFilled ? this.fillStyleId : this.emptyStyleId);
77
+ }
78
+
79
+ if (this.showLabel) {
80
+ const pct = Math.round(ratio * 100);
81
+ const text = `${pct}%`;
82
+ const labelX = Math.max(0, Math.floor((width - text.length) / 2));
83
+ this.writeText(text, { x: labelX, y: barRow, style: this.labelStyleId });
84
+ }
85
+
86
+ super.render();
87
+ }
88
+ }
@@ -0,0 +1,77 @@
1
+ import type { ProgressBarVProperties, WindowProperties, StyleId } from '../types.mjs';
2
+ import { Window } from '../Window.mjs';
3
+ import { getRegistry } from '../RegistryHolder.mjs';
4
+
5
+ /** Character used for the filled portion of the bar. */
6
+ const CHAR_FILL = '█';
7
+ /** Character used for the empty portion of the bar. */
8
+ const CHAR_EMPTY = '░';
9
+
10
+ /** A vertical read-only progress bar that fills from the bottom upward using block characters. */
11
+ export class ProgressBarV extends Window {
12
+ private value: number;
13
+ private max: number;
14
+
15
+ private fillStyleId: StyleId;
16
+ private emptyStyleId: StyleId;
17
+
18
+ /** Creates a ProgressBarV from window properties and optional control-specific properties.
19
+ * Uses the global StyleRegistry set by the Screen constructor. */
20
+ public constructor(wp: WindowProperties, cp?: ProgressBarVProperties) {
21
+ super(wp);
22
+
23
+ this.max = Math.max(1, cp?.max ?? 100);
24
+ this.value = Math.max(0, Math.min(cp?.value ?? 0, this.max));
25
+
26
+ const reg = getRegistry();
27
+ this.fillStyleId = reg.register({ background: cp?.fillColor ?? 75 });
28
+ this.emptyStyleId = reg.register({ background: cp?.emptyColor ?? 237 });
29
+ }
30
+
31
+ /** Sets the current value (clamped to 0–max). Call render() afterwards. */
32
+ public setValue(value: number): void {
33
+ this.value = Math.max(0, Math.min(value, this.max));
34
+ }
35
+
36
+ /** Returns the current value. */
37
+ public getValue(): number {
38
+ return this.value;
39
+ }
40
+
41
+ /** Sets the maximum value (minimum 1). Call render() afterwards. */
42
+ public setMax(max: number): void {
43
+ this.max = Math.max(1, max);
44
+ this.value = Math.min(this.value, this.max);
45
+ }
46
+
47
+ /** Returns the maximum value. */
48
+ public getMax(): number {
49
+ return this.max;
50
+ }
51
+
52
+ public override render(): void {
53
+ this.clear();
54
+
55
+ const { width, height } = this.getInnerSize();
56
+ const { x: ox, y: oy } = this.getInnerOffset();
57
+
58
+ if (width < 1 || height < 1) {
59
+ super.render();
60
+ return;
61
+ }
62
+
63
+ const ratio = this.max > 0 ? Math.min(1, Math.max(0, this.value / this.max)) : 0;
64
+ const filledRows = Math.round(ratio * height);
65
+
66
+ for (let row = 0; row < height; row++) {
67
+ const isFilled = row >= (height - filledRows);
68
+ const char = isFilled ? CHAR_FILL : CHAR_EMPTY;
69
+ const styleId = isFilled ? this.fillStyleId : this.emptyStyleId;
70
+ for (let col = 0; col < width; col++) {
71
+ this.setCell(ox + col, oy + row, char, styleId);
72
+ }
73
+ }
74
+
75
+ super.render();
76
+ }
77
+ }
@@ -0,0 +1,66 @@
1
+ import type { RadioProperties, WindowProperties, StyleId } from '../types.mjs';
2
+ import { BUILTIN_TEXT_CHECKED } from '../types.mjs';
3
+ import { Window } from '../Window.mjs';
4
+ import { Size } from '../Size.mjs';
5
+ import { getRegistry } from '../RegistryHolder.mjs';
6
+
7
+ /** Width of the indicator prefix: `(●) ` = 4 columns. */
8
+ const INDICATOR_WIDTH = 4;
9
+
10
+ /** A single-selection radio control rendering `(●) label` (selected) or `( ) label` (unselected).
11
+ * Width is derived automatically from the label length when size is not provided.
12
+ * Group management (ensuring at most one is selected) is the caller's responsibility. */
13
+ export class Radio extends Window {
14
+ private checked: boolean;
15
+ private onChange?: (checked: boolean) => void;
16
+ private checkedStyleId: StyleId;
17
+
18
+ /** Creates a Radio button from window properties and optional control-specific properties.
19
+ * When wp.size is omitted, width is computed from wp.label automatically.
20
+ * Uses the global StyleRegistry set by the Screen constructor. */
21
+ public constructor(wp: WindowProperties, cp?: RadioProperties) {
22
+ const label = wp.label ?? '';
23
+ const size = wp.size ?? new Size(INDICATOR_WIDTH + label.length, 1);
24
+ super({ ...wp, size });
25
+
26
+ this.checked = cp?.checked ?? false;
27
+ this.onChange = cp?.onChange;
28
+
29
+ this.checkedStyleId = getRegistry().getNamed(BUILTIN_TEXT_CHECKED)!;
30
+ }
31
+
32
+ /** Sets the selected state. */
33
+ public setChecked(checked: boolean): void {
34
+ this.checked = checked;
35
+ }
36
+
37
+ /** Returns the current selected state. */
38
+ public isChecked(): boolean {
39
+ return this.checked;
40
+ }
41
+
42
+ /** Processes a key press; Space selects this radio button and fires onChange. */
43
+ public handleKey(key: string): void {
44
+ if (this.disabled) return;
45
+ if (key === ' ' || key === 'space') {
46
+ this.checked = true;
47
+ this.onChange?.(true);
48
+ }
49
+ }
50
+
51
+ /** Rebuilds the radio button: draws indicator and label with appropriate styles. */
52
+ public override render(): void {
53
+ this.clear();
54
+
55
+ const indicator = this.checked ? '(●)' : '( )';
56
+ // For indicator: use checkedStyleId when checked (and not disabled); auto-pick otherwise.
57
+ const indicatorStyle = (!this.disabled && this.checked) ? this.checkedStyleId : undefined;
58
+
59
+ // Write indicator (first 3 chars) and label separately to allow distinct colouring.
60
+ // Label uses auto-style (disabled/focused/normal via writeText).
61
+ this.writeText(indicator, { style: indicatorStyle });
62
+ this.writeText(` ${this.label}`, { x: INDICATOR_WIDTH - 1 });
63
+
64
+ super.render();
65
+ }
66
+ }
@@ -0,0 +1,101 @@
1
+ import type { SparklineProperties, WindowProperties, StyleId } from '../types.mjs';
2
+ import { Window } from '../Window.mjs';
3
+ import { getRegistry } from '../RegistryHolder.mjs';
4
+
5
+ /** Eight-level block character ramp from empty → full. */
6
+ const RAMP = [' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'] as const;
7
+
8
+ /** A one-row inline sparkline chart using block-character glyphs. Ideal for
9
+ * tucking a live trend indicator into status bars, table rows, or tight
10
+ * spaces where a full LineChart would be overkill. The control can be any
11
+ * height, but only the last row is used for the glyphs. */
12
+ export class Sparkline extends Window {
13
+ private data: number[];
14
+ private minValue: number | undefined;
15
+ private maxValue: number | undefined;
16
+
17
+ private glyphStyleId: StyleId;
18
+
19
+ /** Creates a Sparkline from window properties and optional control-specific properties.
20
+ * Uses the global StyleRegistry set by the Screen constructor. */
21
+ public constructor(wp: WindowProperties, cp?: SparklineProperties) {
22
+ super(wp);
23
+
24
+ this.data = cp?.data ?? [];
25
+ this.minValue = cp?.min;
26
+ this.maxValue = cp?.max;
27
+
28
+ this.glyphStyleId = getRegistry().register({ foreground: cp?.color ?? 75 });
29
+ }
30
+
31
+ /** Sets the data series. Call render() afterwards. */
32
+ public setData(data: number[]): void {
33
+ this.data = data;
34
+ }
35
+
36
+ /** Returns the current data series. */
37
+ public getData(): number[] {
38
+ return this.data;
39
+ }
40
+
41
+ /** Sets the minimum value. Pass undefined to derive from data. Call render() afterwards. */
42
+ public setMin(min: number | undefined): void {
43
+ this.minValue = min;
44
+ }
45
+
46
+ /** Returns the configured minimum value, or undefined if derived from data. */
47
+ public getMin(): number | undefined {
48
+ return this.minValue;
49
+ }
50
+
51
+ /** Sets the maximum value. Pass undefined to derive from data. Call render() afterwards. */
52
+ public setMax(max: number | undefined): void {
53
+ this.maxValue = max;
54
+ }
55
+
56
+ /** Returns the configured maximum value, or undefined if derived from data. */
57
+ public getMax(): number | undefined {
58
+ return this.maxValue;
59
+ }
60
+
61
+ public override render(): void {
62
+ this.clear();
63
+
64
+ const { width, height } = this.getInnerSize();
65
+ const { x: ox, y: oy } = this.getInnerOffset();
66
+
67
+ if (width < 1 || height < 1 || this.data.length === 0) {
68
+ super.render();
69
+ return;
70
+ }
71
+
72
+ const dataMin = this.minValue ?? Math.min(...this.data);
73
+ const dataMax = this.maxValue ?? Math.max(...this.data);
74
+ const range = dataMax - dataMin;
75
+
76
+ // Glyphs live on the bottom-most row of the inner area; any rows above
77
+ // remain blank (useful when the caller wants the sparkline aligned to
78
+ // the baseline of a taller container).
79
+ const row = oy + height - 1;
80
+
81
+ for (let c = 0; c < width; c++) {
82
+ // Map column → fractional data index so that width != data.length still works.
83
+ let value: number;
84
+ if (this.data.length === 1) {
85
+ value = this.data[0];
86
+ } else {
87
+ const t = (c / Math.max(1, width - 1)) * (this.data.length - 1);
88
+ const i0 = Math.floor(t);
89
+ const i1 = Math.min(i0 + 1, this.data.length - 1);
90
+ const frac = t - i0;
91
+ value = this.data[i0] * (1 - frac) + this.data[i1] * frac;
92
+ }
93
+
94
+ const norm = range > 0 ? Math.min(1, Math.max(0, (value - dataMin) / range)) : 0.5;
95
+ const level = Math.round(norm * (RAMP.length - 1));
96
+ this.setCell(ox + c, row, RAMP[level], this.glyphStyleId);
97
+ }
98
+
99
+ super.render();
100
+ }
101
+ }