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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # pyre
2
2
 
3
- Mac system monitoring CLI: temps, CPU, memory, disk, battery, GPU, power draw, live dashboard, packet monitor, process management, export, alerts, P2P live data streaming, web dashboard, SSH monitoring, and micro-benchmarking.
3
+ Mac system monitoring CLI: temps, CPU, memory, disk, battery, GPU, power draw, live dashboard, packet monitor, process management, export, alerts, P2P live data streaming, web dashboard, SSH monitoring, micro-benchmarking, and Blender render tracking.
4
4
 
5
5
  ![Version](https://img.shields.io/badge/version-5.0.0-blue)
6
6
  ![macOS](https://img.shields.io/badge/macos-14%2B-lightgrey)
@@ -21,6 +21,7 @@ Mac system monitoring CLI: temps, CPU, memory, disk, battery, GPU, power draw, l
21
21
  - **Disk I/O throughput & space** — read/write speed per volume and mounted volume space usage
22
22
  - **Battery & power health** — level, power source, condition, charge cycles, max capacity, health trends, estimated time to empty, discharge rate, power draw in watts
23
23
  - **Packet monitor** — network packet counts, packet rates, active TCP connections, top network processes
24
+ - **Advanced network monitoring** — real-time bandwidth rates (RX/TX bytes/sec), protocol breakdown (TCP/UDP), connection state analysis (ESTABLISHED, LISTENING, TIME_WAIT, CLOSE_WAIT), listening port detection, top remote hosts, per-process network I/O tracking, and GlassWire-style modern TUI panel with sparkline graphs
24
25
  - **Task list** — running tasks/applications sorted by CPU with PID, user, memory, state, and runtime
25
26
  - **Thermal state** — CPU temperature via `pmset` and `powermetrics` (sudo for detailed readings)
26
27
  - **Network** — RX/TX bytes and per-second rates
@@ -28,6 +29,7 @@ Mac system monitoring CLI: temps, CPU, memory, disk, battery, GPU, power draw, l
28
29
  - **Interactive live dashboard** — full-screen TUI with keyboard-driven controls, mouse support, configurable panel layout, and tab-based panel navigation
29
30
  - **Snapshot export** — JSON, CSV, TSV, HTML, or Markdown output formats
30
31
  - **Continuous CSV logging & Anomaly Digest** — automatic per-tick logging and statistical spike analysis (`pyre anomalies`)
32
+ - **Blender render tracking** — automatically detects running Blender background renders, tracks elapsed time, frame progress, completion percentage, render engine, and blend file; fully loggable via CSV/JSON/Markdown exports
31
33
  - **Visual themes & custom themes** — six built-in color themes plus user-defined JSON theme support in `~/.config/pyre/themes/`
32
34
  - **Persistent configuration & Profiles** — save themes and thresholds in `~/.config/pyre/config.json`, plus atomic profile management (`pyre profile`)
33
35
  - **Alert system & webhooks** — configurable threshold alerts with terminal bell, desktop notifications, webhook POST payload execution (`--webhook-url`), and custom shell command execution (`--alert-cmd`)
@@ -132,6 +134,7 @@ These options apply to both static snapshots and `pyre live`, except where noted
132
134
  | `pyre doctor` | Run system diagnostics (permissions, Gatekeeper, SIP, XProtect, P2P network) |
133
135
  | `pyre extensions` | System Extensions inspector (`systemextensionsctl list`) |
134
136
  | `pyre brew` | Homebrew health panel (installed formulae, outdated count, Cellar disk size, doctor summary) |
137
+ | `pyre blender` | Track active Blender background renders: PID, blend file, engine, frame progress, elapsed time, and completion status |
135
138
  | `pyre update` | Check npm registry for latest `pyre-cli` release |
136
139
  | `pyre profile <save\|load\|list> [name]` | Atomic configuration profile management (`~/.config/pyre/profiles/`) |
137
140
  | `pyre config <show\|reset>` | View or reset persistent configuration file (`~/.config/pyre/config.json`) |
@@ -314,6 +317,8 @@ Run with `pyre live`.
314
317
  | `←` / `→` / `Tab` | Cycle to previous / next panel tab |
315
318
  | `1`–`9`, `0` | Jump to panel by number (cpu, mem, gpu, power, battery, thermal, network, packets, tasks, disk) |
316
319
  | `P` | Toggle process panel |
320
+ | `C` | Toggle containers panel |
321
+ | `B` | Toggle Blender render tracker panel |
317
322
  | `Esc` | Close customizer or clear filter/kill input |
318
323
 
319
324
  ### UI Customizer
@@ -321,7 +326,7 @@ Run with `pyre live`.
321
326
  Press `c` to open the customizer overlay. From there:
322
327
 
323
328
  - Cycle through six built-in themes (Default, Dracula, Cyberpunk, Monochrome, Nord, Gruvbox) — `↑`/`↓` or `j`/`k` to navigate, `Enter`/`Space` to select
324
- - Toggle individual panels (CPU, Memory, GPU, Power, Battery, Thermal, Network, Packets, Tasks, Disk, Processes) on or off
329
+ - Toggle individual panels (CPU, Memory, GPU, Power, Battery, Thermal, Network, Packets, Tasks, Disk, Processes, Containers, Blender) on or off
325
330
  - Switch graph mode between sparkline and bar charts
326
331
  - Toggle process tree view
327
332
  - Toggle temperature unit between Celsius and Fahrenheit
@@ -440,6 +445,14 @@ timestamp,cpu_usage,mem_usage_percent,temp_c,net_rx_bytes,net_tx_bytes,net_rx_pa
440
445
  - **Node.js 18+**
441
446
  - **Optional:** `sudo` for `powermetrics` detailed temperatures in `--detailed` mode
442
447
 
448
+ To enable password‑less access for `powermetrics` (required for accurate sensor readings), run:
449
+
450
+ ```bash
451
+ echo "$(whoami) ALL=(root) NOPASSWD: /usr/bin/powermetrics" | sudo tee /etc/sudoers.d/pyre-powermetrics
452
+ ```
453
+
454
+ This adds a single sudoers entry granting `pyre` the ability to execute `powermetrics` without a password prompt. Remove it later with `sudo rm /etc/sudoers.d/pyre-powermetrics`.
455
+
443
456
  ## Project Structure
444
457
 
445
458
  ```
@@ -0,0 +1,7 @@
1
+ import {
2
+ collectBlenderRenders
3
+ } from "./chunk-25I533OF.js";
4
+ import "./chunk-FH5WDJBF.js";
5
+ export {
6
+ collectBlenderRenders
7
+ };
@@ -1,10 +1,11 @@
1
1
  import {
2
2
  formatBytes
3
- } from "./chunk-MOAEGMFP.js";
4
- import "./chunk-SG26Y3WF.js";
3
+ } from "./chunk-3TE7MJLS.js";
4
+ import "./chunk-IM3IQMMT.js";
5
5
  import {
6
6
  collectAll
7
- } from "./chunk-PV4MXTAJ.js";
7
+ } from "./chunk-N4WJVF5M.js";
8
+ import "./chunk-25I533OF.js";
8
9
  import "./chunk-FH5WDJBF.js";
9
10
 
10
11
  // src/checkCmd.ts
@@ -0,0 +1,133 @@
1
+ import {
2
+ run
3
+ } from "./chunk-FH5WDJBF.js";
4
+
5
+ // src/monitors/blender.ts
6
+ var RENDER_LOG_DIR = "/tmp/blender_render_logs";
7
+ function blendFileFromArgs(args) {
8
+ const nonFlag = args.filter((a) => !a.startsWith("-"));
9
+ for (const a of nonFlag) {
10
+ if (a.endsWith(".blend") || a.includes(".blend")) {
11
+ return a.split("/").pop() || a;
12
+ }
13
+ }
14
+ return "unknown.blend";
15
+ }
16
+ function engineFromArgs(args) {
17
+ const idx = args.indexOf("-E");
18
+ if (idx !== -1 && args[idx + 1]) return args[idx + 1];
19
+ const flag = args.find((a) => a.startsWith("-E="));
20
+ if (flag) return flag.slice(3);
21
+ return "unknown";
22
+ }
23
+ function outputPathFromArgs(args) {
24
+ const oIdx = args.indexOf("-o");
25
+ if (oIdx !== -1 && args[oIdx + 1]) {
26
+ let p = args[oIdx + 1];
27
+ if (p.endsWith("/")) p = p.slice(0, -1);
28
+ return p.split("/").slice(0, -1).join("/") || "/tmp";
29
+ }
30
+ const fFlag = args.find((a) => a.startsWith("-o="));
31
+ if (fFlag) {
32
+ const p = fFlag.slice(3);
33
+ return p.endsWith("/") ? p.slice(0, -1).split("/").slice(0, -1).join("/") || "/tmp" : p.split("/").slice(0, -1).join("/") || "/tmp";
34
+ }
35
+ return "/tmp";
36
+ }
37
+ function frameRangeFromArgs(args) {
38
+ const fIdx = args.indexOf("-f");
39
+ if (fIdx !== -1 && args[fIdx + 1]) {
40
+ const raw = args[fIdx + 1];
41
+ if (raw.includes("..")) {
42
+ const [s, e] = raw.split("..").map(Number);
43
+ if (!isNaN(s) && !isNaN(e)) return { start: s, end: e };
44
+ }
45
+ const n = parseInt(raw, 10);
46
+ if (!isNaN(n)) return { start: n, end: n };
47
+ }
48
+ const sFlag = args.find((a) => a.startsWith("-s="));
49
+ const eFlag = args.find((a) => a.startsWith("-e="));
50
+ const start = sFlag ? parseInt(sFlag.slice(3), 10) : 1;
51
+ const end = eFlag ? parseInt(eFlag.slice(3), 10) : 250;
52
+ return { start: isNaN(start) ? 1 : start, end: isNaN(end) ? 250 : end };
53
+ }
54
+ async function parseRenderLog(blendName) {
55
+ const baseName = blendName.replace(/\.blend$/i, "");
56
+ try {
57
+ const entries = (await run(`ls -t "${RENDER_LOG_DIR}" 2>/dev/null | grep "^${baseName}_" | head -5`, "")).trim().split("\n").filter(Boolean);
58
+ for (const entry of entries) {
59
+ const logPath = `${RENDER_LOG_DIR}/${entry}`;
60
+ try {
61
+ const content = await run(`cat "${logPath}" 2>/dev/null`, "", 500);
62
+ if (!content) continue;
63
+ const lines = content.split("\n");
64
+ const rendered = lines.filter((l) => /Fra:(\d+)/i.test(l)).length;
65
+ const totalMatch = content.match(/Fra:\s*\d+\s*\/\s*(\d+)/);
66
+ const totalFrames = totalMatch ? parseInt(totalMatch[1], 10) : 0;
67
+ const startMatch = content.match(/^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})/m);
68
+ const startTime = startMatch ? new Date(startMatch[1]).getTime() : Date.now() - 6e4;
69
+ const sampleMatch = content.match(/Sample\s*(\d+)/i);
70
+ if (rendered > 0) {
71
+ return { framesRendered: rendered, totalFrames: totalFrames || rendered, startTime, samples: sampleMatch ? parseInt(sampleMatch[1], 10) : void 0 };
72
+ }
73
+ } catch {
74
+ }
75
+ }
76
+ } catch {
77
+ }
78
+ return null;
79
+ }
80
+ async function collectBlenderRenders() {
81
+ const renders = [];
82
+ try {
83
+ const psRaw = await run("ps -eo pid,pcpu,pmem,state,command 2>/dev/null | grep -iE '[b]lender'", "", 2e3);
84
+ if (!psRaw) return renders;
85
+ const lines = psRaw.split("\n");
86
+ for (const line of lines) {
87
+ const parts = line.trim().match(/\s*(\d+)\s+([\d.]+)\s+([\d.]+)\s+(\S+)\s+(.+)/);
88
+ if (!parts) continue;
89
+ const pid = parseInt(parts[1], 10);
90
+ const cpuPct = parseFloat(parts[2]);
91
+ const command = parts[5];
92
+ const args = command.split(/\s+/);
93
+ const isRender = /-b|--background|-f\s/.test(command) || /render/i.test(command);
94
+ if (!isRender && !command.includes("blender")) continue;
95
+ const blendFile = blendFileFromArgs(args);
96
+ const engine = engineFromArgs(args);
97
+ const outputPath = outputPathFromArgs(args);
98
+ const { start: frameStart, end: frameEnd } = frameRangeFromArgs(args);
99
+ const totalFrames = Math.max(1, frameEnd - frameStart + 1);
100
+ const logData = await parseRenderLog(blendFile);
101
+ const startTime = logData?.startTime ?? Date.now() - 6e4;
102
+ const elapsedSec = Math.max(0, Math.round((Date.now() - startTime) / 1e3));
103
+ let currentFrame = logData?.framesRendered ?? 0;
104
+ if (currentFrame === 0 && cpuPct > 0) currentFrame = 1;
105
+ let status = "unknown";
106
+ if (cpuPct > 0 && currentFrame > 0 && currentFrame < totalFrames) status = "rendering";
107
+ else if (cpuPct > 0 && currentFrame >= totalFrames) status = "finalizing";
108
+ else if (currentFrame === 0 && cpuPct > 0) status = "loading";
109
+ else if (cpuPct === 0) status = "idle";
110
+ const completionPercent = totalFrames > 0 ? Math.min(100, Math.round(currentFrame / totalFrames * 100)) : 0;
111
+ renders.push({
112
+ pid,
113
+ blendFile,
114
+ renderEngine: engine,
115
+ frameStart,
116
+ frameEnd,
117
+ currentFrame,
118
+ totalFrames,
119
+ elapsedSec,
120
+ completionPercent,
121
+ status,
122
+ outputPath,
123
+ sampleCount: logData?.samples
124
+ });
125
+ }
126
+ } catch {
127
+ }
128
+ return renders;
129
+ }
130
+
131
+ export {
132
+ collectBlenderRenders
133
+ };
@@ -106,7 +106,8 @@ var BUILTIN_THEMES = {
106
106
  network: chalk.hex("#60a5fa"),
107
107
  disk: chalk.hex("#f472b6"),
108
108
  graphs: chalk.hex("#22d3ee"),
109
- process: chalk.hex("#e5e7eb")
109
+ process: chalk.hex("#e5e7eb"),
110
+ blender: chalk.hex("#f97316")
110
111
  },
111
112
  dracula: {
112
113
  border: chalk.hex("#6272a4"),
@@ -119,7 +120,8 @@ var BUILTIN_THEMES = {
119
120
  network: chalk.hex("#ff79c6"),
120
121
  disk: chalk.hex("#ff5555"),
121
122
  graphs: chalk.hex("#8be9fd"),
122
- process: chalk.hex("#f8f8f2")
123
+ process: chalk.hex("#f8f8f2"),
124
+ blender: chalk.hex("#ffb86c")
123
125
  },
124
126
  cyberpunk: {
125
127
  border: chalk.hex("#ff0055"),
@@ -132,7 +134,8 @@ var BUILTIN_THEMES = {
132
134
  network: chalk.hex("#00ffff"),
133
135
  disk: chalk.hex("#ff007f"),
134
136
  graphs: chalk.hex("#00ffcc"),
135
- process: chalk.hex("#ffffff")
137
+ process: chalk.hex("#ffffff"),
138
+ blender: chalk.hex("#ff6600")
136
139
  },
137
140
  monochrome: {
138
141
  border: chalk.gray,
@@ -145,7 +148,8 @@ var BUILTIN_THEMES = {
145
148
  network: chalk.white.bold,
146
149
  disk: chalk.white.bold,
147
150
  graphs: chalk.white.bold,
148
- process: chalk.white.bold
151
+ process: chalk.white.bold,
152
+ blender: chalk.white.bold
149
153
  },
150
154
  nord: {
151
155
  border: chalk.hex("#4c566a"),
@@ -158,7 +162,8 @@ var BUILTIN_THEMES = {
158
162
  network: chalk.hex("#81a1c1"),
159
163
  disk: chalk.hex("#b48ead"),
160
164
  graphs: chalk.hex("#88c0d0"),
161
- process: chalk.hex("#eceff4")
165
+ process: chalk.hex("#eceff4"),
166
+ blender: chalk.hex("#d08770")
162
167
  },
163
168
  gruvbox: {
164
169
  border: chalk.hex("#928374"),
@@ -171,7 +176,8 @@ var BUILTIN_THEMES = {
171
176
  network: chalk.hex("#83a598"),
172
177
  disk: chalk.hex("#d3869b"),
173
178
  graphs: chalk.hex("#fb4934"),
174
- process: chalk.hex("#ebdbb2")
179
+ process: chalk.hex("#ebdbb2"),
180
+ blender: chalk.hex("#fe8019")
175
181
  }
176
182
  };
177
183
  function loadCustomThemes() {
@@ -198,7 +204,8 @@ function loadCustomThemes() {
198
204
  network: buildColor(data.network),
199
205
  disk: buildColor(data.disk),
200
206
  graphs: buildColor(data.graphs),
201
- process: buildColor(data.process)
207
+ process: buildColor(data.process),
208
+ blender: buildColor(data.blender)
202
209
  };
203
210
  } catch {
204
211
  }
@@ -458,11 +465,83 @@ function thermalCard(data, contentWidth) {
458
465
  }
459
466
  function networkCard(data, contentWidth) {
460
467
  const valW = Math.max(6, contentWidth - 8);
461
- return [
462
- statRow("Iface", truncatePlain(`${data.network.interface} (${data.network.ip})`, valW)),
463
- statRow("RX", `${formatBytes(data.network.rxBytes)} / ${data.network.rxPackets} pkt`),
464
- statRow("TX", `${formatBytes(data.network.txBytes)} / ${data.network.txPackets} pkt`)
465
- ];
468
+ const lines = [];
469
+ lines.push(statRow("Iface", truncatePlain(`${data.network.interface} (${data.network.ip})`, valW)));
470
+ const rxTotal = formatBytes(data.network.rxBytes);
471
+ const txTotal = formatBytes(data.network.txBytes);
472
+ const rxRateStr = formatBytes(data.network.rxRate);
473
+ const txRateStr = formatBytes(data.network.txRate);
474
+ lines.push(statRow("RX", `${rxTotal} \xB7 ${rxRateStr}/s`));
475
+ lines.push(statRow("TX", `${txTotal} \xB7 ${txRateStr}/s`));
476
+ const totalBytes = data.network.rxBytes + data.network.txBytes;
477
+ if (totalBytes > 0) {
478
+ const rxPct = Math.round(data.network.rxBytes / totalBytes * 100);
479
+ const barW = Math.max(4, contentWidth - 20);
480
+ const rxBar = gaugeBar(rxPct, barW);
481
+ lines.push(statRow("RX/TX", `${rxBar} ${rxPct}%`));
482
+ }
483
+ if (data.network.protocols) {
484
+ const p = data.network.protocols;
485
+ const totalConns = (p.tcp.connections || 0) + (p.udp.connections || 0);
486
+ if (totalConns > 0) {
487
+ const tcpPct = Math.round(p.tcp.connections / totalConns * 100);
488
+ lines.push(statRow("TCP", `${p.tcp.connections} conns \xB7 ${gaugeBar(tcpPct, Math.max(4, contentWidth - 20))} ${tcpPct}%`));
489
+ lines.push(statRow("UDP", `${p.udp.connections} conns`));
490
+ }
491
+ }
492
+ if (data.network.connectionStates) {
493
+ const cs = data.network.connectionStates;
494
+ const total = cs.established + cs.listening + cs.timeWait + cs.closeWait + cs.other;
495
+ if (total > 0) {
496
+ const estPct = Math.round(cs.established / total * 100);
497
+ lines.push(statRow("Active", `${cs.established} ESTABLISHED \xB7 ${estPct}%`));
498
+ lines.push(statRow("Listen", `${cs.listening} LISTEN`));
499
+ if (cs.timeWait > 0) lines.push(statRow("TimeWait", `${cs.timeWait}`));
500
+ if (cs.closeWait > 0) lines.push(statRow("CloseWait", `${cs.closeWait}`));
501
+ }
502
+ }
503
+ if (data.network.listeningPorts && data.network.listeningPorts.length > 0) {
504
+ const ports = data.network.listeningPorts.slice(0, 8).join(", ");
505
+ lines.push(statRow("Ports", truncatePlain(ports, valW)));
506
+ }
507
+ if (data.network.topRemoteHosts && data.network.topRemoteHosts.length > 0) {
508
+ for (const host of data.network.topRemoteHosts.slice(0, 3)) {
509
+ lines.push(statRow("Host", truncatePlain(`${host.host} (${host.connectionCount})`, valW)));
510
+ }
511
+ }
512
+ return lines;
513
+ }
514
+ function networkDetailCard(data, width, theme) {
515
+ const contentWidth = width - 4;
516
+ const lines = networkCard(data, contentWidth);
517
+ if (data.packets && data.packets.allProcesses && data.packets.allProcesses.length > 0) {
518
+ lines.push("");
519
+ lines.push(chalk2.bold("PROCESSES & BANDWIDTH USAGE:"));
520
+ lines.push(chalk2.dim("\u2500".repeat(contentWidth)));
521
+ const pWidth = 8;
522
+ const rxWidth = 12;
523
+ const txWidth = 12;
524
+ const totalWidth = 12;
525
+ const cmdWidth = Math.max(15, contentWidth - (pWidth + rxWidth + txWidth + totalWidth + 4 * 2));
526
+ lines.push(
527
+ chalk2.bold("PID".padEnd(pWidth)) + " " + chalk2.bold("COMMAND".padEnd(cmdWidth)) + " " + chalk2.bold("RX".padStart(rxWidth)) + " " + chalk2.bold("TX".padStart(txWidth)) + " " + chalk2.bold("TOTAL".padStart(totalWidth))
528
+ );
529
+ const sorted = [...data.packets.allProcesses].sort((a, b) => b.rxBytes + b.txBytes - (a.rxBytes + a.txBytes));
530
+ for (const proc of sorted) {
531
+ if (proc.rxBytes + proc.txBytes === 0) continue;
532
+ const rxStr = formatBytes(proc.rxBytes).padStart(rxWidth);
533
+ const txStr = formatBytes(proc.txBytes).padStart(txWidth);
534
+ const totalStr = formatBytes(proc.rxBytes + proc.txBytes).padStart(totalWidth);
535
+ const cmdStr = truncatePlain(proc.command, cmdWidth).padEnd(cmdWidth);
536
+ lines.push(
537
+ chalk2.cyan(proc.pid.toString().padEnd(pWidth)) + " " + cmdStr + " " + chalk2.yellow(rxStr) + " " + chalk2.yellow(txStr) + " " + chalk2.bold(totalStr)
538
+ );
539
+ }
540
+ } else {
541
+ lines.push("");
542
+ lines.push(chalk2.dim("No active network process data available."));
543
+ }
544
+ return lines;
466
545
  }
467
546
  function tasksCard(data, contentWidth) {
468
547
  if (!data.tasks || !data.tasks.length) return null;
@@ -488,13 +567,14 @@ var TAB_DEFS = [
488
567
  { id: "power", label: "Power", key: "4" },
489
568
  { id: "battery", label: "Battery", key: "5" },
490
569
  { id: "thermal", label: "Thermal", key: "6" },
491
- { id: "network", label: "Network", key: "7" },
492
- { id: "packets", label: "Packets", key: "8" },
493
- { id: "tasks", label: "Tasks", key: "9" },
494
- { id: "disk", label: "Disk", key: "0" },
495
- { id: "process", label: "Process", key: "P" },
570
+ { id: "packets", label: "Conns", key: "7" },
571
+ { id: "tasks", label: "Tasks", key: "8" },
572
+ { id: "disk", label: "Disk", key: "9" },
573
+ { id: "process", label: "Process", key: "0" },
496
574
  { id: "p2p", label: "P2P", key: "R" },
497
- { id: "anomalies", label: "Anomalies", key: "A" }
575
+ { id: "anomalies", label: "Anomalies", key: "A" },
576
+ { id: "containers", label: "Containers", key: "C" },
577
+ { id: "blender", label: "Blender", key: "B" }
498
578
  ];
499
579
  var TAB_BAR_INDENT = 2;
500
580
  var TAB_SEPARATOR_WIDTH = 3;
@@ -645,6 +725,31 @@ function diskTableLines(disks, contentWidth, borderAccent = THEMES.default.borde
645
725
  });
646
726
  return [head, borderAccent("\u2500".repeat(contentWidth)), ...rows];
647
727
  }
728
+ function containersTableLines(containers, contentWidth, borderAccent = THEMES.default.border) {
729
+ const idW = 14, cpuW = 8, memW = 8, memUsgW = 20, netW = 20, blockW = 20, pidsW = 6;
730
+ const nameW = Math.max(10, contentWidth - idW - cpuW - memW - memUsgW - netW - blockW - pidsW - 7);
731
+ const head = chalk2.bold.hex("#f8f8f2").bgHex("#44475a")(" CONTAINER ID ") + chalk2.bold.hex("#f8f8f2").bgHex("#44475a")(" CPU% ") + chalk2.bold.hex("#f8f8f2").bgHex("#44475a")(" MEM% ") + chalk2.bold.hex("#f8f8f2").bgHex("#44475a")(" MEM USAGE ") + chalk2.bold.hex("#f8f8f2").bgHex("#44475a")(" NET I/O ") + chalk2.bold.hex("#f8f8f2").bgHex("#44475a")(" BLOCK I/O ") + chalk2.bold.hex("#f8f8f2").bgHex("#44475a")(" PIDS ") + chalk2.bold.hex("#f8f8f2").bgHex("#44475a")(" NAME".padEnd(nameW + 1));
732
+ const rows = containers.map((c) => {
733
+ const cpuStr = pctColor(c.cpuPercent)(`${c.cpuPercent.toFixed(1)}`.padEnd(cpuW));
734
+ const memStr = pctColor(c.memPercent)(`${c.memPercent.toFixed(1)}`.padEnd(memW));
735
+ return truncatePlain(c.id, idW - 1).padEnd(idW) + cpuStr + memStr + c.memUsage.padEnd(memUsgW) + c.netIO.padEnd(netW) + c.blockIO.padEnd(blockW) + String(c.pids).padEnd(pidsW) + truncatePlain(c.name, nameW);
736
+ });
737
+ return [head, borderAccent("\u2500".repeat(contentWidth)), ...rows];
738
+ }
739
+ function blenderCard(renders, contentWidth, theme) {
740
+ if (!renders.length) return ["No active Blender renders detected"];
741
+ const lines = [];
742
+ for (const r of renders.slice(0, 8)) {
743
+ const statusColor = r.status === "rendering" ? chalk2.green : r.status === "finalizing" ? chalk2.yellow : r.status === "loading" ? chalk2.cyan : chalk2.dim;
744
+ const pctStr = `${r.completionPercent}%`;
745
+ const barW = Math.max(4, contentWidth - 28);
746
+ const bar = gaugeBar(r.completionPercent, barW);
747
+ lines.push(`${theme.blender(`PID ${r.pid}`)} ${truncatePlain(r.blendFile, 18)} ${r.renderEngine}`);
748
+ lines.push(` Fra ${String(r.currentFrame).padStart(4)}/${r.totalFrames} ${bar} ${pctStr}`);
749
+ lines.push(` ${r.elapsedSec}s elapsed \xB7 ${statusColor(r.status)}${r.sampleCount !== void 0 ? ` \xB7 ${r.sampleCount} spp` : ""}`);
750
+ }
751
+ return lines;
752
+ }
648
753
  function detailPanelTitle(panel2, trackedPid) {
649
754
  const map = {
650
755
  cpu: "CPU",
@@ -657,7 +762,9 @@ function detailPanelTitle(panel2, trackedPid) {
657
762
  packets: "Packets",
658
763
  tasks: "Tasks",
659
764
  disk: "Disk",
660
- process: trackedPid !== void 0 && trackedPid !== null ? `Processes (Following PID ${trackedPid})` : "Processes"
765
+ process: trackedPid !== void 0 && trackedPid !== null ? `Processes (Following PID ${trackedPid})` : "Processes",
766
+ containers: "Containers",
767
+ blender: "Blender Renders"
661
768
  };
662
769
  return map[panel2] || panel2;
663
770
  }
@@ -689,24 +796,42 @@ function activeDetailLines(data, activePanel, width, opts = {}) {
689
796
  case "thermal":
690
797
  return thermalCard(data, contentWidth);
691
798
  case "network":
692
- return networkCard(data, contentWidth);
799
+ return networkDetailCard(data, width, theme);
693
800
  case "packets": {
694
- if (!data.packets) return ["No packet data available"];
695
- const lines = [];
696
- lines.push(statRow("Total", `${data.packets.totalPackets} pkt`));
697
- lines.push(statRow("RX", `${data.packets.rxPackets} pkt`));
698
- lines.push(statRow("TX", `${data.packets.txPackets} pkt`));
699
- lines.push(statRow("Conns", `${data.packets.connections} TCP`));
700
- if (data.packets.interfaces && data.packets.interfaces.length) {
701
- lines.push(statRow("Ifaces", `${data.packets.interfaces.length} active`));
702
- for (const iface of data.packets.interfaces.slice(0, 8)) {
703
- lines.push(statRow(` ${iface.iface}`, `RX ${formatBytes(iface.rxBytes)} / TX ${formatBytes(iface.txBytes)}`));
704
- }
801
+ if (!data.packets || !data.packets.allProcesses || !data.packets.allProcesses.length) {
802
+ return ["No advanced network tracking data available."];
705
803
  }
706
- if (data.packets.allProcesses && data.packets.allProcesses.length) {
707
- lines.push(statRow("Net Procs", `${data.packets.allProcesses.length} total`));
708
- for (const proc of data.packets.allProcesses.slice(0, 10)) {
709
- lines.push(statRow(` ${proc.pid}`, `${truncatePlain(proc.command, 20)} TX ${formatBytes(proc.txBytes)}`));
804
+ const lines = [];
805
+ const pWidth = 8;
806
+ const cmdWidth = 15;
807
+ const protoWidth = 5;
808
+ const stateWidth = 11;
809
+ const rxWidth = 10;
810
+ const txWidth = 10;
811
+ const remaining = Math.max(20, width - (pWidth + cmdWidth + protoWidth + stateWidth + rxWidth + txWidth + 7 * 2));
812
+ const localWidth = Math.floor(remaining * 0.4);
813
+ const remoteWidth = remaining - localWidth;
814
+ const header2 = chalk2.bold("PID".padEnd(pWidth)) + " " + chalk2.bold("COMMAND".padEnd(cmdWidth)) + " " + chalk2.bold("PROTO".padEnd(protoWidth)) + " " + chalk2.bold("LOCAL".padEnd(localWidth)) + " " + chalk2.bold("REMOTE".padEnd(remoteWidth)) + " " + chalk2.bold("STATE".padEnd(stateWidth)) + " " + chalk2.bold("RX".padStart(rxWidth)) + " " + chalk2.bold("TX".padStart(txWidth));
815
+ lines.push(header2);
816
+ lines.push(chalk2.dim("\u2500".repeat(width)));
817
+ for (const proc of data.packets.allProcesses) {
818
+ if (!proc.connections || proc.connections.length === 0) continue;
819
+ const summaryCmd = truncatePlain(proc.command, cmdWidth + protoWidth + localWidth + remoteWidth + stateWidth + 8);
820
+ const summaryRx = formatBytes(proc.rxBytes).padStart(rxWidth);
821
+ const summaryTx = formatBytes(proc.txBytes).padStart(txWidth);
822
+ lines.push(
823
+ chalk2.cyan(proc.pid.toString().padEnd(pWidth)) + " " + chalk2.cyan(summaryCmd.padEnd(width - pWidth - rxWidth - txWidth - 6)) + " " + chalk2.yellow(summaryRx) + " " + chalk2.yellow(summaryTx)
824
+ );
825
+ for (const conn of proc.connections) {
826
+ const cProto = chalk2.dim(truncatePlain(conn.protocol, protoWidth).padEnd(protoWidth));
827
+ const cLocal = chalk2.dim(truncatePlain(conn.local, localWidth).padEnd(localWidth));
828
+ const cRemote = truncatePlain(conn.remote, remoteWidth).padEnd(remoteWidth);
829
+ const cStateStr = conn.state === "Established" ? chalk2.green("ESTABLISHED".padEnd(stateWidth)) : chalk2.dim(truncatePlain(conn.state, stateWidth).padEnd(stateWidth));
830
+ const cRx = chalk2.dim(formatBytes(conn.rxBytes).padStart(rxWidth));
831
+ const cTx = chalk2.dim(formatBytes(conn.txBytes).padStart(txWidth));
832
+ lines.push(
833
+ " ".repeat(pWidth) + " " + " ".repeat(cmdWidth) + " " + cProto + " " + cLocal + " " + cRemote + " " + cStateStr + " " + cRx + " " + cTx
834
+ );
710
835
  }
711
836
  }
712
837
  return lines;
@@ -769,6 +894,13 @@ function activeDetailLines(data, activePanel, width, opts = {}) {
769
894
  }
770
895
  return lines;
771
896
  }
897
+ case "containers": {
898
+ return data.containers && data.containers.length ? containersTableLines(data.containers, width - 4, theme.border) : ["No container data available"];
899
+ }
900
+ case "blender": {
901
+ const renders = data.blenderRenders && data.blenderRenders.length ? data.blenderRenders : [];
902
+ return blenderCard(renders, contentWidth, theme);
903
+ }
772
904
  default:
773
905
  return null;
774
906
  }
@@ -992,7 +1124,7 @@ function formatTable(data, opts = {}) {
992
1124
  lines.push(gaugeRow("GPU", data.gpu.utilization, contentWidth));
993
1125
  }
994
1126
  const tempC = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die;
995
- if (tempC !== void 0) {
1127
+ if (tempC != null) {
996
1128
  const tempStr = formatTempUnit(tempC, opts.tempUnit);
997
1129
  lines.push(statRow("Temp", thermalColor(data.thermal.pressureLevel)(tempStr)));
998
1130
  }
@@ -29,6 +29,7 @@ var DEFAULT_CONFIG = {
29
29
  splashAnimation: "classic",
30
30
  webhookUrl: "",
31
31
  alertCmd: "",
32
+ dockerModeConfirmed: false,
32
33
  visiblePanels: {
33
34
  cpu: true,
34
35
  mem: true,
@@ -41,6 +42,8 @@ var DEFAULT_CONFIG = {
41
42
  tasks: true,
42
43
  disk: true,
43
44
  process: true,
45
+ containers: true,
46
+ blender: true,
44
47
  tree: false
45
48
  },
46
49
  panelLayout: ["mem", "disk", "net"]