pyre-cli 3.4.0 → 3.5.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;
@@ -2348,8 +2533,20 @@ function deriveHMACKey2(password) {
2348
2533
  }
2349
2534
 
2350
2535
  // src/live/input.ts
2536
+ function isUpKey(key, str) {
2537
+ return key.name === "up" || str === "k" || key.sequence === "\x1B[A" || key.sequence === "\x1BOA";
2538
+ }
2539
+ function isDownKey(key, str) {
2540
+ return key.name === "down" || str === "j" || key.sequence === "\x1B[B" || key.sequence === "\x1BOB";
2541
+ }
2542
+ function isEnterKey(key, str) {
2543
+ return key.name === "return" || key.name === "enter" || key.sequence === "\r" || key.sequence === "\n" || str === "\r" || str === "\n";
2544
+ }
2545
+ function isEscKey(key, str) {
2546
+ return key.name === "escape" || key.name === "esc" || key.sequence === "\x1B" || str === "\x1B";
2547
+ }
2351
2548
  function handleInputModeKey(str, key) {
2352
- if (key.name === "escape") {
2549
+ if (isEscKey(key, str)) {
2353
2550
  state.inputMode = null;
2354
2551
  state.inputBuffer = "";
2355
2552
  render();
@@ -2357,38 +2554,55 @@ function handleInputModeKey(str, key) {
2357
2554
  }
2358
2555
  if (state.inputMode === "customizer") {
2359
2556
  const themesList = Object.keys(THEMES);
2360
- if (key.name === "up" || str === "k") {
2557
+ if (isUpKey(key, str)) {
2361
2558
  state.customizerIndex = (state.customizerIndex - 1 + state.CUSTOMIZER_OPTIONS.length) % state.CUSTOMIZER_OPTIONS.length;
2362
- } else if (key.name === "down" || str === "j") {
2559
+ } else if (isDownKey(key, str)) {
2363
2560
  state.customizerIndex = (state.customizerIndex + 1) % state.CUSTOMIZER_OPTIONS.length;
2364
- } else if (key.name === "return" || str === " ") {
2561
+ } else if (isEnterKey(key, str) || str === " ") {
2365
2562
  const selected = state.CUSTOMIZER_OPTIONS[state.customizerIndex];
2366
2563
  if (selected === "Theme") {
2367
2564
  const nextIdx = (themesList.indexOf(state.currentTheme) + 1) % themesList.length;
2368
2565
  state.currentTheme = themesList[nextIdx];
2369
- } else if (selected === "Toggle Tree View") {
2370
- state.treeView = !state.treeView;
2371
2566
  } else if (selected === "Graph Mode") {
2372
2567
  state.graphMode = state.graphMode === "spark" ? "bar" : "spark";
2373
2568
  } else {
2374
- const itemKey = selected.replace("Toggle ", "").toLowerCase();
2375
- state.visiblePanels[itemKey] = !state.visiblePanels[itemKey];
2569
+ const toggleKey = getToggleKey(selected);
2570
+ if (toggleKey) {
2571
+ if (toggleKey === "tree") {
2572
+ state.treeView = !state.treeView;
2573
+ state.visiblePanels.tree = state.treeView;
2574
+ } else {
2575
+ const isVisible = state.visiblePanels[toggleKey] !== false;
2576
+ state.visiblePanels[toggleKey] = !isVisible;
2577
+ }
2578
+ }
2376
2579
  }
2580
+ invalidateTableCache();
2581
+ invalidateFrame();
2377
2582
  }
2378
2583
  render();
2379
2584
  return;
2380
2585
  }
2381
2586
  if (state.inputMode === "signal") {
2382
- if (key.name === "up" || str === "k") {
2587
+ if (isUpKey(key, str)) {
2383
2588
  const idx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer);
2384
2589
  state.inputBuffer = state.SIGNAL_OPTIONS[(idx - 1 + state.SIGNAL_OPTIONS.length) % state.SIGNAL_OPTIONS.length];
2385
- } else if (key.name === "down" || str === "j") {
2590
+ state.selectedSignal = state.inputBuffer;
2591
+ } else if (isDownKey(key, str)) {
2386
2592
  const idx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer);
2387
2593
  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 = "";
2594
+ state.selectedSignal = state.inputBuffer;
2595
+ } else if (isEnterKey(key, str) || str === " ") {
2596
+ state.selectedSignal = state.inputBuffer.trim();
2597
+ if (state.targetPid) {
2598
+ killProcess(state.targetPid, state.selectedSignal);
2599
+ state.inputMode = null;
2600
+ state.inputBuffer = "";
2601
+ } else {
2602
+ state.inputMode = "kill";
2603
+ state.inputBuffer = "";
2604
+ setStatus(`Signal ${state.selectedSignal} selected. Enter target PID to kill:`, 5e3);
2605
+ }
2392
2606
  render();
2393
2607
  return;
2394
2608
  }
@@ -2396,13 +2610,13 @@ function handleInputModeKey(str, key) {
2396
2610
  return;
2397
2611
  }
2398
2612
  if (state.inputMode === "p2p") {
2399
- if (key.name === "escape") {
2613
+ if (isEscKey(key, str)) {
2400
2614
  state.inputMode = null;
2401
2615
  state.inputBuffer = "";
2402
2616
  render();
2403
2617
  return;
2404
2618
  }
2405
- if (key.name === "return") {
2619
+ if (isEnterKey(key, str)) {
2406
2620
  const password = state.inputBuffer.trim();
2407
2621
  if (password) {
2408
2622
  state.p2pPassword = password;
@@ -2424,12 +2638,14 @@ function handleInputModeKey(str, key) {
2424
2638
  }
2425
2639
  return;
2426
2640
  }
2427
- if (key.name === "return") {
2641
+ if (isEnterKey(key, str)) {
2428
2642
  if (state.inputMode === "filter") {
2429
2643
  state.processFilter = state.inputBuffer.trim();
2430
2644
  setStatus(state.processFilter ? `Filtering: "${state.processFilter}"` : "Filter cleared");
2431
2645
  } else if (state.inputMode === "kill") {
2432
- killProcess(state.inputBuffer.trim());
2646
+ const pidStr = state.inputBuffer.trim();
2647
+ state.targetPid = pidStr;
2648
+ killProcess(pidStr, state.selectedSignal);
2433
2649
  }
2434
2650
  state.inputMode = null;
2435
2651
  state.inputBuffer = "";
@@ -2509,6 +2725,8 @@ function onResize() {
2509
2725
  state.termWidth = process.stdout.columns || 80;
2510
2726
  state.termHeight = process.stdout.rows || 24;
2511
2727
  state.history.setMaxLen(Math.max(20, Math.min(200, state.termWidth - 30)));
2728
+ invalidateFrame();
2729
+ process.stdout.write("\x1B[2J\x1B[H");
2512
2730
  render();
2513
2731
  }
2514
2732
  function restartTicker() {
@@ -2517,8 +2735,9 @@ function restartTicker() {
2517
2735
  }
2518
2736
  async function doWarmup() {
2519
2737
  try {
2520
- const data = await collectAll({ detailed: state.detailed, processLimit: processRowBudget() });
2738
+ const data = await collectAll({ detailed: state.detailed });
2521
2739
  state.lastData = data;
2740
+ invalidateTableCache();
2522
2741
  const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
2523
2742
  state.history.push({
2524
2743
  cpuUsage: data.cpu.usage,
@@ -2541,8 +2760,9 @@ async function doWarmup() {
2541
2760
  async function tick() {
2542
2761
  if (state.paused) return;
2543
2762
  try {
2544
- const data = await collectAll({ detailed: state.detailed, processLimit: processRowBudget() });
2763
+ const data = await collectAll({ detailed: state.detailed });
2545
2764
  state.lastData = data;
2765
+ invalidateTableCache();
2546
2766
  const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
2547
2767
  state.history.push({
2548
2768
  cpuUsage: data.cpu.usage,
@@ -2594,6 +2814,10 @@ async function startLive(opts2, splashPromise) {
2594
2814
  if (process.stdin.isTTY) process.stdin.setRawMode(true);
2595
2815
  process.stdin.resume();
2596
2816
  process.stdout.on("resize", onResize);
2817
+ if (state.uiIntervalHandle) clearInterval(state.uiIntervalHandle);
2818
+ state.uiIntervalHandle = setInterval(() => {
2819
+ if (state.running && !state.paused) render();
2820
+ }, 250);
2597
2821
  state.keypressHandler = (str, key) => {
2598
2822
  if (!key) return;
2599
2823
  if (key.ctrl && key.name === "c") {
@@ -2684,28 +2908,12 @@ async function startLive(opts2, splashPromise) {
2684
2908
  case "q":
2685
2909
  stopLive();
2686
2910
  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();
2911
+ case "left":
2912
+ cycleTab(-1);
2696
2913
  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();
2914
+ case "right":
2915
+ cycleTab(1);
2707
2916
  break;
2708
- }
2709
2917
  case "c":
2710
2918
  state.inputMode = "customizer";
2711
2919
  state.customizerIndex = 0;
@@ -2767,12 +2975,6 @@ async function startLive(opts2, splashPromise) {
2767
2975
  case "tab":
2768
2976
  cycleTab(1);
2769
2977
  break;
2770
- case "left":
2771
- cycleTab(-1);
2772
- break;
2773
- case "right":
2774
- cycleTab(1);
2775
- break;
2776
2978
  default:
2777
2979
  if (str && str.length === 1) {
2778
2980
  const tabId = tabKeyToId(str);
@@ -2812,8 +3014,8 @@ function handleSGRMouse(seq) {
2812
3014
  const match = seq.match(/^\x1b\[<(\d+);(\d+);(\d+)([Mm])/);
2813
3015
  if (!match) return;
2814
3016
  const button = parseInt(match[1]);
2815
- const cx = parseInt(match[2]);
2816
- const cy = parseInt(match[3]);
3017
+ const cx = parseInt(match[2]) - 1;
3018
+ const cy = parseInt(match[3]) - 1;
2817
3019
  if (button === 0 || button === 1) {
2818
3020
  handleMouseClick(cy, cx);
2819
3021
  }
@@ -2833,6 +3035,10 @@ function stopLive() {
2833
3035
  clearInterval(state.intervalHandle);
2834
3036
  state.intervalHandle = null;
2835
3037
  }
3038
+ if (state.uiIntervalHandle) {
3039
+ clearInterval(state.uiIntervalHandle);
3040
+ state.uiIntervalHandle = null;
3041
+ }
2836
3042
  if (state.statusTimer) {
2837
3043
  clearTimeout(state.statusTimer);
2838
3044
  state.statusTimer = null;