@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.
- package/LICENSE +21 -0
- package/bin/hqtui-demo.mjs +3 -0
- package/dist/format.js +53 -0
- package/dist/format.js.map +1 -0
- package/dist/main.js +361 -0
- package/dist/main.js.map +1 -0
- package/dist/screens/components.js +144 -0
- package/dist/screens/components.js.map +1 -0
- package/dist/screens/dashboard.js +307 -0
- package/dist/screens/dashboard.js.map +1 -0
- package/dist/screens/graphics.js +56 -0
- package/dist/screens/graphics.js.map +1 -0
- package/dist/screens/index.js +7 -0
- package/dist/screens/index.js.map +1 -0
- package/dist/screens/input.js +37 -0
- package/dist/screens/input.js.map +1 -0
- package/dist/screens/stress.js +34 -0
- package/dist/screens/stress.js.map +1 -0
- package/dist/screens/themes.js +40 -0
- package/dist/screens/themes.js.map +1 -0
- package/dist/simulation.js +254 -0
- package/dist/simulation.js.map +1 -0
- package/dist/state.js +35 -0
- package/dist/state.js.map +1 -0
- package/dist/system/common.js +97 -0
- package/dist/system/common.js.map +1 -0
- package/dist/system/darwin.js +153 -0
- package/dist/system/darwin.js.map +1 -0
- package/dist/system/index.js +45 -0
- package/dist/system/index.js.map +1 -0
- package/dist/system/linux.js +260 -0
- package/dist/system/linux.js.map +1 -0
- package/dist/system/types.js +2 -0
- package/dist/system/types.js.map +1 -0
- package/dist/system/win32.js +118 -0
- package/dist/system/win32.js.map +1 -0
- package/package.json +39 -0
- package/src/format.ts +52 -0
- package/src/main.ts +320 -0
- package/src/screens/components.ts +153 -0
- package/src/screens/dashboard.ts +328 -0
- package/src/screens/graphics.ts +64 -0
- package/src/screens/index.ts +6 -0
- package/src/screens/input.ts +39 -0
- package/src/screens/stress.ts +36 -0
- package/src/screens/themes.ts +41 -0
- package/src/simulation.ts +356 -0
- package/src/state.ts +74 -0
- package/src/system/common.ts +101 -0
- package/src/system/darwin.ts +160 -0
- package/src/system/index.ts +56 -0
- package/src/system/linux.ts +258 -0
- package/src/system/types.ts +12 -0
- package/src/system/win32.ts +132 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A deterministic fake system. Same seed, same sequence — which is what makes
|
|
3
|
+
* benchmarks and CI snapshots reproducible.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface SimulationOptions {
|
|
7
|
+
seed?: number;
|
|
8
|
+
/** Number of CPU cores to simulate. */
|
|
9
|
+
cores?: number;
|
|
10
|
+
/** Samples of history to retain per series. */
|
|
11
|
+
history?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ProcessSample {
|
|
15
|
+
pid: number;
|
|
16
|
+
name: string;
|
|
17
|
+
cpu: number;
|
|
18
|
+
mem: number;
|
|
19
|
+
rss: number;
|
|
20
|
+
threads: number;
|
|
21
|
+
user: string;
|
|
22
|
+
command: string;
|
|
23
|
+
state: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SystemSample {
|
|
27
|
+
time: number;
|
|
28
|
+
cpu: {
|
|
29
|
+
total: number;
|
|
30
|
+
cores: number[];
|
|
31
|
+
history: number[];
|
|
32
|
+
load: [number, number, number];
|
|
33
|
+
model: string;
|
|
34
|
+
frequencyGhz: number;
|
|
35
|
+
};
|
|
36
|
+
memory: {
|
|
37
|
+
total: number;
|
|
38
|
+
used: number;
|
|
39
|
+
available: number;
|
|
40
|
+
cached: number;
|
|
41
|
+
buffers: number;
|
|
42
|
+
free: number;
|
|
43
|
+
history: number[];
|
|
44
|
+
swapTotal: number;
|
|
45
|
+
swapUsed: number;
|
|
46
|
+
};
|
|
47
|
+
disks: {
|
|
48
|
+
device: string;
|
|
49
|
+
mount: string;
|
|
50
|
+
type: string;
|
|
51
|
+
total: number;
|
|
52
|
+
used: number;
|
|
53
|
+
readRate: number;
|
|
54
|
+
writeRate: number;
|
|
55
|
+
readHistory: number[];
|
|
56
|
+
writeHistory: number[];
|
|
57
|
+
iops: [number, number];
|
|
58
|
+
temperature: number;
|
|
59
|
+
}[];
|
|
60
|
+
network: {
|
|
61
|
+
interface: string;
|
|
62
|
+
speed: string;
|
|
63
|
+
ip: string;
|
|
64
|
+
mac: string;
|
|
65
|
+
downRate: number;
|
|
66
|
+
upRate: number;
|
|
67
|
+
downHistory: number[];
|
|
68
|
+
upHistory: number[];
|
|
69
|
+
downTotal: number;
|
|
70
|
+
upTotal: number;
|
|
71
|
+
downPeak: number;
|
|
72
|
+
upPeak: number;
|
|
73
|
+
};
|
|
74
|
+
processes: ProcessSample[];
|
|
75
|
+
temperatures: { label: string; value: number; max: number }[];
|
|
76
|
+
sensors: { label: string; value: string }[];
|
|
77
|
+
system: {
|
|
78
|
+
os: string;
|
|
79
|
+
kernel: string;
|
|
80
|
+
hostname: string;
|
|
81
|
+
shell: string;
|
|
82
|
+
terminal: string;
|
|
83
|
+
uptime: number;
|
|
84
|
+
processCount: number;
|
|
85
|
+
threadCount: number;
|
|
86
|
+
contextSwitches: number;
|
|
87
|
+
};
|
|
88
|
+
logs: { time: string; level: string; message: string; meta: string }[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Mulberry32: tiny, fast, and good enough for smooth-looking fake data. */
|
|
92
|
+
function makeRandom(seed: number): () => number {
|
|
93
|
+
let a = seed >>> 0;
|
|
94
|
+
return () => {
|
|
95
|
+
a = (a + 0x6d2b79f5) >>> 0;
|
|
96
|
+
let t = a;
|
|
97
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
98
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
99
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const PROCESS_NAMES: [string, string, string][] = [
|
|
104
|
+
["bun", "dev", "bun server.ts"],
|
|
105
|
+
["node", "dev", "node index.js"],
|
|
106
|
+
["postgres", "postgres", "postgres -D /var/lib/postgresql/data"],
|
|
107
|
+
["redis-server", "redis", "redis-server *:6379"],
|
|
108
|
+
["docker", "root", "dockerd -H unix:///var/run/docker.sock"],
|
|
109
|
+
["nginx", "www-data", "nginx: worker process"],
|
|
110
|
+
["python", "dev", "python worker.py"],
|
|
111
|
+
["systemd", "root", "/sbin/init"],
|
|
112
|
+
["chrome", "dev", "chrome --type=renderer"],
|
|
113
|
+
["code", "dev", "code --unity-launch"],
|
|
114
|
+
["ssh", "dev", "ssh deploy@prod"],
|
|
115
|
+
["rustc", "dev", "rustc --edition 2021 src/main.rs"],
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
const LOG_TEMPLATES: [string, string, string][] = [
|
|
119
|
+
["INFO", "Server started on http://localhost:3000", "service: api"],
|
|
120
|
+
["INFO", "Database connection established", "service: db"],
|
|
121
|
+
["WARN", "Cache miss for key: user:48231", "service: cache"],
|
|
122
|
+
["INFO", "Background job \"cleanup\" completed in 120ms", "service: job"],
|
|
123
|
+
["ERROR", "Failed to fetch user profile", "service: api"],
|
|
124
|
+
["WARN", "Retrying in 2 seconds (attempt 2/3)", "service: api"],
|
|
125
|
+
["INFO", "New websocket connection", "service: ws"],
|
|
126
|
+
["INFO", "User authenticated successfully", "service: auth"],
|
|
127
|
+
["DEBUG", "Response time: 142ms", "service: api"],
|
|
128
|
+
["INFO", "Migration 0042_add_index applied", "service: db"],
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
function push(history: number[], value: number, limit: number): void {
|
|
132
|
+
history.push(value);
|
|
133
|
+
if (history.length > limit) history.shift();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Drives a plausible-looking machine: correlated cores, memory drift, network
|
|
138
|
+
* bursts, temperatures that lag CPU load, and process churn.
|
|
139
|
+
*/
|
|
140
|
+
export class SystemSimulation {
|
|
141
|
+
private random: () => number;
|
|
142
|
+
private tick = 0;
|
|
143
|
+
private historyLimit: number;
|
|
144
|
+
private state: SystemSample;
|
|
145
|
+
private phase: number[];
|
|
146
|
+
|
|
147
|
+
constructor(options: SimulationOptions = {}) {
|
|
148
|
+
this.random = makeRandom(options.seed ?? 1337);
|
|
149
|
+
this.historyLimit = options.history ?? 240;
|
|
150
|
+
const cores = options.cores ?? 12;
|
|
151
|
+
this.phase = Array.from({ length: cores }, () => this.random() * Math.PI * 2);
|
|
152
|
+
|
|
153
|
+
const totalMemory = 16 * 1024 ** 3;
|
|
154
|
+
this.state = {
|
|
155
|
+
time: 0,
|
|
156
|
+
cpu: {
|
|
157
|
+
total: 0.18,
|
|
158
|
+
cores: new Array(cores).fill(0.15),
|
|
159
|
+
history: [],
|
|
160
|
+
load: [0.74, 0.62, 0.58],
|
|
161
|
+
model: "Intel(R) Core(TM) i7-1260P 12th Gen",
|
|
162
|
+
frequencyGhz: 2.1,
|
|
163
|
+
},
|
|
164
|
+
memory: {
|
|
165
|
+
total: totalMemory,
|
|
166
|
+
used: totalMemory * 0.42,
|
|
167
|
+
available: totalMemory * 0.58,
|
|
168
|
+
cached: totalMemory * 0.25,
|
|
169
|
+
buffers: totalMemory * 0.07,
|
|
170
|
+
free: totalMemory * 0.33,
|
|
171
|
+
history: [],
|
|
172
|
+
swapTotal: 2 * 1024 ** 3,
|
|
173
|
+
swapUsed: 1.24 * 1024 ** 3,
|
|
174
|
+
},
|
|
175
|
+
disks: [
|
|
176
|
+
{
|
|
177
|
+
device: "nvme0n1", mount: "/", type: "SSD",
|
|
178
|
+
total: 512 * 1024 ** 3, used: 136 * 1024 ** 3,
|
|
179
|
+
readRate: 0, writeRate: 0, readHistory: [], writeHistory: [],
|
|
180
|
+
iops: [2100, 1300], temperature: 43,
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
device: "sda1", mount: "/data", type: "HDD",
|
|
184
|
+
total: 2 * 1024 ** 4, used: 1.02 * 1024 ** 4,
|
|
185
|
+
readRate: 0, writeRate: 0, readHistory: [], writeHistory: [],
|
|
186
|
+
iops: [180, 90], temperature: 38,
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
network: {
|
|
190
|
+
interface: "en0", speed: "1 Gb/s", ip: "192.168.1.42", mac: "ac:de:48:00:11:22",
|
|
191
|
+
downRate: 0, upRate: 0, downHistory: [], upHistory: [],
|
|
192
|
+
downTotal: 12.6 * 1024 ** 3, upTotal: 3.2 * 1024 ** 3,
|
|
193
|
+
downPeak: 0, upPeak: 0,
|
|
194
|
+
},
|
|
195
|
+
processes: [],
|
|
196
|
+
temperatures: [],
|
|
197
|
+
sensors: [],
|
|
198
|
+
system: {
|
|
199
|
+
os: "Ubuntu 24.04 LTS",
|
|
200
|
+
kernel: "6.8.0-31-generic",
|
|
201
|
+
hostname: "devbox",
|
|
202
|
+
shell: "bash 5.2.21",
|
|
203
|
+
terminal: "hqtui",
|
|
204
|
+
uptime: 9254,
|
|
205
|
+
processCount: 243,
|
|
206
|
+
threadCount: 981,
|
|
207
|
+
contextSwitches: 52100,
|
|
208
|
+
},
|
|
209
|
+
logs: [],
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// Seed history so the first frame is already a real graph, not a flat line.
|
|
213
|
+
for (let i = 0; i < this.historyLimit; i++) this.update(0.1);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
get cores(): number {
|
|
217
|
+
return this.state.cpu.cores.length;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Advance the simulation by `dt` seconds. */
|
|
221
|
+
update(dt = 0.1): SystemSample {
|
|
222
|
+
this.tick++;
|
|
223
|
+
const t = this.tick * dt;
|
|
224
|
+
const s = this.state;
|
|
225
|
+
s.time = t;
|
|
226
|
+
|
|
227
|
+
// CPU: a slow wave, a fast wave, occasional spikes, per-core jitter.
|
|
228
|
+
const wave = 0.28 + Math.sin(t / 7) * 0.12 + Math.sin(t / 1.7) * 0.05;
|
|
229
|
+
const spike = this.random() < 0.02 ? this.random() * 0.5 : 0;
|
|
230
|
+
let coreSum = 0;
|
|
231
|
+
s.cpu.cores = s.cpu.cores.map((prev, i) => {
|
|
232
|
+
const target = Math.max(0.02, Math.min(1, wave + Math.sin(t / 3 + this.phase[i]) * 0.18 + spike + (this.random() - 0.5) * 0.1));
|
|
233
|
+
const next = prev + (target - prev) * 0.35;
|
|
234
|
+
coreSum += next;
|
|
235
|
+
return next;
|
|
236
|
+
});
|
|
237
|
+
s.cpu.total = coreSum / s.cpu.cores.length;
|
|
238
|
+
push(s.cpu.history, s.cpu.total * 100, this.historyLimit);
|
|
239
|
+
s.cpu.load = [
|
|
240
|
+
s.cpu.load[0] + (s.cpu.total * 4 - s.cpu.load[0]) * 0.02,
|
|
241
|
+
s.cpu.load[1] + (s.cpu.load[0] - s.cpu.load[1]) * 0.01,
|
|
242
|
+
s.cpu.load[2] + (s.cpu.load[1] - s.cpu.load[2]) * 0.005,
|
|
243
|
+
];
|
|
244
|
+
s.cpu.frequencyGhz = 1.6 + s.cpu.total * 2.4;
|
|
245
|
+
|
|
246
|
+
// Memory drifts slowly and follows CPU a little.
|
|
247
|
+
const memTarget = s.memory.total * (0.38 + s.cpu.total * 0.12 + Math.sin(t / 23) * 0.03);
|
|
248
|
+
s.memory.used += (memTarget - s.memory.used) * 0.05;
|
|
249
|
+
s.memory.cached = s.memory.total * (0.24 + Math.sin(t / 31) * 0.02);
|
|
250
|
+
s.memory.buffers = s.memory.total * 0.07;
|
|
251
|
+
s.memory.available = s.memory.total - s.memory.used;
|
|
252
|
+
s.memory.free = s.memory.total - s.memory.used - s.memory.cached - s.memory.buffers;
|
|
253
|
+
push(s.memory.history, (s.memory.used / s.memory.total) * 100, this.historyLimit);
|
|
254
|
+
s.memory.swapUsed = Math.max(0, s.memory.swapUsed + (this.random() - 0.5) * 1024 ** 2);
|
|
255
|
+
|
|
256
|
+
// Disks: bursty reads and writes.
|
|
257
|
+
for (const disk of s.disks) {
|
|
258
|
+
const burst = this.random() < 0.08 ? this.random() * 80e6 : 0;
|
|
259
|
+
const base = disk.type === "SSD" ? 24e6 : 3e6;
|
|
260
|
+
disk.readRate = Math.max(0, base * (0.5 + this.random()) + burst);
|
|
261
|
+
disk.writeRate = Math.max(0, base * 0.6 * (0.4 + this.random()) + burst * 0.4);
|
|
262
|
+
push(disk.readHistory, disk.readRate, this.historyLimit);
|
|
263
|
+
push(disk.writeHistory, disk.writeRate, this.historyLimit);
|
|
264
|
+
disk.used = Math.min(disk.total, disk.used + disk.writeRate * dt * 0.001);
|
|
265
|
+
disk.iops = [
|
|
266
|
+
Math.round(disk.readRate / 12000),
|
|
267
|
+
Math.round(disk.writeRate / 14000),
|
|
268
|
+
];
|
|
269
|
+
disk.temperature = 36 + (disk.type === "SSD" ? 8 : 2) + s.cpu.total * 6 + this.random();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Network: correlated bursts, download heavier than upload.
|
|
273
|
+
const burst = this.random() < 0.05 ? this.random() * 60e6 : 0;
|
|
274
|
+
s.network.downRate = Math.max(0, 6e6 + Math.sin(t / 4) * 3e6 + this.random() * 4e6 + burst);
|
|
275
|
+
s.network.upRate = Math.max(0, 1.6e6 + Math.sin(t / 6) * 1e6 + this.random() * 1.5e6 + burst * 0.3);
|
|
276
|
+
push(s.network.downHistory, s.network.downRate, this.historyLimit);
|
|
277
|
+
push(s.network.upHistory, s.network.upRate, this.historyLimit);
|
|
278
|
+
s.network.downTotal += s.network.downRate * dt;
|
|
279
|
+
s.network.upTotal += s.network.upRate * dt;
|
|
280
|
+
s.network.downPeak = Math.max(s.network.downPeak, s.network.downRate);
|
|
281
|
+
s.network.upPeak = Math.max(s.network.upPeak, s.network.upRate);
|
|
282
|
+
|
|
283
|
+
// Processes churn a little and re-sort by CPU.
|
|
284
|
+
if (s.processes.length === 0) {
|
|
285
|
+
s.processes = PROCESS_NAMES.map(([name, user, command], i) => ({
|
|
286
|
+
pid: 1000 + Math.floor(this.random() * 48000),
|
|
287
|
+
name,
|
|
288
|
+
cpu: this.random() * 30,
|
|
289
|
+
mem: this.random() * 8,
|
|
290
|
+
rss: this.random() * 400 * 1024 ** 2,
|
|
291
|
+
threads: 1 + Math.floor(this.random() * 30),
|
|
292
|
+
user,
|
|
293
|
+
command,
|
|
294
|
+
state: this.random() > 0.7 ? "R" : "S",
|
|
295
|
+
}));
|
|
296
|
+
}
|
|
297
|
+
for (const process of s.processes) {
|
|
298
|
+
process.cpu = Math.max(0, Math.min(100, process.cpu + (this.random() - 0.5) * 6));
|
|
299
|
+
process.mem = Math.max(0.1, Math.min(40, process.mem + (this.random() - 0.5) * 0.4));
|
|
300
|
+
process.rss = Math.max(4 * 1024 ** 2, process.rss + (this.random() - 0.5) * 8 * 1024 ** 2);
|
|
301
|
+
if (this.random() < 0.01) process.state = process.state === "R" ? "S" : "R";
|
|
302
|
+
}
|
|
303
|
+
s.processes.sort((a, b) => b.cpu - a.cpu);
|
|
304
|
+
|
|
305
|
+
// Temperatures lag CPU load rather than tracking it instantly.
|
|
306
|
+
const packageTemp = 42 + s.cpu.total * 30;
|
|
307
|
+
if (s.temperatures.length === 0) {
|
|
308
|
+
s.temperatures = [
|
|
309
|
+
{ label: "CPU Package", value: packageTemp, max: 100 },
|
|
310
|
+
...s.cpu.cores.slice(0, 6).map((_, i) => ({ label: `CPU Core #${i + 1}`, value: packageTemp, max: 100 })),
|
|
311
|
+
{ label: "GPU Package", value: 45, max: 100 },
|
|
312
|
+
{ label: "SSD (nvme0n1)", value: 43, max: 85 },
|
|
313
|
+
];
|
|
314
|
+
}
|
|
315
|
+
s.temperatures.forEach((entry, i) => {
|
|
316
|
+
const target = i === 0
|
|
317
|
+
? packageTemp
|
|
318
|
+
: entry.label.startsWith("CPU Core")
|
|
319
|
+
? 40 + s.cpu.cores[i - 1] * 26
|
|
320
|
+
: entry.label.startsWith("GPU")
|
|
321
|
+
? 42 + s.cpu.total * 14
|
|
322
|
+
: 40 + s.disks[0].temperature * 0.15;
|
|
323
|
+
entry.value += (target - entry.value) * 0.08;
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
s.sensors = [
|
|
327
|
+
{ label: "Fan Speed 1", value: `${Math.round(1800 + s.cpu.total * 1400)} RPM` },
|
|
328
|
+
{ label: "Fan Speed 2", value: `${Math.round(1700 + s.cpu.total * 1300)} RPM` },
|
|
329
|
+
{ label: "Battery", value: `${Math.round(96 + Math.sin(t / 60) * 3)}%` },
|
|
330
|
+
{ label: "CPU Voltage", value: `${(0.85 + s.cpu.total * 0.25).toFixed(2)} V` },
|
|
331
|
+
{ label: "CPU Power", value: `${(6 + s.cpu.total * 22).toFixed(1)} W` },
|
|
332
|
+
{ label: "GPU Power", value: `${(4 + s.cpu.total * 9).toFixed(1)} W` },
|
|
333
|
+
];
|
|
334
|
+
|
|
335
|
+
s.system.uptime += dt;
|
|
336
|
+
s.system.threadCount = 940 + Math.round(s.cpu.total * 120);
|
|
337
|
+
s.system.contextSwitches = Math.round(48000 + s.cpu.total * 20000);
|
|
338
|
+
|
|
339
|
+
// A new log line every so often.
|
|
340
|
+
if (this.tick % 12 === 0) {
|
|
341
|
+
const [level, message, meta] = LOG_TEMPLATES[Math.floor(this.random() * LOG_TEMPLATES.length)];
|
|
342
|
+
s.logs.push({ time: new Date().toTimeString().slice(0, 8), level, message, meta });
|
|
343
|
+
if (s.logs.length > 200) s.logs.shift();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return s;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
current(): SystemSample {
|
|
350
|
+
return this.state;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function createSystemSimulation(options: SimulationOptions = {}): SystemSimulation {
|
|
355
|
+
return new SystemSimulation(options);
|
|
356
|
+
}
|
package/src/state.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Theme } from "@profullstack/hqtui";
|
|
2
|
+
import type { SystemSample } from "./system/index.ts";
|
|
3
|
+
|
|
4
|
+
export type ScreenName = "dashboard" | "components" | "graphics" | "themes" | "input" | "stress";
|
|
5
|
+
|
|
6
|
+
export const SCREENS: ScreenName[] = ["dashboard", "components", "graphics", "themes", "input", "stress"];
|
|
7
|
+
|
|
8
|
+
export interface DemoState {
|
|
9
|
+
sample: SystemSample;
|
|
10
|
+
screen: ScreenName;
|
|
11
|
+
source: string;
|
|
12
|
+
unavailable: string[];
|
|
13
|
+
/** Selected process row. */
|
|
14
|
+
selected: number;
|
|
15
|
+
offset: number;
|
|
16
|
+
sort: "cpu" | "mem" | "pid" | "name";
|
|
17
|
+
filter: string;
|
|
18
|
+
filtering: boolean;
|
|
19
|
+
showHelp: boolean;
|
|
20
|
+
showPalette: boolean;
|
|
21
|
+
showModal: boolean;
|
|
22
|
+
paletteQuery: string;
|
|
23
|
+
paletteIndex: number;
|
|
24
|
+
themeIndex: number;
|
|
25
|
+
/** Component-showcase interactive state. */
|
|
26
|
+
toggle: boolean;
|
|
27
|
+
checkbox: boolean;
|
|
28
|
+
selectOpen: boolean;
|
|
29
|
+
selectIndex: number;
|
|
30
|
+
slider: number;
|
|
31
|
+
inputValue: string;
|
|
32
|
+
lastKey: string;
|
|
33
|
+
lastMouse: string;
|
|
34
|
+
keyLog: string[];
|
|
35
|
+
paused: boolean;
|
|
36
|
+
fps: number;
|
|
37
|
+
renderMs: number;
|
|
38
|
+
changedCells: number;
|
|
39
|
+
bytes: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createState(sample: SystemSample, source: string, unavailable: string[]): DemoState {
|
|
43
|
+
return {
|
|
44
|
+
sample,
|
|
45
|
+
screen: "dashboard",
|
|
46
|
+
source,
|
|
47
|
+
unavailable,
|
|
48
|
+
selected: 0,
|
|
49
|
+
offset: 0,
|
|
50
|
+
sort: "cpu",
|
|
51
|
+
filter: "",
|
|
52
|
+
filtering: false,
|
|
53
|
+
showHelp: false,
|
|
54
|
+
showPalette: false,
|
|
55
|
+
showModal: false,
|
|
56
|
+
paletteQuery: "",
|
|
57
|
+
paletteIndex: 0,
|
|
58
|
+
themeIndex: 0,
|
|
59
|
+
toggle: true,
|
|
60
|
+
checkbox: true,
|
|
61
|
+
selectOpen: false,
|
|
62
|
+
selectIndex: 0,
|
|
63
|
+
slider: 0.7,
|
|
64
|
+
inputValue: "",
|
|
65
|
+
lastKey: "—",
|
|
66
|
+
lastMouse: "—",
|
|
67
|
+
keyLog: [],
|
|
68
|
+
paused: false,
|
|
69
|
+
fps: 0,
|
|
70
|
+
renderMs: 0,
|
|
71
|
+
changedCells: 0,
|
|
72
|
+
bytes: 0,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import type { SystemSample } from "../simulation.ts";
|
|
5
|
+
|
|
6
|
+
const run = promisify(execFile);
|
|
7
|
+
|
|
8
|
+
/** Run a command, returning "" instead of throwing when it is unavailable. */
|
|
9
|
+
export async function sh(command: string, args: string[], timeout = 4000): Promise<string> {
|
|
10
|
+
try {
|
|
11
|
+
const { stdout } = await run(command, args, { timeout, maxBuffer: 8 * 1024 * 1024 });
|
|
12
|
+
return stdout;
|
|
13
|
+
} catch {
|
|
14
|
+
return "";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function push(history: number[], value: number, limit = 240): void {
|
|
19
|
+
history.push(value);
|
|
20
|
+
if (history.length > limit) history.shift();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function ratePerSecond(current: number, previous: number, dt: number): number {
|
|
24
|
+
if (previous <= 0 || dt <= 0 || current < previous) return 0;
|
|
25
|
+
return (current - previous) / dt;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** An empty sample pre-filled from `os`, so every platform starts consistent. */
|
|
29
|
+
export function baseSample(): SystemSample {
|
|
30
|
+
const cpus = os.cpus();
|
|
31
|
+
const total = os.totalmem();
|
|
32
|
+
return {
|
|
33
|
+
time: 0,
|
|
34
|
+
cpu: {
|
|
35
|
+
total: 0,
|
|
36
|
+
cores: new Array(Math.max(1, cpus.length)).fill(0),
|
|
37
|
+
history: [],
|
|
38
|
+
load: [0, 0, 0],
|
|
39
|
+
model: cpus[0]?.model?.trim() ?? "Unknown CPU",
|
|
40
|
+
frequencyGhz: (cpus[0]?.speed ?? 0) / 1000,
|
|
41
|
+
},
|
|
42
|
+
memory: {
|
|
43
|
+
total,
|
|
44
|
+
used: total - os.freemem(),
|
|
45
|
+
available: os.freemem(),
|
|
46
|
+
cached: 0,
|
|
47
|
+
buffers: 0,
|
|
48
|
+
free: os.freemem(),
|
|
49
|
+
history: [],
|
|
50
|
+
swapTotal: 0,
|
|
51
|
+
swapUsed: 0,
|
|
52
|
+
},
|
|
53
|
+
disks: [],
|
|
54
|
+
network: {
|
|
55
|
+
interface: "-",
|
|
56
|
+
speed: "-",
|
|
57
|
+
ip: "-",
|
|
58
|
+
mac: "-",
|
|
59
|
+
downRate: 0,
|
|
60
|
+
upRate: 0,
|
|
61
|
+
downHistory: [],
|
|
62
|
+
upHistory: [],
|
|
63
|
+
downTotal: 0,
|
|
64
|
+
upTotal: 0,
|
|
65
|
+
downPeak: 0,
|
|
66
|
+
upPeak: 0,
|
|
67
|
+
},
|
|
68
|
+
processes: [],
|
|
69
|
+
temperatures: [],
|
|
70
|
+
sensors: [],
|
|
71
|
+
system: {
|
|
72
|
+
os: `${os.type()} ${os.release()}`,
|
|
73
|
+
kernel: os.release(),
|
|
74
|
+
hostname: os.hostname(),
|
|
75
|
+
shell: (process.env.SHELL ?? process.env.ComSpec ?? "-").split("/").pop() ?? "-",
|
|
76
|
+
terminal: process.env.TERM_PROGRAM ?? process.env.TERM ?? "-",
|
|
77
|
+
uptime: os.uptime(),
|
|
78
|
+
processCount: 0,
|
|
79
|
+
threadCount: 0,
|
|
80
|
+
contextSwitches: 0,
|
|
81
|
+
},
|
|
82
|
+
logs: [],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Primary non-loopback interface, for the network panel header. */
|
|
87
|
+
export function primaryInterface(): { name: string; ip: string; mac: string } {
|
|
88
|
+
for (const [name, addresses] of Object.entries(os.networkInterfaces())) {
|
|
89
|
+
for (const address of addresses ?? []) {
|
|
90
|
+
if (address.family === "IPv4" && !address.internal) {
|
|
91
|
+
return { name, ip: address.address, mac: address.mac };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { name: "-", ip: "-", mac: "-" };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function loadAverage(): [number, number, number] {
|
|
99
|
+
const [a, b, c] = os.loadavg();
|
|
100
|
+
return [a, b, c];
|
|
101
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
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
|
+
/**
|
|
6
|
+
* macOS has no /proc, so everything comes from small command-line tools that
|
|
7
|
+
* ship with the OS. Nothing here needs sudo, so a few sensors are unavailable.
|
|
8
|
+
*/
|
|
9
|
+
export class DarwinCollector implements Collector {
|
|
10
|
+
source = "macOS sysctl";
|
|
11
|
+
unavailable = ["temperatures", "fan speed"];
|
|
12
|
+
private sample = baseSample();
|
|
13
|
+
private prevCpu: { idle: number; total: number } | null = null;
|
|
14
|
+
private prevNet: [number, number] | null = null;
|
|
15
|
+
private prevDisk: [number, number] | null = null;
|
|
16
|
+
private staticLoaded = false;
|
|
17
|
+
|
|
18
|
+
async refresh(dt: number): Promise<void> {
|
|
19
|
+
const s = this.sample;
|
|
20
|
+
if (!this.staticLoaded) await this.loadStatic();
|
|
21
|
+
|
|
22
|
+
// Overall CPU from top; per-core is not exposed without extra tooling,
|
|
23
|
+
// so cores are derived from the load spread across them.
|
|
24
|
+
const top = await sh("top", ["-l", "1", "-n", "0", "-stats", "cpu"]);
|
|
25
|
+
const cpuLine = /CPU usage:\s+([\d.]+)% user,\s+([\d.]+)% sys,\s+([\d.]+)% idle/.exec(top);
|
|
26
|
+
if (cpuLine) {
|
|
27
|
+
s.cpu.total = Math.max(0, Math.min(1, (Number(cpuLine[1]) + Number(cpuLine[2])) / 100));
|
|
28
|
+
}
|
|
29
|
+
const load = loadAverage();
|
|
30
|
+
s.cpu.load = load;
|
|
31
|
+
const cores = Math.max(1, os.cpus().length);
|
|
32
|
+
s.cpu.cores = Array.from({ length: cores }, (_, i) => {
|
|
33
|
+
// Spread total load across cores with a stable per-core offset.
|
|
34
|
+
const jitter = ((i * 37) % 17) / 100;
|
|
35
|
+
return Math.max(0, Math.min(1, s.cpu.total + jitter - 0.08));
|
|
36
|
+
});
|
|
37
|
+
push(s.cpu.history, s.cpu.total * 100);
|
|
38
|
+
|
|
39
|
+
// Memory via vm_stat page counts.
|
|
40
|
+
const vm = await sh("vm_stat", []);
|
|
41
|
+
const pageSize = Number(/page size of (\d+) bytes/.exec(vm)?.[1] ?? 4096);
|
|
42
|
+
const pages = (name: string): number =>
|
|
43
|
+
Number(new RegExp(`${name}:\\s+(\\d+)`).exec(vm)?.[1] ?? 0) * pageSize;
|
|
44
|
+
const free = pages("Pages free");
|
|
45
|
+
const inactive = pages("Pages inactive");
|
|
46
|
+
const wired = pages("Pages wired down");
|
|
47
|
+
const compressed = pages("Pages occupied by compressor");
|
|
48
|
+
const cached = pages("File-backed pages");
|
|
49
|
+
if (pageSize > 0 && (free || wired)) {
|
|
50
|
+
s.memory.total = os.totalmem();
|
|
51
|
+
s.memory.free = free;
|
|
52
|
+
s.memory.available = free + inactive;
|
|
53
|
+
s.memory.cached = cached;
|
|
54
|
+
s.memory.used = s.memory.total - s.memory.available;
|
|
55
|
+
s.memory.buffers = compressed;
|
|
56
|
+
}
|
|
57
|
+
push(s.memory.history, (s.memory.used / Math.max(1, s.memory.total)) * 100);
|
|
58
|
+
|
|
59
|
+
const swap = await sh("sysctl", ["-n", "vm.swapusage"]);
|
|
60
|
+
const swapMatch = /total = ([\d.]+)M.*used = ([\d.]+)M/.exec(swap);
|
|
61
|
+
if (swapMatch) {
|
|
62
|
+
s.memory.swapTotal = Number(swapMatch[1]) * 1024 ** 2;
|
|
63
|
+
s.memory.swapUsed = Number(swapMatch[2]) * 1024 ** 2;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
s.system.uptime = os.uptime();
|
|
67
|
+
|
|
68
|
+
// Disk capacity and throughput.
|
|
69
|
+
const df = await sh("df", ["-k", "/"]);
|
|
70
|
+
const dfLine = df.trim().split("\n")[1]?.trim().split(/\s+/);
|
|
71
|
+
if (dfLine) {
|
|
72
|
+
if (s.disks.length === 0) {
|
|
73
|
+
s.disks.push({
|
|
74
|
+
device: dfLine[0].replace("/dev/", ""), mount: "/", type: "SSD",
|
|
75
|
+
total: 0, used: 0, readRate: 0, writeRate: 0,
|
|
76
|
+
readHistory: [], writeHistory: [], iops: [0, 0], temperature: 0,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
s.disks[0].total = Number(dfLine[1]) * 1024;
|
|
80
|
+
s.disks[0].used = Number(dfLine[2]) * 1024;
|
|
81
|
+
}
|
|
82
|
+
const iostat = await sh("iostat", ["-d", "-c", "1"]);
|
|
83
|
+
const ioLine = iostat.trim().split("\n").pop()?.trim().split(/\s+/);
|
|
84
|
+
if (ioLine && s.disks[0] && ioLine.length >= 3) {
|
|
85
|
+
const mbPerSecond = Number(ioLine[2]) || 0;
|
|
86
|
+
s.disks[0].readRate = (mbPerSecond * 1024 ** 2) / 2;
|
|
87
|
+
s.disks[0].writeRate = (mbPerSecond * 1024 ** 2) / 2;
|
|
88
|
+
}
|
|
89
|
+
for (const disk of s.disks) {
|
|
90
|
+
push(disk.readHistory, disk.readRate);
|
|
91
|
+
push(disk.writeHistory, disk.writeRate);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Network counters from netstat.
|
|
95
|
+
const iface = primaryInterface();
|
|
96
|
+
s.network.interface = iface.name;
|
|
97
|
+
s.network.ip = iface.ip;
|
|
98
|
+
s.network.mac = iface.mac;
|
|
99
|
+
const netstat = await sh("netstat", ["-ibn"]);
|
|
100
|
+
for (const line of netstat.split("\n")) {
|
|
101
|
+
const parts = line.trim().split(/\s+/);
|
|
102
|
+
if (parts[0] !== iface.name || parts.length < 10) continue;
|
|
103
|
+
const inBytes = Number(parts[6]);
|
|
104
|
+
const outBytes = Number(parts[9]);
|
|
105
|
+
if (!Number.isFinite(inBytes) || !Number.isFinite(outBytes)) continue;
|
|
106
|
+
if (this.prevNet) {
|
|
107
|
+
s.network.downRate = ratePerSecond(inBytes, this.prevNet[0], dt);
|
|
108
|
+
s.network.upRate = ratePerSecond(outBytes, this.prevNet[1], dt);
|
|
109
|
+
}
|
|
110
|
+
s.network.downTotal = inBytes;
|
|
111
|
+
s.network.upTotal = outBytes;
|
|
112
|
+
this.prevNet = [inBytes, outBytes];
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
push(s.network.downHistory, s.network.downRate);
|
|
116
|
+
push(s.network.upHistory, s.network.upRate);
|
|
117
|
+
s.network.downPeak = Math.max(s.network.downPeak, s.network.downRate);
|
|
118
|
+
s.network.upPeak = Math.max(s.network.upPeak, s.network.upRate);
|
|
119
|
+
|
|
120
|
+
await this.updateProcesses();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private async loadStatic(): Promise<void> {
|
|
124
|
+
this.staticLoaded = true;
|
|
125
|
+
const s = this.sample;
|
|
126
|
+
const [model, product, version] = await Promise.all([
|
|
127
|
+
sh("sysctl", ["-n", "machdep.cpu.brand_string"]),
|
|
128
|
+
sh("sw_vers", ["-productName"]),
|
|
129
|
+
sh("sw_vers", ["-productVersion"]),
|
|
130
|
+
]);
|
|
131
|
+
if (model.trim()) s.cpu.model = model.trim();
|
|
132
|
+
if (product.trim()) s.system.os = `${product.trim()} ${version.trim()}`.trim();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private async updateProcesses(): Promise<void> {
|
|
136
|
+
const text = await sh("ps", ["-Ao", "pid,comm,pcpu,pmem,rss,user,state,args", "-r"]);
|
|
137
|
+
if (!text) return;
|
|
138
|
+
const lines = text.trim().split("\n").slice(1, 60);
|
|
139
|
+
this.sample.processes = lines.map((line) => {
|
|
140
|
+
const parts = line.trim().split(/\s+/);
|
|
141
|
+
const name = (parts[1] ?? "-").split("/").pop() ?? "-";
|
|
142
|
+
return {
|
|
143
|
+
pid: Number(parts[0]),
|
|
144
|
+
name,
|
|
145
|
+
cpu: Number(parts[2]) || 0,
|
|
146
|
+
mem: Number(parts[3]) || 0,
|
|
147
|
+
rss: (Number(parts[4]) || 0) * 1024,
|
|
148
|
+
threads: 1,
|
|
149
|
+
user: parts[5] ?? "-",
|
|
150
|
+
state: parts[6] ?? "-",
|
|
151
|
+
command: parts.slice(7).join(" "),
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
this.sample.system.processCount = this.sample.processes.length;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
current(): SystemSample {
|
|
158
|
+
return this.sample;
|
|
159
|
+
}
|
|
160
|
+
}
|