pyre-cli 3.4.0 → 3.6.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
@@ -10,10 +10,12 @@ import chalk9 from "chalk";
10
10
  import chalk from "chalk";
11
11
 
12
12
  // src/monitors/run.ts
13
- async function run(cmd, fallback = "") {
13
+ import { exec } from "child_process";
14
+ import { promisify } from "util";
15
+ var execAsync = promisify(exec);
16
+ async function run(cmd, fallback = "", timeout = 5e3) {
14
17
  try {
15
- const { execSync } = await import("child_process");
16
- return execSync(cmd, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5e3 }).trim();
18
+ return (await execAsync(cmd, { encoding: "utf8", timeout })).stdout.trim();
17
19
  } catch {
18
20
  return fallback;
19
21
  }
@@ -23,7 +25,7 @@ async function run(cmd, fallback = "") {
23
25
  var smcCache = null;
24
26
  async function getSmcMetrics() {
25
27
  const now = Date.now();
26
- if (smcCache && now - smcCache.ts < 1500) return smcCache.data;
28
+ if (smcCache && now - smcCache.ts < 4e3) return smcCache.data;
27
29
  const result = { temps: {}, power: {}, freq: {} };
28
30
  try {
29
31
  const pm = (await run(
@@ -60,6 +62,46 @@ function parseSuffix(s) {
60
62
  }
61
63
 
62
64
  // src/monitors/collectors.ts
65
+ var SP_TTL_MS = 1e4;
66
+ var NETSTAT_TTL_MS = 1e3;
67
+ var ROUTE_TTL_MS = 5e3;
68
+ var SYSCTL_TTL_MS = 6e4;
69
+ var spGpuCache = null;
70
+ var spBatteryCache = null;
71
+ var netstatIbCache = null;
72
+ var routeCache = null;
73
+ var sysctlCache = /* @__PURE__ */ new Map();
74
+ async function cachedSysctl(key, fallback) {
75
+ const now = Date.now();
76
+ const hit = sysctlCache.get(key);
77
+ if (hit && now - hit.ts < SYSCTL_TTL_MS) return hit.value;
78
+ const value = await run(`sysctl -n ${key}`, fallback);
79
+ sysctlCache.set(key, { value, ts: now });
80
+ return value;
81
+ }
82
+ async function cachedNetstatIb() {
83
+ const now = Date.now();
84
+ if (netstatIbCache && now - netstatIbCache.ts < NETSTAT_TTL_MS) return netstatIbCache.raw;
85
+ const raw = await run("netstat -ib");
86
+ netstatIbCache = { raw, ts: now };
87
+ return raw;
88
+ }
89
+ async function cachedRouteIface() {
90
+ const now = Date.now();
91
+ if (routeCache && now - routeCache.ts < ROUTE_TTL_MS) return routeCache;
92
+ const primary = (await run("route -n get default")).trim();
93
+ const ifaceMatch = primary.match(/interface:\s+(\S+)/);
94
+ const iface = ifaceMatch?.[1] || "en0";
95
+ let ip = "0.0.0.0";
96
+ try {
97
+ const ifconfig = (await run(`ifconfig ${iface}`)).trim();
98
+ const ipMatch = ifconfig.match(/inet\s+([\d.]+)\s+netmask/);
99
+ if (ipMatch) ip = ipMatch[1];
100
+ } catch {
101
+ }
102
+ routeCache = { iface, ip, ts: now };
103
+ return routeCache;
104
+ }
63
105
  var BATTERY_WINDOW_MS = 15 * 60 * 1e3;
64
106
  var MIN_ESTIMATE_WINDOW_MS = 30 * 1e3;
65
107
  var prevBattery = { samples: [] };
@@ -158,46 +200,49 @@ async function collectPower() {
158
200
  return null;
159
201
  }
160
202
  async function collectGpu(detailed) {
161
- try {
162
- const spRaw = (await run("system_profiler SPDisplaysDataType 2>/dev/null")).trim();
163
- if (!spRaw) return null;
164
- const modelMatch = spRaw.match(/Chipset Model:\s*(.+)/i);
165
- const model = modelMatch ? modelMatch[1].trim() : "Unknown";
166
- const memMatch = spRaw.match(/VRAM \(Total\):\s*(.+)/i);
167
- let memory = 0;
168
- if (memMatch) {
169
- const memStr = memMatch[1].trim();
170
- const memNum = parseFloat(memStr);
171
- if (memStr.includes("GB")) memory = memNum * 1024 * 1024 * 1024;
172
- else if (memStr.includes("MB")) memory = memNum * 1024 * 1024;
173
- }
174
- let utilization = 0;
175
- let temperature;
176
- let gpuProcesses = 0;
177
- if (detailed) {
178
- try {
179
- const pm = (await run("sudo -n powermetrics --samplers gpu_power -n 1 --format text 2>/dev/null", "")).trim();
180
- const gpuPowerMatch = pm.match(/GPU Power:\s*([\d.]+)\s*mW/i);
181
- if (gpuPowerMatch) {
182
- utilization = Math.min(100, Math.round(parseFloat(gpuPowerMatch[1]) / 10));
183
- }
184
- } catch {
185
- }
186
- try {
187
- const { temps } = await getSmcMetrics();
188
- if (temps.gpu_die !== void 0) temperature = temps.gpu_die;
189
- } catch {
190
- }
191
- try {
192
- const psRaw = (await run('ps aux | grep -i "[g]pu" | wc -l')).trim();
193
- gpuProcesses = parseInt(psRaw) || 0;
194
- } catch {
203
+ let spRaw = "";
204
+ const now = Date.now();
205
+ if (spGpuCache && now - spGpuCache.ts < SP_TTL_MS) {
206
+ spRaw = spGpuCache.raw;
207
+ } else {
208
+ spRaw = (await run("system_profiler SPDisplaysDataType 2>/dev/null")).trim();
209
+ spGpuCache = { raw: spRaw, ts: now };
210
+ }
211
+ if (!spRaw) return null;
212
+ const modelMatch = spRaw.match(/Chipset Model:\s*(.+)/i);
213
+ const model = modelMatch ? modelMatch[1].trim() : "Unknown";
214
+ const memMatch = spRaw.match(/VRAM \(Total\):\s*(.+)/i);
215
+ let memory = 0;
216
+ if (memMatch) {
217
+ const memStr = memMatch[1].trim();
218
+ const memNum = parseFloat(memStr);
219
+ if (memStr.includes("GB")) memory = memNum * 1024 * 1024 * 1024;
220
+ else if (memStr.includes("MB")) memory = memNum * 1024 * 1024;
221
+ }
222
+ let utilization = 0;
223
+ let temperature;
224
+ let gpuProcesses = 0;
225
+ if (detailed) {
226
+ try {
227
+ const pm = (await run("sudo -n powermetrics --samplers gpu_power -n 1 --format text 2>/dev/null", "")).trim();
228
+ const gpuPowerMatch = pm.match(/GPU Power:\s*([\d.]+)\s*mW/i);
229
+ if (gpuPowerMatch) {
230
+ utilization = Math.min(100, Math.round(parseFloat(gpuPowerMatch[1]) / 10));
195
231
  }
232
+ } catch {
233
+ }
234
+ try {
235
+ const { temps } = await getSmcMetrics();
236
+ if (temps.gpu_die !== void 0) temperature = temps.gpu_die;
237
+ } catch {
238
+ }
239
+ try {
240
+ const psRaw = (await run('ps aux | grep -i "[g]pu" | wc -l')).trim();
241
+ gpuProcesses = parseInt(psRaw) || 0;
242
+ } catch {
196
243
  }
197
- return { model, memory, utilization, temperature, processes: gpuProcesses };
198
- } catch {
199
- return null;
200
244
  }
245
+ return { model, memory, utilization, temperature, processes: gpuProcesses };
201
246
  }
202
247
  async function collectSystem() {
203
248
  const hostname = (await run("hostname", "Mac")).trim();
@@ -211,10 +256,10 @@ async function collectSystem() {
211
256
  };
212
257
  }
213
258
  async function collectCpu() {
214
- const brand = (await run("sysctl -n machdep.cpu.brand_string", "Unknown CPU")).trim();
215
- const cores = parseInt((await run("sysctl -n hw.ncpu")).trim()) || 1;
216
- const physicalCores = parseInt((await run("sysctl -n hw.physicalcpu")).trim()) || cores;
217
- const frequencyHz = parseInt((await run("sysctl -n hw.cpufrequency")).trim());
259
+ const brand = (await cachedSysctl("machdep.cpu.brand_string", "Unknown CPU")).trim();
260
+ const cores = parseInt(await cachedSysctl("hw.ncpu", "1")) || 1;
261
+ const physicalCores = parseInt(await cachedSysctl("hw.physicalcpu", String(cores))) || cores;
262
+ const frequencyHz = parseInt(await cachedSysctl("hw.cpufrequency", "0"));
218
263
  let frequency = frequencyHz > 0 ? Math.round(frequencyHz / 1e6) : 0;
219
264
  if (frequency === 0) {
220
265
  try {
@@ -247,8 +292,8 @@ async function collectCpu() {
247
292
  return { brand, cores, physicalCores, frequency, usage, loadAvg, temperature };
248
293
  }
249
294
  async function collectMemory() {
250
- const totalBytes = parseInt((await run("sysctl -n hw.memsize")).trim()) || 0;
251
- const pageSize = parseInt((await run("sysctl -n hw.pagesize", "4096")).trim()) || 4096;
295
+ const totalBytes = parseInt(await cachedSysctl("hw.memsize", "0")) || 0;
296
+ const pageSize = parseInt(await cachedSysctl("hw.pagesize", "4096")) || 4096;
252
297
  const vmStat = (await run("vm_stat")).trim();
253
298
  const getPageCount = (label) => {
254
299
  const m = vmStat.match(new RegExp(`Pages\\s+${label}:\\s+(\\d+)\\.`));
@@ -318,7 +363,14 @@ async function collectBattery() {
318
363
  let condition;
319
364
  let maxCapacityPercent;
320
365
  try {
321
- const battInfo = (await run("system_profiler SPPowerDataType 2>/dev/null")).trim();
366
+ const now = Date.now();
367
+ let battInfo;
368
+ if (spBatteryCache && now - spBatteryCache.ts < SP_TTL_MS) {
369
+ battInfo = spBatteryCache.raw;
370
+ } else {
371
+ battInfo = (await run("system_profiler SPPowerDataType 2>/dev/null")).trim();
372
+ spBatteryCache = { raw: battInfo, ts: now };
373
+ }
322
374
  const cycleMatch = battInfo.match(/Cycle Count:\s+(\d+)/);
323
375
  if (cycleMatch) cycles = parseInt(cycleMatch[1]);
324
376
  const conditionMatch = battInfo.match(/Condition:\s+(.+)/);
@@ -426,17 +478,8 @@ async function collectThermal(detailed) {
426
478
  }
427
479
  async function collectNetwork() {
428
480
  try {
429
- const primary = (await run("route -n get default")).trim();
430
- const ifaceMatch = primary.match(/interface:\s+(\S+)/);
431
- const iface = ifaceMatch?.[1] || "en0";
432
- let ip = "0.0.0.0";
433
- try {
434
- const ifconfig = (await run(`ifconfig ${iface}`)).trim();
435
- const ipMatch = ifconfig.match(/inet\s+([\d.]+)\s+netmask/);
436
- if (ipMatch) ip = ipMatch[1];
437
- } catch {
438
- }
439
- const netStat = (await run("netstat -ib")).trim();
481
+ const { iface, ip } = await cachedRouteIface();
482
+ const netStat = await cachedNetstatIb();
440
483
  let rxBytes = 0;
441
484
  let txBytes = 0;
442
485
  let rxPackets = 0;
@@ -477,7 +520,7 @@ async function collectProcesses(limit) {
477
520
  }
478
521
  async function collectPackets() {
479
522
  try {
480
- const netStat = (await run("netstat -ib")).trim();
523
+ const netStat = await cachedNetstatIb();
481
524
  const lines = netStat.split("\n").slice(1);
482
525
  let totalRxPackets = 0;
483
526
  let totalTxPackets = 0;
@@ -520,15 +563,7 @@ async function collectPackets() {
520
563
  }
521
564
  return { pid: 0, command: "", rxBytes: 0, txBytes: 0 };
522
565
  }).filter((p) => p.pid > 0);
523
- const allLsof = (await run("lsof -i -n -P 2>/dev/null | tail -n +2 | awk '{print $1, $2}' | sort | uniq -c | sort -rn", "")).trim();
524
- const allLsofLines = allLsof.split("\n");
525
- allProcesses = allLsofLines.map((line) => {
526
- const parts = line.trim().split(/\s+/);
527
- if (parts.length >= 3) {
528
- return { pid: parseInt(parts[2]) || 0, command: parts[1], rxBytes: 0, txBytes: parseInt(parts[0]) || 0 };
529
- }
530
- return { pid: 0, command: "", rxBytes: 0, txBytes: 0 };
531
- }).filter((p) => p.pid > 0);
566
+ allProcesses = topProcesses;
532
567
  } catch {
533
568
  topProcesses = [];
534
569
  allProcesses = [];
@@ -677,7 +712,7 @@ function panel(title, lines, width, accent, borderAccent, height) {
677
712
  }
678
713
  return [top, ...body, bottom];
679
714
  }
680
- function hstack(blocks, gap = 2) {
715
+ function hstack(blocks, gap = 2, targetWidth) {
681
716
  if (!blocks.length) return [];
682
717
  const height = Math.max(...blocks.map((b) => b.length));
683
718
  const widths = blocks.map((b) => visLen(b[0] ?? ""));
@@ -689,7 +724,12 @@ function hstack(blocks, gap = 2) {
689
724
  });
690
725
  const out = [];
691
726
  for (let row = 0; row < height; row++) {
692
- out.push(padded.map((a) => a[row]).join(" ".repeat(gap)));
727
+ let line = padded.map((a) => a[row]).join(" ".repeat(gap));
728
+ if (targetWidth !== void 0) {
729
+ const pad = Math.max(0, targetWidth - visLen(line));
730
+ line += " ".repeat(pad);
731
+ }
732
+ out.push(line);
693
733
  }
694
734
  return out;
695
735
  }
@@ -941,6 +981,15 @@ function buildProcessTree(processes) {
941
981
  for (const p of processes) {
942
982
  if (p.ppid === 0 || !map.has(p.ppid)) roots.push(p);
943
983
  }
984
+ const childrenMap = /* @__PURE__ */ new Map();
985
+ for (const p of processes) {
986
+ const list = childrenMap.get(p.ppid) || [];
987
+ list.push(p);
988
+ childrenMap.set(p.ppid, list);
989
+ }
990
+ for (const list of childrenMap.values()) {
991
+ list.sort((a, b) => b.cpu - a.cpu);
992
+ }
944
993
  const lines = [];
945
994
  const visited = /* @__PURE__ */ new Set();
946
995
  function renderNode(proc, prefix, isLast) {
@@ -954,12 +1003,19 @@ function buildProcessTree(processes) {
954
1003
  const thrStr = String(proc.threads).padEnd(6);
955
1004
  const cmd = truncatePlain(proc.command, Math.max(10, 100 - pidStr.length - cpuStr.length - memStr.length - stateStr.length - thrStr.length - 10));
956
1005
  lines.push(`${prefix}${connector}${pidStr}${cpuStr}${memStr}${stateStr}${thrStr} ${cmd}`);
957
- const children = processes.filter((p) => p.ppid === proc.pid && !visited.has(p.pid));
958
- children.sort((a, b) => b.cpu - a.cpu);
959
- children.forEach((child, i) => {
960
- const nextPrefix = prefix + (isLast ? " " : "\u2502 ");
961
- renderNode(child, nextPrefix, i === children.length - 1);
962
- });
1006
+ const children = childrenMap.get(proc.pid);
1007
+ if (children) {
1008
+ let idx = 0;
1009
+ let total = 0;
1010
+ for (const c of children) {
1011
+ if (!visited.has(c.pid)) total++;
1012
+ }
1013
+ for (const child of children) {
1014
+ if (visited.has(child.pid)) continue;
1015
+ const nextPrefix = prefix + (isLast ? " " : "\u2502 ");
1016
+ renderNode(child, nextPrefix, ++idx === total);
1017
+ }
1018
+ }
963
1019
  }
964
1020
  roots.sort((a, b) => b.cpu - a.cpu);
965
1021
  roots.forEach((root, i) => renderNode(root, "", i === roots.length - 1));
@@ -992,8 +1048,10 @@ function detailPanelTitle(panel2) {
992
1048
  };
993
1049
  return map[panel2] || panel2;
994
1050
  }
995
- function activeDetailLines(data, activePanel, width) {
1051
+ function activeDetailLines(data, activePanel, width, opts2 = {}) {
996
1052
  const contentWidth = width - 4;
1053
+ const themeName = opts2.theme || "default";
1054
+ const theme = THEMES[themeName] || THEMES.default;
997
1055
  switch (activePanel) {
998
1056
  case "cpu":
999
1057
  return cpuCard(data, contentWidth);
@@ -1043,10 +1101,15 @@ function activeDetailLines(data, activePanel, width) {
1043
1101
  case "tasks":
1044
1102
  return data.tasks?.length ? tasksCard(data, contentWidth) : ["No task data available"];
1045
1103
  case "disk":
1046
- return data.disk.length ? diskTableLines(data.disk, width - 4, THEMES.default.border) : ["No disk data available"];
1104
+ return data.disk.length ? diskTableLines(data.disk, width - 4, theme.border) : ["No disk data available"];
1047
1105
  case "process": {
1048
- const sorted = sortProcesses([...data.processes], "cpu");
1049
- return processTableLines(sorted.slice(0, 50), width - 4, THEMES.default.border);
1106
+ const filtered = opts2.filter ? data.processes.filter((p) => p.command.toLowerCase().includes(opts2.filter.toLowerCase())) : data.processes;
1107
+ const sorted = sortProcesses(filtered, opts2.sortBy ?? "cpu");
1108
+ const limited = opts2.processLimit ? sorted.slice(0, opts2.processLimit) : sorted;
1109
+ if (opts2.treeView) {
1110
+ return buildProcessTree(limited);
1111
+ }
1112
+ return processTableLines(limited, width - 4, theme.border);
1050
1113
  }
1051
1114
  default:
1052
1115
  return null;
@@ -1068,7 +1131,7 @@ function formatTable(data, opts2 = {}) {
1068
1131
  out.push(tabBar(activePanel, width, theme));
1069
1132
  out.push("");
1070
1133
  if (activePanel !== "grid") {
1071
- const lines = activeDetailLines(data, activePanel, width);
1134
+ const lines = activeDetailLines(data, activePanel, width, opts2);
1072
1135
  if (lines) {
1073
1136
  out.push(...panel(detailPanelTitle(activePanel), lines, width, theme[activePanel] ?? theme.cpu, theme.border));
1074
1137
  }
@@ -1111,7 +1174,7 @@ function formatTable(data, opts2 = {}) {
1111
1174
  const row = cards.slice(i, i + columns);
1112
1175
  const rowHeight = Math.max(...row.map((c) => c.lines.length));
1113
1176
  const blocks = row.map((c) => panel(c.title, c.lines, cardWidth, c.accent, theme.border, rowHeight));
1114
- out.push(...hstack(blocks, gap));
1177
+ out.push(...hstack(blocks, gap, width));
1115
1178
  out.push("");
1116
1179
  }
1117
1180
  if (vis.disk !== false && data.disk.length) {
@@ -1156,8 +1219,14 @@ import chalk4 from "chalk";
1156
1219
  var BLOCKS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
1157
1220
  function sparkline(values, opts2 = {}) {
1158
1221
  if (!values.length) return "";
1159
- const min = opts2.min ?? Math.min(...values);
1160
- const max = opts2.max ?? Math.max(...values);
1222
+ let min = opts2.min ?? Infinity;
1223
+ let max = opts2.max ?? -Infinity;
1224
+ if (opts2.min === void 0 || opts2.max === void 0) {
1225
+ for (const v of values) {
1226
+ if (v < min) min = v;
1227
+ if (v > max) max = v;
1228
+ }
1229
+ }
1161
1230
  const range = max - min || 1;
1162
1231
  return values.map((v) => {
1163
1232
  const idx = Math.round((v - min) / range * (BLOCKS.length - 1));
@@ -1251,21 +1320,21 @@ function formatGraphs(history, width = 80, themeName = "default", graphMode = "s
1251
1320
  const sampleCount = history.cpuUsage.length;
1252
1321
  const sparkWidth = Math.max(10, contentWidth - 28);
1253
1322
  const lines = [];
1254
- lines.push(graphRow("CPU %", history.cpuUsage, { min: 0, max: 100 }, (v) => `${v.toFixed(0)}%`, sparkWidth, graphMode));
1255
- lines.push(graphRow("Mem %", history.memUsage, { min: 0, max: 100 }, (v) => `${v.toFixed(0)}%`, sparkWidth, graphMode));
1323
+ lines.push(graphRow("CPU %", history.cpuUsage, { min: 0, max: 100 }, (v) => `${v.toFixed(0)}%`, sparkWidth, "collecting data...", graphMode));
1324
+ lines.push(graphRow("Mem %", history.memUsage, { min: 0, max: 100 }, (v) => `${v.toFixed(0)}%`, sparkWidth, "collecting data...", graphMode));
1256
1325
  lines.push(
1257
1326
  graphRow("Temp", history.temp, {}, formatTemp, sparkWidth, "no sensor access (needs sudo powermetrics)", graphMode)
1258
1327
  );
1259
- lines.push(graphRow("Net RX/s", history.netRxRate, {}, (v) => `${formatBytes(v)}/s`, sparkWidth, graphMode));
1260
- lines.push(graphRow("Net TX/s", history.netTxRate, {}, (v) => `${formatBytes(v)}/s`, sparkWidth, graphMode));
1328
+ lines.push(graphRow("Net RX/s", history.netRxRate, {}, (v) => `${formatBytes(v)}/s`, sparkWidth, "collecting data...", graphMode));
1329
+ lines.push(graphRow("Net TX/s", history.netTxRate, {}, (v) => `${formatBytes(v)}/s`, sparkWidth, "collecting data...", graphMode));
1261
1330
  if (history.powerWatts.length) {
1262
- lines.push(graphRow("Power W", history.powerWatts, {}, (v) => `${v.toFixed(1)} W`, sparkWidth, graphMode));
1331
+ lines.push(graphRow("Power W", history.powerWatts, {}, (v) => `${v.toFixed(1)} W`, sparkWidth, "collecting data...", graphMode));
1263
1332
  }
1264
1333
  if (history.rxPacketRate.length) {
1265
- lines.push(graphRow("RX pkt/s", history.rxPacketRate, {}, (v) => `${v.toFixed(0)} pkt/s`, sparkWidth, graphMode));
1334
+ lines.push(graphRow("RX pkt/s", history.rxPacketRate, {}, (v) => `${v.toFixed(0)} pkt/s`, sparkWidth, "collecting data...", graphMode));
1266
1335
  }
1267
1336
  if (history.txPacketRate.length) {
1268
- lines.push(graphRow("TX pkt/s", history.txPacketRate, {}, (v) => `${v.toFixed(0)} pkt/s`, sparkWidth, graphMode));
1337
+ lines.push(graphRow("TX pkt/s", history.txPacketRate, {}, (v) => `${v.toFixed(0)} pkt/s`, sparkWidth, "collecting data...", graphMode));
1269
1338
  }
1270
1339
  const title = `Graphs \xB7 mode:${graphMode} \xB7 last ${sampleCount} sample${sampleCount === 1 ? "" : "s"}`;
1271
1340
  return panel(title, lines, width, theme.graphs, theme.border).join("\n");
@@ -1285,8 +1354,14 @@ function graphRow(label, values, bounds, fmt, sparkWidth = 40, emptyMessage = "c
1285
1354
  return `${chalk4.dim(label.padEnd(10))}${color(spark)} ${chalk4.bold(fmt(current))}`;
1286
1355
  }
1287
1356
  function barGraph(values, bounds, width) {
1288
- const min = bounds.min ?? Math.min(...values);
1289
- const max = bounds.max ?? Math.max(...values);
1357
+ let min = bounds.min ?? Infinity;
1358
+ let max = bounds.max ?? -Infinity;
1359
+ if (bounds.min === void 0 || bounds.max === void 0) {
1360
+ for (const v of values) {
1361
+ if (v < min) min = v;
1362
+ if (v > max) max = v;
1363
+ }
1364
+ }
1290
1365
  const range = max - min || 1;
1291
1366
  return values.map((v) => {
1292
1367
  const pct = (v - min) / range;
@@ -1305,6 +1380,8 @@ import readline from "readline";
1305
1380
  var History = class {
1306
1381
  /** Maximum number of samples retained in the rolling window. */
1307
1382
  maxLen;
1383
+ /** Monotonically increasing version, bumped on every push. */
1384
+ version = 0;
1308
1385
  /** Rolling window of CPU usage percentages (0–100). */
1309
1386
  cpuUsage = [];
1310
1387
  /** Rolling window of memory usage percentages (0–100). */
@@ -1376,6 +1453,7 @@ var History = class {
1376
1453
  if (sample.rxPackets !== void 0) this.pushBounded(this.rxPacketRate, rxPktRate);
1377
1454
  if (sample.txPackets !== void 0) this.pushBounded(this.txPacketRate, txPktRate);
1378
1455
  if (sample.connections !== void 0) this.pushBounded(this.connections, sample.connections);
1456
+ this.version++;
1379
1457
  }
1380
1458
  /** Clear all rolling windows and reset cumulative byte counters. */
1381
1459
  reset() {
@@ -1394,6 +1472,7 @@ var History = class {
1394
1472
  this.lastRxPackets = 0;
1395
1473
  this.lastTxPackets = 0;
1396
1474
  this.lastTs = 0;
1475
+ this.version = 0;
1397
1476
  }
1398
1477
  /**
1399
1478
  * Grow or shrink the rolling window (e.g. on terminal resize),
@@ -1416,6 +1495,7 @@ var History = class {
1416
1495
  var SIGNAL_OPTIONS = ["SIGTERM", "SIGKILL", "SIGINT", "SIGHUP", "SIGSTOP", "SIGCONT"];
1417
1496
  var state = {
1418
1497
  intervalHandle: null,
1498
+ uiIntervalHandle: null,
1419
1499
  running: false,
1420
1500
  paused: false,
1421
1501
  detailed: false,
@@ -1451,6 +1531,8 @@ var state = {
1451
1531
  processFilter: "",
1452
1532
  inputMode: null,
1453
1533
  inputBuffer: "",
1534
+ targetPid: "",
1535
+ selectedSignal: "SIGTERM",
1454
1536
  treeView: false,
1455
1537
  SIGNAL_OPTIONS,
1456
1538
  customizerIndex: 0,
@@ -1503,6 +1585,23 @@ function setStatus(msg, ms = 3e3) {
1503
1585
  state.statusMessage = "";
1504
1586
  }, ms);
1505
1587
  }
1588
+ function getToggleKey(opt) {
1589
+ const map = {
1590
+ "Toggle CPU": "cpu",
1591
+ "Toggle Memory": "mem",
1592
+ "Toggle GPU": "gpu",
1593
+ "Toggle Power": "power",
1594
+ "Toggle Battery": "battery",
1595
+ "Toggle Thermal": "thermal",
1596
+ "Toggle Network": "network",
1597
+ "Toggle Packets": "packets",
1598
+ "Toggle Tasks": "tasks",
1599
+ "Toggle Disk": "disk",
1600
+ "Toggle Processes": "process",
1601
+ "Toggle Tree View": "tree"
1602
+ };
1603
+ return map[opt] ?? null;
1604
+ }
1506
1605
 
1507
1606
  // src/live/export.ts
1508
1607
  import fs from "fs";
@@ -1564,12 +1663,48 @@ function writeLogRow(data) {
1564
1663
 
1565
1664
  // src/live/render.ts
1566
1665
  import chalk5 from "chalk";
1666
+ var _cachedTable = "";
1667
+ var _cachedTableData = null;
1668
+ var _cachedTableParams = "";
1669
+ var _tableDataDirty = false;
1670
+ function invalidateTableCache() {
1671
+ _tableDataDirty = true;
1672
+ }
1673
+ var _cachedGraphs = "";
1674
+ var _cachedGraphsVersion = -1;
1675
+ var _cachedGraphsParams = "";
1676
+ function tableCacheParams() {
1677
+ const limit = processRowBudget();
1678
+ return [
1679
+ state.sortMode,
1680
+ state.processFilter || "",
1681
+ limit,
1682
+ state.currentTheme,
1683
+ state.activePanel,
1684
+ state.treeView ? "1" : "0",
1685
+ JSON.stringify(state.visiblePanels)
1686
+ ].join("|");
1687
+ }
1688
+ function graphsCacheParams() {
1689
+ return [
1690
+ state.termWidth,
1691
+ state.currentTheme,
1692
+ state.graphMode
1693
+ ].join("|");
1694
+ }
1567
1695
  function processRowBudget() {
1696
+ let overlayRows = 0;
1697
+ if (state.inputMode === "customizer") overlayRows = state.CUSTOMIZER_OPTIONS.length + 3;
1698
+ else if (state.inputMode === "signal") overlayRows = 6;
1699
+ else if (state.inputMode === "filter" || state.inputMode === "kill" || state.inputMode === "p2p") overlayRows = 3;
1700
+ if (state.activePanel === "process") {
1701
+ return Math.max(10, state.termHeight - 12 - overlayRows);
1702
+ }
1568
1703
  const columns = gridColumns(state.termWidth);
1569
1704
  const visibleCards = Object.values(state.visiblePanels).filter((v) => v !== false).length;
1570
1705
  const cardRows = Math.ceil(visibleCards / columns);
1571
- const reserved = 2 + cardRows * 8 + 11 + (state.showGraphs ? 9 : 0) + 4;
1572
- return Math.max(3, Math.min(40, state.termHeight - reserved));
1706
+ const reserved = 4 + cardRows * 8 + 8 + (state.showGraphs ? 11 : 0) + 5 + overlayRows;
1707
+ return Math.max(5, state.termHeight - reserved);
1573
1708
  }
1574
1709
  function p2pPanelLines() {
1575
1710
  const events = state.p2pServer ? state.p2pServer.peerEventHistory : state.p2pEvents;
@@ -1606,94 +1741,144 @@ function renderCustomizerOverlay() {
1606
1741
  lines.push(`${prefix}${opt}: ${chalk5.bold.green(state.currentTheme)} [${themesList.join(", ")}]`);
1607
1742
  } else if (opt === "Graph Mode") {
1608
1743
  lines.push(`${prefix}${opt}: ${chalk5.bold.green(state.graphMode)} [spark, bar]`);
1609
- } else if (opt === "Toggle Tree View") {
1610
- const status = state.treeView ? chalk5.green("[VISIBLE]") : chalk5.red("[HIDDEN]");
1611
- lines.push(`${prefix}${opt}: ${status}`);
1612
1744
  } else {
1613
- const key = opt.replace("Toggle ", "").toLowerCase();
1614
- const isVisible = state.visiblePanels[key] !== false;
1745
+ const toggleKey = getToggleKey(opt);
1746
+ const isVisible = toggleKey ? toggleKey === "tree" ? state.treeView : state.visiblePanels[toggleKey] !== false : true;
1615
1747
  const status = isVisible ? chalk5.green("[VISIBLE]") : chalk5.red("[HIDDEN]");
1616
1748
  lines.push(`${prefix}${opt}: ${status}`);
1617
1749
  }
1618
1750
  });
1619
1751
  return lines.map((l) => ` ${l}`).join("\n");
1620
1752
  }
1753
+ var _prevFrameLines = [];
1754
+ var _frameDirty = true;
1755
+ function invalidateFrame() {
1756
+ _frameDirty = true;
1757
+ }
1758
+ function writeFrame(lines) {
1759
+ if (_frameDirty || _prevFrameLines.length === 0 || lines.length !== _prevFrameLines.length) {
1760
+ process.stdout.write("\x1B[?25l\x1B[H" + lines.map((l) => `\x1B[2K${l}`).join("\r\n") + "\x1B[0J");
1761
+ _prevFrameLines = lines;
1762
+ _frameDirty = false;
1763
+ return;
1764
+ }
1765
+ let out = "";
1766
+ for (let i = 0; i < lines.length; i++) {
1767
+ if (lines[i] !== _prevFrameLines[i]) {
1768
+ out += `\x1B[${i + 1};1H\x1B[2K${lines[i]}`;
1769
+ }
1770
+ }
1771
+ if (out) process.stdout.write(out);
1772
+ _prevFrameLines = lines;
1773
+ }
1621
1774
  function render() {
1622
1775
  if (!state.lastData) return;
1623
1776
  const lines = [];
1624
1777
  const theme = THEMES[state.currentTheme] || THEMES.default;
1778
+ const params = tableCacheParams();
1779
+ const tableStr = _tableDataDirty || state.lastData !== _cachedTableData || params !== _cachedTableParams ? (() => {
1780
+ const out = formatTable(state.lastData, {
1781
+ width: state.termWidth,
1782
+ sortBy: state.sortMode,
1783
+ filter: state.processFilter || void 0,
1784
+ processLimit: processRowBudget(),
1785
+ theme: state.currentTheme,
1786
+ visible: state.visiblePanels,
1787
+ treeView: state.treeView,
1788
+ activePanel: state.activePanel
1789
+ });
1790
+ _cachedTableData = state.lastData;
1791
+ _cachedTableParams = params;
1792
+ _cachedTable = out;
1793
+ _tableDataDirty = false;
1794
+ return out;
1795
+ })() : _cachedTable;
1796
+ const bodyLines = [];
1625
1797
  if (state.activePanel === "p2p") {
1626
1798
  const panelLines = p2pPanelLines();
1627
- lines.push(...panel(panelLines.title, panelLines.body, state.termWidth, theme.process, theme.border));
1799
+ bodyLines.push(...panel(panelLines.title, panelLines.body, state.termWidth, theme.process, theme.border));
1628
1800
  } else {
1629
- lines.push(
1630
- formatTable(state.lastData, {
1631
- width: state.termWidth,
1632
- sortBy: state.sortMode,
1633
- filter: state.processFilter || void 0,
1634
- processLimit: processRowBudget(),
1635
- theme: state.currentTheme,
1636
- visible: state.visiblePanels,
1637
- treeView: state.treeView,
1638
- activePanel: state.activePanel
1639
- })
1640
- );
1641
- }
1801
+ bodyLines.push(...tableStr.split("\n"));
1802
+ }
1803
+ const graphsParams = graphsCacheParams();
1804
+ const graphsStr = state.history.version !== _cachedGraphsVersion || graphsParams !== _cachedGraphsParams ? (() => {
1805
+ const out = formatGraphs(state.history, state.termWidth, state.currentTheme, state.graphMode);
1806
+ _cachedGraphsVersion = state.history.version;
1807
+ _cachedGraphsParams = graphsParams;
1808
+ _cachedGraphs = out;
1809
+ return out;
1810
+ })() : _cachedGraphs;
1642
1811
  if (state.showGraphs && state.activePanel !== "p2p") {
1643
- lines.push("");
1644
- lines.push(formatGraphs(state.history, state.termWidth, state.currentTheme, state.graphMode));
1812
+ bodyLines.push("");
1813
+ bodyLines.push(...graphsStr.split("\n"));
1645
1814
  }
1646
- lines.push("");
1647
- lines.push(footerLine());
1815
+ const footerLines = [];
1816
+ footerLines.push("");
1817
+ footerLines.push(...footerLine().split("\n"));
1648
1818
  if (state.inputMode === "customizer") {
1649
- lines.push(renderCustomizerOverlay());
1819
+ footerLines.push(...renderCustomizerOverlay().split("\n"));
1650
1820
  } else if (state.inputMode === "filter") {
1651
- lines.push(chalk5.cyan(` Filter processes: ${state.inputBuffer}_`));
1821
+ footerLines.push(chalk5.cyan(` Filter processes: ${state.inputBuffer}_`));
1652
1822
  } else if (state.inputMode === "kill") {
1653
- lines.push(chalk5.cyan(` Kill PID: ${state.inputBuffer}_ (enter to confirm, esc to cancel)`));
1823
+ footerLines.push(chalk5.cyan(` Kill PID: ${state.inputBuffer}_ (enter to confirm, esc to cancel)`));
1654
1824
  } else if (state.inputMode === "signal") {
1655
1825
  const sigIdx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer);
1656
1826
  const sigList = state.SIGNAL_OPTIONS.map((s, i) => i === sigIdx ? chalk5.yellow(s) : chalk5.dim(s)).join(" ");
1657
- lines.push(chalk5.cyan(` Signal: ${state.inputBuffer}_`));
1658
- lines.push(chalk5.dim(` ${sigList}`));
1659
- lines.push(chalk5.dim(" \u2191/\u2193 to select, Enter to confirm, Esc to cancel"));
1827
+ footerLines.push(chalk5.cyan(` Signal: ${state.inputBuffer}_`));
1828
+ footerLines.push(chalk5.dim(` ${sigList}`));
1829
+ footerLines.push(chalk5.dim(" \u2191/\u2193 to select, Enter to confirm, Esc to cancel"));
1660
1830
  } else if (state.inputMode === "p2p") {
1661
- lines.push(chalk5.cyan(` P2P password (${state.p2pPort}): ${state.inputBuffer}_ (enter to start, esc to cancel)`));
1831
+ footerLines.push(chalk5.cyan(` P2P password (${state.p2pPort}): ${state.inputBuffer}_ (enter to start, esc to cancel)`));
1662
1832
  } else if (state.statusMessage) {
1663
- lines.push(` ${state.statusMessage}`);
1833
+ footerLines.push(` ${state.statusMessage}`);
1834
+ } else {
1835
+ footerLines.push("");
1664
1836
  }
1665
- process.stdout.write("\x1B[2J\x1B[H");
1666
- process.stdout.write(lines.join("\n"));
1837
+ const maxBodyLines = Math.max(1, state.termHeight - footerLines.length);
1838
+ if (bodyLines.length > maxBodyLines) {
1839
+ bodyLines.length = maxBodyLines;
1840
+ }
1841
+ const frameLines = [...bodyLines, ...footerLines];
1842
+ if (frameLines.length > state.termHeight) {
1843
+ frameLines.length = state.termHeight;
1844
+ }
1845
+ writeFrame(frameLines);
1667
1846
  }
1668
1847
  function footerLine() {
1669
- const keys = [
1848
+ const row1 = [
1670
1849
  ["q", "quit"],
1671
1850
  ["c", "customize UI"],
1672
- ["r", state.p2pServerRunning ? "stop p2p" : "start p2p"],
1673
- ["p", state.paused ? "resume" : "pause"],
1851
+ ["\u2190/\u2192", "tab"],
1852
+ ["1-9", "panels"],
1674
1853
  ["g", state.showGraphs ? "hide graphs" : "show graphs"],
1675
1854
  ["b", `graph:${state.graphMode}`],
1855
+ ["t", state.treeView ? "flat" : "tree"],
1676
1856
  ["d", state.detailed ? "basic" : "detailed"],
1677
- ["\u2190/\u2192", "tab"],
1678
- ["1-9", "panels"],
1857
+ ["p", state.paused ? "resume" : "pause"]
1858
+ ];
1859
+ const row2 = [
1679
1860
  ["s", `sort:${state.sortMode}`],
1680
1861
  ["/", "filter"],
1681
1862
  ["k", "kill"],
1682
1863
  ["S", "signal"],
1683
- ["t", state.treeView ? "flat" : "tree"],
1684
1864
  ["e", "export"],
1685
1865
  ["l", state.logging ? "stop log" : "log"],
1686
1866
  ["f", state.exportFormat],
1687
- ["+/-", `${state.interval}s`]
1867
+ ["+/-", `${state.interval}s`],
1868
+ ["r", state.p2pServerRunning ? "stop p2p" : "start p2p"]
1688
1869
  ];
1689
- const keyStr = keys.map(([k, label]) => `${chalk5.hex("#50fa7b").bold(k)} ${chalk5.dim(label)}`).join(chalk5.dim(" | "));
1870
+ const fmt = (list) => list.map(([k, label]) => `${chalk5.hex("#50fa7b").bold(k)} ${chalk5.dim(label)}`).join(chalk5.dim(" | "));
1871
+ const str1 = fmt(row1);
1872
+ const str2 = fmt(row2);
1690
1873
  const badges = [];
1691
1874
  if (state.paused) badges.push(chalk5.yellow.bold("\u23F8 PAUSED"));
1692
1875
  if (state.logging) badges.push(chalk5.red.bold("\u25CF REC"));
1693
1876
  if (state.activePanel !== "grid") badges.push(chalk5.cyan.bold(`\u25C9 ${state.activePanel.toUpperCase()}`));
1694
1877
  if (state.p2pServerRunning) badges.push(chalk5.green.bold(`P2P ${state.p2pBind || "0.0.0.0"}:${state.p2pPort}`));
1695
1878
  const badgeStr = badges.join(" ");
1696
- return badgeStr ? `${keyStr} ${badgeStr}` : keyStr;
1879
+ return badgeStr ? `${str1}
1880
+ ${str2} ${badgeStr}` : `${str1}
1881
+ ${str2}`;
1697
1882
  }
1698
1883
  function checkAlerts(data) {
1699
1884
  const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
@@ -2320,6 +2505,8 @@ var P2PClient = class {
2320
2505
  this.status("Connection refused \u2014 is the server running?");
2321
2506
  } else if (code === "ETIMEDOUT") {
2322
2507
  this.status("Connection timed out");
2508
+ } else if (code === "EHOSTDOWN" || code === "EHOSTUNREACH") {
2509
+ this.status(`Host unreachable \u2014 ${err.message}`);
2323
2510
  } else if (code === "UNABLE_TO_VERIFY_LEAF_SIGNATURE") {
2324
2511
  this.status("TLS certificate verification failed \u2014 use --p2p-insecure to skip");
2325
2512
  } else {
@@ -2348,8 +2535,20 @@ function deriveHMACKey2(password) {
2348
2535
  }
2349
2536
 
2350
2537
  // src/live/input.ts
2538
+ function isUpKey(key, str) {
2539
+ return key.name === "up" || str === "k" || key.sequence === "\x1B[A" || key.sequence === "\x1BOA";
2540
+ }
2541
+ function isDownKey(key, str) {
2542
+ return key.name === "down" || str === "j" || key.sequence === "\x1B[B" || key.sequence === "\x1BOB";
2543
+ }
2544
+ function isEnterKey(key, str) {
2545
+ return key.name === "return" || key.name === "enter" || key.sequence === "\r" || key.sequence === "\n" || str === "\r" || str === "\n";
2546
+ }
2547
+ function isEscKey(key, str) {
2548
+ return key.name === "escape" || key.name === "esc" || key.sequence === "\x1B" || str === "\x1B";
2549
+ }
2351
2550
  function handleInputModeKey(str, key) {
2352
- if (key.name === "escape") {
2551
+ if (isEscKey(key, str)) {
2353
2552
  state.inputMode = null;
2354
2553
  state.inputBuffer = "";
2355
2554
  render();
@@ -2357,38 +2556,55 @@ function handleInputModeKey(str, key) {
2357
2556
  }
2358
2557
  if (state.inputMode === "customizer") {
2359
2558
  const themesList = Object.keys(THEMES);
2360
- if (key.name === "up" || str === "k") {
2559
+ if (isUpKey(key, str)) {
2361
2560
  state.customizerIndex = (state.customizerIndex - 1 + state.CUSTOMIZER_OPTIONS.length) % state.CUSTOMIZER_OPTIONS.length;
2362
- } else if (key.name === "down" || str === "j") {
2561
+ } else if (isDownKey(key, str)) {
2363
2562
  state.customizerIndex = (state.customizerIndex + 1) % state.CUSTOMIZER_OPTIONS.length;
2364
- } else if (key.name === "return" || str === " ") {
2563
+ } else if (isEnterKey(key, str) || str === " ") {
2365
2564
  const selected = state.CUSTOMIZER_OPTIONS[state.customizerIndex];
2366
2565
  if (selected === "Theme") {
2367
2566
  const nextIdx = (themesList.indexOf(state.currentTheme) + 1) % themesList.length;
2368
2567
  state.currentTheme = themesList[nextIdx];
2369
- } else if (selected === "Toggle Tree View") {
2370
- state.treeView = !state.treeView;
2371
2568
  } else if (selected === "Graph Mode") {
2372
2569
  state.graphMode = state.graphMode === "spark" ? "bar" : "spark";
2373
2570
  } else {
2374
- const itemKey = selected.replace("Toggle ", "").toLowerCase();
2375
- state.visiblePanels[itemKey] = !state.visiblePanels[itemKey];
2571
+ const toggleKey = getToggleKey(selected);
2572
+ if (toggleKey) {
2573
+ if (toggleKey === "tree") {
2574
+ state.treeView = !state.treeView;
2575
+ state.visiblePanels.tree = state.treeView;
2576
+ } else {
2577
+ const isVisible = state.visiblePanels[toggleKey] !== false;
2578
+ state.visiblePanels[toggleKey] = !isVisible;
2579
+ }
2580
+ }
2376
2581
  }
2582
+ invalidateTableCache();
2583
+ invalidateFrame();
2377
2584
  }
2378
2585
  render();
2379
2586
  return;
2380
2587
  }
2381
2588
  if (state.inputMode === "signal") {
2382
- if (key.name === "up" || str === "k") {
2589
+ if (isUpKey(key, str)) {
2383
2590
  const idx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer);
2384
2591
  state.inputBuffer = state.SIGNAL_OPTIONS[(idx - 1 + state.SIGNAL_OPTIONS.length) % state.SIGNAL_OPTIONS.length];
2385
- } else if (key.name === "down" || str === "j") {
2592
+ state.selectedSignal = state.inputBuffer;
2593
+ } else if (isDownKey(key, str)) {
2386
2594
  const idx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer);
2387
2595
  state.inputBuffer = state.SIGNAL_OPTIONS[(idx + 1) % state.SIGNAL_OPTIONS.length];
2388
- } else if (key.name === "return" || str === " ") {
2389
- killProcess(state.inputBuffer.trim(), state.inputBuffer.trim());
2390
- state.inputMode = null;
2391
- state.inputBuffer = "";
2596
+ state.selectedSignal = state.inputBuffer;
2597
+ } else if (isEnterKey(key, str) || str === " ") {
2598
+ state.selectedSignal = state.inputBuffer.trim();
2599
+ if (state.targetPid) {
2600
+ killProcess(state.targetPid, state.selectedSignal);
2601
+ state.inputMode = null;
2602
+ state.inputBuffer = "";
2603
+ } else {
2604
+ state.inputMode = "kill";
2605
+ state.inputBuffer = "";
2606
+ setStatus(`Signal ${state.selectedSignal} selected. Enter target PID to kill:`, 5e3);
2607
+ }
2392
2608
  render();
2393
2609
  return;
2394
2610
  }
@@ -2396,13 +2612,13 @@ function handleInputModeKey(str, key) {
2396
2612
  return;
2397
2613
  }
2398
2614
  if (state.inputMode === "p2p") {
2399
- if (key.name === "escape") {
2615
+ if (isEscKey(key, str)) {
2400
2616
  state.inputMode = null;
2401
2617
  state.inputBuffer = "";
2402
2618
  render();
2403
2619
  return;
2404
2620
  }
2405
- if (key.name === "return") {
2621
+ if (isEnterKey(key, str)) {
2406
2622
  const password = state.inputBuffer.trim();
2407
2623
  if (password) {
2408
2624
  state.p2pPassword = password;
@@ -2424,12 +2640,14 @@ function handleInputModeKey(str, key) {
2424
2640
  }
2425
2641
  return;
2426
2642
  }
2427
- if (key.name === "return") {
2643
+ if (isEnterKey(key, str)) {
2428
2644
  if (state.inputMode === "filter") {
2429
2645
  state.processFilter = state.inputBuffer.trim();
2430
2646
  setStatus(state.processFilter ? `Filtering: "${state.processFilter}"` : "Filter cleared");
2431
2647
  } else if (state.inputMode === "kill") {
2432
- killProcess(state.inputBuffer.trim());
2648
+ const pidStr = state.inputBuffer.trim();
2649
+ state.targetPid = pidStr;
2650
+ killProcess(pidStr, state.selectedSignal);
2433
2651
  }
2434
2652
  state.inputMode = null;
2435
2653
  state.inputBuffer = "";
@@ -2509,6 +2727,8 @@ function onResize() {
2509
2727
  state.termWidth = process.stdout.columns || 80;
2510
2728
  state.termHeight = process.stdout.rows || 24;
2511
2729
  state.history.setMaxLen(Math.max(20, Math.min(200, state.termWidth - 30)));
2730
+ invalidateFrame();
2731
+ process.stdout.write("\x1B[2J\x1B[H");
2512
2732
  render();
2513
2733
  }
2514
2734
  function restartTicker() {
@@ -2517,8 +2737,9 @@ function restartTicker() {
2517
2737
  }
2518
2738
  async function doWarmup() {
2519
2739
  try {
2520
- const data = await collectAll({ detailed: state.detailed, processLimit: processRowBudget() });
2740
+ const data = await collectAll({ detailed: state.detailed });
2521
2741
  state.lastData = data;
2742
+ invalidateTableCache();
2522
2743
  const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
2523
2744
  state.history.push({
2524
2745
  cpuUsage: data.cpu.usage,
@@ -2541,8 +2762,9 @@ async function doWarmup() {
2541
2762
  async function tick() {
2542
2763
  if (state.paused) return;
2543
2764
  try {
2544
- const data = await collectAll({ detailed: state.detailed, processLimit: processRowBudget() });
2765
+ const data = await collectAll({ detailed: state.detailed });
2545
2766
  state.lastData = data;
2767
+ invalidateTableCache();
2546
2768
  const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
2547
2769
  state.history.push({
2548
2770
  cpuUsage: data.cpu.usage,
@@ -2594,6 +2816,10 @@ async function startLive(opts2, splashPromise) {
2594
2816
  if (process.stdin.isTTY) process.stdin.setRawMode(true);
2595
2817
  process.stdin.resume();
2596
2818
  process.stdout.on("resize", onResize);
2819
+ if (state.uiIntervalHandle) clearInterval(state.uiIntervalHandle);
2820
+ state.uiIntervalHandle = setInterval(() => {
2821
+ if (state.running && !state.paused) render();
2822
+ }, 250);
2597
2823
  state.keypressHandler = (str, key) => {
2598
2824
  if (!key) return;
2599
2825
  if (key.ctrl && key.name === "c") {
@@ -2684,28 +2910,12 @@ async function startLive(opts2, splashPromise) {
2684
2910
  case "q":
2685
2911
  stopLive();
2686
2912
  break;
2687
- case "left": {
2688
- const currentIdx = state.PANEL_TABS.findIndex((t) => t.id === state.activePanel);
2689
- if (currentIdx > 0) {
2690
- state.activePanel = state.PANEL_TABS[currentIdx - 1].id;
2691
- } else if (currentIdx === -1) {
2692
- state.activePanel = state.PANEL_TABS[state.PANEL_TABS.length - 1].id;
2693
- }
2694
- setStatus(`Panel: ${state.activePanel}`);
2695
- render();
2913
+ case "left":
2914
+ cycleTab(-1);
2696
2915
  break;
2697
- }
2698
- case "right": {
2699
- const currentIdx = state.PANEL_TABS.findIndex((t) => t.id === state.activePanel);
2700
- if (currentIdx < state.PANEL_TABS.length - 1) {
2701
- state.activePanel = state.PANEL_TABS[currentIdx + 1].id;
2702
- } else if (currentIdx === -1) {
2703
- state.activePanel = state.PANEL_TABS[0].id;
2704
- }
2705
- setStatus(`Panel: ${state.activePanel}`);
2706
- render();
2916
+ case "right":
2917
+ cycleTab(1);
2707
2918
  break;
2708
- }
2709
2919
  case "c":
2710
2920
  state.inputMode = "customizer";
2711
2921
  state.customizerIndex = 0;
@@ -2767,12 +2977,6 @@ async function startLive(opts2, splashPromise) {
2767
2977
  case "tab":
2768
2978
  cycleTab(1);
2769
2979
  break;
2770
- case "left":
2771
- cycleTab(-1);
2772
- break;
2773
- case "right":
2774
- cycleTab(1);
2775
- break;
2776
2980
  default:
2777
2981
  if (str && str.length === 1) {
2778
2982
  const tabId = tabKeyToId(str);
@@ -2812,8 +3016,8 @@ function handleSGRMouse(seq) {
2812
3016
  const match = seq.match(/^\x1b\[<(\d+);(\d+);(\d+)([Mm])/);
2813
3017
  if (!match) return;
2814
3018
  const button = parseInt(match[1]);
2815
- const cx = parseInt(match[2]);
2816
- const cy = parseInt(match[3]);
3019
+ const cx = parseInt(match[2]) - 1;
3020
+ const cy = parseInt(match[3]) - 1;
2817
3021
  if (button === 0 || button === 1) {
2818
3022
  handleMouseClick(cy, cx);
2819
3023
  }
@@ -2833,6 +3037,10 @@ function stopLive() {
2833
3037
  clearInterval(state.intervalHandle);
2834
3038
  state.intervalHandle = null;
2835
3039
  }
3040
+ if (state.uiIntervalHandle) {
3041
+ clearInterval(state.uiIntervalHandle);
3042
+ state.uiIntervalHandle = null;
3043
+ }
2836
3044
  if (state.statusTimer) {
2837
3045
  clearTimeout(state.statusTimer);
2838
3046
  state.statusTimer = null;
@@ -3155,7 +3363,26 @@ async function runP2PConnect() {
3155
3363
  try {
3156
3364
  await client.connect();
3157
3365
  } catch (err) {
3158
- console.log(chalk9.red(`Connection failed: ${err.message}`));
3366
+ const code = err.code;
3367
+ const msg = err.message || String(err);
3368
+ console.log(chalk9.red(`Connection failed: ${msg}`));
3369
+ if (code === "ECONNREFUSED") {
3370
+ console.log(chalk9.yellow(" Hints:"));
3371
+ console.log(chalk9.dim(" 1. Start the server: pyre p2p server --p2p-host 0.0.0.0 --p2p-port " + port + " --p2p-password <password>"));
3372
+ console.log(chalk9.dim(" 2. Check the server IP with: hostname -I"));
3373
+ console.log(chalk9.dim(" 3. Open the port on the server firewall: sudo ufw allow " + port + "/tcp"));
3374
+ } else if (code === "ETIMEDOUT") {
3375
+ console.log(chalk9.yellow(" Hints:"));
3376
+ console.log(chalk9.dim(" 1. Verify the server IP is correct"));
3377
+ console.log(chalk9.dim(" 2. Check that both machines are on the same network/subnet"));
3378
+ console.log(chalk9.dim(" 3. Open the port on the server firewall"));
3379
+ } else if (code === "EHOSTDOWN" || code === "EHOSTUNREACH") {
3380
+ console.log(chalk9.yellow(" Hints:"));
3381
+ console.log(chalk9.dim(" 1. The server at " + host + " is not responding"));
3382
+ console.log(chalk9.dim(" 2. Start the server first: pyre p2p server --p2p-host 0.0.0.0 --p2p-port " + port + " --p2p-password <password>"));
3383
+ console.log(chalk9.dim(" 3. Check connectivity: ping " + host));
3384
+ console.log(chalk9.dim(" 4. Ensure both machines are on the same LAN (ping each other first)"));
3385
+ }
3159
3386
  process.exit(1);
3160
3387
  }
3161
3388
  process.once("SIGINT", () => {