@profullstack/hqtui-demo 0.1.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 (54) hide show
  1. package/LICENSE +21 -0
  2. package/bin/hqtui-demo.mjs +3 -0
  3. package/dist/format.js +53 -0
  4. package/dist/format.js.map +1 -0
  5. package/dist/main.js +361 -0
  6. package/dist/main.js.map +1 -0
  7. package/dist/screens/components.js +144 -0
  8. package/dist/screens/components.js.map +1 -0
  9. package/dist/screens/dashboard.js +307 -0
  10. package/dist/screens/dashboard.js.map +1 -0
  11. package/dist/screens/graphics.js +56 -0
  12. package/dist/screens/graphics.js.map +1 -0
  13. package/dist/screens/index.js +7 -0
  14. package/dist/screens/index.js.map +1 -0
  15. package/dist/screens/input.js +37 -0
  16. package/dist/screens/input.js.map +1 -0
  17. package/dist/screens/stress.js +34 -0
  18. package/dist/screens/stress.js.map +1 -0
  19. package/dist/screens/themes.js +40 -0
  20. package/dist/screens/themes.js.map +1 -0
  21. package/dist/simulation.js +254 -0
  22. package/dist/simulation.js.map +1 -0
  23. package/dist/state.js +35 -0
  24. package/dist/state.js.map +1 -0
  25. package/dist/system/common.js +97 -0
  26. package/dist/system/common.js.map +1 -0
  27. package/dist/system/darwin.js +153 -0
  28. package/dist/system/darwin.js.map +1 -0
  29. package/dist/system/index.js +45 -0
  30. package/dist/system/index.js.map +1 -0
  31. package/dist/system/linux.js +260 -0
  32. package/dist/system/linux.js.map +1 -0
  33. package/dist/system/types.js +2 -0
  34. package/dist/system/types.js.map +1 -0
  35. package/dist/system/win32.js +118 -0
  36. package/dist/system/win32.js.map +1 -0
  37. package/package.json +39 -0
  38. package/src/format.ts +52 -0
  39. package/src/main.ts +320 -0
  40. package/src/screens/components.ts +153 -0
  41. package/src/screens/dashboard.ts +328 -0
  42. package/src/screens/graphics.ts +64 -0
  43. package/src/screens/index.ts +6 -0
  44. package/src/screens/input.ts +39 -0
  45. package/src/screens/stress.ts +36 -0
  46. package/src/screens/themes.ts +41 -0
  47. package/src/simulation.ts +356 -0
  48. package/src/state.ts +74 -0
  49. package/src/system/common.ts +101 -0
  50. package/src/system/darwin.ts +160 -0
  51. package/src/system/index.ts +56 -0
  52. package/src/system/linux.ts +258 -0
  53. package/src/system/types.ts +12 -0
  54. package/src/system/win32.ts +132 -0
@@ -0,0 +1,328 @@
1
+ import type { Container, Theme } from "@profullstack/hqtui";
2
+ import { heatColor } from "@profullstack/hqtui";
3
+ import type { DemoState } from "../state.ts";
4
+ import { bitRate, byteRate, bytes, clock, duration, num, percent } from "../format.ts";
5
+
6
+ const TIME_LABELS = ["60s", "45s", "30s", "15s", "0s"];
7
+
8
+ function cpuPanel(ui: Container, state: DemoState, theme: Theme, coreColumns: number): void {
9
+ const cpu = state.sample.cpu;
10
+ ui.panel({ title: "CPU Overview", subtitle: percent(cpu.total) }, (p) => {
11
+ p.label(`${cpu.model} ${num(cpu.frequencyGhz, 1)} GHz`, { size: 1 });
12
+ p.graph({ values: cpu.history, min: 0, max: 100, fill: true, color: theme.success, size: "1fr" });
13
+ p.meters(
14
+ cpu.cores.map((value, i) => ({ label: `P${i}`, value })),
15
+ { columns: coreColumns, labelWidth: 4, valueWidth: 5, style: "segmented" },
16
+ );
17
+ p.divider();
18
+ p.keyValues(
19
+ [{
20
+ label: "Load Avg",
21
+ value: `${num(cpu.load[0], 2)} ${num(cpu.load[1], 2)} ${num(cpu.load[2], 2)}`,
22
+ color: theme.warning,
23
+ }],
24
+ { size: 1 },
25
+ );
26
+ });
27
+ }
28
+
29
+ function memoryPanel(ui: Container, state: DemoState, theme: Theme): void {
30
+ const memory = state.sample.memory;
31
+ const used = memory.used / Math.max(1, memory.total);
32
+ const swap = memory.swapTotal > 0 ? memory.swapUsed / memory.swapTotal : 0;
33
+ ui.panel({ title: "Memory & Swap" }, (p) => {
34
+ p.text(`Memory${" ".repeat(6)}${bytes(memory.used)} / ${bytes(memory.total)} (${percent(used)})`, {
35
+ fg: theme.foreground,
36
+ size: 1,
37
+ });
38
+ p.meter({ value: used, showValue: false, style: "segmented", size: 1 });
39
+ p.spacer(1);
40
+ p.keyValues([
41
+ { label: "Used:", value: bytes(memory.used), color: theme.warning },
42
+ { label: "Available:", value: bytes(memory.available), color: theme.success },
43
+ { label: "Cached:", value: bytes(memory.cached), color: theme.accent },
44
+ { label: "Buffers:", value: bytes(memory.buffers), color: theme.secondary },
45
+ { label: "Free:", value: bytes(memory.free), color: theme.muted },
46
+ ]);
47
+ p.spacer("fill");
48
+ p.divider();
49
+ p.text(`Swap${" ".repeat(8)}${bytes(memory.swapUsed)} / ${bytes(memory.swapTotal)} (${percent(swap)})`, {
50
+ fg: theme.foreground,
51
+ size: 1,
52
+ });
53
+ p.meter({ value: swap, showValue: false, color: theme.secondary, heat: false, style: "segmented", size: 1 });
54
+ p.keyValues([
55
+ { label: "Used:", value: bytes(memory.swapUsed), color: theme.secondary },
56
+ { label: "Free:", value: bytes(Math.max(0, memory.swapTotal - memory.swapUsed)), color: theme.muted },
57
+ ]);
58
+ });
59
+ }
60
+
61
+ function disksPanel(ui: Container, state: DemoState, theme: Theme): void {
62
+ const disks = state.sample.disks;
63
+ ui.panel({ title: "Disks" }, (p) => {
64
+ if (disks.length === 0) {
65
+ p.label("No disks reported");
66
+ return;
67
+ }
68
+ disks.slice(0, 2).forEach((disk, i) => {
69
+ const used = disk.total > 0 ? disk.used / disk.total : 0;
70
+ p.text(`${disk.device} — ${bytes(disk.total)} (${disk.type})`, { fg: theme.foreground, size: 1 });
71
+ p.text(`Used: ${bytes(disk.used)} (${percent(used)})`, { fg: theme.muted, size: 1 });
72
+ p.meter({ value: used, showValue: false, style: "segmented", size: 1 });
73
+ p.text(`Free: ${bytes(Math.max(0, disk.total - disk.used))}`, { fg: theme.muted, size: 1 });
74
+ p.row({ size: 1 }, (r) => {
75
+ r.text(`Read: ${byteRate(disk.readRate)}`, { fg: theme.success });
76
+ r.text(`Write: ${byteRate(disk.writeRate)}`, { fg: theme.secondary, align: "right" });
77
+ });
78
+ p.multiGraph(
79
+ [
80
+ { values: disk.readHistory, color: theme.success, fill: true },
81
+ { values: disk.writeHistory, color: theme.secondary, fill: true },
82
+ ],
83
+ { size: "1fr", min: 0 },
84
+ );
85
+ if (i === 0 && disks.length > 1) p.divider();
86
+ });
87
+ });
88
+ }
89
+
90
+ function systemPanel(ui: Container, state: DemoState, theme: Theme): void {
91
+ const s = state.sample;
92
+ ui.panel({ title: "System" }, (p) => {
93
+ p.row({ size: 6, gap: 2, min: 6 }, (r) => {
94
+ r.keyValues([
95
+ { label: "OS:", value: s.system.os },
96
+ { label: "Kernel:", value: s.system.kernel },
97
+ { label: "Uptime:", value: duration(s.system.uptime) },
98
+ { label: "Hostname:", value: s.system.hostname },
99
+ { label: "Shell:", value: s.system.shell },
100
+ { label: "Source:", value: state.source, color: theme.accent },
101
+ ], { spread: false });
102
+ r.keyValues([
103
+ { label: "CPU:", value: percent(s.cpu.total), color: heatColor(theme, s.cpu.total) },
104
+ { label: "Memory:", value: `${percent(s.memory.used / Math.max(1, s.memory.total))} (${bytes(s.memory.used)})`, color: theme.warning },
105
+ { label: "Swap:", value: s.memory.swapTotal ? percent(s.memory.swapUsed / s.memory.swapTotal) : "—", color: theme.secondary },
106
+ { label: "Load:", value: `${num(s.cpu.load[0], 2)} ${num(s.cpu.load[1], 2)} ${num(s.cpu.load[2], 2)}` },
107
+ { label: "Processes:", value: String(s.system.processCount || s.processes.length) },
108
+ { label: "Threads:", value: String(s.system.threadCount) },
109
+ ], { spread: false });
110
+ });
111
+ p.panel({ title: "CPU History", size: "1fr", min: 5 }, (g) => {
112
+ g.graph({ values: s.cpu.history, min: 0, max: 100, axis: true, fill: true, color: theme.success });
113
+ });
114
+ // Sub-panels need both width and height to be legible; otherwise they are
115
+ // replaced by a single dense stat line.
116
+ if (p.width >= 46 && p.height >= 16) {
117
+ p.row({ size: 6, gap: 1 }, (r) => {
118
+ r.panel({ title: "Quick Stats" }, (q) => {
119
+ q.keyValues([
120
+ { label: "Uptime", value: duration(s.system.uptime), color: theme.accent },
121
+ { label: "Procs", value: String(s.system.processCount || s.processes.length), color: theme.accent },
122
+ { label: "Threads", value: String(s.system.threadCount), color: theme.accent },
123
+ { label: "Ctx/s", value: `${(s.system.contextSwitches / 1000).toFixed(1)}K`, color: theme.accent },
124
+ ]);
125
+ });
126
+ r.panel({ title: "Memory" }, (q) => {
127
+ q.text(percent(s.memory.used / Math.max(1, s.memory.total)), { fg: theme.warning, size: 1 });
128
+ q.graph({ values: s.memory.history, min: 0, max: 100, fill: true, color: theme.primary });
129
+ });
130
+ r.panel({ title: "Temp", width: 14 }, (q) => {
131
+ const temp = s.temperatures[0];
132
+ q.gauge({
133
+ value: temp ? Math.min(1, temp.value / (temp.max || 100)) : s.cpu.total,
134
+ label: temp ? `${Math.round(temp.value)}°C` : percent(s.cpu.total),
135
+ });
136
+ });
137
+ });
138
+ } else {
139
+ p.divider({ size: 1 });
140
+ p.keyValues([
141
+ { label: "Threads", value: String(s.system.threadCount), color: theme.accent },
142
+ { label: "Ctx switches", value: `${(s.system.contextSwitches / 1000).toFixed(1)}K`, color: theme.accent },
143
+ ], { size: 2 });
144
+ }
145
+ });
146
+ }
147
+
148
+ function processesPanel(ui: Container, state: DemoState, theme: Theme): void {
149
+ const rows = visibleProcesses(state);
150
+ ui.panel({
151
+ title: `Processes (sorted by ${state.sort.toUpperCase()})`,
152
+ subtitle: state.filter ? `filter: ${state.filter}` : undefined,
153
+ focusable: true,
154
+ }, (p) => {
155
+ p.table({
156
+ rows,
157
+ selected: state.selected,
158
+ offset: state.offset,
159
+ scrollbar: true,
160
+ columns: [
161
+ { key: "pid", title: "PID", width: 7, align: "right" },
162
+ { key: "name", title: "Name", min: 8, color: theme.primary },
163
+ { key: "cpu", title: "CPU%", width: 6, align: "right", render: (r) => num(r.cpu, 1), color: (r) => heatColor(theme, Math.min(1, r.cpu / 100)) },
164
+ { key: "mem", title: "MEM%", width: 6, align: "right", render: (r) => num(r.mem, 1), color: theme.warning },
165
+ { key: "rss", title: "RSS", width: 9, align: "right", render: (r) => bytes(r.rss, 0) },
166
+ { key: "threads", title: "Threads", width: 7, align: "right" },
167
+ { key: "state", title: "S", width: 2, color: (r) => (r.state === "R" ? theme.success : theme.muted) },
168
+ { key: "user", title: "User", width: 10, color: theme.muted },
169
+ { key: "command", title: "Command", min: 10, color: theme.muted },
170
+ ],
171
+ });
172
+ });
173
+ }
174
+
175
+ function networkPanel(ui: Container, state: DemoState, theme: Theme): void {
176
+ const net = state.sample.network;
177
+ ui.panel({ title: "Network" }, (p) => {
178
+ p.row({ size: 1 }, (r) => {
179
+ r.text(`Download: ${bitRate(net.downRate)}`, { fg: theme.primary });
180
+ r.text(`Upload: ${bitRate(net.upRate)}`, { fg: theme.secondary, align: "right" });
181
+ });
182
+ p.graph({ values: net.downHistory, fill: true, color: theme.primary, min: 0, size: "1fr", axis: true, axisFormat: (v) => bitRate(v).replace(" ", "") });
183
+ p.graph({ values: net.upHistory, fill: true, color: theme.secondary, min: 0, size: "1fr", axis: true, axisFormat: (v) => bitRate(v).replace(" ", "") });
184
+ p.divider();
185
+ p.row({ size: 3, gap: 2 }, (r) => {
186
+ r.keyValues([
187
+ { label: "Total:", value: bytes(net.downTotal), color: theme.primary },
188
+ { label: "Current:", value: bitRate(net.downRate), color: theme.primary },
189
+ { label: "Peak:", value: bitRate(net.downPeak), color: theme.primary },
190
+ ], { spread: false });
191
+ r.keyValues([
192
+ { label: "Total:", value: bytes(net.upTotal), color: theme.secondary },
193
+ { label: "Current:", value: bitRate(net.upRate), color: theme.secondary },
194
+ { label: "Peak:", value: bitRate(net.upPeak), color: theme.secondary },
195
+ ], { spread: false });
196
+ });
197
+ });
198
+ }
199
+
200
+ function diskUsagePanel(ui: Container, state: DemoState, theme: Theme): void {
201
+ const disks = state.sample.disks;
202
+ ui.panel({ title: "Disk Usage", subtitle: disks[0]?.device }, (p) => {
203
+ disks.forEach((disk) => {
204
+ const used = disk.total > 0 ? disk.used / disk.total : 0;
205
+ p.meter({
206
+ value: used,
207
+ text: `${percent(used)} ${bytes(disk.used, 0)} / ${bytes(disk.total, 0)}`,
208
+ style: "segmented",
209
+ size: 1,
210
+ });
211
+ p.label(`${disk.mount} (${disk.device})`, { size: 1 });
212
+ });
213
+ p.spacer(1);
214
+ p.panel({ title: "I/O Summary", size: "1fr" }, (io) => {
215
+ io.row({ gap: 2 }, (r) => {
216
+ r.column({}, (c) => {
217
+ c.text(`Read: ${byteRate(disks[0]?.readRate ?? 0)}`, { fg: theme.success, size: 1 });
218
+ c.graph({ values: disks[0]?.readHistory ?? [], color: theme.success, fill: true, min: 0 });
219
+ });
220
+ r.column({}, (c) => {
221
+ c.text(`Write: ${byteRate(disks[0]?.writeRate ?? 0)}`, { fg: theme.secondary, size: 1 });
222
+ c.graph({ values: disks[0]?.writeHistory ?? [], color: theme.secondary, fill: true, min: 0 });
223
+ });
224
+ });
225
+ });
226
+ });
227
+ }
228
+
229
+ function temperaturesPanel(ui: Container, state: DemoState, theme: Theme): void {
230
+ const temps = state.sample.temperatures;
231
+ ui.panel({ title: "Temperatures" }, (p) => {
232
+ if (temps.length === 0) {
233
+ p.label("Not available on this platform");
234
+ p.label("(run with --sim to see them)", { size: 1 });
235
+ return;
236
+ }
237
+ temps.slice(0, 10).forEach((temp) => {
238
+ p.row({ size: 1 }, (r) => {
239
+ r.text(temp.label, { fg: theme.muted, width: 16 });
240
+ r.heatBar({ value: Math.min(1, temp.value / (temp.max || 100)) });
241
+ r.text(`${Math.round(temp.value)}°C`, { fg: heatColor(theme, temp.value / (temp.max || 100)), width: 6, align: "right" });
242
+ });
243
+ });
244
+ });
245
+ }
246
+
247
+ function sensorsPanel(ui: Container, state: DemoState, theme: Theme): void {
248
+ ui.panel({ title: "Sensors" }, (p) => {
249
+ const sensors = state.sample.sensors;
250
+ if (sensors.length === 0) {
251
+ p.label("No sensors reported");
252
+ return;
253
+ }
254
+ p.keyValues(sensors.map((s) => ({ label: s.label, value: s.value, color: theme.accent })));
255
+ });
256
+ }
257
+
258
+ function logsPanel(ui: Container, state: DemoState): void {
259
+ ui.panel({ title: "Logs" }, (p) => {
260
+ p.log({ entries: state.sample.logs.map((l) => ({ time: l.time, level: l.level, message: l.message, meta: `{${l.meta}}` })) });
261
+ });
262
+ }
263
+
264
+ export function visibleProcesses(state: DemoState): DemoState["sample"]["processes"] {
265
+ const filter = state.filter.toLowerCase();
266
+ const rows = filter
267
+ ? state.sample.processes.filter((p) => p.name.toLowerCase().includes(filter) || p.command.toLowerCase().includes(filter))
268
+ : state.sample.processes;
269
+ const sorted = [...rows];
270
+ sorted.sort((a, b) => {
271
+ switch (state.sort) {
272
+ case "mem": return b.mem - a.mem;
273
+ case "pid": return a.pid - b.pid;
274
+ case "name": return a.name.localeCompare(b.name);
275
+ default: return b.cpu - a.cpu;
276
+ }
277
+ });
278
+ return sorted;
279
+ }
280
+
281
+ /** The reference dashboard. Three layouts, chosen by terminal width. */
282
+ export function dashboardScreen(ui: Container, state: DemoState, theme: Theme): void {
283
+ ui.responsive({
284
+ 150: (wide) => {
285
+ // The System column holds nested panels, so it gets the extra width —
286
+ // and extra height whenever the terminal is tall enough to spare it.
287
+ wide.row({ size: wide.height >= 44 ? 19 : 16, gap: 1 }, (r) => {
288
+ r.column({ width: "1fr" }, (c) => cpuPanel(c, state, theme, 2));
289
+ r.column({ width: "0.95fr" }, (c) => memoryPanel(c, state, theme));
290
+ r.column({ width: "0.95fr" }, (c) => disksPanel(c, state, theme));
291
+ r.column({ width: "1.35fr" }, (c) => systemPanel(c, state, theme));
292
+ });
293
+ wide.row({ size: "1fr", gap: 1 }, (r) => {
294
+ r.column({ width: "2fr" }, (c) => processesPanel(c, state, theme));
295
+ r.column({ width: "1.2fr" }, (c) => networkPanel(c, state, theme));
296
+ r.column({ width: "1.2fr" }, (c) => diskUsagePanel(c, state, theme));
297
+ });
298
+ wide.row({ size: 12, gap: 1 }, (r) => {
299
+ temperaturesPanel(r, state, theme);
300
+ sensorsPanel(r, state, theme);
301
+ r.column({ width: "1.6fr" }, (c) => logsPanel(c, state));
302
+ });
303
+ },
304
+ 100: (medium) => {
305
+ medium.row({ size: 14, gap: 1 }, (r) => {
306
+ cpuPanel(r, state, theme, 2);
307
+ memoryPanel(r, state, theme);
308
+ systemPanel(r, state, theme);
309
+ });
310
+ medium.row({ size: "1fr", gap: 1 }, (r) => {
311
+ r.column({ width: "1.6fr" }, (c) => processesPanel(c, state, theme));
312
+ r.column({}, (c) => networkPanel(c, state, theme));
313
+ });
314
+ medium.row({ size: 10, gap: 1 }, (r) => {
315
+ temperaturesPanel(r, state, theme);
316
+ logsPanel(r, state);
317
+ });
318
+ },
319
+ 0: (compact) => {
320
+ compact.row({ size: 10, gap: 1 }, (r) => {
321
+ cpuPanel(r, state, theme, 1);
322
+ memoryPanel(r, state, theme);
323
+ });
324
+ compact.column({ size: "1fr" }, (c) => processesPanel(c, state, theme));
325
+ compact.row({ size: 8, gap: 1 }, (r) => networkPanel(r, state, theme));
326
+ },
327
+ });
328
+ }
@@ -0,0 +1,64 @@
1
+ import type { Container, Theme } from "@profullstack/hqtui";
2
+ import { gradientSteps } from "@profullstack/hqtui";
3
+ import type { DemoState } from "../state.ts";
4
+
5
+ function wave(count: number, phase: number, freq: number): number[] {
6
+ return Array.from({ length: count }, (_, i) => Math.sin(i / freq + phase) * 50 + 50);
7
+ }
8
+
9
+ /** Rendering-mode comparison: braille vs block vs ascii, plus raw canvas access. */
10
+ export function graphicsScreen(ui: Container, state: DemoState, theme: Theme): void {
11
+ const t = state.sample.time;
12
+ const a = wave(240, t / 3, 9);
13
+ const b = wave(240, t / 3 + 2, 5);
14
+ const c = wave(240, t / 2, 17);
15
+
16
+ ui.row({ size: "1fr", gap: 1 }, (row) => {
17
+ row.column({ gap: 1 }, (left) => {
18
+ left.panel({ title: "Braille (2×4 pixels per cell)" }, (p) => {
19
+ p.graph({ values: a, min: 0, max: 100, fill: true, color: theme.accent, grid: true });
20
+ });
21
+ left.panel({ title: "Block elements" }, (p) => {
22
+ p.graph({ values: a, min: 0, max: 100, mode: "block", colors: theme.heat });
23
+ });
24
+ left.panel({ title: "ASCII fallback" }, (p) => {
25
+ p.graph({ values: a, min: 0, max: 100, mode: "ascii", color: theme.foreground });
26
+ });
27
+ });
28
+
29
+ row.column({ gap: 1 }, (right) => {
30
+ right.panel({ title: "Multi-series" }, (p) => {
31
+ p.multiGraph(
32
+ [
33
+ { values: a, color: theme.primary, label: "alpha" },
34
+ { values: b, color: theme.success, label: "beta" },
35
+ { values: c, color: theme.secondary, label: "gamma" },
36
+ ],
37
+ { min: 0, max: 100, legend: true, axis: true },
38
+ );
39
+ });
40
+ right.panel({ title: "Gradients" }, (p) => {
41
+ p.draw((surface) => {
42
+ const steps = gradientSteps(theme.heat, surface.width);
43
+ for (let y = 0; y < surface.height; y++) {
44
+ for (let x = 0; x < surface.width; x++) {
45
+ surface.char(x, y, "█", { fg: steps[x] });
46
+ }
47
+ }
48
+ });
49
+ });
50
+ right.panel({ title: "Raw Braille canvas" }, (p) => {
51
+ p.canvas((canvas) => {
52
+ const cx = canvas.width / 2;
53
+ const cy = canvas.height / 2;
54
+ const r = Math.min(cx, cy) - 2;
55
+ canvas.circle(cx, cy, r);
56
+ for (let i = 0; i < 12; i++) {
57
+ const angle = (i / 12) * Math.PI * 2 + t / 4;
58
+ canvas.line(cx, cy, cx + Math.cos(angle) * r, cy + Math.sin(angle) * r * 0.9);
59
+ }
60
+ }, { color: theme.accent });
61
+ });
62
+ });
63
+ });
64
+ }
@@ -0,0 +1,6 @@
1
+ export { dashboardScreen, visibleProcesses } from "./dashboard.ts";
2
+ export { componentsScreen } from "./components.ts";
3
+ export { graphicsScreen } from "./graphics.ts";
4
+ export { themesScreen } from "./themes.ts";
5
+ export { inputScreen } from "./input.ts";
6
+ export { stressScreen } from "./stress.ts";
@@ -0,0 +1,39 @@
1
+ import type { Container, Theme } from "@profullstack/hqtui";
2
+ import type { DemoState } from "../state.ts";
3
+
4
+ /** Keyboard and mouse event visualizer — useful when debugging bindings. */
5
+ export function inputScreen(ui: Container, state: DemoState, theme: Theme): void {
6
+ ui.row({ size: "1fr", gap: 1 }, (row) => {
7
+ row.panel({ title: "Last Events" }, (p) => {
8
+ p.keyValues([
9
+ { label: "Key", value: state.lastKey, color: theme.accent },
10
+ { label: "Mouse", value: state.lastMouse, color: theme.primary },
11
+ ]);
12
+ p.spacer(1);
13
+ p.divider({ label: "history" });
14
+ p.list({ items: state.keyLog.slice(-20).reverse() });
15
+ });
16
+ row.panel({ title: "Try it" }, (p) => {
17
+ p.text("Press any key — modifiers are normalized.", { fg: theme.foreground, size: 1 });
18
+ p.label("Arrows, Function keys, Ctrl/Alt/Shift combinations,", { size: 1 });
19
+ p.label("paste, focus, mouse move, click, drag and scroll.", { size: 1 });
20
+ p.spacer(1);
21
+ p.divider({ label: "focusable controls" });
22
+ p.spacer(1);
23
+ p.row({ size: 1, gap: 2 }, (r) => {
24
+ r.button({ label: "Button A", width: 12, size: 12 });
25
+ r.button({ label: "Button B", width: 12, size: 12, variant: "success" });
26
+ r.checkbox({ label: "Check", checked: state.checkbox, size: 12, onToggle: () => { state.checkbox = !state.checkbox; } });
27
+ r.spacer("fill");
28
+ });
29
+ p.spacer(1);
30
+ p.label("Tab / Shift+Tab moves focus. Enter activates.", { size: 1 });
31
+ p.spacer("fill");
32
+ p.keyValues([
33
+ { label: "Mouse tracking", value: "on" },
34
+ { label: "Bracketed paste", value: "on" },
35
+ { label: "Focus events", value: "on" },
36
+ ]);
37
+ });
38
+ });
39
+ }
@@ -0,0 +1,36 @@
1
+ import type { Container, Theme } from "@profullstack/hqtui";
2
+ import { gradient } from "@profullstack/hqtui";
3
+ import type { DemoState } from "../state.ts";
4
+ import { num } from "../format.ts";
5
+
6
+ /** Every cell changes every frame — the worst case for a differential renderer. */
7
+ export function stressScreen(ui: Container, state: DemoState, theme: Theme): void {
8
+ const t = state.sample.time;
9
+ ui.row({ size: 3, gap: 1 }, (r) => {
10
+ r.panel({ title: "Render" }, (p) => {
11
+ p.text(`${num(state.renderMs, 2)} ms/frame`, { fg: theme.success });
12
+ });
13
+ r.panel({ title: "Changed cells" }, (p) => {
14
+ p.text(String(state.changedCells), { fg: theme.warning });
15
+ });
16
+ r.panel({ title: "Bytes/frame" }, (p) => {
17
+ p.text(String(state.bytes), { fg: theme.primary });
18
+ });
19
+ r.panel({ title: "FPS" }, (p) => {
20
+ p.text(num(state.fps, 1), { fg: theme.accent });
21
+ });
22
+ });
23
+ ui.panel({ title: "Full-screen churn", size: "1fr" }, (p) => {
24
+ p.draw((surface) => {
25
+ const ramp = gradient(theme.graph);
26
+ const chars = "▖▗▘▙▚▛▜▝▞▟█▓▒░";
27
+ for (let y = 0; y < surface.height; y++) {
28
+ for (let x = 0; x < surface.width; x++) {
29
+ const v = (Math.sin(x / 6 + t) + Math.cos(y / 4 - t)) / 2;
30
+ const n = (v + 1) / 2;
31
+ surface.char(x, y, chars[Math.floor(n * (chars.length - 1))], { fg: ramp(n) });
32
+ }
33
+ }
34
+ });
35
+ });
36
+ }
@@ -0,0 +1,41 @@
1
+ import type { Container, Theme } from "@profullstack/hqtui";
2
+ import { themeList } from "@profullstack/hqtui";
3
+ import type { DemoState } from "../state.ts";
4
+
5
+ /** Live theme switching. Every theme renders the same content side by side. */
6
+ export function themesScreen(ui: Container, state: DemoState, theme: Theme): void {
7
+ const values = state.sample.cpu.history;
8
+ ui.label(`Theme ${state.themeIndex + 1}/${themeList.length}: ${theme.name} ←/→ or F2 to change`, { size: 1 });
9
+ ui.spacer(1);
10
+ ui.grid({ columns: 3, rows: Math.ceil(themeList.length / 3), gap: 1 }, (grid) => {
11
+ themeList.forEach((entry, i) => {
12
+ grid.panel({
13
+ title: entry.name,
14
+ borderColor: i === state.themeIndex ? entry.borderFocused : entry.border,
15
+ background: entry.background,
16
+ }, (p) => {
17
+ p.row({ size: 1, gap: 1 }, (r) => {
18
+ r.badge({ text: "primary", color: entry.primary, size: 10 });
19
+ r.badge({ text: "ok", color: entry.success, size: 5 });
20
+ r.badge({ text: "warn", color: entry.warning, size: 7 });
21
+ r.badge({ text: "err", color: entry.danger, size: 6 });
22
+ r.spacer("fill");
23
+ });
24
+ p.meter({ value: 0.72, label: "cpu", background: entry.background, size: 1 });
25
+ p.graph({
26
+ values,
27
+ min: 0,
28
+ max: 100,
29
+ fill: true,
30
+ color: entry.graph[0],
31
+ background: entry.background,
32
+ });
33
+ p.draw((surface) => {
34
+ entry.graph.forEach((color, ci) => {
35
+ for (let x = 0; x < 3; x++) surface.char(ci * 4 + x, 0, "█", { fg: color, bg: entry.background });
36
+ });
37
+ }, { size: 1 });
38
+ });
39
+ });
40
+ });
41
+ }