pyre-cli 5.1.0 → 6.1.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/README.md +1 -1
- package/dist/index.js +922 -367
- package/package.json +1 -1
- package/src/completions.ts +118 -0
- package/src/diffCmd.ts +36 -0
- package/src/doctor.ts +123 -0
- package/src/formatters/index.ts +1 -1
- package/src/formatters/output.ts +14 -13
- package/src/formatters/render.ts +305 -114
- package/src/formatters/themes.ts +47 -12
- package/src/formatters/types.ts +6 -1
- package/src/historyCmd.ts +71 -0
- package/src/index.ts +129 -24
- package/src/live/input.ts +14 -5
- package/src/live/render.ts +126 -90
- package/src/live/state.ts +3 -0
- package/src/live/types.ts +6 -1
- package/src/monitors/collectors.ts +160 -36
- package/src/monitors/smc.ts +60 -12
- package/src/monitors/types.ts +2 -0
- package/src/p2p/server.test.ts +2 -2
- package/src/sparkline.ts +82 -13
- package/src/state/config.ts +6 -1
- package/Formula/pyre.rb +0 -19
package/src/formatters/render.ts
CHANGED
|
@@ -10,22 +10,58 @@
|
|
|
10
10
|
* all layout-related code is co-located.
|
|
11
11
|
*/
|
|
12
12
|
import chalk from 'chalk';
|
|
13
|
-
import { sparkline } from '../sparkline.js';
|
|
13
|
+
import { sparkline, multiRowSparkline } from '../sparkline.js';
|
|
14
14
|
import type { History } from '../history.js';
|
|
15
15
|
import type { StatsData, VisibleItems, TableOptions, AnomalyAlert } from './types.js';
|
|
16
|
-
import { THEMES, type ThemeName } from './themes.js';
|
|
16
|
+
import { THEMES, type ThemeName, type ThemeColors } from './themes.js';
|
|
17
|
+
|
|
17
18
|
|
|
18
19
|
// --- low-level box drawing ---------------------------------------------------
|
|
19
20
|
|
|
20
|
-
function visLen(s: string): number {
|
|
21
|
+
export function visLen(s: string): number {
|
|
21
22
|
// eslint-disable-next-line no-control-regex
|
|
22
23
|
return s.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
function
|
|
26
|
+
export function fitVisible(s: string, width: number): string {
|
|
27
|
+
const w = Math.max(0, width);
|
|
26
28
|
const len = visLen(s);
|
|
27
|
-
if (len
|
|
28
|
-
return s + ' '.repeat(
|
|
29
|
+
if (len === w) return s;
|
|
30
|
+
if (len < w) return s + ' '.repeat(w - len);
|
|
31
|
+
|
|
32
|
+
let visibleCount = 0;
|
|
33
|
+
let out = '';
|
|
34
|
+
let inAnsi = false;
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < s.length; i++) {
|
|
37
|
+
const char = s[i];
|
|
38
|
+
if (char === '\x1b') {
|
|
39
|
+
inAnsi = true;
|
|
40
|
+
out += char;
|
|
41
|
+
} else if (inAnsi) {
|
|
42
|
+
out += char;
|
|
43
|
+
if ((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z')) {
|
|
44
|
+
inAnsi = false;
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
if (visibleCount < w) {
|
|
48
|
+
out += char;
|
|
49
|
+
visibleCount++;
|
|
50
|
+
} else {
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
out += '\x1b[0m';
|
|
56
|
+
const resLen = visLen(out);
|
|
57
|
+
if (resLen < w) {
|
|
58
|
+
out += ' '.repeat(w - resLen);
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function padVisible(s: string, width: number): string {
|
|
64
|
+
return fitVisible(s, width);
|
|
29
65
|
}
|
|
30
66
|
|
|
31
67
|
function truncatePlain(s: string, width: number): string {
|
|
@@ -40,29 +76,50 @@ export function panel(
|
|
|
40
76
|
width: number,
|
|
41
77
|
accent: (s: string) => string,
|
|
42
78
|
borderAccent: (s: string) => string,
|
|
43
|
-
height?: number
|
|
79
|
+
height?: number,
|
|
80
|
+
rightTitle?: string
|
|
44
81
|
): string[] {
|
|
45
82
|
const h = height ?? lines.length;
|
|
46
83
|
const contentWidth = Math.max(1, width - 4);
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const
|
|
84
|
+
const leftLabel = title ? ` ${title} ` : '';
|
|
85
|
+
const rightLabel = rightTitle ? ` ${rightTitle} ` : '';
|
|
86
|
+
const innerW = Math.max(0, width - 2);
|
|
87
|
+
let topInner = leftLabel + '─'.repeat(Math.max(0, innerW - visLen(leftLabel) - visLen(rightLabel))) + rightLabel;
|
|
88
|
+
if (visLen(topInner) > innerW) {
|
|
89
|
+
topInner = fitVisible(topInner, innerW);
|
|
90
|
+
} else if (visLen(topInner) < innerW) {
|
|
91
|
+
topInner = topInner + '─'.repeat(innerW - visLen(topInner));
|
|
92
|
+
}
|
|
93
|
+
const top = borderAccent('┌' + topInner + '┐');
|
|
94
|
+
const bottom = borderAccent('└' + '─'.repeat(innerW) + '┘');
|
|
50
95
|
const body: string[] = [];
|
|
51
96
|
for (let i = 0; i < h; i++) {
|
|
52
97
|
const line = lines[i] ?? '';
|
|
53
|
-
body.push(`${borderAccent('│')} ${
|
|
98
|
+
body.push(`${borderAccent('│')} ${fitVisible(line, contentWidth)} ${borderAccent('│')}`);
|
|
54
99
|
}
|
|
55
100
|
return [top, ...body, bottom];
|
|
56
101
|
}
|
|
57
102
|
|
|
103
|
+
export function gaugeBar(percent: number, width = 12): string {
|
|
104
|
+
const w = Math.max(1, width);
|
|
105
|
+
const pct = Math.min(100, Math.max(0, percent));
|
|
106
|
+
const filled = Math.round((pct / 100) * w);
|
|
107
|
+
const empty = w - filled;
|
|
108
|
+
const filledStr = '█'.repeat(filled);
|
|
109
|
+
const emptyStr = '░'.repeat(empty);
|
|
110
|
+
const color = pct > 85 ? chalk.red : pct > 65 ? chalk.yellow : chalk.cyan;
|
|
111
|
+
return color(filledStr) + chalk.dim(emptyStr);
|
|
112
|
+
}
|
|
113
|
+
|
|
58
114
|
/** Join equal-or-uneven-height panel blocks side by side with a gap. */
|
|
59
115
|
function hstack(blocks: string[][], gap = 2, targetWidth?: number): string[] {
|
|
60
116
|
if (!blocks.length) return [];
|
|
61
117
|
const height = Math.max(...blocks.map(b => b.length));
|
|
62
|
-
const widths = blocks.map(b =>
|
|
118
|
+
const widths = blocks.map(b => Math.max(0, ...b.map(l => visLen(l))));
|
|
63
119
|
const padded = blocks.map((b, i) => {
|
|
64
|
-
const
|
|
65
|
-
const
|
|
120
|
+
const w = widths[i];
|
|
121
|
+
const filler = ' '.repeat(w);
|
|
122
|
+
const arr = b.map(line => fitVisible(line, w));
|
|
66
123
|
while (arr.length < height) arr.push(filler);
|
|
67
124
|
return arr;
|
|
68
125
|
});
|
|
@@ -70,14 +127,14 @@ function hstack(blocks: string[][], gap = 2, targetWidth?: number): string[] {
|
|
|
70
127
|
for (let row = 0; row < height; row++) {
|
|
71
128
|
let line = padded.map(a => a[row]).join(' '.repeat(gap));
|
|
72
129
|
if (targetWidth !== undefined) {
|
|
73
|
-
|
|
74
|
-
line += ' '.repeat(pad);
|
|
130
|
+
line = fitVisible(line, targetWidth);
|
|
75
131
|
}
|
|
76
132
|
out.push(line);
|
|
77
133
|
}
|
|
78
134
|
return out;
|
|
79
135
|
}
|
|
80
136
|
|
|
137
|
+
|
|
81
138
|
function statRow(label: string, value: string, width = 9): string {
|
|
82
139
|
const w = Math.max(width, label.length + 1);
|
|
83
140
|
return `${chalk.dim(label.padEnd(w))}${value}`;
|
|
@@ -117,7 +174,8 @@ export function capacityColor(percent: number) {
|
|
|
117
174
|
}
|
|
118
175
|
|
|
119
176
|
export function formatBytes(bytes: number): string {
|
|
120
|
-
if (bytes <
|
|
177
|
+
if (isNaN(bytes) || bytes < 0) return '0 B';
|
|
178
|
+
if (bytes < 1024) return `${Math.round(bytes)} B`;
|
|
121
179
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
122
180
|
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
123
181
|
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
@@ -280,7 +338,7 @@ function header(data: StatsData, width: number, badges: string[] = [], borderAcc
|
|
|
280
338
|
const left = `${chalk.hex('#ff5500').bold(' PYRE ')} ${chalk.bold(data.header.hostname)} ${chalk.dim(data.header.os)} ${chalk.dim('up ' + data.header.uptime)}`;
|
|
281
339
|
const right = `${chalk.dim(now)} ${badges.join(' ')}`;
|
|
282
340
|
const gap = Math.max(1, width - visLen(left) - visLen(right));
|
|
283
|
-
const line = left + ' '.repeat(gap) + right;
|
|
341
|
+
const line = fitVisible(left + ' '.repeat(gap) + right, width);
|
|
284
342
|
return [line, borderAccent('─'.repeat(width))];
|
|
285
343
|
}
|
|
286
344
|
|
|
@@ -342,9 +400,27 @@ function tabBar(activePanel: string, width: number, theme: ThemeColors): string
|
|
|
342
400
|
return `${keyHint}${label}`;
|
|
343
401
|
});
|
|
344
402
|
|
|
345
|
-
const
|
|
346
|
-
const
|
|
347
|
-
|
|
403
|
+
const separator = chalk.dim(' │ ');
|
|
404
|
+
const joined = rendered.join(separator);
|
|
405
|
+
const indent = ' ';
|
|
406
|
+
const available = width - visLen(indent);
|
|
407
|
+
let result = joined;
|
|
408
|
+
if (visLen(joined) > available) {
|
|
409
|
+
let used = 0;
|
|
410
|
+
const kept: string[] = [];
|
|
411
|
+
for (let i = 0; i < rendered.length; i++) {
|
|
412
|
+
const tabW = visLen(rendered[i]) + (kept.length > 0 ? visLen(separator) : 0);
|
|
413
|
+
if (used + tabW > available && kept.length > 0) break;
|
|
414
|
+
kept.push(rendered[i]);
|
|
415
|
+
used += tabW;
|
|
416
|
+
}
|
|
417
|
+
if (kept.length < rendered.length) {
|
|
418
|
+
result = kept.join(separator) + chalk.dim('⋯');
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const pad = Math.max(0, available - visLen(result));
|
|
423
|
+
return fitVisible(chalk.dim(indent) + result + ' '.repeat(pad), width);
|
|
348
424
|
}
|
|
349
425
|
|
|
350
426
|
// --- process / disk tables ---------------------------------------------------
|
|
@@ -596,126 +672,241 @@ function anomalyPanel(anomalies: AnomalyAlert[], width: number, theme: ThemeColo
|
|
|
596
672
|
for (const a of anomalies) {
|
|
597
673
|
const color = a.severity === 'critical' ? chalk.red.bold : chalk.yellow;
|
|
598
674
|
const direction = a.zScore > 0 ? '↑ spike' : '↓ drop';
|
|
599
|
-
|
|
675
|
+
const valStr = a.metric.toLowerCase().includes('net') ? `${formatBytes(a.value)}/s` : `${a.value.toFixed(1)}%`;
|
|
676
|
+
lines.push(color(` ⚠ ${a.metric}: ${valStr} (${direction}, σ=${a.zScore.toFixed(1)})`));
|
|
600
677
|
}
|
|
601
678
|
return lines;
|
|
602
679
|
}
|
|
603
680
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
const columns = gridColumns(width);
|
|
608
|
-
const gap = 2;
|
|
609
|
-
const cardWidth = Math.floor((width - gap * (columns - 1)) / columns);
|
|
610
|
-
const contentWidth = cardWidth - 4;
|
|
611
|
-
|
|
612
|
-
const themeName = opts.theme || 'default';
|
|
613
|
-
const theme = THEMES[themeName] || THEMES.default;
|
|
614
|
-
const vis = opts.visible ?? {};
|
|
615
|
-
|
|
616
|
-
const out: string[] = [];
|
|
617
|
-
out.push(...header(data, width, [], theme.border));
|
|
618
|
-
out.push('');
|
|
619
|
-
|
|
620
|
-
const anomalies = opts.anomalies ?? [];
|
|
621
|
-
if (anomalies.length && activePanel === 'grid') {
|
|
622
|
-
out.push(...anomalyPanel(anomalies, width, theme));
|
|
623
|
-
out.push('');
|
|
624
|
-
}
|
|
681
|
+
function btopCpuBox(data: StatsData, width: number, theme: ThemeColors, opts: TableOptions): string[] {
|
|
682
|
+
const contentWidth = width - 4;
|
|
683
|
+
const rightTitle = `${data.header.hostname} · up ${data.header.uptime} · ${data.cpu.usage.toFixed(0)}%`;
|
|
625
684
|
|
|
626
|
-
|
|
627
|
-
|
|
685
|
+
if (contentWidth >= 65) {
|
|
686
|
+
const leftW = Math.floor(contentWidth * 0.48);
|
|
687
|
+
const rightW = contentWidth - leftW - 2;
|
|
628
688
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
689
|
+
const leftLines: string[] = [];
|
|
690
|
+
leftLines.push(fitVisible(theme.cpu('CPU Usage History'), leftW));
|
|
691
|
+
|
|
692
|
+
const cpuHistory = opts.history?.cpuUsage || [];
|
|
693
|
+
if (cpuHistory.length > 1) {
|
|
694
|
+
const graphRows = multiRowSparkline(cpuHistory, 3, { min: 0, max: 100 });
|
|
695
|
+
for (const gr of graphRows) {
|
|
696
|
+
leftLines.push(theme.cpu(fitVisible(gr, leftW)));
|
|
697
|
+
}
|
|
698
|
+
} else {
|
|
699
|
+
leftLines.push(theme.cpu(fitVisible(gaugeBar(data.cpu.usage, leftW), leftW)));
|
|
700
|
+
leftLines.push(' '.repeat(leftW));
|
|
701
|
+
leftLines.push(' '.repeat(leftW));
|
|
633
702
|
}
|
|
634
|
-
|
|
703
|
+
|
|
704
|
+
const rightLines: string[] = [];
|
|
705
|
+
rightLines.push(fitVisible(chalk.bold(truncatePlain(data.cpu.brand, rightW)), rightW));
|
|
706
|
+
|
|
707
|
+
const cpuBarW = Math.max(6, rightW - 18);
|
|
708
|
+
const tempStr = data.cpu.temperature ? formatTempUnit(data.cpu.temperature, opts.tempUnit) : '';
|
|
709
|
+
rightLines.push(fitVisible(`CPU ${gaugeBar(data.cpu.usage, cpuBarW)} ${data.cpu.usage.toFixed(0).padStart(3)}% ${tempStr}`, rightW));
|
|
710
|
+
|
|
711
|
+
const loadStr = data.cpu.loadAvg.map(l => l.toFixed(2)).join(' ');
|
|
712
|
+
rightLines.push(fitVisible(`${chalk.dim('Load:')} ${loadStr}`, rightW));
|
|
713
|
+
|
|
714
|
+
if (data.cpu.coreUsage && data.cpu.coreUsage.length) {
|
|
715
|
+
const coreColW = Math.floor((rightW - 1) / 2);
|
|
716
|
+
const half = Math.ceil(data.cpu.coreUsage.length / 2);
|
|
717
|
+
for (let i = 0; i < half; i++) {
|
|
718
|
+
const u1 = data.cpu.coreUsage[i];
|
|
719
|
+
const u2 = data.cpu.coreUsage[i + half];
|
|
720
|
+
const c1BarW = Math.max(3, coreColW - 8);
|
|
721
|
+
const c1Str = `C${i} ${gaugeBar(u1, c1BarW)} ${Math.round(u1)}%`;
|
|
722
|
+
const c2Str = u2 !== undefined ? `C${i + half} ${gaugeBar(u2, c1BarW)} ${Math.round(u2)}%` : '';
|
|
723
|
+
rightLines.push(fitVisible(fitVisible(c1Str, coreColW) + ' ' + fitVisible(c2Str, coreColW), rightW));
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (data.gpu) {
|
|
728
|
+
rightLines.push(fitVisible(`GPU ${gaugeBar(data.gpu.utilization, cpuBarW)} ${data.gpu.utilization}% ${formatBytes(data.gpu.memory)}`, rightW));
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const combinedBody = hstack([leftLines, rightLines], 2, contentWidth);
|
|
732
|
+
return panel('1 cpu', combinedBody, width, theme.cpu, theme.border, combinedBody.length, rightTitle);
|
|
733
|
+
} else {
|
|
734
|
+
const lines: string[] = [];
|
|
735
|
+
lines.push(chalk.bold(truncatePlain(data.cpu.brand, contentWidth)));
|
|
736
|
+
lines.push(`CPU ${gaugeBar(data.cpu.usage, Math.max(4, contentWidth - 12))} ${data.cpu.usage.toFixed(0)}%`);
|
|
737
|
+
if (opts.history?.cpuUsage.length) {
|
|
738
|
+
lines.push(theme.cpu(sparkline(opts.history.cpuUsage, { min: 0, max: 100 })));
|
|
739
|
+
}
|
|
740
|
+
lines.push(`Load: ${data.cpu.loadAvg.map(l => l.toFixed(2)).join(' ')}`);
|
|
741
|
+
return panel('1 cpu', lines, width, theme.cpu, theme.border, lines.length, rightTitle);
|
|
635
742
|
}
|
|
743
|
+
}
|
|
636
744
|
|
|
637
|
-
|
|
638
|
-
const
|
|
745
|
+
function btopLeftColumn(data: StatsData, width: number, theme: ThemeColors, opts: TableOptions): string[] {
|
|
746
|
+
const contentWidth = width - 4;
|
|
747
|
+
const out: string[] = [];
|
|
639
748
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
749
|
+
// --- Box 1: 2 mem ---
|
|
750
|
+
const memLines: string[] = [];
|
|
751
|
+
memLines.push(`RAM [${gaugeBar(data.memory.usagePercent, Math.max(4, contentWidth - 12))}] ${data.memory.usagePercent}%`);
|
|
752
|
+
memLines.push(`Used: ${formatBytes(data.memory.used)} / ${formatBytes(data.memory.total)}`);
|
|
753
|
+
memLines.push(`Free: ${formatBytes(data.memory.free)} · Cache: ${formatBytes(data.memory.total - data.memory.free - data.memory.used)}`);
|
|
754
|
+
if (data.memory.swapTotal > 0) {
|
|
755
|
+
const swapPct = Math.round((data.memory.swapUsed / data.memory.swapTotal) * 100);
|
|
756
|
+
memLines.push(`Swap [${gaugeBar(swapPct, Math.max(4, contentWidth - 12))}] ${swapPct}%`);
|
|
757
|
+
}
|
|
758
|
+
out.push(...panel('2 mem', memLines, width, theme.mem, theme.border, memLines.length, `${formatBytes(data.memory.used)}`));
|
|
759
|
+
|
|
760
|
+
// --- Box 2: 0 disk ---
|
|
761
|
+
if (data.disk.length) {
|
|
762
|
+
const diskLines: string[] = [];
|
|
763
|
+
for (const d of data.disk.slice(0, 2)) {
|
|
764
|
+
const capNum = parseInt(d.capacity, 10) || 0;
|
|
765
|
+
const mountShort = truncatePlain(d.mountpoint, 8);
|
|
766
|
+
let ioStr = '';
|
|
767
|
+
if (d.readBytesSec !== undefined || d.writeBytesSec !== undefined) {
|
|
768
|
+
const rStr = formatBytes(d.readBytesSec || 0);
|
|
769
|
+
const wStr = formatBytes(d.writeBytesSec || 0);
|
|
770
|
+
ioStr = ` R:${rStr}/s W:${wStr}/s`;
|
|
771
|
+
}
|
|
772
|
+
diskLines.push(`${mountShort} ${d.used}/${d.size} [${gaugeBar(capNum, Math.max(3, contentWidth - 18 - ioStr.length))}] ${d.capacity}${ioStr}`);
|
|
773
|
+
}
|
|
774
|
+
const totalRead = data.disk.reduce((acc, d) => acc + (d.readBytesSec || 0), 0);
|
|
775
|
+
const totalWrite = data.disk.reduce((acc, d) => acc + (d.writeBytesSec || 0), 0);
|
|
776
|
+
const diskHeaderRight = `R:${formatBytes(totalRead)}/s W:${formatBytes(totalWrite)}/s`;
|
|
777
|
+
out.push(...panel('0 disk io', diskLines, width, theme.disk, theme.border, diskLines.length, diskHeaderRight));
|
|
657
778
|
}
|
|
658
|
-
|
|
659
|
-
|
|
779
|
+
|
|
780
|
+
// --- Box 3: 7 net ---
|
|
781
|
+
const netLines: string[] = [];
|
|
782
|
+
const rxRate = opts.history?.netRxRate.length ? opts.history.netRxRate[opts.history.netRxRate.length - 1] : 0;
|
|
783
|
+
const txRate = opts.history?.netTxRate.length ? opts.history.netTxRate[opts.history.netTxRate.length - 1] : 0;
|
|
784
|
+
|
|
785
|
+
netLines.push(`down ▼ ${formatBytes(rxRate)}/s (${formatBytes(data.network.rxBytes)})`);
|
|
786
|
+
if (opts.history?.netRxRate.length) {
|
|
787
|
+
netLines.push(chalk.cyan(fitVisible(sparkline(opts.history.netRxRate, {}), contentWidth)));
|
|
660
788
|
}
|
|
661
|
-
|
|
662
|
-
|
|
789
|
+
|
|
790
|
+
netLines.push(`up ▲ ${formatBytes(txRate)}/s (${formatBytes(data.network.txBytes)})`);
|
|
791
|
+
if (opts.history?.netTxRate.length) {
|
|
792
|
+
netLines.push(theme.network(fitVisible(sparkline(opts.history.netTxRate, {}), contentWidth)));
|
|
663
793
|
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
794
|
+
|
|
795
|
+
out.push(...panel('7 net', netLines, width, theme.network, theme.border, netLines.length, `${data.network.interface}`));
|
|
796
|
+
|
|
797
|
+
return out;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function btopProcBox(data: StatsData, width: number, targetHeight: number, theme: ThemeColors, opts: TableOptions): string[] {
|
|
801
|
+
const contentWidth = width - 4;
|
|
802
|
+
const filtered = opts.filter
|
|
803
|
+
? data.processes.filter(p => p.command.toLowerCase().includes(opts.filter!.toLowerCase()))
|
|
804
|
+
: data.processes;
|
|
805
|
+
const sorted = sortProcesses(filtered, opts.sortBy ?? 'cpu');
|
|
806
|
+
|
|
807
|
+
const headerRowOverhead = 2; // top & bottom border
|
|
808
|
+
const tableHeaderOverhead = 2; // header + separator line
|
|
809
|
+
const availableRows = Math.max(1, targetHeight - headerRowOverhead - tableHeaderOverhead);
|
|
810
|
+
|
|
811
|
+
const limited = sorted.slice(0, availableRows);
|
|
812
|
+
|
|
813
|
+
const pidW = 6, userW = 7, thrW = 3, memW = 6, cpuW = 6;
|
|
814
|
+
const fixedW = pidW + userW + thrW + memW + cpuW + 5;
|
|
815
|
+
const cmdW = Math.max(8, contentWidth - fixedW);
|
|
816
|
+
|
|
817
|
+
const head =
|
|
818
|
+
chalk.bold('PID'.padEnd(pidW)) + ' ' +
|
|
819
|
+
chalk.bold('PROGRAM'.padEnd(cmdW)) + ' ' +
|
|
820
|
+
chalk.bold('USER'.padEnd(userW)) + ' ' +
|
|
821
|
+
chalk.bold('THR'.padStart(thrW)) + ' ' +
|
|
822
|
+
chalk.bold('MEM%'.padStart(memW)) + ' ' +
|
|
823
|
+
chalk.bold('CPU%'.padStart(cpuW));
|
|
824
|
+
|
|
825
|
+
if (opts.treeView) {
|
|
826
|
+
const treeLines = buildProcessTree(limited);
|
|
827
|
+
const rightTitle = opts.filter ? `filter: "${opts.filter}"` : `tree · sort: ${opts.sortBy || 'cpu'}`;
|
|
828
|
+
return panel('P proc', [head, theme.border('─'.repeat(contentWidth)), ...treeLines], width, theme.process, theme.border, targetHeight - 2, rightTitle);
|
|
667
829
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
830
|
+
|
|
831
|
+
const rows = limited.map(p => {
|
|
832
|
+
const cpuVal = p.cpu.toFixed(1) + '%';
|
|
833
|
+
const cpuStr = p.cpu > 50 ? chalk.red.bold(cpuVal.padStart(cpuW)) : p.cpu > 20 ? chalk.yellow(cpuVal.padStart(cpuW)) : chalk.green(cpuVal.padStart(cpuW));
|
|
834
|
+
const memStr = (p.mem.toFixed(1) + '%').padStart(memW);
|
|
835
|
+
const userStr = truncatePlain(p.user, userW).padEnd(userW);
|
|
836
|
+
const thrStr = String(p.threads).padStart(thrW);
|
|
837
|
+
const pidStr = String(p.pid).padEnd(pidW);
|
|
838
|
+
const cmdStr = truncatePlain(p.command, cmdW).padEnd(cmdW);
|
|
839
|
+
|
|
840
|
+
return `${pidStr} ${cmdStr} ${userStr} ${thrStr} ${memStr} ${cpuStr}`;
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
const lines = [head, theme.border('─'.repeat(contentWidth)), ...rows];
|
|
844
|
+
if (rows.length === 0) {
|
|
845
|
+
lines.push(chalk.dim(`No processes match "${opts.filter || ''}"`));
|
|
671
846
|
}
|
|
672
847
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
848
|
+
const rightTitle = opts.filter ? `filter: "${opts.filter}"` : `sort: ${opts.sortBy || 'cpu'}`;
|
|
849
|
+
return panel('P proc', lines, width, theme.process, theme.border, targetHeight - 2, rightTitle);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
export function formatTable(data: StatsData, opts: TableOptions = {}): string {
|
|
853
|
+
const width = clampWidth(opts.width);
|
|
854
|
+
const activePanel = opts.activePanel || 'grid';
|
|
855
|
+
|
|
856
|
+
const themeName = opts.theme || 'default';
|
|
857
|
+
const theme = THEMES[themeName] || THEMES.default;
|
|
858
|
+
|
|
859
|
+
const out: string[] = [];
|
|
860
|
+
out.push(...header(data, width, [], theme.border));
|
|
861
|
+
out.push('');
|
|
680
862
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
out.push(...
|
|
863
|
+
const anomalies = opts.anomalies ?? [];
|
|
864
|
+
if (anomalies.length && activePanel === 'grid') {
|
|
865
|
+
out.push(...anomalyPanel(anomalies, width, theme));
|
|
684
866
|
out.push('');
|
|
685
867
|
}
|
|
686
868
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
869
|
+
out.push(tabBar(activePanel, width, theme));
|
|
870
|
+
out.push('');
|
|
871
|
+
|
|
872
|
+
if (activePanel !== 'grid') {
|
|
873
|
+
const lines = activeDetailLines(data, activePanel, width, opts);
|
|
874
|
+
if (lines) {
|
|
875
|
+
out.push(...panel(detailPanelTitle(activePanel), lines, width, theme[activePanel as keyof ThemeColors] ?? theme.cpu, theme.border));
|
|
876
|
+
}
|
|
877
|
+
return out.join('\n');
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// --- btop Dashboard Grid ---
|
|
881
|
+
// 1. Top CPU & System Overview Box
|
|
882
|
+
out.push(...btopCpuBox(data, width, theme, opts));
|
|
883
|
+
out.push('');
|
|
884
|
+
|
|
885
|
+
// 2. Bottom Split Section
|
|
886
|
+
if (width >= 85) {
|
|
887
|
+
const leftColW = Math.max(32, Math.floor(width * 0.38));
|
|
888
|
+
const rightColW = width - leftColW - 2;
|
|
889
|
+
|
|
890
|
+
const leftLines = btopLeftColumn(data, leftColW, theme, opts);
|
|
891
|
+
const procLines = btopProcBox(data, rightColW, leftLines.length, theme, opts);
|
|
892
|
+
|
|
893
|
+
out.push(...hstack([leftLines, procLines], 2, width));
|
|
894
|
+
} else {
|
|
895
|
+
const leftLines = btopLeftColumn(data, width, theme, opts);
|
|
896
|
+
out.push(...leftLines);
|
|
897
|
+
out.push('');
|
|
898
|
+
const procLines = btopProcBox(data, width, 12, theme, opts);
|
|
899
|
+
out.push(...procLines);
|
|
900
|
+
}
|
|
711
901
|
|
|
712
902
|
return out.join('\n');
|
|
713
903
|
}
|
|
714
904
|
|
|
905
|
+
|
|
715
906
|
/** Clamp/normalize a terminal width to a sane range, defaulting to 80 cols. */
|
|
716
907
|
export function clampWidth(width?: number): number {
|
|
717
908
|
if (!width || Number.isNaN(width)) return 80;
|
|
718
|
-
return Math.max(
|
|
909
|
+
return Math.max(40, Math.min(width, 240));
|
|
719
910
|
}
|
|
720
911
|
|
|
721
912
|
/**
|
package/src/formatters/themes.ts
CHANGED
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import chalk from 'chalk';
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
|
|
15
|
+
export type ThemeName = string;
|
|
12
16
|
|
|
13
17
|
export interface ThemeColors {
|
|
14
18
|
border: (s: string) => string;
|
|
@@ -24,16 +28,7 @@ export interface ThemeColors {
|
|
|
24
28
|
process: (s: string) => string;
|
|
25
29
|
}
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
* Built-in colour themes.
|
|
29
|
-
*
|
|
30
|
-
* - **default** — muted greys and cyan accents, safe for any terminal.
|
|
31
|
-
* - **dracula** — the popular Dracula dark palette.
|
|
32
|
-
* - **cyberpunk** — high-contrast neon on black.
|
|
33
|
-
* - **monochrome** — white-only, useful for colour-blind users or
|
|
34
|
-
* terminals without 256-colour support.
|
|
35
|
-
*/
|
|
36
|
-
export const THEMES: Record<ThemeName, ThemeColors> = {
|
|
31
|
+
export const BUILTIN_THEMES: Record<string, ThemeColors> = {
|
|
37
32
|
default: {
|
|
38
33
|
border: chalk.hex('#4b5563'),
|
|
39
34
|
cpu: chalk.hex('#22d3ee'),
|
|
@@ -112,4 +107,44 @@ export const THEMES: Record<ThemeName, ThemeColors> = {
|
|
|
112
107
|
graphs: chalk.hex('#fb4934'),
|
|
113
108
|
process: chalk.hex('#ebdbb2'),
|
|
114
109
|
},
|
|
115
|
-
};
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export function loadCustomThemes(): Record<string, ThemeColors> {
|
|
113
|
+
const themes = { ...BUILTIN_THEMES };
|
|
114
|
+
const themesDir = path.join(os.homedir(), '.config', 'pyre', 'themes');
|
|
115
|
+
try {
|
|
116
|
+
if (fs.existsSync(themesDir)) {
|
|
117
|
+
const files = fs.readdirSync(themesDir);
|
|
118
|
+
for (const file of files) {
|
|
119
|
+
if (file.endsWith('.json')) {
|
|
120
|
+
const name = path.basename(file, '.json');
|
|
121
|
+
try {
|
|
122
|
+
const raw = fs.readFileSync(path.join(themesDir, file), 'utf-8');
|
|
123
|
+
const data = JSON.parse(raw);
|
|
124
|
+
const buildColor = (val?: string) => (val ? (val.startsWith('#') ? chalk.hex(val) : (chalk as any)[val] || chalk.white) : chalk.white);
|
|
125
|
+
themes[name] = {
|
|
126
|
+
border: buildColor(data.border),
|
|
127
|
+
cpu: buildColor(data.cpu),
|
|
128
|
+
mem: buildColor(data.mem),
|
|
129
|
+
gpu: buildColor(data.gpu),
|
|
130
|
+
power: buildColor(data.power),
|
|
131
|
+
battery: buildColor(data.battery),
|
|
132
|
+
thermal: buildColor(data.thermal),
|
|
133
|
+
network: buildColor(data.network),
|
|
134
|
+
disk: buildColor(data.disk),
|
|
135
|
+
graphs: buildColor(data.graphs),
|
|
136
|
+
process: buildColor(data.process),
|
|
137
|
+
};
|
|
138
|
+
} catch {
|
|
139
|
+
// ignore bad theme file
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
// ignore directory read error
|
|
146
|
+
}
|
|
147
|
+
return themes;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export const THEMES: Record<string, ThemeColors> = loadCustomThemes();
|
package/src/formatters/types.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { ThemeName } from './themes.js';
|
|
10
10
|
import type { StatsData } from '../monitors/types.js';
|
|
11
|
+
import type { History } from '../history.js';
|
|
11
12
|
|
|
12
13
|
export type { ThemeName };
|
|
13
14
|
|
|
@@ -70,4 +71,8 @@ export interface TableOptions {
|
|
|
70
71
|
anomalies?: AnomalyAlert[];
|
|
71
72
|
/** Temperature display unit: 'c' or 'f'. */
|
|
72
73
|
tempUnit?: 'c' | 'f';
|
|
73
|
-
|
|
74
|
+
/** Rolling history instance for live graphs. */
|
|
75
|
+
history?: History;
|
|
76
|
+
/** Graph mode: 'spark' or 'bar'. */
|
|
77
|
+
graphMode?: 'spark' | 'bar';
|
|
78
|
+
}
|