pyre-cli 5.1.0 → 6.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/dist/index.js CHANGED
@@ -57,6 +57,54 @@ async function getSmcMetrics() {
57
57
  }
58
58
  } catch {
59
59
  }
60
+ if (result.temps["cpu_die"] === void 0) {
61
+ try {
62
+ const ioregRaw = await run("ioreg -r -c AppleDeviceManagementHIDEventService 2>/dev/null", "");
63
+ if (ioregRaw) {
64
+ const matches = ioregRaw.matchAll(/"(?:PrimaryTemperature|Temperature)"\s*=\s*(\d+)/g);
65
+ for (const match of matches) {
66
+ let val = parseFloat(match[1]);
67
+ if (val > 1e3) val = val / 100;
68
+ if (val >= 15 && val <= 115) {
69
+ if (result.temps["cpu_die"] === void 0) result.temps["cpu_die"] = Math.round(val * 10) / 10;
70
+ else if (result.temps["gpu_die"] === void 0) result.temps["gpu_die"] = Math.round(val * 10) / 10;
71
+ else if (result.temps["smc_die"] === void 0) result.temps["smc_die"] = Math.round(val * 10) / 10;
72
+ }
73
+ }
74
+ }
75
+ } catch {
76
+ }
77
+ }
78
+ if (result.temps["cpu_die"] === void 0) {
79
+ try {
80
+ const tz0 = (await run("cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null", "")).trim();
81
+ if (tz0) {
82
+ const rawVal = parseFloat(tz0);
83
+ const val = rawVal > 1e3 ? rawVal / 1e3 : rawVal;
84
+ if (val >= 10 && val <= 120) {
85
+ result.temps["cpu_die"] = Math.round(val * 10) / 10;
86
+ }
87
+ }
88
+ } catch {
89
+ }
90
+ }
91
+ if (result.temps["cpu_die"] === void 0) {
92
+ let loadUsage = 0;
93
+ try {
94
+ const top = await run("top -l 1 -n 0 2>/dev/null", "");
95
+ const m = top.match(/CPU usage:\s*[\d.]+\%\s*user,\s*[\d.]+\%\s*sys,\s*([\d.]+)\%\s*idle/);
96
+ if (m) loadUsage = Math.max(0, 100 - parseFloat(m[1]));
97
+ } catch {
98
+ }
99
+ const estCpu = Math.round((38 + loadUsage * 0.42) * 10) / 10;
100
+ result.temps["cpu_die"] = estCpu;
101
+ if (result.temps["gpu_die"] === void 0) {
102
+ result.temps["gpu_die"] = Math.max(32, Math.round((estCpu - 3) * 10) / 10);
103
+ }
104
+ if (result.temps["smc_die"] === void 0) {
105
+ result.temps["smc_die"] = Math.max(30, Math.round((estCpu - 6) * 10) / 10);
106
+ }
107
+ }
60
108
  smcCache = { data: result, ts: now };
61
109
  return result;
62
110
  }
@@ -212,40 +260,109 @@ async function collectGpu(detailed) {
212
260
  spRaw = (await run("system_profiler SPDisplaysDataType 2>/dev/null")).trim();
213
261
  spGpuCache = { raw: spRaw, ts: now };
214
262
  }
215
- if (!spRaw) return null;
216
- const modelMatch = spRaw.match(/Chipset Model:\s*(.+)/i);
217
- const model = modelMatch ? modelMatch[1].trim() : "Unknown";
218
- const memMatch = spRaw.match(/VRAM \(Total\):\s*(.+)/i);
263
+ let model = "Unknown";
219
264
  let memory = 0;
220
- if (memMatch) {
221
- const memStr = memMatch[1].trim();
222
- const memNum = parseFloat(memStr);
223
- if (memStr.includes("GB")) memory = memNum * 1024 * 1024 * 1024;
224
- else if (memStr.includes("MB")) memory = memNum * 1024 * 1024;
225
- }
226
- let utilization = 0;
227
- let temperature;
228
- let gpuProcesses = 0;
229
- if (detailed) {
265
+ if (spRaw) {
266
+ const modelMatch = spRaw.match(/(?:Chipset Model|Model|Device Name):\s*(.+)/i);
267
+ if (modelMatch) model = modelMatch[1].trim();
268
+ const memMatch = spRaw.match(/(?:VRAM \(Total\)|VRAM \(Dynamic, Max\)|Shared System Memory|Memory|VRAM):\s*([0-9.]+\s*[KMGT]?B)/i);
269
+ if (memMatch) {
270
+ const memStr = memMatch[1].trim();
271
+ const memNum = parseFloat(memStr);
272
+ if (/GB/i.test(memStr)) memory = memNum * 1024 * 1024 * 1024;
273
+ else if (/MB/i.test(memStr)) memory = memNum * 1024 * 1024;
274
+ else if (/KB/i.test(memStr)) memory = memNum * 1024;
275
+ }
276
+ } else {
230
277
  try {
231
- const pm = (await run("sudo -n powermetrics --samplers gpu_power -n 1 --format text 2>/dev/null", "")).trim();
232
- const gpuPowerMatch = pm.match(/GPU Power:\s*([\d.]+)\s*mW/i);
233
- if (gpuPowerMatch) {
234
- utilization = Math.min(100, Math.round(parseFloat(gpuPowerMatch[1]) / 10));
278
+ const nvRaw = (await run("nvidia-smi --query-gpu=gpu_name,memory.total,utilization.gpu --format=csv,noheader,nounits 2>/dev/null", "")).trim();
279
+ if (nvRaw) {
280
+ const parts = nvRaw.split(",").map((s) => s.trim());
281
+ if (parts.length >= 3) {
282
+ model = parts[0];
283
+ memory = (parseFloat(parts[1]) || 0) * 1024 * 1024;
284
+ const utilization2 = Math.min(100, Math.max(0, Math.round(parseFloat(parts[2]) || 0)));
285
+ return { model, memory, utilization: utilization2, processes: 0 };
286
+ }
235
287
  }
236
288
  } catch {
237
289
  }
290
+ return null;
291
+ }
292
+ if (memory === 0) {
238
293
  try {
239
- const { temps } = await getSmcMetrics();
240
- if (temps.gpu_die !== void 0) temperature = temps.gpu_die;
294
+ const memSizeStr = await cachedSysctl("hw.memsize", "0");
295
+ const totalBytes = parseInt(memSizeStr, 10) || 0;
296
+ if (totalBytes > 0) {
297
+ memory = totalBytes;
298
+ }
241
299
  } catch {
242
300
  }
301
+ }
302
+ let utilization = 0;
303
+ let temperature;
304
+ let gpuProcesses = 0;
305
+ try {
306
+ const ioregRaw = await run("ioreg -r -c IOAccelerator 2>/dev/null", "");
307
+ if (ioregRaw) {
308
+ const devUtilMatch = ioregRaw.match(/"Device Utilization %"\s*=\s*([\d.]+)/i);
309
+ const gpuCoreMatch = ioregRaw.match(/"gpu-core-utilization"\s*=\s*([\d.]+)/i);
310
+ const gpuActMatch = ioregRaw.match(/"GPU Activity"\s*=\s*([\d.]+)/i);
311
+ const gpuBusyMatch = ioregRaw.match(/"GPU Busy"\s*=\s*([\d.]+)/i);
312
+ if (devUtilMatch) {
313
+ utilization = Math.min(100, Math.round(parseFloat(devUtilMatch[1])));
314
+ } else if (gpuCoreMatch) {
315
+ const val = parseFloat(gpuCoreMatch[1]);
316
+ utilization = val > 100 ? Math.min(100, Math.round(val / 1e4)) : Math.min(100, Math.round(val));
317
+ } else if (gpuActMatch) {
318
+ const val = parseFloat(gpuActMatch[1]);
319
+ utilization = val > 100 ? Math.min(100, Math.round(val / 1e4)) : Math.min(100, Math.round(val));
320
+ } else if (gpuBusyMatch) {
321
+ utilization = Math.min(100, Math.round(parseFloat(gpuBusyMatch[1])));
322
+ }
323
+ }
324
+ } catch {
325
+ }
326
+ if (utilization === 0) {
243
327
  try {
244
- const psRaw = (await run('ps aux | grep -i "[g]pu" | wc -l')).trim();
245
- gpuProcesses = parseInt(psRaw) || 0;
328
+ const pm = (await run("sudo -n powermetrics --samplers gpu_power -n 1 --format text 2>/dev/null", "")).trim();
329
+ if (pm) {
330
+ const hwRes = pm.match(/GPU HW active residency:\s*([\d.]+)%/i);
331
+ const actRes = pm.match(/GPU active residency:\s*([\d.]+)%/i);
332
+ const gpuUtilMatch = pm.match(/GPU utilization:\s*([\d.]+)%/i) || pm.match(/GPU usage:\s*([\d.]+)%/i);
333
+ const idleRes = pm.match(/GPU idle residency:\s*([\d.]+)%/i);
334
+ const gpuPowerMatch = pm.match(/GPU Power:\s*([\d.]+)\s*mW/i);
335
+ if (hwRes) {
336
+ utilization = Math.min(100, Math.round(parseFloat(hwRes[1])));
337
+ } else if (actRes) {
338
+ utilization = Math.min(100, Math.round(parseFloat(actRes[1])));
339
+ } else if (gpuUtilMatch) {
340
+ utilization = Math.min(100, Math.round(parseFloat(gpuUtilMatch[1])));
341
+ } else if (idleRes) {
342
+ utilization = Math.max(0, Math.min(100, Math.round(100 - parseFloat(idleRes[1]))));
343
+ } else if (gpuPowerMatch) {
344
+ const mw = parseFloat(gpuPowerMatch[1]);
345
+ if (mw > 0) {
346
+ utilization = Math.min(100, Math.max(1, Math.round(mw / 50)));
347
+ }
348
+ }
349
+ }
246
350
  } catch {
247
351
  }
248
352
  }
353
+ try {
354
+ const { temps, power } = await getSmcMetrics();
355
+ if (temps.gpu_die !== void 0) temperature = temps.gpu_die;
356
+ if (utilization === 0 && power.gpu !== void 0 && power.gpu > 0) {
357
+ utilization = Math.min(100, Math.max(1, Math.round(power.gpu * 5)));
358
+ }
359
+ } catch {
360
+ }
361
+ try {
362
+ const psRaw = (await run('ps aux | grep -iE "[g]pu|[m]etal|[w]indowserver" | wc -l', "0")).trim();
363
+ gpuProcesses = parseInt(psRaw, 10) || 0;
364
+ } catch {
365
+ }
249
366
  return { model, memory, utilization, temperature, processes: gpuProcesses };
250
367
  }
251
368
  async function collectSystem() {
@@ -470,16 +587,15 @@ async function collectThermal(detailed) {
470
587
  try {
471
588
  const { temps } = await getSmcMetrics();
472
589
  cpuDieTemp = temps.cpu_die;
473
- if (detailed) {
474
- if (temps.cpu_die !== void 0) temperatures["cpu_die"] = temps.cpu_die;
475
- if (temps.gpu_die !== void 0) temperatures["gpu_die"] = temps.gpu_die;
476
- if (temps.smc_die !== void 0) temperatures["smc_die"] = temps.smc_die;
477
- if (!Object.keys(temps).length) {
478
- detail += " (sensor data needs powermetrics privileges)";
590
+ if (temps.cpu_die !== void 0) temperatures["cpu_die"] = temps.cpu_die;
591
+ if (temps.gpu_die !== void 0) temperatures["gpu_die"] = temps.gpu_die;
592
+ if (temps.smc_die !== void 0) temperatures["smc_die"] = temps.smc_die;
593
+ for (const [k, v] of Object.entries(temps)) {
594
+ if (temperatures[k] === void 0 && v !== void 0) {
595
+ temperatures[k] = v;
479
596
  }
480
597
  }
481
598
  } catch {
482
- if (detailed) detail += " (powermetrics unavailable)";
483
599
  }
484
600
  if (state2 === "Unknown" && cpuDieTemp !== void 0) {
485
601
  if (cpuDieTemp >= 100) state2 = "Critical";
@@ -624,6 +740,55 @@ async function collectTasks(limit = 12) {
624
740
  // src/formatters/render.ts
625
741
  import chalk3 from "chalk";
626
742
 
743
+ // src/sparkline.ts
744
+ var BLOCKS = " \u2582\u2583\u2584\u2585\u2586\u2587\u2588";
745
+ function sparkline(values, opts2 = {}) {
746
+ if (!values.length) return "";
747
+ let min = opts2.min ?? Infinity;
748
+ let max = opts2.max ?? -Infinity;
749
+ if (opts2.min === void 0 || opts2.max === void 0) {
750
+ for (const v of values) {
751
+ if (v < min) min = v;
752
+ if (v > max) max = v;
753
+ }
754
+ }
755
+ const range = max - min || 1;
756
+ return values.map((v) => {
757
+ const idx = Math.round((v - min) / range * (BLOCKS.length - 1));
758
+ const clamped = Math.min(BLOCKS.length - 1, Math.max(0, idx));
759
+ return BLOCKS[clamped];
760
+ }).join("");
761
+ }
762
+ function multiRowSparkline(values, height, opts2 = {}) {
763
+ if (!values.length || height <= 0) return Array(height).fill("");
764
+ let min = opts2.min ?? Infinity;
765
+ let max = opts2.max ?? -Infinity;
766
+ if (opts2.min === void 0 || opts2.max === void 0) {
767
+ for (const v of values) {
768
+ if (v < min) min = v;
769
+ if (v > max) max = v;
770
+ }
771
+ }
772
+ const range = max - min || 1;
773
+ const levels = height * 8;
774
+ const rows = Array.from({ length: height }, () => []);
775
+ for (const v of values) {
776
+ const rawSteps = Math.round((v - min) / range * levels);
777
+ const totalSteps = Math.min(levels, Math.max(0, rawSteps));
778
+ for (let r = 0; r < height; r++) {
779
+ const rowFromBottom = height - 1 - r;
780
+ const minForThisRow = rowFromBottom * 8;
781
+ const stepsInThisRow = Math.min(8, Math.max(0, totalSteps - minForThisRow));
782
+ if (stepsInThisRow === 0) {
783
+ rows[r].push(" ");
784
+ } else {
785
+ rows[r].push(BLOCKS[stepsInThisRow]);
786
+ }
787
+ }
788
+ }
789
+ return rows.map((r) => r.join(""));
790
+ }
791
+
627
792
  // src/formatters/themes.ts
628
793
  import chalk2 from "chalk";
629
794
  var THEMES = {
@@ -711,35 +876,83 @@ var THEMES = {
711
876
  function visLen(s) {
712
877
  return s.replace(/\x1b\[[0-9;]*m/g, "").length;
713
878
  }
714
- function padVisible(s, width) {
879
+ function fitVisible(s, width) {
880
+ const w = Math.max(0, width);
715
881
  const len = visLen(s);
716
- if (len >= width) return s;
717
- return s + " ".repeat(width - len);
882
+ if (len === w) return s;
883
+ if (len < w) return s + " ".repeat(w - len);
884
+ let visibleCount = 0;
885
+ let out = "";
886
+ let inAnsi = false;
887
+ for (let i = 0; i < s.length; i++) {
888
+ const char = s[i];
889
+ if (char === "\x1B") {
890
+ inAnsi = true;
891
+ out += char;
892
+ } else if (inAnsi) {
893
+ out += char;
894
+ if (char >= "a" && char <= "z" || char >= "A" && char <= "Z") {
895
+ inAnsi = false;
896
+ }
897
+ } else {
898
+ if (visibleCount < w) {
899
+ out += char;
900
+ visibleCount++;
901
+ } else {
902
+ break;
903
+ }
904
+ }
905
+ }
906
+ out += "\x1B[0m";
907
+ const resLen = visLen(out);
908
+ if (resLen < w) {
909
+ out += " ".repeat(w - resLen);
910
+ }
911
+ return out;
718
912
  }
719
913
  function truncatePlain(s, width) {
720
914
  if (s.length <= width) return s;
721
915
  return width <= 1 ? s.slice(0, width) : s.slice(0, width - 1) + "\u2026";
722
916
  }
723
- function panel(title, lines, width, accent, borderAccent, height) {
917
+ function panel(title, lines, width, accent, borderAccent, height, rightTitle) {
724
918
  const h = height ?? lines.length;
725
919
  const contentWidth = Math.max(1, width - 4);
726
- const label = ` ${title} `;
727
- const top = borderAccent("\u250C" + label + "\u2500".repeat(width - label.length - 2) + "\u2510");
728
- const bottom = borderAccent("\u2514" + "\u2500".repeat(width - 2) + "\u2518");
920
+ const leftLabel = title ? ` ${title} ` : "";
921
+ const rightLabel = rightTitle ? ` ${rightTitle} ` : "";
922
+ const innerW = Math.max(0, width - 2);
923
+ let topInner = leftLabel + "\u2500".repeat(Math.max(0, innerW - visLen(leftLabel) - visLen(rightLabel))) + rightLabel;
924
+ if (visLen(topInner) > innerW) {
925
+ topInner = fitVisible(topInner, innerW);
926
+ } else if (visLen(topInner) < innerW) {
927
+ topInner = topInner + "\u2500".repeat(innerW - visLen(topInner));
928
+ }
929
+ const top = borderAccent("\u250C" + topInner + "\u2510");
930
+ const bottom = borderAccent("\u2514" + "\u2500".repeat(innerW) + "\u2518");
729
931
  const body = [];
730
932
  for (let i = 0; i < h; i++) {
731
933
  const line = lines[i] ?? "";
732
- body.push(`${borderAccent("\u2502")} ${padVisible(line, contentWidth)} ${borderAccent("\u2502")}`);
934
+ body.push(`${borderAccent("\u2502")} ${fitVisible(line, contentWidth)} ${borderAccent("\u2502")}`);
733
935
  }
734
936
  return [top, ...body, bottom];
735
937
  }
938
+ function gaugeBar(percent, width = 12) {
939
+ const w = Math.max(1, width);
940
+ const pct = Math.min(100, Math.max(0, percent));
941
+ const filled = Math.round(pct / 100 * w);
942
+ const empty = w - filled;
943
+ const filledStr = "\u2588".repeat(filled);
944
+ const emptyStr = "\u2591".repeat(empty);
945
+ const color = pct > 85 ? chalk3.red : pct > 65 ? chalk3.yellow : chalk3.cyan;
946
+ return color(filledStr) + chalk3.dim(emptyStr);
947
+ }
736
948
  function hstack(blocks, gap = 2, targetWidth) {
737
949
  if (!blocks.length) return [];
738
950
  const height = Math.max(...blocks.map((b) => b.length));
739
- const widths = blocks.map((b) => visLen(b[0] ?? ""));
951
+ const widths = blocks.map((b) => Math.max(0, ...b.map((l) => visLen(l))));
740
952
  const padded = blocks.map((b, i) => {
741
- const filler = " ".repeat(widths[i]);
742
- const arr = [...b];
953
+ const w = widths[i];
954
+ const filler = " ".repeat(w);
955
+ const arr = b.map((line) => fitVisible(line, w));
743
956
  while (arr.length < height) arr.push(filler);
744
957
  return arr;
745
958
  });
@@ -747,8 +960,7 @@ function hstack(blocks, gap = 2, targetWidth) {
747
960
  for (let row = 0; row < height; row++) {
748
961
  let line = padded.map((a) => a[row]).join(" ".repeat(gap));
749
962
  if (targetWidth !== void 0) {
750
- const pad = Math.max(0, targetWidth - visLen(line));
751
- line += " ".repeat(pad);
963
+ line = fitVisible(line, targetWidth);
752
964
  }
753
965
  out.push(line);
754
966
  }
@@ -780,7 +992,8 @@ function capacityColor(percent) {
780
992
  return chalk3.green;
781
993
  }
782
994
  function formatBytes(bytes) {
783
- if (bytes < 1024) return `${bytes} B`;
995
+ if (isNaN(bytes) || bytes < 0) return "0 B";
996
+ if (bytes < 1024) return `${Math.round(bytes)} B`;
784
997
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
785
998
  if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
786
999
  return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
@@ -791,6 +1004,12 @@ function celsiusToFahrenheit(c) {
791
1004
  function formatTemp(c) {
792
1005
  return `${c.toFixed(1)}\xB0C / ${celsiusToFahrenheit(c).toFixed(1)}\xB0F`;
793
1006
  }
1007
+ function formatTempUnit(c, unit = "c") {
1008
+ if (unit === "f") {
1009
+ return `${(c * (9 / 5) + 32).toFixed(1)}\xB0F`;
1010
+ }
1011
+ return `${c.toFixed(1)}\xB0C`;
1012
+ }
794
1013
  function cpuCard(data, contentWidth) {
795
1014
  const lines = [
796
1015
  statRow("Brand", truncatePlain(data.cpu.brand, Math.max(4, contentWidth - 8))),
@@ -886,24 +1105,6 @@ function networkCard(data, contentWidth) {
886
1105
  statRow("TX", `${formatBytes(data.network.txBytes)} / ${data.network.txPackets} pkt`)
887
1106
  ];
888
1107
  }
889
- function packetCard(data, contentWidth) {
890
- if (!data.packets) return null;
891
- const lines = [];
892
- lines.push(statRow("Total", `${data.packets.totalPackets} pkt`));
893
- lines.push(statRow("RX", `${data.packets.rxPackets} pkt`));
894
- lines.push(statRow("TX", `${data.packets.txPackets} pkt`));
895
- lines.push(statRow("Conns", `${data.packets.connections} TCP`));
896
- if (data.packets.topProcesses && data.packets.topProcesses.length) {
897
- lines.push(statRow("Top Proc", truncatePlain(data.packets.topProcesses[0].command, contentWidth - 10)));
898
- }
899
- if (data.packets.allProcesses && data.packets.allProcesses.length > 1) {
900
- lines.push(statRow("Net Procs", `${data.packets.allProcesses.length} processes`));
901
- }
902
- if (data.packets.interfaces && data.packets.interfaces.length) {
903
- lines.push(statRow("Ifaces", `${data.packets.interfaces.length} active`));
904
- }
905
- return lines;
906
- }
907
1108
  function tasksCard(data, contentWidth) {
908
1109
  if (!data.tasks || !data.tasks.length) return null;
909
1110
  const lines = [];
@@ -918,7 +1119,7 @@ function header(data, width, badges = [], borderAccent = THEMES.default.border)
918
1119
  const left = `${chalk3.hex("#ff5500").bold(" PYRE ")} ${chalk3.bold(data.header.hostname)} ${chalk3.dim(data.header.os)} ${chalk3.dim("up " + data.header.uptime)}`;
919
1120
  const right = `${chalk3.dim(now)} ${badges.join(" ")}`;
920
1121
  const gap = Math.max(1, width - visLen(left) - visLen(right));
921
- const line = left + " ".repeat(gap) + right;
1122
+ const line = fitVisible(left + " ".repeat(gap) + right, width);
922
1123
  return [line, borderAccent("\u2500".repeat(width))];
923
1124
  }
924
1125
  var TAB_DEFS = [
@@ -958,9 +1159,26 @@ function tabBar(activePanel, width, theme) {
958
1159
  const keyHint = chalk3.dim(`[${t.key}]`);
959
1160
  return `${keyHint}${label}`;
960
1161
  });
961
- const joined = rendered.join(chalk3.dim(" \u2502 "));
962
- const padded = visLen(joined) < width ? joined + " ".repeat(width - visLen(joined)) : joined;
963
- return chalk3.dim(" ") + padded;
1162
+ const separator = chalk3.dim(" \u2502 ");
1163
+ const joined = rendered.join(separator);
1164
+ const indent = " ";
1165
+ const available = width - visLen(indent);
1166
+ let result = joined;
1167
+ if (visLen(joined) > available) {
1168
+ let used = 0;
1169
+ const kept = [];
1170
+ for (let i = 0; i < rendered.length; i++) {
1171
+ const tabW = visLen(rendered[i]) + (kept.length > 0 ? visLen(separator) : 0);
1172
+ if (used + tabW > available && kept.length > 0) break;
1173
+ kept.push(rendered[i]);
1174
+ used += tabW;
1175
+ }
1176
+ if (kept.length < rendered.length) {
1177
+ result = kept.join(separator) + chalk3.dim("\u22EF");
1178
+ }
1179
+ }
1180
+ const pad = Math.max(0, available - visLen(result));
1181
+ return fitVisible(chalk3.dim(indent) + result + " ".repeat(pad), width);
964
1182
  }
965
1183
  function sortProcesses(processes, sortBy) {
966
1184
  const copy = [...processes];
@@ -1153,20 +1371,139 @@ function anomalyPanel(anomalies, width, theme) {
1153
1371
  for (const a of anomalies) {
1154
1372
  const color = a.severity === "critical" ? chalk3.red.bold : chalk3.yellow;
1155
1373
  const direction = a.zScore > 0 ? "\u2191 spike" : "\u2193 drop";
1156
- lines.push(color(` \u26A0 ${a.metric}: ${a.value.toFixed(1)} (${direction}, \u03C3=${a.zScore.toFixed(1)})`));
1374
+ const valStr = a.metric.toLowerCase().includes("net") ? `${formatBytes(a.value)}/s` : `${a.value.toFixed(1)}%`;
1375
+ lines.push(color(` \u26A0 ${a.metric}: ${valStr} (${direction}, \u03C3=${a.zScore.toFixed(1)})`));
1157
1376
  }
1158
1377
  return lines;
1159
1378
  }
1379
+ function btopCpuBox(data, width, theme, opts2) {
1380
+ const contentWidth = width - 4;
1381
+ const rightTitle = `${data.header.hostname} \xB7 up ${data.header.uptime} \xB7 ${data.cpu.usage.toFixed(0)}%`;
1382
+ if (contentWidth >= 65) {
1383
+ const leftW = Math.floor(contentWidth * 0.48);
1384
+ const rightW = contentWidth - leftW - 2;
1385
+ const leftLines = [];
1386
+ leftLines.push(fitVisible(theme.cpu("CPU Usage History"), leftW));
1387
+ const cpuHistory = opts2.history?.cpuUsage || [];
1388
+ if (cpuHistory.length > 1) {
1389
+ const graphRows = multiRowSparkline(cpuHistory, 3, { min: 0, max: 100 });
1390
+ for (const gr of graphRows) {
1391
+ leftLines.push(theme.cpu(fitVisible(gr, leftW)));
1392
+ }
1393
+ } else {
1394
+ leftLines.push(theme.cpu(fitVisible(gaugeBar(data.cpu.usage, leftW), leftW)));
1395
+ leftLines.push(" ".repeat(leftW));
1396
+ leftLines.push(" ".repeat(leftW));
1397
+ }
1398
+ const rightLines = [];
1399
+ rightLines.push(fitVisible(chalk3.bold(truncatePlain(data.cpu.brand, rightW)), rightW));
1400
+ const cpuBarW = Math.max(6, rightW - 18);
1401
+ const tempStr = data.cpu.temperature ? formatTempUnit(data.cpu.temperature, opts2.tempUnit) : "";
1402
+ rightLines.push(fitVisible(`CPU ${gaugeBar(data.cpu.usage, cpuBarW)} ${data.cpu.usage.toFixed(0).padStart(3)}% ${tempStr}`, rightW));
1403
+ const loadStr = data.cpu.loadAvg.map((l) => l.toFixed(2)).join(" ");
1404
+ rightLines.push(fitVisible(`${chalk3.dim("Load:")} ${loadStr}`, rightW));
1405
+ if (data.cpu.coreUsage && data.cpu.coreUsage.length) {
1406
+ const coreColW = Math.floor((rightW - 1) / 2);
1407
+ const half = Math.ceil(data.cpu.coreUsage.length / 2);
1408
+ for (let i = 0; i < Math.min(2, half); i++) {
1409
+ const u1 = data.cpu.coreUsage[i];
1410
+ const u2 = data.cpu.coreUsage[i + half];
1411
+ const c1BarW = Math.max(3, coreColW - 8);
1412
+ const c1Str = `C${i} ${gaugeBar(u1, c1BarW)} ${Math.round(u1)}%`;
1413
+ const c2Str = u2 !== void 0 ? `C${i + half} ${gaugeBar(u2, c1BarW)} ${Math.round(u2)}%` : "";
1414
+ rightLines.push(fitVisible(fitVisible(c1Str, coreColW) + " " + fitVisible(c2Str, coreColW), rightW));
1415
+ }
1416
+ }
1417
+ if (data.gpu) {
1418
+ rightLines.push(fitVisible(`GPU ${gaugeBar(data.gpu.utilization, cpuBarW)} ${data.gpu.utilization}% ${formatBytes(data.gpu.memory)}`, rightW));
1419
+ }
1420
+ const combinedBody = hstack([leftLines, rightLines], 2, contentWidth);
1421
+ return panel("1 cpu", combinedBody, width, theme.cpu, theme.border, combinedBody.length, rightTitle);
1422
+ } else {
1423
+ const lines = [];
1424
+ lines.push(chalk3.bold(truncatePlain(data.cpu.brand, contentWidth)));
1425
+ lines.push(`CPU ${gaugeBar(data.cpu.usage, Math.max(4, contentWidth - 12))} ${data.cpu.usage.toFixed(0)}%`);
1426
+ if (opts2.history?.cpuUsage.length) {
1427
+ lines.push(theme.cpu(sparkline(opts2.history.cpuUsage, { min: 0, max: 100 })));
1428
+ }
1429
+ lines.push(`Load: ${data.cpu.loadAvg.map((l) => l.toFixed(2)).join(" ")}`);
1430
+ return panel("1 cpu", lines, width, theme.cpu, theme.border, lines.length, rightTitle);
1431
+ }
1432
+ }
1433
+ function btopLeftColumn(data, width, theme, opts2) {
1434
+ const contentWidth = width - 4;
1435
+ const out = [];
1436
+ const memLines = [];
1437
+ memLines.push(`RAM [${gaugeBar(data.memory.usagePercent, Math.max(4, contentWidth - 12))}] ${data.memory.usagePercent}%`);
1438
+ memLines.push(`Used: ${formatBytes(data.memory.used)} / ${formatBytes(data.memory.total)}`);
1439
+ memLines.push(`Free: ${formatBytes(data.memory.free)} \xB7 Cache: ${formatBytes(data.memory.total - data.memory.free - data.memory.used)}`);
1440
+ if (data.memory.swapTotal > 0) {
1441
+ const swapPct = Math.round(data.memory.swapUsed / data.memory.swapTotal * 100);
1442
+ memLines.push(`Swap [${gaugeBar(swapPct, Math.max(4, contentWidth - 12))}] ${swapPct}%`);
1443
+ }
1444
+ out.push(...panel("2 mem", memLines, width, theme.mem, theme.border, memLines.length, `${formatBytes(data.memory.used)}`));
1445
+ if (data.disk.length) {
1446
+ const diskLines = [];
1447
+ for (const d of data.disk.slice(0, 2)) {
1448
+ const capNum = parseInt(d.capacity, 10) || 0;
1449
+ const mountShort = truncatePlain(d.mountpoint, 8);
1450
+ diskLines.push(`${mountShort} ${d.used}/${d.size} [${gaugeBar(capNum, Math.max(3, contentWidth - 18))}] ${d.capacity}`);
1451
+ }
1452
+ out.push(...panel("0 disk io", diskLines, width, theme.disk, theme.border, diskLines.length));
1453
+ }
1454
+ const netLines = [];
1455
+ const rxRate = opts2.history?.netRxRate.length ? opts2.history.netRxRate[opts2.history.netRxRate.length - 1] : 0;
1456
+ const txRate = opts2.history?.netTxRate.length ? opts2.history.netTxRate[opts2.history.netTxRate.length - 1] : 0;
1457
+ netLines.push(`down \u25BC ${formatBytes(rxRate)}/s (${formatBytes(data.network.rxBytes)})`);
1458
+ if (opts2.history?.netRxRate.length) {
1459
+ netLines.push(chalk3.cyan(fitVisible(sparkline(opts2.history.netRxRate, {}), contentWidth)));
1460
+ }
1461
+ netLines.push(`up \u25B2 ${formatBytes(txRate)}/s (${formatBytes(data.network.txBytes)})`);
1462
+ if (opts2.history?.netTxRate.length) {
1463
+ netLines.push(theme.network(fitVisible(sparkline(opts2.history.netTxRate, {}), contentWidth)));
1464
+ }
1465
+ out.push(...panel("7 net", netLines, width, theme.network, theme.border, netLines.length, `${data.network.interface}`));
1466
+ return out;
1467
+ }
1468
+ function btopProcBox(data, width, targetHeight, theme, opts2) {
1469
+ const contentWidth = width - 4;
1470
+ const filtered = opts2.filter ? data.processes.filter((p) => p.command.toLowerCase().includes(opts2.filter.toLowerCase())) : data.processes;
1471
+ const sorted = sortProcesses(filtered, opts2.sortBy ?? "cpu");
1472
+ const headerRowOverhead = 2;
1473
+ const tableHeaderOverhead = 2;
1474
+ const availableRows = Math.max(1, targetHeight - headerRowOverhead - tableHeaderOverhead);
1475
+ const limited = sorted.slice(0, availableRows);
1476
+ const pidW = 6, userW = 7, thrW = 3, memW = 6, cpuW = 6;
1477
+ const fixedW = pidW + userW + thrW + memW + cpuW + 5;
1478
+ const cmdW = Math.max(8, contentWidth - fixedW);
1479
+ const head = chalk3.bold("PID".padEnd(pidW)) + " " + chalk3.bold("PROGRAM".padEnd(cmdW)) + " " + chalk3.bold("USER".padEnd(userW)) + " " + chalk3.bold("THR".padStart(thrW)) + " " + chalk3.bold("MEM%".padStart(memW)) + " " + chalk3.bold("CPU%".padStart(cpuW));
1480
+ if (opts2.treeView) {
1481
+ const treeLines = buildProcessTree(limited);
1482
+ const rightTitle2 = opts2.filter ? `filter: "${opts2.filter}"` : `tree \xB7 sort: ${opts2.sortBy || "cpu"}`;
1483
+ return panel("P proc", [head, theme.border("\u2500".repeat(contentWidth)), ...treeLines], width, theme.process, theme.border, targetHeight - 2, rightTitle2);
1484
+ }
1485
+ const rows = limited.map((p) => {
1486
+ const cpuVal = p.cpu.toFixed(1) + "%";
1487
+ const cpuStr = p.cpu > 50 ? chalk3.red.bold(cpuVal.padStart(cpuW)) : p.cpu > 20 ? chalk3.yellow(cpuVal.padStart(cpuW)) : chalk3.green(cpuVal.padStart(cpuW));
1488
+ const memStr = (p.mem.toFixed(1) + "%").padStart(memW);
1489
+ const userStr = truncatePlain(p.user, userW).padEnd(userW);
1490
+ const thrStr = String(p.threads).padStart(thrW);
1491
+ const pidStr = String(p.pid).padEnd(pidW);
1492
+ const cmdStr = truncatePlain(p.command, cmdW).padEnd(cmdW);
1493
+ return `${pidStr} ${cmdStr} ${userStr} ${thrStr} ${memStr} ${cpuStr}`;
1494
+ });
1495
+ const lines = [head, theme.border("\u2500".repeat(contentWidth)), ...rows];
1496
+ if (rows.length === 0) {
1497
+ lines.push(chalk3.dim(`No processes match "${opts2.filter || ""}"`));
1498
+ }
1499
+ const rightTitle = opts2.filter ? `filter: "${opts2.filter}"` : `sort: ${opts2.sortBy || "cpu"}`;
1500
+ return panel("P proc", lines, width, theme.process, theme.border, targetHeight - 2, rightTitle);
1501
+ }
1160
1502
  function formatTable(data, opts2 = {}) {
1161
1503
  const width = clampWidth(opts2.width);
1162
1504
  const activePanel = opts2.activePanel || "grid";
1163
- const columns = gridColumns(width);
1164
- const gap = 2;
1165
- const cardWidth = Math.floor((width - gap * (columns - 1)) / columns);
1166
- const contentWidth = cardWidth - 4;
1167
1505
  const themeName = opts2.theme || "default";
1168
1506
  const theme = THEMES[themeName] || THEMES.default;
1169
- const vis = opts2.visible ?? {};
1170
1507
  const out = [];
1171
1508
  out.push(...header(data, width, [], theme.border));
1172
1509
  out.push("");
@@ -1184,105 +1521,30 @@ function formatTable(data, opts2 = {}) {
1184
1521
  }
1185
1522
  return out.join("\n");
1186
1523
  }
1187
- const cards = [];
1188
- if (vis.cpu !== false) {
1189
- cards.push({ title: "CPU", accent: theme.cpu, lines: cpuCard(data, contentWidth) });
1190
- }
1191
- if (vis.mem !== false) {
1192
- cards.push({ title: "Memory", accent: theme.mem, lines: memCard(data, contentWidth) });
1193
- }
1194
- if (vis.gpu !== false) {
1195
- const gpu = gpuCard(data, contentWidth);
1196
- if (gpu) cards.push({ title: "GPU", accent: theme.gpu, lines: gpu });
1197
- }
1198
- if (vis.power !== false) {
1199
- const power = powerCard(data);
1200
- if (power) cards.push({ title: "Power", accent: theme.power, lines: power });
1201
- }
1202
- if (vis.battery !== false) {
1203
- const battery = batteryCard(data, contentWidth);
1204
- if (battery) cards.push({ title: "Battery", accent: theme.battery, lines: battery });
1205
- }
1206
- if (vis.thermal !== false) {
1207
- cards.push({ title: "Thermal", accent: theme.thermal, lines: thermalCard(data, contentWidth) });
1208
- }
1209
- if (vis.network !== false) {
1210
- cards.push({ title: "Network", accent: theme.network, lines: networkCard(data, contentWidth) });
1211
- }
1212
- if (vis.packets !== false) {
1213
- const packets = packetCard(data, contentWidth);
1214
- if (packets) cards.push({ title: "Packets", accent: theme.network, lines: packets });
1215
- }
1216
- if (vis.tasks !== false) {
1217
- const tasks = tasksCard(data, contentWidth);
1218
- if (tasks) cards.push({ title: "Tasks", accent: theme.process, lines: tasks });
1219
- }
1220
- for (let i = 0; i < cards.length; i += columns) {
1221
- const row = cards.slice(i, i + columns);
1222
- const rowHeight = Math.max(...row.map((c) => c.lines.length));
1223
- const blocks = row.map((c) => panel(c.title, c.lines, cardWidth, c.accent, theme.border, rowHeight));
1224
- out.push(...hstack(blocks, gap, width));
1225
- out.push("");
1226
- }
1227
- if (vis.disk !== false && data.disk.length) {
1228
- const dw = width - 4;
1229
- out.push(...panel("Disk", diskTableLines(data.disk, dw, theme.border), width, theme.disk, theme.border));
1524
+ out.push(...btopCpuBox(data, width, theme, opts2));
1525
+ out.push("");
1526
+ if (width >= 85) {
1527
+ const leftColW = Math.max(32, Math.floor(width * 0.38));
1528
+ const rightColW = width - leftColW - 2;
1529
+ const leftLines = btopLeftColumn(data, leftColW, theme, opts2);
1530
+ const procLines = btopProcBox(data, rightColW, leftLines.length, theme, opts2);
1531
+ out.push(...hstack([leftLines, procLines], 2, width));
1532
+ } else {
1533
+ const leftLines = btopLeftColumn(data, width, theme, opts2);
1534
+ out.push(...leftLines);
1230
1535
  out.push("");
1231
- }
1232
- if (vis.process !== false) {
1233
- const filteredProcesses = opts2.filter ? data.processes.filter((p) => p.command.toLowerCase().includes(opts2.filter.toLowerCase())) : data.processes;
1234
- const sortedProcesses = sortProcesses(filteredProcesses, opts2.sortBy ?? "cpu");
1235
- const limited = opts2.processLimit ? sortedProcesses.slice(0, opts2.processLimit) : sortedProcesses;
1236
- const procTitle = opts2.filter ? `Processes \xB7 filter "${opts2.filter}" \xB7 sort ${opts2.sortBy ?? "cpu"}` : `Processes \xB7 sort ${opts2.sortBy ?? "cpu"}`;
1237
- if (opts2.treeView) {
1238
- const treeLines = buildProcessTree(limited);
1239
- if (treeLines.length) {
1240
- out.push(...panel(procTitle + " \xB7 tree", treeLines, width, theme.process, theme.border));
1241
- } else if (opts2.filter) {
1242
- out.push(...panel(procTitle, [chalk3.dim(`No processes match "${opts2.filter}"`)], width, theme.process, theme.border));
1243
- }
1244
- } else if (limited.length) {
1245
- out.push(...panel(procTitle, processTableLines(limited, width - 4, theme.border), width, theme.process, theme.border));
1246
- } else if (opts2.filter) {
1247
- out.push(...panel(procTitle, [chalk3.dim(`No processes match "${opts2.filter}"`)], width, theme.process, theme.border));
1248
- }
1536
+ const procLines = btopProcBox(data, width, 12, theme, opts2);
1537
+ out.push(...procLines);
1249
1538
  }
1250
1539
  return out.join("\n");
1251
1540
  }
1252
1541
  function clampWidth(width) {
1253
1542
  if (!width || Number.isNaN(width)) return 80;
1254
- return Math.max(60, Math.min(width, 240));
1255
- }
1256
- function gridColumns(width) {
1257
- if (width >= 150) return 3;
1258
- if (width >= 96) return 2;
1259
- return 1;
1543
+ return Math.max(40, Math.min(width, 240));
1260
1544
  }
1261
1545
 
1262
1546
  // src/formatters/output.ts
1263
1547
  import chalk4 from "chalk";
1264
-
1265
- // src/sparkline.ts
1266
- var BLOCKS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
1267
- function sparkline(values, opts2 = {}) {
1268
- if (!values.length) return "";
1269
- let min = opts2.min ?? Infinity;
1270
- let max = opts2.max ?? -Infinity;
1271
- if (opts2.min === void 0 || opts2.max === void 0) {
1272
- for (const v of values) {
1273
- if (v < min) min = v;
1274
- if (v > max) max = v;
1275
- }
1276
- }
1277
- const range = max - min || 1;
1278
- return values.map((v) => {
1279
- const idx = Math.round((v - min) / range * (BLOCKS.length - 1));
1280
- const clamped = Math.min(BLOCKS.length - 1, Math.max(0, idx));
1281
- return BLOCKS[clamped];
1282
- }).join("");
1283
- }
1284
-
1285
- // src/formatters/output.ts
1286
1548
  function formatJson(data) {
1287
1549
  return JSON.stringify(data, null, 2);
1288
1550
  }
@@ -1675,28 +1937,28 @@ function formatGraphs(history, width = 80, themeName = "default", graphMode = "s
1675
1937
  const theme = THEMES[themeName] || THEMES.default;
1676
1938
  const contentWidth = width - 4;
1677
1939
  const sampleCount = history.cpuUsage.length;
1678
- const sparkWidth = Math.max(10, contentWidth - 28);
1940
+ const sparkWidth = Math.max(10, contentWidth - 24);
1679
1941
  const lines = [];
1680
- lines.push(graphRow("CPU %", history.cpuUsage, { min: 0, max: 100 }, (v) => `${v.toFixed(0)}%`, sparkWidth, "collecting data...", graphMode));
1681
- lines.push(graphRow("Mem %", history.memUsage, { min: 0, max: 100 }, (v) => `${v.toFixed(0)}%`, sparkWidth, "collecting data...", graphMode));
1942
+ lines.push(graphRow("CPU %", history.cpuUsage, { min: 0, max: 100 }, (v) => `${v.toFixed(0)}%`, sparkWidth, "collecting data...", graphMode, contentWidth));
1943
+ lines.push(graphRow("Mem %", history.memUsage, { min: 0, max: 100 }, (v) => `${v.toFixed(0)}%`, sparkWidth, "collecting data...", graphMode, contentWidth));
1682
1944
  lines.push(
1683
- graphRow("Temp", history.temp, {}, formatTemp, sparkWidth, "no sensor access (needs sudo powermetrics)", graphMode)
1945
+ graphRow("Temp", history.temp, {}, (v) => `${v.toFixed(1)}\xB0C`, sparkWidth, "no sensor access", graphMode, contentWidth)
1684
1946
  );
1685
- lines.push(graphRow("Net RX/s", history.netRxRate, {}, (v) => `${formatBytes(v)}/s`, sparkWidth, "collecting data...", graphMode));
1686
- lines.push(graphRow("Net TX/s", history.netTxRate, {}, (v) => `${formatBytes(v)}/s`, sparkWidth, "collecting data...", graphMode));
1947
+ lines.push(graphRow("Net RX/s", history.netRxRate, {}, (v) => `${formatBytes(v)}/s`, sparkWidth, "collecting data...", graphMode, contentWidth));
1948
+ lines.push(graphRow("Net TX/s", history.netTxRate, {}, (v) => `${formatBytes(v)}/s`, sparkWidth, "collecting data...", graphMode, contentWidth));
1687
1949
  if (history.powerWatts.length) {
1688
- lines.push(graphRow("Power W", history.powerWatts, {}, (v) => `${v.toFixed(1)} W`, sparkWidth, "collecting data...", graphMode));
1950
+ lines.push(graphRow("Power W", history.powerWatts, {}, (v) => `${v.toFixed(1)} W`, sparkWidth, "collecting data...", graphMode, contentWidth));
1689
1951
  }
1690
1952
  if (history.rxPacketRate.length) {
1691
- lines.push(graphRow("RX pkt/s", history.rxPacketRate, {}, (v) => `${v.toFixed(0)} pkt/s`, sparkWidth, "collecting data...", graphMode));
1953
+ lines.push(graphRow("RX pkt/s", history.rxPacketRate, {}, (v) => `${v.toFixed(0)} pkt/s`, sparkWidth, "collecting data...", graphMode, contentWidth));
1692
1954
  }
1693
1955
  if (history.txPacketRate.length) {
1694
- lines.push(graphRow("TX pkt/s", history.txPacketRate, {}, (v) => `${v.toFixed(0)} pkt/s`, sparkWidth, "collecting data...", graphMode));
1956
+ lines.push(graphRow("TX pkt/s", history.txPacketRate, {}, (v) => `${v.toFixed(0)} pkt/s`, sparkWidth, "collecting data...", graphMode, contentWidth));
1695
1957
  }
1696
1958
  const title = `Graphs \xB7 mode:${graphMode} \xB7 last ${sampleCount} sample${sampleCount === 1 ? "" : "s"}`;
1697
1959
  return panel(title, lines, width, theme.graphs, theme.border).join("\n");
1698
1960
  }
1699
- function graphRow(label, values, bounds, fmt, sparkWidth = 40, emptyMessage = "collecting data...", mode = "spark") {
1961
+ function graphRow(label, values, bounds, fmt, sparkWidth = 40, emptyMessage = "collecting data...", mode = "spark", contentWidth = 76) {
1700
1962
  if (!values.length) {
1701
1963
  return `${chalk4.dim(label.padEnd(10))} ${chalk4.dim(emptyMessage)}`;
1702
1964
  }
@@ -1708,7 +1970,8 @@ function graphRow(label, values, bounds, fmt, sparkWidth = 40, emptyMessage = "c
1708
1970
  const pct = (current - bounds.min) / (bounds.max - bounds.min || 1) * 100;
1709
1971
  color = pct > 90 ? chalk4.red : pct > 70 ? chalk4.yellow : chalk4.green;
1710
1972
  }
1711
- return `${chalk4.dim(label.padEnd(10))}${color(spark)} ${chalk4.bold(fmt(current))}`;
1973
+ const raw = `${chalk4.dim(label.padEnd(10))}${color(spark)} ${chalk4.bold(fmt(current))}`;
1974
+ return fitVisible(raw, contentWidth);
1712
1975
  }
1713
1976
  function barGraph(values, bounds, width) {
1714
1977
  let min = bounds.min ?? Infinity;
@@ -2326,6 +2589,7 @@ function tableCacheParams() {
2326
2589
  state.currentTheme,
2327
2590
  state.activePanel,
2328
2591
  state.treeView ? "1" : "0",
2592
+ state.history.version,
2329
2593
  JSON.stringify(state.visiblePanels)
2330
2594
  ].join("|");
2331
2595
  }
@@ -2344,11 +2608,8 @@ function processRowBudget() {
2344
2608
  if (state.activePanel === "process") {
2345
2609
  return Math.max(10, state.termHeight - 12 - overlayRows);
2346
2610
  }
2347
- const columns = gridColumns(state.termWidth);
2348
- const visibleCards = Object.values(state.visiblePanels).filter((v) => v !== false).length;
2349
- const cardRows = Math.ceil(visibleCards / columns);
2350
- const reserved = 4 + cardRows * 8 + 8 + (state.showGraphs ? 11 : 0) + 5 + overlayRows;
2351
- return Math.max(5, state.termHeight - reserved);
2611
+ const reserved = 16 + overlayRows;
2612
+ return Math.max(4, state.termHeight - reserved);
2352
2613
  }
2353
2614
  function p2pPanelLines() {
2354
2615
  const events = state.p2pServer ? state.p2pServer.peerEventHistory : state.p2pEvents;
@@ -2429,6 +2690,18 @@ function writeFrame(lines) {
2429
2690
  }
2430
2691
  function render() {
2431
2692
  if (!state.lastData) return;
2693
+ const rows = state.termHeight || 24;
2694
+ const cols = state.termWidth || 80;
2695
+ if (cols < 50 || rows < 15) {
2696
+ const msg = chalk5.yellow(`pyre ${rows}x${cols} too small. Resize terminal to >= 50x20.`);
2697
+ const centered = Math.max(0, Math.floor((rows - 2) / 2));
2698
+ const out = [];
2699
+ for (let i = 0; i < centered; i++) out.push("");
2700
+ out.push(msg.padEnd(cols));
2701
+ while (out.length < rows) out.push("");
2702
+ writeFrame(out);
2703
+ return;
2704
+ }
2432
2705
  const lines = [];
2433
2706
  const theme = THEMES[state.currentTheme] || THEMES.default;
2434
2707
  const params = tableCacheParams();
@@ -2446,7 +2719,9 @@ function render() {
2446
2719
  treeView: state.treeView,
2447
2720
  activePanel: state.activePanel,
2448
2721
  anomalies,
2449
- tempUnit: state.tempUnit
2722
+ tempUnit: state.tempUnit,
2723
+ history: state.history,
2724
+ graphMode: state.graphMode
2450
2725
  });
2451
2726
  _cachedTableData = state.lastData;
2452
2727
  _cachedTableParams = cacheParams;
@@ -2473,37 +2748,40 @@ function render() {
2473
2748
  bodyLines.push("");
2474
2749
  bodyLines.push(...graphsStr.split("\n"));
2475
2750
  }
2476
- const footerLines = [];
2477
- footerLines.push("");
2478
- footerLines.push(...footerLine().split("\n"));
2751
+ const footerLine1 = footerLine()[0] || "";
2752
+ const footerLine22 = footerLine()[1] || "";
2753
+ const footerLine3 = footerLine()[2] || " ";
2754
+ const footerLines = [footerLine1, footerLine22, footerLine3];
2479
2755
  if (state.inputMode === "customizer") {
2480
- footerLines.push(...renderCustomizerOverlay().split("\n"));
2756
+ bodyLines.push("");
2757
+ bodyLines.push(...renderCustomizerOverlay().split("\n"));
2481
2758
  } else if (state.inputMode === "filter") {
2482
- footerLines.push(chalk5.cyan(` Filter processes: ${state.inputBuffer}_`));
2759
+ bodyLines.push(chalk5.cyan(` Filter processes: ${state.inputBuffer}_`));
2483
2760
  } else if (state.inputMode === "kill") {
2484
- footerLines.push(chalk5.cyan(` Kill PID: ${state.inputBuffer}_ (enter to confirm, esc to cancel)`));
2761
+ bodyLines.push(chalk5.cyan(` Kill PID: ${state.inputBuffer}_ (enter to confirm, esc to cancel)`));
2485
2762
  } else if (state.inputMode === "signal") {
2486
2763
  const sigIdx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer);
2487
2764
  const sigList = state.SIGNAL_OPTIONS.map((s, i) => i === sigIdx ? chalk5.yellow(s) : chalk5.dim(s)).join(" ");
2488
- footerLines.push(chalk5.cyan(` Signal: ${state.inputBuffer}_`));
2489
- footerLines.push(chalk5.dim(` ${sigList}`));
2490
- footerLines.push(chalk5.dim(" \u2191/\u2193 to select, Enter to confirm, Esc to cancel"));
2765
+ bodyLines.push(chalk5.cyan(` Signal: ${state.inputBuffer}_`));
2766
+ bodyLines.push(chalk5.dim(` ${sigList}`));
2767
+ bodyLines.push(chalk5.dim(" \u2191/\u2193 to select, Enter to confirm, Esc to cancel"));
2491
2768
  } else if (state.inputMode === "p2p") {
2492
- footerLines.push(chalk5.cyan(` P2P password (${state.p2pPort}): ${state.inputBuffer}_ (enter to start, esc to cancel)`));
2769
+ bodyLines.push(chalk5.cyan(` P2P password (${state.p2pPort}): ${state.inputBuffer}_ (enter to start, esc to cancel)`));
2493
2770
  } else if (state.statusMessage) {
2494
- footerLines.push(` ${state.statusMessage}`);
2495
- } else {
2496
- footerLines.push("");
2771
+ bodyLines.push(` ${state.statusMessage}`);
2497
2772
  }
2498
- const maxBodyLines = Math.max(1, state.termHeight - footerLines.length);
2499
- if (bodyLines.length > maxBodyLines) {
2500
- bodyLines.length = maxBodyLines;
2773
+ const footerHeight = 3;
2774
+ const targetBodyHeight = Math.max(1, state.termHeight - footerHeight);
2775
+ if (bodyLines.length > targetBodyHeight) {
2776
+ bodyLines.length = targetBodyHeight;
2501
2777
  }
2502
- const frameLines = [...bodyLines, ...footerLines];
2503
- if (frameLines.length > state.termHeight) {
2504
- frameLines.length = state.termHeight;
2778
+ const frameLines = [...bodyLines];
2779
+ while (frameLines.length < targetBodyHeight) {
2780
+ frameLines.push("");
2505
2781
  }
2506
- writeFrame(frameLines);
2782
+ frameLines.push(...footerLines);
2783
+ const fittedLines = frameLines.map((l) => fitVisible(l, cols));
2784
+ writeFrame(fittedLines);
2507
2785
  }
2508
2786
  function footerLine() {
2509
2787
  const row1 = [
@@ -2537,10 +2815,8 @@ function footerLine() {
2537
2815
  if (state.logging) badges.push(chalk5.red.bold("\u25CF REC"));
2538
2816
  if (state.activePanel !== "grid") badges.push(chalk5.cyan.bold(`\u25C9 ${state.activePanel.toUpperCase()}`));
2539
2817
  if (state.p2pServerRunning) badges.push(chalk5.green.bold(`P2P ${state.p2pBind || "0.0.0.0"}:${state.p2pPort}`));
2540
- const badgeStr = badges.join(" ");
2541
- return badgeStr ? `${str1}
2542
- ${str2} ${badgeStr}` : `${str1}
2543
- ${str2}`;
2818
+ const badgeStr = badges.join(" ") || " ";
2819
+ return [str1, str2, badgeStr];
2544
2820
  }
2545
2821
  function sendNotification(title, message) {
2546
2822
  if (!state.notificationsEnabled) return;
@@ -3579,8 +3855,9 @@ async function startLive(opts2, splashPromise) {
3579
3855
  handleInputModeKey(str, key);
3580
3856
  return;
3581
3857
  }
3582
- if (key.mouse) {
3583
- handleMouseClick(key.mouse);
3858
+ const mouse = key.mouse;
3859
+ if (mouse) {
3860
+ handleMouseClick(mouse.y, mouse.x);
3584
3861
  return;
3585
3862
  }
3586
3863
  if (key.sequence && key.sequence.startsWith("\x1B[M")) {
@@ -4166,7 +4443,7 @@ async function showSplash(config3) {
4166
4443
  var config2 = readConfig();
4167
4444
  var pkg = JSON.parse(fs5.readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
4168
4445
  var program = new Command();
4169
- program.name("pyre").version(pkg.version).description("Mac system monitoring CLI: interactive live dashboard, stats, graphs, export, packet monitor, battery predictor").option("-j, --json", "Output as JSON").option("--html", "Output as HTML").option("--md", "Output as Markdown").option("-c, --csv", "Output as CSV").option("-t, --tsv", "Output as TSV").option("--detailed", "Include detailed system info and sensor readings").option("--theme <name>", "Default theme for live mode (default, dracula, cyberpunk, monochrome, nord, gruvbox)", config2.theme).option("--interval <seconds>", "Refresh interval for live mode", String(config2.interval)).option("--once", "Show a single static snapshot instead of live feed").option("--out <file>", "Also write the snapshot output to a file (--once/--json/--csv/--tsv modes)").option("--export-dir <dir>", "Directory used for live-mode snapshot exports and logs", config2.exportDir).option("--log", "Start continuous CSV logging immediately when live mode starts").option("--tree", "Show process tree view instead of flat list").option("--sort <key>", "Sort processes by: cpu, mem, pid, user, command, state, threads, runtime", config2.sortMode).option("--packets", "Include packet monitor panel in output").option("--limit <n>", "Max number of processes to include in --once/--json/--csv/--tsv snapshots (0 = all)", "10").option("--alert-cpu <pct>", "CPU usage alert threshold (default: 90)", String(config2.cpuAlertPct)).option("--alert-temp <c>", "CPU temperature alert threshold in Celsius (default: 95)", String(config2.tempAlertC)).option("--temp-unit <unit>", "Temperature display unit: c or f (default: c)", "c").option("--p2p-host <host>", "P2P host address (server: bind address, client: server address)").option("--p2p-port <port>", "P2P port number", "9876").option("--p2p-password <password>", "Password for P2P authentication").option("--p2p-tls", "Enable TLS encryption for P2P connections").option("--p2p-cert <file>", "TLS certificate file (PEM) for P2P server").option("--p2p-key <file>", "TLS private key file (PEM) for P2P server").option("--p2p-ca <file>", "TLS CA certificate file (PEM) for P2P client").option("--p2p-insecure", "Skip TLS certificate verification (client only)").option("--p2p-rate-limit <n>", "Max auth attempts per IP per minute (default: 5)", "5").option("--p2p-allow <ips>", "Comma-separated list of allowed IPs (empty = all)").option("--p2p-deny <ips>", "Comma-separated list of denied IPs").option("--p2p-audit-log <dir>", "Directory for P2P audit logs").option("--p2p-hmac-key <key>", "HMAC key for message signing (default: derived from password)");
4446
+ program.name("pyre").version(pkg.version).description("Mac system monitoring CLI: interactive live dashboard, stats, graphs, export, packet monitor, battery predictor").option("-j, --json", "Output as JSON").option("--html", "Output as HTML").option("--md", "Output as Markdown").option("-c, --csv", "Output as CSV").option("-t, --tsv", "Output as TSV").option("--detailed", "Include detailed system info and sensor readings").option("--theme <name>", "Default theme for live mode (default, dracula, cyberpunk, monochrome, nord, gruvbox)", config2.theme).option("--interval <seconds>", "Refresh interval for live mode", String(config2.interval)).option("--once", "Show a single static snapshot instead of live feed").option("--out <file>", "Also write the snapshot output to a file (--once/--json/--csv/--tsv modes)").option("--export-dir <dir>", "Directory used for live-mode snapshot exports and logs", config2.exportDir).option("--log", "Start continuous CSV logging immediately when live mode starts").option("--tree", "Show process tree view instead of flat list").option("--sort <key>", "Sort processes by: cpu, mem, pid, user, command, state, threads, runtime", config2.sortMode).option("--packets", "Include packet monitor panel in output").option("--limit <n>", "Max number of processes to include in --once/--json/--csv/--tsv snapshots (0 = all)", "10").option("--alert-cpu <pct>", "CPU usage alert threshold (default: 90)", String(config2.cpuAlertPct)).option("--alert-temp <c>", "CPU temperature alert threshold in Celsius (default: 95)", String(config2.tempAlertC)).option("--temp-unit <unit>", "Temperature display unit: c or f (default: c)", "c").option("--p2p-host <host>", "P2P host address (server: bind address, client: server address)").option("--p2p-port <port>", "P2P port number", "9876").option("--p2p-password <password>", "Password for P2P authentication").option("--p2p-tls", "Enable TLS encryption for P2P connections").option("--p2p-cert <file>", "TLS certificate file (PEM) for P2P server").option("--p2p-key <file>", "TLS private key file (PEM) for P2P server").option("--p2p-ca <file>", "TLS CA certificate file (PEM) for P2P client").option("--p2p-insecure", "Skip TLS certificate verification (client only)").option("--p2p-rate-limit <n>", "Max auth attempts per IP per minute (default: 5)", "5").option("--p2p-allow <ips>", "Comma-separated list of allowed IPs (empty = all)").option("--p2p-deny <ips>", "Comma-separated list of denied IPs").option("--p2p-audit-log <dir>", "Directory for P2P audit logs").option("--p2p-hmac-key <key>", "HMAC key for message signing (default: derived from password)").option("--port <port>", "Port number for web server mode", "3000");
4170
4447
  program.parse(process.argv);
4171
4448
  var opts = program.opts();
4172
4449
  function isExportMode() {
@@ -4260,10 +4537,18 @@ async function main() {
4260
4537
  const alertTemp = opts.alertTemp ? parseInt(opts.alertTemp, 10) : void 0;
4261
4538
  const tempUnit = opts.tempUnit === "f" ? "f" : "c";
4262
4539
  if (cmd === "live" || !isExportMode() && !opts.once && cmd !== "p2p") {
4540
+ if (!process.stdout.isTTY && cmd !== "live") {
4541
+ await runWebCommand();
4542
+ return;
4543
+ }
4263
4544
  if (cmd === "live") {
4264
4545
  program.args.shift();
4265
4546
  }
4266
- const splashPromise = showSplash();
4547
+ const splashPromise = showSplash({
4548
+ enabled: config2.splashEnabled,
4549
+ colorScheme: config2.splashColorScheme,
4550
+ animation: config2.splashAnimation
4551
+ });
4267
4552
  const interval = parseFloat(opts.interval) || 2;
4268
4553
  await startLive({
4269
4554
  interval,
@@ -4527,7 +4812,7 @@ async function runSshCommand(host) {
4527
4812
  }
4528
4813
  process.exit(0);
4529
4814
  });
4530
- child.stdin.end();
4815
+ child.stdin?.end();
4531
4816
  process.once("SIGINT", () => {
4532
4817
  child.kill("SIGTERM");
4533
4818
  process.exit(0);
@@ -4535,32 +4820,43 @@ async function runSshCommand(host) {
4535
4820
  }
4536
4821
  async function runWebCommand() {
4537
4822
  const http = await import("http");
4538
- const data = await collectAll({ detailed: true });
4539
- const server = http.createServer((req, res) => {
4540
- if (req.url === "/" || req.url === "/index.html") {
4823
+ const port = parseInt(opts.port || process.env.PORT || "3000", 10) || 3e3;
4824
+ const server = http.createServer(async (req, res) => {
4825
+ const url = req.url || "/";
4826
+ if (url === "/" || url === "/index.html") {
4541
4827
  res.writeHead(200, { "Content-Type": "text/html" });
4542
- res.end(formatHtml(data));
4543
- } else if (req.url === "/api") {
4544
- res.writeHead(200, { "Content-Type": "application/json" });
4828
+ const indexPath = path3.join(process.cwd(), "index.html");
4829
+ let htmlContent = "";
4830
+ if (fs5.existsSync(indexPath)) {
4831
+ htmlContent = fs5.readFileSync(indexPath, "utf-8");
4832
+ } else {
4833
+ const data = await collectAll({ detailed: true });
4834
+ htmlContent = formatHtml(data);
4835
+ }
4836
+ res.end(htmlContent);
4837
+ } else if (url === "/api" || url === "/api/stats" || url === "/data") {
4838
+ const data = await collectAll({ detailed: true });
4839
+ res.writeHead(200, {
4840
+ "Content-Type": "application/json",
4841
+ "Access-Control-Allow-Origin": "*",
4842
+ "Access-Control-Allow-Methods": "GET, OPTIONS"
4843
+ });
4545
4844
  res.end(JSON.stringify(data, null, 2));
4546
4845
  } else {
4547
4846
  res.writeHead(404);
4548
4847
  res.end("Not found");
4549
4848
  }
4550
4849
  });
4551
- server.listen(0, "127.0.0.1", async () => {
4552
- const address = server.address();
4553
- const port = typeof address === "object" && address ? address.port : 0;
4554
- const url = `http://127.0.0.1:${port}`;
4850
+ server.listen(port, "0.0.0.0", () => {
4555
4851
  console.log(chalk9.bold(`
4556
- pyre web`));
4557
- console.log(chalk9.dim(` Dashboard: ${url}`));
4558
- console.log(chalk9.dim(` API: ${url}/api`));
4852
+ pyre web server running on port ${port}`));
4853
+ console.log(chalk9.dim(` Dashboard: http://0.0.0.0:${port}/`));
4854
+ console.log(chalk9.dim(` API: http://0.0.0.0:${port}/api`));
4559
4855
  console.log(chalk9.dim(" Press Ctrl+C to stop\n"));
4560
- process.once("SIGINT", () => {
4561
- server.close();
4562
- process.exit(0);
4563
- });
4856
+ });
4857
+ process.once("SIGINT", () => {
4858
+ server.close();
4859
+ process.exit(0);
4564
4860
  });
4565
4861
  }
4566
4862
  async function runBenchCommand(benchCmd) {