@profullstack/hqtui-demo 0.2.0 → 0.4.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 (44) hide show
  1. package/README.md +8 -2
  2. package/dist/main.js +34 -7
  3. package/dist/main.js.map +1 -1
  4. package/dist/screens/components.js +5 -4
  5. package/dist/screens/components.js.map +1 -1
  6. package/dist/screens/dashboard.js +46 -36
  7. package/dist/screens/dashboard.js.map +1 -1
  8. package/dist/screens/graphics.js +5 -3
  9. package/dist/screens/graphics.js.map +1 -1
  10. package/dist/screens/index.js +1 -0
  11. package/dist/screens/index.js.map +1 -1
  12. package/dist/screens/input.js +3 -1
  13. package/dist/screens/input.js.map +1 -1
  14. package/dist/screens/network.js +5 -4
  15. package/dist/screens/network.js.map +1 -1
  16. package/dist/screens/services.js +4 -3
  17. package/dist/screens/services.js.map +1 -1
  18. package/dist/screens/sessions.js +5 -4
  19. package/dist/screens/sessions.js.map +1 -1
  20. package/dist/screens/stress.js +3 -1
  21. package/dist/screens/stress.js.map +1 -1
  22. package/dist/screens/themes.js +3 -1
  23. package/dist/screens/themes.js.map +1 -1
  24. package/dist/screens/traffic.js +6 -5
  25. package/dist/screens/traffic.js.map +1 -1
  26. package/dist/screens/world.js +97 -0
  27. package/dist/screens/world.js.map +1 -0
  28. package/dist/state.js +17 -1
  29. package/dist/state.js.map +1 -1
  30. package/package.json +2 -2
  31. package/src/main.ts +28 -7
  32. package/src/screens/components.ts +5 -4
  33. package/src/screens/dashboard.ts +47 -37
  34. package/src/screens/graphics.ts +5 -4
  35. package/src/screens/index.ts +1 -0
  36. package/src/screens/input.ts +3 -2
  37. package/src/screens/network.ts +5 -4
  38. package/src/screens/services.ts +4 -3
  39. package/src/screens/sessions.ts +5 -4
  40. package/src/screens/stress.ts +3 -2
  41. package/src/screens/themes.ts +3 -2
  42. package/src/screens/traffic.ts +6 -5
  43. package/src/screens/world.ts +102 -0
  44. package/src/state.ts +31 -2
package/src/main.ts CHANGED
@@ -10,10 +10,10 @@
10
10
  import { createApp, themeList, themes, type KeyEvent } from "@profullstack/hqtui";
11
11
  import { createCollector } from "./system/index.ts";
12
12
  import { intervalMs } from "./options.ts";
13
- import { createState, focusedPane, moveSelection, SCREENS, type ScreenName } from "./state.ts";
13
+ import { createState, focusedPane, moveSelection, SCREENS, SCREEN_KEYS, type ScreenName } from "./state.ts";
14
14
  import {
15
15
  componentsScreen, dashboardScreen, graphicsScreen, inputScreen, networkScreen, servicesScreen,
16
- sessionsScreen, stressScreen, themesScreen, trafficScreen, visibleProcesses,
16
+ sessionsScreen, stressScreen, themesScreen, trafficScreen, visibleProcesses, worldScreen,
17
17
  } from "./screens/index.ts";
18
18
  import { clock, num } from "./format.ts";
19
19
 
@@ -51,7 +51,7 @@ function parseArgs(argv: string[]): Options {
51
51
  case "-h":
52
52
  case "--help": printHelp(); process.exit(0);
53
53
  case "-v":
54
- case "--version": console.log("hqtui-demo 0.2.0"); process.exit(0);
54
+ case "--version": console.log("hqtui-demo 0.4.0"); process.exit(0);
55
55
  }
56
56
  }
57
57
  return options;
@@ -69,13 +69,13 @@ Options:
69
69
  --seed <n> Simulation seed (default 1337)
70
70
  --fps <n> Frame cap (default 30, 15 over SSH)
71
71
  --theme <name> dark, dracula, nord, tokyoNight, gruvbox, matrix, monochrome, highContrast, light
72
- --screen <name> dashboard, components, graphics, themes, input, stress
72
+ --screen <name> dashboard, components, graphics, themes, input, stress, world
73
73
  --interval <ms> Metric refresh interval (default 1000)
74
74
  -h, --help Show this help
75
75
  -v, --version Show the version
76
76
 
77
77
  Keys:
78
- 1-6 / Tab screens F2 theme F3 filter F6 sort Ctrl+K palette
78
+ 1-0/w / Tab screens F2 theme F3 filter F6 sort Ctrl+K palette
79
79
  ↑/↓ select Space pause F1 help q quit
80
80
  `);
81
81
  }
@@ -91,6 +91,7 @@ const PALETTE_COMMANDS: { label: string; hint: string; run: (state: ReturnType<t
91
91
  { label: "Go to Themes", hint: "8", run: (s) => { s.screen = "themes"; } },
92
92
  { label: "Go to Input", hint: "9", run: (s) => { s.screen = "input"; } },
93
93
  { label: "Go to Stress Test", hint: "0", run: (s) => { s.screen = "stress"; } },
94
+ { label: "Go to World Map", hint: "w", run: (s) => { s.screen = "world"; } },
94
95
  { label: "Sort by CPU", hint: "F6", run: (s) => { s.sort = "cpu"; } },
95
96
  { label: "Sort by Memory", hint: "F6", run: (s) => { s.sort = "mem"; } },
96
97
  { label: "Pause updates", hint: "Space", run: (s) => { s.paused = !s.paused; } },
@@ -210,11 +211,28 @@ async function main(): Promise<void> {
210
211
  app.setTheme(themeList[state.themeIndex]);
211
212
  return;
212
213
  case "f3": state.filtering = true; return;
214
+ case "c":
215
+ // Shows what collapseBorders does, live. Worth a key because the
216
+ // difference is only visible when panels sit next to each other —
217
+ // which is also why the screens have to close their one-column gaps
218
+ // at the same time. The library only merges a seam where two bordered
219
+ // siblings already touch, so toggling the flag alone moved nothing.
220
+ state.collapsed = !state.collapsed;
221
+ app.setCollapseBorders(state.collapsed);
222
+ return;
213
223
  case "f6": {
214
224
  const order = ["cpu", "mem", "pid", "name"] as const;
215
225
  state.sort = order[(order.indexOf(state.sort) + 1) % order.length];
216
226
  return;
217
227
  }
228
+ case "w": state.screen = "world"; return;
229
+ case "z":
230
+ // The map is the only screen with somewhere to zoom to.
231
+ if (state.screen === "world") state.worldZoom = !state.worldZoom;
232
+ return;
233
+ case "r":
234
+ if (state.screen === "world") state.worldZoom = false;
235
+ return;
218
236
  case "f10": app.quit(); return;
219
237
  case "ctrl+k":
220
238
  state.showPalette = true;
@@ -257,7 +275,7 @@ async function main(): Promise<void> {
257
275
  ui.row({ size: 1 }, (header) => {
258
276
  header.text(" hqtui.com", { fg: theme.title, bold: true, size: 12 });
259
277
  header.tabs({
260
- tabs: SCREENS.map((s, i) => `${(i + 1) % 10} ${s}`),
278
+ tabs: SCREENS.map((s) => `${SCREEN_KEYS[s]} ${s}`),
261
279
  active: SCREENS.indexOf(state.screen),
262
280
  onSelect: (index) => { state.screen = SCREENS[index]; },
263
281
  });
@@ -279,6 +297,7 @@ async function main(): Promise<void> {
279
297
  case "themes": themesScreen(body, state, theme); break;
280
298
  case "input": inputScreen(body, state, theme); break;
281
299
  case "stress": stressScreen(body, state, theme); break;
300
+ case "world": worldScreen(body, state, theme); break;
282
301
  default: dashboardScreen(body, state, theme); break;
283
302
  }
284
303
  });
@@ -289,6 +308,7 @@ async function main(): Promise<void> {
289
308
  { key: "F1", label: "Help" },
290
309
  { key: "F2", label: `Theme (${theme.name})` },
291
310
  { key: "F3", label: state.filtering ? `Filter: ${state.filter}_` : "Filter", active: state.filtering },
311
+ { key: "c", label: "Collapse", active: app.collapseBorders },
292
312
  { key: "F6", label: `Sort: ${state.sort}` },
293
313
  { key: "^K", label: "Palette" },
294
314
  { key: "Tab", label: "Screen" },
@@ -303,8 +323,9 @@ async function main(): Promise<void> {
303
323
  width: 62,
304
324
  height: 18,
305
325
  message:
306
- "1-6 or Tab switch screens.\n" +
326
+ "1-0, w or Tab switch screens; w is the clickable world map.\n" +
307
327
  "F2 cycles themes, F3 filters processes, F6 changes sort.\n" +
328
+ "c collapses adjacent panel borders into shared lines.\n" +
308
329
  "Ctrl+K opens the command palette, Space pauses updates.\n" +
309
330
  "Arrows, PageUp/PageDown, Home/End move the selection.\n" +
310
331
  "Mouse: click tabs and buttons, scroll the process list.\n\n" +
@@ -1,5 +1,5 @@
1
1
  import type { Container, Theme } from "@profullstack/hqtui";
2
- import { focusPane, pane, scrollPane, type DemoState } from "../state.ts";
2
+ import { focusPane, pane, panelGap, scrollPane, type DemoState } from "../state.ts";
3
3
  import { bytes, percent } from "../format.ts";
4
4
 
5
5
  const FILES = [
@@ -39,8 +39,9 @@ const TREE = [
39
39
 
40
40
  /** Every widget in the library, in one screen. Also the interaction sandbox. */
41
41
  export function componentsScreen(ui: Container, state: DemoState, theme: Theme): void {
42
- ui.row({ size: "1fr", gap: 1 }, (row) => {
43
- row.column({ gap: 1 }, (left) => {
42
+ const gap = panelGap(state);
43
+ ui.row({ size: "1fr", gap }, (row) => {
44
+ row.column({ gap, bordered: true }, (left) => {
44
45
  left.panel({ title: "Buttons & Inputs", size: 13 }, (p) => {
45
46
  p.row({ size: 1, gap: 1 }, (r) => {
46
47
  r.button({ label: "Primary", width: 11, size: 11, onPress: () => { state.showModal = true; } });
@@ -115,7 +116,7 @@ export function componentsScreen(ui: Container, state: DemoState, theme: Theme):
115
116
  });
116
117
  });
117
118
 
118
- row.column({ gap: 1 }, (right) => {
119
+ row.column({ gap, bordered: true }, (right) => {
119
120
  right.panel({ title: "Process Tree", size: 13 }, (p) => {
120
121
  p.row({ size: 1 }, (r) => {
121
122
  r.text("Name", { fg: theme.muted, bold: true });
@@ -1,13 +1,13 @@
1
- import type { Container, Theme } from "@profullstack/hqtui";
1
+ import type { Container, Size, Theme } from "@profullstack/hqtui";
2
2
  import { heatColor } from "@profullstack/hqtui";
3
- import { focusPane, pane, scrollPane, type DemoState } from "../state.ts";
3
+ import { focusPane, pane, panelGap, scrollPane, type DemoState } from "../state.ts";
4
4
  import { bitRate, byteRate, bytes, clock, duration, num, percent } from "../format.ts";
5
5
 
6
6
  const TIME_LABELS = ["60s", "45s", "30s", "15s", "0s"];
7
7
 
8
- function cpuPanel(ui: Container, state: DemoState, theme: Theme, coreColumns: number): void {
8
+ function cpuPanel(ui: Container, state: DemoState, theme: Theme, coreColumns: number, width?: Size): void {
9
9
  const cpu = state.sample.cpu;
10
- ui.panel({ title: "CPU Overview", subtitle: percent(cpu.total) }, (p) => {
10
+ ui.panel({ title: "CPU Overview", subtitle: percent(cpu.total), width }, (p) => {
11
11
  p.label(`${cpu.model} ${num(cpu.frequencyGhz, 1)} GHz`, { size: 1 });
12
12
  p.graph({ values: cpu.history, min: 0, max: 100, fill: true, color: theme.success, size: "1fr" });
13
13
  p.meters(
@@ -26,11 +26,11 @@ function cpuPanel(ui: Container, state: DemoState, theme: Theme, coreColumns: nu
26
26
  });
27
27
  }
28
28
 
29
- function memoryPanel(ui: Container, state: DemoState, theme: Theme): void {
29
+ function memoryPanel(ui: Container, state: DemoState, theme: Theme, width?: Size): void {
30
30
  const memory = state.sample.memory;
31
31
  const used = memory.used / Math.max(1, memory.total);
32
32
  const swap = memory.swapTotal > 0 ? memory.swapUsed / memory.swapTotal : 0;
33
- ui.panel({ title: "Memory & Swap" }, (p) => {
33
+ ui.panel({ title: "Memory & Swap", width }, (p) => {
34
34
  p.text(`Memory${" ".repeat(6)}${bytes(memory.used)} / ${bytes(memory.total)} (${percent(used)})`, {
35
35
  fg: theme.foreground,
36
36
  size: 1,
@@ -58,9 +58,9 @@ function memoryPanel(ui: Container, state: DemoState, theme: Theme): void {
58
58
  });
59
59
  }
60
60
 
61
- function disksPanel(ui: Container, state: DemoState, theme: Theme): void {
61
+ function disksPanel(ui: Container, state: DemoState, theme: Theme, width?: Size): void {
62
62
  const disks = state.sample.disks;
63
- ui.panel({ title: "Disks" }, (p) => {
63
+ ui.panel({ title: "Disks", width }, (p) => {
64
64
  if (disks.length === 0) {
65
65
  p.label("No disks reported");
66
66
  return;
@@ -87,9 +87,10 @@ function disksPanel(ui: Container, state: DemoState, theme: Theme): void {
87
87
  });
88
88
  }
89
89
 
90
- function systemPanel(ui: Container, state: DemoState, theme: Theme): void {
90
+ function systemPanel(ui: Container, state: DemoState, theme: Theme, width?: Size): void {
91
91
  const s = state.sample;
92
- ui.panel({ title: "System" }, (p) => {
92
+ const gap = panelGap(state);
93
+ ui.panel({ title: "System", width }, (p) => {
93
94
  p.row({ size: 6, gap: 2, min: 6 }, (r) => {
94
95
  r.keyValues([
95
96
  { label: "OS:", value: s.system.os },
@@ -114,7 +115,7 @@ function systemPanel(ui: Container, state: DemoState, theme: Theme): void {
114
115
  // Sub-panels need both width and height to be legible; otherwise they are
115
116
  // replaced by a single dense stat line.
116
117
  if (p.width >= 46 && p.height >= 16) {
117
- p.row({ size: 6, gap: 1 }, (r) => {
118
+ p.row({ size: 6, gap }, (r) => {
118
119
  r.panel({ title: "Quick Stats" }, (q) => {
119
120
  q.keyValues([
120
121
  { label: "Uptime", value: duration(s.system.uptime), color: theme.accent },
@@ -147,12 +148,13 @@ function systemPanel(ui: Container, state: DemoState, theme: Theme): void {
147
148
  });
148
149
  }
149
150
 
150
- function processesPanel(ui: Container, state: DemoState, theme: Theme): void {
151
+ function processesPanel(ui: Container, state: DemoState, theme: Theme, width?: Size): void {
151
152
  const rows = visibleProcesses(state);
152
153
  ui.panel({
153
154
  title: `Processes (sorted by ${state.sort.toUpperCase()})`,
154
155
  subtitle: state.filter ? `filter: ${state.filter}` : undefined,
155
156
  focusable: true,
157
+ width,
156
158
  }, (p) => {
157
159
  const procs = pane(state, "dashboard.processes", rows.length);
158
160
  p.table({
@@ -179,9 +181,9 @@ function processesPanel(ui: Container, state: DemoState, theme: Theme): void {
179
181
  });
180
182
  }
181
183
 
182
- function networkPanel(ui: Container, state: DemoState, theme: Theme): void {
184
+ function networkPanel(ui: Container, state: DemoState, theme: Theme, width?: Size): void {
183
185
  const net = state.sample.network;
184
- ui.panel({ title: "Network" }, (p) => {
186
+ ui.panel({ title: "Network", width }, (p) => {
185
187
  p.row({ size: 1 }, (r) => {
186
188
  r.text(`Download: ${bitRate(net.downRate)}`, { fg: theme.primary });
187
189
  r.text(`Upload: ${bitRate(net.upRate)}`, { fg: theme.secondary, align: "right" });
@@ -204,9 +206,9 @@ function networkPanel(ui: Container, state: DemoState, theme: Theme): void {
204
206
  });
205
207
  }
206
208
 
207
- function diskUsagePanel(ui: Container, state: DemoState, theme: Theme): void {
209
+ function diskUsagePanel(ui: Container, state: DemoState, theme: Theme, width?: Size): void {
208
210
  const disks = state.sample.disks;
209
- ui.panel({ title: "Disk Usage", subtitle: disks[0]?.device }, (p) => {
211
+ ui.panel({ title: "Disk Usage", subtitle: disks[0]?.device, width }, (p) => {
210
212
  disks.forEach((disk) => {
211
213
  const used = disk.total > 0 ? disk.used / disk.total : 0;
212
214
  p.meter({
@@ -268,8 +270,8 @@ function sensorsPanel(ui: Container, state: DemoState, theme: Theme): void {
268
270
  });
269
271
  }
270
272
 
271
- function logsPanel(ui: Container, state: DemoState): void {
272
- ui.panel({ title: "Logs" }, (p) => {
273
+ function logsPanel(ui: Container, state: DemoState, width?: Size): void {
274
+ ui.panel({ title: "Logs", width }, (p) => {
273
275
  const logs = pane(state, "dashboard.logs", state.sample.logs.length, "log");
274
276
  p.log({
275
277
  entries: state.sample.logs.map((l) => ({
@@ -304,51 +306,59 @@ export function visibleProcesses(state: DemoState): DemoState["sample"]["process
304
306
  return sorted;
305
307
  }
306
308
 
307
- /** The reference dashboard. Three layouts, chosen by terminal width. */
309
+ /**
310
+ * The reference dashboard. Three layouts, chosen by terminal width.
311
+ *
312
+ * Panels go straight into their row rather than each inside a sizing column.
313
+ * A column is not a bordered child, so a row of them has no seam to merge and
314
+ * [c] could never change this screen; `width` on the panel does the same job
315
+ * and leaves the borders adjacent to each other.
316
+ */
308
317
  export function dashboardScreen(ui: Container, state: DemoState, theme: Theme): void {
318
+ const gap = panelGap(state);
309
319
  ui.responsive({
310
320
  150: (wide) => {
311
321
  // The System column holds nested panels, so it gets the extra width —
312
322
  // and extra height whenever the terminal is tall enough to spare it.
313
- wide.row({ size: wide.height >= 44 ? 19 : 16, gap: 1 }, (r) => {
314
- r.column({ width: "1fr" }, (c) => cpuPanel(c, state, theme, 2));
315
- r.column({ width: "0.95fr" }, (c) => memoryPanel(c, state, theme));
316
- r.column({ width: "0.95fr" }, (c) => disksPanel(c, state, theme));
317
- r.column({ width: "1.35fr" }, (c) => systemPanel(c, state, theme));
323
+ wide.row({ size: wide.height >= 44 ? 19 : 16, gap }, (r) => {
324
+ cpuPanel(r, state, theme, 2, "1fr");
325
+ memoryPanel(r, state, theme, "0.95fr");
326
+ disksPanel(r, state, theme, "0.95fr");
327
+ systemPanel(r, state, theme, "1.35fr");
318
328
  });
319
- wide.row({ size: "1fr", gap: 1 }, (r) => {
320
- r.column({ width: "2fr" }, (c) => processesPanel(c, state, theme));
321
- r.column({ width: "1.2fr" }, (c) => networkPanel(c, state, theme));
322
- r.column({ width: "1.2fr" }, (c) => diskUsagePanel(c, state, theme));
329
+ wide.row({ size: "1fr", gap }, (r) => {
330
+ processesPanel(r, state, theme, "2fr");
331
+ networkPanel(r, state, theme, "1.2fr");
332
+ diskUsagePanel(r, state, theme, "1.2fr");
323
333
  });
324
- wide.row({ size: 12, gap: 1 }, (r) => {
334
+ wide.row({ size: 12, gap }, (r) => {
325
335
  temperaturesPanel(r, state, theme);
326
336
  sensorsPanel(r, state, theme);
327
- r.column({ width: "1.6fr" }, (c) => logsPanel(c, state));
337
+ logsPanel(r, state, "1.6fr");
328
338
  });
329
339
  },
330
340
  100: (medium) => {
331
- medium.row({ size: 14, gap: 1 }, (r) => {
341
+ medium.row({ size: 14, gap }, (r) => {
332
342
  cpuPanel(r, state, theme, 2);
333
343
  memoryPanel(r, state, theme);
334
344
  systemPanel(r, state, theme);
335
345
  });
336
- medium.row({ size: "1fr", gap: 1 }, (r) => {
337
- r.column({ width: "1.6fr" }, (c) => processesPanel(c, state, theme));
338
- r.column({}, (c) => networkPanel(c, state, theme));
346
+ medium.row({ size: "1fr", gap }, (r) => {
347
+ processesPanel(r, state, theme, "1.6fr");
348
+ networkPanel(r, state, theme);
339
349
  });
340
- medium.row({ size: 10, gap: 1 }, (r) => {
350
+ medium.row({ size: 10, gap }, (r) => {
341
351
  temperaturesPanel(r, state, theme);
342
352
  logsPanel(r, state);
343
353
  });
344
354
  },
345
355
  0: (compact) => {
346
- compact.row({ size: 10, gap: 1 }, (r) => {
356
+ compact.row({ size: 10, gap }, (r) => {
347
357
  cpuPanel(r, state, theme, 1);
348
358
  memoryPanel(r, state, theme);
349
359
  });
350
360
  compact.column({ size: "1fr" }, (c) => processesPanel(c, state, theme));
351
- compact.row({ size: 8, gap: 1 }, (r) => networkPanel(r, state, theme));
361
+ compact.row({ size: 8, gap }, (r) => networkPanel(r, state, theme));
352
362
  },
353
363
  });
354
364
  }
@@ -1,6 +1,6 @@
1
1
  import type { Container, Theme } from "@profullstack/hqtui";
2
2
  import { gradientSteps } from "@profullstack/hqtui";
3
- import type { DemoState } from "../state.ts";
3
+ import { panelGap, type DemoState } from "../state.ts";
4
4
 
5
5
  function wave(count: number, phase: number, freq: number): number[] {
6
6
  return Array.from({ length: count }, (_, i) => Math.sin(i / freq + phase) * 50 + 50);
@@ -8,13 +8,14 @@ function wave(count: number, phase: number, freq: number): number[] {
8
8
 
9
9
  /** Rendering-mode comparison: braille vs block vs ascii, plus raw canvas access. */
10
10
  export function graphicsScreen(ui: Container, state: DemoState, theme: Theme): void {
11
+ const gap = panelGap(state);
11
12
  const t = state.sample.time;
12
13
  const a = wave(240, t / 3, 9);
13
14
  const b = wave(240, t / 3 + 2, 5);
14
15
  const c = wave(240, t / 2, 17);
15
16
 
16
- ui.row({ size: "1fr", gap: 1 }, (row) => {
17
- row.column({ gap: 1 }, (left) => {
17
+ ui.row({ size: "1fr", gap }, (row) => {
18
+ row.column({ gap, bordered: true }, (left) => {
18
19
  left.panel({ title: "Braille (2×4 pixels per cell)" }, (p) => {
19
20
  p.graph({ values: a, min: 0, max: 100, fill: true, color: theme.accent, grid: true });
20
21
  });
@@ -26,7 +27,7 @@ export function graphicsScreen(ui: Container, state: DemoState, theme: Theme): v
26
27
  });
27
28
  });
28
29
 
29
- row.column({ gap: 1 }, (right) => {
30
+ row.column({ gap, bordered: true }, (right) => {
30
31
  right.panel({ title: "Multi-series" }, (p) => {
31
32
  p.multiGraph(
32
33
  [
@@ -8,3 +8,4 @@ export { graphicsScreen } from "./graphics.ts";
8
8
  export { themesScreen } from "./themes.ts";
9
9
  export { inputScreen } from "./input.ts";
10
10
  export { stressScreen } from "./stress.ts";
11
+ export { worldScreen } from "./world.ts";
@@ -1,9 +1,10 @@
1
1
  import type { Container, Theme } from "@profullstack/hqtui";
2
- import type { DemoState } from "../state.ts";
2
+ import { panelGap, type DemoState } from "../state.ts";
3
3
 
4
4
  /** Keyboard and mouse event visualizer — useful when debugging bindings. */
5
5
  export function inputScreen(ui: Container, state: DemoState, theme: Theme): void {
6
- ui.row({ size: "1fr", gap: 1 }, (row) => {
6
+ const gap = panelGap(state);
7
+ ui.row({ size: "1fr", gap }, (row) => {
7
8
  row.panel({ title: "Last Events" }, (p) => {
8
9
  p.keyValues([
9
10
  { label: "Key", value: state.lastKey, color: theme.accent },
@@ -1,14 +1,15 @@
1
1
  import type { Container, Theme } from "@profullstack/hqtui";
2
- import { focusPane, pane, scrollPane, type DemoState } from "../state.ts";
2
+ import { focusPane, pane, panelGap, scrollPane, type DemoState } from "../state.ts";
3
3
  import { byteRate, bytes } from "../format.ts";
4
4
 
5
5
  /** Interfaces, live throughput, open connections and listening ports. */
6
6
  export function networkScreen(ui: Container, state: DemoState, theme: Theme): void {
7
+ const gap = panelGap(state);
7
8
  const t = state.sample.telemetry;
8
9
  const active = t.interfaces.filter((i) => i.rxTotal > 0 || i.state === "up");
9
10
  const shown = (active.length ? active : t.interfaces).slice(0, 3);
10
11
 
11
- ui.row({ size: 13, gap: 1 }, (row) => {
12
+ ui.row({ size: 13, gap }, (row) => {
12
13
  if (shown.length === 0) {
13
14
  row.panel({ title: "Interfaces" }, (p) => p.label("No interfaces reported."));
14
15
  return;
@@ -43,7 +44,7 @@ export function networkScreen(ui: Container, state: DemoState, theme: Theme): vo
43
44
  });
44
45
  });
45
46
 
46
- ui.row({ size: "1fr", gap: 1 }, (row) => {
47
+ ui.row({ size: "1fr", gap }, (row) => {
47
48
  row.panel({
48
49
  title: "Connections",
49
50
  subtitle: `${t.connections.length} open`,
@@ -74,7 +75,7 @@ export function networkScreen(ui: Container, state: DemoState, theme: Theme): vo
74
75
  });
75
76
  });
76
77
 
77
- row.column({ width: "0.7fr", gap: 1 }, (column) => {
78
+ row.column({ width: "0.7fr", gap, bordered: true }, (column) => {
78
79
  column.panel({ title: "Listening Ports", subtitle: String(t.listeners.length), borderColor: theme.warning }, (p) => {
79
80
  const listeners = pane(state, "network.listeners", t.listeners.length);
80
81
  p.table({
@@ -1,5 +1,5 @@
1
1
  import type { Container, Theme } from "@profullstack/hqtui";
2
- import { focusPane, pane, scrollPane, type DemoState } from "../state.ts";
2
+ import { focusPane, pane, panelGap, scrollPane, type DemoState } from "../state.ts";
3
3
  import { bytes, num, percent } from "../format.ts";
4
4
 
5
5
  function rate(value: number): string {
@@ -9,10 +9,11 @@ function rate(value: number): string {
9
9
 
10
10
  /** Services, containers, kernel counters, filesystems and hardware sensors. */
11
11
  export function servicesScreen(ui: Container, state: DemoState, theme: Theme): void {
12
+ const gap = panelGap(state);
12
13
  const t = state.sample.telemetry;
13
14
  const failed = t.services.filter((s) => s.active === "failed");
14
15
 
15
- ui.row({ size: "1fr", gap: 1 }, (row) => {
16
+ ui.row({ size: "1fr", gap }, (row) => {
16
17
  row.panel({
17
18
  title: "Services",
18
19
  subtitle: failed.length ? `${failed.length} failed` : `${t.services.length} units`,
@@ -49,7 +50,7 @@ export function servicesScreen(ui: Container, state: DemoState, theme: Theme): v
49
50
  });
50
51
  });
51
52
 
52
- row.column({ width: "0.85fr", gap: 1 }, (column) => {
53
+ row.column({ width: "0.85fr", gap, bordered: true }, (column) => {
53
54
  column.panel({ title: "Kernel", size: 11, borderColor: theme.accent }, (p) => {
54
55
  const k = t.kernel;
55
56
  p.keyValues([
@@ -1,11 +1,12 @@
1
1
  import type { Container, Theme } from "@profullstack/hqtui";
2
- import { focusPane, pane, scrollPane, type DemoState } from "../state.ts";
2
+ import { focusPane, pane, panelGap, scrollPane, type DemoState } from "../state.ts";
3
3
 
4
4
  /** Who is on this machine, who has been, and who failed to get in. */
5
5
  export function sessionsScreen(ui: Container, state: DemoState, theme: Theme): void {
6
+ const gap = panelGap(state);
6
7
  const t = state.sample.telemetry;
7
8
 
8
- ui.row({ size: 9, gap: 1 }, (row) => {
9
+ ui.row({ size: 9, gap }, (row) => {
9
10
  row.panel({ title: "Active Sessions", subtitle: String(t.sessions.length), borderColor: theme.success }, (p) => {
10
11
  if (t.sessions.length === 0) {
11
12
  p.label("No interactive sessions.");
@@ -42,7 +43,7 @@ export function sessionsScreen(ui: Container, state: DemoState, theme: Theme): v
42
43
  });
43
44
  });
44
45
 
45
- ui.row({ size: "1fr", gap: 1 }, (row) => {
46
+ ui.row({ size: "1fr", gap }, (row) => {
46
47
  row.panel({ title: "Recent Logins", subtitle: `${t.logins.length} from wtmp`, borderColor: theme.accent }, (p) => {
47
48
  if (t.logins.length === 0) {
48
49
  p.label("No login history available.");
@@ -74,7 +75,7 @@ export function sessionsScreen(ui: Container, state: DemoState, theme: Theme): v
74
75
  });
75
76
  });
76
77
 
77
- row.column({ width: "0.8fr", gap: 1 }, (column) => {
78
+ row.column({ width: "0.8fr", gap, bordered: true }, (column) => {
78
79
  column.panel({ title: "Failed Logins", borderColor: theme.danger }, (p) => {
79
80
  if (t.failedLogins.length === 0) {
80
81
  p.label("None recorded.");
@@ -1,12 +1,13 @@
1
1
  import type { Container, Theme } from "@profullstack/hqtui";
2
2
  import { gradient } from "@profullstack/hqtui";
3
- import type { DemoState } from "../state.ts";
3
+ import { panelGap, type DemoState } from "../state.ts";
4
4
  import { num } from "../format.ts";
5
5
 
6
6
  /** Every cell changes every frame — the worst case for a differential renderer. */
7
7
  export function stressScreen(ui: Container, state: DemoState, theme: Theme): void {
8
+ const gap = panelGap(state);
8
9
  const t = state.sample.time;
9
- ui.row({ size: 3, gap: 1 }, (r) => {
10
+ ui.row({ size: 3, gap }, (r) => {
10
11
  r.panel({ title: "Render" }, (p) => {
11
12
  p.text(`${num(state.renderMs, 2)} ms/frame`, { fg: theme.success });
12
13
  });
@@ -1,13 +1,14 @@
1
1
  import type { Container, Theme } from "@profullstack/hqtui";
2
2
  import { themeList } from "@profullstack/hqtui";
3
- import type { DemoState } from "../state.ts";
3
+ import { panelGap, type DemoState } from "../state.ts";
4
4
 
5
5
  /** Live theme switching. Every theme renders the same content side by side. */
6
6
  export function themesScreen(ui: Container, state: DemoState, theme: Theme): void {
7
7
  const values = state.sample.cpu.history;
8
+ const gap = panelGap(state);
8
9
  ui.label(`Theme ${state.themeIndex + 1}/${themeList.length}: ${theme.name} ←/→ or F2 to change`, { size: 1 });
9
10
  ui.spacer(1);
10
- ui.grid({ columns: 3, rows: Math.ceil(themeList.length / 3), gap: 1 }, (grid) => {
11
+ ui.grid({ columns: 3, rows: Math.ceil(themeList.length / 3), gap }, (grid) => {
11
12
  themeList.forEach((entry, i) => {
12
13
  grid.panel({
13
14
  title: entry.name,
@@ -1,6 +1,6 @@
1
1
  import type { Container, Theme } from "@profullstack/hqtui";
2
2
  import { seriesColor } from "@profullstack/hqtui";
3
- import { focusPane, pane, scrollPane, type DemoState } from "../state.ts";
3
+ import { focusPane, pane, panelGap, scrollPane, type DemoState } from "../state.ts";
4
4
  import { num, percent } from "../format.ts";
5
5
 
6
6
  function rate(value: number): string {
@@ -19,11 +19,12 @@ const STATUS_COLORS = (theme: Theme): Record<string, number> => ({
19
19
 
20
20
  /** Every protocol in and out of this host: sockets, TCP counters, SSH, HTTP. */
21
21
  export function trafficScreen(ui: Container, state: DemoState, theme: Theme): void {
22
+ const gap = panelGap(state);
22
23
  const t = state.sample.telemetry;
23
24
  const net = t.net;
24
25
  const http = t.http;
25
26
 
26
- ui.row({ size: 13, gap: 1 }, (row) => {
27
+ ui.row({ size: 13, gap }, (row) => {
27
28
  row.panel({
28
29
  title: "Protocols",
29
30
  subtitle: `${t.inboundConnections} in / ${t.outboundConnections} out`,
@@ -85,8 +86,8 @@ export function trafficScreen(ui: Container, state: DemoState, theme: Theme): vo
85
86
  });
86
87
  });
87
88
 
88
- ui.row({ size: "1fr", gap: 1 }, (row) => {
89
- row.column({ gap: 1 }, (column) => {
89
+ ui.row({ size: "1fr", gap }, (row) => {
90
+ row.column({ gap, bordered: true }, (column) => {
90
91
  column.panel({
91
92
  title: "HTTP",
92
93
  subtitle: http ? `${num(http.requestsPerSecond, 1)} req/s` : "no access log",
@@ -135,7 +136,7 @@ export function trafficScreen(ui: Container, state: DemoState, theme: Theme): vo
135
136
  });
136
137
  });
137
138
 
138
- row.column({ width: "0.85fr", gap: 1 }, (column) => {
139
+ row.column({ width: "0.85fr", gap, bordered: true }, (column) => {
139
140
  column.panel({ title: "SSH Activity", subtitle: String(t.ssh.length), borderColor: theme.warning }, (p) => {
140
141
  if (t.ssh.length === 0) {
141
142
  p.label("No sshd events in the journal.");