@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 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. Disk and net sparklines scale to the **max in that buffer** (rates have no 100% ceiling). CPU / RAM / GPU sparklines are percent 0–100.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yourfam/yf-vitals",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Live terminal dashboard for CPU, RAM, disk, net, and optional GPU",
5
5
  "type": "module",
6
6
  "bin": {
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 sparklines scale to the max in the last 60 ticks (no 100% ceiling).
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 " ".repeat(w);
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 " ".repeat(pad) + chars.join("");
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[]} dsk
9
- * @property {number[]} net
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: [], dsk: [], net: [] };
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
- dsk: pushSample(history.dsk, dsk),
56
- net: pushSample(history.net, net),
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 { BAR_WIDTH, bar, barCharset, sparkWidth, sparkline } from "./bar.js";
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
- barStr == null
115
+ pct == null
60
116
  ? `${label} ${pctStr}${mem ? ` ${mem}` : ""}`
61
- : `${label} [${color.green(barStr)}] ${pctStr}${mem ? ` ${mem}` : ""}`;
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")} [${b}] ${formatPercent(snap.cpu.percent)} ${detail}`,
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")} [${b}] ${formatPercent(snap.ram.percent)} ${formatBytePair(snap.ram.used, snap.ram.total)}`,
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(` ${color.green(sparkline(history.dsk, sparkW, { ascii }))}`);
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(` ${color.green(sparkline(history.dsk, sparkW, { ascii }))}`);
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(` ${color.yellow(sparkline(history.net, sparkW, { ascii }))}`);
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(` ${color.yellow(sparkline(history.net, sparkW, { ascii }))}`);
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");