@yourfam/yf-vitals 0.1.1 → 0.2.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 +3 -1
- package/package.json +1 -1
- package/src/args.js +2 -1
- package/src/bar.js +17 -2
- package/src/color.js +1 -0
- package/src/history.js +12 -23
- package/src/render.js +76 -15
package/README.md
CHANGED
|
@@ -42,7 +42,9 @@ One live dashboard. Refresh every **1.0s** (or `--interval`, or **2.0s** with `-
|
|
|
42
42
|
|
|
43
43
|
Header: `yf-vitals` · hostname · OS · `NP/NL` cores · RAM total · tick interval.
|
|
44
44
|
|
|
45
|
-
Each percent row has a 20-cell bar and a sparkline of the last 60 ticks.
|
|
45
|
+
Each percent row has a 20-cell bar and a sparkline of the last 60 ticks. Unused spark slots use the lowest tick (`▁` / `_`), not blank space. Bar fill stays the row color below 50%, turns **yellow at 50%**, **red at 80%**. Sparklines stay the row color.
|
|
46
|
+
|
|
47
|
+
Disk and net each show **two** sparks (`R`/`W`, `↑`/`↓`) scaled to the **max of that pair** in the buffer (rates have no 100% ceiling). CPU / RAM / GPU sparks are percent 0–100.
|
|
46
48
|
|
|
47
49
|
First disk/net tick may show `—` while counters settle.
|
|
48
50
|
|
package/package.json
CHANGED
package/src/args.js
CHANGED
|
@@ -99,6 +99,7 @@ Usage:
|
|
|
99
99
|
|
|
100
100
|
q / Q / Ctrl+C quit. GPU row is omitted when the OS has no GPU telemetry.
|
|
101
101
|
Disk rates are KiB/s–GiB/s; network rates are kbps/Mbps/Gbps.
|
|
102
|
-
Disk / net
|
|
102
|
+
Disk / net sparks are split (R/W, ↑/↓) and scale to the pair max in the last 60 ticks.
|
|
103
|
+
Bar fill turns yellow ≥50% and red ≥80%. Unused spark slots use the lowest tick.
|
|
103
104
|
Needs a terminal (no pipes). Node 20+.`;
|
|
104
105
|
}
|
package/src/bar.js
CHANGED
|
@@ -55,8 +55,9 @@ export function sparkline(values, width, opts = {}) {
|
|
|
55
55
|
if (w === 0) return "";
|
|
56
56
|
const charset = opts.ascii ? ASC_SPARK : UNI_SPARK;
|
|
57
57
|
const list = Array.isArray(values) ? values : [];
|
|
58
|
+
const floor = charset[0];
|
|
58
59
|
if (list.length === 0) {
|
|
59
|
-
return
|
|
60
|
+
return floor.repeat(w);
|
|
60
61
|
}
|
|
61
62
|
const slice = list.slice(-w);
|
|
62
63
|
const pad = w - slice.length;
|
|
@@ -64,7 +65,21 @@ export function sparkline(values, width, opts = {}) {
|
|
|
64
65
|
const dataMax = slice.reduce((m, v) => (v > m ? v : m), 0);
|
|
65
66
|
const max = explicitMax != null ? explicitMax : dataMax;
|
|
66
67
|
const chars = slice.map((v) => sparkChar(v, max, charset));
|
|
67
|
-
return
|
|
68
|
+
return floor.repeat(pad) + chars.join("");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const HEAT_WARM = 50;
|
|
72
|
+
export const HEAT_HOT = 80;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {number} percent
|
|
76
|
+
* @returns {"ok" | "warm" | "hot"}
|
|
77
|
+
*/
|
|
78
|
+
export function heatLevel(percent) {
|
|
79
|
+
const n = Number(percent);
|
|
80
|
+
if (!Number.isFinite(n) || n < HEAT_WARM) return "ok";
|
|
81
|
+
if (n < HEAT_HOT) return "warm";
|
|
82
|
+
return "hot";
|
|
68
83
|
}
|
|
69
84
|
|
|
70
85
|
/**
|
package/src/color.js
CHANGED
|
@@ -12,6 +12,7 @@ export function createColor(opts) {
|
|
|
12
12
|
magenta: (value) => pc.magenta(String(value)),
|
|
13
13
|
green: (value) => pc.green(String(value)),
|
|
14
14
|
yellow: (value) => pc.yellow(String(value)),
|
|
15
|
+
red: (value) => pc.red(String(value)),
|
|
15
16
|
dim: (value) => pc.dim(String(value)),
|
|
16
17
|
};
|
|
17
18
|
}
|
package/src/history.js
CHANGED
|
@@ -5,12 +5,14 @@ import { HISTORY_SIZE } from "./bar.js";
|
|
|
5
5
|
* @property {number[]} cpu
|
|
6
6
|
* @property {number[]} ram
|
|
7
7
|
* @property {number[]} gpu
|
|
8
|
-
* @property {number[]}
|
|
9
|
-
* @property {number[]}
|
|
8
|
+
* @property {number[]} dskR
|
|
9
|
+
* @property {number[]} dskW
|
|
10
|
+
* @property {number[]} netUp
|
|
11
|
+
* @property {number[]} netDn
|
|
10
12
|
*/
|
|
11
13
|
|
|
12
14
|
export function createHistory() {
|
|
13
|
-
return { cpu: [], ram: [], gpu: [],
|
|
15
|
+
return { cpu: [], ram: [], gpu: [], dskR: [], dskW: [], netUp: [], netDn: [] };
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
/**
|
|
@@ -33,26 +35,13 @@ export function pushSample(buf, value, size = HISTORY_SIZE) {
|
|
|
33
35
|
* @returns {History}
|
|
34
36
|
*/
|
|
35
37
|
export function appendHistory(history, snap) {
|
|
36
|
-
const cpu = snap.cpu?.percent;
|
|
37
|
-
const ram = snap.ram?.percent;
|
|
38
|
-
const gpu = snap.gpu?.percent;
|
|
39
|
-
const read = snap.disk?.readBps;
|
|
40
|
-
const write = snap.disk?.writeBps;
|
|
41
|
-
const tx = snap.net?.txBps;
|
|
42
|
-
const rx = snap.net?.rxBps;
|
|
43
|
-
let dsk;
|
|
44
|
-
if (read != null || write != null) {
|
|
45
|
-
dsk = (read ?? 0) + (write ?? 0);
|
|
46
|
-
}
|
|
47
|
-
let net;
|
|
48
|
-
if (tx != null || rx != null) {
|
|
49
|
-
net = (tx ?? 0) + (rx ?? 0);
|
|
50
|
-
}
|
|
51
38
|
return {
|
|
52
|
-
cpu: pushSample(history.cpu, cpu),
|
|
53
|
-
ram: pushSample(history.ram, ram),
|
|
54
|
-
gpu: pushSample(history.gpu, gpu),
|
|
55
|
-
|
|
56
|
-
|
|
39
|
+
cpu: pushSample(history.cpu, snap.cpu?.percent),
|
|
40
|
+
ram: pushSample(history.ram, snap.ram?.percent),
|
|
41
|
+
gpu: pushSample(history.gpu, snap.gpu?.percent),
|
|
42
|
+
dskR: pushSample(history.dskR, snap.disk?.readBps),
|
|
43
|
+
dskW: pushSample(history.dskW, snap.disk?.writeBps),
|
|
44
|
+
netUp: pushSample(history.netUp, snap.net?.txBps),
|
|
45
|
+
netDn: pushSample(history.netDn, snap.net?.rxBps),
|
|
57
46
|
};
|
|
58
47
|
}
|
package/src/render.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { useAscii } from "./ascii.js";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
BAR_WIDTH,
|
|
4
|
+
bar,
|
|
5
|
+
barCharset,
|
|
6
|
+
heatLevel,
|
|
7
|
+
sparkWidth,
|
|
8
|
+
sparkline,
|
|
9
|
+
} from "./bar.js";
|
|
3
10
|
import { createColor, stripAnsi } from "./color.js";
|
|
4
11
|
import {
|
|
5
12
|
formatBitRate,
|
|
@@ -39,6 +46,58 @@ export function fitLine(line, columns) {
|
|
|
39
46
|
return out;
|
|
40
47
|
}
|
|
41
48
|
|
|
49
|
+
/**
|
|
50
|
+
* @param {ReturnType<typeof createColor>} color
|
|
51
|
+
* @param {number} percent
|
|
52
|
+
* @param {"cyan" | "magenta" | "green"} row
|
|
53
|
+
*/
|
|
54
|
+
export function heatPaint(color, percent, row) {
|
|
55
|
+
const level = heatLevel(percent);
|
|
56
|
+
if (level === "hot") return color.red;
|
|
57
|
+
if (level === "warm") return color.yellow;
|
|
58
|
+
return color[row];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Percent bar: fill takes heat color, empty stays dim. Sparkline stays the row color.
|
|
63
|
+
*
|
|
64
|
+
* @param {number} percent
|
|
65
|
+
* @param {ReturnType<typeof createColor>} color
|
|
66
|
+
* @param {"cyan" | "magenta" | "green"} row
|
|
67
|
+
* @param {boolean} ascii
|
|
68
|
+
*/
|
|
69
|
+
export function paintPercentBar(percent, color, row, ascii) {
|
|
70
|
+
const cs = barCharset(ascii);
|
|
71
|
+
const raw = bar(percent, BAR_WIDTH, cs);
|
|
72
|
+
const filled = raw.replaceAll(cs.empty, "").length;
|
|
73
|
+
const fill = raw.slice(0, filled);
|
|
74
|
+
const empty = raw.slice(filled);
|
|
75
|
+
const paint = heatPaint(color, percent, row);
|
|
76
|
+
return `[${paint(fill)}${color.dim(empty)}]`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Two labeled sparks on one line, sharing a max so the pair is comparable.
|
|
81
|
+
*
|
|
82
|
+
* @param {string} leftLabel
|
|
83
|
+
* @param {number[]} leftVals
|
|
84
|
+
* @param {string} rightLabel
|
|
85
|
+
* @param {number[]} rightVals
|
|
86
|
+
* @param {number} sparkW
|
|
87
|
+
* @param {boolean} ascii
|
|
88
|
+
*/
|
|
89
|
+
export function splitSparkLine(leftLabel, leftVals, rightLabel, rightVals, sparkW, ascii) {
|
|
90
|
+
const leftTag = `${leftLabel} `;
|
|
91
|
+
const mid = ` ${rightLabel} `;
|
|
92
|
+
const overhead = leftTag.length + mid.length;
|
|
93
|
+
const inner = Math.max(overhead + 8, Number(sparkW) || 16);
|
|
94
|
+
const each = Math.max(4, Math.floor((inner - overhead) / 2));
|
|
95
|
+
const pairMax = Math.max(0, ...leftVals, ...rightVals);
|
|
96
|
+
const left = sparkline(leftVals, each, { ascii, max: pairMax || undefined });
|
|
97
|
+
const right = sparkline(rightVals, each, { ascii, max: pairMax || undefined });
|
|
98
|
+
return `${leftTag}${left}${mid}${right}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
42
101
|
/**
|
|
43
102
|
* @param {import("./sample.js").GpuSample | null | undefined} gpu
|
|
44
103
|
* @param {{ history: number[], ascii: boolean, color: ReturnType<typeof createColor>, sparkW: number }} opts
|
|
@@ -47,18 +106,15 @@ export function fitLine(line, columns) {
|
|
|
47
106
|
export function buildGpuRow(gpu, opts) {
|
|
48
107
|
if (gpu == null) return null;
|
|
49
108
|
const { ascii, color, sparkW, history } = opts;
|
|
50
|
-
const cs = barCharset(ascii);
|
|
51
109
|
const pct = gpu.percent;
|
|
52
|
-
const barStr =
|
|
53
|
-
pct == null ? null : bar(pct, BAR_WIDTH, cs);
|
|
54
110
|
const pctStr = pct == null ? "n/a" : formatPercent(pct);
|
|
55
111
|
const mem =
|
|
56
112
|
gpu.used != null && gpu.total != null ? formatBytePair(gpu.used, gpu.total) : "";
|
|
57
113
|
const label = color.green("GPU");
|
|
58
114
|
const head =
|
|
59
|
-
|
|
115
|
+
pct == null
|
|
60
116
|
? `${label} ${pctStr}${mem ? ` ${mem}` : ""}`
|
|
61
|
-
: `${label}
|
|
117
|
+
: `${label} ${paintPercentBar(pct, color, "green", ascii)} ${pctStr}${mem ? ` ${mem}` : ""}`;
|
|
62
118
|
const spark = color.green(sparkline(history, sparkW, { ascii, max: 100 }));
|
|
63
119
|
return [head, ` ${spark}`];
|
|
64
120
|
}
|
|
@@ -74,7 +130,6 @@ export function renderFrame(snap, history, opts = {}) {
|
|
|
74
130
|
const ascii = useAscii(env);
|
|
75
131
|
const color = createColor({ isTTY: Boolean(opts.isTTY), env });
|
|
76
132
|
const sparkW = sparkWidth(columns);
|
|
77
|
-
const cs = barCharset(ascii);
|
|
78
133
|
const interval = formatInterval(opts.intervalSec ?? 1);
|
|
79
134
|
const cores = formatCores(snap.physical, snap.logical);
|
|
80
135
|
const ramTot = formatBytes(snap.ramTotal);
|
|
@@ -91,10 +146,9 @@ export function renderFrame(snap, history, opts = {}) {
|
|
|
91
146
|
const lines = [header, ""];
|
|
92
147
|
|
|
93
148
|
if (snap.cpu) {
|
|
94
|
-
const b = color.cyan(bar(snap.cpu.percent, BAR_WIDTH, cs));
|
|
95
149
|
const detail = formatCoresDetail(snap.physical, snap.logical);
|
|
96
150
|
lines.push(
|
|
97
|
-
`${color.cyan("CPU")}
|
|
151
|
+
`${color.cyan("CPU")} ${paintPercentBar(snap.cpu.percent, color, "cyan", ascii)} ${formatPercent(snap.cpu.percent)} ${detail}`,
|
|
98
152
|
);
|
|
99
153
|
lines.push(` ${color.cyan(sparkline(history.cpu, sparkW, { ascii, max: 100 }))}`);
|
|
100
154
|
} else {
|
|
@@ -104,9 +158,8 @@ export function renderFrame(snap, history, opts = {}) {
|
|
|
104
158
|
lines.push("");
|
|
105
159
|
|
|
106
160
|
if (snap.ram) {
|
|
107
|
-
const b = color.magenta(bar(snap.ram.percent, BAR_WIDTH, cs));
|
|
108
161
|
lines.push(
|
|
109
|
-
`${color.magenta("RAM")}
|
|
162
|
+
`${color.magenta("RAM")} ${paintPercentBar(snap.ram.percent, color, "magenta", ascii)} ${formatPercent(snap.ram.percent)} ${formatBytePair(snap.ram.used, snap.ram.total)}`,
|
|
110
163
|
);
|
|
111
164
|
lines.push(` ${color.magenta(sparkline(history.ram, sparkW, { ascii, max: 100 }))}`);
|
|
112
165
|
} else {
|
|
@@ -130,10 +183,14 @@ export function renderFrame(snap, history, opts = {}) {
|
|
|
130
183
|
const r = formatByteRate(snap.disk.readBps);
|
|
131
184
|
const w = formatByteRate(snap.disk.writeBps);
|
|
132
185
|
lines.push(`${color.green("DSK")} R ${r.padStart(11)} W ${w.padStart(11)}`);
|
|
133
|
-
lines.push(
|
|
186
|
+
lines.push(
|
|
187
|
+
` ${color.green(splitSparkLine("R", history.dskR, "W", history.dskW, sparkW, ascii))}`,
|
|
188
|
+
);
|
|
134
189
|
} else {
|
|
135
190
|
lines.push(`${color.green("DSK")} n/a`);
|
|
136
|
-
lines.push(
|
|
191
|
+
lines.push(
|
|
192
|
+
` ${color.green(splitSparkLine("R", history.dskR, "W", history.dskW, sparkW, ascii))}`,
|
|
193
|
+
);
|
|
137
194
|
}
|
|
138
195
|
lines.push("");
|
|
139
196
|
|
|
@@ -141,10 +198,14 @@ export function renderFrame(snap, history, opts = {}) {
|
|
|
141
198
|
const up = formatBitRate(snap.net.txBps);
|
|
142
199
|
const down = formatBitRate(snap.net.rxBps);
|
|
143
200
|
lines.push(`${color.yellow("NET")} ↑ ${up.padStart(12)} ↓ ${down.padStart(12)}`);
|
|
144
|
-
lines.push(
|
|
201
|
+
lines.push(
|
|
202
|
+
` ${color.yellow(splitSparkLine("↑", history.netUp, "↓", history.netDn, sparkW, ascii))}`,
|
|
203
|
+
);
|
|
145
204
|
} else {
|
|
146
205
|
lines.push(`${color.yellow("NET")} n/a`);
|
|
147
|
-
lines.push(
|
|
206
|
+
lines.push(
|
|
207
|
+
` ${color.yellow(splitSparkLine("↑", history.netUp, "↓", history.netDn, sparkW, ascii))}`,
|
|
208
|
+
);
|
|
148
209
|
}
|
|
149
210
|
lines.push("");
|
|
150
211
|
lines.push("q quit");
|