pyre-cli 1.4.0 → 3.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 +78 -11
- package/dist/index.js +1325 -33
- package/package.json +2 -2
- package/src/formatters/index.ts +1 -1
- package/src/formatters/output.ts +47 -19
- package/src/formatters/render.ts +207 -16
- package/src/formatters/types.ts +4 -0
- package/src/history.ts +85 -47
- package/src/index.ts +200 -20
- package/src/live/export.ts +5 -2
- package/src/live/input.ts +296 -164
- package/src/live/render.ts +32 -28
- package/src/live/state.ts +41 -21
- package/src/monitors/collectors.ts +220 -17
- package/src/monitors/index.ts +2 -2
- package/src/monitors/types.ts +42 -1
- package/src/p2p/client.ts +271 -0
- package/src/p2p/index.ts +3 -0
- package/src/p2p/protocol.ts +112 -0
- package/src/p2p/server.ts +390 -0
- package/src/p2p/types.ts +97 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pyre-cli",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "CLI Mac monitoring tool: temps, system stats, live dashboard,
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "CLI Mac monitoring tool: temps, system stats, live dashboard, packet monitor, battery predictor, export",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
package/src/formatters/index.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - `render.ts` — dashboard layout, cards, tables, and the main `formatTable`
|
|
12
12
|
* - `output.ts` — JSON/CSV/TSV export and sparkline graph rendering
|
|
13
13
|
*/
|
|
14
|
-
export { formatTable, clampWidth, gridColumns, thermalColor, capacityColor } from './render.js';
|
|
14
|
+
export { formatTable, clampWidth, gridColumns, thermalColor, capacityColor, getTabHitboxes, TAB_BAR_ROW, TAB_DEFS } from './render.js';
|
|
15
15
|
export { formatJson, formatCsv, formatTsv, formatGraphs } from './output.js';
|
|
16
16
|
export type { ThemeName, ThemeColors, VisibleItems, TableOptions } from './types.js';
|
|
17
17
|
export { THEMES } from './themes.js';
|
package/src/formatters/output.ts
CHANGED
|
@@ -39,39 +39,58 @@ export function formatCsv(data: StatsData): string {
|
|
|
39
39
|
],
|
|
40
40
|
['network', 'rxBytes', String(data.network.rxBytes)],
|
|
41
41
|
['network', 'txBytes', String(data.network.txBytes)],
|
|
42
|
+
['network', 'rxPackets', String(data.network.rxPackets)],
|
|
43
|
+
['network', 'txPackets', String(data.network.txPackets)],
|
|
44
|
+
['network', 'connections', data.network.connections ? String(data.network.connections) : ''],
|
|
42
45
|
['battery', 'level', data.battery ? String(data.battery.level) : ''],
|
|
43
46
|
['battery', 'powerSource', data.battery ? data.battery.powerSource : ''],
|
|
44
47
|
['battery', 'condition', data.battery?.condition ?? ''],
|
|
45
48
|
['battery', 'maxCapacityPercent', data.battery?.maxCapacityPercent !== undefined ? String(data.battery.maxCapacityPercent) : ''],
|
|
46
49
|
['battery', 'cycles', data.battery?.cycles !== undefined ? String(data.battery.cycles) : ''],
|
|
50
|
+
['battery', 'estimatedTimeToEmpty', data.battery?.estimatedTimeToEmpty ?? ''],
|
|
51
|
+
['battery', 'dischargeRatePerHour', data.battery?.dischargeRatePerHour !== undefined ? String(data.battery.dischargeRatePerHour) : ''],
|
|
52
|
+
['battery', 'powerWatts', data.battery?.powerWatts !== undefined ? String(data.battery.powerWatts) : ''],
|
|
47
53
|
['power', 'cpuWatts', data.power?.cpuWatts !== undefined ? String(data.power.cpuWatts) : ''],
|
|
48
54
|
['power', 'gpuWatts', data.power?.gpuWatts !== undefined ? String(data.power.gpuWatts) : ''],
|
|
49
55
|
['power', 'combinedWatts', data.power?.combinedWatts !== undefined ? String(data.power.combinedWatts) : ''],
|
|
50
|
-
['system', 'hostname', data.header.hostname],
|
|
56
|
+
['system', 'hostname', data.header.hostname],
|
|
51
57
|
['system', 'uptime', data.header.uptime],
|
|
52
58
|
['system', 'timestamp', data.timestamp],
|
|
53
59
|
];
|
|
54
60
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
if (data.packets) {
|
|
62
|
+
rows.push(['packets', 'totalPackets', String(data.packets.totalPackets)]);
|
|
63
|
+
rows.push(['packets', 'rxPackets', String(data.packets.rxPackets)]);
|
|
64
|
+
rows.push(['packets', 'txPackets', String(data.packets.txPackets)]);
|
|
65
|
+
rows.push(['packets', 'connections', String(data.packets.connections)]);
|
|
66
|
+
for (const iface of data.packets.interfaces || []) {
|
|
67
|
+
rows.push(['packets_iface', iface.iface, `${iface.rxPackets},${iface.txPackets},${iface.rxBytes},${iface.txBytes}`]);
|
|
68
|
+
}
|
|
69
|
+
for (const proc of data.packets.allProcesses || []) {
|
|
70
|
+
rows.push(['packets_proc', `pid=${proc.pid}`, `${proc.command},${proc.rxBytes},${proc.txBytes}`]);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
62
73
|
|
|
63
|
-
|
|
64
|
-
rows.push(['
|
|
65
|
-
rows.push(['
|
|
66
|
-
rows.push(['
|
|
67
|
-
rows.push(['
|
|
68
|
-
rows.push(['
|
|
69
|
-
rows.push(['process', `state=${p.pid}`, p.state]);
|
|
70
|
-
rows.push(['process', `threads=${p.pid}`, String(p.threads)]);
|
|
71
|
-
rows.push(['process', `runtime=${p.pid}`, String(p.runtime)]);
|
|
72
|
-
rows.push(['process', `command=${p.pid}`, p.command]);
|
|
74
|
+
if (data.gpu) {
|
|
75
|
+
rows.push(['gpu', 'model', data.gpu.model]);
|
|
76
|
+
rows.push(['gpu', 'memory', String(data.gpu.memory)]);
|
|
77
|
+
rows.push(['gpu', 'utilization', String(data.gpu.utilization)]);
|
|
78
|
+
if (data.gpu.temperature) rows.push(['gpu', 'tempC', String(data.gpu.temperature)]);
|
|
79
|
+
rows.push(['gpu', 'processes', String(data.gpu.processes)]);
|
|
73
80
|
}
|
|
74
|
-
|
|
81
|
+
|
|
82
|
+
for (const p of data.processes) {
|
|
83
|
+
rows.push(['process', `pid=${p.pid}`, `${p.pid}`]);
|
|
84
|
+
rows.push(['process', `ppid=${p.pid}`, String(p.ppid)]);
|
|
85
|
+
rows.push(['process', `user=${p.pid}`, p.user]);
|
|
86
|
+
rows.push(['process', `cpu=${p.pid}`, String(p.cpu)]);
|
|
87
|
+
rows.push(['process', `mem=${p.pid}`, String(p.mem)]);
|
|
88
|
+
rows.push(['process', `state=${p.pid}`, p.state]);
|
|
89
|
+
rows.push(['process', `threads=${p.pid}`, String(p.threads)]);
|
|
90
|
+
rows.push(['process', `runtime=${p.pid}`, String(p.runtime)]);
|
|
91
|
+
rows.push(['process', `command=${p.pid}`, p.command]);
|
|
92
|
+
}
|
|
93
|
+
return rows.map(r => r.map(item => `"${String(item).replace(/"/g, '""')}"`).join(',')).join('\n');
|
|
75
94
|
}
|
|
76
95
|
|
|
77
96
|
export function formatTsv(data: StatsData): string {
|
|
@@ -92,6 +111,15 @@ export function formatGraphs(history: History, width = 80, themeName: ThemeName
|
|
|
92
111
|
);
|
|
93
112
|
lines.push(graphRow('Net RX/s', history.netRxRate, {}, v => `${formatBytes(v)}/s`, sparkWidth, graphMode));
|
|
94
113
|
lines.push(graphRow('Net TX/s', history.netTxRate, {}, v => `${formatBytes(v)}/s`, sparkWidth, graphMode));
|
|
114
|
+
if (history.powerWatts.length) {
|
|
115
|
+
lines.push(graphRow('Power W', history.powerWatts, {}, v => `${v.toFixed(1)} W`, sparkWidth, graphMode));
|
|
116
|
+
}
|
|
117
|
+
if (history.rxPacketRate.length) {
|
|
118
|
+
lines.push(graphRow('RX pkt/s', history.rxPacketRate, {}, v => `${v.toFixed(0)} pkt/s`, sparkWidth, graphMode));
|
|
119
|
+
}
|
|
120
|
+
if (history.txPacketRate.length) {
|
|
121
|
+
lines.push(graphRow('TX pkt/s', history.txPacketRate, {}, v => `${v.toFixed(0)} pkt/s`, sparkWidth, graphMode));
|
|
122
|
+
}
|
|
95
123
|
|
|
96
124
|
const title = `Graphs · mode:${graphMode} · last ${sampleCount} sample${sampleCount === 1 ? '' : 's'}`;
|
|
97
125
|
return panel(title, lines, width, theme.graphs, theme.border).join('\n');
|
package/src/formatters/render.ts
CHANGED
|
@@ -163,14 +163,18 @@ function gpuCard(data: StatsData, contentWidth: number): string[] | null {
|
|
|
163
163
|
return lines;
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
166
|
+
function powerCard(data: StatsData): string[] | null {
|
|
167
|
+
const lines: string[] = [];
|
|
168
|
+
if (data.power) {
|
|
169
|
+
if (data.power.cpuWatts !== undefined) lines.push(statRow('CPU', `${data.power.cpuWatts.toFixed(2)} W`));
|
|
170
|
+
if (data.power.gpuWatts !== undefined) lines.push(statRow('GPU', `${data.power.gpuWatts.toFixed(2)} W`));
|
|
171
|
+
if (data.power.combinedWatts !== undefined) lines.push(statRow('Total', `${data.power.combinedWatts.toFixed(2)} W`));
|
|
172
|
+
}
|
|
173
|
+
if (data.battery?.powerWatts !== undefined && data.battery.powerWatts > 0) {
|
|
174
|
+
lines.push(statRow('Battery', `${data.battery.powerWatts.toFixed(2)} W`));
|
|
175
|
+
}
|
|
176
|
+
return lines.length ? lines : null;
|
|
177
|
+
}
|
|
174
178
|
|
|
175
179
|
function batteryCard(data: StatsData, contentWidth: number): string[] | null {
|
|
176
180
|
if (!data.battery) return null;
|
|
@@ -185,6 +189,12 @@ function batteryCard(data: StatsData, contentWidth: number): string[] | null {
|
|
|
185
189
|
lines.push(statRow('Capacity', capacityColor(data.battery.maxCapacityPercent)(`${data.battery.maxCapacityPercent}%`)));
|
|
186
190
|
}
|
|
187
191
|
lines.push(statRow('Source', data.battery.powerSource));
|
|
192
|
+
if (data.battery.estimatedTimeToEmpty && data.battery.state !== 'charged') {
|
|
193
|
+
lines.push(statRow('Est. Empty', data.battery.estimatedTimeToEmpty));
|
|
194
|
+
}
|
|
195
|
+
if (data.battery.dischargeRatePerHour !== undefined && data.battery.state !== 'charged') {
|
|
196
|
+
lines.push(statRow('Disch. Rate', `${data.battery.dischargeRatePerHour}%/h`));
|
|
197
|
+
}
|
|
188
198
|
return lines;
|
|
189
199
|
}
|
|
190
200
|
|
|
@@ -209,7 +219,36 @@ function networkCard(data: StatsData, contentWidth: number): string[] {
|
|
|
209
219
|
];
|
|
210
220
|
}
|
|
211
221
|
|
|
212
|
-
|
|
222
|
+
function packetCard(data: StatsData, contentWidth: number): string[] | null {
|
|
223
|
+
if (!data.packets) return null;
|
|
224
|
+
const lines: string[] = [];
|
|
225
|
+
lines.push(statRow('Total', `${data.packets.totalPackets} pkt`));
|
|
226
|
+
lines.push(statRow('RX', `${data.packets.rxPackets} pkt`));
|
|
227
|
+
lines.push(statRow('TX', `${data.packets.txPackets} pkt`));
|
|
228
|
+
lines.push(statRow('Conns', `${data.packets.connections} TCP`));
|
|
229
|
+
if (data.packets.topProcesses && data.packets.topProcesses.length) {
|
|
230
|
+
lines.push(statRow('Top Proc', truncatePlain(data.packets.topProcesses[0].command, contentWidth - 10)));
|
|
231
|
+
}
|
|
232
|
+
if (data.packets.allProcesses && data.packets.allProcesses.length > 1) {
|
|
233
|
+
lines.push(statRow('Net Procs', `${data.packets.allProcesses.length} processes`));
|
|
234
|
+
}
|
|
235
|
+
if (data.packets.interfaces && data.packets.interfaces.length) {
|
|
236
|
+
lines.push(statRow('Ifaces', `${data.packets.interfaces.length} active`));
|
|
237
|
+
}
|
|
238
|
+
return lines;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function tasksCard(data: StatsData, contentWidth: number): string[] | null {
|
|
242
|
+
if (!data.tasks || !data.tasks.length) return null;
|
|
243
|
+
const lines: string[] = [];
|
|
244
|
+
for (const task of data.tasks.slice(0, 6)) {
|
|
245
|
+
const cmd = truncatePlain(task.command, Math.max(4, contentWidth - 28));
|
|
246
|
+
lines.push(statRow(`${task.pid}`, `${task.user.padEnd(8)} ${task.cpu.toFixed(1).padStart(5)}% ${task.mem.toFixed(1).padStart(5)}% ${task.state} ${cmd}`));
|
|
247
|
+
}
|
|
248
|
+
return lines;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// --- header / tabs -----------------------------------------------------------
|
|
213
252
|
|
|
214
253
|
function header(data: StatsData, width: number, badges: string[] = [], borderAccent = THEMES.default.border): string[] {
|
|
215
254
|
const now = new Date().toLocaleTimeString();
|
|
@@ -220,6 +259,68 @@ function header(data: StatsData, width: number, badges: string[] = [], borderAcc
|
|
|
220
259
|
return [line, borderAccent('─'.repeat(width))];
|
|
221
260
|
}
|
|
222
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Canonical tab list shared between the renderer (drawing the tab bar)
|
|
264
|
+
* and the live dashboard's mouse handler (hit-testing clicks against it).
|
|
265
|
+
* Keeping a single source of truth here avoids the two drifting apart.
|
|
266
|
+
*/
|
|
267
|
+
export const TAB_DEFS: { id: string; label: string; key: string }[] = [
|
|
268
|
+
{ id: 'cpu', label: 'CPU', key: '1' },
|
|
269
|
+
{ id: 'mem', label: 'Memory', key: '2' },
|
|
270
|
+
{ id: 'gpu', label: 'GPU', key: '3' },
|
|
271
|
+
{ id: 'power', label: 'Power', key: '4' },
|
|
272
|
+
{ id: 'battery', label: 'Battery', key: '5' },
|
|
273
|
+
{ id: 'thermal', label: 'Thermal', key: '6' },
|
|
274
|
+
{ id: 'network', label: 'Network', key: '7' },
|
|
275
|
+
{ id: 'packets', label: 'Packets', key: '8' },
|
|
276
|
+
{ id: 'tasks', label: 'Tasks', key: '9' },
|
|
277
|
+
{ id: 'disk', label: 'Disk', key: '0' },
|
|
278
|
+
{ id: 'process', label: 'Process', key: 'p' },
|
|
279
|
+
];
|
|
280
|
+
|
|
281
|
+
/** The number of leading columns of indent before the tab bar's first character (see `tabBar`). */
|
|
282
|
+
const TAB_BAR_INDENT = 2;
|
|
283
|
+
/** Visible width of the ' │ ' separator drawn between tabs. */
|
|
284
|
+
const TAB_SEPARATOR_WIDTH = 3;
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Compute the column range (inclusive start, exclusive end) each tab
|
|
288
|
+
* occupies when rendered by `tabBar`, so a click at a given column can be
|
|
289
|
+
* mapped back to a panel id. Row is always the same line the tab bar is
|
|
290
|
+
* printed on (see `formatTable`: header takes 2 lines, then a blank line,
|
|
291
|
+
* then the tab bar — i.e. row 4 in a 1-indexed terminal, row index 3 if
|
|
292
|
+
* 0-indexed from the top of the dashboard).
|
|
293
|
+
*/
|
|
294
|
+
export function getTabHitboxes(): { id: string; start: number; end: number }[] {
|
|
295
|
+
const boxes: { id: string; start: number; end: number }[] = [];
|
|
296
|
+
let col = TAB_BAR_INDENT;
|
|
297
|
+
TAB_DEFS.forEach((t, i) => {
|
|
298
|
+
const text = `[${t.key}]${t.label}`;
|
|
299
|
+
const start = col;
|
|
300
|
+
const end = col + text.length;
|
|
301
|
+
boxes.push({ id: t.id, start, end });
|
|
302
|
+
col = end;
|
|
303
|
+
if (i < TAB_DEFS.length - 1) col += TAB_SEPARATOR_WIDTH;
|
|
304
|
+
});
|
|
305
|
+
return boxes;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Row (1-indexed, matching terminal mouse-report coordinates) the tab bar renders on. */
|
|
309
|
+
export const TAB_BAR_ROW = 4;
|
|
310
|
+
|
|
311
|
+
function tabBar(activePanel: string, width: number, theme: ThemeColors): string {
|
|
312
|
+
const rendered = TAB_DEFS.map(t => {
|
|
313
|
+
const isActive = activePanel === t.id;
|
|
314
|
+
const label = isActive ? chalk.bold(t.label) : chalk.dim(t.label);
|
|
315
|
+
const keyHint = chalk.dim(`[${t.key}]`);
|
|
316
|
+
return `${keyHint}${label}`;
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const joined = rendered.join(chalk.dim(' │ '));
|
|
320
|
+
const padded = visLen(joined) < width ? joined + ' '.repeat(width - visLen(joined)) : joined;
|
|
321
|
+
return chalk.dim(' ') + padded;
|
|
322
|
+
}
|
|
323
|
+
|
|
223
324
|
// --- process / disk tables ---------------------------------------------------
|
|
224
325
|
|
|
225
326
|
function sortProcesses(
|
|
@@ -359,15 +460,87 @@ function diskTableLines(
|
|
|
359
460
|
|
|
360
461
|
// --- main dashboard ------------------------------------------------------------
|
|
361
462
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
463
|
+
function detailPanelTitle(panel: string): string {
|
|
464
|
+
const map: Record<string, string> = {
|
|
465
|
+
cpu: 'CPU',
|
|
466
|
+
mem: 'Memory',
|
|
467
|
+
gpu: 'GPU',
|
|
468
|
+
power: 'Power',
|
|
469
|
+
battery: 'Battery',
|
|
470
|
+
thermal: 'Thermal',
|
|
471
|
+
network: 'Network',
|
|
472
|
+
packets: 'Packets',
|
|
473
|
+
tasks: 'Tasks',
|
|
474
|
+
disk: 'Disk',
|
|
475
|
+
process: 'Processes',
|
|
476
|
+
};
|
|
477
|
+
return map[panel] || panel;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function activeDetailLines(data: StatsData, activePanel: string, width: number): string[] | null {
|
|
481
|
+
const contentWidth = width - 4;
|
|
482
|
+
switch (activePanel) {
|
|
483
|
+
case 'cpu':
|
|
484
|
+
return cpuCard(data, contentWidth);
|
|
485
|
+
case 'mem':
|
|
486
|
+
return memCard(data, contentWidth);
|
|
487
|
+
case 'gpu': {
|
|
488
|
+
const gpu = gpuCard(data, contentWidth);
|
|
489
|
+
return gpu ?? ['No GPU data available'];
|
|
490
|
+
}
|
|
491
|
+
case 'power': {
|
|
492
|
+
const power = powerCard(data);
|
|
493
|
+
if (!power && !data.battery) return ['No power data available'];
|
|
494
|
+
const lines: string[] = [];
|
|
495
|
+
if (power) lines.push(...power);
|
|
496
|
+
if (data.battery?.powerWatts !== undefined && data.battery.powerWatts > 0) {
|
|
497
|
+
lines.push(statRow('Battery', `${data.battery.powerWatts.toFixed(2)} W`));
|
|
498
|
+
}
|
|
499
|
+
return lines.length ? lines : ['No power data available'];
|
|
500
|
+
}
|
|
501
|
+
case 'battery':
|
|
502
|
+
return data.battery ? batteryCard(data, contentWidth) : ['No battery data available'];
|
|
503
|
+
case 'thermal':
|
|
504
|
+
return thermalCard(data, contentWidth);
|
|
505
|
+
case 'network':
|
|
506
|
+
return networkCard(data, contentWidth);
|
|
507
|
+
case 'packets': {
|
|
508
|
+
if (!data.packets) return ['No packet data available'];
|
|
509
|
+
const lines: string[] = [];
|
|
510
|
+
lines.push(statRow('Total', `${data.packets.totalPackets} pkt`));
|
|
511
|
+
lines.push(statRow('RX', `${data.packets.rxPackets} pkt`));
|
|
512
|
+
lines.push(statRow('TX', `${data.packets.txPackets} pkt`));
|
|
513
|
+
lines.push(statRow('Conns', `${data.packets.connections} TCP`));
|
|
514
|
+
if (data.packets.interfaces && data.packets.interfaces.length) {
|
|
515
|
+
lines.push(statRow('Ifaces', `${data.packets.interfaces.length} active`));
|
|
516
|
+
for (const iface of data.packets.interfaces.slice(0, 8)) {
|
|
517
|
+
lines.push(statRow(` ${iface.iface}`, `RX ${formatBytes(iface.rxBytes)} / TX ${formatBytes(iface.txBytes)}`));
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (data.packets.allProcesses && data.packets.allProcesses.length) {
|
|
521
|
+
lines.push(statRow('Net Procs', `${data.packets.allProcesses.length} total`));
|
|
522
|
+
for (const proc of data.packets.allProcesses.slice(0, 10)) {
|
|
523
|
+
lines.push(statRow(` ${proc.pid}`, `${truncatePlain(proc.command, 20)} TX ${formatBytes(proc.txBytes)}`));
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return lines;
|
|
527
|
+
}
|
|
528
|
+
case 'tasks':
|
|
529
|
+
return data.tasks?.length ? tasksCard(data, contentWidth) : ['No task data available'];
|
|
530
|
+
case 'disk':
|
|
531
|
+
return data.disk.length ? diskTableLines(data.disk, width - 4, THEMES.default.border) : ['No disk data available'];
|
|
532
|
+
case 'process': {
|
|
533
|
+
const sorted = sortProcesses([...data.processes], 'cpu');
|
|
534
|
+
return processTableLines(sorted.slice(0, 50), width - 4, THEMES.default.border);
|
|
535
|
+
}
|
|
536
|
+
default:
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
369
541
|
export function formatTable(data: StatsData, opts: TableOptions = {}): string {
|
|
370
542
|
const width = clampWidth(opts.width);
|
|
543
|
+
const activePanel = opts.activePanel || 'grid';
|
|
371
544
|
const columns = gridColumns(width);
|
|
372
545
|
const gap = 2;
|
|
373
546
|
const cardWidth = Math.floor((width - gap * (columns - 1)) / columns);
|
|
@@ -380,6 +553,16 @@ export function formatTable(data: StatsData, opts: TableOptions = {}): string {
|
|
|
380
553
|
const out: string[] = [];
|
|
381
554
|
out.push(...header(data, width, [], theme.border));
|
|
382
555
|
out.push('');
|
|
556
|
+
out.push(tabBar(activePanel, width, theme));
|
|
557
|
+
out.push('');
|
|
558
|
+
|
|
559
|
+
if (activePanel !== 'grid') {
|
|
560
|
+
const lines = activeDetailLines(data, activePanel, width);
|
|
561
|
+
if (lines) {
|
|
562
|
+
out.push(...panel(detailPanelTitle(activePanel), lines, width, theme[activePanel as keyof ThemeColors] ?? theme.cpu, theme.border));
|
|
563
|
+
}
|
|
564
|
+
return out.join('\n');
|
|
565
|
+
}
|
|
383
566
|
|
|
384
567
|
type Card = { title: string; accent: (s: string) => string; lines: string[] };
|
|
385
568
|
const cards: Card[] = [];
|
|
@@ -408,6 +591,14 @@ export function formatTable(data: StatsData, opts: TableOptions = {}): string {
|
|
|
408
591
|
if (vis.network !== false) {
|
|
409
592
|
cards.push({ title: 'Network', accent: theme.network, lines: networkCard(data, contentWidth) });
|
|
410
593
|
}
|
|
594
|
+
if (vis.packets !== false) {
|
|
595
|
+
const packets = packetCard(data, contentWidth);
|
|
596
|
+
if (packets) cards.push({ title: 'Packets', accent: theme.network, lines: packets });
|
|
597
|
+
}
|
|
598
|
+
if (vis.tasks !== false) {
|
|
599
|
+
const tasks = tasksCard(data, contentWidth);
|
|
600
|
+
if (tasks) cards.push({ title: 'Tasks', accent: theme.process, lines: tasks });
|
|
601
|
+
}
|
|
411
602
|
|
|
412
603
|
for (let i = 0; i < cards.length; i += columns) {
|
|
413
604
|
const row = cards.slice(i, i + columns);
|
package/src/formatters/types.ts
CHANGED
|
@@ -35,6 +35,8 @@ export interface VisibleItems {
|
|
|
35
35
|
battery?: boolean;
|
|
36
36
|
thermal?: boolean;
|
|
37
37
|
network?: boolean;
|
|
38
|
+
packets?: boolean;
|
|
39
|
+
tasks?: boolean;
|
|
38
40
|
disk?: boolean;
|
|
39
41
|
process?: boolean;
|
|
40
42
|
tree?: boolean;
|
|
@@ -55,4 +57,6 @@ export interface TableOptions {
|
|
|
55
57
|
visible?: VisibleItems;
|
|
56
58
|
/** Show process tree view instead of flat list. */
|
|
57
59
|
treeView?: boolean;
|
|
60
|
+
/** Active panel for detail/focus view. 'grid' means show all panels. */
|
|
61
|
+
activePanel?: 'grid' | 'cpu' | 'mem' | 'gpu' | 'power' | 'battery' | 'thermal' | 'network' | 'packets' | 'tasks' | 'disk' | 'process';
|
|
58
62
|
}
|
package/src/history.ts
CHANGED
|
@@ -34,8 +34,22 @@ export class History {
|
|
|
34
34
|
/** Rolling window of GPU utilization percentages (0–100). */
|
|
35
35
|
gpuUtil: number[] = [];
|
|
36
36
|
|
|
37
|
+
/** Rolling window of combined power draw in watts. */
|
|
38
|
+
powerWatts: number[] = [];
|
|
39
|
+
|
|
40
|
+
/** Rolling window of network receive packet rates in packets/sec. */
|
|
41
|
+
rxPacketRate: number[] = [];
|
|
42
|
+
|
|
43
|
+
/** Rolling window of network transmit packet rates in packets/sec. */
|
|
44
|
+
txPacketRate: number[] = [];
|
|
45
|
+
|
|
46
|
+
/** Rolling window of active TCP connections. */
|
|
47
|
+
connections: number[] = [];
|
|
48
|
+
|
|
37
49
|
private lastRxBytes = 0;
|
|
38
50
|
private lastTxBytes = 0;
|
|
51
|
+
private lastRxPackets = 0;
|
|
52
|
+
private lastTxPackets = 0;
|
|
39
53
|
private lastTs = 0;
|
|
40
54
|
|
|
41
55
|
/**
|
|
@@ -54,59 +68,83 @@ export class History {
|
|
|
54
68
|
* because there is no prior sample to compute a delta from.
|
|
55
69
|
*/
|
|
56
70
|
push(sample: {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
71
|
+
cpuUsage: number;
|
|
72
|
+
memUsage: number;
|
|
73
|
+
temp: number | null;
|
|
74
|
+
rxBytes: number;
|
|
75
|
+
txBytes: number;
|
|
76
|
+
gpuUtil?: number;
|
|
77
|
+
powerWatts?: number;
|
|
78
|
+
rxPackets?: number;
|
|
79
|
+
txPackets?: number;
|
|
80
|
+
connections?: number;
|
|
81
|
+
}) {
|
|
82
|
+
const now = Date.now();
|
|
83
|
+
let rxRate = 0;
|
|
84
|
+
let txRate = 0;
|
|
85
|
+
let rxPktRate = 0;
|
|
86
|
+
let txPktRate = 0;
|
|
87
|
+
|
|
88
|
+
if (this.lastTs > 0) {
|
|
89
|
+
const dtSeconds = Math.max((now - this.lastTs) / 1000, 0.001);
|
|
90
|
+
rxRate = Math.max(0, (sample.rxBytes - this.lastRxBytes) / dtSeconds);
|
|
91
|
+
txRate = Math.max(0, (sample.txBytes - this.lastTxBytes) / dtSeconds);
|
|
92
|
+
if (sample.rxPackets !== undefined) {
|
|
93
|
+
rxPktRate = Math.max(0, (sample.rxPackets - this.lastRxPackets) / dtSeconds);
|
|
94
|
+
}
|
|
95
|
+
if (sample.txPackets !== undefined) {
|
|
96
|
+
txPktRate = Math.max(0, (sample.txPackets - this.lastTxPackets) / dtSeconds);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this.lastRxBytes = sample.rxBytes;
|
|
101
|
+
this.lastTxBytes = sample.txBytes;
|
|
102
|
+
if (sample.rxPackets !== undefined) this.lastRxPackets = sample.rxPackets;
|
|
103
|
+
if (sample.txPackets !== undefined) this.lastTxPackets = sample.txPackets;
|
|
104
|
+
this.lastTs = now;
|
|
105
|
+
|
|
106
|
+
this.pushBounded(this.cpuUsage, sample.cpuUsage);
|
|
107
|
+
this.pushBounded(this.memUsage, sample.memUsage);
|
|
108
|
+
if (sample.temp !== null) this.pushBounded(this.temp, sample.temp);
|
|
109
|
+
this.pushBounded(this.netRxRate, rxRate);
|
|
110
|
+
this.pushBounded(this.netTxRate, txRate);
|
|
111
|
+
if (sample.gpuUtil !== undefined) this.pushBounded(this.gpuUtil, sample.gpuUtil);
|
|
112
|
+
if (sample.powerWatts !== undefined) this.pushBounded(this.powerWatts, sample.powerWatts);
|
|
113
|
+
if (sample.rxPackets !== undefined) this.pushBounded(this.rxPacketRate, rxPktRate);
|
|
114
|
+
if (sample.txPackets !== undefined) this.pushBounded(this.txPacketRate, txPktRate);
|
|
115
|
+
if (sample.connections !== undefined) this.pushBounded(this.connections, sample.connections);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Clear all rolling windows and reset cumulative byte counters. */
|
|
119
|
+
reset() {
|
|
120
|
+
this.cpuUsage = [];
|
|
121
|
+
this.memUsage = [];
|
|
122
|
+
this.temp = [];
|
|
123
|
+
this.netRxRate = [];
|
|
124
|
+
this.netTxRate = [];
|
|
125
|
+
this.gpuUtil = [];
|
|
126
|
+
this.powerWatts = [];
|
|
127
|
+
this.rxPacketRate = [];
|
|
128
|
+
this.txPacketRate = [];
|
|
129
|
+
this.connections = [];
|
|
130
|
+
this.lastRxBytes = 0;
|
|
131
|
+
this.lastTxBytes = 0;
|
|
132
|
+
this.lastRxPackets = 0;
|
|
133
|
+
this.lastTxPackets = 0;
|
|
134
|
+
this.lastTs = 0;
|
|
135
|
+
}
|
|
98
136
|
|
|
99
137
|
/**
|
|
100
138
|
* Grow or shrink the rolling window (e.g. on terminal resize),
|
|
101
139
|
* trimming from the oldest samples.
|
|
102
140
|
* @param n - New maximum length; clamped to at least 1.
|
|
103
141
|
*/
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
142
|
+
setMaxLen(n: number) {
|
|
143
|
+
this.maxLen = Math.max(1, n);
|
|
144
|
+
for (const arr of [this.cpuUsage, this.memUsage, this.temp, this.netRxRate, this.netTxRate, this.gpuUtil, this.powerWatts, this.rxPacketRate, this.txPacketRate, this.connections]) {
|
|
145
|
+
while (arr.length > this.maxLen) arr.shift();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
110
148
|
|
|
111
149
|
private pushBounded(arr: number[], v: number) {
|
|
112
150
|
arr.push(v);
|