pyre-cli 1.3.0 → 2.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.
@@ -0,0 +1,443 @@
1
+ /**
2
+ * Live dashboard input handling and session lifecycle.
3
+ *
4
+ * Contains `startLive`, `stopLive`, the keypress handler,
5
+ * resize handling, and the ticker restart logic.
6
+ *
7
+ * Imported by `index.ts` as the public API of the live module.
8
+ */
9
+ import readline from 'node:readline';
10
+ import chalk from 'chalk';
11
+ import { collectAll, StatsData } from '../monitors/index.js';
12
+ import { THEMES, type ThemeName, type VisibleItems, getTabHitboxes, TAB_BAR_ROW } from '../formatters/index.js';
13
+ import { state, setStatus } from './state.js';
14
+ import { exportSnapshot, startLogging, stopLogging, toggleLogging, writeLogRow } from './export.js';
15
+ import { render, footerLine, checkAlerts, processRowBudget } from './render.js';
16
+ import type { LiveOptions, ExportFormat, InputMode, SortMode, GraphMode } from './types.js';
17
+
18
+ function handleInputModeKey(str: string, key: readline.Key) {
19
+ if (key.name === 'escape') {
20
+ state.inputMode = null;
21
+ state.inputBuffer = '';
22
+ render();
23
+ return;
24
+ }
25
+
26
+ if (state.inputMode === 'customizer') {
27
+ const themesList = Object.keys(THEMES) as ThemeName[];
28
+
29
+ if (key.name === 'up' || str === 'k') {
30
+ state.customizerIndex = (state.customizerIndex - 1 + state.CUSTOMIZER_OPTIONS.length) % state.CUSTOMIZER_OPTIONS.length;
31
+ } else if (key.name === 'down' || str === 'j') {
32
+ state.customizerIndex = (state.customizerIndex + 1) % state.CUSTOMIZER_OPTIONS.length;
33
+ } else if (key.name === 'return' || str === ' ') {
34
+ const selected = state.CUSTOMIZER_OPTIONS[state.customizerIndex];
35
+ if (selected === 'Theme') {
36
+ const nextIdx = (themesList.indexOf(state.currentTheme) + 1) % themesList.length;
37
+ state.currentTheme = themesList[nextIdx];
38
+ } else if (selected === 'Toggle Tree View') {
39
+ state.treeView = !state.treeView;
40
+ } else if (selected === 'Graph Mode') {
41
+ state.graphMode = state.graphMode === 'spark' ? 'bar' : 'spark';
42
+ } else {
43
+
44
+ const itemKey = selected.replace('Toggle ', '').toLowerCase() as keyof VisibleItems;
45
+ state.visiblePanels[itemKey] = !state.visiblePanels[itemKey];
46
+ }
47
+ }
48
+ render();
49
+ return;
50
+ }
51
+
52
+ if (state.inputMode === 'signal') {
53
+ if (key.name === 'up' || str === 'k') {
54
+ const idx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer as typeof state.SIGNAL_OPTIONS[number]);
55
+ state.inputBuffer = state.SIGNAL_OPTIONS[(idx - 1 + state.SIGNAL_OPTIONS.length) % state.SIGNAL_OPTIONS.length];
56
+ } else if (key.name === 'down' || str === 'j') {
57
+ const idx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer as typeof state.SIGNAL_OPTIONS[number]);
58
+ state.inputBuffer = state.SIGNAL_OPTIONS[(idx + 1) % state.SIGNAL_OPTIONS.length];
59
+ } else if (key.name === 'return' || str === ' ') {
60
+ killProcess(state.inputBuffer.trim(), state.inputBuffer.trim() as NodeJS.Signals);
61
+ state.inputMode = null;
62
+ state.inputBuffer = '';
63
+ render();
64
+ return;
65
+ }
66
+ render();
67
+ return;
68
+ }
69
+
70
+ if (key.name === 'return') {
71
+ if (state.inputMode === 'filter') {
72
+ state.processFilter = state.inputBuffer.trim();
73
+ setStatus(state.processFilter ? `Filtering: "${state.processFilter}"` : 'Filter cleared');
74
+ } else if (state.inputMode === 'kill') {
75
+ killProcess(state.inputBuffer.trim());
76
+ }
77
+ state.inputMode = null;
78
+ state.inputBuffer = '';
79
+ render();
80
+ return;
81
+ }
82
+
83
+ if (key.name === 'backspace') {
84
+ state.inputBuffer = state.inputBuffer.slice(0, -1);
85
+ render();
86
+ return;
87
+ }
88
+
89
+ if (str && str.length === 1 && !key.ctrl && !key.meta) {
90
+ state.inputBuffer += str;
91
+ render();
92
+ }
93
+ }
94
+
95
+ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
96
+ const pid = parseInt(pidStr, 10);
97
+ if (!pid || pid <= 0) {
98
+ setStatus(`Invalid PID: ${pidStr}`);
99
+ return;
100
+ }
101
+ try {
102
+ process.kill(pid, signal as NodeJS.Signals);
103
+ setStatus(`Sent ${signal} to PID ${pid}`);
104
+ } catch (err: any) {
105
+ setStatus(`Failed to send ${signal} to ${pid}: ${err.message}`);
106
+ }
107
+ }
108
+
109
+ function onResize() {
110
+ state.termWidth = process.stdout.columns || 80;
111
+ state.termHeight = process.stdout.rows || 24;
112
+ state.history.setMaxLen(Math.max(20, Math.min(200, state.termWidth - 30)));
113
+ render();
114
+ }
115
+
116
+ function restartTicker() {
117
+ if (state.intervalHandle) clearInterval(state.intervalHandle);
118
+ state.intervalHandle = setInterval(tick, state.interval * 1000);
119
+ }
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
+ 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
+ }
148
+
149
+ /**
150
+ * Start the interactive live dashboard.
151
+ *
152
+ * Sets up raw TTY mode, keypress listeners, and the
153
+ * rendering interval. Idempotent — calling while already
154
+ * running is a no-op.
155
+ */
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
+ }
176
+
177
+ await tick();
178
+ restartTicker();
179
+
180
+ if (opts.autoLog) startLogging();
181
+
182
+ readline.emitKeypressEvents(process.stdin);
183
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
184
+ process.stdin.resume();
185
+
186
+ process.stdout.on('resize', onResize);
187
+
188
+ state.keypressHandler = (str: string, key: readline.Key) => {
189
+ if (!key) return;
190
+
191
+ if (key.ctrl && key.name === 'c') {
192
+ stopLive();
193
+ return;
194
+ }
195
+
196
+ if (state.inputMode) {
197
+ handleInputModeKey(str, key);
198
+ return;
199
+ }
200
+
201
+ if (key.mouse) {
202
+ handleMouseClick(key.mouse);
203
+ return;
204
+ }
205
+
206
+ if (key.sequence && key.sequence.startsWith('\x1b[M')) {
207
+ handleNormalMouse(key.sequence);
208
+ return;
209
+ }
210
+
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');
249
+ render();
250
+ return;
251
+ }
252
+ }
253
+
254
+ switch (key.name) {
255
+ case 'q':
256
+ stopLive();
257
+ break;
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}`);
266
+ render();
267
+ break;
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
+ };
359
+
360
+ process.stdin.on('keypress', state.keypressHandler);
361
+ process.once('SIGINT', () => stopLive());
362
+ }
363
+
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
+ }
398
+
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);
428
+
429
+ state.running = false;
430
+ state.logging = false;
431
+
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');
441
+
442
+ process.exit(0);
443
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Live dashboard rendering.
3
+ *
4
+ * Contains the main `render` loop, the footer key-bindings
5
+ * line, the UI customizer overlay, and the thermal/CPU
6
+ * alert checker. All state variables are imported from
7
+ * `state.ts`.
8
+ */
9
+ import chalk from 'chalk';
10
+ import { formatTable, formatGraphs, gridColumns, THEMES, type ThemeName } from '../formatters/index.js';
11
+ import { state, setStatus, SIGNAL_OPTIONS } from './state.js';
12
+ import type { StatsData } from '../monitors/index.js';
13
+
14
+ /**
15
+ * How many process rows we can realistically fit given
16
+ * current terminal height. Computed from the number of
17
+ * card columns and the reserved space for the header,
18
+ * cards, graphs, and footer.
19
+ */
20
+ function processRowBudget(): number {
21
+ const columns = gridColumns(state.termWidth);
22
+ const visibleCards = Object.values(state.visiblePanels).filter(v => v !== false).length;
23
+ const cardRows = Math.ceil(visibleCards / columns);
24
+ const reserved = 2 + cardRows * 8 + 11 + (state.showGraphs ? 9 : 0) + 4;
25
+ return Math.max(3, Math.min(40, state.termHeight - reserved));
26
+ }
27
+
28
+ function renderCustomizerOverlay(): string {
29
+ const themesList = Object.keys(THEMES) as ThemeName[];
30
+ const lines = [
31
+ chalk.bgCyan.black.bold(' UI CUSTOMIZER (Use ↑/↓ or J/K to navigate, Enter/Space to toggle, Esc to exit) '),
32
+ ];
33
+
34
+ state.CUSTOMIZER_OPTIONS.forEach((opt, idx) => {
35
+ const isSelected = idx === state.customizerIndex;
36
+ const prefix = isSelected ? chalk.yellow('▶ ') : ' ';
37
+
38
+ if (opt === 'Theme') {
39
+ lines.push(`${prefix}${opt}: ${chalk.bold.green(state.currentTheme)} [${themesList.join(', ')}]`);
40
+ } else if (opt === 'Graph Mode') {
41
+ lines.push(`${prefix}${opt}: ${chalk.bold.green(state.graphMode)} [spark, bar]`);
42
+ } else if (opt === 'Toggle Tree View') {
43
+
44
+ const status = state.treeView ? chalk.green('[VISIBLE]') : chalk.red('[HIDDEN]');
45
+ lines.push(`${prefix}${opt}: ${status}`);
46
+ } else {
47
+ const key = opt.replace('Toggle ', '').toLowerCase();
48
+ const isVisible = state.visiblePanels[key as keyof typeof state.visiblePanels] !== false;
49
+ const status = isVisible ? chalk.green('[VISIBLE]') : chalk.red('[HIDDEN]');
50
+ lines.push(`${prefix}${opt}: ${status}`);
51
+ }
52
+ });
53
+
54
+ return lines.map(l => ` ${l}`).join('\n');
55
+ }
56
+
57
+ function render() {
58
+ if (!state.lastData) return;
59
+ const lines: string[] = [];
60
+ lines.push(
61
+ formatTable(state.lastData, {
62
+ width: state.termWidth,
63
+ sortBy: state.sortMode,
64
+ filter: state.processFilter || undefined,
65
+ processLimit: processRowBudget(),
66
+ theme: state.currentTheme,
67
+ visible: state.visiblePanels,
68
+ treeView: state.treeView,
69
+ activePanel: state.activePanel,
70
+ })
71
+ );
72
+
73
+ if (state.showGraphs) {
74
+ lines.push('');
75
+ lines.push(formatGraphs(state.history, state.termWidth, state.currentTheme, state.graphMode));
76
+ }
77
+
78
+ lines.push('');
79
+ lines.push(footerLine());
80
+
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
+ }
96
+
97
+ process.stdout.write('\x1b[2J\x1b[H');
98
+ process.stdout.write(lines.join('\n'));
99
+ }
100
+
101
+ function footerLine(): string {
102
+ const keys: [string, string][] = [
103
+ ['q', 'quit'],
104
+ ['c', 'customize UI'],
105
+ ['p', state.paused ? 'resume' : 'pause'],
106
+ ['g', state.showGraphs ? 'hide graphs' : 'show graphs'],
107
+ ['b', `graph:${state.graphMode}`],
108
+ ['d', state.detailed ? 'basic' : 'detailed'],
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
+ ];
121
+ const keyStr = keys.map(([k, label]) => `${chalk.hex('#50fa7b').bold(k)} ${chalk.dim(label)}`).join(chalk.dim(' | '));
122
+
123
+
124
+ const badges: string[] = [];
125
+ if (state.paused) badges.push(chalk.yellow.bold('⏸ PAUSED'));
126
+ if (state.logging) badges.push(chalk.red.bold('● REC'));
127
+ if (state.activePanel !== 'grid') badges.push(chalk.cyan.bold(`◉ ${state.activePanel.toUpperCase()}`));
128
+ const badgeStr = badges.join(' ');
129
+
130
+ return badgeStr ? `${keyStr} ${badgeStr}` : keyStr;
131
+ }
132
+
133
+ function checkAlerts(data: StatsData) {
134
+ const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
135
+ const hot = data.cpu.usage >= state.CPU_ALERT_PCT || (temp !== null && temp >= state.TEMP_ALERT_C);
136
+ if (hot && !state.alerted) {
137
+ state.alerted = true;
138
+ process.stdout.write('\x07');
139
+ const reason = data.cpu.usage >= state.CPU_ALERT_PCT ? `CPU at ${data.cpu.usage}%` : `Temp at ${temp}°C`;
140
+ setStatus(chalk.red(`⚠ Alert: ${reason}`), 5000);
141
+ } else if (!hot) {
142
+ state.alerted = false;
143
+ }
144
+ }
145
+
146
+ export { render, footerLine, renderCustomizerOverlay, checkAlerts, processRowBudget };
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Live dashboard session state.
3
+ *
4
+ * All mutable state for the interactive live mode lives
5
+ * in this module so that render, input, and export
6
+ * sub-modules can share it without circular dependencies.
7
+ */
8
+ import fs from 'node:fs';
9
+ import readline from 'node:readline';
10
+ import { History } from '../history.js';
11
+ import type { StatsData } from '../monitors/index.js';
12
+ import type { ExportFormat, InputMode, SortMode, GraphMode } from './types.js';
13
+ import type { ThemeName, VisibleItems } from '../formatters/types.js';
14
+
15
+ export const SIGNAL_OPTIONS = ['SIGTERM', 'SIGKILL', 'SIGINT', 'SIGHUP', 'SIGSTOP', 'SIGCONT'] as const;
16
+
17
+ const state = {
18
+ intervalHandle: null as NodeJS.Timeout | null,
19
+ running: false,
20
+ paused: false,
21
+ detailed: false,
22
+ interval: 2,
23
+ showGraphs: true,
24
+ graphMode: 'spark' as GraphMode,
25
+ exportFormat: 'json' as ExportFormat,
26
+
27
+
28
+ exportDir: './pyre-exports',
29
+ currentTheme: 'default' as ThemeName,
30
+ visiblePanels: {
31
+ cpu: true,
32
+ mem: true,
33
+ gpu: true,
34
+ power: true,
35
+ battery: true,
36
+ thermal: true,
37
+ network: true,
38
+ packets: true,
39
+ tasks: true,
40
+ disk: true,
41
+ process: true,
42
+ } as VisibleItems,
43
+ logging: false,
44
+ logStream: null as fs.WriteStream | null,
45
+ statusMessage: '',
46
+ statusTimer: null as NodeJS.Timeout | null,
47
+ lastData: null as StatsData | null,
48
+ history: new History(40),
49
+ keypressHandler: null as ((str: string, key: readline.Key) => void) | null,
50
+ termWidth: process.stdout.columns || 80,
51
+ termHeight: process.stdout.rows || 24,
52
+ sortMode: 'cpu' as SortMode,
53
+ processFilter: '',
54
+ inputMode: null as InputMode,
55
+ inputBuffer: '',
56
+ treeView: false,
57
+ SIGNAL_OPTIONS,
58
+ customizerIndex: 0,
59
+ CUSTOMIZER_OPTIONS: [
60
+ 'Theme',
61
+ 'Graph Mode',
62
+ 'Toggle CPU',
63
+ 'Toggle Memory',
64
+ 'Toggle GPU',
65
+ 'Toggle Power',
66
+ 'Toggle Battery',
67
+ 'Toggle Thermal',
68
+ 'Toggle Network',
69
+ 'Toggle Packets',
70
+ 'Toggle Tasks',
71
+ 'Toggle Disk',
72
+ 'Toggle Processes',
73
+ 'Toggle Tree View',
74
+ ],
75
+ CPU_ALERT_PCT: 90,
76
+ TEMP_ALERT_C: 95,
77
+ alerted: false,
78
+ activePanel: 'grid' as 'grid' | 'cpu' | 'mem' | 'gpu' | 'power' | 'battery' | 'thermal' | 'network' | 'packets' | 'tasks' | 'disk' | 'process',
79
+ mouseEnabled: true,
80
+ PANEL_TABS: [
81
+ { id: 'cpu', label: 'CPU', key: '1' },
82
+ { id: 'mem', label: 'Memory', key: '2' },
83
+ { id: 'gpu', label: 'GPU', key: '3' },
84
+ { id: 'power', label: 'Power', key: '4' },
85
+ { id: 'battery', label: 'Battery', key: '5' },
86
+ { id: 'thermal', label: 'Thermal', key: '6' },
87
+ { id: 'network', label: 'Network', key: '7' },
88
+ { id: 'packets', label: 'Packets', key: '8' },
89
+ { id: 'tasks', label: 'Tasks', key: '9' },
90
+ { id: 'disk', label: 'Disk', key: '0' },
91
+ { id: 'process', label: 'Process', key: 'P' },
92
+ ] as const,
93
+ };
94
+
95
+ function setStatus(msg: string, ms = 3000) {
96
+ state.statusMessage = msg;
97
+ if (state.statusTimer) clearTimeout(state.statusTimer);
98
+ state.statusTimer = setTimeout(() => {
99
+ state.statusMessage = '';
100
+ }, ms);
101
+ }
102
+
103
+ export { state, setStatus };