pyre-cli 1.3.0 → 1.4.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
@@ -3,10 +3,12 @@
3
3
  // src/index.ts
4
4
  import fs2 from "fs";
5
5
  import { Command } from "commander";
6
- import chalk4 from "chalk";
6
+ import chalk7 from "chalk";
7
7
 
8
- // src/monitors.ts
8
+ // src/monitors/collectors.ts
9
9
  import chalk from "chalk";
10
+
11
+ // src/monitors/run.ts
10
12
  async function run(cmd, fallback = "") {
11
13
  try {
12
14
  const { execSync } = await import("child_process");
@@ -15,6 +17,8 @@ async function run(cmd, fallback = "") {
15
17
  return fallback;
16
18
  }
17
19
  }
20
+
21
+ // src/monitors/smc.ts
18
22
  var smcCache = null;
19
23
  async function getSmcMetrics() {
20
24
  const now = Date.now();
@@ -53,9 +57,12 @@ async function getSmcMetrics() {
53
57
  function parseSuffix(s) {
54
58
  return s.toUpperCase() === "K" ? 1024 : s.toUpperCase() === "M" ? 1024 * 1024 : s.toUpperCase() === "G" ? 1024 * 1024 * 1024 : 1;
55
59
  }
60
+
61
+ // src/monitors/collectors.ts
56
62
  async function collectAll(opts2 = {}) {
57
- const [cpu, memory, disk, battery, thermal, network, processes, system, power] = await Promise.all([
63
+ const [cpu, gpu, memory, disk, battery, thermal, network, processes, system, power] = await Promise.all([
58
64
  collectCpu(),
65
+ collectGpu(opts2.detailed),
59
66
  collectMemory(),
60
67
  collectDisk(),
61
68
  collectBattery(),
@@ -73,6 +80,7 @@ async function collectAll(opts2 = {}) {
73
80
  uptime: system.uptime
74
81
  },
75
82
  cpu,
83
+ gpu,
76
84
  memory,
77
85
  disk,
78
86
  battery,
@@ -92,6 +100,48 @@ async function collectPower() {
92
100
  return null;
93
101
  }
94
102
  }
103
+ async function collectGpu(detailed) {
104
+ try {
105
+ const spRaw = (await run("system_profiler SPDisplaysDataType 2>/dev/null")).trim();
106
+ if (!spRaw) return null;
107
+ const modelMatch = spRaw.match(/Chipset Model:\s*(.+)/i);
108
+ const model = modelMatch ? modelMatch[1].trim() : "Unknown";
109
+ const memMatch = spRaw.match(/VRAM \(Total\):\s*(.+)/i);
110
+ let memory = 0;
111
+ if (memMatch) {
112
+ const memStr = memMatch[1].trim();
113
+ const memNum = parseFloat(memStr);
114
+ if (memStr.includes("GB")) memory = memNum * 1024 * 1024 * 1024;
115
+ else if (memStr.includes("MB")) memory = memNum * 1024 * 1024;
116
+ }
117
+ let utilization = 0;
118
+ let temperature;
119
+ let gpuProcesses = 0;
120
+ if (detailed) {
121
+ try {
122
+ const pm = (await run("sudo -n powermetrics --samplers gpu_power -n 1 --format text 2>/dev/null", "")).trim();
123
+ const gpuPowerMatch = pm.match(/GPU Power:\s*([\d.]+)\s*mW/i);
124
+ if (gpuPowerMatch) {
125
+ utilization = Math.min(100, Math.round(parseFloat(gpuPowerMatch[1]) / 10));
126
+ }
127
+ } catch {
128
+ }
129
+ try {
130
+ const { temps } = await getSmcMetrics();
131
+ if (temps.gpu_die !== void 0) temperature = temps.gpu_die;
132
+ } catch {
133
+ }
134
+ try {
135
+ const psRaw = (await run('ps aux | grep -i "[g]pu" | wc -l')).trim();
136
+ gpuProcesses = parseInt(psRaw) || 0;
137
+ } catch {
138
+ }
139
+ }
140
+ return { model, memory, utilization, temperature, processes: gpuProcesses };
141
+ } catch {
142
+ return null;
143
+ }
144
+ }
95
145
  async function collectSystem() {
96
146
  const hostname = (await run("hostname", "Mac")).trim();
97
147
  const osRaw = (await run("sw_vers -productVersion", "Unknown")).trim();
@@ -113,7 +163,9 @@ async function collectCpu() {
113
163
  try {
114
164
  const { freq } = await getSmcMetrics();
115
165
  const values = Object.values(freq);
116
- if (values.length) frequency = Math.round(Math.max(...values));
166
+ if (values.length) {
167
+ frequency = Math.round(Math.max(...values) / 1e6);
168
+ }
117
169
  } catch {
118
170
  }
119
171
  }
@@ -203,7 +255,7 @@ async function collectBattery() {
203
255
  const timeMatch = raw.match(/:\s*([\d:]+)\s+remaining/i);
204
256
  const powerPlugged = raw.includes("AC Power") || raw.includes("attached to charger");
205
257
  const level = levelMatch ? parseInt(levelMatch[1]) : 0;
206
- const state = powerPlugged ? "charged" : stateMatch ? stateMatch[1].toLowerCase() : "discharging";
258
+ const state2 = powerPlugged ? "charged" : stateMatch ? stateMatch[1].toLowerCase() : "discharging";
207
259
  const timeRemaining = timeMatch ? timeMatch[1] : powerPlugged ? "\u221E" : "calculating";
208
260
  let cycles;
209
261
  let condition;
@@ -235,7 +287,7 @@ async function collectBattery() {
235
287
  const health = healthParts.length ? healthParts.join(", ") : "unknown";
236
288
  return {
237
289
  level,
238
- state,
290
+ state: state2,
239
291
  timeRemaining,
240
292
  health,
241
293
  powerSource: powerPlugged ? "AC" : "Battery",
@@ -247,36 +299,36 @@ async function collectBattery() {
247
299
  return null;
248
300
  }
249
301
  }
250
- function pressureFromState(state) {
251
- const s = state.toLowerCase();
302
+ function pressureFromState(state2) {
303
+ const s = state2.toLowerCase();
252
304
  if (s.includes("critical")) return 3;
253
305
  if (s.includes("serious") || s.includes("heavy")) return 2;
254
306
  if (s.includes("fair") || s.includes("moderate")) return 1;
255
307
  return 0;
256
308
  }
257
- async function collectThermal(detailed2) {
309
+ async function collectThermal(detailed) {
258
310
  try {
259
311
  const therm = (await run("pmset -g therm", "")).trim();
260
- let state = "Unknown";
312
+ let state2 = "Unknown";
261
313
  let detail = therm || "No pmset thermal data";
262
314
  const stateMatch = therm.match(/Thermal state:\s*(.+)/i);
263
315
  if (stateMatch) {
264
316
  const raw = stateMatch[1].trim();
265
- state = /0|no warning|normal/i.test(raw) ? "Nominal" : raw;
266
- detail = state;
317
+ state2 = /0|no warning|normal/i.test(raw) ? "Nominal" : raw;
318
+ detail = state2;
267
319
  } else {
268
320
  const limitMatch = therm.match(
269
321
  /CPU_(?:Speed|Scheduler)_Limit\s*(?:=|:)\s*([\d.]+)/i
270
322
  );
271
323
  if (limitMatch) {
272
324
  const limit = parseFloat(limitMatch[1]);
273
- if (limit >= 100) state = "Nominal";
274
- else if (limit >= 85) state = "Fair";
275
- else if (limit >= 50) state = "Serious";
276
- else state = "Critical";
277
- detail = `${state} (CPU limit ${limit}%)`;
325
+ if (limit >= 100) state2 = "Nominal";
326
+ else if (limit >= 85) state2 = "Fair";
327
+ else if (limit >= 50) state2 = "Serious";
328
+ else state2 = "Critical";
329
+ detail = `${state2} (CPU limit ${limit}%)`;
278
330
  } else if (/no thermal warning level has been recorded/i.test(therm)) {
279
- state = "Nominal";
331
+ state2 = "Nominal";
280
332
  detail = "Nominal (no thermal warning recorded)";
281
333
  }
282
334
  }
@@ -285,7 +337,7 @@ async function collectThermal(detailed2) {
285
337
  try {
286
338
  const { temps } = await getSmcMetrics();
287
339
  cpuDieTemp = temps.cpu_die;
288
- if (detailed2) {
340
+ if (detailed) {
289
341
  if (temps.cpu_die !== void 0) temperatures["cpu_die"] = temps.cpu_die;
290
342
  if (temps.gpu_die !== void 0) temperatures["gpu_die"] = temps.gpu_die;
291
343
  if (temps.smc_die !== void 0) temperatures["smc_die"] = temps.smc_die;
@@ -294,19 +346,19 @@ async function collectThermal(detailed2) {
294
346
  }
295
347
  }
296
348
  } catch {
297
- if (detailed2) detail += " (powermetrics unavailable)";
349
+ if (detailed) detail += " (powermetrics unavailable)";
298
350
  }
299
- if (state === "Unknown" && cpuDieTemp !== void 0) {
300
- if (cpuDieTemp >= 100) state = "Critical";
301
- else if (cpuDieTemp >= 90) state = "Serious";
302
- else if (cpuDieTemp >= 80) state = "Fair";
303
- else state = "Nominal";
304
- detail = `${state} (estimated from ${cpuDieTemp}\xB0C)`;
351
+ if (state2 === "Unknown" && cpuDieTemp !== void 0) {
352
+ if (cpuDieTemp >= 100) state2 = "Critical";
353
+ else if (cpuDieTemp >= 90) state2 = "Serious";
354
+ else if (cpuDieTemp >= 80) state2 = "Fair";
355
+ else state2 = "Nominal";
356
+ detail = `${state2} (estimated from ${cpuDieTemp}\xB0C)`;
305
357
  }
306
358
  return {
307
- state,
359
+ state: state2,
308
360
  detail,
309
- pressureLevel: pressureFromState(state),
361
+ pressureLevel: pressureFromState(state2),
310
362
  temperatures: Object.keys(temperatures).length ? temperatures : void 0
311
363
  };
312
364
  } catch {
@@ -348,41 +400,30 @@ async function collectNetwork() {
348
400
  }
349
401
  async function collectProcesses(limit = 10) {
350
402
  try {
351
- const raw = (await run(`ps -Ao pid,user,pcpu,pmem,comm -r | head -n ${limit + 1}`)).trim();
403
+ const raw = (await run(`ps -Ao pid,ppid,user,pcpu,pmem,state,threads,time,comm -r | head -n ${limit + 1}`)).trim();
352
404
  const lines = raw.split("\n").slice(1);
353
405
  return lines.map((line) => {
354
- const parts = line.match(/\s*(\d+)\s+(\S+)\s+([\d.]+)\s+([\d.]+)\s+(.+)/);
406
+ const parts = line.match(/\s*(\d+)\s+(\d+)\s+(\S+)\s+([\d.]+)\s+([\d.]+)\s+(\S+)\s+(\d+)\s+([\d:]+)\s+(.+)/);
355
407
  if (!parts) return null;
356
- return { pid: parseInt(parts[1]), user: parts[2], cpu: parseFloat(parts[3]), mem: parseFloat(parts[4]), command: parts[5] };
408
+ const runtimeSec = parts[8].split(":").reduce((acc, val, idx) => acc + parseInt(val) * Math.pow(60, 2 - idx), 0);
409
+ return { pid: parseInt(parts[1]), ppid: parseInt(parts[2]), user: parts[3], cpu: parseFloat(parts[4]), mem: parseFloat(parts[5]), state: parts[6], threads: parseInt(parts[7]), runtime: runtimeSec, command: parts[9] };
357
410
  }).filter((p) => p !== null && p.pid > 0);
358
411
  } catch {
359
412
  return [];
360
413
  }
361
414
  }
362
415
 
363
- // src/formatters.ts
364
- import chalk2 from "chalk";
365
-
366
- // src/sparkline.ts
367
- var BLOCKS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
368
- function sparkline(values, opts2 = {}) {
369
- if (!values.length) return "";
370
- const min = opts2.min ?? Math.min(...values);
371
- const max = opts2.max ?? Math.max(...values);
372
- const range = max - min || 1;
373
- return values.map((v) => {
374
- const idx = Math.round((v - min) / range * (BLOCKS.length - 1));
375
- const clamped = Math.min(BLOCKS.length - 1, Math.max(0, idx));
376
- return BLOCKS[clamped];
377
- }).join("");
378
- }
416
+ // src/formatters/render.ts
417
+ import chalk3 from "chalk";
379
418
 
380
- // src/formatters.ts
419
+ // src/formatters/themes.ts
420
+ import chalk2 from "chalk";
381
421
  var THEMES = {
382
422
  default: {
383
423
  border: chalk2.hex("#4b5563"),
384
424
  cpu: chalk2.hex("#22d3ee"),
385
425
  mem: chalk2.hex("#a78bfa"),
426
+ gpu: chalk2.hex("#f472b6"),
386
427
  power: chalk2.hex("#fbbf24"),
387
428
  battery: chalk2.hex("#34d399"),
388
429
  thermal: chalk2.hex("#fb923c"),
@@ -395,6 +436,7 @@ var THEMES = {
395
436
  border: chalk2.hex("#6272a4"),
396
437
  cpu: chalk2.hex("#8be9fd"),
397
438
  mem: chalk2.hex("#bd93f9"),
439
+ gpu: chalk2.hex("#ff5555"),
398
440
  power: chalk2.hex("#f1fa8c"),
399
441
  battery: chalk2.hex("#50fa7b"),
400
442
  thermal: chalk2.hex("#ffb86c"),
@@ -407,6 +449,7 @@ var THEMES = {
407
449
  border: chalk2.hex("#ff0055"),
408
450
  cpu: chalk2.hex("#00ffcc"),
409
451
  mem: chalk2.hex("#ff00ff"),
452
+ gpu: chalk2.hex("#ffff00"),
410
453
  power: chalk2.hex("#ffff00"),
411
454
  battery: chalk2.hex("#00ff00"),
412
455
  thermal: chalk2.hex("#ff6600"),
@@ -419,6 +462,7 @@ var THEMES = {
419
462
  border: chalk2.gray,
420
463
  cpu: chalk2.white.bold,
421
464
  mem: chalk2.white.bold,
465
+ gpu: chalk2.white.bold,
422
466
  power: chalk2.white.bold,
423
467
  battery: chalk2.white.bold,
424
468
  thermal: chalk2.white.bold,
@@ -426,17 +470,36 @@ var THEMES = {
426
470
  disk: chalk2.white.bold,
427
471
  graphs: chalk2.white.bold,
428
472
  process: chalk2.white.bold
473
+ },
474
+ nord: {
475
+ border: chalk2.hex("#4c566a"),
476
+ cpu: chalk2.hex("#88c0d0"),
477
+ mem: chalk2.hex("#b48ead"),
478
+ gpu: chalk2.hex("#bf616a"),
479
+ power: chalk2.hex("#ebcb8b"),
480
+ battery: chalk2.hex("#a3be8c"),
481
+ thermal: chalk2.hex("#d08770"),
482
+ network: chalk2.hex("#81a1c1"),
483
+ disk: chalk2.hex("#b48ead"),
484
+ graphs: chalk2.hex("#88c0d0"),
485
+ process: chalk2.hex("#eceff4")
486
+ },
487
+ gruvbox: {
488
+ border: chalk2.hex("#928374"),
489
+ cpu: chalk2.hex("#fb4934"),
490
+ mem: chalk2.hex("#b8bb26"),
491
+ gpu: chalk2.hex("#d3869b"),
492
+ power: chalk2.hex("#fabd2f"),
493
+ battery: chalk2.hex("#b8bb26"),
494
+ thermal: chalk2.hex("#fb4934"),
495
+ network: chalk2.hex("#83a598"),
496
+ disk: chalk2.hex("#d3869b"),
497
+ graphs: chalk2.hex("#fb4934"),
498
+ process: chalk2.hex("#ebdbb2")
429
499
  }
430
500
  };
431
- function clampWidth(width) {
432
- if (!width || Number.isNaN(width)) return 80;
433
- return Math.max(60, Math.min(width, 240));
434
- }
435
- function gridColumns(width) {
436
- if (width >= 150) return 3;
437
- if (width >= 96) return 2;
438
- return 1;
439
- }
501
+
502
+ // src/formatters/render.ts
440
503
  function visLen(s) {
441
504
  return s.replace(/\x1b\[[0-9;]*m/g, "").length;
442
505
  }
@@ -453,9 +516,8 @@ function panel(title, lines, width, accent, borderAccent, height) {
453
516
  const h = height ?? lines.length;
454
517
  const contentWidth = Math.max(1, width - 4);
455
518
  const label = ` ${title} `;
456
- const dashLen = Math.max(0, width - 3 - label.length);
457
- const top = borderAccent("\u256D\u2500" + label + "\u2500".repeat(dashLen) + "\u256E");
458
- const bottom = borderAccent("\u2570" + "\u2500".repeat(width - 2) + "\u256F");
519
+ const top = borderAccent("\u250C" + label + "\u2500".repeat(width - label.length - 2) + "\u2510");
520
+ const bottom = borderAccent("\u2514" + "\u2500".repeat(width - 2) + "\u2518");
459
521
  const body = [];
460
522
  for (let i = 0; i < h; i++) {
461
523
  const line = lines[i] ?? "";
@@ -481,31 +543,28 @@ function hstack(blocks, gap = 2) {
481
543
  }
482
544
  function statRow(label, value, width = 9) {
483
545
  const w = Math.max(width, label.length + 1);
484
- return `${chalk2.dim(label.padEnd(w))}${value}`;
485
- }
486
- function bar(percent, width = 20) {
487
- const filled = Math.round(Math.min(100, Math.max(0, percent)) / 100 * width);
488
- const empty = width - filled;
489
- const barStr = "\u2588".repeat(Math.max(0, filled)) + "\u2591".repeat(Math.max(0, empty));
490
- const color = percent > 90 ? chalk2.red : percent > 70 ? chalk2.yellow : chalk2.green;
491
- return color(barStr);
546
+ return `${chalk3.dim(label.padEnd(w))}${value}`;
492
547
  }
493
548
  function gaugeRow(label, percent, contentWidth) {
494
549
  const labelW = 9;
495
550
  const pctText = `${Math.round(percent)}%`;
496
551
  const barW = Math.max(6, contentWidth - labelW - pctText.length - 1);
497
- return `${chalk2.dim(label.padEnd(labelW))}${bar(percent, barW)} ${pctText}`;
552
+ const filled = Math.round(Math.min(100, Math.max(0, percent)) / 100 * barW);
553
+ const empty = barW - filled;
554
+ const barStr = "\u2588".repeat(Math.max(0, filled)) + "\u2591".repeat(Math.max(0, empty));
555
+ const color = percent > 90 ? chalk3.red : percent > 70 ? chalk3.yellow : chalk3.green;
556
+ return `${chalk3.dim(label.padEnd(labelW))}${color(barStr)} ${pctText}`;
498
557
  }
499
558
  function thermalColor(pressureLevel) {
500
- if (pressureLevel >= 3) return chalk2.red;
501
- if (pressureLevel === 2) return chalk2.yellow;
502
- if (pressureLevel === 1) return chalk2.yellowBright;
503
- return chalk2.green;
559
+ if (pressureLevel >= 3) return chalk3.red;
560
+ if (pressureLevel === 2) return chalk3.yellow;
561
+ if (pressureLevel === 1) return chalk3.yellowBright;
562
+ return chalk3.green;
504
563
  }
505
564
  function capacityColor(percent) {
506
- if (percent < 80) return chalk2.red;
507
- if (percent < 90) return chalk2.yellow;
508
- return chalk2.green;
565
+ if (percent < 80) return chalk3.red;
566
+ if (percent < 90) return chalk3.yellow;
567
+ return chalk3.green;
509
568
  }
510
569
  function formatBytes(bytes) {
511
570
  if (bytes < 1024) return `${bytes} B`;
@@ -525,7 +584,7 @@ function cpuCard(data, contentWidth) {
525
584
  statRow("Cores", `${data.cpu.physicalCores}/${data.cpu.cores} phys/log`),
526
585
  statRow("Freq", `${data.cpu.frequency} MHz`),
527
586
  gaugeRow("Usage", data.cpu.usage, contentWidth),
528
- statRow("Load", data.cpu.loadAvg.map((l) => l.toFixed(2)).join(" \xB7 "))
587
+ statRow("Load", data.cpu.loadAvg.map((l) => l.toFixed(2)).join(" "))
529
588
  ];
530
589
  if (data.cpu.temperature) lines.push(statRow("Temp", formatTemp(data.cpu.temperature)));
531
590
  return lines;
@@ -534,11 +593,22 @@ function memCard(data, contentWidth) {
534
593
  return [
535
594
  statRow("Total", formatBytes(data.memory.total)),
536
595
  statRow("Used", formatBytes(data.memory.used)),
596
+ statRow("Cached", formatBytes(data.memory.total - data.memory.free - data.memory.used)),
537
597
  statRow("Free", formatBytes(data.memory.free)),
538
598
  gaugeRow("Usage", data.memory.usagePercent, contentWidth),
539
599
  statRow("Swap", `${formatBytes(data.memory.swapUsed)} / ${formatBytes(data.memory.swapTotal)}`)
540
600
  ];
541
601
  }
602
+ function gpuCard(data, contentWidth) {
603
+ if (!data.gpu) return null;
604
+ const lines = [];
605
+ lines.push(statRow("Model", truncatePlain(data.gpu.model, Math.max(4, contentWidth - 8))));
606
+ lines.push(statRow("Memory", formatBytes(data.gpu.memory)));
607
+ lines.push(statRow("Util", `${data.gpu.utilization}%`));
608
+ if (data.gpu.temperature) lines.push(statRow("Temp", formatTemp(data.gpu.temperature)));
609
+ lines.push(statRow("Procs", String(data.gpu.processes)));
610
+ return lines;
611
+ }
542
612
  function powerCard(data) {
543
613
  if (!data.power) return null;
544
614
  const lines = [];
@@ -553,7 +623,8 @@ function batteryCard(data, contentWidth) {
553
623
  const lines = [
554
624
  statRow("Level", `${data.battery.level}% (${data.battery.state})`),
555
625
  statRow("Time", data.battery.timeRemaining),
556
- statRow("Health", truncatePlain(data.battery.health, valW))
626
+ statRow("Cycles", String(data.battery.cycles ?? "N/A")),
627
+ statRow("Condition", data.battery.condition ?? "N/A")
557
628
  ];
558
629
  if (data.battery.maxCapacityPercent !== void 0) {
559
630
  lines.push(statRow("Capacity", capacityColor(data.battery.maxCapacityPercent)(`${data.battery.maxCapacityPercent}%`)));
@@ -576,42 +647,99 @@ function networkCard(data, contentWidth) {
576
647
  const valW = Math.max(6, contentWidth - 8);
577
648
  return [
578
649
  statRow("Iface", truncatePlain(`${data.network.interface} (${data.network.ip})`, valW)),
579
- statRow("RX", `${formatBytes(data.network.rxBytes)} \xB7 ${data.network.rxPackets} pkts`),
580
- statRow("TX", `${formatBytes(data.network.txBytes)} \xB7 ${data.network.txPackets} pkts`)
650
+ statRow("RX", `${formatBytes(data.network.rxBytes)} / ${data.network.rxPackets} pkt`),
651
+ statRow("TX", `${formatBytes(data.network.txBytes)} / ${data.network.txPackets} pkt`)
581
652
  ];
582
653
  }
583
654
  function header(data, width, badges = [], borderAccent = THEMES.default.border) {
584
- const left = `${chalk2.hex("#ff5500").bold(" PYRE ")} ${chalk2.bold(data.header.hostname)} ${chalk2.dim(data.header.os)} ${chalk2.dim("up " + data.header.uptime)}`;
585
- const right = badges.join(" ");
655
+ const now = (/* @__PURE__ */ new Date()).toLocaleTimeString();
656
+ const left = `${chalk3.hex("#ff5500").bold(" PYRE ")} ${chalk3.bold(data.header.hostname)} ${chalk3.dim(data.header.os)} ${chalk3.dim("up " + data.header.uptime)}`;
657
+ const right = `${chalk3.dim(now)} ${badges.join(" ")}`;
586
658
  const gap = Math.max(1, width - visLen(left) - visLen(right));
587
659
  const line = left + " ".repeat(gap) + right;
588
660
  return [line, borderAccent("\u2500".repeat(width))];
589
661
  }
590
662
  function sortProcesses(processes, sortBy) {
591
663
  const copy = [...processes];
592
- if (sortBy === "pid") return copy.sort((a, b) => a.pid - b.pid);
593
- if (sortBy === "mem") return copy.sort((a, b) => b.mem - a.mem);
594
- return copy.sort((a, b) => b.cpu - a.cpu);
664
+ switch (sortBy) {
665
+ case "pid":
666
+ return copy.sort((a, b) => a.pid - b.pid);
667
+ case "mem":
668
+ return copy.sort((a, b) => b.mem - a.mem);
669
+ case "user":
670
+ return copy.sort((a, b) => a.user.localeCompare(b.user));
671
+ case "command":
672
+ return copy.sort((a, b) => a.command.localeCompare(b.command));
673
+ case "state":
674
+ return copy.sort((a, b) => a.state.localeCompare(b.state));
675
+ case "threads":
676
+ return copy.sort((a, b) => b.threads - a.threads);
677
+ case "runtime":
678
+ return copy.sort((a, b) => b.runtime - a.runtime);
679
+ default:
680
+ return copy.sort((a, b) => b.cpu - a.cpu);
681
+ }
595
682
  }
596
683
  function pctColor(v) {
597
- return v > 80 ? chalk2.red : v > 50 ? chalk2.yellow : chalk2.dim;
684
+ return v > 80 ? chalk3.red : v > 50 ? chalk3.yellow : chalk3.dim;
598
685
  }
599
686
  function processTableLines(processes, contentWidth, borderAccent = THEMES.default.border) {
600
- const pidW = 7, userW = 10, cpuW = 7, memW = 7;
601
- const cmdW = Math.max(10, contentWidth - pidW - userW - cpuW - memW - 4);
602
- const head = chalk2.bold.dim("PID".padEnd(pidW)) + chalk2.bold.dim("USER".padEnd(userW)) + chalk2.bold.dim("CPU%".padEnd(cpuW)) + chalk2.bold.dim("MEM%".padEnd(memW)) + chalk2.bold.dim("COMMAND");
687
+ const pidW = 8, ppidW = 8, userW = 12, cpuW = 8, memW = 8, stateW = 8, thrW = 6, rtW = 10;
688
+ const cmdW = Math.max(10, contentWidth - pidW - ppidW - userW - cpuW - memW - stateW - thrW - rtW - 8);
689
+ const head = chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" PID ") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" PPID ") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" USER ") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" CPU% ") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" MEM% ") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" STATE ") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" THREADS") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" RUNTIME ") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" COMMAND".padEnd(cmdW + 1));
603
690
  const rows = processes.map((p) => {
604
691
  const cpuStr = pctColor(p.cpu)(`${p.cpu.toFixed(1)}`.padEnd(cpuW));
605
692
  const memStr = pctColor(p.mem)(`${p.mem.toFixed(1)}`.padEnd(memW));
606
- return String(p.pid).padEnd(pidW) + truncatePlain(p.user, userW - 1).padEnd(userW) + cpuStr + memStr + truncatePlain(p.command, cmdW);
693
+ const stateStr = chalk3.dim(p.state.padEnd(stateW));
694
+ const thrStr = String(p.threads).padEnd(thrW);
695
+ const rtStr = formatRuntime(p.runtime).padEnd(rtW);
696
+ return String(p.pid).padEnd(pidW) + String(p.ppid).padEnd(ppidW) + truncatePlain(p.user, userW - 1).padEnd(userW) + cpuStr + memStr + stateStr + thrStr + rtStr + truncatePlain(p.command, cmdW);
607
697
  });
608
698
  return [head, borderAccent("\u2500".repeat(contentWidth)), ...rows];
609
699
  }
700
+ function formatRuntime(seconds) {
701
+ if (seconds < 60) return `${seconds}s`;
702
+ const mins = Math.floor(seconds / 60);
703
+ if (mins < 60) return `${mins}m ${seconds % 60}s`;
704
+ const hrs = Math.floor(mins / 60);
705
+ return `${hrs}h ${mins % 60}m`;
706
+ }
707
+ function buildProcessTree(processes) {
708
+ const map = /* @__PURE__ */ new Map();
709
+ const roots = [];
710
+ for (const p of processes) map.set(p.pid, p);
711
+ for (const p of processes) {
712
+ if (p.ppid === 0 || !map.has(p.ppid)) roots.push(p);
713
+ }
714
+ const lines = [];
715
+ const visited = /* @__PURE__ */ new Set();
716
+ function renderNode(proc, prefix, isLast) {
717
+ if (visited.has(proc.pid)) return;
718
+ visited.add(proc.pid);
719
+ const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
720
+ const pidStr = String(proc.pid).padEnd(8);
721
+ const cpuStr = pctColor(proc.cpu)(`${proc.cpu.toFixed(1)}%`.padEnd(8));
722
+ const memStr = pctColor(proc.mem)(`${proc.mem.toFixed(1)}%`.padEnd(8));
723
+ const stateStr = chalk3.dim(proc.state.padEnd(8));
724
+ const thrStr = String(proc.threads).padEnd(6);
725
+ const cmd = truncatePlain(proc.command, Math.max(10, 100 - pidStr.length - cpuStr.length - memStr.length - stateStr.length - thrStr.length - 10));
726
+ lines.push(`${prefix}${connector}${pidStr}${cpuStr}${memStr}${stateStr}${thrStr} ${cmd}`);
727
+ const children = processes.filter((p) => p.ppid === proc.pid && !visited.has(p.pid));
728
+ children.sort((a, b) => b.cpu - a.cpu);
729
+ children.forEach((child, i) => {
730
+ const nextPrefix = prefix + (isLast ? " " : "\u2502 ");
731
+ renderNode(child, nextPrefix, i === children.length - 1);
732
+ });
733
+ }
734
+ roots.sort((a, b) => b.cpu - a.cpu);
735
+ roots.forEach((root, i) => renderNode(root, "", i === roots.length - 1));
736
+ return lines;
737
+ }
610
738
  function diskTableLines(disks, contentWidth, borderAccent = THEMES.default.border) {
611
- const fsW = 16, sizeW = 8, usedW = 8, availW = 8, capW = 7;
739
+ const fsW = 20, sizeW = 10, usedW = 10, availW = 10, capW = 8;
612
740
  const mountW = Math.max(8, contentWidth - fsW - sizeW - usedW - availW - capW - 5);
613
- const head = chalk2.bold.dim("FILESYSTEM".padEnd(fsW)) + chalk2.bold.dim("SIZE".padEnd(sizeW)) + chalk2.bold.dim("USED".padEnd(usedW)) + chalk2.bold.dim("AVAIL".padEnd(availW)) + chalk2.bold.dim("CAP".padEnd(capW)) + chalk2.bold.dim("MOUNT");
614
- const rows = disks.slice(0, 8).map((d) => {
741
+ const head = chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" FILESYSTEM ") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" SIZE ") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" USED ") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" AVAIL ") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" CAP ") + chalk3.bold.hex("#f8f8f2").bgHex("#44475a")(" MOUNT".padEnd(mountW + 1));
742
+ const rows = disks.map((d) => {
615
743
  const capNum = parseInt(d.capacity, 10) || 0;
616
744
  const capStr = pctColor(capNum)(d.capacity.padEnd(capW));
617
745
  return truncatePlain(d.filesystem, fsW - 1).padEnd(fsW) + d.size.padEnd(sizeW) + d.used.padEnd(usedW) + d.available.padEnd(availW) + capStr + truncatePlain(d.mountpoint, mountW);
@@ -637,6 +765,10 @@ function formatTable(data, opts2 = {}) {
637
765
  if (vis.mem !== false) {
638
766
  cards.push({ title: "Memory", accent: theme.mem, lines: memCard(data, contentWidth) });
639
767
  }
768
+ if (vis.gpu !== false) {
769
+ const gpu = gpuCard(data, contentWidth);
770
+ if (gpu) cards.push({ title: "GPU", accent: theme.gpu, lines: gpu });
771
+ }
640
772
  if (vis.power !== false) {
641
773
  const power = powerCard(data);
642
774
  if (power) cards.push({ title: "Power", accent: theme.power, lines: power });
@@ -668,14 +800,49 @@ function formatTable(data, opts2 = {}) {
668
800
  const sortedProcesses = sortProcesses(filteredProcesses, opts2.sortBy ?? "cpu");
669
801
  const limited = opts2.processLimit ? sortedProcesses.slice(0, opts2.processLimit) : sortedProcesses;
670
802
  const procTitle = opts2.filter ? `Processes \xB7 filter "${opts2.filter}" \xB7 sort ${opts2.sortBy ?? "cpu"}` : `Processes \xB7 sort ${opts2.sortBy ?? "cpu"}`;
671
- if (limited.length) {
803
+ if (opts2.treeView) {
804
+ const treeLines = buildProcessTree(limited);
805
+ if (treeLines.length) {
806
+ out.push(...panel(procTitle + " \xB7 tree", treeLines, width, theme.process, theme.border));
807
+ } else if (opts2.filter) {
808
+ out.push(...panel(procTitle, [chalk3.dim(`No processes match "${opts2.filter}"`)], width, theme.process, theme.border));
809
+ }
810
+ } else if (limited.length) {
672
811
  out.push(...panel(procTitle, processTableLines(limited, width - 4, theme.border), width, theme.process, theme.border));
673
812
  } else if (opts2.filter) {
674
- out.push(...panel(procTitle, [chalk2.dim(`No processes match "${opts2.filter}"`)], width, theme.process, theme.border));
813
+ out.push(...panel(procTitle, [chalk3.dim(`No processes match "${opts2.filter}"`)], width, theme.process, theme.border));
675
814
  }
676
815
  }
677
816
  return out.join("\n");
678
817
  }
818
+ function clampWidth(width) {
819
+ if (!width || Number.isNaN(width)) return 80;
820
+ return Math.max(60, Math.min(width, 240));
821
+ }
822
+ function gridColumns(width) {
823
+ if (width >= 150) return 3;
824
+ if (width >= 96) return 2;
825
+ return 1;
826
+ }
827
+
828
+ // src/formatters/output.ts
829
+ import chalk4 from "chalk";
830
+
831
+ // src/sparkline.ts
832
+ var BLOCKS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
833
+ function sparkline(values, opts2 = {}) {
834
+ if (!values.length) return "";
835
+ const min = opts2.min ?? Math.min(...values);
836
+ const max = opts2.max ?? Math.max(...values);
837
+ const range = max - min || 1;
838
+ return values.map((v) => {
839
+ const idx = Math.round((v - min) / range * (BLOCKS.length - 1));
840
+ const clamped = Math.min(BLOCKS.length - 1, Math.max(0, idx));
841
+ return BLOCKS[clamped];
842
+ }).join("");
843
+ }
844
+
845
+ // src/formatters/output.ts
679
846
  function formatJson(data) {
680
847
  return JSON.stringify(data, null, 2);
681
848
  }
@@ -697,7 +864,7 @@ function formatCsv(data) {
697
864
  [
698
865
  "thermal",
699
866
  "cpuTempF",
700
- data.thermal.temperatures?.cpu_die !== void 0 ? String(celsiusToFahrenheit(data.thermal.temperatures.cpu_die)) : ""
867
+ data.thermal.temperatures?.cpu_die != null ? String(celsiusToFahrenheit(data.thermal.temperatures.cpu_die)) : ""
701
868
  ],
702
869
  ["network", "rxBytes", String(data.network.rxBytes)],
703
870
  ["network", "txBytes", String(data.network.txBytes)],
@@ -713,62 +880,109 @@ function formatCsv(data) {
713
880
  ["system", "uptime", data.header.uptime],
714
881
  ["system", "timestamp", data.timestamp]
715
882
  ];
883
+ if (data.gpu) {
884
+ rows.push(["gpu", "model", data.gpu.model]);
885
+ rows.push(["gpu", "memory", String(data.gpu.memory)]);
886
+ rows.push(["gpu", "utilization", String(data.gpu.utilization)]);
887
+ if (data.gpu.temperature) rows.push(["gpu", "tempC", String(data.gpu.temperature)]);
888
+ rows.push(["gpu", "processes", String(data.gpu.processes)]);
889
+ }
890
+ for (const p of data.processes) {
891
+ rows.push(["process", `pid=${p.pid}`, `${p.pid}`]);
892
+ rows.push(["process", `ppid=${p.pid}`, String(p.ppid)]);
893
+ rows.push(["process", `user=${p.pid}`, p.user]);
894
+ rows.push(["process", `cpu=${p.pid}`, String(p.cpu)]);
895
+ rows.push(["process", `mem=${p.pid}`, String(p.mem)]);
896
+ rows.push(["process", `state=${p.pid}`, p.state]);
897
+ rows.push(["process", `threads=${p.pid}`, String(p.threads)]);
898
+ rows.push(["process", `runtime=${p.pid}`, String(p.runtime)]);
899
+ rows.push(["process", `command=${p.pid}`, p.command]);
900
+ }
716
901
  return rows.map((r) => r.map((item) => `"${String(item).replace(/"/g, '""')}"`).join(",")).join("\n");
717
902
  }
718
903
  function formatTsv(data) {
719
904
  return formatCsv(data).split("\n").map((r) => r.split(",").join(" ")).join("\n");
720
905
  }
721
- function formatGraphs(history2, width = 80, themeName = "default") {
906
+ function formatGraphs(history, width = 80, themeName = "default", graphMode = "spark") {
722
907
  const theme = THEMES[themeName] || THEMES.default;
723
908
  const contentWidth = width - 4;
724
- const sampleCount = history2.cpuUsage.length;
909
+ const sampleCount = history.cpuUsage.length;
725
910
  const sparkWidth = Math.max(10, contentWidth - 28);
726
911
  const lines = [];
727
- lines.push(graphRow("CPU %", history2.cpuUsage, { min: 0, max: 100 }, (v) => `${v.toFixed(0)}%`, sparkWidth));
728
- lines.push(graphRow("Mem %", history2.memUsage, { min: 0, max: 100 }, (v) => `${v.toFixed(0)}%`, sparkWidth));
912
+ lines.push(graphRow("CPU %", history.cpuUsage, { min: 0, max: 100 }, (v) => `${v.toFixed(0)}%`, sparkWidth, graphMode));
913
+ lines.push(graphRow("Mem %", history.memUsage, { min: 0, max: 100 }, (v) => `${v.toFixed(0)}%`, sparkWidth, graphMode));
729
914
  lines.push(
730
- graphRow("Temp", history2.temp, {}, formatTemp, sparkWidth, "no sensor access (needs sudo powermetrics)")
915
+ graphRow("Temp", history.temp, {}, formatTemp, sparkWidth, "no sensor access (needs sudo powermetrics)", graphMode)
731
916
  );
732
- lines.push(graphRow("Net RX/s", history2.netRxRate, {}, (v) => `${formatBytes(v)}/s`, sparkWidth));
733
- lines.push(graphRow("Net TX/s", history2.netTxRate, {}, (v) => `${formatBytes(v)}/s`, sparkWidth));
734
- const title = `Graphs \xB7 last ${sampleCount} sample${sampleCount === 1 ? "" : "s"}`;
917
+ lines.push(graphRow("Net RX/s", history.netRxRate, {}, (v) => `${formatBytes(v)}/s`, sparkWidth, graphMode));
918
+ lines.push(graphRow("Net TX/s", history.netTxRate, {}, (v) => `${formatBytes(v)}/s`, sparkWidth, graphMode));
919
+ const title = `Graphs \xB7 mode:${graphMode} \xB7 last ${sampleCount} sample${sampleCount === 1 ? "" : "s"}`;
735
920
  return panel(title, lines, width, theme.graphs, theme.border).join("\n");
736
921
  }
737
- function graphRow(label, values, bounds, fmt, sparkWidth = 40, emptyMessage = "collecting data...") {
922
+ function graphRow(label, values, bounds, fmt, sparkWidth = 40, emptyMessage = "collecting data...", mode = "spark") {
738
923
  if (!values.length) {
739
- return `${chalk2.dim(label.padEnd(10))} ${chalk2.dim(emptyMessage)}`;
924
+ return `${chalk4.dim(label.padEnd(10))} ${chalk4.dim(emptyMessage)}`;
740
925
  }
741
926
  const visible = values.slice(-sparkWidth);
742
- const spark = sparkline(visible, bounds);
927
+ const spark = mode === "spark" ? sparkline(visible, bounds) : barGraph(visible, bounds, sparkWidth);
743
928
  const current = values[values.length - 1];
744
- let color = chalk2.cyan;
929
+ let color = chalk4.cyan;
745
930
  if (bounds.min !== void 0 && bounds.max !== void 0) {
746
931
  const pct = (current - bounds.min) / (bounds.max - bounds.min || 1) * 100;
747
- color = pct > 90 ? chalk2.red : pct > 70 ? chalk2.yellow : chalk2.green;
932
+ color = pct > 90 ? chalk4.red : pct > 70 ? chalk4.yellow : chalk4.green;
748
933
  }
749
- return `${chalk2.dim(label.padEnd(10))}${color(spark)} ${chalk2.bold(fmt(current))}`;
934
+ return `${chalk4.dim(label.padEnd(10))}${color(spark)} ${chalk4.bold(fmt(current))}`;
935
+ }
936
+ function barGraph(values, bounds, width) {
937
+ const min = bounds.min ?? Math.min(...values);
938
+ const max = bounds.max ?? Math.max(...values);
939
+ const range = max - min || 1;
940
+ return values.map((v) => {
941
+ const pct = (v - min) / range;
942
+ if (pct > 0.8) return "\u2588";
943
+ if (pct > 0.6) return "\u2587";
944
+ if (pct > 0.4) return "\u2586";
945
+ if (pct > 0.2) return "\u2584";
946
+ return " ";
947
+ }).join("");
750
948
  }
751
949
 
752
- // src/live.ts
753
- import fs from "fs";
754
- import path from "path";
950
+ // src/live/input.ts
755
951
  import readline from "readline";
756
- import chalk3 from "chalk";
757
952
 
758
953
  // src/history.ts
759
954
  var History = class {
955
+ /** Maximum number of samples retained in the rolling window. */
760
956
  maxLen;
957
+ /** Rolling window of CPU usage percentages (0–100). */
761
958
  cpuUsage = [];
959
+ /** Rolling window of memory usage percentages (0–100). */
762
960
  memUsage = [];
961
+ /** Rolling window of CPU die temperatures in °C. */
763
962
  temp = [];
963
+ /** Rolling window of network receive rates in bytes/sec. */
764
964
  netRxRate = [];
965
+ /** Rolling window of network transmit rates in bytes/sec. */
765
966
  netTxRate = [];
967
+ /** Rolling window of GPU utilization percentages (0–100). */
968
+ gpuUtil = [];
766
969
  lastRxBytes = 0;
767
970
  lastTxBytes = 0;
768
971
  lastTs = 0;
972
+ /**
973
+ * Create a new History instance.
974
+ * @param maxLen - Maximum number of samples to retain. Defaults to 40.
975
+ */
769
976
  constructor(maxLen = 40) {
770
977
  this.maxLen = maxLen;
771
978
  }
979
+ /**
980
+ * Push a new sample into the rolling window.
981
+ *
982
+ * Network rates are computed as bytes/sec from the cumulative
983
+ * counters. The first sample always reports 0 for rates
984
+ * because there is no prior sample to compute a delta from.
985
+ */
772
986
  push(sample) {
773
987
  const now = Date.now();
774
988
  let rxRate = 0;
@@ -786,21 +1000,28 @@ var History = class {
786
1000
  if (sample.temp !== null) this.pushBounded(this.temp, sample.temp);
787
1001
  this.pushBounded(this.netRxRate, rxRate);
788
1002
  this.pushBounded(this.netTxRate, txRate);
1003
+ if (sample.gpuUtil !== void 0) this.pushBounded(this.gpuUtil, sample.gpuUtil);
789
1004
  }
1005
+ /** Clear all rolling windows and reset cumulative byte counters. */
790
1006
  reset() {
791
1007
  this.cpuUsage = [];
792
1008
  this.memUsage = [];
793
1009
  this.temp = [];
794
1010
  this.netRxRate = [];
795
1011
  this.netTxRate = [];
1012
+ this.gpuUtil = [];
796
1013
  this.lastRxBytes = 0;
797
1014
  this.lastTxBytes = 0;
798
1015
  this.lastTs = 0;
799
1016
  }
800
- /** Grow or shrink the rolling window (e.g. on terminal resize), trimming from the oldest samples. */
1017
+ /**
1018
+ * Grow or shrink the rolling window (e.g. on terminal resize),
1019
+ * trimming from the oldest samples.
1020
+ * @param n - New maximum length; clamped to at least 1.
1021
+ */
801
1022
  setMaxLen(n) {
802
1023
  this.maxLen = Math.max(1, n);
803
- for (const arr of [this.cpuUsage, this.memUsage, this.temp, this.netRxRate, this.netTxRate]) {
1024
+ for (const arr of [this.cpuUsage, this.memUsage, this.temp, this.netRxRate, this.netTxRate, this.gpuUtil]) {
804
1025
  while (arr.length > this.maxLen) arr.shift();
805
1026
  }
806
1027
  }
@@ -810,62 +1031,73 @@ var History = class {
810
1031
  }
811
1032
  };
812
1033
 
813
- // src/live.ts
814
- var intervalHandle = null;
815
- var running = false;
816
- var paused = false;
817
- var detailed = false;
818
- var interval = 2;
819
- var showGraphs = true;
820
- var exportFormat = "json";
821
- var exportDir = "./pyre-exports";
822
- var currentTheme = "default";
823
- var visiblePanels = {
824
- cpu: true,
825
- mem: true,
826
- power: true,
827
- battery: true,
828
- thermal: true,
829
- network: true,
830
- disk: true,
831
- process: true
1034
+ // src/live/state.ts
1035
+ var SIGNAL_OPTIONS = ["SIGTERM", "SIGKILL", "SIGINT", "SIGHUP", "SIGSTOP", "SIGCONT"];
1036
+ var state = {
1037
+ intervalHandle: null,
1038
+ running: false,
1039
+ paused: false,
1040
+ detailed: false,
1041
+ interval: 2,
1042
+ showGraphs: true,
1043
+ graphMode: "spark",
1044
+ exportFormat: "json",
1045
+ exportDir: "./pyre-exports",
1046
+ currentTheme: "default",
1047
+ visiblePanels: {
1048
+ cpu: true,
1049
+ mem: true,
1050
+ power: true,
1051
+ battery: true,
1052
+ thermal: true,
1053
+ network: true,
1054
+ disk: true,
1055
+ process: true
1056
+ },
1057
+ logging: false,
1058
+ logStream: null,
1059
+ statusMessage: "",
1060
+ statusTimer: null,
1061
+ lastData: null,
1062
+ history: new History(40),
1063
+ keypressHandler: null,
1064
+ termWidth: process.stdout.columns || 80,
1065
+ termHeight: process.stdout.rows || 24,
1066
+ sortMode: "cpu",
1067
+ processFilter: "",
1068
+ inputMode: null,
1069
+ inputBuffer: "",
1070
+ treeView: false,
1071
+ SIGNAL_OPTIONS,
1072
+ customizerIndex: 0,
1073
+ CUSTOMIZER_OPTIONS: [
1074
+ "Theme",
1075
+ "Graph Mode",
1076
+ "Toggle CPU",
1077
+ "Toggle Memory",
1078
+ "Toggle Power",
1079
+ "Toggle Battery",
1080
+ "Toggle Thermal",
1081
+ "Toggle Network",
1082
+ "Toggle Disk",
1083
+ "Toggle Processes",
1084
+ "Toggle Tree View"
1085
+ ],
1086
+ CPU_ALERT_PCT: 90,
1087
+ TEMP_ALERT_C: 95,
1088
+ alerted: false
832
1089
  };
833
- var logging = false;
834
- var logStream = null;
835
- var statusMessage = "";
836
- var statusTimer = null;
837
- var lastData = null;
838
- var history = new History(40);
839
- var keypressHandler = null;
840
- var termWidth = process.stdout.columns || 80;
841
- var termHeight = process.stdout.rows || 24;
842
- var sortMode = "cpu";
843
- var processFilter = "";
844
- var inputMode = null;
845
- var inputBuffer = "";
846
- var customizerIndex = 0;
847
- var CUSTOMIZER_OPTIONS = [
848
- "Theme",
849
- "Toggle CPU",
850
- "Toggle Memory",
851
- "Toggle Power",
852
- "Toggle Battery",
853
- "Toggle Thermal",
854
- "Toggle Network",
855
- "Toggle Disk",
856
- "Toggle Processes"
857
- ];
858
- var CPU_ALERT_PCT = 90;
859
- var TEMP_ALERT_C = 95;
860
- var alerted = false;
861
1090
  function setStatus(msg, ms = 3e3) {
862
- statusMessage = msg;
863
- if (statusTimer) clearTimeout(statusTimer);
864
- statusTimer = setTimeout(() => {
865
- statusMessage = "";
866
- render();
1091
+ state.statusMessage = msg;
1092
+ if (state.statusTimer) clearTimeout(state.statusTimer);
1093
+ state.statusTimer = setTimeout(() => {
1094
+ state.statusMessage = "";
867
1095
  }, ms);
868
1096
  }
1097
+
1098
+ // src/live/export.ts
1099
+ import fs from "fs";
1100
+ import path from "path";
869
1101
  function ensureDir(dir) {
870
1102
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
871
1103
  }
@@ -873,14 +1105,14 @@ function timestampForFile() {
873
1105
  return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
874
1106
  }
875
1107
  function exportSnapshot() {
876
- if (!lastData) {
1108
+ if (!state.lastData) {
877
1109
  setStatus("No data yet \u2014 try again in a moment");
878
1110
  return;
879
1111
  }
880
1112
  try {
881
- ensureDir(exportDir);
882
- const file = path.join(exportDir, `pyre-${timestampForFile()}.${exportFormat}`);
883
- const content = exportFormat === "json" ? formatJson(lastData) : exportFormat === "csv" ? formatCsv(lastData) : formatTsv(lastData);
1113
+ ensureDir(state.exportDir);
1114
+ const file = path.join(state.exportDir, `pyre-${timestampForFile()}.${state.exportFormat}`);
1115
+ const content = state.exportFormat === "json" ? formatJson(state.lastData) : state.exportFormat === "csv" ? formatCsv(state.lastData) : formatTsv(state.lastData);
884
1116
  fs.writeFileSync(file, content);
885
1117
  setStatus(`Exported snapshot -> ${file}`);
886
1118
  } catch (err) {
@@ -889,86 +1121,101 @@ function exportSnapshot() {
889
1121
  }
890
1122
  function startLogging() {
891
1123
  try {
892
- ensureDir(exportDir);
893
- const file = path.join(exportDir, `pyre-log-${timestampForFile()}.csv`);
894
- logStream = fs.createWriteStream(file, { flags: "a" });
895
- logStream.write("timestamp,cpu_usage,mem_usage_percent,temp_c,net_rx_bytes,net_tx_bytes,thermal_state\n");
896
- logging = true;
1124
+ ensureDir(state.exportDir);
1125
+ const file = path.join(state.exportDir, `pyre-log-${timestampForFile()}.csv`);
1126
+ state.logStream = fs.createWriteStream(file, { flags: "a" });
1127
+ state.logStream.write("timestamp,cpu_usage,mem_usage_percent,temp_c,net_rx_bytes,net_tx_bytes,thermal_state\n");
1128
+ state.logging = true;
897
1129
  setStatus(`Logging every tick -> ${file}`);
898
1130
  } catch (err) {
899
1131
  setStatus(`Logging failed to start: ${err.message}`);
900
1132
  }
901
1133
  }
902
1134
  function stopLogging() {
903
- logging = false;
904
- logStream?.end();
905
- logStream = null;
1135
+ state.logging = false;
1136
+ state.logStream?.end();
1137
+ state.logStream = null;
906
1138
  setStatus("Logging stopped");
907
1139
  }
908
1140
  function toggleLogging() {
909
- if (logging) stopLogging();
1141
+ if (state.logging) stopLogging();
910
1142
  else startLogging();
911
1143
  }
912
1144
  function writeLogRow(data) {
913
- if (!logging || !logStream) return;
1145
+ if (!state.logging || !state.logStream) return;
914
1146
  const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? "";
915
- logStream.write(
1147
+ state.logStream.write(
916
1148
  `${data.timestamp},${data.cpu.usage},${data.memory.usagePercent},${temp},${data.network.rxBytes},${data.network.txBytes},${data.thermal.state}
917
1149
  `
918
1150
  );
919
1151
  }
1152
+
1153
+ // src/live/render.ts
1154
+ import chalk5 from "chalk";
920
1155
  function processRowBudget() {
921
- const columns = gridColumns(termWidth);
1156
+ const columns = gridColumns(state.termWidth);
922
1157
  const cardRows = Math.ceil(6 / columns);
923
- const reserved = 2 + cardRows * 8 + 11 + (showGraphs ? 9 : 0) + 4;
924
- return Math.max(3, Math.min(40, termHeight - reserved));
1158
+ const reserved = 2 + cardRows * 8 + 11 + (state.showGraphs ? 9 : 0) + 4;
1159
+ return Math.max(3, Math.min(40, state.termHeight - reserved));
925
1160
  }
926
1161
  function renderCustomizerOverlay() {
927
1162
  const themesList = Object.keys(THEMES);
928
1163
  const lines = [
929
- chalk3.bgCyan.black.bold(" UI CUSTOMIZER (Use \u2191/\u2193 or J/K to navigate, Enter/Space to toggle, Esc to exit) ")
1164
+ chalk5.bgCyan.black.bold(" UI CUSTOMIZER (Use \u2191/\u2193 or J/K to navigate, Enter/Space to toggle, Esc to exit) ")
930
1165
  ];
931
- CUSTOMIZER_OPTIONS.forEach((opt, idx) => {
932
- const isSelected = idx === customizerIndex;
933
- const prefix = isSelected ? chalk3.yellow("\u25B6 ") : " ";
1166
+ state.CUSTOMIZER_OPTIONS.forEach((opt, idx) => {
1167
+ const isSelected = idx === state.customizerIndex;
1168
+ const prefix = isSelected ? chalk5.yellow("\u25B6 ") : " ";
934
1169
  if (opt === "Theme") {
935
- lines.push(`${prefix}${opt}: ${chalk3.bold.green(currentTheme)} [${themesList.join(", ")}]`);
1170
+ lines.push(`${prefix}${opt}: ${chalk5.bold.green(state.currentTheme)} [${themesList.join(", ")}]`);
1171
+ } else if (opt === "Graph Mode") {
1172
+ lines.push(`${prefix}${opt}: ${chalk5.bold.green(state.graphMode)} [spark, bar]`);
1173
+ } else if (opt === "Toggle Tree View") {
1174
+ const status = state.treeView ? chalk5.green("[VISIBLE]") : chalk5.red("[HIDDEN]");
1175
+ lines.push(`${prefix}${opt}: ${status}`);
936
1176
  } else {
937
1177
  const key = opt.replace("Toggle ", "").toLowerCase();
938
- const isVisible = visiblePanels[key] !== false;
939
- const status = isVisible ? chalk3.green("[VISIBLE]") : chalk3.red("[HIDDEN]");
1178
+ const isVisible = state.visiblePanels[key] !== false;
1179
+ const status = isVisible ? chalk5.green("[VISIBLE]") : chalk5.red("[HIDDEN]");
940
1180
  lines.push(`${prefix}${opt}: ${status}`);
941
1181
  }
942
1182
  });
943
1183
  return lines.map((l) => ` ${l}`).join("\n");
944
1184
  }
945
1185
  function render() {
946
- if (!lastData) return;
1186
+ if (!state.lastData) return;
947
1187
  const lines = [];
948
1188
  lines.push(
949
- formatTable(lastData, {
950
- width: termWidth,
951
- sortBy: sortMode,
952
- filter: processFilter || void 0,
1189
+ formatTable(state.lastData, {
1190
+ width: state.termWidth,
1191
+ sortBy: state.sortMode,
1192
+ filter: state.processFilter || void 0,
953
1193
  processLimit: processRowBudget(),
954
- theme: currentTheme,
955
- visible: visiblePanels
1194
+ theme: state.currentTheme,
1195
+ visible: state.visiblePanels,
1196
+ treeView: state.treeView
956
1197
  })
957
1198
  );
958
- if (showGraphs) {
1199
+ if (state.showGraphs) {
959
1200
  lines.push("");
960
- lines.push(formatGraphs(history, termWidth, currentTheme));
1201
+ lines.push(formatGraphs(state.history, state.termWidth, state.currentTheme, state.graphMode));
961
1202
  }
962
1203
  lines.push("");
963
1204
  lines.push(footerLine());
964
- if (inputMode === "customizer") {
1205
+ if (state.inputMode === "customizer") {
965
1206
  lines.push(renderCustomizerOverlay());
966
- } else if (inputMode === "filter") {
967
- lines.push(chalk3.cyan(` Filter processes: ${inputBuffer}_`));
968
- } else if (inputMode === "kill") {
969
- lines.push(chalk3.cyan(` Kill PID: ${inputBuffer}_ (enter to confirm, esc to cancel)`));
970
- } else if (statusMessage) {
971
- lines.push(` ${statusMessage}`);
1207
+ } else if (state.inputMode === "filter") {
1208
+ lines.push(chalk5.cyan(` Filter processes: ${state.inputBuffer}_`));
1209
+ } else if (state.inputMode === "kill") {
1210
+ lines.push(chalk5.cyan(` Kill PID: ${state.inputBuffer}_ (enter to confirm, esc to cancel)`));
1211
+ } else if (state.inputMode === "signal") {
1212
+ const sigIdx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer);
1213
+ const sigList = state.SIGNAL_OPTIONS.map((s, i) => i === sigIdx ? chalk5.yellow(s) : chalk5.dim(s)).join(" ");
1214
+ lines.push(chalk5.cyan(` Signal: ${state.inputBuffer}_`));
1215
+ lines.push(chalk5.dim(` ${sigList}`));
1216
+ lines.push(chalk5.dim(" \u2191/\u2193 to select, Enter to confirm, Esc to cancel"));
1217
+ } else if (state.statusMessage) {
1218
+ lines.push(` ${state.statusMessage}`);
972
1219
  }
973
1220
  process.stdout.write("\x1B[2J\x1B[H");
974
1221
  process.stdout.write(lines.join("\n"));
@@ -977,137 +1224,164 @@ function footerLine() {
977
1224
  const keys = [
978
1225
  ["q", "quit"],
979
1226
  ["c", "customize UI"],
980
- ["p", paused ? "resume" : "pause"],
981
- ["g", showGraphs ? "hide graphs" : "show graphs"],
982
- ["d", detailed ? "basic" : "detailed"],
983
- ["s", `sort:${sortMode}`],
1227
+ ["p", state.paused ? "resume" : "pause"],
1228
+ ["g", state.showGraphs ? "hide graphs" : "show graphs"],
1229
+ ["b", `graph:${state.graphMode}`],
1230
+ ["d", state.detailed ? "basic" : "detailed"],
1231
+ ["s", `sort:${state.sortMode}`],
984
1232
  ["/", "filter"],
985
1233
  ["k", "kill"],
1234
+ ["S", "signal"],
1235
+ ["t", state.treeView ? "flat" : "tree"],
986
1236
  ["e", "export"],
987
- ["l", logging ? "stop log" : "log"],
988
- ["f", exportFormat],
989
- ["+/-", `${interval}s`]
1237
+ ["l", state.logging ? "stop log" : "log"],
1238
+ ["f", state.exportFormat],
1239
+ ["+/-", `${state.interval}s`]
990
1240
  ];
991
- const keyStr = keys.map(([k, label]) => `${chalk3.cyan.bold(k)} ${chalk3.dim(label)}`).join(chalk3.dim(" \xB7 "));
1241
+ const keyStr = keys.map(([k, label]) => `${chalk5.hex("#50fa7b").bold(k)} ${chalk5.dim(label)}`).join(chalk5.dim(" | "));
992
1242
  const badges = [];
993
- if (paused) badges.push(chalk3.yellow.bold("\u23F8 PAUSED"));
994
- if (logging) badges.push(chalk3.red.bold("\u25CF REC"));
1243
+ if (state.paused) badges.push(chalk5.yellow.bold("\u23F8 PAUSED"));
1244
+ if (state.logging) badges.push(chalk5.red.bold("\u25CF REC"));
995
1245
  const badgeStr = badges.join(" ");
996
1246
  return badgeStr ? `${keyStr} ${badgeStr}` : keyStr;
997
1247
  }
998
1248
  function checkAlerts(data) {
999
1249
  const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
1000
- const hot = data.cpu.usage >= CPU_ALERT_PCT || temp !== null && temp >= TEMP_ALERT_C;
1001
- if (hot && !alerted) {
1002
- alerted = true;
1250
+ const hot = data.cpu.usage >= state.CPU_ALERT_PCT || temp !== null && temp >= state.TEMP_ALERT_C;
1251
+ if (hot && !state.alerted) {
1252
+ state.alerted = true;
1003
1253
  process.stdout.write("\x07");
1004
- const reason = data.cpu.usage >= CPU_ALERT_PCT ? `CPU at ${data.cpu.usage}%` : `Temp at ${temp}\xB0C`;
1005
- setStatus(chalk3.red(`\u26A0 Alert: ${reason}`), 5e3);
1254
+ const reason = data.cpu.usage >= state.CPU_ALERT_PCT ? `CPU at ${data.cpu.usage}%` : `Temp at ${temp}\xB0C`;
1255
+ setStatus(chalk5.red(`\u26A0 Alert: ${reason}`), 5e3);
1006
1256
  } else if (!hot) {
1007
- alerted = false;
1008
- }
1009
- }
1010
- async function tick() {
1011
- if (paused) return;
1012
- try {
1013
- const data = await collectAll({ detailed, processLimit: processRowBudget() });
1014
- lastData = data;
1015
- const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
1016
- history.push({
1017
- cpuUsage: data.cpu.usage,
1018
- memUsage: data.memory.usagePercent,
1019
- temp,
1020
- rxBytes: data.network.rxBytes,
1021
- txBytes: data.network.txBytes
1022
- });
1023
- writeLogRow(data);
1024
- checkAlerts(data);
1025
- render();
1026
- } catch {
1027
- }
1028
- }
1029
- function killProcess(pidStr) {
1030
- const pid = parseInt(pidStr, 10);
1031
- if (!pid || pid <= 0) {
1032
- setStatus(`Invalid PID: ${pidStr}`);
1033
- return;
1034
- }
1035
- try {
1036
- process.kill(pid, "SIGTERM");
1037
- setStatus(`Sent SIGTERM to PID ${pid}`);
1038
- } catch (err) {
1039
- setStatus(`Failed to kill ${pid}: ${err.message}`);
1257
+ state.alerted = false;
1040
1258
  }
1041
1259
  }
1260
+
1261
+ // src/live/input.ts
1042
1262
  function handleInputModeKey(str, key) {
1043
1263
  if (key.name === "escape") {
1044
- inputMode = null;
1045
- inputBuffer = "";
1264
+ state.inputMode = null;
1265
+ state.inputBuffer = "";
1046
1266
  render();
1047
1267
  return;
1048
1268
  }
1049
- if (inputMode === "customizer") {
1269
+ if (state.inputMode === "customizer") {
1050
1270
  const themesList = Object.keys(THEMES);
1051
1271
  if (key.name === "up" || str === "k") {
1052
- customizerIndex = (customizerIndex - 1 + CUSTOMIZER_OPTIONS.length) % CUSTOMIZER_OPTIONS.length;
1272
+ state.customizerIndex = (state.customizerIndex - 1 + state.CUSTOMIZER_OPTIONS.length) % state.CUSTOMIZER_OPTIONS.length;
1053
1273
  } else if (key.name === "down" || str === "j") {
1054
- customizerIndex = (customizerIndex + 1) % CUSTOMIZER_OPTIONS.length;
1274
+ state.customizerIndex = (state.customizerIndex + 1) % state.CUSTOMIZER_OPTIONS.length;
1055
1275
  } else if (key.name === "return" || str === " ") {
1056
- const selected = CUSTOMIZER_OPTIONS[customizerIndex];
1276
+ const selected = state.CUSTOMIZER_OPTIONS[state.customizerIndex];
1057
1277
  if (selected === "Theme") {
1058
- const nextIdx = (themesList.indexOf(currentTheme) + 1) % themesList.length;
1059
- currentTheme = themesList[nextIdx];
1278
+ const nextIdx = (themesList.indexOf(state.currentTheme) + 1) % themesList.length;
1279
+ state.currentTheme = themesList[nextIdx];
1280
+ } else if (selected === "Toggle Tree View") {
1281
+ state.treeView = !state.treeView;
1282
+ } else if (selected === "Graph Mode") {
1283
+ state.graphMode = state.graphMode === "spark" ? "bar" : "spark";
1060
1284
  } else {
1061
1285
  const itemKey = selected.replace("Toggle ", "").toLowerCase();
1062
- visiblePanels[itemKey] = !visiblePanels[itemKey];
1286
+ state.visiblePanels[itemKey] = !state.visiblePanels[itemKey];
1063
1287
  }
1064
1288
  }
1065
1289
  render();
1066
1290
  return;
1067
1291
  }
1292
+ if (state.inputMode === "signal") {
1293
+ if (key.name === "up" || str === "k") {
1294
+ const idx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer);
1295
+ state.inputBuffer = state.SIGNAL_OPTIONS[(idx - 1 + state.SIGNAL_OPTIONS.length) % state.SIGNAL_OPTIONS.length];
1296
+ } else if (key.name === "down" || str === "j") {
1297
+ const idx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer);
1298
+ state.inputBuffer = state.SIGNAL_OPTIONS[(idx + 1) % state.SIGNAL_OPTIONS.length];
1299
+ } else if (key.name === "return" || str === " ") {
1300
+ killProcess(state.inputBuffer.trim(), state.inputBuffer.trim());
1301
+ state.inputMode = null;
1302
+ state.inputBuffer = "";
1303
+ render();
1304
+ return;
1305
+ }
1306
+ render();
1307
+ return;
1308
+ }
1068
1309
  if (key.name === "return") {
1069
- if (inputMode === "filter") {
1070
- processFilter = inputBuffer.trim();
1071
- setStatus(processFilter ? `Filtering: "${processFilter}"` : "Filter cleared");
1072
- } else if (inputMode === "kill") {
1073
- killProcess(inputBuffer.trim());
1310
+ if (state.inputMode === "filter") {
1311
+ state.processFilter = state.inputBuffer.trim();
1312
+ setStatus(state.processFilter ? `Filtering: "${state.processFilter}"` : "Filter cleared");
1313
+ } else if (state.inputMode === "kill") {
1314
+ killProcess(state.inputBuffer.trim());
1074
1315
  }
1075
- inputMode = null;
1076
- inputBuffer = "";
1316
+ state.inputMode = null;
1317
+ state.inputBuffer = "";
1077
1318
  render();
1078
1319
  return;
1079
1320
  }
1080
1321
  if (key.name === "backspace") {
1081
- inputBuffer = inputBuffer.slice(0, -1);
1322
+ state.inputBuffer = state.inputBuffer.slice(0, -1);
1082
1323
  render();
1083
1324
  return;
1084
1325
  }
1085
1326
  if (str && str.length === 1 && !key.ctrl && !key.meta) {
1086
- inputBuffer += str;
1327
+ state.inputBuffer += str;
1087
1328
  render();
1088
1329
  }
1089
1330
  }
1331
+ function killProcess(pidStr, signal = "SIGTERM") {
1332
+ const pid = parseInt(pidStr, 10);
1333
+ if (!pid || pid <= 0) {
1334
+ setStatus(`Invalid PID: ${pidStr}`);
1335
+ return;
1336
+ }
1337
+ try {
1338
+ process.kill(pid, signal);
1339
+ setStatus(`Sent ${signal} to PID ${pid}`);
1340
+ } catch (err) {
1341
+ setStatus(`Failed to send ${signal} to ${pid}: ${err.message}`);
1342
+ }
1343
+ }
1090
1344
  function onResize() {
1091
- termWidth = process.stdout.columns || 80;
1092
- termHeight = process.stdout.rows || 24;
1093
- history.setMaxLen(Math.max(20, Math.min(200, termWidth - 30)));
1345
+ state.termWidth = process.stdout.columns || 80;
1346
+ state.termHeight = process.stdout.rows || 24;
1347
+ state.history.setMaxLen(Math.max(20, Math.min(200, state.termWidth - 30)));
1094
1348
  render();
1095
1349
  }
1096
1350
  function restartTicker() {
1097
- if (intervalHandle) clearInterval(intervalHandle);
1098
- intervalHandle = setInterval(tick, interval * 1e3);
1351
+ if (state.intervalHandle) clearInterval(state.intervalHandle);
1352
+ state.intervalHandle = setInterval(tick, state.interval * 1e3);
1353
+ }
1354
+ async function tick() {
1355
+ if (state.paused) return;
1356
+ try {
1357
+ const data = await collectAll({ detailed: state.detailed, processLimit: processRowBudget() });
1358
+ state.lastData = data;
1359
+ const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
1360
+ state.history.push({
1361
+ cpuUsage: data.cpu.usage,
1362
+ memUsage: data.memory.usagePercent,
1363
+ temp,
1364
+ rxBytes: data.network.rxBytes,
1365
+ txBytes: data.network.txBytes,
1366
+ gpuUtil: data.gpu?.utilization
1367
+ });
1368
+ writeLogRow(data);
1369
+ checkAlerts(data);
1370
+ render();
1371
+ } catch {
1372
+ }
1099
1373
  }
1100
1374
  async function startLive(opts2) {
1101
- if (running) return;
1102
- running = true;
1103
- paused = false;
1104
- detailed = !!opts2.detailed;
1105
- interval = opts2.interval;
1106
- if (opts2.exportDir) exportDir = opts2.exportDir;
1107
- termWidth = process.stdout.columns || 80;
1108
- termHeight = process.stdout.rows || 24;
1109
- history.reset();
1110
- history.setMaxLen(Math.max(20, Math.min(200, termWidth - 30)));
1375
+ if (state.running) return;
1376
+ state.running = true;
1377
+ state.paused = false;
1378
+ state.detailed = !!opts2.detailed;
1379
+ state.interval = opts2.interval;
1380
+ if (opts2.exportDir) state.exportDir = opts2.exportDir;
1381
+ state.termWidth = process.stdout.columns || 80;
1382
+ state.termHeight = process.stdout.rows || 24;
1383
+ state.history.reset();
1384
+ state.history.setMaxLen(Math.max(20, Math.min(200, state.termWidth - 30)));
1111
1385
  process.stdout.write("\x1B[?1049h");
1112
1386
  process.stdout.write("\x1B[?25l");
1113
1387
  process.stdout.write("\x1B[2J\x1B[H");
@@ -1119,30 +1393,30 @@ async function startLive(opts2) {
1119
1393
  if (process.stdin.isTTY) process.stdin.setRawMode(true);
1120
1394
  process.stdin.resume();
1121
1395
  process.stdout.on("resize", onResize);
1122
- keypressHandler = (str, key) => {
1396
+ state.keypressHandler = (str, key) => {
1123
1397
  if (!key) return;
1124
1398
  if (key.ctrl && key.name === "c") {
1125
1399
  stopLive();
1126
1400
  return;
1127
1401
  }
1128
- if (inputMode) {
1402
+ if (state.inputMode) {
1129
1403
  handleInputModeKey(str, key);
1130
1404
  return;
1131
1405
  }
1132
1406
  switch (key.sequence) {
1133
1407
  case "+":
1134
- interval += 1;
1408
+ state.interval += 1;
1135
1409
  restartTicker();
1136
- setStatus(`Interval set to ${interval}s`);
1410
+ setStatus(`Interval set to ${state.interval}s`);
1137
1411
  return;
1138
1412
  case "-":
1139
- interval = Math.max(1, interval - 1);
1413
+ state.interval = Math.max(1, state.interval - 1);
1140
1414
  restartTicker();
1141
- setStatus(`Interval set to ${interval}s`);
1415
+ setStatus(`Interval set to ${state.interval}s`);
1142
1416
  return;
1143
1417
  case "/":
1144
- inputMode = "filter";
1145
- inputBuffer = processFilter;
1418
+ state.inputMode = "filter";
1419
+ state.inputBuffer = state.processFilter;
1146
1420
  render();
1147
1421
  return;
1148
1422
  }
@@ -1151,31 +1425,48 @@ async function startLive(opts2) {
1151
1425
  stopLive();
1152
1426
  break;
1153
1427
  case "c":
1154
- inputMode = "customizer";
1155
- customizerIndex = 0;
1428
+ state.inputMode = "customizer";
1429
+ state.customizerIndex = 0;
1156
1430
  render();
1157
1431
  break;
1158
1432
  case "p":
1159
- paused = !paused;
1160
- setStatus(paused ? "Paused" : "Resumed");
1433
+ state.paused = !state.paused;
1434
+ setStatus(state.paused ? "Paused" : "Resumed");
1161
1435
  render();
1162
1436
  break;
1163
1437
  case "g":
1164
- showGraphs = !showGraphs;
1438
+ state.showGraphs = !state.showGraphs;
1439
+ render();
1440
+ break;
1441
+ case "b":
1442
+ state.graphMode = state.graphMode === "spark" ? "bar" : "spark";
1443
+ setStatus(`Graph mode: ${state.graphMode}`);
1165
1444
  render();
1166
1445
  break;
1167
1446
  case "d":
1168
- detailed = !detailed;
1169
- setStatus(`Detailed sensor mode: ${detailed ? "on" : "off"}`);
1447
+ state.detailed = !state.detailed;
1448
+ setStatus(`Detailed sensor mode: ${state.detailed ? "on" : "off"}`);
1170
1449
  break;
1171
1450
  case "s":
1172
- sortMode = sortMode === "cpu" ? "mem" : sortMode === "mem" ? "pid" : "cpu";
1173
- setStatus(`Sorting by ${sortMode}`);
1451
+ const sortCycle = ["cpu", "mem", "pid", "user", "command", "state", "threads", "runtime"];
1452
+ const curIdx = sortCycle.indexOf(state.sortMode);
1453
+ state.sortMode = sortCycle[(curIdx + 1) % sortCycle.length];
1454
+ setStatus(`Sorting by ${state.sortMode}`);
1174
1455
  render();
1175
1456
  break;
1176
1457
  case "k":
1177
- inputMode = "kill";
1178
- inputBuffer = "";
1458
+ state.inputMode = "kill";
1459
+ state.inputBuffer = "";
1460
+ render();
1461
+ break;
1462
+ case "S":
1463
+ state.inputMode = "signal";
1464
+ state.inputBuffer = "SIGTERM";
1465
+ render();
1466
+ break;
1467
+ case "t":
1468
+ state.treeView = !state.treeView;
1469
+ setStatus(state.treeView ? "Tree view enabled" : "Flat view enabled");
1179
1470
  render();
1180
1471
  break;
1181
1472
  case "e":
@@ -1187,35 +1478,35 @@ async function startLive(opts2) {
1187
1478
  render();
1188
1479
  break;
1189
1480
  case "f":
1190
- exportFormat = exportFormat === "json" ? "csv" : exportFormat === "csv" ? "tsv" : "json";
1191
- setStatus(`Export format: ${exportFormat}`);
1481
+ state.exportFormat = state.exportFormat === "json" ? "csv" : state.exportFormat === "csv" ? "tsv" : "json";
1482
+ setStatus(`Export format: ${state.exportFormat}`);
1192
1483
  render();
1193
1484
  break;
1194
1485
  }
1195
1486
  };
1196
- process.stdin.on("keypress", keypressHandler);
1487
+ process.stdin.on("keypress", state.keypressHandler);
1197
1488
  process.once("SIGINT", () => stopLive());
1198
1489
  }
1199
1490
  function stopLive() {
1200
- if (intervalHandle) {
1201
- clearInterval(intervalHandle);
1202
- intervalHandle = null;
1491
+ if (state.intervalHandle) {
1492
+ clearInterval(state.intervalHandle);
1493
+ state.intervalHandle = null;
1203
1494
  }
1204
- if (statusTimer) {
1205
- clearTimeout(statusTimer);
1206
- statusTimer = null;
1495
+ if (state.statusTimer) {
1496
+ clearTimeout(state.statusTimer);
1497
+ state.statusTimer = null;
1207
1498
  }
1208
- if (logStream) {
1209
- logStream.end();
1210
- logStream = null;
1499
+ if (state.logStream) {
1500
+ state.logStream.end();
1501
+ state.logStream = null;
1211
1502
  }
1212
- if (keypressHandler) {
1213
- process.stdin.removeListener("keypress", keypressHandler);
1214
- keypressHandler = null;
1503
+ if (state.keypressHandler) {
1504
+ process.stdin.removeListener("keypress", state.keypressHandler);
1505
+ state.keypressHandler = null;
1215
1506
  }
1216
1507
  process.stdout.removeListener("resize", onResize);
1217
- running = false;
1218
- logging = false;
1508
+ state.running = false;
1509
+ state.logging = false;
1219
1510
  if (process.stdin.isTTY) process.stdin.setRawMode(false);
1220
1511
  process.stdin.pause();
1221
1512
  process.stdout.write("\x1B[?25h");
@@ -1224,9 +1515,120 @@ function stopLive() {
1224
1515
  process.exit(0);
1225
1516
  }
1226
1517
 
1518
+ // src/splash.ts
1519
+ import chalk6 from "chalk";
1520
+ var MAXV = 36;
1521
+ var CHAR_RAMP = [" ", ".", "`", ",", ":", ";", "+", "*", "?", "%", "#", "@"];
1522
+ var palette = [
1523
+ "#0a0505",
1524
+ "#1a0806",
1525
+ "#2a0b06",
1526
+ "#3f1004",
1527
+ "#591404",
1528
+ "#791804",
1529
+ "#9e1c04",
1530
+ "#c92004",
1531
+ "#d63004",
1532
+ "#e34004",
1533
+ "#f05004",
1534
+ "#fd6004",
1535
+ "#ff7004",
1536
+ "#ff8004",
1537
+ "#ff9004",
1538
+ "#ffa004",
1539
+ "#ffb004",
1540
+ "#ffc004",
1541
+ "#ffd004",
1542
+ "#ffe004",
1543
+ "#fff004",
1544
+ "#fff820",
1545
+ "#ffff40",
1546
+ "#ffff60",
1547
+ "#ffff80",
1548
+ "#ffffa0",
1549
+ "#ffffc0",
1550
+ "#ffffe0",
1551
+ "#ffffff",
1552
+ "#ffffff",
1553
+ "#ffffff",
1554
+ "#ffffff",
1555
+ "#ffffff",
1556
+ "#ffffff",
1557
+ "#ffffff",
1558
+ "#ffffff"
1559
+ ];
1560
+ function getRgb(hex) {
1561
+ const r = parseInt(hex.slice(1, 3), 16);
1562
+ const g = parseInt(hex.slice(3, 5), 16);
1563
+ const b = parseInt(hex.slice(5, 7), 16);
1564
+ return { r, g, b };
1565
+ }
1566
+ var rgbPalette = palette.map(getRgb);
1567
+ function getChar(val) {
1568
+ const idx = Math.min(CHAR_RAMP.length - 1, Math.floor(val / (MAXV + 1) * CHAR_RAMP.length));
1569
+ return CHAR_RAMP[idx];
1570
+ }
1571
+ async function showSplash() {
1572
+ const cols = 60;
1573
+ const rows = 15;
1574
+ let fire = new Uint8Array(cols * rows);
1575
+ process.stdout.write("\x1B[2J");
1576
+ const termRows = process.stdout.rows || 24;
1577
+ const termCols = process.stdout.columns || 80;
1578
+ const startRow = Math.max(0, Math.floor((termRows - rows) / 2));
1579
+ const startCol = Math.max(0, Math.floor((termCols - cols) / 2));
1580
+ return new Promise((resolve) => {
1581
+ const interval = setInterval(() => {
1582
+ for (let x = 0; x < cols; x++) {
1583
+ fire[(rows - 1) * cols + x] = MAXV - Math.floor(Math.random() * 4);
1584
+ }
1585
+ for (let y = rows - 1; y >= 1; y--) {
1586
+ for (let x = 0; x < cols; x++) {
1587
+ const src = y * cols + x;
1588
+ const val = fire[src];
1589
+ if (val === 0) {
1590
+ fire[(y - 1) * cols + x] = 0;
1591
+ continue;
1592
+ }
1593
+ const decay = Math.floor(Math.random() * 3);
1594
+ const drift = Math.floor(Math.random() * 3) - 1;
1595
+ let nx = x + drift;
1596
+ if (nx < 0) nx = 0;
1597
+ else if (nx >= cols) nx = cols - 1;
1598
+ fire[(y - 1) * cols + nx] = Math.max(0, val - decay);
1599
+ }
1600
+ }
1601
+ let output = "";
1602
+ for (let i = 0; i < startRow; i++) output += "\n";
1603
+ for (let y = 0; y < rows; y++) {
1604
+ output += " ".repeat(startCol);
1605
+ for (let x = 0; x < cols; x++) {
1606
+ const v = fire[y * cols + x];
1607
+ const color = rgbPalette[Math.min(v, rgbPalette.length - 1)];
1608
+ output += chalk6.rgb(color.r, color.g, color.b)(getChar(v));
1609
+ }
1610
+ output += "\n";
1611
+ }
1612
+ output += " ".repeat(Math.max(0, startCol + (cols - 45) / 2)) + chalk6.bold.white("\u{1F525} pyre \u2014 system monitor (warming up sensors...)\n");
1613
+ output += " ".repeat(Math.max(0, startCol + (cols - 25) / 2)) + chalk6.dim("[Press Enter to continue]");
1614
+ process.stdout.write(`\x1B[H${output}`);
1615
+ }, 65);
1616
+ const onEnter = () => {
1617
+ process.stdin.removeListener("data", onEnter);
1618
+ clearInterval(interval);
1619
+ process.stdout.write("\x1B[2J\x1B[H");
1620
+ resolve();
1621
+ };
1622
+ process.stdin.setRawMode(true);
1623
+ process.stdin.resume();
1624
+ process.stdin.on("data", onEnter);
1625
+ setTimeout(onEnter, 5e3);
1626
+ });
1627
+ }
1628
+
1227
1629
  // src/index.ts
1228
1630
  var program = new Command();
1229
- program.name("pyre").version("1.1.0").description("Mac system monitoring CLI: interactive live dashboard, stats, graphs, export").option("-j, --json", "Output as JSON").option("-c, --csv", "Output as CSV").option("-t, --tsv", "Output as TSV").option("--detailed", "Include detailed system info and sensor readings").option("--interval <seconds>", "Refresh interval for live mode", "2").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", "./pyre-exports").option("--log", "Start continuous CSV logging immediately when live mode starts");
1631
+ program.name("pyre").version("2.0.0").description("Mac system monitoring CLI: interactive live dashboard, stats, graphs, export").option("-j, --json", "Output as JSON").option("-c, --csv", "Output as CSV").option("-t, --tsv", "Output as TSV").option("--detailed", "Include detailed system info and sensor readings").option("--interval <seconds>", "Refresh interval for live mode", "2").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", "./pyre-exports").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", "cpu");
1230
1632
  program.parse(process.argv);
1231
1633
  var opts = program.opts();
1232
1634
  function isExportMode() {
@@ -1238,9 +1640,10 @@ function stripAnsi(s) {
1238
1640
  async function main() {
1239
1641
  const requestedLive = program.args[0] === "live" || !isExportMode() && !opts.once;
1240
1642
  if (requestedLive) {
1241
- const interval2 = parseFloat(opts.interval) || 2;
1643
+ await showSplash();
1644
+ const interval = parseFloat(opts.interval) || 2;
1242
1645
  await startLive({
1243
- interval: interval2,
1646
+ interval,
1244
1647
  detailed: opts.detailed,
1245
1648
  exportDir: opts.exportDir,
1246
1649
  autoLog: opts.log
@@ -1252,16 +1655,16 @@ async function main() {
1252
1655
  if (opts.json) output = formatJson(data);
1253
1656
  else if (opts.csv) output = formatCsv(data);
1254
1657
  else if (opts.tsv) output = formatTsv(data);
1255
- else output = formatTable(data, { width: process.stdout.columns || 80 });
1658
+ else output = formatTable(data, { width: process.stdout.columns || 80, sortBy: opts.sort, treeView: opts.tree });
1256
1659
  console.log(output);
1257
1660
  if (opts.out) {
1258
1661
  fs2.writeFileSync(opts.out, stripAnsi(output));
1259
- console.log(chalk4.dim(`
1662
+ console.log(chalk7.dim(`
1260
1663
  Saved to ${opts.out}`));
1261
1664
  }
1262
1665
  }
1263
1666
  main().catch((err) => {
1264
- console.error(chalk4.red(`Error: ${err.message}`));
1667
+ console.error(chalk7.red(`Error: ${err.message}`));
1265
1668
  stopLive();
1266
1669
  process.exit(1);
1267
1670
  });