pyre-cli 1.3.0 → 2.0.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 +278 -17
- package/dist/index.js +1763 -350
- package/package.json +6 -3
- package/parity.md +542 -0
- package/src/formatters/index.ts +17 -0
- package/src/formatters/output.ts +165 -0
- package/src/formatters/render.ts +659 -0
- package/src/formatters/themes.ts +115 -0
- package/src/formatters/types.ts +62 -0
- package/src/history.ts +153 -0
- package/src/index.ts +201 -0
- package/src/live/export.ts +76 -0
- package/src/live/index.ts +16 -0
- package/src/live/input.ts +443 -0
- package/src/live/render.ts +146 -0
- package/src/live/state.ts +103 -0
- package/src/live/types.ts +23 -0
- package/src/monitors/collectors.ts +687 -0
- package/src/monitors/index.ts +14 -0
- package/src/monitors/run.ts +18 -0
- package/src/monitors/smc.ts +107 -0
- package/src/monitors/types.ts +148 -0
- package/src/p2p/client.ts +197 -0
- package/src/p2p/index.ts +3 -0
- package/src/p2p/protocol.ts +71 -0
- package/src/p2p/server.ts +219 -0
- package/src/p2p/types.ts +71 -0
- package/src/pyre_img +0 -0
- package/src/sparkline.ts +34 -0
- package/src/splash.ts +102 -0
- package/tsconfig.json +19 -0
package/dist/index.js
CHANGED
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import fs2 from "fs";
|
|
5
|
+
import readline2 from "readline";
|
|
5
6
|
import { Command } from "commander";
|
|
6
|
-
import
|
|
7
|
+
import chalk8 from "chalk";
|
|
7
8
|
|
|
8
|
-
// src/monitors.ts
|
|
9
|
+
// src/monitors/collectors.ts
|
|
9
10
|
import chalk from "chalk";
|
|
11
|
+
|
|
12
|
+
// src/monitors/run.ts
|
|
10
13
|
async function run(cmd, fallback = "") {
|
|
11
14
|
try {
|
|
12
15
|
const { execSync } = await import("child_process");
|
|
@@ -15,6 +18,8 @@ async function run(cmd, fallback = "") {
|
|
|
15
18
|
return fallback;
|
|
16
19
|
}
|
|
17
20
|
}
|
|
21
|
+
|
|
22
|
+
// src/monitors/smc.ts
|
|
18
23
|
var smcCache = null;
|
|
19
24
|
async function getSmcMetrics() {
|
|
20
25
|
const now = Date.now();
|
|
@@ -53,17 +58,53 @@ async function getSmcMetrics() {
|
|
|
53
58
|
function parseSuffix(s) {
|
|
54
59
|
return s.toUpperCase() === "K" ? 1024 : s.toUpperCase() === "M" ? 1024 * 1024 : s.toUpperCase() === "G" ? 1024 * 1024 * 1024 : 1;
|
|
55
60
|
}
|
|
61
|
+
|
|
62
|
+
// src/monitors/collectors.ts
|
|
63
|
+
var BATTERY_WINDOW_MS = 15 * 60 * 1e3;
|
|
64
|
+
var MIN_ESTIMATE_WINDOW_MS = 30 * 1e3;
|
|
65
|
+
var prevBattery = { samples: [] };
|
|
66
|
+
function estimateBatteryLife(level, state2) {
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
if (state2 === "charged" || state2 === "charging") {
|
|
69
|
+
prevBattery.samples = [];
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
prevBattery.samples.push({ level, ts: now });
|
|
73
|
+
prevBattery.samples = prevBattery.samples.filter((s) => now - s.ts <= BATTERY_WINDOW_MS);
|
|
74
|
+
const first = prevBattery.samples[0];
|
|
75
|
+
const elapsedMs = now - first.ts;
|
|
76
|
+
if (elapsedMs < MIN_ESTIMATE_WINDOW_MS) {
|
|
77
|
+
return {};
|
|
78
|
+
}
|
|
79
|
+
const dtHours = Math.max(elapsedMs / 1e3 / 3600, 1e-4);
|
|
80
|
+
const discharged = first.level - level;
|
|
81
|
+
if (discharged <= 0) {
|
|
82
|
+
return { estimatedTimeToEmpty: "calculating" };
|
|
83
|
+
}
|
|
84
|
+
const dischargeRate = discharged / dtHours;
|
|
85
|
+
const remainingLevel = Math.max(level, 0);
|
|
86
|
+
const hoursToEmpty = remainingLevel / dischargeRate;
|
|
87
|
+
const h = Math.floor(hoursToEmpty);
|
|
88
|
+
const m = Math.round((hoursToEmpty - h) * 60);
|
|
89
|
+
const estimatedTimeToEmpty = hoursToEmpty > 0 ? `${h}h ${m}m` : "calculating";
|
|
90
|
+
const ASSUMED_CAPACITY_WH = 55;
|
|
91
|
+
const powerWatts = Math.round(dischargeRate * (ASSUMED_CAPACITY_WH / 100) * 100) / 100;
|
|
92
|
+
return { estimatedTimeToEmpty, dischargeRatePerHour: Math.round(dischargeRate * 10) / 10, powerWatts };
|
|
93
|
+
}
|
|
56
94
|
async function collectAll(opts2 = {}) {
|
|
57
|
-
const [cpu, memory, disk, battery, thermal, network, processes, system, power] = await Promise.all([
|
|
95
|
+
const [cpu, gpu, memory, disk, battery, thermal, network, processes, system, power, packets, tasks] = await Promise.all([
|
|
58
96
|
collectCpu(),
|
|
97
|
+
collectGpu(opts2.detailed),
|
|
59
98
|
collectMemory(),
|
|
60
99
|
collectDisk(),
|
|
61
100
|
collectBattery(),
|
|
62
101
|
collectThermal(opts2.detailed),
|
|
63
102
|
collectNetwork(),
|
|
64
|
-
collectProcesses(opts2.processLimit
|
|
103
|
+
collectProcesses(opts2.processLimit),
|
|
65
104
|
collectSystem(),
|
|
66
|
-
collectPower()
|
|
105
|
+
collectPower(),
|
|
106
|
+
collectPackets(),
|
|
107
|
+
collectTasks()
|
|
67
108
|
]);
|
|
68
109
|
return {
|
|
69
110
|
header: {
|
|
@@ -73,6 +114,7 @@ async function collectAll(opts2 = {}) {
|
|
|
73
114
|
uptime: system.uptime
|
|
74
115
|
},
|
|
75
116
|
cpu,
|
|
117
|
+
gpu,
|
|
76
118
|
memory,
|
|
77
119
|
disk,
|
|
78
120
|
battery,
|
|
@@ -80,14 +122,79 @@ async function collectAll(opts2 = {}) {
|
|
|
80
122
|
network,
|
|
81
123
|
processes,
|
|
82
124
|
power,
|
|
83
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
125
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
126
|
+
packets,
|
|
127
|
+
tasks
|
|
84
128
|
};
|
|
85
129
|
}
|
|
86
130
|
async function collectPower() {
|
|
87
131
|
try {
|
|
88
132
|
const { power } = await getSmcMetrics();
|
|
89
|
-
if (power.cpu
|
|
90
|
-
|
|
133
|
+
if (power.cpu !== void 0 || power.gpu !== void 0 || power.combined !== void 0) {
|
|
134
|
+
return { cpuWatts: power.cpu, gpuWatts: power.gpu, combinedWatts: power.combined };
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
const ioreg = (await run("ioreg -rn AppleSmartBattery 2>/dev/null")).trim();
|
|
140
|
+
const ampMatch = ioreg.match(/"InstantAmperage"\s*=\s*(-?\d+)/);
|
|
141
|
+
const voltMatch = ioreg.match(/"Voltage"\s*=\s*(\d+)/);
|
|
142
|
+
if (ampMatch && voltMatch) {
|
|
143
|
+
let ampsBig = BigInt(ampMatch[1]);
|
|
144
|
+
if (!ampMatch[1].startsWith("-")) {
|
|
145
|
+
const UINT64_MAX_PLUS_1 = 1n << 64n;
|
|
146
|
+
const INT64_MAX = (1n << 63n) - 1n;
|
|
147
|
+
if (ampsBig > INT64_MAX) ampsBig -= UINT64_MAX_PLUS_1;
|
|
148
|
+
}
|
|
149
|
+
const amps = Number(ampsBig);
|
|
150
|
+
const volts = parseInt(voltMatch[1], 10) / 1e3;
|
|
151
|
+
const watts = Math.abs(amps / 1e3 * volts);
|
|
152
|
+
if (Number.isFinite(watts) && watts < 500) {
|
|
153
|
+
return { combinedWatts: Math.round(watts * 100) / 100 };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
async function collectGpu(detailed) {
|
|
161
|
+
try {
|
|
162
|
+
const spRaw = (await run("system_profiler SPDisplaysDataType 2>/dev/null")).trim();
|
|
163
|
+
if (!spRaw) return null;
|
|
164
|
+
const modelMatch = spRaw.match(/Chipset Model:\s*(.+)/i);
|
|
165
|
+
const model = modelMatch ? modelMatch[1].trim() : "Unknown";
|
|
166
|
+
const memMatch = spRaw.match(/VRAM \(Total\):\s*(.+)/i);
|
|
167
|
+
let memory = 0;
|
|
168
|
+
if (memMatch) {
|
|
169
|
+
const memStr = memMatch[1].trim();
|
|
170
|
+
const memNum = parseFloat(memStr);
|
|
171
|
+
if (memStr.includes("GB")) memory = memNum * 1024 * 1024 * 1024;
|
|
172
|
+
else if (memStr.includes("MB")) memory = memNum * 1024 * 1024;
|
|
173
|
+
}
|
|
174
|
+
let utilization = 0;
|
|
175
|
+
let temperature;
|
|
176
|
+
let gpuProcesses = 0;
|
|
177
|
+
if (detailed) {
|
|
178
|
+
try {
|
|
179
|
+
const pm = (await run("sudo -n powermetrics --samplers gpu_power -n 1 --format text 2>/dev/null", "")).trim();
|
|
180
|
+
const gpuPowerMatch = pm.match(/GPU Power:\s*([\d.]+)\s*mW/i);
|
|
181
|
+
if (gpuPowerMatch) {
|
|
182
|
+
utilization = Math.min(100, Math.round(parseFloat(gpuPowerMatch[1]) / 10));
|
|
183
|
+
}
|
|
184
|
+
} catch {
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
const { temps } = await getSmcMetrics();
|
|
188
|
+
if (temps.gpu_die !== void 0) temperature = temps.gpu_die;
|
|
189
|
+
} catch {
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
const psRaw = (await run('ps aux | grep -i "[g]pu" | wc -l')).trim();
|
|
193
|
+
gpuProcesses = parseInt(psRaw) || 0;
|
|
194
|
+
} catch {
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return { model, memory, utilization, temperature, processes: gpuProcesses };
|
|
91
198
|
} catch {
|
|
92
199
|
return null;
|
|
93
200
|
}
|
|
@@ -113,7 +220,9 @@ async function collectCpu() {
|
|
|
113
220
|
try {
|
|
114
221
|
const { freq } = await getSmcMetrics();
|
|
115
222
|
const values = Object.values(freq);
|
|
116
|
-
if (values.length)
|
|
223
|
+
if (values.length) {
|
|
224
|
+
frequency = Math.round(Math.max(...values) / 1e6);
|
|
225
|
+
}
|
|
117
226
|
} catch {
|
|
118
227
|
}
|
|
119
228
|
}
|
|
@@ -200,10 +309,10 @@ async function collectBattery() {
|
|
|
200
309
|
if (!raw.includes("Battery Power") && !raw.includes("AC Power")) return null;
|
|
201
310
|
const levelMatch = raw.match(/(\d+)%/);
|
|
202
311
|
const stateMatch = raw.match(/;\s*(charging|discharging|charged|finishing charge|attached to charger)/i);
|
|
203
|
-
const timeMatch = raw.match(/:\s*([\d:]+)\s+remaining/i);
|
|
312
|
+
const timeMatch = raw.match(/:\s*([\d:]+)\s+remaining/i) || raw.match(/(\d+:\d+)\s+remaining/i);
|
|
204
313
|
const powerPlugged = raw.includes("AC Power") || raw.includes("attached to charger");
|
|
205
314
|
const level = levelMatch ? parseInt(levelMatch[1]) : 0;
|
|
206
|
-
const
|
|
315
|
+
const state2 = powerPlugged ? "charged" : stateMatch ? stateMatch[1].toLowerCase() : "discharging";
|
|
207
316
|
const timeRemaining = timeMatch ? timeMatch[1] : powerPlugged ? "\u221E" : "calculating";
|
|
208
317
|
let cycles;
|
|
209
318
|
let condition;
|
|
@@ -233,50 +342,52 @@ async function collectBattery() {
|
|
|
233
342
|
if (maxCapacityPercent !== void 0) healthParts.push(`${maxCapacityPercent}% capacity`);
|
|
234
343
|
if (cycles !== void 0) healthParts.push(`${cycles} cycles`);
|
|
235
344
|
const health = healthParts.length ? healthParts.join(", ") : "unknown";
|
|
345
|
+
const estimate = estimateBatteryLife(level, state2);
|
|
236
346
|
return {
|
|
237
347
|
level,
|
|
238
|
-
state,
|
|
348
|
+
state: state2,
|
|
239
349
|
timeRemaining,
|
|
240
350
|
health,
|
|
241
351
|
powerSource: powerPlugged ? "AC" : "Battery",
|
|
242
352
|
cycles,
|
|
243
353
|
condition,
|
|
244
|
-
maxCapacityPercent
|
|
354
|
+
maxCapacityPercent,
|
|
355
|
+
...estimate
|
|
245
356
|
};
|
|
246
357
|
} catch {
|
|
247
358
|
return null;
|
|
248
359
|
}
|
|
249
360
|
}
|
|
250
|
-
function pressureFromState(
|
|
251
|
-
const s =
|
|
361
|
+
function pressureFromState(state2) {
|
|
362
|
+
const s = state2.toLowerCase();
|
|
252
363
|
if (s.includes("critical")) return 3;
|
|
253
364
|
if (s.includes("serious") || s.includes("heavy")) return 2;
|
|
254
365
|
if (s.includes("fair") || s.includes("moderate")) return 1;
|
|
255
366
|
return 0;
|
|
256
367
|
}
|
|
257
|
-
async function collectThermal(
|
|
368
|
+
async function collectThermal(detailed) {
|
|
258
369
|
try {
|
|
259
370
|
const therm = (await run("pmset -g therm", "")).trim();
|
|
260
|
-
let
|
|
371
|
+
let state2 = "Unknown";
|
|
261
372
|
let detail = therm || "No pmset thermal data";
|
|
262
373
|
const stateMatch = therm.match(/Thermal state:\s*(.+)/i);
|
|
263
374
|
if (stateMatch) {
|
|
264
375
|
const raw = stateMatch[1].trim();
|
|
265
|
-
|
|
266
|
-
detail =
|
|
376
|
+
state2 = /0|no warning|normal/i.test(raw) ? "Nominal" : raw;
|
|
377
|
+
detail = state2;
|
|
267
378
|
} else {
|
|
268
379
|
const limitMatch = therm.match(
|
|
269
380
|
/CPU_(?:Speed|Scheduler)_Limit\s*(?:=|:)\s*([\d.]+)/i
|
|
270
381
|
);
|
|
271
382
|
if (limitMatch) {
|
|
272
383
|
const limit = parseFloat(limitMatch[1]);
|
|
273
|
-
if (limit >= 100)
|
|
274
|
-
else if (limit >= 85)
|
|
275
|
-
else if (limit >= 50)
|
|
276
|
-
else
|
|
277
|
-
detail = `${
|
|
384
|
+
if (limit >= 100) state2 = "Nominal";
|
|
385
|
+
else if (limit >= 85) state2 = "Fair";
|
|
386
|
+
else if (limit >= 50) state2 = "Serious";
|
|
387
|
+
else state2 = "Critical";
|
|
388
|
+
detail = `${state2} (CPU limit ${limit}%)`;
|
|
278
389
|
} else if (/no thermal warning level has been recorded/i.test(therm)) {
|
|
279
|
-
|
|
390
|
+
state2 = "Nominal";
|
|
280
391
|
detail = "Nominal (no thermal warning recorded)";
|
|
281
392
|
}
|
|
282
393
|
}
|
|
@@ -285,7 +396,7 @@ async function collectThermal(detailed2) {
|
|
|
285
396
|
try {
|
|
286
397
|
const { temps } = await getSmcMetrics();
|
|
287
398
|
cpuDieTemp = temps.cpu_die;
|
|
288
|
-
if (
|
|
399
|
+
if (detailed) {
|
|
289
400
|
if (temps.cpu_die !== void 0) temperatures["cpu_die"] = temps.cpu_die;
|
|
290
401
|
if (temps.gpu_die !== void 0) temperatures["gpu_die"] = temps.gpu_die;
|
|
291
402
|
if (temps.smc_die !== void 0) temperatures["smc_die"] = temps.smc_die;
|
|
@@ -294,19 +405,19 @@ async function collectThermal(detailed2) {
|
|
|
294
405
|
}
|
|
295
406
|
}
|
|
296
407
|
} catch {
|
|
297
|
-
if (
|
|
408
|
+
if (detailed) detail += " (powermetrics unavailable)";
|
|
298
409
|
}
|
|
299
|
-
if (
|
|
300
|
-
if (cpuDieTemp >= 100)
|
|
301
|
-
else if (cpuDieTemp >= 90)
|
|
302
|
-
else if (cpuDieTemp >= 80)
|
|
303
|
-
else
|
|
304
|
-
detail = `${
|
|
410
|
+
if (state2 === "Unknown" && cpuDieTemp !== void 0) {
|
|
411
|
+
if (cpuDieTemp >= 100) state2 = "Critical";
|
|
412
|
+
else if (cpuDieTemp >= 90) state2 = "Serious";
|
|
413
|
+
else if (cpuDieTemp >= 80) state2 = "Fair";
|
|
414
|
+
else state2 = "Nominal";
|
|
415
|
+
detail = `${state2} (estimated from ${cpuDieTemp}\xB0C)`;
|
|
305
416
|
}
|
|
306
417
|
return {
|
|
307
|
-
state,
|
|
418
|
+
state: state2,
|
|
308
419
|
detail,
|
|
309
|
-
pressureLevel: pressureFromState(
|
|
420
|
+
pressureLevel: pressureFromState(state2),
|
|
310
421
|
temperatures: Object.keys(temperatures).length ? temperatures : void 0
|
|
311
422
|
};
|
|
312
423
|
} catch {
|
|
@@ -346,43 +457,125 @@ async function collectNetwork() {
|
|
|
346
457
|
return { interface: "unknown", ip: "0.0.0.0", rxBytes: 0, txBytes: 0, rxPackets: 0, txPackets: 0 };
|
|
347
458
|
}
|
|
348
459
|
}
|
|
349
|
-
async function collectProcesses(limit
|
|
460
|
+
async function collectProcesses(limit) {
|
|
350
461
|
try {
|
|
351
|
-
const
|
|
462
|
+
const isMac = process.platform === "darwin";
|
|
463
|
+
const sortArg = isMac ? "-r" : "--sort=-pcpu";
|
|
464
|
+
const threadsArg = isMac ? "threads" : "nlwp";
|
|
465
|
+
const headClause = limit && limit > 0 ? ` | head -n ${limit + 1}` : "";
|
|
466
|
+
const raw = (await run(`ps -eo pid,ppid,user,pcpu,pmem,state,${threadsArg},time,comm ${sortArg}${headClause}`)).trim();
|
|
352
467
|
const lines = raw.split("\n").slice(1);
|
|
353
468
|
return lines.map((line) => {
|
|
354
|
-
const parts = line.match(/\s*(\d+)\s+(\S+)\s+([\d.]+)\s+([\d.]+)\s+(.+)/);
|
|
469
|
+
const parts = line.match(/\s*(\d+)\s+(\d+)\s+(\S+)\s+([\d.]+)\s+([\d.]+)\s+(\S+)\s+(\d+)\s+([\d:]+)\s+(.+)/);
|
|
355
470
|
if (!parts) return null;
|
|
356
|
-
|
|
471
|
+
const runtimeSec = parts[8].split(":").reduce((acc, val, idx) => acc + parseInt(val) * Math.pow(60, 2 - idx), 0);
|
|
472
|
+
return { pid: parseInt(parts[1]), ppid: parseInt(parts[2]), user: parts[3], cpu: parseFloat(parts[4]), mem: parseFloat(parts[5]), state: parts[6], threads: parseInt(parts[7]), runtime: runtimeSec, command: parts[9] };
|
|
357
473
|
}).filter((p) => p !== null && p.pid > 0);
|
|
358
474
|
} catch {
|
|
359
475
|
return [];
|
|
360
476
|
}
|
|
361
477
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
478
|
+
async function collectPackets() {
|
|
479
|
+
try {
|
|
480
|
+
const netStat = (await run("netstat -ib")).trim();
|
|
481
|
+
const lines = netStat.split("\n").slice(1);
|
|
482
|
+
let totalRxPackets = 0;
|
|
483
|
+
let totalTxPackets = 0;
|
|
484
|
+
let totalRxBytes = 0;
|
|
485
|
+
let totalTxBytes = 0;
|
|
486
|
+
const ifaceStats = [];
|
|
487
|
+
for (const line of lines) {
|
|
488
|
+
const parts = line.split(/\s+/);
|
|
489
|
+
if (parts.length >= 11 && parts[0] && !parts[0].startsWith("Name")) {
|
|
490
|
+
const iface = parts[0];
|
|
491
|
+
const rxPackets = parseInt(parts[4]) || 0;
|
|
492
|
+
const rxBytes = parseInt(parts[5]) || 0;
|
|
493
|
+
const txPackets = parseInt(parts[6]) || 0;
|
|
494
|
+
const txBytes = parseInt(parts[7]) || 0;
|
|
495
|
+
totalRxPackets += rxPackets;
|
|
496
|
+
totalTxPackets += txPackets;
|
|
497
|
+
totalRxBytes += rxBytes;
|
|
498
|
+
totalTxBytes += txBytes;
|
|
499
|
+
if (iface && !iface.includes("lo")) {
|
|
500
|
+
ifaceStats.push({ iface, rxPackets, txPackets, rxBytes, txBytes });
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
let connections = 0;
|
|
505
|
+
try {
|
|
506
|
+
const tcpConns = (await run("netstat -an | grep ESTABLISHED | wc -l", "0")).trim();
|
|
507
|
+
connections = parseInt(tcpConns) || 0;
|
|
508
|
+
} catch {
|
|
509
|
+
connections = 0;
|
|
510
|
+
}
|
|
511
|
+
let topProcesses = [];
|
|
512
|
+
let allProcesses = [];
|
|
513
|
+
try {
|
|
514
|
+
const lsof = (await run("lsof -i -n -P -r 1 2>/dev/null | tail -n +2 | awk '{print $1, $2}' | sort | uniq -c | sort -rn", "")).trim();
|
|
515
|
+
const lsofLines = lsof.split("\n");
|
|
516
|
+
topProcesses = lsofLines.map((line) => {
|
|
517
|
+
const parts = line.trim().split(/\s+/);
|
|
518
|
+
if (parts.length >= 3) {
|
|
519
|
+
return { pid: parseInt(parts[2]) || 0, command: parts[1], rxBytes: 0, txBytes: parseInt(parts[0]) || 0 };
|
|
520
|
+
}
|
|
521
|
+
return { pid: 0, command: "", rxBytes: 0, txBytes: 0 };
|
|
522
|
+
}).filter((p) => p.pid > 0);
|
|
523
|
+
const allLsof = (await run("lsof -i -n -P 2>/dev/null | tail -n +2 | awk '{print $1, $2}' | sort | uniq -c | sort -rn", "")).trim();
|
|
524
|
+
const allLsofLines = allLsof.split("\n");
|
|
525
|
+
allProcesses = allLsofLines.map((line) => {
|
|
526
|
+
const parts = line.trim().split(/\s+/);
|
|
527
|
+
if (parts.length >= 3) {
|
|
528
|
+
return { pid: parseInt(parts[2]) || 0, command: parts[1], rxBytes: 0, txBytes: parseInt(parts[0]) || 0 };
|
|
529
|
+
}
|
|
530
|
+
return { pid: 0, command: "", rxBytes: 0, txBytes: 0 };
|
|
531
|
+
}).filter((p) => p.pid > 0);
|
|
532
|
+
} catch {
|
|
533
|
+
topProcesses = [];
|
|
534
|
+
allProcesses = [];
|
|
535
|
+
}
|
|
536
|
+
return {
|
|
537
|
+
totalPackets: totalRxPackets + totalTxPackets,
|
|
538
|
+
rxPackets: totalRxPackets,
|
|
539
|
+
txPackets: totalTxPackets,
|
|
540
|
+
rxRate: 0,
|
|
541
|
+
txRate: 0,
|
|
542
|
+
connections,
|
|
543
|
+
topProcesses,
|
|
544
|
+
interfaces: ifaceStats,
|
|
545
|
+
allProcesses
|
|
546
|
+
};
|
|
547
|
+
} catch {
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
async function collectTasks(limit = 12) {
|
|
552
|
+
try {
|
|
553
|
+
const isMac = process.platform === "darwin";
|
|
554
|
+
const sortArg = isMac ? "-r" : "--sort=-pcpu";
|
|
555
|
+
const raw = (await run(`ps -eo pid,user,pcpu,pmem,state,time,comm ${sortArg} | head -n ${limit + 1}`)).trim();
|
|
556
|
+
const lines = raw.split("\n").slice(1);
|
|
557
|
+
return lines.map((line) => {
|
|
558
|
+
const parts = line.match(/\s*(\d+)\s+(\S+)\s+([\d.]+)\s+([\d.]+)\s+(\S+)\s+([\d:]+)\s+(.+)/);
|
|
559
|
+
if (!parts) return null;
|
|
560
|
+
const runtimeSec = parts[6].split(":").reduce((acc, val, idx) => acc + parseInt(val) * Math.pow(60, 2 - idx), 0);
|
|
561
|
+
return { pid: parseInt(parts[1]), user: parts[2], cpu: parseFloat(parts[3]), mem: parseFloat(parts[4]), state: parts[5], runtime: runtimeSec, command: parts[7] };
|
|
562
|
+
}).filter((t) => t !== null && t.pid > 0);
|
|
563
|
+
} catch {
|
|
564
|
+
return [];
|
|
565
|
+
}
|
|
378
566
|
}
|
|
379
567
|
|
|
380
|
-
// src/formatters.ts
|
|
568
|
+
// src/formatters/render.ts
|
|
569
|
+
import chalk3 from "chalk";
|
|
570
|
+
|
|
571
|
+
// src/formatters/themes.ts
|
|
572
|
+
import chalk2 from "chalk";
|
|
381
573
|
var THEMES = {
|
|
382
574
|
default: {
|
|
383
575
|
border: chalk2.hex("#4b5563"),
|
|
384
576
|
cpu: chalk2.hex("#22d3ee"),
|
|
385
577
|
mem: chalk2.hex("#a78bfa"),
|
|
578
|
+
gpu: chalk2.hex("#f472b6"),
|
|
386
579
|
power: chalk2.hex("#fbbf24"),
|
|
387
580
|
battery: chalk2.hex("#34d399"),
|
|
388
581
|
thermal: chalk2.hex("#fb923c"),
|
|
@@ -395,6 +588,7 @@ var THEMES = {
|
|
|
395
588
|
border: chalk2.hex("#6272a4"),
|
|
396
589
|
cpu: chalk2.hex("#8be9fd"),
|
|
397
590
|
mem: chalk2.hex("#bd93f9"),
|
|
591
|
+
gpu: chalk2.hex("#ff5555"),
|
|
398
592
|
power: chalk2.hex("#f1fa8c"),
|
|
399
593
|
battery: chalk2.hex("#50fa7b"),
|
|
400
594
|
thermal: chalk2.hex("#ffb86c"),
|
|
@@ -407,6 +601,7 @@ var THEMES = {
|
|
|
407
601
|
border: chalk2.hex("#ff0055"),
|
|
408
602
|
cpu: chalk2.hex("#00ffcc"),
|
|
409
603
|
mem: chalk2.hex("#ff00ff"),
|
|
604
|
+
gpu: chalk2.hex("#ffff00"),
|
|
410
605
|
power: chalk2.hex("#ffff00"),
|
|
411
606
|
battery: chalk2.hex("#00ff00"),
|
|
412
607
|
thermal: chalk2.hex("#ff6600"),
|
|
@@ -419,6 +614,7 @@ var THEMES = {
|
|
|
419
614
|
border: chalk2.gray,
|
|
420
615
|
cpu: chalk2.white.bold,
|
|
421
616
|
mem: chalk2.white.bold,
|
|
617
|
+
gpu: chalk2.white.bold,
|
|
422
618
|
power: chalk2.white.bold,
|
|
423
619
|
battery: chalk2.white.bold,
|
|
424
620
|
thermal: chalk2.white.bold,
|
|
@@ -426,17 +622,36 @@ var THEMES = {
|
|
|
426
622
|
disk: chalk2.white.bold,
|
|
427
623
|
graphs: chalk2.white.bold,
|
|
428
624
|
process: chalk2.white.bold
|
|
625
|
+
},
|
|
626
|
+
nord: {
|
|
627
|
+
border: chalk2.hex("#4c566a"),
|
|
628
|
+
cpu: chalk2.hex("#88c0d0"),
|
|
629
|
+
mem: chalk2.hex("#b48ead"),
|
|
630
|
+
gpu: chalk2.hex("#bf616a"),
|
|
631
|
+
power: chalk2.hex("#ebcb8b"),
|
|
632
|
+
battery: chalk2.hex("#a3be8c"),
|
|
633
|
+
thermal: chalk2.hex("#d08770"),
|
|
634
|
+
network: chalk2.hex("#81a1c1"),
|
|
635
|
+
disk: chalk2.hex("#b48ead"),
|
|
636
|
+
graphs: chalk2.hex("#88c0d0"),
|
|
637
|
+
process: chalk2.hex("#eceff4")
|
|
638
|
+
},
|
|
639
|
+
gruvbox: {
|
|
640
|
+
border: chalk2.hex("#928374"),
|
|
641
|
+
cpu: chalk2.hex("#fb4934"),
|
|
642
|
+
mem: chalk2.hex("#b8bb26"),
|
|
643
|
+
gpu: chalk2.hex("#d3869b"),
|
|
644
|
+
power: chalk2.hex("#fabd2f"),
|
|
645
|
+
battery: chalk2.hex("#b8bb26"),
|
|
646
|
+
thermal: chalk2.hex("#fb4934"),
|
|
647
|
+
network: chalk2.hex("#83a598"),
|
|
648
|
+
disk: chalk2.hex("#d3869b"),
|
|
649
|
+
graphs: chalk2.hex("#fb4934"),
|
|
650
|
+
process: chalk2.hex("#ebdbb2")
|
|
429
651
|
}
|
|
430
652
|
};
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
return Math.max(60, Math.min(width, 240));
|
|
434
|
-
}
|
|
435
|
-
function gridColumns(width) {
|
|
436
|
-
if (width >= 150) return 3;
|
|
437
|
-
if (width >= 96) return 2;
|
|
438
|
-
return 1;
|
|
439
|
-
}
|
|
653
|
+
|
|
654
|
+
// src/formatters/render.ts
|
|
440
655
|
function visLen(s) {
|
|
441
656
|
return s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
442
657
|
}
|
|
@@ -453,9 +668,8 @@ function panel(title, lines, width, accent, borderAccent, height) {
|
|
|
453
668
|
const h = height ?? lines.length;
|
|
454
669
|
const contentWidth = Math.max(1, width - 4);
|
|
455
670
|
const label = ` ${title} `;
|
|
456
|
-
const
|
|
457
|
-
const
|
|
458
|
-
const bottom = borderAccent("\u2570" + "\u2500".repeat(width - 2) + "\u256F");
|
|
671
|
+
const top = borderAccent("\u250C" + label + "\u2500".repeat(width - label.length - 2) + "\u2510");
|
|
672
|
+
const bottom = borderAccent("\u2514" + "\u2500".repeat(width - 2) + "\u2518");
|
|
459
673
|
const body = [];
|
|
460
674
|
for (let i = 0; i < h; i++) {
|
|
461
675
|
const line = lines[i] ?? "";
|
|
@@ -481,31 +695,28 @@ function hstack(blocks, gap = 2) {
|
|
|
481
695
|
}
|
|
482
696
|
function statRow(label, value, width = 9) {
|
|
483
697
|
const w = Math.max(width, label.length + 1);
|
|
484
|
-
return `${
|
|
485
|
-
}
|
|
486
|
-
function bar(percent, width = 20) {
|
|
487
|
-
const filled = Math.round(Math.min(100, Math.max(0, percent)) / 100 * width);
|
|
488
|
-
const empty = width - filled;
|
|
489
|
-
const barStr = "\u2588".repeat(Math.max(0, filled)) + "\u2591".repeat(Math.max(0, empty));
|
|
490
|
-
const color = percent > 90 ? chalk2.red : percent > 70 ? chalk2.yellow : chalk2.green;
|
|
491
|
-
return color(barStr);
|
|
698
|
+
return `${chalk3.dim(label.padEnd(w))}${value}`;
|
|
492
699
|
}
|
|
493
700
|
function gaugeRow(label, percent, contentWidth) {
|
|
494
701
|
const labelW = 9;
|
|
495
702
|
const pctText = `${Math.round(percent)}%`;
|
|
496
703
|
const barW = Math.max(6, contentWidth - labelW - pctText.length - 1);
|
|
497
|
-
|
|
704
|
+
const filled = Math.round(Math.min(100, Math.max(0, percent)) / 100 * barW);
|
|
705
|
+
const empty = barW - filled;
|
|
706
|
+
const barStr = "\u2588".repeat(Math.max(0, filled)) + "\u2591".repeat(Math.max(0, empty));
|
|
707
|
+
const color = percent > 90 ? chalk3.red : percent > 70 ? chalk3.yellow : chalk3.green;
|
|
708
|
+
return `${chalk3.dim(label.padEnd(labelW))}${color(barStr)} ${pctText}`;
|
|
498
709
|
}
|
|
499
710
|
function thermalColor(pressureLevel) {
|
|
500
|
-
if (pressureLevel >= 3) return
|
|
501
|
-
if (pressureLevel === 2) return
|
|
502
|
-
if (pressureLevel === 1) return
|
|
503
|
-
return
|
|
711
|
+
if (pressureLevel >= 3) return chalk3.red;
|
|
712
|
+
if (pressureLevel === 2) return chalk3.yellow;
|
|
713
|
+
if (pressureLevel === 1) return chalk3.yellowBright;
|
|
714
|
+
return chalk3.green;
|
|
504
715
|
}
|
|
505
716
|
function capacityColor(percent) {
|
|
506
|
-
if (percent < 80) return
|
|
507
|
-
if (percent < 90) return
|
|
508
|
-
return
|
|
717
|
+
if (percent < 80) return chalk3.red;
|
|
718
|
+
if (percent < 90) return chalk3.yellow;
|
|
719
|
+
return chalk3.green;
|
|
509
720
|
}
|
|
510
721
|
function formatBytes(bytes) {
|
|
511
722
|
if (bytes < 1024) return `${bytes} B`;
|
|
@@ -525,7 +736,7 @@ function cpuCard(data, contentWidth) {
|
|
|
525
736
|
statRow("Cores", `${data.cpu.physicalCores}/${data.cpu.cores} phys/log`),
|
|
526
737
|
statRow("Freq", `${data.cpu.frequency} MHz`),
|
|
527
738
|
gaugeRow("Usage", data.cpu.usage, contentWidth),
|
|
528
|
-
statRow("Load", data.cpu.loadAvg.map((l) => l.toFixed(2)).join("
|
|
739
|
+
statRow("Load", data.cpu.loadAvg.map((l) => l.toFixed(2)).join(" "))
|
|
529
740
|
];
|
|
530
741
|
if (data.cpu.temperature) lines.push(statRow("Temp", formatTemp(data.cpu.temperature)));
|
|
531
742
|
return lines;
|
|
@@ -534,17 +745,32 @@ function memCard(data, contentWidth) {
|
|
|
534
745
|
return [
|
|
535
746
|
statRow("Total", formatBytes(data.memory.total)),
|
|
536
747
|
statRow("Used", formatBytes(data.memory.used)),
|
|
748
|
+
statRow("Cached", formatBytes(data.memory.total - data.memory.free - data.memory.used)),
|
|
537
749
|
statRow("Free", formatBytes(data.memory.free)),
|
|
538
750
|
gaugeRow("Usage", data.memory.usagePercent, contentWidth),
|
|
539
751
|
statRow("Swap", `${formatBytes(data.memory.swapUsed)} / ${formatBytes(data.memory.swapTotal)}`)
|
|
540
752
|
];
|
|
541
753
|
}
|
|
754
|
+
function gpuCard(data, contentWidth) {
|
|
755
|
+
if (!data.gpu) return null;
|
|
756
|
+
const lines = [];
|
|
757
|
+
lines.push(statRow("Model", truncatePlain(data.gpu.model, Math.max(4, contentWidth - 8))));
|
|
758
|
+
lines.push(statRow("Memory", formatBytes(data.gpu.memory)));
|
|
759
|
+
lines.push(statRow("Util", `${data.gpu.utilization}%`));
|
|
760
|
+
if (data.gpu.temperature) lines.push(statRow("Temp", formatTemp(data.gpu.temperature)));
|
|
761
|
+
lines.push(statRow("Procs", String(data.gpu.processes)));
|
|
762
|
+
return lines;
|
|
763
|
+
}
|
|
542
764
|
function powerCard(data) {
|
|
543
|
-
if (!data.power) return null;
|
|
544
765
|
const lines = [];
|
|
545
|
-
if (data.power
|
|
546
|
-
|
|
547
|
-
|
|
766
|
+
if (data.power) {
|
|
767
|
+
if (data.power.cpuWatts !== void 0) lines.push(statRow("CPU", `${data.power.cpuWatts.toFixed(2)} W`));
|
|
768
|
+
if (data.power.gpuWatts !== void 0) lines.push(statRow("GPU", `${data.power.gpuWatts.toFixed(2)} W`));
|
|
769
|
+
if (data.power.combinedWatts !== void 0) lines.push(statRow("Total", `${data.power.combinedWatts.toFixed(2)} W`));
|
|
770
|
+
}
|
|
771
|
+
if (data.battery?.powerWatts !== void 0 && data.battery.powerWatts > 0) {
|
|
772
|
+
lines.push(statRow("Battery", `${data.battery.powerWatts.toFixed(2)} W`));
|
|
773
|
+
}
|
|
548
774
|
return lines.length ? lines : null;
|
|
549
775
|
}
|
|
550
776
|
function batteryCard(data, contentWidth) {
|
|
@@ -553,12 +779,19 @@ function batteryCard(data, contentWidth) {
|
|
|
553
779
|
const lines = [
|
|
554
780
|
statRow("Level", `${data.battery.level}% (${data.battery.state})`),
|
|
555
781
|
statRow("Time", data.battery.timeRemaining),
|
|
556
|
-
statRow("
|
|
782
|
+
statRow("Cycles", String(data.battery.cycles ?? "N/A")),
|
|
783
|
+
statRow("Condition", data.battery.condition ?? "N/A")
|
|
557
784
|
];
|
|
558
785
|
if (data.battery.maxCapacityPercent !== void 0) {
|
|
559
786
|
lines.push(statRow("Capacity", capacityColor(data.battery.maxCapacityPercent)(`${data.battery.maxCapacityPercent}%`)));
|
|
560
787
|
}
|
|
561
788
|
lines.push(statRow("Source", data.battery.powerSource));
|
|
789
|
+
if (data.battery.estimatedTimeToEmpty && data.battery.state !== "charged") {
|
|
790
|
+
lines.push(statRow("Est. Empty", data.battery.estimatedTimeToEmpty));
|
|
791
|
+
}
|
|
792
|
+
if (data.battery.dischargeRatePerHour !== void 0 && data.battery.state !== "charged") {
|
|
793
|
+
lines.push(statRow("Disch. Rate", `${data.battery.dischargeRatePerHour}%/h`));
|
|
794
|
+
}
|
|
562
795
|
return lines;
|
|
563
796
|
}
|
|
564
797
|
function thermalCard(data, contentWidth) {
|
|
@@ -576,50 +809,251 @@ function networkCard(data, contentWidth) {
|
|
|
576
809
|
const valW = Math.max(6, contentWidth - 8);
|
|
577
810
|
return [
|
|
578
811
|
statRow("Iface", truncatePlain(`${data.network.interface} (${data.network.ip})`, valW)),
|
|
579
|
-
statRow("RX", `${formatBytes(data.network.rxBytes)}
|
|
580
|
-
statRow("TX", `${formatBytes(data.network.txBytes)}
|
|
812
|
+
statRow("RX", `${formatBytes(data.network.rxBytes)} / ${data.network.rxPackets} pkt`),
|
|
813
|
+
statRow("TX", `${formatBytes(data.network.txBytes)} / ${data.network.txPackets} pkt`)
|
|
581
814
|
];
|
|
582
815
|
}
|
|
816
|
+
function packetCard(data, contentWidth) {
|
|
817
|
+
if (!data.packets) return null;
|
|
818
|
+
const lines = [];
|
|
819
|
+
lines.push(statRow("Total", `${data.packets.totalPackets} pkt`));
|
|
820
|
+
lines.push(statRow("RX", `${data.packets.rxPackets} pkt`));
|
|
821
|
+
lines.push(statRow("TX", `${data.packets.txPackets} pkt`));
|
|
822
|
+
lines.push(statRow("Conns", `${data.packets.connections} TCP`));
|
|
823
|
+
if (data.packets.topProcesses && data.packets.topProcesses.length) {
|
|
824
|
+
lines.push(statRow("Top Proc", truncatePlain(data.packets.topProcesses[0].command, contentWidth - 10)));
|
|
825
|
+
}
|
|
826
|
+
if (data.packets.allProcesses && data.packets.allProcesses.length > 1) {
|
|
827
|
+
lines.push(statRow("Net Procs", `${data.packets.allProcesses.length} processes`));
|
|
828
|
+
}
|
|
829
|
+
if (data.packets.interfaces && data.packets.interfaces.length) {
|
|
830
|
+
lines.push(statRow("Ifaces", `${data.packets.interfaces.length} active`));
|
|
831
|
+
}
|
|
832
|
+
return lines;
|
|
833
|
+
}
|
|
834
|
+
function tasksCard(data, contentWidth) {
|
|
835
|
+
if (!data.tasks || !data.tasks.length) return null;
|
|
836
|
+
const lines = [];
|
|
837
|
+
for (const task of data.tasks.slice(0, 6)) {
|
|
838
|
+
const cmd = truncatePlain(task.command, Math.max(4, contentWidth - 28));
|
|
839
|
+
lines.push(statRow(`${task.pid}`, `${task.user.padEnd(8)} ${task.cpu.toFixed(1).padStart(5)}% ${task.mem.toFixed(1).padStart(5)}% ${task.state} ${cmd}`));
|
|
840
|
+
}
|
|
841
|
+
return lines;
|
|
842
|
+
}
|
|
583
843
|
function header(data, width, badges = [], borderAccent = THEMES.default.border) {
|
|
584
|
-
const
|
|
585
|
-
const
|
|
844
|
+
const now = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
845
|
+
const left = `${chalk3.hex("#ff5500").bold(" PYRE ")} ${chalk3.bold(data.header.hostname)} ${chalk3.dim(data.header.os)} ${chalk3.dim("up " + data.header.uptime)}`;
|
|
846
|
+
const right = `${chalk3.dim(now)} ${badges.join(" ")}`;
|
|
586
847
|
const gap = Math.max(1, width - visLen(left) - visLen(right));
|
|
587
848
|
const line = left + " ".repeat(gap) + right;
|
|
588
849
|
return [line, borderAccent("\u2500".repeat(width))];
|
|
589
850
|
}
|
|
851
|
+
var TAB_DEFS = [
|
|
852
|
+
{ id: "cpu", label: "CPU", key: "1" },
|
|
853
|
+
{ id: "mem", label: "Memory", key: "2" },
|
|
854
|
+
{ id: "gpu", label: "GPU", key: "3" },
|
|
855
|
+
{ id: "power", label: "Power", key: "4" },
|
|
856
|
+
{ id: "battery", label: "Battery", key: "5" },
|
|
857
|
+
{ id: "thermal", label: "Thermal", key: "6" },
|
|
858
|
+
{ id: "network", label: "Network", key: "7" },
|
|
859
|
+
{ id: "packets", label: "Packets", key: "8" },
|
|
860
|
+
{ id: "tasks", label: "Tasks", key: "9" },
|
|
861
|
+
{ id: "disk", label: "Disk", key: "0" },
|
|
862
|
+
{ id: "process", label: "Process", key: "p" }
|
|
863
|
+
];
|
|
864
|
+
var TAB_BAR_INDENT = 2;
|
|
865
|
+
var TAB_SEPARATOR_WIDTH = 3;
|
|
866
|
+
function getTabHitboxes() {
|
|
867
|
+
const boxes = [];
|
|
868
|
+
let col = TAB_BAR_INDENT;
|
|
869
|
+
TAB_DEFS.forEach((t, i) => {
|
|
870
|
+
const text = `[${t.key}]${t.label}`;
|
|
871
|
+
const start = col;
|
|
872
|
+
const end = col + text.length;
|
|
873
|
+
boxes.push({ id: t.id, start, end });
|
|
874
|
+
col = end;
|
|
875
|
+
if (i < TAB_DEFS.length - 1) col += TAB_SEPARATOR_WIDTH;
|
|
876
|
+
});
|
|
877
|
+
return boxes;
|
|
878
|
+
}
|
|
879
|
+
var TAB_BAR_ROW = 4;
|
|
880
|
+
function tabBar(activePanel, width, theme) {
|
|
881
|
+
const rendered = TAB_DEFS.map((t) => {
|
|
882
|
+
const isActive = activePanel === t.id;
|
|
883
|
+
const label = isActive ? chalk3.bold(t.label) : chalk3.dim(t.label);
|
|
884
|
+
const keyHint = chalk3.dim(`[${t.key}]`);
|
|
885
|
+
return `${keyHint}${label}`;
|
|
886
|
+
});
|
|
887
|
+
const joined = rendered.join(chalk3.dim(" \u2502 "));
|
|
888
|
+
const padded = visLen(joined) < width ? joined + " ".repeat(width - visLen(joined)) : joined;
|
|
889
|
+
return chalk3.dim(" ") + padded;
|
|
890
|
+
}
|
|
590
891
|
function sortProcesses(processes, sortBy) {
|
|
591
892
|
const copy = [...processes];
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
893
|
+
switch (sortBy) {
|
|
894
|
+
case "pid":
|
|
895
|
+
return copy.sort((a, b) => a.pid - b.pid);
|
|
896
|
+
case "mem":
|
|
897
|
+
return copy.sort((a, b) => b.mem - a.mem);
|
|
898
|
+
case "user":
|
|
899
|
+
return copy.sort((a, b) => a.user.localeCompare(b.user));
|
|
900
|
+
case "command":
|
|
901
|
+
return copy.sort((a, b) => a.command.localeCompare(b.command));
|
|
902
|
+
case "state":
|
|
903
|
+
return copy.sort((a, b) => a.state.localeCompare(b.state));
|
|
904
|
+
case "threads":
|
|
905
|
+
return copy.sort((a, b) => b.threads - a.threads);
|
|
906
|
+
case "runtime":
|
|
907
|
+
return copy.sort((a, b) => b.runtime - a.runtime);
|
|
908
|
+
default:
|
|
909
|
+
return copy.sort((a, b) => b.cpu - a.cpu);
|
|
910
|
+
}
|
|
595
911
|
}
|
|
596
912
|
function pctColor(v) {
|
|
597
|
-
return v > 80 ?
|
|
913
|
+
return v > 80 ? chalk3.red : v > 50 ? chalk3.yellow : chalk3.dim;
|
|
598
914
|
}
|
|
599
915
|
function processTableLines(processes, contentWidth, borderAccent = THEMES.default.border) {
|
|
600
|
-
const pidW =
|
|
601
|
-
const cmdW = Math.max(10, contentWidth - pidW - userW - cpuW - memW -
|
|
602
|
-
const head =
|
|
916
|
+
const pidW = 8, ppidW = 8, userW = 12, cpuW = 8, memW = 8, stateW = 8, thrW = 6, rtW = 10;
|
|
917
|
+
const cmdW = Math.max(10, contentWidth - pidW - ppidW - userW - cpuW - memW - stateW - thrW - rtW - 8);
|
|
918
|
+
const head = chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" PID ") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" PPID ") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" USER ") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" CPU% ") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" MEM% ") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" STATE ") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" THREADS") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" RUNTIME ") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" COMMAND".padEnd(cmdW + 1));
|
|
603
919
|
const rows = processes.map((p) => {
|
|
604
920
|
const cpuStr = pctColor(p.cpu)(`${p.cpu.toFixed(1)}`.padEnd(cpuW));
|
|
605
921
|
const memStr = pctColor(p.mem)(`${p.mem.toFixed(1)}`.padEnd(memW));
|
|
606
|
-
|
|
922
|
+
const stateStr = chalk3.dim(p.state.padEnd(stateW));
|
|
923
|
+
const thrStr = String(p.threads).padEnd(thrW);
|
|
924
|
+
const rtStr = formatRuntime(p.runtime).padEnd(rtW);
|
|
925
|
+
return String(p.pid).padEnd(pidW) + String(p.ppid).padEnd(ppidW) + truncatePlain(p.user, userW - 1).padEnd(userW) + cpuStr + memStr + stateStr + thrStr + rtStr + truncatePlain(p.command, cmdW);
|
|
607
926
|
});
|
|
608
927
|
return [head, borderAccent("\u2500".repeat(contentWidth)), ...rows];
|
|
609
928
|
}
|
|
929
|
+
function formatRuntime(seconds) {
|
|
930
|
+
if (seconds < 60) return `${seconds}s`;
|
|
931
|
+
const mins = Math.floor(seconds / 60);
|
|
932
|
+
if (mins < 60) return `${mins}m ${seconds % 60}s`;
|
|
933
|
+
const hrs = Math.floor(mins / 60);
|
|
934
|
+
return `${hrs}h ${mins % 60}m`;
|
|
935
|
+
}
|
|
936
|
+
function buildProcessTree(processes) {
|
|
937
|
+
const map = /* @__PURE__ */ new Map();
|
|
938
|
+
const roots = [];
|
|
939
|
+
for (const p of processes) map.set(p.pid, p);
|
|
940
|
+
for (const p of processes) {
|
|
941
|
+
if (p.ppid === 0 || !map.has(p.ppid)) roots.push(p);
|
|
942
|
+
}
|
|
943
|
+
const lines = [];
|
|
944
|
+
const visited = /* @__PURE__ */ new Set();
|
|
945
|
+
function renderNode(proc, prefix, isLast) {
|
|
946
|
+
if (visited.has(proc.pid)) return;
|
|
947
|
+
visited.add(proc.pid);
|
|
948
|
+
const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
949
|
+
const pidStr = String(proc.pid).padEnd(8);
|
|
950
|
+
const cpuStr = pctColor(proc.cpu)(`${proc.cpu.toFixed(1)}%`.padEnd(8));
|
|
951
|
+
const memStr = pctColor(proc.mem)(`${proc.mem.toFixed(1)}%`.padEnd(8));
|
|
952
|
+
const stateStr = chalk3.dim(proc.state.padEnd(8));
|
|
953
|
+
const thrStr = String(proc.threads).padEnd(6);
|
|
954
|
+
const cmd = truncatePlain(proc.command, Math.max(10, 100 - pidStr.length - cpuStr.length - memStr.length - stateStr.length - thrStr.length - 10));
|
|
955
|
+
lines.push(`${prefix}${connector}${pidStr}${cpuStr}${memStr}${stateStr}${thrStr} ${cmd}`);
|
|
956
|
+
const children = processes.filter((p) => p.ppid === proc.pid && !visited.has(p.pid));
|
|
957
|
+
children.sort((a, b) => b.cpu - a.cpu);
|
|
958
|
+
children.forEach((child, i) => {
|
|
959
|
+
const nextPrefix = prefix + (isLast ? " " : "\u2502 ");
|
|
960
|
+
renderNode(child, nextPrefix, i === children.length - 1);
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
roots.sort((a, b) => b.cpu - a.cpu);
|
|
964
|
+
roots.forEach((root, i) => renderNode(root, "", i === roots.length - 1));
|
|
965
|
+
return lines;
|
|
966
|
+
}
|
|
610
967
|
function diskTableLines(disks, contentWidth, borderAccent = THEMES.default.border) {
|
|
611
|
-
const fsW =
|
|
968
|
+
const fsW = 20, sizeW = 10, usedW = 10, availW = 10, capW = 8;
|
|
612
969
|
const mountW = Math.max(8, contentWidth - fsW - sizeW - usedW - availW - capW - 5);
|
|
613
|
-
const head =
|
|
614
|
-
const rows = disks.
|
|
970
|
+
const head = chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" FILESYSTEM ") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" SIZE ") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" USED ") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" AVAIL ") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" CAP ") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" MOUNT".padEnd(mountW + 1));
|
|
971
|
+
const rows = disks.map((d) => {
|
|
615
972
|
const capNum = parseInt(d.capacity, 10) || 0;
|
|
616
973
|
const capStr = pctColor(capNum)(d.capacity.padEnd(capW));
|
|
617
974
|
return truncatePlain(d.filesystem, fsW - 1).padEnd(fsW) + d.size.padEnd(sizeW) + d.used.padEnd(usedW) + d.available.padEnd(availW) + capStr + truncatePlain(d.mountpoint, mountW);
|
|
618
975
|
});
|
|
619
976
|
return [head, borderAccent("\u2500".repeat(contentWidth)), ...rows];
|
|
620
977
|
}
|
|
978
|
+
function detailPanelTitle(panel2) {
|
|
979
|
+
const map = {
|
|
980
|
+
cpu: "CPU",
|
|
981
|
+
mem: "Memory",
|
|
982
|
+
gpu: "GPU",
|
|
983
|
+
power: "Power",
|
|
984
|
+
battery: "Battery",
|
|
985
|
+
thermal: "Thermal",
|
|
986
|
+
network: "Network",
|
|
987
|
+
packets: "Packets",
|
|
988
|
+
tasks: "Tasks",
|
|
989
|
+
disk: "Disk",
|
|
990
|
+
process: "Processes"
|
|
991
|
+
};
|
|
992
|
+
return map[panel2] || panel2;
|
|
993
|
+
}
|
|
994
|
+
function activeDetailLines(data, activePanel, width) {
|
|
995
|
+
const contentWidth = width - 4;
|
|
996
|
+
switch (activePanel) {
|
|
997
|
+
case "cpu":
|
|
998
|
+
return cpuCard(data, contentWidth);
|
|
999
|
+
case "mem":
|
|
1000
|
+
return memCard(data, contentWidth);
|
|
1001
|
+
case "gpu": {
|
|
1002
|
+
const gpu = gpuCard(data, contentWidth);
|
|
1003
|
+
return gpu ?? ["No GPU data available"];
|
|
1004
|
+
}
|
|
1005
|
+
case "power": {
|
|
1006
|
+
const power = powerCard(data);
|
|
1007
|
+
if (!power && !data.battery) return ["No power data available"];
|
|
1008
|
+
const lines = [];
|
|
1009
|
+
if (power) lines.push(...power);
|
|
1010
|
+
if (data.battery?.powerWatts !== void 0 && data.battery.powerWatts > 0) {
|
|
1011
|
+
lines.push(statRow("Battery", `${data.battery.powerWatts.toFixed(2)} W`));
|
|
1012
|
+
}
|
|
1013
|
+
return lines.length ? lines : ["No power data available"];
|
|
1014
|
+
}
|
|
1015
|
+
case "battery":
|
|
1016
|
+
return data.battery ? batteryCard(data, contentWidth) : ["No battery data available"];
|
|
1017
|
+
case "thermal":
|
|
1018
|
+
return thermalCard(data, contentWidth);
|
|
1019
|
+
case "network":
|
|
1020
|
+
return networkCard(data, contentWidth);
|
|
1021
|
+
case "packets": {
|
|
1022
|
+
if (!data.packets) return ["No packet data available"];
|
|
1023
|
+
const lines = [];
|
|
1024
|
+
lines.push(statRow("Total", `${data.packets.totalPackets} pkt`));
|
|
1025
|
+
lines.push(statRow("RX", `${data.packets.rxPackets} pkt`));
|
|
1026
|
+
lines.push(statRow("TX", `${data.packets.txPackets} pkt`));
|
|
1027
|
+
lines.push(statRow("Conns", `${data.packets.connections} TCP`));
|
|
1028
|
+
if (data.packets.interfaces && data.packets.interfaces.length) {
|
|
1029
|
+
lines.push(statRow("Ifaces", `${data.packets.interfaces.length} active`));
|
|
1030
|
+
for (const iface of data.packets.interfaces.slice(0, 8)) {
|
|
1031
|
+
lines.push(statRow(` ${iface.iface}`, `RX ${formatBytes(iface.rxBytes)} / TX ${formatBytes(iface.txBytes)}`));
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
if (data.packets.allProcesses && data.packets.allProcesses.length) {
|
|
1035
|
+
lines.push(statRow("Net Procs", `${data.packets.allProcesses.length} total`));
|
|
1036
|
+
for (const proc of data.packets.allProcesses.slice(0, 10)) {
|
|
1037
|
+
lines.push(statRow(` ${proc.pid}`, `${truncatePlain(proc.command, 20)} TX ${formatBytes(proc.txBytes)}`));
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
return lines;
|
|
1041
|
+
}
|
|
1042
|
+
case "tasks":
|
|
1043
|
+
return data.tasks?.length ? tasksCard(data, contentWidth) : ["No task data available"];
|
|
1044
|
+
case "disk":
|
|
1045
|
+
return data.disk.length ? diskTableLines(data.disk, width - 4, THEMES.default.border) : ["No disk data available"];
|
|
1046
|
+
case "process": {
|
|
1047
|
+
const sorted = sortProcesses([...data.processes], "cpu");
|
|
1048
|
+
return processTableLines(sorted.slice(0, 50), width - 4, THEMES.default.border);
|
|
1049
|
+
}
|
|
1050
|
+
default:
|
|
1051
|
+
return null;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
621
1054
|
function formatTable(data, opts2 = {}) {
|
|
622
1055
|
const width = clampWidth(opts2.width);
|
|
1056
|
+
const activePanel = opts2.activePanel || "grid";
|
|
623
1057
|
const columns = gridColumns(width);
|
|
624
1058
|
const gap = 2;
|
|
625
1059
|
const cardWidth = Math.floor((width - gap * (columns - 1)) / columns);
|
|
@@ -630,6 +1064,15 @@ function formatTable(data, opts2 = {}) {
|
|
|
630
1064
|
const out = [];
|
|
631
1065
|
out.push(...header(data, width, [], theme.border));
|
|
632
1066
|
out.push("");
|
|
1067
|
+
out.push(tabBar(activePanel, width, theme));
|
|
1068
|
+
out.push("");
|
|
1069
|
+
if (activePanel !== "grid") {
|
|
1070
|
+
const lines = activeDetailLines(data, activePanel, width);
|
|
1071
|
+
if (lines) {
|
|
1072
|
+
out.push(...panel(detailPanelTitle(activePanel), lines, width, theme[activePanel] ?? theme.cpu, theme.border));
|
|
1073
|
+
}
|
|
1074
|
+
return out.join("\n");
|
|
1075
|
+
}
|
|
633
1076
|
const cards = [];
|
|
634
1077
|
if (vis.cpu !== false) {
|
|
635
1078
|
cards.push({ title: "CPU", accent: theme.cpu, lines: cpuCard(data, contentWidth) });
|
|
@@ -637,6 +1080,10 @@ function formatTable(data, opts2 = {}) {
|
|
|
637
1080
|
if (vis.mem !== false) {
|
|
638
1081
|
cards.push({ title: "Memory", accent: theme.mem, lines: memCard(data, contentWidth) });
|
|
639
1082
|
}
|
|
1083
|
+
if (vis.gpu !== false) {
|
|
1084
|
+
const gpu = gpuCard(data, contentWidth);
|
|
1085
|
+
if (gpu) cards.push({ title: "GPU", accent: theme.gpu, lines: gpu });
|
|
1086
|
+
}
|
|
640
1087
|
if (vis.power !== false) {
|
|
641
1088
|
const power = powerCard(data);
|
|
642
1089
|
if (power) cards.push({ title: "Power", accent: theme.power, lines: power });
|
|
@@ -651,6 +1098,14 @@ function formatTable(data, opts2 = {}) {
|
|
|
651
1098
|
if (vis.network !== false) {
|
|
652
1099
|
cards.push({ title: "Network", accent: theme.network, lines: networkCard(data, contentWidth) });
|
|
653
1100
|
}
|
|
1101
|
+
if (vis.packets !== false) {
|
|
1102
|
+
const packets = packetCard(data, contentWidth);
|
|
1103
|
+
if (packets) cards.push({ title: "Packets", accent: theme.network, lines: packets });
|
|
1104
|
+
}
|
|
1105
|
+
if (vis.tasks !== false) {
|
|
1106
|
+
const tasks = tasksCard(data, contentWidth);
|
|
1107
|
+
if (tasks) cards.push({ title: "Tasks", accent: theme.process, lines: tasks });
|
|
1108
|
+
}
|
|
654
1109
|
for (let i = 0; i < cards.length; i += columns) {
|
|
655
1110
|
const row = cards.slice(i, i + columns);
|
|
656
1111
|
const rowHeight = Math.max(...row.map((c) => c.lines.length));
|
|
@@ -668,14 +1123,49 @@ function formatTable(data, opts2 = {}) {
|
|
|
668
1123
|
const sortedProcesses = sortProcesses(filteredProcesses, opts2.sortBy ?? "cpu");
|
|
669
1124
|
const limited = opts2.processLimit ? sortedProcesses.slice(0, opts2.processLimit) : sortedProcesses;
|
|
670
1125
|
const procTitle = opts2.filter ? `Processes \xB7 filter "${opts2.filter}" \xB7 sort ${opts2.sortBy ?? "cpu"}` : `Processes \xB7 sort ${opts2.sortBy ?? "cpu"}`;
|
|
671
|
-
if (
|
|
1126
|
+
if (opts2.treeView) {
|
|
1127
|
+
const treeLines = buildProcessTree(limited);
|
|
1128
|
+
if (treeLines.length) {
|
|
1129
|
+
out.push(...panel(procTitle + " \xB7 tree", treeLines, width, theme.process, theme.border));
|
|
1130
|
+
} else if (opts2.filter) {
|
|
1131
|
+
out.push(...panel(procTitle, [chalk3.dim(`No processes match "${opts2.filter}"`)], width, theme.process, theme.border));
|
|
1132
|
+
}
|
|
1133
|
+
} else if (limited.length) {
|
|
672
1134
|
out.push(...panel(procTitle, processTableLines(limited, width - 4, theme.border), width, theme.process, theme.border));
|
|
673
1135
|
} else if (opts2.filter) {
|
|
674
|
-
out.push(...panel(procTitle, [
|
|
1136
|
+
out.push(...panel(procTitle, [chalk3.dim(`No processes match "${opts2.filter}"`)], width, theme.process, theme.border));
|
|
675
1137
|
}
|
|
676
1138
|
}
|
|
677
1139
|
return out.join("\n");
|
|
678
1140
|
}
|
|
1141
|
+
function clampWidth(width) {
|
|
1142
|
+
if (!width || Number.isNaN(width)) return 80;
|
|
1143
|
+
return Math.max(60, Math.min(width, 240));
|
|
1144
|
+
}
|
|
1145
|
+
function gridColumns(width) {
|
|
1146
|
+
if (width >= 150) return 3;
|
|
1147
|
+
if (width >= 96) return 2;
|
|
1148
|
+
return 1;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// src/formatters/output.ts
|
|
1152
|
+
import chalk4 from "chalk";
|
|
1153
|
+
|
|
1154
|
+
// src/sparkline.ts
|
|
1155
|
+
var BLOCKS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
1156
|
+
function sparkline(values, opts2 = {}) {
|
|
1157
|
+
if (!values.length) return "";
|
|
1158
|
+
const min = opts2.min ?? Math.min(...values);
|
|
1159
|
+
const max = opts2.max ?? Math.max(...values);
|
|
1160
|
+
const range = max - min || 1;
|
|
1161
|
+
return values.map((v) => {
|
|
1162
|
+
const idx = Math.round((v - min) / range * (BLOCKS.length - 1));
|
|
1163
|
+
const clamped = Math.min(BLOCKS.length - 1, Math.max(0, idx));
|
|
1164
|
+
return BLOCKS[clamped];
|
|
1165
|
+
}).join("");
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// src/formatters/output.ts
|
|
679
1169
|
function formatJson(data) {
|
|
680
1170
|
return JSON.stringify(data, null, 2);
|
|
681
1171
|
}
|
|
@@ -697,15 +1187,21 @@ function formatCsv(data) {
|
|
|
697
1187
|
[
|
|
698
1188
|
"thermal",
|
|
699
1189
|
"cpuTempF",
|
|
700
|
-
data.thermal.temperatures?.cpu_die
|
|
1190
|
+
data.thermal.temperatures?.cpu_die != null ? String(celsiusToFahrenheit(data.thermal.temperatures.cpu_die)) : ""
|
|
701
1191
|
],
|
|
702
1192
|
["network", "rxBytes", String(data.network.rxBytes)],
|
|
703
1193
|
["network", "txBytes", String(data.network.txBytes)],
|
|
1194
|
+
["network", "rxPackets", String(data.network.rxPackets)],
|
|
1195
|
+
["network", "txPackets", String(data.network.txPackets)],
|
|
1196
|
+
["network", "connections", data.network.connections ? String(data.network.connections) : ""],
|
|
704
1197
|
["battery", "level", data.battery ? String(data.battery.level) : ""],
|
|
705
1198
|
["battery", "powerSource", data.battery ? data.battery.powerSource : ""],
|
|
706
1199
|
["battery", "condition", data.battery?.condition ?? ""],
|
|
707
1200
|
["battery", "maxCapacityPercent", data.battery?.maxCapacityPercent !== void 0 ? String(data.battery.maxCapacityPercent) : ""],
|
|
708
1201
|
["battery", "cycles", data.battery?.cycles !== void 0 ? String(data.battery.cycles) : ""],
|
|
1202
|
+
["battery", "estimatedTimeToEmpty", data.battery?.estimatedTimeToEmpty ?? ""],
|
|
1203
|
+
["battery", "dischargeRatePerHour", data.battery?.dischargeRatePerHour !== void 0 ? String(data.battery.dischargeRatePerHour) : ""],
|
|
1204
|
+
["battery", "powerWatts", data.battery?.powerWatts !== void 0 ? String(data.battery.powerWatts) : ""],
|
|
709
1205
|
["power", "cpuWatts", data.power?.cpuWatts !== void 0 ? String(data.power.cpuWatts) : ""],
|
|
710
1206
|
["power", "gpuWatts", data.power?.gpuWatts !== void 0 ? String(data.power.gpuWatts) : ""],
|
|
711
1207
|
["power", "combinedWatts", data.power?.combinedWatts !== void 0 ? String(data.power.combinedWatts) : ""],
|
|
@@ -713,94 +1209,199 @@ function formatCsv(data) {
|
|
|
713
1209
|
["system", "uptime", data.header.uptime],
|
|
714
1210
|
["system", "timestamp", data.timestamp]
|
|
715
1211
|
];
|
|
1212
|
+
if (data.packets) {
|
|
1213
|
+
rows.push(["packets", "totalPackets", String(data.packets.totalPackets)]);
|
|
1214
|
+
rows.push(["packets", "rxPackets", String(data.packets.rxPackets)]);
|
|
1215
|
+
rows.push(["packets", "txPackets", String(data.packets.txPackets)]);
|
|
1216
|
+
rows.push(["packets", "connections", String(data.packets.connections)]);
|
|
1217
|
+
for (const iface of data.packets.interfaces || []) {
|
|
1218
|
+
rows.push(["packets_iface", iface.iface, `${iface.rxPackets},${iface.txPackets},${iface.rxBytes},${iface.txBytes}`]);
|
|
1219
|
+
}
|
|
1220
|
+
for (const proc of data.packets.allProcesses || []) {
|
|
1221
|
+
rows.push(["packets_proc", `pid=${proc.pid}`, `${proc.command},${proc.rxBytes},${proc.txBytes}`]);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
if (data.gpu) {
|
|
1225
|
+
rows.push(["gpu", "model", data.gpu.model]);
|
|
1226
|
+
rows.push(["gpu", "memory", String(data.gpu.memory)]);
|
|
1227
|
+
rows.push(["gpu", "utilization", String(data.gpu.utilization)]);
|
|
1228
|
+
if (data.gpu.temperature) rows.push(["gpu", "tempC", String(data.gpu.temperature)]);
|
|
1229
|
+
rows.push(["gpu", "processes", String(data.gpu.processes)]);
|
|
1230
|
+
}
|
|
1231
|
+
for (const p of data.processes) {
|
|
1232
|
+
rows.push(["process", `pid=${p.pid}`, `${p.pid}`]);
|
|
1233
|
+
rows.push(["process", `ppid=${p.pid}`, String(p.ppid)]);
|
|
1234
|
+
rows.push(["process", `user=${p.pid}`, p.user]);
|
|
1235
|
+
rows.push(["process", `cpu=${p.pid}`, String(p.cpu)]);
|
|
1236
|
+
rows.push(["process", `mem=${p.pid}`, String(p.mem)]);
|
|
1237
|
+
rows.push(["process", `state=${p.pid}`, p.state]);
|
|
1238
|
+
rows.push(["process", `threads=${p.pid}`, String(p.threads)]);
|
|
1239
|
+
rows.push(["process", `runtime=${p.pid}`, String(p.runtime)]);
|
|
1240
|
+
rows.push(["process", `command=${p.pid}`, p.command]);
|
|
1241
|
+
}
|
|
716
1242
|
return rows.map((r) => r.map((item) => `"${String(item).replace(/"/g, '""')}"`).join(",")).join("\n");
|
|
717
1243
|
}
|
|
718
1244
|
function formatTsv(data) {
|
|
719
1245
|
return formatCsv(data).split("\n").map((r) => r.split(",").join(" ")).join("\n");
|
|
720
1246
|
}
|
|
721
|
-
function formatGraphs(
|
|
1247
|
+
function formatGraphs(history, width = 80, themeName = "default", graphMode = "spark") {
|
|
722
1248
|
const theme = THEMES[themeName] || THEMES.default;
|
|
723
1249
|
const contentWidth = width - 4;
|
|
724
|
-
const sampleCount =
|
|
1250
|
+
const sampleCount = history.cpuUsage.length;
|
|
725
1251
|
const sparkWidth = Math.max(10, contentWidth - 28);
|
|
726
1252
|
const lines = [];
|
|
727
|
-
lines.push(graphRow("CPU %",
|
|
728
|
-
lines.push(graphRow("Mem %",
|
|
1253
|
+
lines.push(graphRow("CPU %", history.cpuUsage, { min: 0, max: 100 }, (v) => `${v.toFixed(0)}%`, sparkWidth, graphMode));
|
|
1254
|
+
lines.push(graphRow("Mem %", history.memUsage, { min: 0, max: 100 }, (v) => `${v.toFixed(0)}%`, sparkWidth, graphMode));
|
|
729
1255
|
lines.push(
|
|
730
|
-
graphRow("Temp",
|
|
1256
|
+
graphRow("Temp", history.temp, {}, formatTemp, sparkWidth, "no sensor access (needs sudo powermetrics)", graphMode)
|
|
731
1257
|
);
|
|
732
|
-
lines.push(graphRow("Net RX/s",
|
|
733
|
-
lines.push(graphRow("Net TX/s",
|
|
734
|
-
|
|
1258
|
+
lines.push(graphRow("Net RX/s", history.netRxRate, {}, (v) => `${formatBytes(v)}/s`, sparkWidth, graphMode));
|
|
1259
|
+
lines.push(graphRow("Net TX/s", history.netTxRate, {}, (v) => `${formatBytes(v)}/s`, sparkWidth, graphMode));
|
|
1260
|
+
if (history.powerWatts.length) {
|
|
1261
|
+
lines.push(graphRow("Power W", history.powerWatts, {}, (v) => `${v.toFixed(1)} W`, sparkWidth, graphMode));
|
|
1262
|
+
}
|
|
1263
|
+
if (history.rxPacketRate.length) {
|
|
1264
|
+
lines.push(graphRow("RX pkt/s", history.rxPacketRate, {}, (v) => `${v.toFixed(0)} pkt/s`, sparkWidth, graphMode));
|
|
1265
|
+
}
|
|
1266
|
+
if (history.txPacketRate.length) {
|
|
1267
|
+
lines.push(graphRow("TX pkt/s", history.txPacketRate, {}, (v) => `${v.toFixed(0)} pkt/s`, sparkWidth, graphMode));
|
|
1268
|
+
}
|
|
1269
|
+
const title = `Graphs \xB7 mode:${graphMode} \xB7 last ${sampleCount} sample${sampleCount === 1 ? "" : "s"}`;
|
|
735
1270
|
return panel(title, lines, width, theme.graphs, theme.border).join("\n");
|
|
736
1271
|
}
|
|
737
|
-
function graphRow(label, values, bounds, fmt, sparkWidth = 40, emptyMessage = "collecting data...") {
|
|
1272
|
+
function graphRow(label, values, bounds, fmt, sparkWidth = 40, emptyMessage = "collecting data...", mode = "spark") {
|
|
738
1273
|
if (!values.length) {
|
|
739
|
-
return `${
|
|
1274
|
+
return `${chalk4.dim(label.padEnd(10))} ${chalk4.dim(emptyMessage)}`;
|
|
740
1275
|
}
|
|
741
1276
|
const visible = values.slice(-sparkWidth);
|
|
742
|
-
const spark = sparkline(visible, bounds);
|
|
1277
|
+
const spark = mode === "spark" ? sparkline(visible, bounds) : barGraph(visible, bounds, sparkWidth);
|
|
743
1278
|
const current = values[values.length - 1];
|
|
744
|
-
let color =
|
|
1279
|
+
let color = chalk4.cyan;
|
|
745
1280
|
if (bounds.min !== void 0 && bounds.max !== void 0) {
|
|
746
1281
|
const pct = (current - bounds.min) / (bounds.max - bounds.min || 1) * 100;
|
|
747
|
-
color = pct > 90 ?
|
|
1282
|
+
color = pct > 90 ? chalk4.red : pct > 70 ? chalk4.yellow : chalk4.green;
|
|
748
1283
|
}
|
|
749
|
-
return `${
|
|
1284
|
+
return `${chalk4.dim(label.padEnd(10))}${color(spark)} ${chalk4.bold(fmt(current))}`;
|
|
1285
|
+
}
|
|
1286
|
+
function barGraph(values, bounds, width) {
|
|
1287
|
+
const min = bounds.min ?? Math.min(...values);
|
|
1288
|
+
const max = bounds.max ?? Math.max(...values);
|
|
1289
|
+
const range = max - min || 1;
|
|
1290
|
+
return values.map((v) => {
|
|
1291
|
+
const pct = (v - min) / range;
|
|
1292
|
+
if (pct > 0.8) return "\u2588";
|
|
1293
|
+
if (pct > 0.6) return "\u2587";
|
|
1294
|
+
if (pct > 0.4) return "\u2586";
|
|
1295
|
+
if (pct > 0.2) return "\u2584";
|
|
1296
|
+
return " ";
|
|
1297
|
+
}).join("");
|
|
750
1298
|
}
|
|
751
1299
|
|
|
752
|
-
// src/live.ts
|
|
753
|
-
import fs from "fs";
|
|
754
|
-
import path from "path";
|
|
1300
|
+
// src/live/input.ts
|
|
755
1301
|
import readline from "readline";
|
|
756
|
-
import chalk3 from "chalk";
|
|
757
1302
|
|
|
758
1303
|
// src/history.ts
|
|
759
1304
|
var History = class {
|
|
1305
|
+
/** Maximum number of samples retained in the rolling window. */
|
|
760
1306
|
maxLen;
|
|
1307
|
+
/** Rolling window of CPU usage percentages (0–100). */
|
|
761
1308
|
cpuUsage = [];
|
|
1309
|
+
/** Rolling window of memory usage percentages (0–100). */
|
|
762
1310
|
memUsage = [];
|
|
1311
|
+
/** Rolling window of CPU die temperatures in °C. */
|
|
763
1312
|
temp = [];
|
|
1313
|
+
/** Rolling window of network receive rates in bytes/sec. */
|
|
764
1314
|
netRxRate = [];
|
|
1315
|
+
/** Rolling window of network transmit rates in bytes/sec. */
|
|
765
1316
|
netTxRate = [];
|
|
1317
|
+
/** Rolling window of GPU utilization percentages (0–100). */
|
|
1318
|
+
gpuUtil = [];
|
|
1319
|
+
/** Rolling window of combined power draw in watts. */
|
|
1320
|
+
powerWatts = [];
|
|
1321
|
+
/** Rolling window of network receive packet rates in packets/sec. */
|
|
1322
|
+
rxPacketRate = [];
|
|
1323
|
+
/** Rolling window of network transmit packet rates in packets/sec. */
|
|
1324
|
+
txPacketRate = [];
|
|
1325
|
+
/** Rolling window of active TCP connections. */
|
|
1326
|
+
connections = [];
|
|
766
1327
|
lastRxBytes = 0;
|
|
767
1328
|
lastTxBytes = 0;
|
|
1329
|
+
lastRxPackets = 0;
|
|
1330
|
+
lastTxPackets = 0;
|
|
768
1331
|
lastTs = 0;
|
|
1332
|
+
/**
|
|
1333
|
+
* Create a new History instance.
|
|
1334
|
+
* @param maxLen - Maximum number of samples to retain. Defaults to 40.
|
|
1335
|
+
*/
|
|
769
1336
|
constructor(maxLen = 40) {
|
|
770
1337
|
this.maxLen = maxLen;
|
|
771
1338
|
}
|
|
1339
|
+
/**
|
|
1340
|
+
* Push a new sample into the rolling window.
|
|
1341
|
+
*
|
|
1342
|
+
* Network rates are computed as bytes/sec from the cumulative
|
|
1343
|
+
* counters. The first sample always reports 0 for rates
|
|
1344
|
+
* because there is no prior sample to compute a delta from.
|
|
1345
|
+
*/
|
|
772
1346
|
push(sample) {
|
|
773
1347
|
const now = Date.now();
|
|
774
1348
|
let rxRate = 0;
|
|
775
1349
|
let txRate = 0;
|
|
1350
|
+
let rxPktRate = 0;
|
|
1351
|
+
let txPktRate = 0;
|
|
776
1352
|
if (this.lastTs > 0) {
|
|
777
1353
|
const dtSeconds = Math.max((now - this.lastTs) / 1e3, 1e-3);
|
|
778
1354
|
rxRate = Math.max(0, (sample.rxBytes - this.lastRxBytes) / dtSeconds);
|
|
779
1355
|
txRate = Math.max(0, (sample.txBytes - this.lastTxBytes) / dtSeconds);
|
|
1356
|
+
if (sample.rxPackets !== void 0) {
|
|
1357
|
+
rxPktRate = Math.max(0, (sample.rxPackets - this.lastRxPackets) / dtSeconds);
|
|
1358
|
+
}
|
|
1359
|
+
if (sample.txPackets !== void 0) {
|
|
1360
|
+
txPktRate = Math.max(0, (sample.txPackets - this.lastTxPackets) / dtSeconds);
|
|
1361
|
+
}
|
|
780
1362
|
}
|
|
781
1363
|
this.lastRxBytes = sample.rxBytes;
|
|
782
1364
|
this.lastTxBytes = sample.txBytes;
|
|
1365
|
+
if (sample.rxPackets !== void 0) this.lastRxPackets = sample.rxPackets;
|
|
1366
|
+
if (sample.txPackets !== void 0) this.lastTxPackets = sample.txPackets;
|
|
783
1367
|
this.lastTs = now;
|
|
784
1368
|
this.pushBounded(this.cpuUsage, sample.cpuUsage);
|
|
785
1369
|
this.pushBounded(this.memUsage, sample.memUsage);
|
|
786
1370
|
if (sample.temp !== null) this.pushBounded(this.temp, sample.temp);
|
|
787
1371
|
this.pushBounded(this.netRxRate, rxRate);
|
|
788
1372
|
this.pushBounded(this.netTxRate, txRate);
|
|
1373
|
+
if (sample.gpuUtil !== void 0) this.pushBounded(this.gpuUtil, sample.gpuUtil);
|
|
1374
|
+
if (sample.powerWatts !== void 0) this.pushBounded(this.powerWatts, sample.powerWatts);
|
|
1375
|
+
if (sample.rxPackets !== void 0) this.pushBounded(this.rxPacketRate, rxPktRate);
|
|
1376
|
+
if (sample.txPackets !== void 0) this.pushBounded(this.txPacketRate, txPktRate);
|
|
1377
|
+
if (sample.connections !== void 0) this.pushBounded(this.connections, sample.connections);
|
|
789
1378
|
}
|
|
1379
|
+
/** Clear all rolling windows and reset cumulative byte counters. */
|
|
790
1380
|
reset() {
|
|
791
1381
|
this.cpuUsage = [];
|
|
792
1382
|
this.memUsage = [];
|
|
793
1383
|
this.temp = [];
|
|
794
1384
|
this.netRxRate = [];
|
|
795
1385
|
this.netTxRate = [];
|
|
1386
|
+
this.gpuUtil = [];
|
|
1387
|
+
this.powerWatts = [];
|
|
1388
|
+
this.rxPacketRate = [];
|
|
1389
|
+
this.txPacketRate = [];
|
|
1390
|
+
this.connections = [];
|
|
796
1391
|
this.lastRxBytes = 0;
|
|
797
1392
|
this.lastTxBytes = 0;
|
|
1393
|
+
this.lastRxPackets = 0;
|
|
1394
|
+
this.lastTxPackets = 0;
|
|
798
1395
|
this.lastTs = 0;
|
|
799
1396
|
}
|
|
800
|
-
/**
|
|
1397
|
+
/**
|
|
1398
|
+
* Grow or shrink the rolling window (e.g. on terminal resize),
|
|
1399
|
+
* trimming from the oldest samples.
|
|
1400
|
+
* @param n - New maximum length; clamped to at least 1.
|
|
1401
|
+
*/
|
|
801
1402
|
setMaxLen(n) {
|
|
802
1403
|
this.maxLen = Math.max(1, n);
|
|
803
|
-
for (const arr of [this.cpuUsage, this.memUsage, this.temp, this.netRxRate, this.netTxRate]) {
|
|
1404
|
+
for (const arr of [this.cpuUsage, this.memUsage, this.temp, this.netRxRate, this.netTxRate, this.gpuUtil, this.powerWatts, this.rxPacketRate, this.txPacketRate, this.connections]) {
|
|
804
1405
|
while (arr.length > this.maxLen) arr.shift();
|
|
805
1406
|
}
|
|
806
1407
|
}
|
|
@@ -810,62 +1411,94 @@ var History = class {
|
|
|
810
1411
|
}
|
|
811
1412
|
};
|
|
812
1413
|
|
|
813
|
-
// src/live.ts
|
|
814
|
-
var
|
|
815
|
-
var
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
1414
|
+
// src/live/state.ts
|
|
1415
|
+
var SIGNAL_OPTIONS = ["SIGTERM", "SIGKILL", "SIGINT", "SIGHUP", "SIGSTOP", "SIGCONT"];
|
|
1416
|
+
var state = {
|
|
1417
|
+
intervalHandle: null,
|
|
1418
|
+
running: false,
|
|
1419
|
+
paused: false,
|
|
1420
|
+
detailed: false,
|
|
1421
|
+
interval: 2,
|
|
1422
|
+
showGraphs: true,
|
|
1423
|
+
graphMode: "spark",
|
|
1424
|
+
exportFormat: "json",
|
|
1425
|
+
exportDir: "./pyre-exports",
|
|
1426
|
+
currentTheme: "default",
|
|
1427
|
+
visiblePanels: {
|
|
1428
|
+
cpu: true,
|
|
1429
|
+
mem: true,
|
|
1430
|
+
gpu: true,
|
|
1431
|
+
power: true,
|
|
1432
|
+
battery: true,
|
|
1433
|
+
thermal: true,
|
|
1434
|
+
network: true,
|
|
1435
|
+
packets: true,
|
|
1436
|
+
tasks: true,
|
|
1437
|
+
disk: true,
|
|
1438
|
+
process: true
|
|
1439
|
+
},
|
|
1440
|
+
logging: false,
|
|
1441
|
+
logStream: null,
|
|
1442
|
+
statusMessage: "",
|
|
1443
|
+
statusTimer: null,
|
|
1444
|
+
lastData: null,
|
|
1445
|
+
history: new History(40),
|
|
1446
|
+
keypressHandler: null,
|
|
1447
|
+
termWidth: process.stdout.columns || 80,
|
|
1448
|
+
termHeight: process.stdout.rows || 24,
|
|
1449
|
+
sortMode: "cpu",
|
|
1450
|
+
processFilter: "",
|
|
1451
|
+
inputMode: null,
|
|
1452
|
+
inputBuffer: "",
|
|
1453
|
+
treeView: false,
|
|
1454
|
+
SIGNAL_OPTIONS,
|
|
1455
|
+
customizerIndex: 0,
|
|
1456
|
+
CUSTOMIZER_OPTIONS: [
|
|
1457
|
+
"Theme",
|
|
1458
|
+
"Graph Mode",
|
|
1459
|
+
"Toggle CPU",
|
|
1460
|
+
"Toggle Memory",
|
|
1461
|
+
"Toggle GPU",
|
|
1462
|
+
"Toggle Power",
|
|
1463
|
+
"Toggle Battery",
|
|
1464
|
+
"Toggle Thermal",
|
|
1465
|
+
"Toggle Network",
|
|
1466
|
+
"Toggle Packets",
|
|
1467
|
+
"Toggle Tasks",
|
|
1468
|
+
"Toggle Disk",
|
|
1469
|
+
"Toggle Processes",
|
|
1470
|
+
"Toggle Tree View"
|
|
1471
|
+
],
|
|
1472
|
+
CPU_ALERT_PCT: 90,
|
|
1473
|
+
TEMP_ALERT_C: 95,
|
|
1474
|
+
alerted: false,
|
|
1475
|
+
activePanel: "grid",
|
|
1476
|
+
mouseEnabled: true,
|
|
1477
|
+
PANEL_TABS: [
|
|
1478
|
+
{ id: "cpu", label: "CPU", key: "1" },
|
|
1479
|
+
{ id: "mem", label: "Memory", key: "2" },
|
|
1480
|
+
{ id: "gpu", label: "GPU", key: "3" },
|
|
1481
|
+
{ id: "power", label: "Power", key: "4" },
|
|
1482
|
+
{ id: "battery", label: "Battery", key: "5" },
|
|
1483
|
+
{ id: "thermal", label: "Thermal", key: "6" },
|
|
1484
|
+
{ id: "network", label: "Network", key: "7" },
|
|
1485
|
+
{ id: "packets", label: "Packets", key: "8" },
|
|
1486
|
+
{ id: "tasks", label: "Tasks", key: "9" },
|
|
1487
|
+
{ id: "disk", label: "Disk", key: "0" },
|
|
1488
|
+
{ id: "process", label: "Process", key: "P" }
|
|
1489
|
+
]
|
|
832
1490
|
};
|
|
833
|
-
var logging = false;
|
|
834
|
-
var logStream = null;
|
|
835
|
-
var statusMessage = "";
|
|
836
|
-
var statusTimer = null;
|
|
837
|
-
var lastData = null;
|
|
838
|
-
var history = new History(40);
|
|
839
|
-
var keypressHandler = null;
|
|
840
|
-
var termWidth = process.stdout.columns || 80;
|
|
841
|
-
var termHeight = process.stdout.rows || 24;
|
|
842
|
-
var sortMode = "cpu";
|
|
843
|
-
var processFilter = "";
|
|
844
|
-
var inputMode = null;
|
|
845
|
-
var inputBuffer = "";
|
|
846
|
-
var customizerIndex = 0;
|
|
847
|
-
var CUSTOMIZER_OPTIONS = [
|
|
848
|
-
"Theme",
|
|
849
|
-
"Toggle CPU",
|
|
850
|
-
"Toggle Memory",
|
|
851
|
-
"Toggle Power",
|
|
852
|
-
"Toggle Battery",
|
|
853
|
-
"Toggle Thermal",
|
|
854
|
-
"Toggle Network",
|
|
855
|
-
"Toggle Disk",
|
|
856
|
-
"Toggle Processes"
|
|
857
|
-
];
|
|
858
|
-
var CPU_ALERT_PCT = 90;
|
|
859
|
-
var TEMP_ALERT_C = 95;
|
|
860
|
-
var alerted = false;
|
|
861
1491
|
function setStatus(msg, ms = 3e3) {
|
|
862
|
-
statusMessage = msg;
|
|
863
|
-
if (statusTimer) clearTimeout(statusTimer);
|
|
864
|
-
statusTimer = setTimeout(() => {
|
|
865
|
-
statusMessage = "";
|
|
866
|
-
render();
|
|
1492
|
+
state.statusMessage = msg;
|
|
1493
|
+
if (state.statusTimer) clearTimeout(state.statusTimer);
|
|
1494
|
+
state.statusTimer = setTimeout(() => {
|
|
1495
|
+
state.statusMessage = "";
|
|
867
1496
|
}, ms);
|
|
868
1497
|
}
|
|
1498
|
+
|
|
1499
|
+
// src/live/export.ts
|
|
1500
|
+
import fs from "fs";
|
|
1501
|
+
import path from "path";
|
|
869
1502
|
function ensureDir(dir) {
|
|
870
1503
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
871
1504
|
}
|
|
@@ -873,14 +1506,14 @@ function timestampForFile() {
|
|
|
873
1506
|
return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
874
1507
|
}
|
|
875
1508
|
function exportSnapshot() {
|
|
876
|
-
if (!lastData) {
|
|
1509
|
+
if (!state.lastData) {
|
|
877
1510
|
setStatus("No data yet \u2014 try again in a moment");
|
|
878
1511
|
return;
|
|
879
1512
|
}
|
|
880
1513
|
try {
|
|
881
|
-
ensureDir(exportDir);
|
|
882
|
-
const file = path.join(exportDir, `pyre-${timestampForFile()}.${exportFormat}`);
|
|
883
|
-
const content = exportFormat === "json" ? formatJson(lastData) : exportFormat === "csv" ? formatCsv(lastData) : formatTsv(lastData);
|
|
1514
|
+
ensureDir(state.exportDir);
|
|
1515
|
+
const file = path.join(state.exportDir, `pyre-${timestampForFile()}.${state.exportFormat}`);
|
|
1516
|
+
const content = state.exportFormat === "json" ? formatJson(state.lastData) : state.exportFormat === "csv" ? formatCsv(state.lastData) : formatTsv(state.lastData);
|
|
884
1517
|
fs.writeFileSync(file, content);
|
|
885
1518
|
setStatus(`Exported snapshot -> ${file}`);
|
|
886
1519
|
} catch (err) {
|
|
@@ -889,86 +1522,106 @@ function exportSnapshot() {
|
|
|
889
1522
|
}
|
|
890
1523
|
function startLogging() {
|
|
891
1524
|
try {
|
|
892
|
-
ensureDir(exportDir);
|
|
893
|
-
const file = path.join(exportDir, `pyre-log-${timestampForFile()}.csv`);
|
|
894
|
-
logStream = fs.createWriteStream(file, { flags: "a" });
|
|
895
|
-
logStream.write("timestamp,cpu_usage,mem_usage_percent,temp_c,net_rx_bytes,net_tx_bytes,thermal_state\n");
|
|
896
|
-
logging = true;
|
|
1525
|
+
ensureDir(state.exportDir);
|
|
1526
|
+
const file = path.join(state.exportDir, `pyre-log-${timestampForFile()}.csv`);
|
|
1527
|
+
state.logStream = fs.createWriteStream(file, { flags: "a" });
|
|
1528
|
+
state.logStream.write("timestamp,cpu_usage,mem_usage_percent,temp_c,net_rx_bytes,net_tx_bytes,net_rx_packets,net_tx_packets,connections,thermal_state\n");
|
|
1529
|
+
state.logging = true;
|
|
897
1530
|
setStatus(`Logging every tick -> ${file}`);
|
|
898
1531
|
} catch (err) {
|
|
899
1532
|
setStatus(`Logging failed to start: ${err.message}`);
|
|
900
1533
|
}
|
|
901
1534
|
}
|
|
902
1535
|
function stopLogging() {
|
|
903
|
-
logging = false;
|
|
904
|
-
logStream?.end();
|
|
905
|
-
logStream = null;
|
|
1536
|
+
state.logging = false;
|
|
1537
|
+
state.logStream?.end();
|
|
1538
|
+
state.logStream = null;
|
|
906
1539
|
setStatus("Logging stopped");
|
|
907
1540
|
}
|
|
908
1541
|
function toggleLogging() {
|
|
909
|
-
if (logging) stopLogging();
|
|
1542
|
+
if (state.logging) stopLogging();
|
|
910
1543
|
else startLogging();
|
|
911
1544
|
}
|
|
912
1545
|
function writeLogRow(data) {
|
|
913
|
-
if (!logging || !logStream) return;
|
|
1546
|
+
if (!state.logging || !state.logStream) return;
|
|
914
1547
|
const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? "";
|
|
915
|
-
|
|
916
|
-
|
|
1548
|
+
const rxPackets = data.network.rxPackets ?? 0;
|
|
1549
|
+
const txPackets = data.network.txPackets ?? 0;
|
|
1550
|
+
const connections = data.network.connections ?? 0;
|
|
1551
|
+
state.logStream.write(
|
|
1552
|
+
`${data.timestamp},${data.cpu.usage},${data.memory.usagePercent},${temp},${data.network.rxBytes},${data.network.txBytes},${rxPackets},${txPackets},${connections},${data.thermal.state}
|
|
917
1553
|
`
|
|
918
1554
|
);
|
|
919
1555
|
}
|
|
1556
|
+
|
|
1557
|
+
// src/live/render.ts
|
|
1558
|
+
import chalk5 from "chalk";
|
|
920
1559
|
function processRowBudget() {
|
|
921
|
-
const columns = gridColumns(termWidth);
|
|
922
|
-
const
|
|
923
|
-
const
|
|
924
|
-
|
|
1560
|
+
const columns = gridColumns(state.termWidth);
|
|
1561
|
+
const visibleCards = Object.values(state.visiblePanels).filter((v) => v !== false).length;
|
|
1562
|
+
const cardRows = Math.ceil(visibleCards / columns);
|
|
1563
|
+
const reserved = 2 + cardRows * 8 + 11 + (state.showGraphs ? 9 : 0) + 4;
|
|
1564
|
+
return Math.max(3, Math.min(40, state.termHeight - reserved));
|
|
925
1565
|
}
|
|
926
1566
|
function renderCustomizerOverlay() {
|
|
927
1567
|
const themesList = Object.keys(THEMES);
|
|
928
1568
|
const lines = [
|
|
929
|
-
|
|
1569
|
+
chalk5.bgCyan.black.bold(" UI CUSTOMIZER (Use \u2191/\u2193 or J/K to navigate, Enter/Space to toggle, Esc to exit) ")
|
|
930
1570
|
];
|
|
931
|
-
CUSTOMIZER_OPTIONS.forEach((opt, idx) => {
|
|
932
|
-
const isSelected = idx === customizerIndex;
|
|
933
|
-
const prefix = isSelected ?
|
|
1571
|
+
state.CUSTOMIZER_OPTIONS.forEach((opt, idx) => {
|
|
1572
|
+
const isSelected = idx === state.customizerIndex;
|
|
1573
|
+
const prefix = isSelected ? chalk5.yellow("\u25B6 ") : " ";
|
|
934
1574
|
if (opt === "Theme") {
|
|
935
|
-
lines.push(`${prefix}${opt}: ${
|
|
1575
|
+
lines.push(`${prefix}${opt}: ${chalk5.bold.green(state.currentTheme)} [${themesList.join(", ")}]`);
|
|
1576
|
+
} else if (opt === "Graph Mode") {
|
|
1577
|
+
lines.push(`${prefix}${opt}: ${chalk5.bold.green(state.graphMode)} [spark, bar]`);
|
|
1578
|
+
} else if (opt === "Toggle Tree View") {
|
|
1579
|
+
const status = state.treeView ? chalk5.green("[VISIBLE]") : chalk5.red("[HIDDEN]");
|
|
1580
|
+
lines.push(`${prefix}${opt}: ${status}`);
|
|
936
1581
|
} else {
|
|
937
1582
|
const key = opt.replace("Toggle ", "").toLowerCase();
|
|
938
|
-
const isVisible = visiblePanels[key] !== false;
|
|
939
|
-
const status = isVisible ?
|
|
1583
|
+
const isVisible = state.visiblePanels[key] !== false;
|
|
1584
|
+
const status = isVisible ? chalk5.green("[VISIBLE]") : chalk5.red("[HIDDEN]");
|
|
940
1585
|
lines.push(`${prefix}${opt}: ${status}`);
|
|
941
1586
|
}
|
|
942
1587
|
});
|
|
943
1588
|
return lines.map((l) => ` ${l}`).join("\n");
|
|
944
1589
|
}
|
|
945
1590
|
function render() {
|
|
946
|
-
if (!lastData) return;
|
|
1591
|
+
if (!state.lastData) return;
|
|
947
1592
|
const lines = [];
|
|
948
1593
|
lines.push(
|
|
949
|
-
formatTable(lastData, {
|
|
950
|
-
width: termWidth,
|
|
951
|
-
sortBy: sortMode,
|
|
952
|
-
filter: processFilter || void 0,
|
|
1594
|
+
formatTable(state.lastData, {
|
|
1595
|
+
width: state.termWidth,
|
|
1596
|
+
sortBy: state.sortMode,
|
|
1597
|
+
filter: state.processFilter || void 0,
|
|
953
1598
|
processLimit: processRowBudget(),
|
|
954
|
-
theme: currentTheme,
|
|
955
|
-
visible: visiblePanels
|
|
1599
|
+
theme: state.currentTheme,
|
|
1600
|
+
visible: state.visiblePanels,
|
|
1601
|
+
treeView: state.treeView,
|
|
1602
|
+
activePanel: state.activePanel
|
|
956
1603
|
})
|
|
957
1604
|
);
|
|
958
|
-
if (showGraphs) {
|
|
1605
|
+
if (state.showGraphs) {
|
|
959
1606
|
lines.push("");
|
|
960
|
-
lines.push(formatGraphs(history, termWidth, currentTheme));
|
|
1607
|
+
lines.push(formatGraphs(state.history, state.termWidth, state.currentTheme, state.graphMode));
|
|
961
1608
|
}
|
|
962
1609
|
lines.push("");
|
|
963
1610
|
lines.push(footerLine());
|
|
964
|
-
if (inputMode === "customizer") {
|
|
1611
|
+
if (state.inputMode === "customizer") {
|
|
965
1612
|
lines.push(renderCustomizerOverlay());
|
|
966
|
-
} else if (inputMode === "filter") {
|
|
967
|
-
lines.push(
|
|
968
|
-
} else if (inputMode === "kill") {
|
|
969
|
-
lines.push(
|
|
970
|
-
} else if (
|
|
971
|
-
|
|
1613
|
+
} else if (state.inputMode === "filter") {
|
|
1614
|
+
lines.push(chalk5.cyan(` Filter processes: ${state.inputBuffer}_`));
|
|
1615
|
+
} else if (state.inputMode === "kill") {
|
|
1616
|
+
lines.push(chalk5.cyan(` Kill PID: ${state.inputBuffer}_ (enter to confirm, esc to cancel)`));
|
|
1617
|
+
} else if (state.inputMode === "signal") {
|
|
1618
|
+
const sigIdx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer);
|
|
1619
|
+
const sigList = state.SIGNAL_OPTIONS.map((s, i) => i === sigIdx ? chalk5.yellow(s) : chalk5.dim(s)).join(" ");
|
|
1620
|
+
lines.push(chalk5.cyan(` Signal: ${state.inputBuffer}_`));
|
|
1621
|
+
lines.push(chalk5.dim(` ${sigList}`));
|
|
1622
|
+
lines.push(chalk5.dim(" \u2191/\u2193 to select, Enter to confirm, Esc to cancel"));
|
|
1623
|
+
} else if (state.statusMessage) {
|
|
1624
|
+
lines.push(` ${state.statusMessage}`);
|
|
972
1625
|
}
|
|
973
1626
|
process.stdout.write("\x1B[2J\x1B[H");
|
|
974
1627
|
process.stdout.write(lines.join("\n"));
|
|
@@ -977,141 +1630,178 @@ function footerLine() {
|
|
|
977
1630
|
const keys = [
|
|
978
1631
|
["q", "quit"],
|
|
979
1632
|
["c", "customize UI"],
|
|
980
|
-
["p", paused ? "resume" : "pause"],
|
|
981
|
-
["g", showGraphs ? "hide graphs" : "show graphs"],
|
|
982
|
-
["
|
|
983
|
-
["
|
|
1633
|
+
["p", state.paused ? "resume" : "pause"],
|
|
1634
|
+
["g", state.showGraphs ? "hide graphs" : "show graphs"],
|
|
1635
|
+
["b", `graph:${state.graphMode}`],
|
|
1636
|
+
["d", state.detailed ? "basic" : "detailed"],
|
|
1637
|
+
["\u2190/\u2192", "tab"],
|
|
1638
|
+
["1-9", "panels"],
|
|
1639
|
+
["s", `sort:${state.sortMode}`],
|
|
984
1640
|
["/", "filter"],
|
|
985
1641
|
["k", "kill"],
|
|
1642
|
+
["S", "signal"],
|
|
1643
|
+
["t", state.treeView ? "flat" : "tree"],
|
|
986
1644
|
["e", "export"],
|
|
987
|
-
["l", logging ? "stop log" : "log"],
|
|
988
|
-
["f", exportFormat],
|
|
989
|
-
["+/-", `${interval}s`]
|
|
1645
|
+
["l", state.logging ? "stop log" : "log"],
|
|
1646
|
+
["f", state.exportFormat],
|
|
1647
|
+
["+/-", `${state.interval}s`]
|
|
990
1648
|
];
|
|
991
|
-
const keyStr = keys.map(([k, label]) => `${
|
|
1649
|
+
const keyStr = keys.map(([k, label]) => `${chalk5.hex("#50fa7b").bold(k)} ${chalk5.dim(label)}`).join(chalk5.dim(" | "));
|
|
992
1650
|
const badges = [];
|
|
993
|
-
if (paused) badges.push(
|
|
994
|
-
if (logging) badges.push(
|
|
1651
|
+
if (state.paused) badges.push(chalk5.yellow.bold("\u23F8 PAUSED"));
|
|
1652
|
+
if (state.logging) badges.push(chalk5.red.bold("\u25CF REC"));
|
|
1653
|
+
if (state.activePanel !== "grid") badges.push(chalk5.cyan.bold(`\u25C9 ${state.activePanel.toUpperCase()}`));
|
|
995
1654
|
const badgeStr = badges.join(" ");
|
|
996
1655
|
return badgeStr ? `${keyStr} ${badgeStr}` : keyStr;
|
|
997
1656
|
}
|
|
998
1657
|
function checkAlerts(data) {
|
|
999
1658
|
const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
|
|
1000
|
-
const hot = data.cpu.usage >= CPU_ALERT_PCT || temp !== null && temp >= TEMP_ALERT_C;
|
|
1001
|
-
if (hot && !alerted) {
|
|
1002
|
-
alerted = true;
|
|
1659
|
+
const hot = data.cpu.usage >= state.CPU_ALERT_PCT || temp !== null && temp >= state.TEMP_ALERT_C;
|
|
1660
|
+
if (hot && !state.alerted) {
|
|
1661
|
+
state.alerted = true;
|
|
1003
1662
|
process.stdout.write("\x07");
|
|
1004
|
-
const reason = data.cpu.usage >= CPU_ALERT_PCT ? `CPU at ${data.cpu.usage}%` : `Temp at ${temp}\xB0C`;
|
|
1005
|
-
setStatus(
|
|
1663
|
+
const reason = data.cpu.usage >= state.CPU_ALERT_PCT ? `CPU at ${data.cpu.usage}%` : `Temp at ${temp}\xB0C`;
|
|
1664
|
+
setStatus(chalk5.red(`\u26A0 Alert: ${reason}`), 5e3);
|
|
1006
1665
|
} else if (!hot) {
|
|
1007
|
-
alerted = false;
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
async function tick() {
|
|
1011
|
-
if (paused) return;
|
|
1012
|
-
try {
|
|
1013
|
-
const data = await collectAll({ detailed, processLimit: processRowBudget() });
|
|
1014
|
-
lastData = data;
|
|
1015
|
-
const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
|
|
1016
|
-
history.push({
|
|
1017
|
-
cpuUsage: data.cpu.usage,
|
|
1018
|
-
memUsage: data.memory.usagePercent,
|
|
1019
|
-
temp,
|
|
1020
|
-
rxBytes: data.network.rxBytes,
|
|
1021
|
-
txBytes: data.network.txBytes
|
|
1022
|
-
});
|
|
1023
|
-
writeLogRow(data);
|
|
1024
|
-
checkAlerts(data);
|
|
1025
|
-
render();
|
|
1026
|
-
} catch {
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
function killProcess(pidStr) {
|
|
1030
|
-
const pid = parseInt(pidStr, 10);
|
|
1031
|
-
if (!pid || pid <= 0) {
|
|
1032
|
-
setStatus(`Invalid PID: ${pidStr}`);
|
|
1033
|
-
return;
|
|
1034
|
-
}
|
|
1035
|
-
try {
|
|
1036
|
-
process.kill(pid, "SIGTERM");
|
|
1037
|
-
setStatus(`Sent SIGTERM to PID ${pid}`);
|
|
1038
|
-
} catch (err) {
|
|
1039
|
-
setStatus(`Failed to kill ${pid}: ${err.message}`);
|
|
1666
|
+
state.alerted = false;
|
|
1040
1667
|
}
|
|
1041
1668
|
}
|
|
1669
|
+
|
|
1670
|
+
// src/live/input.ts
|
|
1042
1671
|
function handleInputModeKey(str, key) {
|
|
1043
1672
|
if (key.name === "escape") {
|
|
1044
|
-
inputMode = null;
|
|
1045
|
-
inputBuffer = "";
|
|
1673
|
+
state.inputMode = null;
|
|
1674
|
+
state.inputBuffer = "";
|
|
1046
1675
|
render();
|
|
1047
1676
|
return;
|
|
1048
1677
|
}
|
|
1049
|
-
if (inputMode === "customizer") {
|
|
1678
|
+
if (state.inputMode === "customizer") {
|
|
1050
1679
|
const themesList = Object.keys(THEMES);
|
|
1051
1680
|
if (key.name === "up" || str === "k") {
|
|
1052
|
-
customizerIndex = (customizerIndex - 1 + CUSTOMIZER_OPTIONS.length) % CUSTOMIZER_OPTIONS.length;
|
|
1681
|
+
state.customizerIndex = (state.customizerIndex - 1 + state.CUSTOMIZER_OPTIONS.length) % state.CUSTOMIZER_OPTIONS.length;
|
|
1053
1682
|
} else if (key.name === "down" || str === "j") {
|
|
1054
|
-
customizerIndex = (customizerIndex + 1) % CUSTOMIZER_OPTIONS.length;
|
|
1683
|
+
state.customizerIndex = (state.customizerIndex + 1) % state.CUSTOMIZER_OPTIONS.length;
|
|
1055
1684
|
} else if (key.name === "return" || str === " ") {
|
|
1056
|
-
const selected = CUSTOMIZER_OPTIONS[customizerIndex];
|
|
1685
|
+
const selected = state.CUSTOMIZER_OPTIONS[state.customizerIndex];
|
|
1057
1686
|
if (selected === "Theme") {
|
|
1058
|
-
const nextIdx = (themesList.indexOf(currentTheme) + 1) % themesList.length;
|
|
1059
|
-
currentTheme = themesList[nextIdx];
|
|
1687
|
+
const nextIdx = (themesList.indexOf(state.currentTheme) + 1) % themesList.length;
|
|
1688
|
+
state.currentTheme = themesList[nextIdx];
|
|
1689
|
+
} else if (selected === "Toggle Tree View") {
|
|
1690
|
+
state.treeView = !state.treeView;
|
|
1691
|
+
} else if (selected === "Graph Mode") {
|
|
1692
|
+
state.graphMode = state.graphMode === "spark" ? "bar" : "spark";
|
|
1060
1693
|
} else {
|
|
1061
1694
|
const itemKey = selected.replace("Toggle ", "").toLowerCase();
|
|
1062
|
-
visiblePanels[itemKey] = !visiblePanels[itemKey];
|
|
1695
|
+
state.visiblePanels[itemKey] = !state.visiblePanels[itemKey];
|
|
1063
1696
|
}
|
|
1064
1697
|
}
|
|
1065
1698
|
render();
|
|
1066
1699
|
return;
|
|
1067
1700
|
}
|
|
1701
|
+
if (state.inputMode === "signal") {
|
|
1702
|
+
if (key.name === "up" || str === "k") {
|
|
1703
|
+
const idx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer);
|
|
1704
|
+
state.inputBuffer = state.SIGNAL_OPTIONS[(idx - 1 + state.SIGNAL_OPTIONS.length) % state.SIGNAL_OPTIONS.length];
|
|
1705
|
+
} else if (key.name === "down" || str === "j") {
|
|
1706
|
+
const idx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer);
|
|
1707
|
+
state.inputBuffer = state.SIGNAL_OPTIONS[(idx + 1) % state.SIGNAL_OPTIONS.length];
|
|
1708
|
+
} else if (key.name === "return" || str === " ") {
|
|
1709
|
+
killProcess(state.inputBuffer.trim(), state.inputBuffer.trim());
|
|
1710
|
+
state.inputMode = null;
|
|
1711
|
+
state.inputBuffer = "";
|
|
1712
|
+
render();
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
render();
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1068
1718
|
if (key.name === "return") {
|
|
1069
|
-
if (inputMode === "filter") {
|
|
1070
|
-
processFilter = inputBuffer.trim();
|
|
1071
|
-
setStatus(processFilter ? `Filtering: "${processFilter}"` : "Filter cleared");
|
|
1072
|
-
} else if (inputMode === "kill") {
|
|
1073
|
-
killProcess(inputBuffer.trim());
|
|
1719
|
+
if (state.inputMode === "filter") {
|
|
1720
|
+
state.processFilter = state.inputBuffer.trim();
|
|
1721
|
+
setStatus(state.processFilter ? `Filtering: "${state.processFilter}"` : "Filter cleared");
|
|
1722
|
+
} else if (state.inputMode === "kill") {
|
|
1723
|
+
killProcess(state.inputBuffer.trim());
|
|
1074
1724
|
}
|
|
1075
|
-
inputMode = null;
|
|
1076
|
-
inputBuffer = "";
|
|
1725
|
+
state.inputMode = null;
|
|
1726
|
+
state.inputBuffer = "";
|
|
1077
1727
|
render();
|
|
1078
1728
|
return;
|
|
1079
1729
|
}
|
|
1080
1730
|
if (key.name === "backspace") {
|
|
1081
|
-
inputBuffer = inputBuffer.slice(0, -1);
|
|
1731
|
+
state.inputBuffer = state.inputBuffer.slice(0, -1);
|
|
1082
1732
|
render();
|
|
1083
1733
|
return;
|
|
1084
1734
|
}
|
|
1085
1735
|
if (str && str.length === 1 && !key.ctrl && !key.meta) {
|
|
1086
|
-
inputBuffer += str;
|
|
1736
|
+
state.inputBuffer += str;
|
|
1087
1737
|
render();
|
|
1088
1738
|
}
|
|
1089
1739
|
}
|
|
1740
|
+
function killProcess(pidStr, signal = "SIGTERM") {
|
|
1741
|
+
const pid = parseInt(pidStr, 10);
|
|
1742
|
+
if (!pid || pid <= 0) {
|
|
1743
|
+
setStatus(`Invalid PID: ${pidStr}`);
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
try {
|
|
1747
|
+
process.kill(pid, signal);
|
|
1748
|
+
setStatus(`Sent ${signal} to PID ${pid}`);
|
|
1749
|
+
} catch (err) {
|
|
1750
|
+
setStatus(`Failed to send ${signal} to ${pid}: ${err.message}`);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1090
1753
|
function onResize() {
|
|
1091
|
-
termWidth = process.stdout.columns || 80;
|
|
1092
|
-
termHeight = process.stdout.rows || 24;
|
|
1093
|
-
history.setMaxLen(Math.max(20, Math.min(200, termWidth - 30)));
|
|
1754
|
+
state.termWidth = process.stdout.columns || 80;
|
|
1755
|
+
state.termHeight = process.stdout.rows || 24;
|
|
1756
|
+
state.history.setMaxLen(Math.max(20, Math.min(200, state.termWidth - 30)));
|
|
1094
1757
|
render();
|
|
1095
1758
|
}
|
|
1096
1759
|
function restartTicker() {
|
|
1097
|
-
if (intervalHandle) clearInterval(intervalHandle);
|
|
1098
|
-
intervalHandle = setInterval(tick, interval * 1e3);
|
|
1760
|
+
if (state.intervalHandle) clearInterval(state.intervalHandle);
|
|
1761
|
+
state.intervalHandle = setInterval(tick, state.interval * 1e3);
|
|
1762
|
+
}
|
|
1763
|
+
async function tick() {
|
|
1764
|
+
if (state.paused) return;
|
|
1765
|
+
try {
|
|
1766
|
+
const data = await collectAll({ detailed: state.detailed, processLimit: processRowBudget() });
|
|
1767
|
+
state.lastData = data;
|
|
1768
|
+
const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
|
|
1769
|
+
state.history.push({
|
|
1770
|
+
cpuUsage: data.cpu.usage,
|
|
1771
|
+
memUsage: data.memory.usagePercent,
|
|
1772
|
+
temp,
|
|
1773
|
+
rxBytes: data.network.rxBytes,
|
|
1774
|
+
txBytes: data.network.txBytes,
|
|
1775
|
+
gpuUtil: data.gpu?.utilization,
|
|
1776
|
+
powerWatts: data.power?.combinedWatts ?? data.power?.cpuWatts ?? 0,
|
|
1777
|
+
rxPackets: data.network.rxPackets,
|
|
1778
|
+
txPackets: data.network.txPackets,
|
|
1779
|
+
connections: data.network.connections ?? 0
|
|
1780
|
+
});
|
|
1781
|
+
writeLogRow(data);
|
|
1782
|
+
checkAlerts(data);
|
|
1783
|
+
render();
|
|
1784
|
+
} catch {
|
|
1785
|
+
}
|
|
1099
1786
|
}
|
|
1100
1787
|
async function startLive(opts2) {
|
|
1101
|
-
if (running) return;
|
|
1102
|
-
running = true;
|
|
1103
|
-
paused = false;
|
|
1104
|
-
detailed = !!opts2.detailed;
|
|
1105
|
-
interval = opts2.interval;
|
|
1106
|
-
if (opts2.exportDir) exportDir = opts2.exportDir;
|
|
1107
|
-
termWidth = process.stdout.columns || 80;
|
|
1108
|
-
termHeight = process.stdout.rows || 24;
|
|
1109
|
-
history.reset();
|
|
1110
|
-
history.setMaxLen(Math.max(20, Math.min(200, termWidth - 30)));
|
|
1788
|
+
if (state.running) return;
|
|
1789
|
+
state.running = true;
|
|
1790
|
+
state.paused = false;
|
|
1791
|
+
state.detailed = !!opts2.detailed;
|
|
1792
|
+
state.interval = opts2.interval;
|
|
1793
|
+
if (opts2.exportDir) state.exportDir = opts2.exportDir;
|
|
1794
|
+
state.termWidth = process.stdout.columns || 80;
|
|
1795
|
+
state.termHeight = process.stdout.rows || 24;
|
|
1796
|
+
state.history.reset();
|
|
1797
|
+
state.history.setMaxLen(Math.max(20, Math.min(200, state.termWidth - 30)));
|
|
1111
1798
|
process.stdout.write("\x1B[?1049h");
|
|
1112
1799
|
process.stdout.write("\x1B[?25l");
|
|
1113
1800
|
process.stdout.write("\x1B[2J\x1B[H");
|
|
1114
1801
|
process.title = "pyre";
|
|
1802
|
+
if (state.mouseEnabled) {
|
|
1803
|
+
process.stdout.write("\x1B[?1000h");
|
|
1804
|
+
}
|
|
1115
1805
|
await tick();
|
|
1116
1806
|
restartTicker();
|
|
1117
1807
|
if (opts2.autoLog) startLogging();
|
|
@@ -1119,63 +1809,150 @@ async function startLive(opts2) {
|
|
|
1119
1809
|
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
1120
1810
|
process.stdin.resume();
|
|
1121
1811
|
process.stdout.on("resize", onResize);
|
|
1122
|
-
keypressHandler = (str, key) => {
|
|
1812
|
+
state.keypressHandler = (str, key) => {
|
|
1123
1813
|
if (!key) return;
|
|
1124
1814
|
if (key.ctrl && key.name === "c") {
|
|
1125
1815
|
stopLive();
|
|
1126
1816
|
return;
|
|
1127
1817
|
}
|
|
1128
|
-
if (inputMode) {
|
|
1818
|
+
if (state.inputMode) {
|
|
1129
1819
|
handleInputModeKey(str, key);
|
|
1130
1820
|
return;
|
|
1131
1821
|
}
|
|
1822
|
+
if (key.mouse) {
|
|
1823
|
+
handleMouseClick(key.mouse);
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
1826
|
+
if (key.sequence && key.sequence.startsWith("\x1B[M")) {
|
|
1827
|
+
handleNormalMouse(key.sequence);
|
|
1828
|
+
return;
|
|
1829
|
+
}
|
|
1830
|
+
if (key.sequence && key.sequence.startsWith("\x1B[<")) {
|
|
1831
|
+
handleSGRMouse(key.sequence);
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1132
1834
|
switch (key.sequence) {
|
|
1133
1835
|
case "+":
|
|
1134
|
-
interval += 1;
|
|
1836
|
+
state.interval += 1;
|
|
1135
1837
|
restartTicker();
|
|
1136
|
-
setStatus(`Interval set to ${interval}s`);
|
|
1838
|
+
setStatus(`Interval set to ${state.interval}s`);
|
|
1137
1839
|
return;
|
|
1138
1840
|
case "-":
|
|
1139
|
-
interval = Math.max(1, interval - 1);
|
|
1841
|
+
state.interval = Math.max(1, state.interval - 1);
|
|
1140
1842
|
restartTicker();
|
|
1141
|
-
setStatus(`Interval set to ${interval}s`);
|
|
1843
|
+
setStatus(`Interval set to ${state.interval}s`);
|
|
1142
1844
|
return;
|
|
1143
1845
|
case "/":
|
|
1144
|
-
inputMode = "filter";
|
|
1145
|
-
inputBuffer = processFilter;
|
|
1846
|
+
state.inputMode = "filter";
|
|
1847
|
+
state.inputBuffer = state.processFilter;
|
|
1848
|
+
render();
|
|
1849
|
+
return;
|
|
1850
|
+
case "0":
|
|
1851
|
+
case "1":
|
|
1852
|
+
case "2":
|
|
1853
|
+
case "3":
|
|
1854
|
+
case "4":
|
|
1855
|
+
case "5":
|
|
1856
|
+
case "6":
|
|
1857
|
+
case "7":
|
|
1858
|
+
case "8":
|
|
1859
|
+
case "9": {
|
|
1860
|
+
const tabMap = {
|
|
1861
|
+
"0": "disk",
|
|
1862
|
+
"1": "cpu",
|
|
1863
|
+
"2": "mem",
|
|
1864
|
+
"3": "gpu",
|
|
1865
|
+
"4": "power",
|
|
1866
|
+
"5": "battery",
|
|
1867
|
+
"6": "thermal",
|
|
1868
|
+
"7": "network",
|
|
1869
|
+
"8": "packets",
|
|
1870
|
+
"9": "tasks"
|
|
1871
|
+
};
|
|
1872
|
+
const panelId = tabMap[key.sequence];
|
|
1873
|
+
if (panelId) {
|
|
1874
|
+
state.activePanel = state.activePanel === panelId ? "grid" : panelId;
|
|
1875
|
+
setStatus(state.activePanel === "grid" ? "Grid view" : `${panelId} panel`);
|
|
1876
|
+
render();
|
|
1877
|
+
}
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
case "P": {
|
|
1881
|
+
state.activePanel = state.activePanel === "process" ? "grid" : "process";
|
|
1882
|
+
setStatus(state.activePanel === "grid" ? "Grid view" : "Process panel");
|
|
1146
1883
|
render();
|
|
1147
1884
|
return;
|
|
1885
|
+
}
|
|
1148
1886
|
}
|
|
1149
1887
|
switch (key.name) {
|
|
1150
1888
|
case "q":
|
|
1151
1889
|
stopLive();
|
|
1152
1890
|
break;
|
|
1891
|
+
case "left": {
|
|
1892
|
+
const currentIdx = state.PANEL_TABS.findIndex((t) => t.id === state.activePanel);
|
|
1893
|
+
if (currentIdx > 0) {
|
|
1894
|
+
state.activePanel = state.PANEL_TABS[currentIdx - 1].id;
|
|
1895
|
+
} else if (currentIdx === -1) {
|
|
1896
|
+
state.activePanel = state.PANEL_TABS[state.PANEL_TABS.length - 1].id;
|
|
1897
|
+
}
|
|
1898
|
+
setStatus(`Panel: ${state.activePanel}`);
|
|
1899
|
+
render();
|
|
1900
|
+
break;
|
|
1901
|
+
}
|
|
1902
|
+
case "right": {
|
|
1903
|
+
const currentIdx = state.PANEL_TABS.findIndex((t) => t.id === state.activePanel);
|
|
1904
|
+
if (currentIdx < state.PANEL_TABS.length - 1) {
|
|
1905
|
+
state.activePanel = state.PANEL_TABS[currentIdx + 1].id;
|
|
1906
|
+
} else if (currentIdx === -1) {
|
|
1907
|
+
state.activePanel = state.PANEL_TABS[0].id;
|
|
1908
|
+
}
|
|
1909
|
+
setStatus(`Panel: ${state.activePanel}`);
|
|
1910
|
+
render();
|
|
1911
|
+
break;
|
|
1912
|
+
}
|
|
1153
1913
|
case "c":
|
|
1154
|
-
inputMode = "customizer";
|
|
1155
|
-
customizerIndex = 0;
|
|
1914
|
+
state.inputMode = "customizer";
|
|
1915
|
+
state.customizerIndex = 0;
|
|
1156
1916
|
render();
|
|
1157
1917
|
break;
|
|
1158
1918
|
case "p":
|
|
1159
|
-
paused = !paused;
|
|
1160
|
-
setStatus(paused ? "Paused" : "Resumed");
|
|
1919
|
+
state.paused = !state.paused;
|
|
1920
|
+
setStatus(state.paused ? "Paused" : "Resumed");
|
|
1161
1921
|
render();
|
|
1162
1922
|
break;
|
|
1163
1923
|
case "g":
|
|
1164
|
-
showGraphs = !showGraphs;
|
|
1924
|
+
state.showGraphs = !state.showGraphs;
|
|
1925
|
+
render();
|
|
1926
|
+
break;
|
|
1927
|
+
case "b":
|
|
1928
|
+
state.graphMode = state.graphMode === "spark" ? "bar" : "spark";
|
|
1929
|
+
setStatus(`Graph mode: ${state.graphMode}`);
|
|
1165
1930
|
render();
|
|
1166
1931
|
break;
|
|
1167
1932
|
case "d":
|
|
1168
|
-
detailed = !detailed;
|
|
1169
|
-
setStatus(`Detailed sensor mode: ${detailed ? "on" : "off"}`);
|
|
1933
|
+
state.detailed = !state.detailed;
|
|
1934
|
+
setStatus(`Detailed sensor mode: ${state.detailed ? "on" : "off"}`);
|
|
1170
1935
|
break;
|
|
1171
1936
|
case "s":
|
|
1172
|
-
|
|
1173
|
-
|
|
1937
|
+
const sortCycle = ["cpu", "mem", "pid", "user", "command", "state", "threads", "runtime"];
|
|
1938
|
+
const curIdx = sortCycle.indexOf(state.sortMode);
|
|
1939
|
+
state.sortMode = sortCycle[(curIdx + 1) % sortCycle.length];
|
|
1940
|
+
setStatus(`Sorting by ${state.sortMode}`);
|
|
1174
1941
|
render();
|
|
1175
1942
|
break;
|
|
1176
1943
|
case "k":
|
|
1177
|
-
inputMode = "kill";
|
|
1178
|
-
inputBuffer = "";
|
|
1944
|
+
state.inputMode = "kill";
|
|
1945
|
+
state.inputBuffer = "";
|
|
1946
|
+
render();
|
|
1947
|
+
break;
|
|
1948
|
+
case "S":
|
|
1949
|
+
state.inputMode = "signal";
|
|
1950
|
+
state.inputBuffer = "SIGTERM";
|
|
1951
|
+
render();
|
|
1952
|
+
break;
|
|
1953
|
+
case "t":
|
|
1954
|
+
state.treeView = !state.treeView;
|
|
1955
|
+
setStatus(state.treeView ? "Tree view enabled" : "Flat view enabled");
|
|
1179
1956
|
render();
|
|
1180
1957
|
break;
|
|
1181
1958
|
case "e":
|
|
@@ -1187,46 +1964,585 @@ async function startLive(opts2) {
|
|
|
1187
1964
|
render();
|
|
1188
1965
|
break;
|
|
1189
1966
|
case "f":
|
|
1190
|
-
exportFormat = exportFormat === "json" ? "csv" : exportFormat === "csv" ? "tsv" : "json";
|
|
1191
|
-
setStatus(`Export format: ${exportFormat}`);
|
|
1967
|
+
state.exportFormat = state.exportFormat === "json" ? "csv" : state.exportFormat === "csv" ? "tsv" : "json";
|
|
1968
|
+
setStatus(`Export format: ${state.exportFormat}`);
|
|
1192
1969
|
render();
|
|
1193
1970
|
break;
|
|
1971
|
+
case "tab":
|
|
1972
|
+
cycleTab(1);
|
|
1973
|
+
break;
|
|
1974
|
+
case "left":
|
|
1975
|
+
cycleTab(-1);
|
|
1976
|
+
break;
|
|
1977
|
+
case "right":
|
|
1978
|
+
cycleTab(1);
|
|
1979
|
+
break;
|
|
1980
|
+
default:
|
|
1981
|
+
if (str && str.length === 1) {
|
|
1982
|
+
const tabId = tabKeyToId(str);
|
|
1983
|
+
if (tabId) {
|
|
1984
|
+
state.activePanel = tabId;
|
|
1985
|
+
setStatus(`Panel: ${tabId.toUpperCase()}`);
|
|
1986
|
+
render();
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1194
1989
|
}
|
|
1195
1990
|
};
|
|
1196
|
-
process.stdin.on("keypress", keypressHandler);
|
|
1991
|
+
process.stdin.on("keypress", state.keypressHandler);
|
|
1197
1992
|
process.once("SIGINT", () => stopLive());
|
|
1198
1993
|
}
|
|
1994
|
+
function tabKeyToId(str) {
|
|
1995
|
+
const map = { "1": "cpu", "2": "mem", "3": "gpu", "4": "power", "5": "battery", "6": "thermal", "7": "network", "8": "packets", "9": "tasks", "0": "disk", "p": "process" };
|
|
1996
|
+
return map[str] ?? null;
|
|
1997
|
+
}
|
|
1998
|
+
function cycleTab(direction) {
|
|
1999
|
+
const tabs = state.PANEL_TABS.map((t) => t.id);
|
|
2000
|
+
const current = state.activePanel === "grid" ? -1 : tabs.indexOf(state.activePanel);
|
|
2001
|
+
const next = (current + direction + tabs.length) % tabs.length;
|
|
2002
|
+
state.activePanel = tabs[next];
|
|
2003
|
+
setStatus(`Panel: ${state.activePanel.toUpperCase()}`);
|
|
2004
|
+
render();
|
|
2005
|
+
}
|
|
2006
|
+
function handleNormalMouse(seq) {
|
|
2007
|
+
if (seq.length < 6) return;
|
|
2008
|
+
const cb = seq.charCodeAt(3) - 32;
|
|
2009
|
+
const cx = seq.charCodeAt(4) - 32;
|
|
2010
|
+
const cy = seq.charCodeAt(5) - 32;
|
|
2011
|
+
if (cb === 0 || cb === 1) {
|
|
2012
|
+
handleMouseClick(cy, cx);
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
function handleSGRMouse(seq) {
|
|
2016
|
+
const match = seq.match(/^\x1b\[<(\d+);(\d+);(\d+)([Mm])/);
|
|
2017
|
+
if (!match) return;
|
|
2018
|
+
const button = parseInt(match[1]);
|
|
2019
|
+
const cx = parseInt(match[2]);
|
|
2020
|
+
const cy = parseInt(match[3]);
|
|
2021
|
+
if (button === 0 || button === 1) {
|
|
2022
|
+
handleMouseClick(cy, cx);
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
function handleMouseClick(y, x) {
|
|
2026
|
+
if (y !== TAB_BAR_ROW) return;
|
|
2027
|
+
const hitboxes = getTabHitboxes();
|
|
2028
|
+
const hit = hitboxes.find((h) => x >= h.start && x < h.end);
|
|
2029
|
+
if (hit) {
|
|
2030
|
+
state.activePanel = state.activePanel === hit.id ? "grid" : hit.id;
|
|
2031
|
+
setStatus(state.activePanel === "grid" ? "Grid view" : `Panel: ${hit.id.toUpperCase()}`);
|
|
2032
|
+
render();
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
1199
2035
|
function stopLive() {
|
|
1200
|
-
if (intervalHandle) {
|
|
1201
|
-
clearInterval(intervalHandle);
|
|
1202
|
-
intervalHandle = null;
|
|
2036
|
+
if (state.intervalHandle) {
|
|
2037
|
+
clearInterval(state.intervalHandle);
|
|
2038
|
+
state.intervalHandle = null;
|
|
1203
2039
|
}
|
|
1204
|
-
if (statusTimer) {
|
|
1205
|
-
clearTimeout(statusTimer);
|
|
1206
|
-
statusTimer = null;
|
|
2040
|
+
if (state.statusTimer) {
|
|
2041
|
+
clearTimeout(state.statusTimer);
|
|
2042
|
+
state.statusTimer = null;
|
|
1207
2043
|
}
|
|
1208
|
-
if (logStream) {
|
|
1209
|
-
logStream.end();
|
|
1210
|
-
logStream = null;
|
|
2044
|
+
if (state.logStream) {
|
|
2045
|
+
state.logStream.end();
|
|
2046
|
+
state.logStream = null;
|
|
1211
2047
|
}
|
|
1212
|
-
if (keypressHandler) {
|
|
1213
|
-
process.stdin.removeListener("keypress", keypressHandler);
|
|
1214
|
-
keypressHandler = null;
|
|
2048
|
+
if (state.keypressHandler) {
|
|
2049
|
+
process.stdin.removeListener("keypress", state.keypressHandler);
|
|
2050
|
+
state.keypressHandler = null;
|
|
1215
2051
|
}
|
|
1216
2052
|
process.stdout.removeListener("resize", onResize);
|
|
1217
|
-
running = false;
|
|
1218
|
-
logging = false;
|
|
2053
|
+
state.running = false;
|
|
2054
|
+
state.logging = false;
|
|
1219
2055
|
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
1220
2056
|
process.stdin.pause();
|
|
2057
|
+
if (state.mouseEnabled) {
|
|
2058
|
+
process.stdout.write("\x1B[?1000l");
|
|
2059
|
+
}
|
|
1221
2060
|
process.stdout.write("\x1B[?25h");
|
|
1222
2061
|
process.stdout.write("\x1B[?1049l");
|
|
1223
2062
|
process.stdout.write("\x1B[2J\x1B[H");
|
|
1224
2063
|
process.exit(0);
|
|
1225
2064
|
}
|
|
1226
2065
|
|
|
2066
|
+
// src/splash.ts
|
|
2067
|
+
import chalk6 from "chalk";
|
|
2068
|
+
var MAXV = 36;
|
|
2069
|
+
var CHAR_RAMP = [" ", ".", "`", ",", ":", ";", "+", "*", "?", "%", "#", "@"];
|
|
2070
|
+
var palette = [
|
|
2071
|
+
"#0a0505",
|
|
2072
|
+
"#1a0806",
|
|
2073
|
+
"#2a0b06",
|
|
2074
|
+
"#3f1004",
|
|
2075
|
+
"#591404",
|
|
2076
|
+
"#791804",
|
|
2077
|
+
"#9e1c04",
|
|
2078
|
+
"#c92004",
|
|
2079
|
+
"#d63004",
|
|
2080
|
+
"#e34004",
|
|
2081
|
+
"#f05004",
|
|
2082
|
+
"#fd6004",
|
|
2083
|
+
"#ff7004",
|
|
2084
|
+
"#ff8004",
|
|
2085
|
+
"#ff9004",
|
|
2086
|
+
"#ffa004",
|
|
2087
|
+
"#ffb004",
|
|
2088
|
+
"#ffc004",
|
|
2089
|
+
"#ffd004",
|
|
2090
|
+
"#ffe004",
|
|
2091
|
+
"#fff004",
|
|
2092
|
+
"#fff820",
|
|
2093
|
+
"#ffff40",
|
|
2094
|
+
"#ffff60",
|
|
2095
|
+
"#ffff80",
|
|
2096
|
+
"#ffffa0",
|
|
2097
|
+
"#ffffc0",
|
|
2098
|
+
"#ffffe0",
|
|
2099
|
+
"#ffffff",
|
|
2100
|
+
"#ffffff",
|
|
2101
|
+
"#ffffff",
|
|
2102
|
+
"#ffffff",
|
|
2103
|
+
"#ffffff",
|
|
2104
|
+
"#ffffff",
|
|
2105
|
+
"#ffffff",
|
|
2106
|
+
"#ffffff"
|
|
2107
|
+
];
|
|
2108
|
+
function getRgb(hex) {
|
|
2109
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
2110
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
2111
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
2112
|
+
return { r, g, b };
|
|
2113
|
+
}
|
|
2114
|
+
var rgbPalette = palette.map(getRgb);
|
|
2115
|
+
function getChar(val) {
|
|
2116
|
+
const idx = Math.min(CHAR_RAMP.length - 1, Math.floor(val / (MAXV + 1) * CHAR_RAMP.length));
|
|
2117
|
+
return CHAR_RAMP[idx];
|
|
2118
|
+
}
|
|
2119
|
+
async function showSplash() {
|
|
2120
|
+
const cols = 60;
|
|
2121
|
+
const rows = 15;
|
|
2122
|
+
let fire = new Uint8Array(cols * rows);
|
|
2123
|
+
process.stdout.write("\x1B[2J");
|
|
2124
|
+
const termRows = process.stdout.rows || 24;
|
|
2125
|
+
const termCols = process.stdout.columns || 80;
|
|
2126
|
+
const startRow = Math.max(0, Math.floor((termRows - rows) / 2));
|
|
2127
|
+
const startCol = Math.max(0, Math.floor((termCols - cols) / 2));
|
|
2128
|
+
return new Promise((resolve) => {
|
|
2129
|
+
const interval = setInterval(() => {
|
|
2130
|
+
for (let x = 0; x < cols; x++) {
|
|
2131
|
+
fire[(rows - 1) * cols + x] = MAXV - Math.floor(Math.random() * 4);
|
|
2132
|
+
}
|
|
2133
|
+
for (let y = rows - 1; y >= 1; y--) {
|
|
2134
|
+
for (let x = 0; x < cols; x++) {
|
|
2135
|
+
const src = y * cols + x;
|
|
2136
|
+
const val = fire[src];
|
|
2137
|
+
if (val === 0) {
|
|
2138
|
+
fire[(y - 1) * cols + x] = 0;
|
|
2139
|
+
continue;
|
|
2140
|
+
}
|
|
2141
|
+
const decay = Math.floor(Math.random() * 3);
|
|
2142
|
+
const drift = Math.floor(Math.random() * 3) - 1;
|
|
2143
|
+
let nx = x + drift;
|
|
2144
|
+
if (nx < 0) nx = 0;
|
|
2145
|
+
else if (nx >= cols) nx = cols - 1;
|
|
2146
|
+
fire[(y - 1) * cols + nx] = Math.max(0, val - decay);
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
let output = "";
|
|
2150
|
+
for (let i = 0; i < startRow; i++) output += "\n";
|
|
2151
|
+
for (let y = 0; y < rows; y++) {
|
|
2152
|
+
output += " ".repeat(startCol);
|
|
2153
|
+
for (let x = 0; x < cols; x++) {
|
|
2154
|
+
const v = fire[y * cols + x];
|
|
2155
|
+
const color = rgbPalette[Math.min(v, rgbPalette.length - 1)];
|
|
2156
|
+
output += chalk6.rgb(color.r, color.g, color.b)(getChar(v));
|
|
2157
|
+
}
|
|
2158
|
+
output += "\n";
|
|
2159
|
+
}
|
|
2160
|
+
output += " ".repeat(Math.max(0, startCol + (cols - 45) / 2)) + chalk6.bold.white("\u{1F525} pyre \u2014 system monitor (warming up sensors...)\n");
|
|
2161
|
+
output += " ".repeat(Math.max(0, startCol + (cols - 25) / 2)) + chalk6.dim("[Press Enter to continue]");
|
|
2162
|
+
process.stdout.write(`\x1B[H${output}`);
|
|
2163
|
+
}, 65);
|
|
2164
|
+
const onEnter = () => {
|
|
2165
|
+
process.stdin.removeListener("data", onEnter);
|
|
2166
|
+
clearInterval(interval);
|
|
2167
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
2168
|
+
resolve();
|
|
2169
|
+
};
|
|
2170
|
+
process.stdin.setRawMode(true);
|
|
2171
|
+
process.stdin.resume();
|
|
2172
|
+
process.stdin.on("data", onEnter);
|
|
2173
|
+
setTimeout(onEnter, 5e3);
|
|
2174
|
+
});
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
// src/p2p/server.ts
|
|
2178
|
+
import net from "net";
|
|
2179
|
+
import chalk7 from "chalk";
|
|
2180
|
+
|
|
2181
|
+
// src/p2p/protocol.ts
|
|
2182
|
+
var HEADER_SIZE = 4;
|
|
2183
|
+
var MAX_MESSAGE_SIZE = 16 * 1024 * 1024;
|
|
2184
|
+
function buildMessage(kind, payload) {
|
|
2185
|
+
const envelope = { kind, payload };
|
|
2186
|
+
const body = Buffer.from(JSON.stringify(envelope), "utf-8");
|
|
2187
|
+
const header2 = Buffer.alloc(HEADER_SIZE);
|
|
2188
|
+
header2.writeUInt32BE(body.length, 0);
|
|
2189
|
+
return Buffer.concat([header2, body]);
|
|
2190
|
+
}
|
|
2191
|
+
function encodeAuth(password) {
|
|
2192
|
+
return buildMessage("auth", { password });
|
|
2193
|
+
}
|
|
2194
|
+
function encodeAuthOk() {
|
|
2195
|
+
return buildMessage("auth-ok", { ok: true });
|
|
2196
|
+
}
|
|
2197
|
+
function encodeAuthFail(reason) {
|
|
2198
|
+
return buildMessage("auth-fail", { ok: false, reason });
|
|
2199
|
+
}
|
|
2200
|
+
function encodeData(stats) {
|
|
2201
|
+
return buildMessage("data", { stats });
|
|
2202
|
+
}
|
|
2203
|
+
function encodePing() {
|
|
2204
|
+
return buildMessage("ping", { ts: Date.now() });
|
|
2205
|
+
}
|
|
2206
|
+
function encodePong(ts) {
|
|
2207
|
+
return buildMessage("pong", { ts });
|
|
2208
|
+
}
|
|
2209
|
+
function encodeDisconnect(reason) {
|
|
2210
|
+
return buildMessage("disconnect", { reason });
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
// src/p2p/server.ts
|
|
2214
|
+
var HEADER_SIZE2 = 4;
|
|
2215
|
+
function readMessageFromPeer(peer) {
|
|
2216
|
+
while (peer.readBuffer.length > 0) {
|
|
2217
|
+
if (peer.readExpectingHeader) {
|
|
2218
|
+
if (peer.readBuffer.length >= HEADER_SIZE2) {
|
|
2219
|
+
peer.readMessageLength = peer.readBuffer.readUInt32BE(0);
|
|
2220
|
+
peer.readBuffer = peer.readBuffer.subarray(HEADER_SIZE2);
|
|
2221
|
+
peer.readExpectingHeader = false;
|
|
2222
|
+
} else {
|
|
2223
|
+
break;
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
if (!peer.readExpectingHeader) {
|
|
2227
|
+
if (peer.readBuffer.length >= peer.readMessageLength) {
|
|
2228
|
+
const body = peer.readBuffer.subarray(0, peer.readMessageLength);
|
|
2229
|
+
peer.readBuffer = peer.readBuffer.subarray(peer.readMessageLength);
|
|
2230
|
+
peer.readExpectingHeader = true;
|
|
2231
|
+
return body;
|
|
2232
|
+
} else {
|
|
2233
|
+
break;
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
return null;
|
|
2238
|
+
}
|
|
2239
|
+
var P2PServer = class {
|
|
2240
|
+
server = null;
|
|
2241
|
+
peers = /* @__PURE__ */ new Map();
|
|
2242
|
+
options;
|
|
2243
|
+
running = false;
|
|
2244
|
+
constructor(options) {
|
|
2245
|
+
this.options = options;
|
|
2246
|
+
}
|
|
2247
|
+
get info() {
|
|
2248
|
+
return {
|
|
2249
|
+
host: this.options.host,
|
|
2250
|
+
port: this.options.port,
|
|
2251
|
+
peersConnected: this.peers.size
|
|
2252
|
+
};
|
|
2253
|
+
}
|
|
2254
|
+
async start() {
|
|
2255
|
+
return new Promise((resolve, reject) => {
|
|
2256
|
+
this.server = net.createServer((socket) => this.onConnection(socket));
|
|
2257
|
+
this.server.on("error", (err) => reject(err));
|
|
2258
|
+
this.server.listen(this.options.port, this.options.host, () => {
|
|
2259
|
+
this.running = true;
|
|
2260
|
+
const addr = this.server?.address();
|
|
2261
|
+
const bound = addr ? typeof addr === "object" ? `${addr.address}:${addr.port}` : String(addr) : "unknown";
|
|
2262
|
+
console.log(chalk7.green(`P2P server listening on ${bound}`));
|
|
2263
|
+
console.log(chalk7.dim(` Password: ${this.options.password}`));
|
|
2264
|
+
console.log(chalk7.dim(` Interval: ${this.options.intervalMs}ms`));
|
|
2265
|
+
resolve();
|
|
2266
|
+
});
|
|
2267
|
+
});
|
|
2268
|
+
}
|
|
2269
|
+
stop() {
|
|
2270
|
+
this.running = false;
|
|
2271
|
+
for (const peer of this.peers.values()) {
|
|
2272
|
+
this.disconnectPeer(peer, "Server shutting down");
|
|
2273
|
+
}
|
|
2274
|
+
this.peers.clear();
|
|
2275
|
+
if (this.server) {
|
|
2276
|
+
this.server.close();
|
|
2277
|
+
this.server = null;
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
onConnection(socket) {
|
|
2281
|
+
const peer = {
|
|
2282
|
+
socket,
|
|
2283
|
+
authenticated: false,
|
|
2284
|
+
intervalHandle: null,
|
|
2285
|
+
pingHandle: null,
|
|
2286
|
+
readBuffer: Buffer.alloc(0),
|
|
2287
|
+
readExpectingHeader: true,
|
|
2288
|
+
readMessageLength: 0
|
|
2289
|
+
};
|
|
2290
|
+
this.peers.set(socket, peer);
|
|
2291
|
+
const remoteAddr = socket.remoteAddress ? `${socket.remoteAddress}:${socket.remotePort}` : "unknown";
|
|
2292
|
+
console.log(chalk7.cyan(`Peer connected: ${remoteAddr}`));
|
|
2293
|
+
socket.on("data", (data) => this.onData(peer, data));
|
|
2294
|
+
socket.on("close", () => this.onPeerClose(peer));
|
|
2295
|
+
socket.on("error", (err) => this.onPeerError(peer, err));
|
|
2296
|
+
socket.setTimeout(3e4);
|
|
2297
|
+
socket.on("timeout", () => {
|
|
2298
|
+
if (!peer.authenticated) {
|
|
2299
|
+
this.disconnectPeer(peer, "Authentication timeout");
|
|
2300
|
+
}
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
onData(peer, data) {
|
|
2304
|
+
peer.readBuffer = Buffer.concat([peer.readBuffer, data]);
|
|
2305
|
+
if (!peer.authenticated) {
|
|
2306
|
+
const body = readMessageFromPeer(peer);
|
|
2307
|
+
if (body) {
|
|
2308
|
+
this.handleAuthMessage(peer, body);
|
|
2309
|
+
}
|
|
2310
|
+
return;
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
handleAuthMessage(peer, body) {
|
|
2314
|
+
try {
|
|
2315
|
+
const msg = JSON.parse(body.toString("utf-8"));
|
|
2316
|
+
if (msg.kind === "auth" && typeof msg.payload?.password === "string") {
|
|
2317
|
+
if (msg.payload.password === this.options.password) {
|
|
2318
|
+
peer.authenticated = true;
|
|
2319
|
+
peer.socket.write(encodeAuthOk());
|
|
2320
|
+
console.log(chalk7.green(`Peer authenticated: ${peer.socket.remoteAddress}`));
|
|
2321
|
+
this.startDataStream(peer);
|
|
2322
|
+
this.startPing(peer);
|
|
2323
|
+
} else {
|
|
2324
|
+
peer.socket.write(encodeAuthFail("Invalid password"));
|
|
2325
|
+
this.disconnectPeer(peer, "Authentication failed");
|
|
2326
|
+
}
|
|
2327
|
+
} else {
|
|
2328
|
+
peer.socket.write(encodeAuthFail("Expected auth message first"));
|
|
2329
|
+
this.disconnectPeer(peer, "Expected auth message");
|
|
2330
|
+
}
|
|
2331
|
+
} catch {
|
|
2332
|
+
peer.socket.write(encodeAuthFail("Invalid message format"));
|
|
2333
|
+
this.disconnectPeer(peer, "Invalid message format");
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
startDataStream(peer) {
|
|
2337
|
+
const send = async () => {
|
|
2338
|
+
if (!peer.authenticated || peer.socket.destroyed) return;
|
|
2339
|
+
try {
|
|
2340
|
+
const data = await collectAll({ detailed: this.options.detailed });
|
|
2341
|
+
peer.socket.write(encodeData(data));
|
|
2342
|
+
} catch {
|
|
2343
|
+
}
|
|
2344
|
+
};
|
|
2345
|
+
send();
|
|
2346
|
+
peer.intervalHandle = setInterval(send, this.options.intervalMs);
|
|
2347
|
+
}
|
|
2348
|
+
startPing(peer) {
|
|
2349
|
+
peer.pingHandle = setInterval(() => {
|
|
2350
|
+
if (peer.socket.destroyed) return;
|
|
2351
|
+
peer.socket.write(encodePing());
|
|
2352
|
+
}, 5e3);
|
|
2353
|
+
}
|
|
2354
|
+
onPeerClose(peer) {
|
|
2355
|
+
this.cleanupPeer(peer);
|
|
2356
|
+
console.log(chalk7.yellow(`Peer disconnected: ${peer.socket.remoteAddress}`));
|
|
2357
|
+
}
|
|
2358
|
+
onPeerError(peer, err) {
|
|
2359
|
+
const code = err.code;
|
|
2360
|
+
if (code === "ECONNRESET") {
|
|
2361
|
+
console.log(chalk7.yellow(`Peer connection reset: ${peer.socket.remoteAddress}`));
|
|
2362
|
+
} else {
|
|
2363
|
+
console.log(chalk7.red(`Peer error: ${err.message}`));
|
|
2364
|
+
}
|
|
2365
|
+
this.cleanupPeer(peer);
|
|
2366
|
+
}
|
|
2367
|
+
disconnectPeer(peer, reason) {
|
|
2368
|
+
try {
|
|
2369
|
+
peer.socket.write(encodeDisconnect(reason));
|
|
2370
|
+
} catch {
|
|
2371
|
+
}
|
|
2372
|
+
peer.socket.destroy();
|
|
2373
|
+
}
|
|
2374
|
+
cleanupPeer(peer) {
|
|
2375
|
+
if (peer.intervalHandle) {
|
|
2376
|
+
clearInterval(peer.intervalHandle);
|
|
2377
|
+
peer.intervalHandle = null;
|
|
2378
|
+
}
|
|
2379
|
+
if (peer.pingHandle) {
|
|
2380
|
+
clearInterval(peer.pingHandle);
|
|
2381
|
+
peer.pingHandle = null;
|
|
2382
|
+
}
|
|
2383
|
+
this.peers.delete(peer.socket);
|
|
2384
|
+
}
|
|
2385
|
+
};
|
|
2386
|
+
|
|
2387
|
+
// src/p2p/client.ts
|
|
2388
|
+
import net2 from "net";
|
|
2389
|
+
var P2PClient = class {
|
|
2390
|
+
socket = null;
|
|
2391
|
+
options;
|
|
2392
|
+
running = false;
|
|
2393
|
+
readState = { buffer: Buffer.alloc(0), expectingHeader: true, messageLength: 0 };
|
|
2394
|
+
onDataCallback = null;
|
|
2395
|
+
onStatusCallback = null;
|
|
2396
|
+
reconnectTimer = null;
|
|
2397
|
+
pingHandle = null;
|
|
2398
|
+
constructor(options) {
|
|
2399
|
+
this.options = options;
|
|
2400
|
+
}
|
|
2401
|
+
setOnData(cb) {
|
|
2402
|
+
this.onDataCallback = cb;
|
|
2403
|
+
}
|
|
2404
|
+
setOnStatus(cb) {
|
|
2405
|
+
this.onStatusCallback = cb;
|
|
2406
|
+
}
|
|
2407
|
+
async connect() {
|
|
2408
|
+
return new Promise((resolve, reject) => {
|
|
2409
|
+
this.socket = new net2.Socket();
|
|
2410
|
+
this.socket.on("data", (data) => this.onData(data));
|
|
2411
|
+
this.socket.on("close", () => this.onClose());
|
|
2412
|
+
this.socket.on("error", (err) => this.onError(err));
|
|
2413
|
+
this.socket.on("timeout", () => this.onTimeout());
|
|
2414
|
+
this.socket.setTimeout(1e4);
|
|
2415
|
+
this.socket.connect(this.options.port, this.options.host, () => {
|
|
2416
|
+
this.running = true;
|
|
2417
|
+
this.socket?.setTimeout(0);
|
|
2418
|
+
this.socket?.write(encodeAuth(this.options.password));
|
|
2419
|
+
this.status("Connecting...");
|
|
2420
|
+
resolve();
|
|
2421
|
+
});
|
|
2422
|
+
this.socket.once("error", (err) => {
|
|
2423
|
+
reject(err);
|
|
2424
|
+
});
|
|
2425
|
+
});
|
|
2426
|
+
}
|
|
2427
|
+
disconnect() {
|
|
2428
|
+
this.running = false;
|
|
2429
|
+
if (this.reconnectTimer) {
|
|
2430
|
+
clearTimeout(this.reconnectTimer);
|
|
2431
|
+
this.reconnectTimer = null;
|
|
2432
|
+
}
|
|
2433
|
+
if (this.pingHandle) {
|
|
2434
|
+
clearInterval(this.pingHandle);
|
|
2435
|
+
this.pingHandle = null;
|
|
2436
|
+
}
|
|
2437
|
+
if (this.socket) {
|
|
2438
|
+
try {
|
|
2439
|
+
this.socket.write(encodeDisconnect("Client closing"));
|
|
2440
|
+
} catch {
|
|
2441
|
+
}
|
|
2442
|
+
this.socket.destroy();
|
|
2443
|
+
this.socket = null;
|
|
2444
|
+
}
|
|
2445
|
+
this.status("Disconnected");
|
|
2446
|
+
}
|
|
2447
|
+
onData(data) {
|
|
2448
|
+
this.readState.buffer = Buffer.concat([this.readState.buffer, data]);
|
|
2449
|
+
while (this.readState.buffer.length > 0) {
|
|
2450
|
+
if (this.readState.expectingHeader) {
|
|
2451
|
+
if (this.readState.buffer.length >= 4) {
|
|
2452
|
+
this.readState.messageLength = this.readState.buffer.readUInt32BE(0);
|
|
2453
|
+
this.readState.buffer = this.readState.buffer.subarray(4);
|
|
2454
|
+
this.readState.expectingHeader = false;
|
|
2455
|
+
} else {
|
|
2456
|
+
break;
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
if (!this.readState.expectingHeader) {
|
|
2460
|
+
if (this.readState.buffer.length >= this.readState.messageLength) {
|
|
2461
|
+
const body = this.readState.buffer.subarray(0, this.readState.messageLength);
|
|
2462
|
+
this.readState.buffer = this.readState.buffer.subarray(this.readState.messageLength);
|
|
2463
|
+
this.readState.expectingHeader = true;
|
|
2464
|
+
this.handleMessage(body);
|
|
2465
|
+
} else {
|
|
2466
|
+
break;
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
handleMessage(body) {
|
|
2472
|
+
try {
|
|
2473
|
+
const msg = JSON.parse(body.toString("utf-8"));
|
|
2474
|
+
switch (msg.kind) {
|
|
2475
|
+
case "auth-ok":
|
|
2476
|
+
this.status("Authenticated \u2014 receiving live data");
|
|
2477
|
+
this.startPing();
|
|
2478
|
+
break;
|
|
2479
|
+
case "auth-fail":
|
|
2480
|
+
this.status(`Authentication failed: ${msg.payload.reason}`);
|
|
2481
|
+
this.disconnect();
|
|
2482
|
+
break;
|
|
2483
|
+
case "data":
|
|
2484
|
+
if (this.onDataCallback && msg.payload?.stats) {
|
|
2485
|
+
this.onDataCallback(msg.payload.stats);
|
|
2486
|
+
}
|
|
2487
|
+
break;
|
|
2488
|
+
case "ping":
|
|
2489
|
+
if (this.socket && !this.socket.destroyed) {
|
|
2490
|
+
const ts = msg.payload.ts;
|
|
2491
|
+
this.socket.write(encodePong(ts));
|
|
2492
|
+
}
|
|
2493
|
+
break;
|
|
2494
|
+
case "pong":
|
|
2495
|
+
break;
|
|
2496
|
+
case "disconnect":
|
|
2497
|
+
this.status(`Server disconnected: ${msg.payload.reason}`);
|
|
2498
|
+
this.disconnect();
|
|
2499
|
+
break;
|
|
2500
|
+
default:
|
|
2501
|
+
break;
|
|
2502
|
+
}
|
|
2503
|
+
} catch {
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
startPing() {
|
|
2507
|
+
this.pingHandle = setInterval(() => {
|
|
2508
|
+
if (this.socket && !this.socket.destroyed) {
|
|
2509
|
+
this.socket.write(encodePing());
|
|
2510
|
+
}
|
|
2511
|
+
}, 15e3);
|
|
2512
|
+
}
|
|
2513
|
+
onClose() {
|
|
2514
|
+
if (this.running) {
|
|
2515
|
+
this.status("Connection closed \u2014 reconnecting in 3s");
|
|
2516
|
+
this.reconnectTimer = setTimeout(() => {
|
|
2517
|
+
if (this.running) this.connect().catch(() => {
|
|
2518
|
+
});
|
|
2519
|
+
}, 3e3);
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
onError(err) {
|
|
2523
|
+
const code = err.code;
|
|
2524
|
+
if (code === "ECONNREFUSED") {
|
|
2525
|
+
this.status("Connection refused \u2014 is the server running?");
|
|
2526
|
+
} else if (code === "ETIMEDOUT") {
|
|
2527
|
+
this.status("Connection timed out");
|
|
2528
|
+
} else {
|
|
2529
|
+
this.status(`Connection error: ${err.message}`);
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
onTimeout() {
|
|
2533
|
+
this.status("Connection timed out");
|
|
2534
|
+
this.disconnect();
|
|
2535
|
+
}
|
|
2536
|
+
status(msg) {
|
|
2537
|
+
if (this.onStatusCallback) {
|
|
2538
|
+
this.onStatusCallback(msg);
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
};
|
|
2542
|
+
|
|
1227
2543
|
// src/index.ts
|
|
1228
2544
|
var program = new Command();
|
|
1229
|
-
program.name("pyre").version("
|
|
2545
|
+
program.name("pyre").version("2.0.0").description("Mac system monitoring CLI: interactive live dashboard, stats, graphs, export, packet monitor, battery predictor").option("-j, --json", "Output as JSON").option("-c, --csv", "Output as CSV").option("-t, --tsv", "Output as TSV").option("--detailed", "Include detailed system info and sensor readings").option("--interval <seconds>", "Refresh interval for live mode", "2").option("--once", "Show a single static snapshot instead of live feed").option("--out <file>", "Also write the snapshot output to a file (--once/--json/--csv/--tsv modes)").option("--export-dir <dir>", "Directory used for live-mode snapshot exports and logs", "./pyre-exports").option("--log", "Start continuous CSV logging immediately when live mode starts").option("--tree", "Show process tree view instead of flat list").option("--sort <key>", "Sort processes by: cpu, mem, pid, user, command, state, threads, runtime", "cpu").option("--packets", "Include packet monitor panel in output").option("--limit <n>", "Max number of processes to include in --once/--json/--csv/--tsv snapshots (0 = all)", "10").option("--p2p-host <host>", "P2P host address (server: bind address, client: server address)", "0.0.0.0").option("--p2p-port <port>", "P2P port number", "9876").option("--p2p-password <password>", "Password for P2P authentication");
|
|
1230
2546
|
program.parse(process.argv);
|
|
1231
2547
|
var opts = program.opts();
|
|
1232
2548
|
function isExportMode() {
|
|
@@ -1236,32 +2552,129 @@ function stripAnsi(s) {
|
|
|
1236
2552
|
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
1237
2553
|
}
|
|
1238
2554
|
async function main() {
|
|
1239
|
-
const
|
|
1240
|
-
if (
|
|
1241
|
-
|
|
2555
|
+
const cmd = program.args[0];
|
|
2556
|
+
if (cmd === "live" || !isExportMode() && !opts.once && cmd !== "p2p") {
|
|
2557
|
+
if (cmd === "live") {
|
|
2558
|
+
program.args.shift();
|
|
2559
|
+
}
|
|
2560
|
+
await showSplash();
|
|
2561
|
+
const interval = parseFloat(opts.interval) || 2;
|
|
1242
2562
|
await startLive({
|
|
1243
|
-
interval
|
|
2563
|
+
interval,
|
|
1244
2564
|
detailed: opts.detailed,
|
|
1245
2565
|
exportDir: opts.exportDir,
|
|
1246
2566
|
autoLog: opts.log
|
|
1247
2567
|
});
|
|
1248
2568
|
return;
|
|
1249
2569
|
}
|
|
1250
|
-
|
|
2570
|
+
if (cmd === "p2p") {
|
|
2571
|
+
const subcommand = program.args[1];
|
|
2572
|
+
if (subcommand === "server") {
|
|
2573
|
+
await runP2PServer();
|
|
2574
|
+
return;
|
|
2575
|
+
} else if (subcommand === "connect") {
|
|
2576
|
+
await runP2PConnect();
|
|
2577
|
+
return;
|
|
2578
|
+
} else {
|
|
2579
|
+
console.log(chalk8.red("Usage: pyre p2p <server|connect> [options]"));
|
|
2580
|
+
process.exit(1);
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
const requestedLimit = parseInt(opts.limit, 10);
|
|
2584
|
+
const processLimit = Number.isFinite(requestedLimit) && requestedLimit > 0 ? requestedLimit : void 0;
|
|
2585
|
+
const data = await collectAll({ detailed: opts.detailed, processLimit });
|
|
1251
2586
|
let output;
|
|
1252
2587
|
if (opts.json) output = formatJson(data);
|
|
1253
2588
|
else if (opts.csv) output = formatCsv(data);
|
|
1254
2589
|
else if (opts.tsv) output = formatTsv(data);
|
|
1255
|
-
else output = formatTable(data, { width: process.stdout.columns || 80 });
|
|
2590
|
+
else output = formatTable(data, { width: process.stdout.columns || 80, sortBy: opts.sort, treeView: opts.tree, visible: { packets: opts.packets ? true : void 0 } });
|
|
1256
2591
|
console.log(output);
|
|
1257
2592
|
if (opts.out) {
|
|
1258
2593
|
fs2.writeFileSync(opts.out, stripAnsi(output));
|
|
1259
|
-
console.log(
|
|
2594
|
+
console.log(chalk8.dim(`
|
|
1260
2595
|
Saved to ${opts.out}`));
|
|
1261
2596
|
}
|
|
1262
2597
|
}
|
|
1263
2598
|
main().catch((err) => {
|
|
1264
|
-
console.error(
|
|
1265
|
-
stopLive();
|
|
2599
|
+
console.error(chalk8.red(`Error: ${err.message}`));
|
|
1266
2600
|
process.exit(1);
|
|
1267
2601
|
});
|
|
2602
|
+
async function runP2PServer() {
|
|
2603
|
+
const host = opts.p2pHost || "0.0.0.0";
|
|
2604
|
+
const port = parseInt(opts.p2pPort, 10) || 9876;
|
|
2605
|
+
const password = opts.p2pPassword || "";
|
|
2606
|
+
const interval = parseFloat(opts.interval) || 2;
|
|
2607
|
+
const detailed = !!opts.detailed;
|
|
2608
|
+
if (!password) {
|
|
2609
|
+
console.log(chalk8.red("Error: a password is required for P2P server mode. Use --p2p-password <password>"));
|
|
2610
|
+
process.exit(1);
|
|
2611
|
+
}
|
|
2612
|
+
const server = new P2PServer({ host, port, password, intervalMs: interval * 1e3, detailed });
|
|
2613
|
+
console.log(chalk8.bold(`
|
|
2614
|
+
pyre P2P Server`));
|
|
2615
|
+
console.log(chalk8.dim(` Host: ${host}`));
|
|
2616
|
+
console.log(chalk8.dim(` Port: ${port}`));
|
|
2617
|
+
console.log(chalk8.dim(` Password: ${password}`));
|
|
2618
|
+
console.log(chalk8.dim(` Interval: ${interval}s`));
|
|
2619
|
+
console.log(chalk8.dim(` Detailed: ${detailed ? "on" : "off"}`));
|
|
2620
|
+
console.log(chalk8.dim(` Waiting for peers...
|
|
2621
|
+
`));
|
|
2622
|
+
await server.start();
|
|
2623
|
+
const rl = readline2.createInterface({ input: process.stdin });
|
|
2624
|
+
rl.on("line", (line) => {
|
|
2625
|
+
const trimmed = line.trim().toLowerCase();
|
|
2626
|
+
if (trimmed === "q" || trimmed === "quit" || trimmed === "exit") {
|
|
2627
|
+
server.stop();
|
|
2628
|
+
rl.close();
|
|
2629
|
+
process.exit(0);
|
|
2630
|
+
} else if (trimmed === "status") {
|
|
2631
|
+
const info = server.info;
|
|
2632
|
+
console.log(chalk8.dim(` Peers connected: ${info.peersConnected}`));
|
|
2633
|
+
}
|
|
2634
|
+
});
|
|
2635
|
+
process.once("SIGINT", () => {
|
|
2636
|
+
server.stop();
|
|
2637
|
+
rl.close();
|
|
2638
|
+
process.exit(0);
|
|
2639
|
+
});
|
|
2640
|
+
}
|
|
2641
|
+
async function runP2PConnect() {
|
|
2642
|
+
const host = opts.p2pHost || "127.0.0.1";
|
|
2643
|
+
const port = parseInt(opts.p2pPort, 10) || 9876;
|
|
2644
|
+
const password = opts.p2pPassword || "";
|
|
2645
|
+
if (!password) {
|
|
2646
|
+
console.log(chalk8.red("Error: a password is required for P2P connect mode. Use --p2p-password <password>"));
|
|
2647
|
+
process.exit(1);
|
|
2648
|
+
}
|
|
2649
|
+
const client = new P2PClient({ host, port, password });
|
|
2650
|
+
client.setOnStatus((msg) => {
|
|
2651
|
+
process.stdout.write(`\r${chalk8.dim(msg)}${" ".repeat(40)}\r`);
|
|
2652
|
+
});
|
|
2653
|
+
client.setOnData((data) => {
|
|
2654
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
2655
|
+
console.log(chalk8.bold(` pyre P2P Client \u2014 ${data.header.hostname}`));
|
|
2656
|
+
console.log(chalk8.dim(` ${data.timestamp}`));
|
|
2657
|
+
console.log();
|
|
2658
|
+
console.log(formatTable(data, {
|
|
2659
|
+
width: process.stdout.columns || 80,
|
|
2660
|
+
sortBy: "cpu",
|
|
2661
|
+
treeView: false,
|
|
2662
|
+
visible: { packets: true }
|
|
2663
|
+
}));
|
|
2664
|
+
});
|
|
2665
|
+
console.log(chalk8.bold(`
|
|
2666
|
+
pyre P2P Client`));
|
|
2667
|
+
console.log(chalk8.dim(` Connecting to ${host}:${port}...`));
|
|
2668
|
+
console.log(chalk8.dim(` Press Ctrl+C to disconnect
|
|
2669
|
+
`));
|
|
2670
|
+
try {
|
|
2671
|
+
await client.connect();
|
|
2672
|
+
} catch (err) {
|
|
2673
|
+
console.log(chalk8.red(`Connection failed: ${err.message}`));
|
|
2674
|
+
process.exit(1);
|
|
2675
|
+
}
|
|
2676
|
+
process.once("SIGINT", () => {
|
|
2677
|
+
client.disconnect();
|
|
2678
|
+
process.exit(0);
|
|
2679
|
+
});
|
|
2680
|
+
}
|