pyre-cli 3.4.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +392 -186
- package/package.json +1 -1
- package/src/formatters/formatters.md +164 -0
- package/src/formatters/output.ts +15 -9
- package/src/formatters/render.ts +68 -36
- package/src/history.ts +6 -0
- package/src/live/input.ts +79 -57
- package/src/live/live.md +282 -0
- package/src/live/render.ts +182 -68
- package/src/live/state.ts +22 -1
- package/src/monitors/collectors.ts +126 -97
- package/src/monitors/monitors.md +165 -0
- package/src/monitors/run.ts +7 -3
- package/src/monitors/smc.ts +1 -1
- package/src/p2p/p2p.md +284 -0
- package/src/rest.md +129 -0
- package/src/sparkline.ts +8 -2
- package/src/ux.ts +62 -0
package/package.json
CHANGED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# src/formatters/ — Dashboard Formatting Module
|
|
2
|
+
|
|
3
|
+
The formatters module is responsible for turning raw system-metric data (`StatsData`) into styled terminal output. It provides three categories of functionality:
|
|
4
|
+
|
|
5
|
+
1. **Dashboard rendering** — `formatTable` draws the full terminal dashboard with panels, cards, tables, and tab navigation.
|
|
6
|
+
2. **Data export** — `formatJson`, `formatCsv`, `formatTsv` serialise a `StatsData` snapshot to structured text formats.
|
|
7
|
+
3. **Graph rendering** — `formatGraphs` draws sparkline/bar graphs of historical metrics.
|
|
8
|
+
|
|
9
|
+
## Files
|
|
10
|
+
|
|
11
|
+
| File | Purpose |
|
|
12
|
+
|------|---------|
|
|
13
|
+
| `index.ts` | Barrel export — re-exports all public functions and types |
|
|
14
|
+
| `types.ts` | Public-facing type definitions (`StatsData`, `VisibleItems`, `TableOptions`, `ThemeColors`, `ThemeName`) |
|
|
15
|
+
| `themes.ts` | Six built-in colour themes (`THEMES` record) mapping UI roles to chalk colour functions |
|
|
16
|
+
| `render.ts` | Core dashboard rendering: panels, cards, tables, tab bar, process tree, and the `formatTable` orchestrator |
|
|
17
|
+
| `output.ts` | JSON/CSV/TSV serialisation and sparkline/bar graph rendering |
|
|
18
|
+
|
|
19
|
+
## External Dependencies
|
|
20
|
+
|
|
21
|
+
The formatters module imports from two sibling modules:
|
|
22
|
+
|
|
23
|
+
- **`src/monitors/types.ts`** — defines `StatsData`, the aggregate data structure that every formatter consumes.
|
|
24
|
+
- **`src/history.ts`** — defines the `History` class, a rolling window of metric samples used by `formatGraphs`.
|
|
25
|
+
- **`src/sparkline.ts`** — exports the `sparkline()` function that renders Unicode block-character sparklines.
|
|
26
|
+
|
|
27
|
+
## `index.ts` — Barrel Exports
|
|
28
|
+
|
|
29
|
+
Re-exports everything public from the sub-modules:
|
|
30
|
+
|
|
31
|
+
- From `render.ts`: `formatTable`, `clampWidth`, `gridColumns`, `thermalColor`, `capacityColor`, `getTabHitboxes`, `TAB_BAR_ROW`, `TAB_DEFS`, `panel`
|
|
32
|
+
- From `output.ts`: `formatJson`, `formatCsv`, `formatTsv`, `formatGraphs`
|
|
33
|
+
- From `types.ts`: `ThemeName`, `ThemeColors`, `VisibleItems`, `TableOptions` (types only)
|
|
34
|
+
- From `themes.ts`: `THEMES`
|
|
35
|
+
|
|
36
|
+
## `types.ts` — Public Type Definitions
|
|
37
|
+
|
|
38
|
+
Defines the interfaces that control dashboard layout and visibility. All types are re-exported from `index.ts`.
|
|
39
|
+
|
|
40
|
+
### `ThemeName`
|
|
41
|
+
Union type re-exported from `themes.ts`: `'default' | 'dracula' | 'cyberpunk' | 'monochrome' | 'nord' | 'gruvbox'`.
|
|
42
|
+
|
|
43
|
+
### `ThemeColors`
|
|
44
|
+
Interface mapping 11 UI element roles to chalk colour functions: `border`, `cpu`, `mem`, `gpu`, `power`, `battery`, `thermal`, `network`, `disk`, `graphs`, `process`.
|
|
45
|
+
|
|
46
|
+
### `VisibleItems`
|
|
47
|
+
Optional boolean flags controlling which dashboard cards are shown. All default to visible when `undefined`:
|
|
48
|
+
- `cpu`, `mem`, `gpu`, `power`, `battery`, `thermal`, `network`, `packets`, `tasks`, `disk`, `process`, `tree`
|
|
49
|
+
|
|
50
|
+
### `TableOptions`
|
|
51
|
+
Options object for `formatTable`:
|
|
52
|
+
- `width?` — terminal width in columns (default 80, clamped to 60–240)
|
|
53
|
+
- `sortBy?` — process sort key: `'cpu' | 'mem' | 'pid' | 'user' | 'command' | 'state' | 'threads' | 'runtime'`
|
|
54
|
+
- `filter?` — case-insensitive substring filter on process command
|
|
55
|
+
- `processLimit?` — max process rows to display
|
|
56
|
+
- `theme?` — visual theme name
|
|
57
|
+
- `visible?` — `VisibleItems` override
|
|
58
|
+
- `treeView?` — show process tree instead of flat table
|
|
59
|
+
- `activePanel?` — detail panel to show: `'grid' | 'cpu' | 'mem' | 'gpu' | 'power' | 'battery' | 'thermal' | 'network' | 'packets' | 'tasks' | 'disk' | 'process'`
|
|
60
|
+
|
|
61
|
+
### `StatsData` (re-exported from `monitors/types.ts`)
|
|
62
|
+
The aggregate data structure consumed by all formatters. Contains: `header`, `cpu`, `gpu`, `memory`, `disk`, `battery`, `thermal`, `network`, `processes`, `power`, `timestamp`, `packets`, `tasks`.
|
|
63
|
+
|
|
64
|
+
## `themes.ts` — Colour Themes
|
|
65
|
+
|
|
66
|
+
Exports `THEMES`, a `Record<ThemeName, ThemeColors>` containing six built-in themes. Each theme maps the 11 UI roles to a chalk colour function.
|
|
67
|
+
|
|
68
|
+
### Built-in Themes
|
|
69
|
+
|
|
70
|
+
| Theme | Description |
|
|
71
|
+
|-------|-------------|
|
|
72
|
+
| `default` | Muted greys and cyan accents; safe for any terminal |
|
|
73
|
+
| `dracula` | Popular Dracula dark palette |
|
|
74
|
+
| `cyberpunk` | High-contrast neon on black |
|
|
75
|
+
| `monochrome` | White-only; useful for colour-blind users or terminals without 256-colour support |
|
|
76
|
+
| `nord` | Nord colour palette |
|
|
77
|
+
| `gruvbox` | Gruvbox retro colour palette |
|
|
78
|
+
|
|
79
|
+
Each theme's `ThemeColors` object has keys: `border`, `cpu`, `mem`, `gpu`, `power`, `battery`, `thermal`, `network`, `disk`, `graphs`, `process`.
|
|
80
|
+
|
|
81
|
+
## `render.ts` — Dashboard Rendering
|
|
82
|
+
|
|
83
|
+
The largest file in the module (660 lines). Contains every function that turns a `StatsData` snapshot into styled terminal output.
|
|
84
|
+
|
|
85
|
+
### Low-Level Utilities
|
|
86
|
+
|
|
87
|
+
- `visLen(s: string): number` — computes the visible length of a string by stripping ANSI escape codes.
|
|
88
|
+
- `padVisible(s: string, width: number): string` — pads a string with spaces to a target visible width.
|
|
89
|
+
- `truncatePlain(s: string, width: number): string` — truncates a plain string with `…` if it exceeds `width`.
|
|
90
|
+
|
|
91
|
+
### Panel & Layout
|
|
92
|
+
|
|
93
|
+
- `panel(title, lines, width, accent, borderAccent, height?): string[]` — draws a rounded panel box of exactly `width` columns. Returns an array of strings (one per line).
|
|
94
|
+
- `hstack(blocks, gap?): string[]` — joins equal-or-uneven-height panel blocks side by side with a gap.
|
|
95
|
+
- `statRow(label, value, width?): string` — renders a label/value pair with dim label.
|
|
96
|
+
- `gaugeRow(label, percent, contentWidth): string` — renders a horizontal bar gauge.
|
|
97
|
+
- `bar(percent, width?): string` — renders a filled/empty block bar coloured by threshold.
|
|
98
|
+
|
|
99
|
+
### Colour Helpers
|
|
100
|
+
|
|
101
|
+
- `thermalColor(pressureLevel: number): Chalk` — maps 0–3 pressure level to green/yellow/yellowBright/red.
|
|
102
|
+
- `capacityColor(percent: number): Chalk` — maps battery capacity to red (<80%)/yellow (<90%)/green.
|
|
103
|
+
- `formatBytes(bytes: number): string` — converts bytes to human-readable `B`/`KB`/`MB`/`GB`.
|
|
104
|
+
- `celsiusToFahrenheit(c: number): number` — converts °C to °F.
|
|
105
|
+
- `formatTemp(c: number): string` — renders `"62.0°C / 143.6°F"`.
|
|
106
|
+
|
|
107
|
+
### Card Renderers (each returns `string[] | null`)
|
|
108
|
+
|
|
109
|
+
- `cpuCard(data, contentWidth)` — brand, cores, frequency, usage gauge, load avg, optional temp.
|
|
110
|
+
- `memCard(data, contentWidth)` — total, used, cached, free, usage gauge, swap.
|
|
111
|
+
- `gpuCard(data, contentWidth)` — model, memory, utilization, optional temp, process count. Returns `null` if no GPU data.
|
|
112
|
+
- `powerCard(data)` — CPU/GPU/combined watts, battery watts if discharging. Returns `null` if no power data.
|
|
113
|
+
- `batteryCard(data, contentWidth)` — level/state, time remaining, cycles, condition, capacity, source, estimated empty, discharge rate. Returns `null` if no battery data.
|
|
114
|
+
- `thermalCard(data, contentWidth)` — state (coloured by pressure), per-sensor temps, optional error.
|
|
115
|
+
- `networkCard(data, contentWidth)` — interface name/IP, RX/TX bytes and packets.
|
|
116
|
+
- `packetCard(data, contentWidth)` — total/rx/tx packets, connections, top process, interface count. Returns `null` if no packet data.
|
|
117
|
+
- `tasksCard(data, contentWidth)` — up to 6 tasks with pid, user, cpu%, mem%, state, command. Returns `null` if no tasks.
|
|
118
|
+
|
|
119
|
+
### Table Renderers
|
|
120
|
+
|
|
121
|
+
- `processTableLines(processes, contentWidth, borderAccent?): string[]` — renders a fixed-column process table with PID, PPID, USER, CPU%, MEM%, STATE, THREADS, RUNTIME, COMMAND columns. Colour-codes CPU% and MEM% by threshold.
|
|
122
|
+
- `diskTableLines(disks, contentWidth, borderAccent?): string[]` — renders a filesystem table with FILESYSTEM, SIZE, USED, AVAIL, CAP, MOUNT columns. Colour-codes capacity by threshold.
|
|
123
|
+
- `buildProcessTree(processes): string[]` — renders a tree view of processes using Unicode box-drawing characters, sorted by CPU usage within each subtree.
|
|
124
|
+
|
|
125
|
+
### Tab Bar
|
|
126
|
+
|
|
127
|
+
- `TAB_DEFS` — canonical array of 12 tab definitions: `{ id, label, key }` mapping panel IDs to display labels and keyboard shortcuts (1–9, 0, P, R).
|
|
128
|
+
- `TAB_BAR_ROW` — constant `4` (1-indexed terminal row where the tab bar renders).
|
|
129
|
+
- `getTabHitboxes(): { id, start, end }[]` — computes column ranges for each tab so mouse clicks can be mapped back to panel IDs.
|
|
130
|
+
- `tabBar(activePanel, width, theme): string` — renders the tab bar line with active tab bold and inactive tabs dim.
|
|
131
|
+
|
|
132
|
+
### Header
|
|
133
|
+
|
|
134
|
+
- `header(data, width, badges?, borderAccent?): string[]` — renders the top header line with PYRE branding, hostname, OS, uptime, current time, and optional badges.
|
|
135
|
+
|
|
136
|
+
### Main Orchestrator
|
|
137
|
+
|
|
138
|
+
- `formatTable(data, opts?): string` — the primary entry point for dashboard rendering.
|
|
139
|
+
1. Clamps width via `clampWidth`.
|
|
140
|
+
2. If `activePanel !== 'grid'`, renders a single detail panel for that tab.
|
|
141
|
+
3. If `activePanel === 'grid'`, lays out visible cards in a grid (1–3 columns depending on width via `gridColumns`), then optionally appends Disk and Process panels.
|
|
142
|
+
4. Process panel supports `filter`, `sortBy`, `processLimit`, and `treeView` options.
|
|
143
|
+
|
|
144
|
+
### Exported Helpers
|
|
145
|
+
|
|
146
|
+
- `clampWidth(width?): number` — clamps terminal width to 60–240, defaulting to 80.
|
|
147
|
+
- `gridColumns(width: number): number` — returns 1/2/3 cards per row based on width thresholds (≥150→3, ≥96→2, else 1).
|
|
148
|
+
|
|
149
|
+
## `output.ts` — Data Export & Graphs
|
|
150
|
+
|
|
151
|
+
### Export Functions
|
|
152
|
+
|
|
153
|
+
- `formatJson(data: StatsData): string` — serialises `StatsData` to pretty-printed JSON (2-space indent).
|
|
154
|
+
- `formatCsv(data: StatsData): string` — serialises `StatsData` to CSV with a `property,key,value` header row. Includes all fields from cpu, memory, thermal, network, battery, power, system, packets (if present), gpu (if present), and every process (pid, ppid, user, cpu, mem, state, threads, runtime, command). String values are double-quote-escaped.
|
|
155
|
+
- `formatTsv(data: StatsData): string` — converts CSV output to TSV by replacing commas with tabs.
|
|
156
|
+
|
|
157
|
+
### Graph Rendering
|
|
158
|
+
|
|
159
|
+
- `formatGraphs(history, width?, themeName?, graphMode?): string` — renders a panel of sparkline/bar graphs for CPU, memory, temperature, network RX/TX, power, and packet rates. Uses `panel()` for the border and `graphRow()` for each metric line.
|
|
160
|
+
|
|
161
|
+
### Internal Helpers
|
|
162
|
+
|
|
163
|
+
- `graphRow(label, values, bounds, fmt, sparkWidth?, emptyMessage?, mode?): string` — renders a single graph line with label, sparkline/bar, and current value. Colour-codes by percentage of the bounds range.
|
|
164
|
+
- `barGraph(values, bounds, width): string` — renders a bar-graph using Unicode block characters (`█` `▇` `▆` `▄` ` `).
|
package/src/formatters/output.ts
CHANGED
|
@@ -104,21 +104,21 @@ export function formatGraphs(history: History, width = 80, themeName: ThemeName
|
|
|
104
104
|
const sparkWidth = Math.max(10, contentWidth - 28);
|
|
105
105
|
|
|
106
106
|
const lines: string[] = [];
|
|
107
|
-
lines.push(graphRow('CPU %', history.cpuUsage, { min: 0, max: 100 }, v => `${v.toFixed(0)}%`, sparkWidth, graphMode));
|
|
108
|
-
lines.push(graphRow('Mem %', history.memUsage, { min: 0, max: 100 }, v => `${v.toFixed(0)}%`, sparkWidth, graphMode));
|
|
107
|
+
lines.push(graphRow('CPU %', history.cpuUsage, { min: 0, max: 100 }, v => `${v.toFixed(0)}%`, sparkWidth, 'collecting data...', graphMode));
|
|
108
|
+
lines.push(graphRow('Mem %', history.memUsage, { min: 0, max: 100 }, v => `${v.toFixed(0)}%`, sparkWidth, 'collecting data...', graphMode));
|
|
109
109
|
lines.push(
|
|
110
110
|
graphRow('Temp', history.temp, {}, formatTemp, sparkWidth, 'no sensor access (needs sudo powermetrics)', graphMode)
|
|
111
111
|
);
|
|
112
|
-
lines.push(graphRow('Net RX/s', history.netRxRate, {}, v => `${formatBytes(v)}/s`, sparkWidth, graphMode));
|
|
113
|
-
lines.push(graphRow('Net TX/s', history.netTxRate, {}, v => `${formatBytes(v)}/s`, sparkWidth, graphMode));
|
|
112
|
+
lines.push(graphRow('Net RX/s', history.netRxRate, {}, v => `${formatBytes(v)}/s`, sparkWidth, 'collecting data...', graphMode));
|
|
113
|
+
lines.push(graphRow('Net TX/s', history.netTxRate, {}, v => `${formatBytes(v)}/s`, sparkWidth, 'collecting data...', graphMode));
|
|
114
114
|
if (history.powerWatts.length) {
|
|
115
|
-
lines.push(graphRow('Power W', history.powerWatts, {}, v => `${v.toFixed(1)} W`, sparkWidth, graphMode));
|
|
115
|
+
lines.push(graphRow('Power W', history.powerWatts, {}, v => `${v.toFixed(1)} W`, sparkWidth, 'collecting data...', graphMode));
|
|
116
116
|
}
|
|
117
117
|
if (history.rxPacketRate.length) {
|
|
118
|
-
lines.push(graphRow('RX pkt/s', history.rxPacketRate, {}, v => `${v.toFixed(0)} pkt/s`, sparkWidth, graphMode));
|
|
118
|
+
lines.push(graphRow('RX pkt/s', history.rxPacketRate, {}, v => `${v.toFixed(0)} pkt/s`, sparkWidth, 'collecting data...', graphMode));
|
|
119
119
|
}
|
|
120
120
|
if (history.txPacketRate.length) {
|
|
121
|
-
lines.push(graphRow('TX pkt/s', history.txPacketRate, {}, v => `${v.toFixed(0)} pkt/s`, sparkWidth, graphMode));
|
|
121
|
+
lines.push(graphRow('TX pkt/s', history.txPacketRate, {}, v => `${v.toFixed(0)} pkt/s`, sparkWidth, 'collecting data...', graphMode));
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
const title = `Graphs · mode:${graphMode} · last ${sampleCount} sample${sampleCount === 1 ? '' : 's'}`;
|
|
@@ -151,8 +151,14 @@ function graphRow(
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
function barGraph(values: number[], bounds: { min?: number; max?: number }, width: number): string {
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
let min = bounds.min ?? Infinity;
|
|
155
|
+
let max = bounds.max ?? -Infinity;
|
|
156
|
+
if (bounds.min === undefined || bounds.max === undefined) {
|
|
157
|
+
for (const v of values) {
|
|
158
|
+
if (v < min) min = v;
|
|
159
|
+
if (v > max) max = v;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
156
162
|
const range = max - min || 1;
|
|
157
163
|
return values.map(v => {
|
|
158
164
|
const pct = (v - min) / range;
|
package/src/formatters/render.ts
CHANGED
|
@@ -56,7 +56,7 @@ export function panel(
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/** Join equal-or-uneven-height panel blocks side by side with a gap. */
|
|
59
|
-
function hstack(blocks: string[][], gap = 2): string[] {
|
|
59
|
+
function hstack(blocks: string[][], gap = 2, targetWidth?: number): string[] {
|
|
60
60
|
if (!blocks.length) return [];
|
|
61
61
|
const height = Math.max(...blocks.map(b => b.length));
|
|
62
62
|
const widths = blocks.map(b => visLen(b[0] ?? ''));
|
|
@@ -68,7 +68,12 @@ function hstack(blocks: string[][], gap = 2): string[] {
|
|
|
68
68
|
});
|
|
69
69
|
const out: string[] = [];
|
|
70
70
|
for (let row = 0; row < height; row++) {
|
|
71
|
-
|
|
71
|
+
let line = padded.map(a => a[row]).join(' '.repeat(gap));
|
|
72
|
+
if (targetWidth !== undefined) {
|
|
73
|
+
const pad = Math.max(0, targetWidth - visLen(line));
|
|
74
|
+
line += ' '.repeat(pad);
|
|
75
|
+
}
|
|
76
|
+
out.push(line);
|
|
72
77
|
}
|
|
73
78
|
return out;
|
|
74
79
|
}
|
|
@@ -393,36 +398,54 @@ function processTableLines(
|
|
|
393
398
|
return `${hrs}h ${mins % 60}m`;
|
|
394
399
|
}
|
|
395
400
|
|
|
396
|
-
|
|
401
|
+
function buildProcessTree(
|
|
397
402
|
processes: { pid: number; ppid: number; user: string; cpu: number; mem: number; command: string; state: string; threads: number; runtime: number }[]
|
|
398
|
-
|
|
403
|
+
): string[] {
|
|
399
404
|
const map = new Map<number, typeof processes[0]>();
|
|
400
405
|
const roots: typeof processes[0][] = [];
|
|
401
406
|
for (const p of processes) map.set(p.pid, p);
|
|
402
407
|
for (const p of processes) {
|
|
403
408
|
if (p.ppid === 0 || !map.has(p.ppid)) roots.push(p);
|
|
404
409
|
}
|
|
405
|
-
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const pidStr = String(proc.pid).padEnd(8);
|
|
412
|
-
const cpuStr = pctColor(proc.cpu)(`${proc.cpu.toFixed(1)}%`.padEnd(8));
|
|
413
|
-
const memStr = pctColor(proc.mem)(`${proc.mem.toFixed(1)}%`.padEnd(8));
|
|
414
|
-
const stateStr = chalk.dim(proc.state.padEnd(8));
|
|
415
|
-
const thrStr = String(proc.threads).padEnd(6);
|
|
416
|
-
|
|
417
|
-
const cmd = truncatePlain(proc.command, Math.max(10, 100 - pidStr.length - cpuStr.length - memStr.length - stateStr.length - thrStr.length - 10));
|
|
418
|
-
lines.push(`${prefix}${connector}${pidStr}${cpuStr}${memStr}${stateStr}${thrStr} ${cmd}`);
|
|
419
|
-
const children = processes.filter(p => p.ppid === proc.pid && !visited.has(p.pid));
|
|
420
|
-
children.sort((a, b) => b.cpu - a.cpu);
|
|
421
|
-
children.forEach((child, i) => {
|
|
422
|
-
const nextPrefix = prefix + (isLast ? ' ' : '│ ');
|
|
423
|
-
renderNode(child, nextPrefix, i === children.length - 1);
|
|
424
|
-
});
|
|
410
|
+
|
|
411
|
+
const childrenMap = new Map<number, typeof processes[0][]>();
|
|
412
|
+
for (const p of processes) {
|
|
413
|
+
const list = childrenMap.get(p.ppid) || [];
|
|
414
|
+
list.push(p);
|
|
415
|
+
childrenMap.set(p.ppid, list);
|
|
425
416
|
}
|
|
417
|
+
for (const list of childrenMap.values()) {
|
|
418
|
+
list.sort((a, b) => b.cpu - a.cpu);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const lines: string[] = [];
|
|
422
|
+
const visited = new Set<number>();
|
|
423
|
+
function renderNode(proc: typeof processes[0], prefix: string, isLast: boolean) {
|
|
424
|
+
if (visited.has(proc.pid)) return;
|
|
425
|
+
visited.add(proc.pid);
|
|
426
|
+
const connector = isLast ? '└── ' : '├── ';
|
|
427
|
+
const pidStr = String(proc.pid).padEnd(8);
|
|
428
|
+
const cpuStr = pctColor(proc.cpu)(`${proc.cpu.toFixed(1)}%`.padEnd(8));
|
|
429
|
+
const memStr = pctColor(proc.mem)(`${proc.mem.toFixed(1)}%`.padEnd(8));
|
|
430
|
+
const stateStr = chalk.dim(proc.state.padEnd(8));
|
|
431
|
+
const thrStr = String(proc.threads).padEnd(6);
|
|
432
|
+
|
|
433
|
+
const cmd = truncatePlain(proc.command, Math.max(10, 100 - pidStr.length - cpuStr.length - memStr.length - stateStr.length - thrStr.length - 10));
|
|
434
|
+
lines.push(`${prefix}${connector}${pidStr}${cpuStr}${memStr}${stateStr}${thrStr} ${cmd}`);
|
|
435
|
+
const children = childrenMap.get(proc.pid);
|
|
436
|
+
if (children) {
|
|
437
|
+
let idx = 0;
|
|
438
|
+
let total = 0;
|
|
439
|
+
for (const c of children) {
|
|
440
|
+
if (!visited.has(c.pid)) total++;
|
|
441
|
+
}
|
|
442
|
+
for (const child of children) {
|
|
443
|
+
if (visited.has(child.pid)) continue;
|
|
444
|
+
const nextPrefix = prefix + (isLast ? ' ' : '│ ');
|
|
445
|
+
renderNode(child, nextPrefix, ++idx === total);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
426
449
|
roots.sort((a, b) => b.cpu - a.cpu);
|
|
427
450
|
roots.forEach((root, i) => renderNode(root, '', i === roots.length - 1));
|
|
428
451
|
return lines;
|
|
@@ -478,8 +501,10 @@ function detailPanelTitle(panel: string): string {
|
|
|
478
501
|
return map[panel] || panel;
|
|
479
502
|
}
|
|
480
503
|
|
|
481
|
-
function activeDetailLines(data: StatsData, activePanel: string, width: number): string[] | null {
|
|
504
|
+
function activeDetailLines(data: StatsData, activePanel: string, width: number, opts: TableOptions = {}): string[] | null {
|
|
482
505
|
const contentWidth = width - 4;
|
|
506
|
+
const themeName = opts.theme || 'default';
|
|
507
|
+
const theme = THEMES[themeName] || THEMES.default;
|
|
483
508
|
switch (activePanel) {
|
|
484
509
|
case 'cpu':
|
|
485
510
|
return cpuCard(data, contentWidth);
|
|
@@ -529,10 +554,17 @@ function activeDetailLines(data: StatsData, activePanel: string, width: number):
|
|
|
529
554
|
case 'tasks':
|
|
530
555
|
return data.tasks?.length ? tasksCard(data, contentWidth) : ['No task data available'];
|
|
531
556
|
case 'disk':
|
|
532
|
-
return data.disk.length ? diskTableLines(data.disk, width - 4,
|
|
557
|
+
return data.disk.length ? diskTableLines(data.disk, width - 4, theme.border) : ['No disk data available'];
|
|
533
558
|
case 'process': {
|
|
534
|
-
const
|
|
535
|
-
|
|
559
|
+
const filtered = opts.filter
|
|
560
|
+
? data.processes.filter(p => p.command.toLowerCase().includes(opts.filter!.toLowerCase()))
|
|
561
|
+
: data.processes;
|
|
562
|
+
const sorted = sortProcesses(filtered, opts.sortBy ?? 'cpu');
|
|
563
|
+
const limited = opts.processLimit ? sorted.slice(0, opts.processLimit) : sorted;
|
|
564
|
+
if (opts.treeView) {
|
|
565
|
+
return buildProcessTree(limited);
|
|
566
|
+
}
|
|
567
|
+
return processTableLines(limited, width - 4, theme.border);
|
|
536
568
|
}
|
|
537
569
|
default:
|
|
538
570
|
return null;
|
|
@@ -558,7 +590,7 @@ export function formatTable(data: StatsData, opts: TableOptions = {}): string {
|
|
|
558
590
|
out.push('');
|
|
559
591
|
|
|
560
592
|
if (activePanel !== 'grid') {
|
|
561
|
-
const lines = activeDetailLines(data, activePanel, width);
|
|
593
|
+
const lines = activeDetailLines(data, activePanel, width, opts);
|
|
562
594
|
if (lines) {
|
|
563
595
|
out.push(...panel(detailPanelTitle(activePanel), lines, width, theme[activePanel as keyof ThemeColors] ?? theme.cpu, theme.border));
|
|
564
596
|
}
|
|
@@ -601,13 +633,13 @@ export function formatTable(data: StatsData, opts: TableOptions = {}): string {
|
|
|
601
633
|
if (tasks) cards.push({ title: 'Tasks', accent: theme.process, lines: tasks });
|
|
602
634
|
}
|
|
603
635
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
636
|
+
for (let i = 0; i < cards.length; i += columns) {
|
|
637
|
+
const row = cards.slice(i, i + columns);
|
|
638
|
+
const rowHeight = Math.max(...row.map(c => c.lines.length));
|
|
639
|
+
const blocks = row.map(c => panel(c.title, c.lines, cardWidth, c.accent, theme.border, rowHeight));
|
|
640
|
+
out.push(...hstack(blocks, gap, width));
|
|
641
|
+
out.push('');
|
|
642
|
+
}
|
|
611
643
|
|
|
612
644
|
if (vis.disk !== false && data.disk.length) {
|
|
613
645
|
const dw = width - 4;
|
package/src/history.ts
CHANGED
|
@@ -16,6 +16,9 @@ export class History {
|
|
|
16
16
|
/** Maximum number of samples retained in the rolling window. */
|
|
17
17
|
maxLen: number;
|
|
18
18
|
|
|
19
|
+
/** Monotonically increasing version, bumped on every push. */
|
|
20
|
+
version = 0;
|
|
21
|
+
|
|
19
22
|
/** Rolling window of CPU usage percentages (0–100). */
|
|
20
23
|
cpuUsage: number[] = [];
|
|
21
24
|
|
|
@@ -113,6 +116,8 @@ export class History {
|
|
|
113
116
|
if (sample.rxPackets !== undefined) this.pushBounded(this.rxPacketRate, rxPktRate);
|
|
114
117
|
if (sample.txPackets !== undefined) this.pushBounded(this.txPacketRate, txPktRate);
|
|
115
118
|
if (sample.connections !== undefined) this.pushBounded(this.connections, sample.connections);
|
|
119
|
+
|
|
120
|
+
this.version++;
|
|
116
121
|
}
|
|
117
122
|
|
|
118
123
|
/** Clear all rolling windows and reset cumulative byte counters. */
|
|
@@ -132,6 +137,7 @@ export class History {
|
|
|
132
137
|
this.lastRxPackets = 0;
|
|
133
138
|
this.lastTxPackets = 0;
|
|
134
139
|
this.lastTs = 0;
|
|
140
|
+
this.version = 0;
|
|
135
141
|
}
|
|
136
142
|
|
|
137
143
|
/**
|