pyre-cli 1.4.0 → 3.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/src/live/input.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  import readline from 'node:readline';
10
10
  import chalk from 'chalk';
11
11
  import { collectAll, StatsData } from '../monitors/index.js';
12
- import { THEMES, type ThemeName, type VisibleItems } from '../formatters/index.js';
12
+ import { THEMES, type ThemeName, type VisibleItems, getTabHitboxes, TAB_BAR_ROW } from '../formatters/index.js';
13
13
  import { state, setStatus } from './state.js';
14
14
  import { exportSnapshot, startLogging, stopLogging, toggleLogging, writeLogRow } from './export.js';
15
15
  import { render, footerLine, checkAlerts, processRowBudget } from './render.js';
@@ -118,29 +118,33 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
118
118
  state.intervalHandle = setInterval(tick, state.interval * 1000);
119
119
  }
120
120
 
121
- async function tick() {
122
- if (state.paused) return;
123
- try {
124
- const data = await collectAll({ detailed: state.detailed, processLimit: processRowBudget() });
125
- state.lastData = data;
126
-
127
- const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
128
- state.history.push({
129
- cpuUsage: data.cpu.usage,
130
- memUsage: data.memory.usagePercent,
131
- temp,
132
- rxBytes: data.network.rxBytes,
133
- txBytes: data.network.txBytes,
134
- gpuUtil: data.gpu?.utilization,
135
- });
136
-
137
- writeLogRow(data);
138
- checkAlerts(data);
139
- render();
140
- } catch {
141
- // skip bad tick
142
- }
143
- }
121
+ async function tick() {
122
+ if (state.paused) return;
123
+ try {
124
+ const data = await collectAll({ detailed: state.detailed, processLimit: processRowBudget() });
125
+ state.lastData = data;
126
+
127
+ const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
128
+ state.history.push({
129
+ cpuUsage: data.cpu.usage,
130
+ memUsage: data.memory.usagePercent,
131
+ temp,
132
+ rxBytes: data.network.rxBytes,
133
+ txBytes: data.network.txBytes,
134
+ gpuUtil: data.gpu?.utilization,
135
+ powerWatts: data.power?.combinedWatts ?? data.power?.cpuWatts ?? 0,
136
+ rxPackets: data.network.rxPackets,
137
+ txPackets: data.network.txPackets,
138
+ connections: data.network.connections ?? 0,
139
+ });
140
+
141
+ writeLogRow(data);
142
+ checkAlerts(data);
143
+ render();
144
+ } catch {
145
+ // skip bad tick
146
+ }
147
+ }
144
148
 
145
149
  /**
146
150
  * Start the interactive live dashboard.
@@ -149,163 +153,291 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
149
153
  * rendering interval. Idempotent — calling while already
150
154
  * running is a no-op.
151
155
  */
152
- export async function startLive(opts: LiveOptions) {
153
- if (state.running) return;
154
- state.running = true;
155
- state.paused = false;
156
- state.detailed = !!opts.detailed;
157
- state.interval = opts.interval;
158
- if (opts.exportDir) state.exportDir = opts.exportDir;
159
- state.termWidth = process.stdout.columns || 80;
160
- state.termHeight = process.stdout.rows || 24;
161
- state.history.reset();
162
- state.history.setMaxLen(Math.max(20, Math.min(200, state.termWidth - 30)));
156
+ export async function startLive(opts: LiveOptions) {
157
+ if (state.running) return;
158
+ state.running = true;
159
+ state.paused = false;
160
+ state.detailed = !!opts.detailed;
161
+ state.interval = opts.interval;
162
+ if (opts.exportDir) state.exportDir = opts.exportDir;
163
+ state.termWidth = process.stdout.columns || 80;
164
+ state.termHeight = process.stdout.rows || 24;
165
+ state.history.reset();
166
+ state.history.setMaxLen(Math.max(20, Math.min(200, state.termWidth - 30)));
167
+
168
+ process.stdout.write('\x1b[?1049h');
169
+ process.stdout.write('\x1b[?25l');
170
+ process.stdout.write('\x1b[2J\x1b[H');
171
+ process.title = 'pyre';
172
+
173
+ if (state.mouseEnabled) {
174
+ process.stdout.write('\x1b[?1000h');
175
+ }
163
176
 
164
- process.stdout.write('\x1b[?1049h');
165
- process.stdout.write('\x1b[?25l');
166
- process.stdout.write('\x1b[2J\x1b[H');
167
- process.title = 'pyre';
177
+ await tick();
178
+ restartTicker();
168
179
 
169
- await tick();
170
- restartTicker();
180
+ if (opts.autoLog) startLogging();
171
181
 
172
- if (opts.autoLog) startLogging();
182
+ readline.emitKeypressEvents(process.stdin);
183
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
184
+ process.stdin.resume();
173
185
 
174
- readline.emitKeypressEvents(process.stdin);
175
- if (process.stdin.isTTY) process.stdin.setRawMode(true);
176
- process.stdin.resume();
186
+ process.stdout.on('resize', onResize);
177
187
 
178
- process.stdout.on('resize', onResize);
188
+ state.keypressHandler = (str: string, key: readline.Key) => {
189
+ if (!key) return;
179
190
 
180
- state.keypressHandler = (str: string, key: readline.Key) => {
181
- if (!key) return;
191
+ if (key.ctrl && key.name === 'c') {
192
+ stopLive();
193
+ return;
194
+ }
182
195
 
183
- if (key.ctrl && key.name === 'c') {
184
- stopLive();
185
- return;
186
- }
196
+ if (state.inputMode) {
197
+ handleInputModeKey(str, key);
198
+ return;
199
+ }
187
200
 
188
- if (state.inputMode) {
189
- handleInputModeKey(str, key);
190
- return;
191
- }
201
+ if (key.mouse) {
202
+ handleMouseClick(key.mouse);
203
+ return;
204
+ }
192
205
 
193
- switch (key.sequence) {
194
- case '+':
195
- state.interval += 1;
196
- restartTicker();
197
- setStatus(`Interval set to ${state.interval}s`);
198
- return;
199
- case '-':
200
- state.interval = Math.max(1, state.interval - 1);
201
- restartTicker();
202
- setStatus(`Interval set to ${state.interval}s`);
203
- return;
204
- case '/':
205
- state.inputMode = 'filter';
206
- state.inputBuffer = state.processFilter;
207
- render();
208
- return;
209
- }
206
+ if (key.sequence && key.sequence.startsWith('\x1b[M')) {
207
+ handleNormalMouse(key.sequence);
208
+ return;
209
+ }
210
210
 
211
- switch (key.name) {
212
- case 'q':
213
- stopLive();
214
- break;
215
- case 'c':
216
- state.inputMode = 'customizer';
217
- state.customizerIndex = 0;
218
- render();
219
- break;
220
- case 'p':
221
- state.paused = !state.paused;
222
- setStatus(state.paused ? 'Paused' : 'Resumed');
223
- render();
224
- break;
225
- case 'g':
226
- state.showGraphs = !state.showGraphs;
211
+ if (key.sequence && key.sequence.startsWith('\x1b[<')) {
212
+ handleSGRMouse(key.sequence);
213
+ return;
214
+ }
215
+
216
+ switch (key.sequence) {
217
+ case '+':
218
+ state.interval += 1;
219
+ restartTicker();
220
+ setStatus(`Interval set to ${state.interval}s`);
221
+ return;
222
+ case '-':
223
+ state.interval = Math.max(1, state.interval - 1);
224
+ restartTicker();
225
+ setStatus(`Interval set to ${state.interval}s`);
226
+ return;
227
+ case '/':
228
+ state.inputMode = 'filter';
229
+ state.inputBuffer = state.processFilter;
230
+ render();
231
+ return;
232
+ case '0': case '1': case '2': case '3': case '4':
233
+ case '5': case '6': case '7': case '8': case '9': {
234
+ const tabMap: Record<string, string> = {
235
+ '0': 'disk', '1': 'cpu', '2': 'mem', '3': 'gpu', '4': 'power',
236
+ '5': 'battery', '6': 'thermal', '7': 'network', '8': 'packets', '9': 'tasks'
237
+ };
238
+ const panelId = tabMap[key.sequence];
239
+ if (panelId) {
240
+ state.activePanel = state.activePanel === panelId ? 'grid' : panelId;
241
+ setStatus(state.activePanel === 'grid' ? 'Grid view' : `${panelId} panel`);
242
+ render();
243
+ }
244
+ return;
245
+ }
246
+ case 'P': {
247
+ state.activePanel = state.activePanel === 'process' ? 'grid' : 'process';
248
+ setStatus(state.activePanel === 'grid' ? 'Grid view' : 'Process panel');
227
249
  render();
250
+ return;
251
+ }
252
+ }
253
+
254
+ switch (key.name) {
255
+ case 'q':
256
+ stopLive();
228
257
  break;
229
- case 'b':
230
- state.graphMode = state.graphMode === 'spark' ? 'bar' : 'spark';
231
- setStatus(`Graph mode: ${state.graphMode}`);
258
+ case 'left': {
259
+ const currentIdx = state.PANEL_TABS.findIndex(t => t.id === state.activePanel);
260
+ if (currentIdx > 0) {
261
+ state.activePanel = state.PANEL_TABS[currentIdx - 1].id;
262
+ } else if (currentIdx === -1) {
263
+ state.activePanel = state.PANEL_TABS[state.PANEL_TABS.length - 1].id;
264
+ }
265
+ setStatus(`Panel: ${state.activePanel}`);
232
266
  render();
233
267
  break;
234
- case 'd':
235
-
236
- state.detailed = !state.detailed;
237
- setStatus(`Detailed sensor mode: ${state.detailed ? 'on' : 'off'}`);
238
- break;
239
- case 's':
240
- const sortCycle: SortMode[] = ['cpu', 'mem', 'pid', 'user', 'command', 'state', 'threads', 'runtime'];
241
- const curIdx = sortCycle.indexOf(state.sortMode);
242
- state.sortMode = sortCycle[(curIdx + 1) % sortCycle.length];
243
- setStatus(`Sorting by ${state.sortMode}`);
244
- render();
245
- break;
246
- case 'k':
247
- state.inputMode = 'kill';
248
- state.inputBuffer = '';
249
- render();
250
- break;
251
- case 'S':
252
- state.inputMode = 'signal';
253
- state.inputBuffer = 'SIGTERM';
254
- render();
255
- break;
256
- case 't':
257
- state.treeView = !state.treeView;
258
- setStatus(state.treeView ? 'Tree view enabled' : 'Flat view enabled');
259
- render();
260
- break;
261
- case 'e':
262
- exportSnapshot();
263
- render();
264
- break;
265
- case 'l':
266
- toggleLogging();
267
- render();
268
- break;
269
- case 'f':
270
- state.exportFormat = state.exportFormat === 'json' ? 'csv' : state.exportFormat === 'csv' ? 'tsv' : 'json';
271
- setStatus(`Export format: ${state.exportFormat}`);
272
- render();
273
- break;
274
- }
275
- };
268
+ }
269
+ case 'right': {
270
+ const currentIdx = state.PANEL_TABS.findIndex(t => t.id === state.activePanel);
271
+ if (currentIdx < state.PANEL_TABS.length - 1) {
272
+ state.activePanel = state.PANEL_TABS[currentIdx + 1].id;
273
+ } else if (currentIdx === -1) {
274
+ state.activePanel = state.PANEL_TABS[0].id;
275
+ }
276
+ setStatus(`Panel: ${state.activePanel}`);
277
+ render();
278
+ break;
279
+ }
280
+ case 'c':
281
+ state.inputMode = 'customizer';
282
+ state.customizerIndex = 0;
283
+ render();
284
+ break;
285
+ case 'p':
286
+ state.paused = !state.paused;
287
+ setStatus(state.paused ? 'Paused' : 'Resumed');
288
+ render();
289
+ break;
290
+ case 'g':
291
+ state.showGraphs = !state.showGraphs;
292
+ render();
293
+ break;
294
+ case 'b':
295
+ state.graphMode = state.graphMode === 'spark' ? 'bar' : 'spark';
296
+ setStatus(`Graph mode: ${state.graphMode}`);
297
+ render();
298
+ break;
299
+ case 'd':
300
+
301
+ state.detailed = !state.detailed;
302
+ setStatus(`Detailed sensor mode: ${state.detailed ? 'on' : 'off'}`);
303
+ break;
304
+ case 's':
305
+ const sortCycle: SortMode[] = ['cpu', 'mem', 'pid', 'user', 'command', 'state', 'threads', 'runtime'];
306
+ const curIdx = sortCycle.indexOf(state.sortMode);
307
+ state.sortMode = sortCycle[(curIdx + 1) % sortCycle.length];
308
+ setStatus(`Sorting by ${state.sortMode}`);
309
+ render();
310
+ break;
311
+ case 'k':
312
+ state.inputMode = 'kill';
313
+ state.inputBuffer = '';
314
+ render();
315
+ break;
316
+ case 'S':
317
+ state.inputMode = 'signal';
318
+ state.inputBuffer = 'SIGTERM';
319
+ render();
320
+ break;
321
+ case 't':
322
+ state.treeView = !state.treeView;
323
+ setStatus(state.treeView ? 'Tree view enabled' : 'Flat view enabled');
324
+ render();
325
+ break;
326
+ case 'e':
327
+ exportSnapshot();
328
+ render();
329
+ break;
330
+ case 'l':
331
+ toggleLogging();
332
+ render();
333
+ break;
334
+ case 'f':
335
+ state.exportFormat = state.exportFormat === 'json' ? 'csv' : state.exportFormat === 'csv' ? 'tsv' : 'json';
336
+ setStatus(`Export format: ${state.exportFormat}`);
337
+ render();
338
+ break;
339
+ case 'tab':
340
+ cycleTab(1);
341
+ break;
342
+ case 'left':
343
+ cycleTab(-1);
344
+ break;
345
+ case 'right':
346
+ cycleTab(1);
347
+ break;
348
+ default:
349
+ if (str && str.length === 1) {
350
+ const tabId = tabKeyToId(str);
351
+ if (tabId) {
352
+ state.activePanel = tabId;
353
+ setStatus(`Panel: ${tabId.toUpperCase()}`);
354
+ render();
355
+ }
356
+ }
357
+ }
358
+ };
276
359
 
277
- process.stdin.on('keypress', state.keypressHandler);
278
- process.once('SIGINT', () => stopLive());
279
- }
360
+ process.stdin.on('keypress', state.keypressHandler);
361
+ process.once('SIGINT', () => stopLive());
362
+ }
280
363
 
281
- export function stopLive() {
282
- if (state.intervalHandle) {
283
- clearInterval(state.intervalHandle);
284
- state.intervalHandle = null;
285
- }
286
- if (state.statusTimer) {
287
- clearTimeout(state.statusTimer);
288
- state.statusTimer = null;
289
- }
290
- if (state.logStream) {
291
- state.logStream.end();
292
- state.logStream = null;
293
- }
294
- if (state.keypressHandler) {
295
- process.stdin.removeListener('keypress', state.keypressHandler);
296
- state.keypressHandler = null;
297
- }
298
- process.stdout.removeListener('resize', onResize);
364
+ function tabKeyToId(str: string): string | null {
365
+ const map: Record<string, string> = { '1': 'cpu', '2': 'mem', '3': 'gpu', '4': 'power', '5': 'battery', '6': 'thermal', '7': 'network', '8': 'packets', '9': 'tasks', '0': 'disk', 'p': 'process' };
366
+ return map[str] ?? null;
367
+ }
368
+
369
+ function cycleTab(direction: number) {
370
+ const tabs = state.PANEL_TABS.map(t => t.id);
371
+ const current = state.activePanel === 'grid' ? -1 : tabs.indexOf(state.activePanel);
372
+ const next = (current + direction + tabs.length) % tabs.length;
373
+ state.activePanel = tabs[next];
374
+ setStatus(`Panel: ${state.activePanel.toUpperCase()}`);
375
+ render();
376
+ }
377
+
378
+ function handleNormalMouse(seq: string) {
379
+ if (seq.length < 6) return;
380
+ const cb = seq.charCodeAt(3) - 32;
381
+ const cx = seq.charCodeAt(4) - 32;
382
+ const cy = seq.charCodeAt(5) - 32;
383
+ if (cb === 0 || cb === 1) {
384
+ handleMouseClick(cy, cx);
385
+ }
386
+ }
387
+
388
+ function handleSGRMouse(seq: string) {
389
+ const match = seq.match(/^\x1b\[<(\d+);(\d+);(\d+)([Mm])/);
390
+ if (!match) return;
391
+ const button = parseInt(match[1]);
392
+ const cx = parseInt(match[2]);
393
+ const cy = parseInt(match[3]);
394
+ if (button === 0 || button === 1) {
395
+ handleMouseClick(cy, cx);
396
+ }
397
+ }
299
398
 
300
- state.running = false;
301
- state.logging = false;
399
+ function handleMouseClick(y: number, x: number) {
400
+ if (y !== TAB_BAR_ROW) return;
401
+ const hitboxes = getTabHitboxes();
402
+ const hit = hitboxes.find(h => x >= h.start && x < h.end);
403
+ if (hit) {
404
+ state.activePanel = state.activePanel === hit.id ? 'grid' : (hit.id as typeof state.activePanel);
405
+ setStatus(state.activePanel === 'grid' ? 'Grid view' : `Panel: ${hit.id.toUpperCase()}`);
406
+ render();
407
+ }
408
+ }
409
+
410
+ export function stopLive() {
411
+ if (state.intervalHandle) {
412
+ clearInterval(state.intervalHandle);
413
+ state.intervalHandle = null;
414
+ }
415
+ if (state.statusTimer) {
416
+ clearTimeout(state.statusTimer);
417
+ state.statusTimer = null;
418
+ }
419
+ if (state.logStream) {
420
+ state.logStream.end();
421
+ state.logStream = null;
422
+ }
423
+ if (state.keypressHandler) {
424
+ process.stdin.removeListener('keypress', state.keypressHandler);
425
+ state.keypressHandler = null;
426
+ }
427
+ process.stdout.removeListener('resize', onResize);
302
428
 
303
- if (process.stdin.isTTY) process.stdin.setRawMode(false);
304
- process.stdin.pause();
429
+ state.running = false;
430
+ state.logging = false;
305
431
 
306
- process.stdout.write('\x1b[?25h');
307
- process.stdout.write('\x1b[?1049l');
308
- process.stdout.write('\x1b[2J\x1b[H');
432
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
433
+ process.stdin.pause();
434
+
435
+ if (state.mouseEnabled) {
436
+ process.stdout.write('\x1b[?1000l');
437
+ }
438
+ process.stdout.write('\x1b[?25h');
439
+ process.stdout.write('\x1b[?1049l');
440
+ process.stdout.write('\x1b[2J\x1b[H');
309
441
 
310
- process.exit(0);
311
- }
442
+ process.exit(0);
443
+ }
@@ -19,7 +19,8 @@ import type { StatsData } from '../monitors/index.js';
19
19
  */
20
20
  function processRowBudget(): number {
21
21
  const columns = gridColumns(state.termWidth);
22
- const cardRows = Math.ceil(6 / columns);
22
+ const visibleCards = Object.values(state.visiblePanels).filter(v => v !== false).length;
23
+ const cardRows = Math.ceil(visibleCards / columns);
23
24
  const reserved = 2 + cardRows * 8 + 11 + (state.showGraphs ? 9 : 0) + 4;
24
25
  return Math.max(3, Math.min(40, state.termHeight - reserved));
25
26
  }
@@ -65,6 +66,7 @@ function render() {
65
66
  theme: state.currentTheme,
66
67
  visible: state.visiblePanels,
67
68
  treeView: state.treeView,
69
+ activePanel: state.activePanel,
68
70
  })
69
71
  );
70
72
 
@@ -76,21 +78,21 @@ function render() {
76
78
  lines.push('');
77
79
  lines.push(footerLine());
78
80
 
79
- if (state.inputMode === 'customizer') {
80
- lines.push(renderCustomizerOverlay());
81
- } else if (state.inputMode === 'filter') {
82
- lines.push(chalk.cyan(` Filter processes: ${state.inputBuffer}_`));
83
- } else if (state.inputMode === 'kill') {
84
- lines.push(chalk.cyan(` Kill PID: ${state.inputBuffer}_ (enter to confirm, esc to cancel)`));
85
- } else if (state.inputMode === 'signal') {
86
- const sigIdx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer as typeof state.SIGNAL_OPTIONS[number]);
87
- const sigList = state.SIGNAL_OPTIONS.map((s, i) => i === sigIdx ? chalk.yellow(s) : chalk.dim(s)).join(' ');
88
- lines.push(chalk.cyan(` Signal: ${state.inputBuffer}_`));
89
- lines.push(chalk.dim(` ${sigList}`));
90
- lines.push(chalk.dim(' ↑/↓ to select, Enter to confirm, Esc to cancel'));
91
- } else if (state.statusMessage) {
92
- lines.push(` ${state.statusMessage}`);
93
- }
81
+ if (state.inputMode === 'customizer') {
82
+ lines.push(renderCustomizerOverlay());
83
+ } else if (state.inputMode === 'filter') {
84
+ lines.push(chalk.cyan(` Filter processes: ${state.inputBuffer}_`));
85
+ } else if (state.inputMode === 'kill') {
86
+ lines.push(chalk.cyan(` Kill PID: ${state.inputBuffer}_ (enter to confirm, esc to cancel)`));
87
+ } else if (state.inputMode === 'signal') {
88
+ const sigIdx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer as typeof state.SIGNAL_OPTIONS[number]);
89
+ const sigList = state.SIGNAL_OPTIONS.map((s, i) => i === sigIdx ? chalk.yellow(s) : chalk.dim(s)).join(' ');
90
+ lines.push(chalk.cyan(` Signal: ${state.inputBuffer}_`));
91
+ lines.push(chalk.dim(` ${sigList}`));
92
+ lines.push(chalk.dim(' ↑/↓ to select, Enter to confirm, Esc to cancel'));
93
+ } else if (state.statusMessage) {
94
+ lines.push(` ${state.statusMessage}`);
95
+ }
94
96
 
95
97
  process.stdout.write('\x1b[2J\x1b[H');
96
98
  process.stdout.write(lines.join('\n'));
@@ -104,27 +106,29 @@ function footerLine(): string {
104
106
  ['g', state.showGraphs ? 'hide graphs' : 'show graphs'],
105
107
  ['b', `graph:${state.graphMode}`],
106
108
  ['d', state.detailed ? 'basic' : 'detailed'],
107
-
108
- ['s', `sort:${state.sortMode}`],
109
- ['/', 'filter'],
110
- ['k', 'kill'],
111
- ['S', 'signal'],
112
- ['t', state.treeView ? 'flat' : 'tree'],
113
- ['e', 'export'],
114
- ['l', state.logging ? 'stop log' : 'log'],
115
- ['f', state.exportFormat],
116
- ['+/-', `${state.interval}s`],
117
- ];
109
+ ['←/→', 'tab'],
110
+ ['1-9', 'panels'],
111
+ ['s', `sort:${state.sortMode}`],
112
+ ['/', 'filter'],
113
+ ['k', 'kill'],
114
+ ['S', 'signal'],
115
+ ['t', state.treeView ? 'flat' : 'tree'],
116
+ ['e', 'export'],
117
+ ['l', state.logging ? 'stop log' : 'log'],
118
+ ['f', state.exportFormat],
119
+ ['+/-', `${state.interval}s`],
120
+ ];
118
121
  const keyStr = keys.map(([k, label]) => `${chalk.hex('#50fa7b').bold(k)} ${chalk.dim(label)}`).join(chalk.dim(' | '));
119
122
 
120
123
 
121
124
  const badges: string[] = [];
122
125
  if (state.paused) badges.push(chalk.yellow.bold('⏸ PAUSED'));
123
126
  if (state.logging) badges.push(chalk.red.bold('● REC'));
127
+ if (state.activePanel !== 'grid') badges.push(chalk.cyan.bold(`◉ ${state.activePanel.toUpperCase()}`));
124
128
  const badgeStr = badges.join(' ');
125
129
 
126
130
  return badgeStr ? `${keyStr} ${badgeStr}` : keyStr;
127
- }
131
+ }
128
132
 
129
133
  function checkAlerts(data: StatsData) {
130
134
  const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;