pyre-cli 9.5.0 → 9.5.2

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.
@@ -13,6 +13,7 @@ import chalk from 'chalk';
13
13
  import { sparkline, multiRowSparkline, brailleGraph, multiRowBrailleGraph } from '../sparkline.js';
14
14
  import type { History } from '../history.js';
15
15
  import type { StatsData, VisibleItems, TableOptions, AnomalyAlert } from './types.js';
16
+ import type { BlenderRenderData } from '../monitors/types.js';
16
17
  import { THEMES, type ThemeName, type ThemeColors } from './themes.js';
17
18
 
18
19
 
@@ -333,11 +334,102 @@ function thermalCard(data: StatsData, contentWidth: number): string[] {
333
334
 
334
335
  function networkCard(data: StatsData, contentWidth: number): string[] {
335
336
  const valW = Math.max(6, contentWidth - 8);
336
- return [
337
- statRow('Iface', truncatePlain(`${data.network.interface} (${data.network.ip})`, valW)),
338
- statRow('RX', `${formatBytes(data.network.rxBytes)} / ${data.network.rxPackets} pkt`),
339
- statRow('TX', `${formatBytes(data.network.txBytes)} / ${data.network.txPackets} pkt`),
340
- ];
337
+ const lines: string[] = [];
338
+ lines.push(statRow('Iface', truncatePlain(`${data.network.interface} (${data.network.ip})`, valW)));
339
+ const rxTotal = formatBytes(data.network.rxBytes);
340
+ const txTotal = formatBytes(data.network.txBytes);
341
+ const rxRateStr = formatBytes(data.network.rxRate);
342
+ const txRateStr = formatBytes(data.network.txRate);
343
+ lines.push(statRow('RX', `${rxTotal} · ${rxRateStr}/s`));
344
+ lines.push(statRow('TX', `${txTotal} · ${txRateStr}/s`));
345
+
346
+ const totalBytes = data.network.rxBytes + data.network.txBytes;
347
+ if (totalBytes > 0) {
348
+ const rxPct = Math.round((data.network.rxBytes / totalBytes) * 100);
349
+ const barW = Math.max(4, contentWidth - 20);
350
+ const rxBar = gaugeBar(rxPct, barW);
351
+ lines.push(statRow('RX/TX', `${rxBar} ${rxPct}%`));
352
+ }
353
+
354
+ if (data.network.protocols) {
355
+ const p = data.network.protocols;
356
+ const totalConns = (p.tcp.connections || 0) + (p.udp.connections || 0);
357
+ if (totalConns > 0) {
358
+ const tcpPct = Math.round((p.tcp.connections / totalConns) * 100);
359
+ lines.push(statRow('TCP', `${p.tcp.connections} conns · ${gaugeBar(tcpPct, Math.max(4, contentWidth - 20))} ${tcpPct}%`));
360
+ lines.push(statRow('UDP', `${p.udp.connections} conns`));
361
+ }
362
+ }
363
+ if (data.network.connectionStates) {
364
+ const cs = data.network.connectionStates;
365
+ const total = cs.established + cs.listening + cs.timeWait + cs.closeWait + cs.other;
366
+ if (total > 0) {
367
+ const estPct = Math.round((cs.established / total) * 100);
368
+ lines.push(statRow('Active', `${cs.established} ESTABLISHED · ${estPct}%`));
369
+ lines.push(statRow('Listen', `${cs.listening} LISTEN`));
370
+ if (cs.timeWait > 0) lines.push(statRow('TimeWait', `${cs.timeWait}`));
371
+ if (cs.closeWait > 0) lines.push(statRow('CloseWait', `${cs.closeWait}`));
372
+ }
373
+ }
374
+ if (data.network.listeningPorts && data.network.listeningPorts.length > 0) {
375
+ const ports = data.network.listeningPorts.slice(0, 8).join(', ');
376
+ lines.push(statRow('Ports', truncatePlain(ports, valW)));
377
+ }
378
+ if (data.network.topRemoteHosts && data.network.topRemoteHosts.length > 0) {
379
+ for (const host of data.network.topRemoteHosts.slice(0, 3)) {
380
+ lines.push(statRow('Host', truncatePlain(`${host.host} (${host.connectionCount})`, valW)));
381
+ }
382
+ }
383
+ return lines;
384
+ }
385
+
386
+ function networkDetailCard(data: StatsData, width: number, theme: ThemeColors): string[] {
387
+ const contentWidth = width - 4;
388
+ const lines = networkCard(data, contentWidth);
389
+
390
+ if (data.packets && data.packets.allProcesses && data.packets.allProcesses.length > 0) {
391
+ lines.push('');
392
+ lines.push(chalk.bold('PROCESSES & BANDWIDTH USAGE:'));
393
+ lines.push(chalk.dim('─'.repeat(contentWidth)));
394
+
395
+ const pWidth = 8;
396
+ const rxWidth = 12;
397
+ const txWidth = 12;
398
+ const totalWidth = 12;
399
+ const cmdWidth = Math.max(15, contentWidth - (pWidth + rxWidth + txWidth + totalWidth + 4 * 2));
400
+
401
+ lines.push(
402
+ chalk.bold('PID'.padEnd(pWidth)) + ' ' +
403
+ chalk.bold('COMMAND'.padEnd(cmdWidth)) + ' ' +
404
+ chalk.bold('RX'.padStart(rxWidth)) + ' ' +
405
+ chalk.bold('TX'.padStart(txWidth)) + ' ' +
406
+ chalk.bold('TOTAL'.padStart(totalWidth))
407
+ );
408
+
409
+ const sorted = [...data.packets.allProcesses]
410
+ .sort((a, b) => (b.rxBytes + b.txBytes) - (a.rxBytes + a.txBytes));
411
+
412
+ for (const proc of sorted) {
413
+ if (proc.rxBytes + proc.txBytes === 0) continue;
414
+ const rxStr = formatBytes(proc.rxBytes).padStart(rxWidth);
415
+ const txStr = formatBytes(proc.txBytes).padStart(txWidth);
416
+ const totalStr = formatBytes(proc.rxBytes + proc.txBytes).padStart(totalWidth);
417
+ const cmdStr = truncatePlain(proc.command, cmdWidth).padEnd(cmdWidth);
418
+
419
+ lines.push(
420
+ chalk.cyan(proc.pid.toString().padEnd(pWidth)) + ' ' +
421
+ cmdStr + ' ' +
422
+ chalk.yellow(rxStr) + ' ' +
423
+ chalk.yellow(txStr) + ' ' +
424
+ chalk.bold(totalStr)
425
+ );
426
+ }
427
+ } else {
428
+ lines.push('');
429
+ lines.push(chalk.dim('No active network process data available.'));
430
+ }
431
+
432
+ return lines;
341
433
  }
342
434
 
343
435
  function packetCard(data: StatsData, contentWidth: number): string[] | null {
@@ -347,8 +439,19 @@ function packetCard(data: StatsData, contentWidth: number): string[] | null {
347
439
  lines.push(statRow('RX', `${data.packets.rxPackets} pkt`));
348
440
  lines.push(statRow('TX', `${data.packets.txPackets} pkt`));
349
441
  lines.push(statRow('Conns', `${data.packets.connections} TCP`));
442
+ if (data.packets.protocolStats) {
443
+ const ps = data.packets.protocolStats;
444
+ lines.push(statRow('TCP', `${ps.tcp.connections} conns`));
445
+ lines.push(statRow('UDP', `${ps.udp.connections} conns`));
446
+ }
447
+ if (data.packets.connectionStates) {
448
+ const cs = data.packets.connectionStates;
449
+ lines.push(statRow('ESTAB', `${cs.established}`));
450
+ lines.push(statRow('LISTEN', `${cs.listening}`));
451
+ }
350
452
  if (data.packets.topProcesses && data.packets.topProcesses.length) {
351
- lines.push(statRow('Top Proc', truncatePlain(data.packets.topProcesses[0].command, contentWidth - 10)));
453
+ const topProc = data.packets.topProcesses[0];
454
+ lines.push(statRow('Top Proc', `${truncatePlain(topProc.command, contentWidth - 25)} (${formatBytes(topProc.rxBytes + topProc.txBytes)})`));
352
455
  }
353
456
  if (data.packets.allProcesses && data.packets.allProcesses.length > 1) {
354
457
  lines.push(statRow('Net Procs', `${data.packets.allProcesses.length} processes`));
@@ -392,13 +495,14 @@ export const TAB_DEFS: { id: string; label: string; key: string }[] = [
392
495
  { id: 'power', label: 'Power', key: '4' },
393
496
  { id: 'battery', label: 'Battery', key: '5' },
394
497
  { id: 'thermal', label: 'Thermal', key: '6' },
395
- { id: 'network', label: 'Network', key: '7' },
396
- { id: 'packets', label: 'Packets', key: '8' },
397
- { id: 'tasks', label: 'Tasks', key: '9' },
398
- { id: 'disk', label: 'Disk', key: '0' },
399
- { id: 'process', label: 'Process', key: 'P' },
498
+ { id: 'packets', label: 'Conns', key: '7' },
499
+ { id: 'tasks', label: 'Tasks', key: '8' },
500
+ { id: 'disk', label: 'Disk', key: '9' },
501
+ { id: 'process', label: 'Process', key: '0' },
400
502
  { id: 'p2p', label: 'P2P', key: 'R' },
401
503
  { id: 'anomalies', label: 'Anomalies', key: 'A' },
504
+ { id: 'containers', label: 'Containers', key: 'C' },
505
+ { id: 'blender', label: 'Blender', key: 'B' },
402
506
  ];
403
507
 
404
508
  /** The number of leading columns of indent before the tab bar's first character (see `tabBar`). */
@@ -620,6 +724,58 @@ function diskTableLines(
620
724
  return [head, borderAccent('─'.repeat(contentWidth)), ...rows];
621
725
  }
622
726
 
727
+ function containersTableLines(
728
+ containers: { id: string; name: string; cpuPercent: number; memUsage: string; memPercent: number; netIO: string; blockIO: string; pids: number }[],
729
+ contentWidth: number,
730
+ borderAccent = THEMES.default.border
731
+ ): string[] {
732
+ const idW = 14, cpuW = 8, memW = 8, memUsgW = 20, netW = 20, blockW = 20, pidsW = 6;
733
+ const nameW = Math.max(10, contentWidth - idW - cpuW - memW - memUsgW - netW - blockW - pidsW - 7);
734
+
735
+ const head =
736
+ chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' CONTAINER ID ') +
737
+ chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' CPU% ') +
738
+ chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' MEM% ') +
739
+ chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' MEM USAGE ') +
740
+ chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' NET I/O ') +
741
+ chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' BLOCK I/O ') +
742
+ chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' PIDS ') +
743
+ chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' NAME'.padEnd(nameW + 1));
744
+
745
+ const rows = containers.map(c => {
746
+ const cpuStr = pctColor(c.cpuPercent)(`${c.cpuPercent.toFixed(1)}`.padEnd(cpuW));
747
+ const memStr = pctColor(c.memPercent)(`${c.memPercent.toFixed(1)}`.padEnd(memW));
748
+
749
+ return (
750
+ truncatePlain(c.id, idW - 1).padEnd(idW) +
751
+ cpuStr +
752
+ memStr +
753
+ c.memUsage.padEnd(memUsgW) +
754
+ c.netIO.padEnd(netW) +
755
+ c.blockIO.padEnd(blockW) +
756
+ String(c.pids).padEnd(pidsW) +
757
+ truncatePlain(c.name, nameW)
758
+ );
759
+ });
760
+
761
+ return [head, borderAccent('─'.repeat(contentWidth)), ...rows];
762
+ }
763
+
764
+ function blenderCard(renders: BlenderRenderData[], contentWidth: number, theme: ThemeColors): string[] {
765
+ if (!renders.length) return ['No active Blender renders detected'];
766
+ const lines: string[] = [];
767
+ for (const r of renders.slice(0, 8)) {
768
+ const statusColor = r.status === 'rendering' ? chalk.green : r.status === 'finalizing' ? chalk.yellow : r.status === 'loading' ? chalk.cyan : chalk.dim;
769
+ const pctStr = `${r.completionPercent}%`;
770
+ const barW = Math.max(4, contentWidth - 28);
771
+ const bar = gaugeBar(r.completionPercent, barW);
772
+ lines.push(`${theme.blender(`PID ${r.pid}`)} ${truncatePlain(r.blendFile, 18)} ${r.renderEngine}`);
773
+ lines.push(` Fra ${String(r.currentFrame).padStart(4)}/${r.totalFrames} ${bar} ${pctStr}`);
774
+ lines.push(` ${r.elapsedSec}s elapsed · ${statusColor(r.status)}${r.sampleCount !== undefined ? ` · ${r.sampleCount} spp` : ''}`);
775
+ }
776
+ return lines;
777
+ }
778
+
623
779
  // --- main dashboard ------------------------------------------------------------
624
780
 
625
781
  function detailPanelTitle(panel: string, trackedPid?: number | null): string {
@@ -635,6 +791,8 @@ function detailPanelTitle(panel: string, trackedPid?: number | null): string {
635
791
  tasks: 'Tasks',
636
792
  disk: 'Disk',
637
793
  process: trackedPid !== undefined && trackedPid !== null ? `Processes (Following PID ${trackedPid})` : 'Processes',
794
+ containers: 'Containers',
795
+ blender: 'Blender Renders',
638
796
  };
639
797
  return map[panel] || panel;
640
798
  }
@@ -667,26 +825,73 @@ function activeDetailLines(data: StatsData, activePanel: string, width: number,
667
825
  case 'thermal':
668
826
  return thermalCard(data, contentWidth);
669
827
  case 'network':
670
- return networkCard(data, contentWidth);
828
+ return networkDetailCard(data, width, theme);
671
829
  case 'packets': {
672
- if (!data.packets) return ['No packet data available'];
673
- const lines: string[] = [];
674
- lines.push(statRow('Total', `${data.packets.totalPackets} pkt`));
675
- lines.push(statRow('RX', `${data.packets.rxPackets} pkt`));
676
- lines.push(statRow('TX', `${data.packets.txPackets} pkt`));
677
- lines.push(statRow('Conns', `${data.packets.connections} TCP`));
678
- if (data.packets.interfaces && data.packets.interfaces.length) {
679
- lines.push(statRow('Ifaces', `${data.packets.interfaces.length} active`));
680
- for (const iface of data.packets.interfaces.slice(0, 8)) {
681
- lines.push(statRow(` ${iface.iface}`, `RX ${formatBytes(iface.rxBytes)} / TX ${formatBytes(iface.txBytes)}`));
682
- }
830
+ if (!data.packets || !data.packets.allProcesses || !data.packets.allProcesses.length) {
831
+ return ['No advanced network tracking data available.'];
683
832
  }
684
- if (data.packets.allProcesses && data.packets.allProcesses.length) {
685
- lines.push(statRow('Net Procs', `${data.packets.allProcesses.length} total`));
686
- for (const proc of data.packets.allProcesses.slice(0, 10)) {
687
- lines.push(statRow(` ${proc.pid}`, `${truncatePlain(proc.command, 20)} TX ${formatBytes(proc.txBytes)}`));
833
+
834
+ const lines: string[] = [];
835
+ const pWidth = 8;
836
+ const cmdWidth = 15;
837
+ const protoWidth = 5;
838
+ const stateWidth = 11;
839
+ const rxWidth = 10;
840
+ const txWidth = 10;
841
+
842
+ const remaining = Math.max(20, width - (pWidth + cmdWidth + protoWidth + stateWidth + rxWidth + txWidth + 7 * 2));
843
+ const localWidth = Math.floor(remaining * 0.4);
844
+ const remoteWidth = remaining - localWidth;
845
+
846
+ const header =
847
+ chalk.bold('PID'.padEnd(pWidth)) + ' ' +
848
+ chalk.bold('COMMAND'.padEnd(cmdWidth)) + ' ' +
849
+ chalk.bold('PROTO'.padEnd(protoWidth)) + ' ' +
850
+ chalk.bold('LOCAL'.padEnd(localWidth)) + ' ' +
851
+ chalk.bold('REMOTE'.padEnd(remoteWidth)) + ' ' +
852
+ chalk.bold('STATE'.padEnd(stateWidth)) + ' ' +
853
+ chalk.bold('RX'.padStart(rxWidth)) + ' ' +
854
+ chalk.bold('TX'.padStart(txWidth));
855
+
856
+ lines.push(header);
857
+ lines.push(chalk.dim('─'.repeat(width)));
858
+
859
+ for (const proc of data.packets.allProcesses) {
860
+ if (!proc.connections || proc.connections.length === 0) continue;
861
+
862
+ const summaryCmd = truncatePlain(proc.command, cmdWidth + protoWidth + localWidth + remoteWidth + stateWidth + 8);
863
+ const summaryRx = formatBytes(proc.rxBytes).padStart(rxWidth);
864
+ const summaryTx = formatBytes(proc.txBytes).padStart(txWidth);
865
+
866
+ lines.push(
867
+ chalk.cyan(proc.pid.toString().padEnd(pWidth)) + ' ' +
868
+ chalk.cyan(summaryCmd.padEnd(width - pWidth - rxWidth - txWidth - 6)) + ' ' +
869
+ chalk.yellow(summaryRx) + ' ' +
870
+ chalk.yellow(summaryTx)
871
+ );
872
+
873
+ for (const conn of proc.connections) {
874
+ const cProto = chalk.dim(truncatePlain(conn.protocol, protoWidth).padEnd(protoWidth));
875
+ const cLocal = chalk.dim(truncatePlain(conn.local, localWidth).padEnd(localWidth));
876
+ const cRemote = truncatePlain(conn.remote, remoteWidth).padEnd(remoteWidth);
877
+
878
+ const cStateStr = conn.state === 'Established' ? chalk.green('ESTABLISHED'.padEnd(stateWidth)) : chalk.dim(truncatePlain(conn.state, stateWidth).padEnd(stateWidth));
879
+ const cRx = chalk.dim(formatBytes(conn.rxBytes).padStart(rxWidth));
880
+ const cTx = chalk.dim(formatBytes(conn.txBytes).padStart(txWidth));
881
+
882
+ lines.push(
883
+ ' '.repeat(pWidth) + ' ' +
884
+ ' '.repeat(cmdWidth) + ' ' +
885
+ cProto + ' ' +
886
+ cLocal + ' ' +
887
+ cRemote + ' ' +
888
+ cStateStr + ' ' +
889
+ cRx + ' ' +
890
+ cTx
891
+ );
688
892
  }
689
893
  }
894
+
690
895
  return lines;
691
896
  }
692
897
  case 'tasks':
@@ -758,6 +963,13 @@ function activeDetailLines(data: StatsData, activePanel: string, width: number,
758
963
  }
759
964
  return lines;
760
965
  }
966
+ case 'containers': {
967
+ return data.containers && data.containers.length ? containersTableLines(data.containers, width - 4, theme.border) : ['No container data available'];
968
+ }
969
+ case 'blender': {
970
+ const renders = data.blenderRenders && data.blenderRenders.length ? data.blenderRenders : [];
971
+ return blenderCard(renders, contentWidth, theme);
972
+ }
761
973
  default:
762
974
  return null;
763
975
  }
@@ -1034,7 +1246,7 @@ export function formatTable(data: StatsData, opts: TableOptions = {}): string {
1034
1246
  lines.push(gaugeRow('GPU', data.gpu.utilization, contentWidth));
1035
1247
  }
1036
1248
  const tempC = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die;
1037
- if (tempC !== undefined) {
1249
+ if (tempC != null) {
1038
1250
  const tempStr = formatTempUnit(tempC, opts.tempUnit);
1039
1251
  lines.push(statRow('Temp', thermalColor(data.thermal.pressureLevel)(tempStr)));
1040
1252
  }
@@ -26,6 +26,7 @@ export interface ThemeColors {
26
26
  disk: (s: string) => string;
27
27
  graphs: (s: string) => string;
28
28
  process: (s: string) => string;
29
+ blender: (s: string) => string;
29
30
  }
30
31
 
31
32
  export const BUILTIN_THEMES: Record<string, ThemeColors> = {
@@ -41,6 +42,7 @@ export const BUILTIN_THEMES: Record<string, ThemeColors> = {
41
42
  disk: chalk.hex('#f472b6'),
42
43
  graphs: chalk.hex('#22d3ee'),
43
44
  process: chalk.hex('#e5e7eb'),
45
+ blender: chalk.hex('#f97316'),
44
46
  },
45
47
  dracula: {
46
48
  border: chalk.hex('#6272a4'),
@@ -54,6 +56,7 @@ export const BUILTIN_THEMES: Record<string, ThemeColors> = {
54
56
  disk: chalk.hex('#ff5555'),
55
57
  graphs: chalk.hex('#8be9fd'),
56
58
  process: chalk.hex('#f8f8f2'),
59
+ blender: chalk.hex('#ffb86c'),
57
60
  },
58
61
  cyberpunk: {
59
62
  border: chalk.hex('#ff0055'),
@@ -67,6 +70,7 @@ export const BUILTIN_THEMES: Record<string, ThemeColors> = {
67
70
  disk: chalk.hex('#ff007f'),
68
71
  graphs: chalk.hex('#00ffcc'),
69
72
  process: chalk.hex('#ffffff'),
73
+ blender: chalk.hex('#ff6600'),
70
74
  },
71
75
  monochrome: {
72
76
  border: chalk.gray,
@@ -80,6 +84,7 @@ export const BUILTIN_THEMES: Record<string, ThemeColors> = {
80
84
  disk: chalk.white.bold,
81
85
  graphs: chalk.white.bold,
82
86
  process: chalk.white.bold,
87
+ blender: chalk.white.bold,
83
88
  },
84
89
  nord: {
85
90
  border: chalk.hex('#4c566a'),
@@ -93,6 +98,7 @@ export const BUILTIN_THEMES: Record<string, ThemeColors> = {
93
98
  disk: chalk.hex('#b48ead'),
94
99
  graphs: chalk.hex('#88c0d0'),
95
100
  process: chalk.hex('#eceff4'),
101
+ blender: chalk.hex('#d08770'),
96
102
  },
97
103
  gruvbox: {
98
104
  border: chalk.hex('#928374'),
@@ -106,6 +112,7 @@ export const BUILTIN_THEMES: Record<string, ThemeColors> = {
106
112
  disk: chalk.hex('#d3869b'),
107
113
  graphs: chalk.hex('#fb4934'),
108
114
  process: chalk.hex('#ebdbb2'),
115
+ blender: chalk.hex('#fe8019'),
109
116
  },
110
117
  };
111
118
 
@@ -134,6 +141,7 @@ export function loadCustomThemes(): Record<string, ThemeColors> {
134
141
  disk: buildColor(data.disk),
135
142
  graphs: buildColor(data.graphs),
136
143
  process: buildColor(data.process),
144
+ blender: buildColor(data.blender),
137
145
  };
138
146
  } catch {
139
147
  // ignore bad theme file
@@ -24,6 +24,7 @@ export interface ThemeColors {
24
24
  disk: (s: string) => string;
25
25
  graphs: (s: string) => string;
26
26
  process: (s: string) => string;
27
+ blender: (s: string) => string;
27
28
  }
28
29
 
29
30
  export type { StatsData };
@@ -40,6 +41,8 @@ export interface VisibleItems {
40
41
  tasks?: boolean;
41
42
  disk?: boolean;
42
43
  process?: boolean;
44
+ containers?: boolean;
45
+ blender?: boolean;
43
46
  tree?: boolean;
44
47
  }
45
48
 
@@ -67,7 +70,7 @@ export interface TableOptions {
67
70
  /** Show process tree view instead of flat list. */
68
71
  treeView?: boolean;
69
72
  /** Active panel for detail/focus view. 'grid' means show all panels. */
70
- activePanel?: 'grid' | 'cpu' | 'mem' | 'gpu' | 'power' | 'battery' | 'thermal' | 'network' | 'packets' | 'tasks' | 'disk' | 'process' | 'p2p' | 'anomalies';
73
+ activePanel?: 'grid' | 'cpu' | 'mem' | 'gpu' | 'power' | 'battery' | 'thermal' | 'network' | 'packets' | 'tasks' | 'disk' | 'process' | 'p2p' | 'anomalies' | 'containers' | 'blender';
71
74
  /** Active anomaly alerts from the statistical detector. */
72
75
  anomalies?: AnomalyAlert[];
73
76
  /** Historical anomaly alerts for the anomalies tab. */
package/src/index.ts CHANGED
@@ -82,32 +82,33 @@ program
82
82
  .option('--port <port>', 'Port number for web server mode', '3000');
83
83
 
84
84
  program.addHelpText('after', `
85
- Commands:
86
- live Interactive live dashboard
87
- check One-line plain-English health summary
88
- pipe Continuous newline-delimited JSON stream for scripting
89
- stress Synthetic CPU/GPU load generator
90
- ui Launch the native macOS UI dashboard
91
- web Serve an auto-refreshing live web portal
92
- ssh <host> Stream live stats from a remote Mac over SSH
93
- fleet <host1> [host2]... Multi-host live dashboard monitoring multiple Macs
94
- bench <cmd> Log CPU, memory, network, power draw, and estimate kWh cost
95
- benchmark Run a 1-minute CPU benchmark calculating digits of PI
96
- anomalies Compute z-score anomalies and print plain-language digest
97
- doctor Run system diagnostics
98
- extensions System Extensions inspector
99
- brew Homebrew health panel
100
- update Check for pyre-cli updates
101
- profile <save|load|list> Atomic configuration profile management
102
- config <show|reset> View or reset persistent configuration file
103
- history Graph historical resource trends from CSV logs
104
- diff <file1> <file2> Compare two saved snapshot files side-by-side
105
- info Concise hardware summary
106
- completions <shell> Generate shell auto-completion scripts
107
- xbar Generate an xbar / SwiftBar menu bar plugin script
108
- p2p <server|connect> Start a P2P server or connect to one
109
- server Print commands for starting P2P server/client
110
- `);
85
+ Commands:
86
+ live Interactive live dashboard
87
+ check One-line plain-English health summary
88
+ pipe Continuous newline-delimited JSON stream for scripting
89
+ stress Synthetic CPU/GPU load generator
90
+ ui Launch the native macOS UI dashboard
91
+ web Serve an auto-refreshing live web portal
92
+ ssh <host> Stream live stats from a remote Mac over SSH
93
+ fleet <host1> [host2]... Multi-host live dashboard monitoring multiple Macs
94
+ bench <cmd> Log CPU, memory, network, power draw, and estimate kWh cost
95
+ benchmark Run a 1-minute CPU benchmark calculating digits of PI
96
+ anomalies Compute z-score anomalies and print plain-language digest
97
+ doctor Run system diagnostics
98
+ extensions System Extensions inspector
99
+ brew Homebrew health panel
100
+ update Check for pyre-cli updates
101
+ profile <save|load|list> Atomic configuration profile management
102
+ config <show|reset> View or reset persistent configuration file
103
+ history Graph historical resource trends from CSV logs
104
+ diff <file1> <file2> Compare two saved snapshot files side-by-side
105
+ info Concise hardware summary
106
+ completions <shell> Generate shell auto-completion scripts
107
+ xbar Generate an xbar / SwiftBar menu bar plugin script
108
+ p2p <server|connect> Start a P2P server or connect to one
109
+ server Print commands for starting P2P server/client
110
+ blender Track active Blender render jobs and their progress
111
+ `);
111
112
 
112
113
  program.parse(process.argv);
113
114
 
@@ -233,6 +234,32 @@ async function main() {
233
234
  return;
234
235
  }
235
236
 
237
+ if (cmd === 'blender') {
238
+ const { collectBlenderRenders } = await import('./monitors/blender.js');
239
+ const renders = await collectBlenderRenders();
240
+ if (!renders.length) {
241
+ console.log(chalk.dim('No active Blender renders detected.'));
242
+ return;
243
+ }
244
+ const lines: string[] = [];
245
+ lines.push(chalk.bold(' Blender Render Tracker'));
246
+ lines.push('');
247
+ for (const r of renders) {
248
+ const statusColor = r.status === 'rendering' ? chalk.green : r.status === 'finalizing' ? chalk.yellow : r.status === 'loading' ? chalk.cyan : chalk.dim;
249
+ lines.push(` PID: ${r.pid}`);
250
+ lines.push(` Blend File: ${r.blendFile}`);
251
+ lines.push(` Engine: ${r.renderEngine}`);
252
+ lines.push(` Frames: ${r.currentFrame} / ${r.totalFrames} (${r.completionPercent}%)`);
253
+ lines.push(` Elapsed: ${r.elapsedSec}s`);
254
+ lines.push(` Status: ${statusColor(r.status)}`);
255
+ lines.push(` Output: ${r.outputPath}`);
256
+ if (r.sampleCount !== undefined) lines.push(` Samples: ${r.sampleCount}`);
257
+ lines.push('');
258
+ }
259
+ console.log(lines.join('\n'));
260
+ return;
261
+ }
262
+
236
263
  if (cmd === 'brew') {
237
264
  const { printBrewHealthReport } = await import('./brewHealth.js');
238
265
  await printBrewHealthReport();
package/src/live/input.ts CHANGED
@@ -38,6 +38,7 @@ import { writeConfig } from '../state/config.js';
38
38
  splashAnimation: state.splashAnimation,
39
39
  visiblePanels: { ...state.visiblePanels },
40
40
  notificationsEnabled: state.notificationsEnabled,
41
+ dockerModeConfirmed: state.dockerModeConfirmed,
41
42
  });
42
43
  }
43
44
 
@@ -179,6 +180,51 @@ function handleInputModeKey(str: string, key: readline.Key) {
179
180
  return;
180
181
  }
181
182
 
183
+ if (state.inputMode === 'docker-alert') {
184
+ if (isEscKey(key, str)) {
185
+ state.inputMode = null;
186
+ state.dockerModeConfirmed = true;
187
+ render();
188
+ return;
189
+ }
190
+ if (isUpKey(key, str)) {
191
+ state.dockerSelectionIndex = Math.max(0, state.dockerSelectionIndex - 1);
192
+ render();
193
+ return;
194
+ }
195
+ if (isDownKey(key, str)) {
196
+ state.dockerSelectionIndex = Math.min(1, state.dockerSelectionIndex + 1);
197
+ render();
198
+ return;
199
+ }
200
+ if (isEnterKey(key, str) || str === ' ') {
201
+ state.dockerModeConfirmed = true;
202
+ state.inputMode = null;
203
+
204
+ if (state.dockerSelectionIndex === 0) {
205
+ // Docker Mode
206
+ state.visiblePanels.battery = false;
207
+ state.visiblePanels.thermal = false;
208
+ state.visiblePanels.power = false;
209
+ state.visiblePanels.gpu = false;
210
+ state.detailed = false;
211
+ } else {
212
+ // Regular TUI
213
+ state.visiblePanels.battery = true;
214
+ state.visiblePanels.thermal = true;
215
+ state.visiblePanels.power = true;
216
+ state.visiblePanels.gpu = true;
217
+ }
218
+
219
+ invalidateTableCache();
220
+ invalidateFrame();
221
+ persistConfig();
222
+ render();
223
+ return;
224
+ }
225
+ return;
226
+ }
227
+
182
228
  if (state.inputMode === 'signal') {
183
229
  if (isUpKey(key, str)) {
184
230
  const idx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer as typeof state.SIGNAL_OPTIONS[number]);
@@ -540,6 +586,18 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
540
586
  render();
541
587
  return;
542
588
  }
589
+ case 'C': {
590
+ state.activePanel = state.activePanel === 'containers' ? 'grid' : 'containers';
591
+ setStatus(state.activePanel === 'grid' ? 'Grid view' : 'Containers panel');
592
+ render();
593
+ return;
594
+ }
595
+ case 'B': {
596
+ state.activePanel = state.activePanel === 'blender' ? 'grid' : 'blender';
597
+ setStatus(state.activePanel === 'grid' ? 'Grid view' : 'Blender panel');
598
+ render();
599
+ return;
600
+ }
543
601
  }
544
602
 
545
603
  switch (key.name) {
@@ -716,16 +774,16 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
716
774
  process.once('SIGINT', () => stopLive());
717
775
  }
718
776
 
719
- function tabKeyToId(str: string): string | null {
720
- const map: Record<string, string> = { '1': 'cpu', '2': 'mem', '3': 'gpu', '4': 'power', '5': 'battery', '6': 'thermal', '7': 'network', '8': 'packets', '9': 'tasks', '0': 'disk', 'p': 'process', 'r': 'p2p', 'A': 'anomalies' };
777
+ function tabKeyToId(str: string): ActivePanel | null {
778
+ const map: Record<string, ActivePanel> = { '1': 'cpu', '2': 'mem', '3': 'gpu', '4': 'power', '5': 'battery', '6': 'thermal', '7': 'network', '8': 'packets', '9': 'tasks', '0': 'disk', 'p': 'process', 'C': 'containers', 'r': 'p2p', 'A': 'anomalies', 'B': 'blender' };
721
779
  return map[str] ?? null;
722
780
  }
723
781
 
724
782
  function cycleTab(direction: number) {
725
- const tabs = state.PANEL_TABS.map(t => t.id);
783
+ const tabs = state.PANEL_TABS.map(t => t.id) as string[];
726
784
  const current = state.activePanel === 'grid' ? -1 : tabs.indexOf(state.activePanel);
727
785
  const next = (current + direction + tabs.length) % tabs.length;
728
- state.activePanel = tabs[next];
786
+ state.activePanel = tabs[next] as typeof state.activePanel;
729
787
  setStatus(`Panel: ${state.activePanel.toUpperCase()}`);
730
788
  render();
731
789
  }
@@ -216,6 +216,10 @@ function render() {
216
216
  writeFrame(renderCredits(cols, rows));
217
217
  return;
218
218
  }
219
+ if (state.inputMode === 'docker-alert') {
220
+ writeFrame(renderDockerAlert(cols, rows));
221
+ return;
222
+ }
219
223
  const lines: string[] = [];
220
224
  const theme = THEMES[state.currentTheme] || THEMES.default;
221
225
 
@@ -564,4 +568,31 @@ function renderCredits(cols: number, rows: number): string[] {
564
568
  return out;
565
569
  }
566
570
 
571
+ function renderDockerAlert(cols: number, rows: number): string[] {
572
+ const out: string[] = [];
573
+ const content = [
574
+ chalk.bold.red('Docker Container Detected'),
575
+ '',
576
+ 'Pyre is running inside a container. Some hardware sensors',
577
+ '(battery, thermal, power) may not be available or relevant.',
578
+ '',
579
+ chalk.cyan('Do you wish to continue in Docker mode?'),
580
+ ''
581
+ ];
582
+
583
+ const options = ['Yes, use Docker mode (hide hardware sensors)', 'No, launch regular TUI with all features'];
584
+ for (let i = 0; i < options.length; i++) {
585
+ const isSelected = i === state.dockerSelectionIndex;
586
+ const prefix = isSelected ? chalk.cyan.bold(' ▶ ') : ' ';
587
+ const text = isSelected ? chalk.bgCyan.black(` ${options[i]} `) : chalk.dim(` ${options[i]} `);
588
+ content.push(prefix + text);
589
+ }
590
+
591
+ content.push('');
592
+ content.push(chalk.dim('Press ↑/↓ to select, Enter to confirm, Esc to skip.'));
593
+
594
+ out.push(...panel('DOCKER DETECTED', content, cols, chalk.red, chalk.dim, rows - 2));
595
+ return out;
596
+ }
597
+
567
598
  export { render, footerLine, renderCustomizerOverlay, checkAlerts, processRowBudget, invalidateTableCache, invalidateFrame };