pyre-cli 5.1.0 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +519 -223
- package/package.json +1 -1
- package/src/formatters/index.ts +1 -1
- package/src/formatters/output.ts +14 -13
- package/src/formatters/render.ts +296 -114
- package/src/formatters/types.ts +6 -1
- package/src/index.ts +40 -20
- package/src/live/input.ts +6 -5
- package/src/live/render.ts +63 -54
- package/src/live/types.ts +6 -1
- package/src/monitors/collectors.ts +119 -34
- package/src/monitors/smc.ts +60 -12
- package/src/p2p/server.test.ts +2 -2
- package/src/sparkline.ts +82 -13
- package/Formula/pyre.rb +0 -19
package/package.json
CHANGED
package/src/formatters/index.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - `render.ts` — dashboard layout, cards, tables, and the main `formatTable`
|
|
12
12
|
* - `output.ts` — JSON/CSV/TSV export and sparkline graph rendering
|
|
13
13
|
*/
|
|
14
|
-
export { formatTable, clampWidth, gridColumns, thermalColor, capacityColor, getTabHitboxes, TAB_BAR_ROW, TAB_DEFS, panel, formatBytes } from './render.js';
|
|
14
|
+
export { formatTable, clampWidth, gridColumns, thermalColor, capacityColor, getTabHitboxes, TAB_BAR_ROW, TAB_DEFS, panel, formatBytes, padVisible, visLen, fitVisible } from './render.js';
|
|
15
15
|
export { formatJson, formatCsv, formatTsv, formatGraphs, formatHtml, formatMarkdown } from './output.js';
|
|
16
16
|
export type { ThemeName, ThemeColors, VisibleItems, TableOptions, AnomalyAlert } from './types.js';
|
|
17
17
|
export { THEMES } from './themes.js';
|
package/src/formatters/output.ts
CHANGED
|
@@ -10,8 +10,7 @@ import { sparkline } from '../sparkline.js';
|
|
|
10
10
|
import type { History } from '../history.js';
|
|
11
11
|
import type { StatsData } from './types.js';
|
|
12
12
|
import { THEMES, type ThemeName } from './themes.js';
|
|
13
|
-
import { formatBytes, celsiusToFahrenheit, formatTemp } from './render.js';
|
|
14
|
-
import { panel } from './render.js';
|
|
13
|
+
import { formatBytes, celsiusToFahrenheit, formatTemp, panel, fitVisible } from './render.js';
|
|
15
14
|
|
|
16
15
|
export function formatJson(data: StatsData): string {
|
|
17
16
|
return JSON.stringify(data, null, 2);
|
|
@@ -434,24 +433,24 @@ export function formatGraphs(history: History, width = 80, themeName: ThemeName
|
|
|
434
433
|
const theme = THEMES[themeName] || THEMES.default;
|
|
435
434
|
const contentWidth = width - 4;
|
|
436
435
|
const sampleCount = history.cpuUsage.length;
|
|
437
|
-
const sparkWidth = Math.max(10, contentWidth -
|
|
436
|
+
const sparkWidth = Math.max(10, contentWidth - 24);
|
|
438
437
|
|
|
439
438
|
const lines: string[] = [];
|
|
440
|
-
lines.push(graphRow('CPU %', history.cpuUsage, { min: 0, max: 100 }, v => `${v.toFixed(0)}%`, sparkWidth, 'collecting data...', graphMode));
|
|
441
|
-
lines.push(graphRow('Mem %', history.memUsage, { min: 0, max: 100 }, v => `${v.toFixed(0)}%`, sparkWidth, 'collecting data...', graphMode));
|
|
439
|
+
lines.push(graphRow('CPU %', history.cpuUsage, { min: 0, max: 100 }, v => `${v.toFixed(0)}%`, sparkWidth, 'collecting data...', graphMode, contentWidth));
|
|
440
|
+
lines.push(graphRow('Mem %', history.memUsage, { min: 0, max: 100 }, v => `${v.toFixed(0)}%`, sparkWidth, 'collecting data...', graphMode, contentWidth));
|
|
442
441
|
lines.push(
|
|
443
|
-
graphRow('Temp', history.temp, {},
|
|
442
|
+
graphRow('Temp', history.temp, {}, v => `${v.toFixed(1)}°C`, sparkWidth, 'no sensor access', graphMode, contentWidth)
|
|
444
443
|
);
|
|
445
|
-
lines.push(graphRow('Net RX/s', history.netRxRate, {}, v => `${formatBytes(v)}/s`, sparkWidth, 'collecting data...', graphMode));
|
|
446
|
-
lines.push(graphRow('Net TX/s', history.netTxRate, {}, v => `${formatBytes(v)}/s`, sparkWidth, 'collecting data...', graphMode));
|
|
444
|
+
lines.push(graphRow('Net RX/s', history.netRxRate, {}, v => `${formatBytes(v)}/s`, sparkWidth, 'collecting data...', graphMode, contentWidth));
|
|
445
|
+
lines.push(graphRow('Net TX/s', history.netTxRate, {}, v => `${formatBytes(v)}/s`, sparkWidth, 'collecting data...', graphMode, contentWidth));
|
|
447
446
|
if (history.powerWatts.length) {
|
|
448
|
-
lines.push(graphRow('Power W', history.powerWatts, {}, v => `${v.toFixed(1)} W`, sparkWidth, 'collecting data...', graphMode));
|
|
447
|
+
lines.push(graphRow('Power W', history.powerWatts, {}, v => `${v.toFixed(1)} W`, sparkWidth, 'collecting data...', graphMode, contentWidth));
|
|
449
448
|
}
|
|
450
449
|
if (history.rxPacketRate.length) {
|
|
451
|
-
lines.push(graphRow('RX pkt/s', history.rxPacketRate, {}, v => `${v.toFixed(0)} pkt/s`, sparkWidth, 'collecting data...', graphMode));
|
|
450
|
+
lines.push(graphRow('RX pkt/s', history.rxPacketRate, {}, v => `${v.toFixed(0)} pkt/s`, sparkWidth, 'collecting data...', graphMode, contentWidth));
|
|
452
451
|
}
|
|
453
452
|
if (history.txPacketRate.length) {
|
|
454
|
-
lines.push(graphRow('TX pkt/s', history.txPacketRate, {}, v => `${v.toFixed(0)} pkt/s`, sparkWidth, 'collecting data...', graphMode));
|
|
453
|
+
lines.push(graphRow('TX pkt/s', history.txPacketRate, {}, v => `${v.toFixed(0)} pkt/s`, sparkWidth, 'collecting data...', graphMode, contentWidth));
|
|
455
454
|
}
|
|
456
455
|
|
|
457
456
|
const title = `Graphs · mode:${graphMode} · last ${sampleCount} sample${sampleCount === 1 ? '' : 's'}`;
|
|
@@ -465,7 +464,8 @@ function graphRow(
|
|
|
465
464
|
fmt: (v: number) => string,
|
|
466
465
|
sparkWidth = 40,
|
|
467
466
|
emptyMessage = 'collecting data...',
|
|
468
|
-
mode: 'spark' | 'bar' = 'spark'
|
|
467
|
+
mode: 'spark' | 'bar' = 'spark',
|
|
468
|
+
contentWidth = 76
|
|
469
469
|
): string {
|
|
470
470
|
if (!values.length) {
|
|
471
471
|
return `${chalk.dim(label.padEnd(10))} ${chalk.dim(emptyMessage)}`;
|
|
@@ -480,7 +480,8 @@ function graphRow(
|
|
|
480
480
|
color = pct > 90 ? chalk.red : pct > 70 ? chalk.yellow : chalk.green;
|
|
481
481
|
}
|
|
482
482
|
|
|
483
|
-
|
|
483
|
+
const raw = `${chalk.dim(label.padEnd(10))}${color(spark)} ${chalk.bold(fmt(current))}`;
|
|
484
|
+
return fitVisible(raw, contentWidth);
|
|
484
485
|
}
|
|
485
486
|
|
|
486
487
|
function barGraph(values: number[], bounds: { min?: number; max?: number }, width: number): string {
|
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,232 @@ 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 < Math.min(2, 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
|
-
if (gpu) cards.push({ title: 'GPU', accent: theme.gpu, lines: gpu });
|
|
649
|
-
}
|
|
650
|
-
if (vis.power !== false) {
|
|
651
|
-
const power = powerCard(data);
|
|
652
|
-
if (power) cards.push({ title: 'Power', accent: theme.power, lines: power });
|
|
653
|
-
}
|
|
654
|
-
if (vis.battery !== false) {
|
|
655
|
-
const battery = batteryCard(data, contentWidth);
|
|
656
|
-
if (battery) cards.push({ title: 'Battery', accent: theme.battery, lines: battery });
|
|
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}%`);
|
|
657
757
|
}
|
|
658
|
-
|
|
659
|
-
|
|
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
|
+
diskLines.push(`${mountShort} ${d.used}/${d.size} [${gaugeBar(capNum, Math.max(3, contentWidth - 18))}] ${d.capacity}`);
|
|
767
|
+
}
|
|
768
|
+
out.push(...panel('0 disk io', diskLines, width, theme.disk, theme.border, diskLines.length));
|
|
660
769
|
}
|
|
661
|
-
|
|
662
|
-
|
|
770
|
+
|
|
771
|
+
// --- Box 3: 7 net ---
|
|
772
|
+
const netLines: string[] = [];
|
|
773
|
+
const rxRate = opts.history?.netRxRate.length ? opts.history.netRxRate[opts.history.netRxRate.length - 1] : 0;
|
|
774
|
+
const txRate = opts.history?.netTxRate.length ? opts.history.netTxRate[opts.history.netTxRate.length - 1] : 0;
|
|
775
|
+
|
|
776
|
+
netLines.push(`down ▼ ${formatBytes(rxRate)}/s (${formatBytes(data.network.rxBytes)})`);
|
|
777
|
+
if (opts.history?.netRxRate.length) {
|
|
778
|
+
netLines.push(chalk.cyan(fitVisible(sparkline(opts.history.netRxRate, {}), contentWidth)));
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
netLines.push(`up ▲ ${formatBytes(txRate)}/s (${formatBytes(data.network.txBytes)})`);
|
|
782
|
+
if (opts.history?.netTxRate.length) {
|
|
783
|
+
netLines.push(theme.network(fitVisible(sparkline(opts.history.netTxRate, {}), contentWidth)));
|
|
663
784
|
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
785
|
+
|
|
786
|
+
out.push(...panel('7 net', netLines, width, theme.network, theme.border, netLines.length, `${data.network.interface}`));
|
|
787
|
+
|
|
788
|
+
return out;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function btopProcBox(data: StatsData, width: number, targetHeight: number, theme: ThemeColors, opts: TableOptions): string[] {
|
|
792
|
+
const contentWidth = width - 4;
|
|
793
|
+
const filtered = opts.filter
|
|
794
|
+
? data.processes.filter(p => p.command.toLowerCase().includes(opts.filter!.toLowerCase()))
|
|
795
|
+
: data.processes;
|
|
796
|
+
const sorted = sortProcesses(filtered, opts.sortBy ?? 'cpu');
|
|
797
|
+
|
|
798
|
+
const headerRowOverhead = 2; // top & bottom border
|
|
799
|
+
const tableHeaderOverhead = 2; // header + separator line
|
|
800
|
+
const availableRows = Math.max(1, targetHeight - headerRowOverhead - tableHeaderOverhead);
|
|
801
|
+
|
|
802
|
+
const limited = sorted.slice(0, availableRows);
|
|
803
|
+
|
|
804
|
+
const pidW = 6, userW = 7, thrW = 3, memW = 6, cpuW = 6;
|
|
805
|
+
const fixedW = pidW + userW + thrW + memW + cpuW + 5;
|
|
806
|
+
const cmdW = Math.max(8, contentWidth - fixedW);
|
|
807
|
+
|
|
808
|
+
const head =
|
|
809
|
+
chalk.bold('PID'.padEnd(pidW)) + ' ' +
|
|
810
|
+
chalk.bold('PROGRAM'.padEnd(cmdW)) + ' ' +
|
|
811
|
+
chalk.bold('USER'.padEnd(userW)) + ' ' +
|
|
812
|
+
chalk.bold('THR'.padStart(thrW)) + ' ' +
|
|
813
|
+
chalk.bold('MEM%'.padStart(memW)) + ' ' +
|
|
814
|
+
chalk.bold('CPU%'.padStart(cpuW));
|
|
815
|
+
|
|
816
|
+
if (opts.treeView) {
|
|
817
|
+
const treeLines = buildProcessTree(limited);
|
|
818
|
+
const rightTitle = opts.filter ? `filter: "${opts.filter}"` : `tree · sort: ${opts.sortBy || 'cpu'}`;
|
|
819
|
+
return panel('P proc', [head, theme.border('─'.repeat(contentWidth)), ...treeLines], width, theme.process, theme.border, targetHeight - 2, rightTitle);
|
|
667
820
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
821
|
+
|
|
822
|
+
const rows = limited.map(p => {
|
|
823
|
+
const cpuVal = p.cpu.toFixed(1) + '%';
|
|
824
|
+
const cpuStr = p.cpu > 50 ? chalk.red.bold(cpuVal.padStart(cpuW)) : p.cpu > 20 ? chalk.yellow(cpuVal.padStart(cpuW)) : chalk.green(cpuVal.padStart(cpuW));
|
|
825
|
+
const memStr = (p.mem.toFixed(1) + '%').padStart(memW);
|
|
826
|
+
const userStr = truncatePlain(p.user, userW).padEnd(userW);
|
|
827
|
+
const thrStr = String(p.threads).padStart(thrW);
|
|
828
|
+
const pidStr = String(p.pid).padEnd(pidW);
|
|
829
|
+
const cmdStr = truncatePlain(p.command, cmdW).padEnd(cmdW);
|
|
830
|
+
|
|
831
|
+
return `${pidStr} ${cmdStr} ${userStr} ${thrStr} ${memStr} ${cpuStr}`;
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
const lines = [head, theme.border('─'.repeat(contentWidth)), ...rows];
|
|
835
|
+
if (rows.length === 0) {
|
|
836
|
+
lines.push(chalk.dim(`No processes match "${opts.filter || ''}"`));
|
|
671
837
|
}
|
|
672
838
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
839
|
+
const rightTitle = opts.filter ? `filter: "${opts.filter}"` : `sort: ${opts.sortBy || 'cpu'}`;
|
|
840
|
+
return panel('P proc', lines, width, theme.process, theme.border, targetHeight - 2, rightTitle);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
export function formatTable(data: StatsData, opts: TableOptions = {}): string {
|
|
844
|
+
const width = clampWidth(opts.width);
|
|
845
|
+
const activePanel = opts.activePanel || 'grid';
|
|
680
846
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
847
|
+
const themeName = opts.theme || 'default';
|
|
848
|
+
const theme = THEMES[themeName] || THEMES.default;
|
|
849
|
+
|
|
850
|
+
const out: string[] = [];
|
|
851
|
+
out.push(...header(data, width, [], theme.border));
|
|
852
|
+
out.push('');
|
|
853
|
+
|
|
854
|
+
const anomalies = opts.anomalies ?? [];
|
|
855
|
+
if (anomalies.length && activePanel === 'grid') {
|
|
856
|
+
out.push(...anomalyPanel(anomalies, width, theme));
|
|
684
857
|
out.push('');
|
|
685
858
|
}
|
|
686
859
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
860
|
+
out.push(tabBar(activePanel, width, theme));
|
|
861
|
+
out.push('');
|
|
862
|
+
|
|
863
|
+
if (activePanel !== 'grid') {
|
|
864
|
+
const lines = activeDetailLines(data, activePanel, width, opts);
|
|
865
|
+
if (lines) {
|
|
866
|
+
out.push(...panel(detailPanelTitle(activePanel), lines, width, theme[activePanel as keyof ThemeColors] ?? theme.cpu, theme.border));
|
|
867
|
+
}
|
|
868
|
+
return out.join('\n');
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// --- btop Dashboard Grid ---
|
|
872
|
+
// 1. Top CPU & System Overview Box
|
|
873
|
+
out.push(...btopCpuBox(data, width, theme, opts));
|
|
874
|
+
out.push('');
|
|
875
|
+
|
|
876
|
+
// 2. Bottom Split Section
|
|
877
|
+
if (width >= 85) {
|
|
878
|
+
const leftColW = Math.max(32, Math.floor(width * 0.38));
|
|
879
|
+
const rightColW = width - leftColW - 2;
|
|
880
|
+
|
|
881
|
+
const leftLines = btopLeftColumn(data, leftColW, theme, opts);
|
|
882
|
+
const procLines = btopProcBox(data, rightColW, leftLines.length, theme, opts);
|
|
883
|
+
|
|
884
|
+
out.push(...hstack([leftLines, procLines], 2, width));
|
|
885
|
+
} else {
|
|
886
|
+
const leftLines = btopLeftColumn(data, width, theme, opts);
|
|
887
|
+
out.push(...leftLines);
|
|
888
|
+
out.push('');
|
|
889
|
+
const procLines = btopProcBox(data, width, 12, theme, opts);
|
|
890
|
+
out.push(...procLines);
|
|
891
|
+
}
|
|
711
892
|
|
|
712
893
|
return out.join('\n');
|
|
713
894
|
}
|
|
714
895
|
|
|
896
|
+
|
|
715
897
|
/** Clamp/normalize a terminal width to a sane range, defaulting to 80 cols. */
|
|
716
898
|
export function clampWidth(width?: number): number {
|
|
717
899
|
if (!width || Number.isNaN(width)) return 80;
|
|
718
|
-
return Math.max(
|
|
900
|
+
return Math.max(40, Math.min(width, 240));
|
|
719
901
|
}
|
|
720
902
|
|
|
721
903
|
/**
|
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
|
+
}
|