@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.
- package/README.md +8 -2
- package/dist/main.js +34 -7
- package/dist/main.js.map +1 -1
- package/dist/screens/components.js +5 -4
- package/dist/screens/components.js.map +1 -1
- package/dist/screens/dashboard.js +46 -36
- package/dist/screens/dashboard.js.map +1 -1
- package/dist/screens/graphics.js +5 -3
- package/dist/screens/graphics.js.map +1 -1
- package/dist/screens/index.js +1 -0
- package/dist/screens/index.js.map +1 -1
- package/dist/screens/input.js +3 -1
- package/dist/screens/input.js.map +1 -1
- package/dist/screens/network.js +5 -4
- package/dist/screens/network.js.map +1 -1
- package/dist/screens/services.js +4 -3
- package/dist/screens/services.js.map +1 -1
- package/dist/screens/sessions.js +5 -4
- package/dist/screens/sessions.js.map +1 -1
- package/dist/screens/stress.js +3 -1
- package/dist/screens/stress.js.map +1 -1
- package/dist/screens/themes.js +3 -1
- package/dist/screens/themes.js.map +1 -1
- package/dist/screens/traffic.js +6 -5
- package/dist/screens/traffic.js.map +1 -1
- package/dist/screens/world.js +97 -0
- package/dist/screens/world.js.map +1 -0
- package/dist/state.js +17 -1
- package/dist/state.js.map +1 -1
- package/package.json +2 -2
- package/src/main.ts +28 -7
- package/src/screens/components.ts +5 -4
- package/src/screens/dashboard.ts +47 -37
- package/src/screens/graphics.ts +5 -4
- package/src/screens/index.ts +1 -0
- package/src/screens/input.ts +3 -2
- package/src/screens/network.ts +5 -4
- package/src/screens/services.ts +4 -3
- package/src/screens/sessions.ts +5 -4
- package/src/screens/stress.ts +3 -2
- package/src/screens/themes.ts +3 -2
- package/src/screens/traffic.ts +6 -5
- package/src/screens/world.ts +102 -0
- 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.
|
|
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-
|
|
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
|
|
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-
|
|
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
|
-
|
|
43
|
-
|
|
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:
|
|
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 });
|
package/src/screens/dashboard.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
314
|
-
|
|
315
|
-
r
|
|
316
|
-
r
|
|
317
|
-
r
|
|
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
|
|
320
|
-
|
|
321
|
-
r
|
|
322
|
-
r
|
|
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
|
|
334
|
+
wide.row({ size: 12, gap }, (r) => {
|
|
325
335
|
temperaturesPanel(r, state, theme);
|
|
326
336
|
sensorsPanel(r, state, theme);
|
|
327
|
-
r
|
|
337
|
+
logsPanel(r, state, "1.6fr");
|
|
328
338
|
});
|
|
329
339
|
},
|
|
330
340
|
100: (medium) => {
|
|
331
|
-
medium.row({ size: 14, gap
|
|
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
|
|
337
|
-
r
|
|
338
|
-
|
|
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
|
|
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
|
|
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
|
|
361
|
+
compact.row({ size: 8, gap }, (r) => networkPanel(r, state, theme));
|
|
352
362
|
},
|
|
353
363
|
});
|
|
354
364
|
}
|
package/src/screens/graphics.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Container, Theme } from "@profullstack/hqtui";
|
|
2
2
|
import { gradientSteps } from "@profullstack/hqtui";
|
|
3
|
-
import type
|
|
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
|
|
17
|
-
row.column({ gap:
|
|
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:
|
|
30
|
+
row.column({ gap, bordered: true }, (right) => {
|
|
30
31
|
right.panel({ title: "Multi-series" }, (p) => {
|
|
31
32
|
p.multiGraph(
|
|
32
33
|
[
|
package/src/screens/index.ts
CHANGED
package/src/screens/input.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { Container, Theme } from "@profullstack/hqtui";
|
|
2
|
-
import type
|
|
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
|
-
|
|
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 },
|
package/src/screens/network.ts
CHANGED
|
@@ -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
|
|
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
|
|
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:
|
|
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({
|
package/src/screens/services.ts
CHANGED
|
@@ -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
|
|
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:
|
|
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([
|
package/src/screens/sessions.ts
CHANGED
|
@@ -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
|
|
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
|
|
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:
|
|
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.");
|
package/src/screens/stress.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { Container, Theme } from "@profullstack/hqtui";
|
|
2
2
|
import { gradient } from "@profullstack/hqtui";
|
|
3
|
-
import type
|
|
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
|
|
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
|
});
|
package/src/screens/themes.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import type { Container, Theme } from "@profullstack/hqtui";
|
|
2
2
|
import { themeList } from "@profullstack/hqtui";
|
|
3
|
-
import type
|
|
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
|
|
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,
|
package/src/screens/traffic.ts
CHANGED
|
@@ -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
|
|
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
|
|
89
|
-
row.column({ gap:
|
|
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:
|
|
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.");
|