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/README.md +209 -16
- package/dist/index.js +735 -332
- package/package.json +5 -2
- package/parity.md +542 -0
- package/src/formatters/index.ts +17 -0
- package/src/formatters/output.ts +137 -0
- package/src/formatters/render.ts +468 -0
- package/src/formatters/themes.ts +115 -0
- package/src/formatters/types.ts +58 -0
- package/src/history.ts +115 -0
- package/src/index.ts +81 -0
- package/src/live/export.ts +73 -0
- package/src/live/index.ts +16 -0
- package/src/live/input.ts +311 -0
- package/src/live/render.ts +142 -0
- package/src/live/state.ts +83 -0
- package/src/live/types.ts +23 -0
- package/src/monitors/collectors.ts +484 -0
- package/src/monitors/index.ts +14 -0
- package/src/monitors/run.ts +18 -0
- package/src/monitors/smc.ts +107 -0
- package/src/monitors/types.ts +107 -0
- package/src/pyre_img +0 -0
- package/src/sparkline.ts +34 -0
- package/src/splash.ts +102 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard rendering helpers.
|
|
3
|
+
*
|
|
4
|
+
* This module contains every function that turns a {@link StatsData}
|
|
5
|
+
* snapshot into styled terminal output: panel drawing, card layout,
|
|
6
|
+
* process/disk tables, and the main `formatTable` orchestrator.
|
|
7
|
+
*
|
|
8
|
+
* Low-level utilities (ANSI stripping, padding, truncation) live
|
|
9
|
+
* here alongside the higher-level card and table renderers so that
|
|
10
|
+
* all layout-related code is co-located.
|
|
11
|
+
*/
|
|
12
|
+
import chalk from 'chalk';
|
|
13
|
+
import { sparkline } from '../sparkline.js';
|
|
14
|
+
import type { History } from '../history.js';
|
|
15
|
+
import type { StatsData, VisibleItems, TableOptions } from './types.js';
|
|
16
|
+
import { THEMES, type ThemeName } from './themes.js';
|
|
17
|
+
|
|
18
|
+
// --- low-level box drawing ---------------------------------------------------
|
|
19
|
+
|
|
20
|
+
function visLen(s: string): number {
|
|
21
|
+
// eslint-disable-next-line no-control-regex
|
|
22
|
+
return s.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function padVisible(s: string, width: number): string {
|
|
26
|
+
const len = visLen(s);
|
|
27
|
+
if (len >= width) return s;
|
|
28
|
+
return s + ' '.repeat(width - len);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function truncatePlain(s: string, width: number): string {
|
|
32
|
+
if (s.length <= width) return s;
|
|
33
|
+
return width <= 1 ? s.slice(0, width) : s.slice(0, width - 1) + '…';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Draw a rounded panel of exactly `width` columns wide and `height` content rows tall. */
|
|
37
|
+
export function panel(
|
|
38
|
+
title: string,
|
|
39
|
+
lines: string[],
|
|
40
|
+
width: number,
|
|
41
|
+
accent: (s: string) => string,
|
|
42
|
+
borderAccent: (s: string) => string,
|
|
43
|
+
height?: number
|
|
44
|
+
): string[] {
|
|
45
|
+
const h = height ?? lines.length;
|
|
46
|
+
const contentWidth = Math.max(1, width - 4);
|
|
47
|
+
const label = ` ${title} `;
|
|
48
|
+
const top = borderAccent('┌' + label + '─'.repeat(width - label.length - 2) + '┐');
|
|
49
|
+
const bottom = borderAccent('└' + '─'.repeat(width - 2) + '┘');
|
|
50
|
+
const body: string[] = [];
|
|
51
|
+
for (let i = 0; i < h; i++) {
|
|
52
|
+
const line = lines[i] ?? '';
|
|
53
|
+
body.push(`${borderAccent('│')} ${padVisible(line, contentWidth)} ${borderAccent('│')}`);
|
|
54
|
+
}
|
|
55
|
+
return [top, ...body, bottom];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Join equal-or-uneven-height panel blocks side by side with a gap. */
|
|
59
|
+
function hstack(blocks: string[][], gap = 2): string[] {
|
|
60
|
+
if (!blocks.length) return [];
|
|
61
|
+
const height = Math.max(...blocks.map(b => b.length));
|
|
62
|
+
const widths = blocks.map(b => visLen(b[0] ?? ''));
|
|
63
|
+
const padded = blocks.map((b, i) => {
|
|
64
|
+
const filler = ' '.repeat(widths[i]);
|
|
65
|
+
const arr = [...b];
|
|
66
|
+
while (arr.length < height) arr.push(filler);
|
|
67
|
+
return arr;
|
|
68
|
+
});
|
|
69
|
+
const out: string[] = [];
|
|
70
|
+
for (let row = 0; row < height; row++) {
|
|
71
|
+
out.push(padded.map(a => a[row]).join(' '.repeat(gap)));
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function statRow(label: string, value: string, width = 9): string {
|
|
77
|
+
const w = Math.max(width, label.length + 1);
|
|
78
|
+
return `${chalk.dim(label.padEnd(w))}${value}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function bar(percent: number, width = 20): string {
|
|
82
|
+
const filled = Math.round((Math.min(100, Math.max(0, percent)) / 100) * width);
|
|
83
|
+
const empty = width - filled;
|
|
84
|
+
const filledStr = '█'.repeat(Math.max(0, filled));
|
|
85
|
+
const emptyStr = '░'.repeat(Math.max(0, empty));
|
|
86
|
+
const color = percent > 90 ? chalk.red : percent > 70 ? chalk.yellow : chalk.green;
|
|
87
|
+
return color(filledStr + emptyStr);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function gaugeRow(label: string, percent: number, contentWidth: number): string {
|
|
91
|
+
const labelW = 9;
|
|
92
|
+
const pctText = `${Math.round(percent)}%`;
|
|
93
|
+
const barW = Math.max(6, contentWidth - labelW - pctText.length - 1);
|
|
94
|
+
const filled = Math.round((Math.min(100, Math.max(0, percent)) / 100) * barW);
|
|
95
|
+
const empty = barW - filled;
|
|
96
|
+
const barStr = '█'.repeat(Math.max(0, filled)) + '░'.repeat(Math.max(0, empty));
|
|
97
|
+
const color = percent > 90 ? chalk.red : percent > 70 ? chalk.yellow : chalk.green;
|
|
98
|
+
return `${chalk.dim(label.padEnd(labelW))}${color(barStr)} ${pctText}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function thermalColor(pressureLevel: number) {
|
|
102
|
+
if (pressureLevel >= 3) return chalk.red;
|
|
103
|
+
if (pressureLevel === 2) return chalk.yellow;
|
|
104
|
+
if (pressureLevel === 1) return chalk.yellowBright;
|
|
105
|
+
return chalk.green;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function capacityColor(percent: number) {
|
|
109
|
+
if (percent < 80) return chalk.red;
|
|
110
|
+
if (percent < 90) return chalk.yellow;
|
|
111
|
+
return chalk.green;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function formatBytes(bytes: number): string {
|
|
115
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
116
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
117
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
118
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function celsiusToFahrenheit(c: number): number {
|
|
122
|
+
return c * (9 / 5) + 32;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Render a Celsius reading with its Fahrenheit equivalent alongside it, e.g. "62.0°C / 143.6°F". */
|
|
126
|
+
export function formatTemp(c: number): string {
|
|
127
|
+
return `${c.toFixed(1)}°C / ${celsiusToFahrenheit(c).toFixed(1)}°F`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// --- cards -------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
function cpuCard(data: StatsData, contentWidth: number): string[] {
|
|
133
|
+
const lines = [
|
|
134
|
+
statRow('Brand', truncatePlain(data.cpu.brand, Math.max(4, contentWidth - 8))),
|
|
135
|
+
statRow('Cores', `${data.cpu.physicalCores}/${data.cpu.cores} phys/log`),
|
|
136
|
+
statRow('Freq', `${data.cpu.frequency} MHz`),
|
|
137
|
+
gaugeRow('Usage', data.cpu.usage, contentWidth),
|
|
138
|
+
statRow('Load', data.cpu.loadAvg.map(l => l.toFixed(2)).join(' ')),
|
|
139
|
+
];
|
|
140
|
+
if (data.cpu.temperature) lines.push(statRow('Temp', formatTemp(data.cpu.temperature)));
|
|
141
|
+
return lines;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function memCard(data: StatsData, contentWidth: number): string[] {
|
|
145
|
+
return [
|
|
146
|
+
statRow('Total', formatBytes(data.memory.total)),
|
|
147
|
+
statRow('Used', formatBytes(data.memory.used)),
|
|
148
|
+
statRow('Cached', formatBytes(data.memory.total - data.memory.free - data.memory.used)),
|
|
149
|
+
statRow('Free', formatBytes(data.memory.free)),
|
|
150
|
+
gaugeRow('Usage', data.memory.usagePercent, contentWidth),
|
|
151
|
+
statRow('Swap', `${formatBytes(data.memory.swapUsed)} / ${formatBytes(data.memory.swapTotal)}`),
|
|
152
|
+
];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function gpuCard(data: StatsData, contentWidth: number): string[] | null {
|
|
156
|
+
if (!data.gpu) return null;
|
|
157
|
+
const lines: string[] = [];
|
|
158
|
+
lines.push(statRow('Model', truncatePlain(data.gpu.model, Math.max(4, contentWidth - 8))));
|
|
159
|
+
lines.push(statRow('Memory', formatBytes(data.gpu.memory)));
|
|
160
|
+
lines.push(statRow('Util', `${data.gpu.utilization}%`));
|
|
161
|
+
if (data.gpu.temperature) lines.push(statRow('Temp', formatTemp(data.gpu.temperature)));
|
|
162
|
+
lines.push(statRow('Procs', String(data.gpu.processes)));
|
|
163
|
+
return lines;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function powerCard(data: StatsData): string[] | null {
|
|
167
|
+
if (!data.power) return null;
|
|
168
|
+
const lines: string[] = [];
|
|
169
|
+
if (data.power.cpuWatts !== undefined) lines.push(statRow('CPU', `${data.power.cpuWatts.toFixed(2)} W`));
|
|
170
|
+
if (data.power.gpuWatts !== undefined) lines.push(statRow('GPU', `${data.power.gpuWatts.toFixed(2)} W`));
|
|
171
|
+
if (data.power.combinedWatts !== undefined) lines.push(statRow('Total', `${data.power.combinedWatts.toFixed(2)} W`));
|
|
172
|
+
return lines.length ? lines : null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function batteryCard(data: StatsData, contentWidth: number): string[] | null {
|
|
176
|
+
if (!data.battery) return null;
|
|
177
|
+
const valW = Math.max(6, contentWidth - 10);
|
|
178
|
+
const lines = [
|
|
179
|
+
statRow('Level', `${data.battery.level}% (${data.battery.state})`),
|
|
180
|
+
statRow('Time', data.battery.timeRemaining),
|
|
181
|
+
statRow('Cycles', String(data.battery.cycles ?? 'N/A')),
|
|
182
|
+
statRow('Condition', data.battery.condition ?? 'N/A'),
|
|
183
|
+
];
|
|
184
|
+
if (data.battery.maxCapacityPercent !== undefined) {
|
|
185
|
+
lines.push(statRow('Capacity', capacityColor(data.battery.maxCapacityPercent)(`${data.battery.maxCapacityPercent}%`)));
|
|
186
|
+
}
|
|
187
|
+
lines.push(statRow('Source', data.battery.powerSource));
|
|
188
|
+
return lines;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function thermalCard(data: StatsData, contentWidth: number): string[] {
|
|
192
|
+
const valW = Math.max(6, contentWidth - 10);
|
|
193
|
+
const lines = [statRow('State', thermalColor(data.thermal.pressureLevel)(truncatePlain(data.thermal.state, valW)))];
|
|
194
|
+
if (data.thermal.temperatures) {
|
|
195
|
+
for (const [k, v] of Object.entries(data.thermal.temperatures)) {
|
|
196
|
+
lines.push(statRow(k.replace('_', ' '), v !== null && v !== undefined ? formatTemp(v) : 'N/A'));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (data.thermal.error) lines.push(statRow('Error', truncatePlain(data.thermal.error, valW)));
|
|
200
|
+
return lines;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function networkCard(data: StatsData, contentWidth: number): string[] {
|
|
204
|
+
const valW = Math.max(6, contentWidth - 8);
|
|
205
|
+
return [
|
|
206
|
+
statRow('Iface', truncatePlain(`${data.network.interface} (${data.network.ip})`, valW)),
|
|
207
|
+
statRow('RX', `${formatBytes(data.network.rxBytes)} / ${data.network.rxPackets} pkt`),
|
|
208
|
+
statRow('TX', `${formatBytes(data.network.txBytes)} / ${data.network.txPackets} pkt`),
|
|
209
|
+
];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// --- header ------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
function header(data: StatsData, width: number, badges: string[] = [], borderAccent = THEMES.default.border): string[] {
|
|
215
|
+
const now = new Date().toLocaleTimeString();
|
|
216
|
+
const left = `${chalk.hex('#ff5500').bold(' PYRE ')} ${chalk.bold(data.header.hostname)} ${chalk.dim(data.header.os)} ${chalk.dim('up ' + data.header.uptime)}`;
|
|
217
|
+
const right = `${chalk.dim(now)} ${badges.join(' ')}`;
|
|
218
|
+
const gap = Math.max(1, width - visLen(left) - visLen(right));
|
|
219
|
+
const line = left + ' '.repeat(gap) + right;
|
|
220
|
+
return [line, borderAccent('─'.repeat(width))];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// --- process / disk tables ---------------------------------------------------
|
|
224
|
+
|
|
225
|
+
function sortProcesses(
|
|
226
|
+
processes: { pid: number; ppid: number; user: string; cpu: number; mem: number; command: string; state: string; threads: number; runtime: number }[],
|
|
227
|
+
sortBy: 'cpu' | 'mem' | 'pid' | 'user' | 'command' | 'state' | 'threads' | 'runtime'
|
|
228
|
+
) {
|
|
229
|
+
const copy = [...processes];
|
|
230
|
+
switch (sortBy) {
|
|
231
|
+
case 'pid': return copy.sort((a, b) => a.pid - b.pid);
|
|
232
|
+
case 'mem': return copy.sort((a, b) => b.mem - a.mem);
|
|
233
|
+
case 'user': return copy.sort((a, b) => a.user.localeCompare(b.user));
|
|
234
|
+
case 'command': return copy.sort((a, b) => a.command.localeCompare(b.command));
|
|
235
|
+
case 'state': return copy.sort((a, b) => a.state.localeCompare(b.state));
|
|
236
|
+
case 'threads': return copy.sort((a, b) => b.threads - a.threads);
|
|
237
|
+
case 'runtime': return copy.sort((a, b) => b.runtime - a.runtime);
|
|
238
|
+
default: return copy.sort((a, b) => b.cpu - a.cpu);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function pctColor(v: number): (s: string) => string {
|
|
243
|
+
return v > 80 ? chalk.red : v > 50 ? chalk.yellow : chalk.dim;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function processTableLines(
|
|
247
|
+
processes: { pid: number; ppid: number; user: string; cpu: number; mem: number; command: string; state: string; threads: number; runtime: number }[],
|
|
248
|
+
contentWidth: number,
|
|
249
|
+
borderAccent = THEMES.default.border
|
|
250
|
+
): string[] {
|
|
251
|
+
const pidW = 8, ppidW = 8, userW = 12, cpuW = 8, memW = 8, stateW = 8, thrW = 6, rtW = 10;
|
|
252
|
+
|
|
253
|
+
const cmdW = Math.max(10, contentWidth - pidW - ppidW - userW - cpuW - memW - stateW - thrW - rtW - 8);
|
|
254
|
+
const head =
|
|
255
|
+
chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' PID ') +
|
|
256
|
+
chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' PPID ') +
|
|
257
|
+
chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' USER ') +
|
|
258
|
+
chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' CPU% ') +
|
|
259
|
+
chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' MEM% ') +
|
|
260
|
+
chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' STATE ') +
|
|
261
|
+
chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' THREADS') +
|
|
262
|
+
chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' RUNTIME ') +
|
|
263
|
+
chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' COMMAND'.padEnd(cmdW + 1));
|
|
264
|
+
|
|
265
|
+
const rows = processes.map(p => {
|
|
266
|
+
const cpuStr = pctColor(p.cpu)(`${p.cpu.toFixed(1)}`.padEnd(cpuW));
|
|
267
|
+
const memStr = pctColor(p.mem)(`${p.mem.toFixed(1)}`.padEnd(memW));
|
|
268
|
+
const stateStr = chalk.dim(p.state.padEnd(stateW));
|
|
269
|
+
const thrStr = String(p.threads).padEnd(thrW);
|
|
270
|
+
const rtStr = formatRuntime(p.runtime).padEnd(rtW);
|
|
271
|
+
return (
|
|
272
|
+
String(p.pid).padEnd(pidW) +
|
|
273
|
+
String(p.ppid).padEnd(ppidW) +
|
|
274
|
+
truncatePlain(p.user, userW - 1).padEnd(userW) +
|
|
275
|
+
cpuStr +
|
|
276
|
+
memStr +
|
|
277
|
+
stateStr +
|
|
278
|
+
thrStr +
|
|
279
|
+
rtStr +
|
|
280
|
+
truncatePlain(p.command, cmdW)
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
return [head, borderAccent('─'.repeat(contentWidth)), ...rows];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function formatRuntime(seconds: number): string {
|
|
287
|
+
if (seconds < 60) return `${seconds}s`;
|
|
288
|
+
const mins = Math.floor(seconds / 60);
|
|
289
|
+
if (mins < 60) return `${mins}m ${seconds % 60}s`;
|
|
290
|
+
const hrs = Math.floor(mins / 60);
|
|
291
|
+
return `${hrs}h ${mins % 60}m`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function buildProcessTree(
|
|
295
|
+
processes: { pid: number; ppid: number; user: string; cpu: number; mem: number; command: string; state: string; threads: number; runtime: number }[]
|
|
296
|
+
): string[] {
|
|
297
|
+
const map = new Map<number, typeof processes[0]>();
|
|
298
|
+
const roots: typeof processes[0][] = [];
|
|
299
|
+
for (const p of processes) map.set(p.pid, p);
|
|
300
|
+
for (const p of processes) {
|
|
301
|
+
if (p.ppid === 0 || !map.has(p.ppid)) roots.push(p);
|
|
302
|
+
}
|
|
303
|
+
const lines: string[] = [];
|
|
304
|
+
const visited = new Set<number>();
|
|
305
|
+
function renderNode(proc: typeof processes[0], prefix: string, isLast: boolean) {
|
|
306
|
+
if (visited.has(proc.pid)) return;
|
|
307
|
+
visited.add(proc.pid);
|
|
308
|
+
const connector = isLast ? '└── ' : '├── ';
|
|
309
|
+
const pidStr = String(proc.pid).padEnd(8);
|
|
310
|
+
const cpuStr = pctColor(proc.cpu)(`${proc.cpu.toFixed(1)}%`.padEnd(8));
|
|
311
|
+
const memStr = pctColor(proc.mem)(`${proc.mem.toFixed(1)}%`.padEnd(8));
|
|
312
|
+
const stateStr = chalk.dim(proc.state.padEnd(8));
|
|
313
|
+
const thrStr = String(proc.threads).padEnd(6);
|
|
314
|
+
|
|
315
|
+
const cmd = truncatePlain(proc.command, Math.max(10, 100 - pidStr.length - cpuStr.length - memStr.length - stateStr.length - thrStr.length - 10));
|
|
316
|
+
lines.push(`${prefix}${connector}${pidStr}${cpuStr}${memStr}${stateStr}${thrStr} ${cmd}`);
|
|
317
|
+
const children = processes.filter(p => p.ppid === proc.pid && !visited.has(p.pid));
|
|
318
|
+
children.sort((a, b) => b.cpu - a.cpu);
|
|
319
|
+
children.forEach((child, i) => {
|
|
320
|
+
const nextPrefix = prefix + (isLast ? ' ' : '│ ');
|
|
321
|
+
renderNode(child, nextPrefix, i === children.length - 1);
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
roots.sort((a, b) => b.cpu - a.cpu);
|
|
325
|
+
roots.forEach((root, i) => renderNode(root, '', i === roots.length - 1));
|
|
326
|
+
return lines;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function diskTableLines(
|
|
330
|
+
disks: { filesystem: string; size: string; used: string; available: string; capacity: string; mountpoint: string }[],
|
|
331
|
+
contentWidth: number,
|
|
332
|
+
borderAccent = THEMES.default.border
|
|
333
|
+
): string[] {
|
|
334
|
+
const fsW = 20, sizeW = 10, usedW = 10, availW = 10, capW = 8;
|
|
335
|
+
|
|
336
|
+
const mountW = Math.max(8, contentWidth - fsW - sizeW - usedW - availW - capW - 5);
|
|
337
|
+
const head =
|
|
338
|
+
chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' FILESYSTEM ') +
|
|
339
|
+
chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' SIZE ') +
|
|
340
|
+
chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' USED ') +
|
|
341
|
+
chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' AVAIL ') +
|
|
342
|
+
chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' CAP ') +
|
|
343
|
+
chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' MOUNT'.padEnd(mountW + 1));
|
|
344
|
+
const rows = disks.map(d => {
|
|
345
|
+
|
|
346
|
+
const capNum = parseInt(d.capacity, 10) || 0;
|
|
347
|
+
const capStr = pctColor(capNum)(d.capacity.padEnd(capW));
|
|
348
|
+
return (
|
|
349
|
+
truncatePlain(d.filesystem, fsW - 1).padEnd(fsW) +
|
|
350
|
+
d.size.padEnd(sizeW) +
|
|
351
|
+
d.used.padEnd(usedW) +
|
|
352
|
+
d.available.padEnd(availW) +
|
|
353
|
+
capStr +
|
|
354
|
+
truncatePlain(d.mountpoint, mountW)
|
|
355
|
+
);
|
|
356
|
+
});
|
|
357
|
+
return [head, borderAccent('─'.repeat(contentWidth)), ...rows];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// --- main dashboard ------------------------------------------------------------
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Render the full static dashboard as a single string.
|
|
364
|
+
*
|
|
365
|
+
* Cards are arranged in rows of {@link gridColumns} columns,
|
|
366
|
+
* and the process table is appended below. Visibility of
|
|
367
|
+
* individual panels is controlled via {@link TableOptions.visible}.
|
|
368
|
+
*/
|
|
369
|
+
export function formatTable(data: StatsData, opts: TableOptions = {}): string {
|
|
370
|
+
const width = clampWidth(opts.width);
|
|
371
|
+
const columns = gridColumns(width);
|
|
372
|
+
const gap = 2;
|
|
373
|
+
const cardWidth = Math.floor((width - gap * (columns - 1)) / columns);
|
|
374
|
+
const contentWidth = cardWidth - 4;
|
|
375
|
+
|
|
376
|
+
const themeName = opts.theme || 'default';
|
|
377
|
+
const theme = THEMES[themeName] || THEMES.default;
|
|
378
|
+
const vis = opts.visible ?? {};
|
|
379
|
+
|
|
380
|
+
const out: string[] = [];
|
|
381
|
+
out.push(...header(data, width, [], theme.border));
|
|
382
|
+
out.push('');
|
|
383
|
+
|
|
384
|
+
type Card = { title: string; accent: (s: string) => string; lines: string[] };
|
|
385
|
+
const cards: Card[] = [];
|
|
386
|
+
|
|
387
|
+
if (vis.cpu !== false) {
|
|
388
|
+
cards.push({ title: 'CPU', accent: theme.cpu, lines: cpuCard(data, contentWidth) });
|
|
389
|
+
}
|
|
390
|
+
if (vis.mem !== false) {
|
|
391
|
+
cards.push({ title: 'Memory', accent: theme.mem, lines: memCard(data, contentWidth) });
|
|
392
|
+
}
|
|
393
|
+
if (vis.gpu !== false) {
|
|
394
|
+
const gpu = gpuCard(data, contentWidth);
|
|
395
|
+
if (gpu) cards.push({ title: 'GPU', accent: theme.gpu, lines: gpu });
|
|
396
|
+
}
|
|
397
|
+
if (vis.power !== false) {
|
|
398
|
+
const power = powerCard(data);
|
|
399
|
+
if (power) cards.push({ title: 'Power', accent: theme.power, lines: power });
|
|
400
|
+
}
|
|
401
|
+
if (vis.battery !== false) {
|
|
402
|
+
const battery = batteryCard(data, contentWidth);
|
|
403
|
+
if (battery) cards.push({ title: 'Battery', accent: theme.battery, lines: battery });
|
|
404
|
+
}
|
|
405
|
+
if (vis.thermal !== false) {
|
|
406
|
+
cards.push({ title: 'Thermal', accent: theme.thermal, lines: thermalCard(data, contentWidth) });
|
|
407
|
+
}
|
|
408
|
+
if (vis.network !== false) {
|
|
409
|
+
cards.push({ title: 'Network', accent: theme.network, lines: networkCard(data, contentWidth) });
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
for (let i = 0; i < cards.length; i += columns) {
|
|
413
|
+
const row = cards.slice(i, i + columns);
|
|
414
|
+
const rowHeight = Math.max(...row.map(c => c.lines.length));
|
|
415
|
+
const blocks = row.map(c => panel(c.title, c.lines, cardWidth, c.accent, theme.border, rowHeight));
|
|
416
|
+
out.push(...hstack(blocks, gap));
|
|
417
|
+
out.push('');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (vis.disk !== false && data.disk.length) {
|
|
421
|
+
const dw = width - 4;
|
|
422
|
+
out.push(...panel('Disk', diskTableLines(data.disk, dw, theme.border), width, theme.disk, theme.border));
|
|
423
|
+
out.push('');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (vis.process !== false) {
|
|
427
|
+
const filteredProcesses = opts.filter
|
|
428
|
+
? data.processes.filter(p => p.command.toLowerCase().includes(opts.filter!.toLowerCase()))
|
|
429
|
+
: data.processes;
|
|
430
|
+
const sortedProcesses = sortProcesses(filteredProcesses, opts.sortBy ?? 'cpu');
|
|
431
|
+
const limited = opts.processLimit ? sortedProcesses.slice(0, opts.processLimit) : sortedProcesses;
|
|
432
|
+
|
|
433
|
+
const procTitle = opts.filter
|
|
434
|
+
? `Processes · filter "${opts.filter}" · sort ${opts.sortBy ?? 'cpu'}`
|
|
435
|
+
: `Processes · sort ${opts.sortBy ?? 'cpu'}`;
|
|
436
|
+
|
|
437
|
+
if (opts.treeView) {
|
|
438
|
+
const treeLines = buildProcessTree(limited);
|
|
439
|
+
if (treeLines.length) {
|
|
440
|
+
out.push(...panel(procTitle + ' · tree', treeLines, width, theme.process, theme.border));
|
|
441
|
+
} else if (opts.filter) {
|
|
442
|
+
out.push(...panel(procTitle, [chalk.dim(`No processes match "${opts.filter}"`)], width, theme.process, theme.border));
|
|
443
|
+
}
|
|
444
|
+
} else if (limited.length) {
|
|
445
|
+
out.push(...panel(procTitle, processTableLines(limited, width - 4, theme.border), width, theme.process, theme.border));
|
|
446
|
+
} else if (opts.filter) {
|
|
447
|
+
out.push(...panel(procTitle, [chalk.dim(`No processes match "${opts.filter}"`)], width, theme.process, theme.border));
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return out.join('\n');
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** Clamp/normalize a terminal width to a sane range, defaulting to 80 cols. */
|
|
455
|
+
export function clampWidth(width?: number): number {
|
|
456
|
+
if (!width || Number.isNaN(width)) return 80;
|
|
457
|
+
return Math.max(60, Math.min(width, 240));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* How many stat cards fit per row at a given terminal width.
|
|
462
|
+
* Shared with the live dashboard for layout budgeting.
|
|
463
|
+
*/
|
|
464
|
+
export function gridColumns(width: number): number {
|
|
465
|
+
if (width >= 150) return 3;
|
|
466
|
+
if (width >= 96) return 2;
|
|
467
|
+
return 1;
|
|
468
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visual theme definitions for the pyre dashboard.
|
|
3
|
+
*
|
|
4
|
+
* Each theme maps UI element roles (border, cpu, mem, …) to a
|
|
5
|
+
* chalk colour function. The {@link THEMES} record provides four
|
|
6
|
+
* built-in themes; additional themes can be added by extending
|
|
7
|
+
* the {@link ThemeName} union and adding an entry here.
|
|
8
|
+
*/
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
|
|
11
|
+
export type ThemeName = 'default' | 'dracula' | 'cyberpunk' | 'monochrome' | 'nord' | 'gruvbox';
|
|
12
|
+
|
|
13
|
+
export interface ThemeColors {
|
|
14
|
+
border: (s: string) => string;
|
|
15
|
+
cpu: (s: string) => string;
|
|
16
|
+
mem: (s: string) => string;
|
|
17
|
+
gpu: (s: string) => string;
|
|
18
|
+
power: (s: string) => string;
|
|
19
|
+
battery: (s: string) => string;
|
|
20
|
+
thermal: (s: string) => string;
|
|
21
|
+
network: (s: string) => string;
|
|
22
|
+
disk: (s: string) => string;
|
|
23
|
+
graphs: (s: string) => string;
|
|
24
|
+
process: (s: string) => string;
|
|
25
|
+
}
|
|
26
|
+
|
|
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> = {
|
|
37
|
+
default: {
|
|
38
|
+
border: chalk.hex('#4b5563'),
|
|
39
|
+
cpu: chalk.hex('#22d3ee'),
|
|
40
|
+
mem: chalk.hex('#a78bfa'),
|
|
41
|
+
gpu: chalk.hex('#f472b6'),
|
|
42
|
+
power: chalk.hex('#fbbf24'),
|
|
43
|
+
battery: chalk.hex('#34d399'),
|
|
44
|
+
thermal: chalk.hex('#fb923c'),
|
|
45
|
+
network: chalk.hex('#60a5fa'),
|
|
46
|
+
disk: chalk.hex('#f472b6'),
|
|
47
|
+
graphs: chalk.hex('#22d3ee'),
|
|
48
|
+
process: chalk.hex('#e5e7eb'),
|
|
49
|
+
},
|
|
50
|
+
dracula: {
|
|
51
|
+
border: chalk.hex('#6272a4'),
|
|
52
|
+
cpu: chalk.hex('#8be9fd'),
|
|
53
|
+
mem: chalk.hex('#bd93f9'),
|
|
54
|
+
gpu: chalk.hex('#ff5555'),
|
|
55
|
+
power: chalk.hex('#f1fa8c'),
|
|
56
|
+
battery: chalk.hex('#50fa7b'),
|
|
57
|
+
thermal: chalk.hex('#ffb86c'),
|
|
58
|
+
network: chalk.hex('#ff79c6'),
|
|
59
|
+
disk: chalk.hex('#ff5555'),
|
|
60
|
+
graphs: chalk.hex('#8be9fd'),
|
|
61
|
+
process: chalk.hex('#f8f8f2'),
|
|
62
|
+
},
|
|
63
|
+
cyberpunk: {
|
|
64
|
+
border: chalk.hex('#ff0055'),
|
|
65
|
+
cpu: chalk.hex('#00ffcc'),
|
|
66
|
+
mem: chalk.hex('#ff00ff'),
|
|
67
|
+
gpu: chalk.hex('#ffff00'),
|
|
68
|
+
power: chalk.hex('#ffff00'),
|
|
69
|
+
battery: chalk.hex('#00ff00'),
|
|
70
|
+
thermal: chalk.hex('#ff6600'),
|
|
71
|
+
network: chalk.hex('#00ffff'),
|
|
72
|
+
disk: chalk.hex('#ff007f'),
|
|
73
|
+
graphs: chalk.hex('#00ffcc'),
|
|
74
|
+
process: chalk.hex('#ffffff'),
|
|
75
|
+
},
|
|
76
|
+
monochrome: {
|
|
77
|
+
border: chalk.gray,
|
|
78
|
+
cpu: chalk.white.bold,
|
|
79
|
+
mem: chalk.white.bold,
|
|
80
|
+
gpu: chalk.white.bold,
|
|
81
|
+
power: chalk.white.bold,
|
|
82
|
+
battery: chalk.white.bold,
|
|
83
|
+
thermal: chalk.white.bold,
|
|
84
|
+
network: chalk.white.bold,
|
|
85
|
+
disk: chalk.white.bold,
|
|
86
|
+
graphs: chalk.white.bold,
|
|
87
|
+
process: chalk.white.bold,
|
|
88
|
+
},
|
|
89
|
+
nord: {
|
|
90
|
+
border: chalk.hex('#4c566a'),
|
|
91
|
+
cpu: chalk.hex('#88c0d0'),
|
|
92
|
+
mem: chalk.hex('#b48ead'),
|
|
93
|
+
gpu: chalk.hex('#bf616a'),
|
|
94
|
+
power: chalk.hex('#ebcb8b'),
|
|
95
|
+
battery: chalk.hex('#a3be8c'),
|
|
96
|
+
thermal: chalk.hex('#d08770'),
|
|
97
|
+
network: chalk.hex('#81a1c1'),
|
|
98
|
+
disk: chalk.hex('#b48ead'),
|
|
99
|
+
graphs: chalk.hex('#88c0d0'),
|
|
100
|
+
process: chalk.hex('#eceff4'),
|
|
101
|
+
},
|
|
102
|
+
gruvbox: {
|
|
103
|
+
border: chalk.hex('#928374'),
|
|
104
|
+
cpu: chalk.hex('#fb4934'),
|
|
105
|
+
mem: chalk.hex('#b8bb26'),
|
|
106
|
+
gpu: chalk.hex('#d3869b'),
|
|
107
|
+
power: chalk.hex('#fabd2f'),
|
|
108
|
+
battery: chalk.hex('#b8bb26'),
|
|
109
|
+
thermal: chalk.hex('#fb4934'),
|
|
110
|
+
network: chalk.hex('#83a598'),
|
|
111
|
+
disk: chalk.hex('#d3869b'),
|
|
112
|
+
graphs: chalk.hex('#fb4934'),
|
|
113
|
+
process: chalk.hex('#ebdbb2'),
|
|
114
|
+
},
|
|
115
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public-facing type definitions for the formatters module.
|
|
3
|
+
*
|
|
4
|
+
* These interfaces control how the dashboard is laid out and
|
|
5
|
+
* which panels are visible. They are consumed by both the
|
|
6
|
+
* static `formatTable` function and the live dashboard in
|
|
7
|
+
* `live/`.
|
|
8
|
+
*/
|
|
9
|
+
import type { ThemeName } from './themes.js';
|
|
10
|
+
import type { StatsData } from '../monitors/types.js';
|
|
11
|
+
|
|
12
|
+
export type { ThemeName };
|
|
13
|
+
|
|
14
|
+
export interface ThemeColors {
|
|
15
|
+
border: (s: string) => string;
|
|
16
|
+
cpu: (s: string) => string;
|
|
17
|
+
mem: (s: string) => string;
|
|
18
|
+
gpu: (s: string) => string;
|
|
19
|
+
power: (s: string) => string;
|
|
20
|
+
battery: (s: string) => string;
|
|
21
|
+
thermal: (s: string) => string;
|
|
22
|
+
network: (s: string) => string;
|
|
23
|
+
disk: (s: string) => string;
|
|
24
|
+
graphs: (s: string) => string;
|
|
25
|
+
process: (s: string) => string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type { StatsData };
|
|
29
|
+
|
|
30
|
+
export interface VisibleItems {
|
|
31
|
+
cpu?: boolean;
|
|
32
|
+
mem?: boolean;
|
|
33
|
+
gpu?: boolean;
|
|
34
|
+
power?: boolean;
|
|
35
|
+
battery?: boolean;
|
|
36
|
+
thermal?: boolean;
|
|
37
|
+
network?: boolean;
|
|
38
|
+
disk?: boolean;
|
|
39
|
+
process?: boolean;
|
|
40
|
+
tree?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface TableOptions {
|
|
44
|
+
/** Terminal width in columns; controls layout, bar sizing, and column count. Defaults to 80. */
|
|
45
|
+
width?: number;
|
|
46
|
+
/** Process sort key. Defaults to 'cpu'. */
|
|
47
|
+
sortBy?: 'cpu' | 'mem' | 'pid' | 'user' | 'command' | 'state' | 'threads' | 'runtime';
|
|
48
|
+
/** Case-insensitive substring filter applied to the process command. */
|
|
49
|
+
filter?: string;
|
|
50
|
+
/** Max process rows to show (the live dashboard sizes this to available terminal height). */
|
|
51
|
+
processLimit?: number;
|
|
52
|
+
/** Selected visual theme. */
|
|
53
|
+
theme?: ThemeName;
|
|
54
|
+
/** Visibility toggle settings for individual cards. */
|
|
55
|
+
visible?: VisibleItems;
|
|
56
|
+
/** Show process tree view instead of flat list. */
|
|
57
|
+
treeView?: boolean;
|
|
58
|
+
}
|