@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,56 @@
1
+ import type { Collector, SystemSample } from "./types.ts";
2
+ import { createSystemSimulation, type SimulationOptions } from "../simulation.ts";
3
+
4
+ export type { Collector, SystemSample };
5
+
6
+ /** Wraps the deterministic simulation in the Collector interface. */
7
+ class SimulationCollector implements Collector {
8
+ source = "simulated";
9
+ unavailable: string[] = [];
10
+ private simulation;
11
+
12
+ constructor(options: SimulationOptions) {
13
+ this.simulation = createSystemSimulation(options);
14
+ }
15
+
16
+ async refresh(dt: number): Promise<void> {
17
+ this.simulation.update(dt);
18
+ }
19
+
20
+ current(): SystemSample {
21
+ return this.simulation.current();
22
+ }
23
+ }
24
+
25
+ export interface SourceOptions extends SimulationOptions {
26
+ /** Read the real machine. Falls back to the simulation if unsupported. */
27
+ real?: boolean;
28
+ }
29
+
30
+ /**
31
+ * Real metrics on Linux, macOS and Windows; a deterministic simulation
32
+ * everywhere else (and whenever `real` is off).
33
+ */
34
+ export async function createCollector(options: SourceOptions = {}): Promise<Collector> {
35
+ if (!options.real) return new SimulationCollector(options);
36
+
37
+ try {
38
+ if (process.platform === "linux") {
39
+ const { LinuxCollector } = await import("./linux.ts");
40
+ return new LinuxCollector();
41
+ }
42
+ if (process.platform === "darwin") {
43
+ const { DarwinCollector } = await import("./darwin.ts");
44
+ return new DarwinCollector();
45
+ }
46
+ if (process.platform === "win32") {
47
+ const { WindowsCollector } = await import("./win32.ts");
48
+ return new WindowsCollector();
49
+ }
50
+ } catch {
51
+ // Fall through to the simulation rather than failing to start.
52
+ }
53
+ const fallback = new SimulationCollector(options);
54
+ fallback.source = `simulated (${process.platform} not supported)`;
55
+ return fallback;
56
+ }
@@ -0,0 +1,258 @@
1
+ import { readFile, readdir } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import type { Collector, SystemSample } from "./types.ts";
4
+ import { baseSample, loadAverage, primaryInterface, push, ratePerSecond, sh } from "./common.ts";
5
+
6
+ async function read(path: string): Promise<string> {
7
+ try {
8
+ return await readFile(path, "utf8");
9
+ } catch {
10
+ return "";
11
+ }
12
+ }
13
+
14
+ interface CpuTimes {
15
+ idle: number;
16
+ total: number;
17
+ }
18
+
19
+ function parseCpuTimes(text: string): CpuTimes[] {
20
+ const out: CpuTimes[] = [];
21
+ for (const line of text.split("\n")) {
22
+ if (!line.startsWith("cpu")) continue;
23
+ const parts = line.trim().split(/\s+/);
24
+ const values = parts.slice(1).map(Number);
25
+ if (values.length < 4) continue;
26
+ const idle = values[3] + (values[4] ?? 0);
27
+ const total = values.reduce((a, b) => a + b, 0);
28
+ out.push({ idle, total });
29
+ }
30
+ return out;
31
+ }
32
+
33
+ function parseMeminfo(text: string): Record<string, number> {
34
+ const out: Record<string, number> = {};
35
+ for (const line of text.split("\n")) {
36
+ const match = /^(\w+):\s+(\d+)/.exec(line);
37
+ if (match) out[match[1]] = Number(match[2]) * 1024;
38
+ }
39
+ return out;
40
+ }
41
+
42
+ /** Reads everything from /proc and /sys — no external commands, no privileges. */
43
+ export class LinuxCollector implements Collector {
44
+ source = "linux /proc";
45
+ unavailable: string[] = [];
46
+ private sample = baseSample();
47
+ private prevCpu: CpuTimes[] = [];
48
+ private prevDisk = new Map<string, [number, number]>();
49
+ private prevNet: [number, number] | null = null;
50
+ private sectorSize = 512;
51
+ private mounts: { device: string; mount: string }[] = [];
52
+
53
+ async refresh(dt: number): Promise<void> {
54
+ const s = this.sample;
55
+ const [stat, meminfo, uptime, diskstats, netdev] = await Promise.all([
56
+ read("/proc/stat"),
57
+ read("/proc/meminfo"),
58
+ read("/proc/uptime"),
59
+ read("/proc/diskstats"),
60
+ read("/proc/net/dev"),
61
+ ]);
62
+
63
+ // CPU — deltas against the previous reading.
64
+ const times = parseCpuTimes(stat);
65
+ if (this.prevCpu.length === times.length && times.length > 0) {
66
+ const usage = times.map((now, i) => {
67
+ const prev = this.prevCpu[i];
68
+ const totalDelta = now.total - prev.total;
69
+ const idleDelta = now.idle - prev.idle;
70
+ if (totalDelta <= 0) return 0;
71
+ return Math.max(0, Math.min(1, 1 - idleDelta / totalDelta));
72
+ });
73
+ s.cpu.total = usage[0];
74
+ s.cpu.cores = usage.slice(1);
75
+ if (s.cpu.cores.length === 0) s.cpu.cores = [s.cpu.total];
76
+ }
77
+ this.prevCpu = times;
78
+ push(s.cpu.history, s.cpu.total * 100);
79
+ s.cpu.load = loadAverage();
80
+ const cpus = os.cpus();
81
+ s.cpu.model = cpus[0]?.model?.trim() ?? s.cpu.model;
82
+ s.cpu.frequencyGhz = (cpus.reduce((a, c) => a + c.speed, 0) / Math.max(1, cpus.length)) / 1000;
83
+
84
+ // Context switches and process/thread counts.
85
+ const ctxt = /^ctxt (\d+)/m.exec(stat);
86
+ if (ctxt) s.system.contextSwitches = Number(ctxt[1]);
87
+ const procs = /^procs_running (\d+)/m.exec(stat);
88
+ if (procs) s.system.threadCount = Number(procs[1]);
89
+
90
+ // Memory.
91
+ const mem = parseMeminfo(meminfo);
92
+ if (mem.MemTotal) {
93
+ s.memory.total = mem.MemTotal;
94
+ s.memory.free = mem.MemFree ?? 0;
95
+ s.memory.available = mem.MemAvailable ?? mem.MemFree ?? 0;
96
+ s.memory.cached = mem.Cached ?? 0;
97
+ s.memory.buffers = mem.Buffers ?? 0;
98
+ s.memory.used = mem.MemTotal - s.memory.available;
99
+ s.memory.swapTotal = mem.SwapTotal ?? 0;
100
+ s.memory.swapUsed = (mem.SwapTotal ?? 0) - (mem.SwapFree ?? 0);
101
+ }
102
+ push(s.memory.history, (s.memory.used / Math.max(1, s.memory.total)) * 100);
103
+
104
+ if (uptime) s.system.uptime = Number(uptime.split(" ")[0]);
105
+
106
+ // Disks: throughput from /proc/diskstats, capacity from `df`.
107
+ if (this.mounts.length === 0) await this.loadMounts();
108
+ const stats = new Map<string, [number, number]>();
109
+ for (const line of diskstats.split("\n")) {
110
+ const parts = line.trim().split(/\s+/);
111
+ if (parts.length < 14) continue;
112
+ const name = parts[2];
113
+ if (/^(loop|ram|dm-|sr)/.test(name)) continue;
114
+ stats.set(name, [Number(parts[5]) * this.sectorSize, Number(parts[9]) * this.sectorSize]);
115
+ }
116
+
117
+ if (s.disks.length === 0) {
118
+ for (const mount of this.mounts.slice(0, 4)) {
119
+ s.disks.push({
120
+ device: mount.device,
121
+ mount: mount.mount,
122
+ type: "disk",
123
+ total: 0, used: 0,
124
+ readRate: 0, writeRate: 0, readHistory: [], writeHistory: [],
125
+ iops: [0, 0], temperature: 0,
126
+ });
127
+ }
128
+ }
129
+ for (const disk of s.disks) {
130
+ const base = disk.device.replace(/p?\d+$/, "");
131
+ const now = stats.get(disk.device) ?? stats.get(base);
132
+ if (now) {
133
+ const prev = this.prevDisk.get(disk.device);
134
+ if (prev) {
135
+ disk.readRate = ratePerSecond(now[0], prev[0], dt);
136
+ disk.writeRate = ratePerSecond(now[1], prev[1], dt);
137
+ }
138
+ this.prevDisk.set(disk.device, now);
139
+ }
140
+ push(disk.readHistory, disk.readRate);
141
+ push(disk.writeHistory, disk.writeRate);
142
+ disk.iops = [Math.round(disk.readRate / 4096), Math.round(disk.writeRate / 4096)];
143
+ }
144
+ await this.updateCapacity();
145
+
146
+ // Network.
147
+ const iface = primaryInterface();
148
+ s.network.interface = iface.name;
149
+ s.network.ip = iface.ip;
150
+ s.network.mac = iface.mac;
151
+ for (const line of netdev.split("\n")) {
152
+ const match = /^\s*([\w.-]+):\s*(\d+)(?:\s+\d+){7}\s+(\d+)/.exec(line);
153
+ if (!match || match[1] !== iface.name) continue;
154
+ const now: [number, number] = [Number(match[2]), Number(match[3])];
155
+ if (this.prevNet) {
156
+ s.network.downRate = ratePerSecond(now[0], this.prevNet[0], dt);
157
+ s.network.upRate = ratePerSecond(now[1], this.prevNet[1], dt);
158
+ }
159
+ s.network.downTotal = now[0];
160
+ s.network.upTotal = now[1];
161
+ this.prevNet = now;
162
+ }
163
+ push(s.network.downHistory, s.network.downRate);
164
+ push(s.network.upHistory, s.network.upRate);
165
+ s.network.downPeak = Math.max(s.network.downPeak, s.network.downRate);
166
+ s.network.upPeak = Math.max(s.network.upPeak, s.network.upRate);
167
+ const speed = await read(`/sys/class/net/${iface.name}/speed`);
168
+ s.network.speed = speed.trim() && Number(speed) > 0 ? `${Number(speed) / 1000} Gb/s` : "-";
169
+
170
+ await Promise.all([this.updateProcesses(), this.updateTemperatures()]);
171
+ }
172
+
173
+ private async loadMounts(): Promise<void> {
174
+ const text = await read("/proc/mounts");
175
+ const seen = new Set<string>();
176
+ for (const line of text.split("\n")) {
177
+ const [device, mount, type] = line.split(" ");
178
+ if (!device?.startsWith("/dev/")) continue;
179
+ if (!["ext4", "xfs", "btrfs", "zfs", "f2fs", "ext3", "vfat", "apfs", "overlay"].includes(type)) continue;
180
+ const name = device.replace("/dev/", "");
181
+ if (seen.has(name)) continue;
182
+ seen.add(name);
183
+ this.mounts.push({ device: name, mount });
184
+ }
185
+ if (this.mounts.length === 0) this.mounts.push({ device: "root", mount: "/" });
186
+ }
187
+
188
+ private async updateCapacity(): Promise<void> {
189
+ const text = await sh("df", ["-kP", ...this.sample.disks.map((d) => d.mount)]);
190
+ const lines = text.trim().split("\n").slice(1);
191
+ for (const line of lines) {
192
+ const parts = line.trim().split(/\s+/);
193
+ if (parts.length < 6) continue;
194
+ const mount = parts[parts.length - 1];
195
+ const disk = this.sample.disks.find((d) => d.mount === mount);
196
+ if (!disk) continue;
197
+ disk.total = Number(parts[1]) * 1024;
198
+ disk.used = Number(parts[2]) * 1024;
199
+ }
200
+ }
201
+
202
+ private async updateProcesses(): Promise<void> {
203
+ const text = await sh("ps", ["-eo", "pid,comm,pcpu,pmem,rss,nlwp,user,state,args", "--sort=-pcpu"]);
204
+ if (!text) {
205
+ if (!this.unavailable.includes("processes")) this.unavailable.push("processes");
206
+ return;
207
+ }
208
+ const lines = text.trim().split("\n").slice(1, 60);
209
+ this.sample.processes = lines.map((line) => {
210
+ const parts = line.trim().split(/\s+/);
211
+ return {
212
+ pid: Number(parts[0]),
213
+ name: parts[1] ?? "-",
214
+ cpu: Number(parts[2]) || 0,
215
+ mem: Number(parts[3]) || 0,
216
+ rss: (Number(parts[4]) || 0) * 1024,
217
+ threads: Number(parts[5]) || 1,
218
+ user: parts[6] ?? "-",
219
+ state: parts[7] ?? "-",
220
+ command: parts.slice(8).join(" "),
221
+ };
222
+ });
223
+ this.sample.system.processCount = this.sample.processes.length;
224
+ }
225
+
226
+ private async updateTemperatures(): Promise<void> {
227
+ const out: { label: string; value: number; max: number }[] = [];
228
+ try {
229
+ const zones = await readdir("/sys/class/hwmon");
230
+ for (const zone of zones) {
231
+ const base = `/sys/class/hwmon/${zone}`;
232
+ const chip = (await read(`${base}/name`)).trim();
233
+ const entries = await readdir(base).catch(() => [] as string[]);
234
+ for (const entry of entries) {
235
+ if (!/^temp\d+_input$/.test(entry)) continue;
236
+ const raw = await read(`${base}/${entry}`);
237
+ const value = Number(raw) / 1000;
238
+ if (!Number.isFinite(value) || value <= 0 || value > 150) continue;
239
+ const label = (await read(`${base}/${entry.replace("_input", "_label")}`)).trim();
240
+ out.push({ label: label || `${chip} ${entry.replace("_input", "")}`, value, max: 100 });
241
+ if (out.length >= 10) break;
242
+ }
243
+ if (out.length >= 10) break;
244
+ }
245
+ } catch {
246
+ // hwmon is optional; containers and VMs frequently have none.
247
+ }
248
+ if (out.length === 0 && !this.unavailable.includes("temperatures")) {
249
+ this.unavailable.push("temperatures");
250
+ }
251
+ this.sample.temperatures = out;
252
+ this.sample.sensors = out.slice(0, 6).map((t) => ({ label: t.label, value: `${t.value.toFixed(1)} °C` }));
253
+ }
254
+
255
+ current(): SystemSample {
256
+ return this.sample;
257
+ }
258
+ }
@@ -0,0 +1,12 @@
1
+ import type { SystemSample } from "../simulation.ts";
2
+
3
+ export type { SystemSample };
4
+
5
+ export interface Collector {
6
+ /** Human label shown in the UI: "linux /proc", "simulated", … */
7
+ source: string;
8
+ /** Metrics this platform could not provide. Shown in the help screen. */
9
+ unavailable: string[];
10
+ refresh(dt: number): Promise<void>;
11
+ current(): SystemSample;
12
+ }
@@ -0,0 +1,132 @@
1
+ import os from "node:os";
2
+ import type { Collector, SystemSample } from "./types.ts";
3
+ import { baseSample, loadAverage, primaryInterface, push, ratePerSecond, sh } from "./common.ts";
4
+
5
+ /** One PowerShell round-trip per refresh; CIM covers CPU, memory, disk and net. */
6
+ async function powershell(script: string): Promise<unknown> {
7
+ const out = await sh("powershell.exe", [
8
+ "-NoProfile", "-NonInteractive", "-Command",
9
+ `${script} | ConvertTo-Json -Compress -Depth 4`,
10
+ ], 8000);
11
+ if (!out.trim()) return null;
12
+ try {
13
+ return JSON.parse(out);
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+
19
+ function asArray<T>(value: unknown): T[] {
20
+ if (Array.isArray(value)) return value as T[];
21
+ if (value === null || value === undefined) return [];
22
+ return [value as T];
23
+ }
24
+
25
+ export class WindowsCollector implements Collector {
26
+ source = "Windows CIM";
27
+ unavailable = ["per-core detail", "temperatures"];
28
+ private sample = baseSample();
29
+ private prevNet: [number, number] | null = null;
30
+
31
+ async refresh(dt: number): Promise<void> {
32
+ const s = this.sample;
33
+
34
+ const cpu = await powershell(
35
+ "Get-CimInstance Win32_PerfFormattedData_PerfOS_Processor | Select-Object Name,PercentProcessorTime",
36
+ );
37
+ const entries = asArray<{ Name: string; PercentProcessorTime: number }>(cpu);
38
+ const cores = entries.filter((e) => e.Name !== "_Total");
39
+ const total = entries.find((e) => e.Name === "_Total");
40
+ if (total) s.cpu.total = Math.max(0, Math.min(1, Number(total.PercentProcessorTime) / 100));
41
+ if (cores.length > 0) {
42
+ s.cpu.cores = cores.map((c) => Math.max(0, Math.min(1, Number(c.PercentProcessorTime) / 100)));
43
+ }
44
+ push(s.cpu.history, s.cpu.total * 100);
45
+ s.cpu.load = loadAverage();
46
+
47
+ const mem = await powershell(
48
+ "Get-CimInstance Win32_OperatingSystem | Select-Object TotalVisibleMemorySize,FreePhysicalMemory,TotalVirtualMemorySize,FreeVirtualMemory,Caption,Version,NumberOfProcesses",
49
+ ) as Record<string, number | string> | null;
50
+ if (mem) {
51
+ const totalBytes = Number(mem.TotalVisibleMemorySize) * 1024;
52
+ const freeBytes = Number(mem.FreePhysicalMemory) * 1024;
53
+ s.memory.total = totalBytes;
54
+ s.memory.free = freeBytes;
55
+ s.memory.available = freeBytes;
56
+ s.memory.used = totalBytes - freeBytes;
57
+ s.memory.swapTotal = Number(mem.TotalVirtualMemorySize) * 1024;
58
+ s.memory.swapUsed = s.memory.swapTotal - Number(mem.FreeVirtualMemory) * 1024;
59
+ s.system.os = `${mem.Caption ?? "Windows"}`.trim();
60
+ s.system.processCount = Number(mem.NumberOfProcesses) || 0;
61
+ }
62
+ push(s.memory.history, (s.memory.used / Math.max(1, s.memory.total)) * 100);
63
+ s.system.uptime = os.uptime();
64
+
65
+ const disks = await powershell(
66
+ "Get-CimInstance Win32_LogicalDisk -Filter \"DriveType=3\" | Select-Object DeviceID,Size,FreeSpace",
67
+ );
68
+ const diskRows = asArray<{ DeviceID: string; Size: number; FreeSpace: number }>(disks);
69
+ if (s.disks.length === 0) {
70
+ for (const row of diskRows.slice(0, 4)) {
71
+ s.disks.push({
72
+ device: row.DeviceID, mount: row.DeviceID, type: "disk",
73
+ total: 0, used: 0, readRate: 0, writeRate: 0,
74
+ readHistory: [], writeHistory: [], iops: [0, 0], temperature: 0,
75
+ });
76
+ }
77
+ }
78
+ for (const disk of s.disks) {
79
+ const row = diskRows.find((r) => r.DeviceID === disk.device);
80
+ if (!row) continue;
81
+ disk.total = Number(row.Size) || 0;
82
+ disk.used = disk.total - (Number(row.FreeSpace) || 0);
83
+ push(disk.readHistory, disk.readRate);
84
+ push(disk.writeHistory, disk.writeRate);
85
+ }
86
+
87
+ const iface = primaryInterface();
88
+ s.network.interface = iface.name;
89
+ s.network.ip = iface.ip;
90
+ s.network.mac = iface.mac;
91
+ const net = await powershell(
92
+ "Get-CimInstance Win32_PerfRawData_Tcpip_NetworkInterface | Select-Object BytesReceivedPersec,BytesSentPersec",
93
+ );
94
+ const netRows = asArray<{ BytesReceivedPersec: number; BytesSentPersec: number }>(net);
95
+ if (netRows.length > 0) {
96
+ const received = netRows.reduce((a, r) => a + Number(r.BytesReceivedPersec || 0), 0);
97
+ const sent = netRows.reduce((a, r) => a + Number(r.BytesSentPersec || 0), 0);
98
+ if (this.prevNet) {
99
+ s.network.downRate = ratePerSecond(received, this.prevNet[0], dt);
100
+ s.network.upRate = ratePerSecond(sent, this.prevNet[1], dt);
101
+ }
102
+ s.network.downTotal = received;
103
+ s.network.upTotal = sent;
104
+ this.prevNet = [received, sent];
105
+ }
106
+ push(s.network.downHistory, s.network.downRate);
107
+ push(s.network.upHistory, s.network.upRate);
108
+ s.network.downPeak = Math.max(s.network.downPeak, s.network.downRate);
109
+ s.network.upPeak = Math.max(s.network.upPeak, s.network.upRate);
110
+
111
+ const processes = await powershell(
112
+ "Get-Process | Sort-Object CPU -Descending | Select-Object -First 40 Id,ProcessName,CPU,WorkingSet,Threads",
113
+ );
114
+ const rows = asArray<{ Id: number; ProcessName: string; CPU: number; WorkingSet: number; Threads: unknown }>(processes);
115
+ const totalMemory = Math.max(1, s.memory.total);
116
+ s.processes = rows.map((row) => ({
117
+ pid: Number(row.Id),
118
+ name: String(row.ProcessName),
119
+ cpu: Number(row.CPU) || 0,
120
+ mem: ((Number(row.WorkingSet) || 0) / totalMemory) * 100,
121
+ rss: Number(row.WorkingSet) || 0,
122
+ threads: Array.isArray(row.Threads) ? row.Threads.length : 1,
123
+ user: "-",
124
+ state: "R",
125
+ command: String(row.ProcessName),
126
+ }));
127
+ }
128
+
129
+ current(): SystemSample {
130
+ return this.sample;
131
+ }
132
+ }