pyre-cli 1.4.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pyre-cli",
3
- "version": "1.4.0",
4
- "description": "CLI Mac monitoring tool: temps, system stats, live dashboard, and export",
3
+ "version": "2.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": {
@@ -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';
@@ -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
- if (data.gpu) {
56
- rows.push(['gpu', 'model', data.gpu.model]);
57
- rows.push(['gpu', 'memory', String(data.gpu.memory)]);
58
- rows.push(['gpu', 'utilization', String(data.gpu.utilization)]);
59
- if (data.gpu.temperature) rows.push(['gpu', 'tempC', String(data.gpu.temperature)]);
60
- rows.push(['gpu', 'processes', String(data.gpu.processes)]);
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
- for (const p of data.processes) {
64
- rows.push(['process', `pid=${p.pid}`, `${p.pid}`]);
65
- rows.push(['process', `ppid=${p.pid}`, String(p.ppid)]);
66
- rows.push(['process', `user=${p.pid}`, p.user]);
67
- rows.push(['process', `cpu=${p.pid}`, String(p.cpu)]);
68
- rows.push(['process', `mem=${p.pid}`, String(p.mem)]);
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
- return rows.map(r => r.map(item => `"${String(item).replace(/"/g, '""')}"`).join(',')).join('\n');
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');
@@ -163,14 +163,18 @@ function gpuCard(data: StatsData, contentWidth: number): string[] | null {
163
163
  return lines;
164
164
  }
165
165
 
166
- function powerCard(data: StatsData): string[] | null {
167
- if (!data.power) return null;
168
- const lines: string[] = [];
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
- return lines.length ? lines : null;
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
- // --- header ------------------------------------------------------------------
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
- * Render the full static dashboard as a single string.
364
- *
365
- * Cards are arranged in rows of {@link gridColumns} columns,
366
- * and the process table is appended below. Visibility of
367
- * individual panels is controlled via {@link TableOptions.visible}.
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);
@@ -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
- cpuUsage: number;
58
- memUsage: number;
59
- temp: number | null;
60
- rxBytes: number;
61
- txBytes: number;
62
- gpuUtil?: number;
63
- }) {
64
- const now = Date.now();
65
- let rxRate = 0;
66
- let txRate = 0;
67
-
68
- if (this.lastTs > 0) {
69
- const dtSeconds = Math.max((now - this.lastTs) / 1000, 0.001);
70
- rxRate = Math.max(0, (sample.rxBytes - this.lastRxBytes) / dtSeconds);
71
- txRate = Math.max(0, (sample.txBytes - this.lastTxBytes) / dtSeconds);
72
- }
73
-
74
- this.lastRxBytes = sample.rxBytes;
75
- this.lastTxBytes = sample.txBytes;
76
- this.lastTs = now;
77
-
78
- this.pushBounded(this.cpuUsage, sample.cpuUsage);
79
- this.pushBounded(this.memUsage, sample.memUsage);
80
- if (sample.temp !== null) this.pushBounded(this.temp, sample.temp);
81
- this.pushBounded(this.netRxRate, rxRate);
82
- this.pushBounded(this.netTxRate, txRate);
83
- if (sample.gpuUtil !== undefined) this.pushBounded(this.gpuUtil, sample.gpuUtil);
84
- }
85
-
86
- /** Clear all rolling windows and reset cumulative byte counters. */
87
- reset() {
88
- this.cpuUsage = [];
89
- this.memUsage = [];
90
- this.temp = [];
91
- this.netRxRate = [];
92
- this.netTxRate = [];
93
- this.gpuUtil = [];
94
- this.lastRxBytes = 0;
95
- this.lastTxBytes = 0;
96
- this.lastTs = 0;
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
- setMaxLen(n: number) {
105
- this.maxLen = Math.max(1, n);
106
- for (const arr of [this.cpuUsage, this.memUsage, this.temp, this.netRxRate, this.netTxRate, this.gpuUtil]) {
107
- while (arr.length > this.maxLen) arr.shift();
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);