pyre-cli 5.0.0 → 6.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.
@@ -6,11 +6,28 @@
6
6
  * alert checker. All state variables are imported from
7
7
  * `state.ts`.
8
8
  */
9
+ import { execFile } from 'node:child_process';
9
10
  import chalk from 'chalk';
10
- import { formatTable, formatGraphs, gridColumns, THEMES, type ThemeName, panel } from '../formatters/index.js';
11
+ import { formatTable, formatGraphs, gridColumns, THEMES, type ThemeName, panel, fitVisible } from '../formatters/index.js';
12
+ import { detectAnomalies } from '../anomalies.js';
11
13
  import { state, setStatus, SIGNAL_OPTIONS, getToggleKey } from './state.js';
12
14
  import type { StatsData } from '../monitors/index.js';
13
15
 
16
+ function formatTempForUnit(c: number): string {
17
+ if (state.tempUnit === 'f') {
18
+ const f = c * (9 / 5) + 32;
19
+ return `${f.toFixed(1)}°F`;
20
+ }
21
+ return `${c.toFixed(1)}°C`;
22
+ }
23
+
24
+ function applyTempUnit(text: string): string {
25
+ if (state.tempUnit === 'f') {
26
+ return text.replace(/([\d.]+)°C \/ ([\d.]+)°F/g, '$2°F');
27
+ }
28
+ return text.replace(/([\d.]+)°C \/ ([\d.]+)°F/g, '$1°C');
29
+ }
30
+
14
31
  // --- render caching -----------------------------------------------------------
15
32
 
16
33
  let _cachedTable = '';
@@ -35,6 +52,7 @@ function tableCacheParams(): string {
35
52
  state.currentTheme,
36
53
  state.activePanel,
37
54
  state.treeView ? '1' : '0',
55
+ state.history.version,
38
56
  JSON.stringify(state.visiblePanels),
39
57
  ].join('|');
40
58
  }
@@ -47,29 +65,21 @@ function graphsCacheParams(): string {
47
65
  ].join('|');
48
66
  }
49
67
 
50
- /**
51
- * How many process rows we can realistically fit given
52
- * current terminal height. Computed from the number of
53
- * card columns and the reserved space for the header,
54
- * cards, graphs, and footer.
55
- */
56
- function processRowBudget(): number {
57
- let overlayRows = 0;
58
- if (state.inputMode === 'customizer') overlayRows = state.CUSTOMIZER_OPTIONS.length + 3;
59
- else if (state.inputMode === 'signal') overlayRows = 6;
60
- else if (state.inputMode === 'filter' || state.inputMode === 'kill' || state.inputMode === 'p2p') overlayRows = 3;
61
-
62
- if (state.activePanel === 'process') {
63
- return Math.max(10, state.termHeight - 12 - overlayRows);
64
- }
68
+ function processRowBudget(): number {
69
+ let overlayRows = 0;
70
+ if (state.inputMode === 'customizer') overlayRows = state.CUSTOMIZER_OPTIONS.length + 3;
71
+ else if (state.inputMode === 'signal') overlayRows = 6;
72
+ else if (state.inputMode === 'filter' || state.inputMode === 'kill' || state.inputMode === 'p2p') overlayRows = 3;
65
73
 
66
- const columns = gridColumns(state.termWidth);
67
- const visibleCards = Object.values(state.visiblePanels).filter(v => v !== false).length;
68
- const cardRows = Math.ceil(visibleCards / columns);
69
- const reserved = 4 + cardRows * 8 + 8 + (state.showGraphs ? 11 : 0) + 5 + overlayRows;
70
- return Math.max(5, state.termHeight - reserved);
74
+ if (state.activePanel === 'process') {
75
+ return Math.max(10, state.termHeight - 12 - overlayRows);
71
76
  }
72
77
 
78
+ const reserved = 16 + overlayRows;
79
+ return Math.max(4, state.termHeight - reserved);
80
+ }
81
+
82
+
73
83
  function p2pPanelLines(): { title: string; body: string[] } {
74
84
  const events = state.p2pServer ? state.p2pServer.peerEventHistory : state.p2pEvents;
75
85
  const body: string[] = [];
@@ -103,28 +113,33 @@ function renderCustomizerOverlay(): string {
103
113
  chalk.bgCyan.black.bold(' UI CUSTOMIZER (Use ↑/↓ or J/K to navigate, Enter/Space to toggle, Esc to exit) '),
104
114
  ];
105
115
 
106
- state.CUSTOMIZER_OPTIONS.forEach((opt, idx) => {
107
- const isSelected = idx === state.customizerIndex;
108
- const prefix = isSelected ? chalk.yellow('▶ ') : ' ';
109
-
110
- if (opt === 'Theme') {
111
- lines.push(`${prefix}${opt}: ${chalk.bold.green(state.currentTheme)} [${themesList.join(', ')}]`);
112
- } else if (opt === 'Graph Mode') {
113
- lines.push(`${prefix}${opt}: ${chalk.bold.green(state.graphMode)} [spark, bar]`);
114
- } else if (opt === 'Splash Screen') {
115
- const status = state.splashEnabled ? chalk.green('[ON]') : chalk.red('[OFF]');
116
- lines.push(`${prefix}${opt}: ${status}`);
117
- } else if (opt === 'Splash Color') {
118
- lines.push(`${prefix}${opt}: ${chalk.bold.green(state.splashColorScheme)} [fire, ocean, forest, purple, monochrome]`);
119
- } else if (opt === 'Splash Animation') {
120
- lines.push(`${prefix}${opt}: ${chalk.bold.green(state.splashAnimation)} [classic, wave, sparks]`);
121
- } else {
122
- const toggleKey = getToggleKey(opt);
123
- const isVisible = toggleKey ? (toggleKey === 'tree' ? state.treeView : state.visiblePanels[toggleKey] !== false) : true;
124
- const status = isVisible ? chalk.green('[VISIBLE]') : chalk.red('[HIDDEN]');
125
- lines.push(`${prefix}${opt}: ${status}`);
126
- }
127
- });
116
+ state.CUSTOMIZER_OPTIONS.forEach((opt, idx) => {
117
+ const isSelected = idx === state.customizerIndex;
118
+ const prefix = isSelected ? chalk.yellow('▶ ') : ' ';
119
+
120
+ if (opt === 'Theme') {
121
+ lines.push(`${prefix}${opt}: ${chalk.bold.green(state.currentTheme)} [${themesList.join(', ')}]`);
122
+ } else if (opt === 'Graph Mode') {
123
+ lines.push(`${prefix}${opt}: ${chalk.bold.green(state.graphMode)} [spark, bar]`);
124
+ } else if (opt === 'Splash Screen') {
125
+ const status = state.splashEnabled ? chalk.green('[ON]') : chalk.red('[OFF]');
126
+ lines.push(`${prefix}${opt}: ${status}`);
127
+ } else if (opt === 'Splash Color') {
128
+ lines.push(`${prefix}${opt}: ${chalk.bold.green(state.splashColorScheme)} [fire, ocean, forest, purple, monochrome]`);
129
+ } else if (opt === 'Splash Animation') {
130
+ lines.push(`${prefix}${opt}: ${chalk.bold.green(state.splashAnimation)} [classic, wave, sparks]`);
131
+ } else if (opt === 'Notifications') {
132
+ const status = state.notificationsEnabled ? chalk.green('[ON]') : chalk.red('[OFF]');
133
+ lines.push(`${prefix}${opt}: ${status}`);
134
+ } else if (opt === 'Temperature Unit') {
135
+ lines.push(`${prefix}${opt}: ${chalk.bold.green(state.tempUnit.toUpperCase())} [C, F]`);
136
+ } else {
137
+ const toggleKey = getToggleKey(opt);
138
+ const isVisible = toggleKey ? (toggleKey === 'tree' ? state.treeView : state.visiblePanels[toggleKey] !== false) : true;
139
+ const status = isVisible ? chalk.green('[VISIBLE]') : chalk.red('[HIDDEN]');
140
+ lines.push(`${prefix}${opt}: ${status}`);
141
+ }
142
+ });
128
143
 
129
144
  return lines.map(l => ` ${l}`).join('\n');
130
145
  }
@@ -167,29 +182,48 @@ function writeFrame(lines: string[]) {
167
182
 
168
183
  function render() {
169
184
  if (!state.lastData) return;
185
+ const rows = state.termHeight || 24;
186
+ const cols = state.termWidth || 80;
187
+ if (cols < 50 || rows < 15) {
188
+ const msg = chalk.yellow(`pyre ${rows}x${cols} too small. Resize terminal to >= 50x20.`);
189
+ const centered = Math.max(0, Math.floor((rows - 2) / 2));
190
+ const out: string[] = [];
191
+ for (let i = 0; i < centered; i++) out.push('');
192
+ out.push(msg.padEnd(cols));
193
+ while (out.length < rows) out.push('');
194
+ writeFrame(out);
195
+ return;
196
+ }
170
197
  const lines: string[] = [];
171
198
  const theme = THEMES[state.currentTheme] || THEMES.default;
172
199
 
173
- const params = tableCacheParams();
174
- const tableStr = _tableDataDirty || state.lastData !== _cachedTableData || params !== _cachedTableParams
175
- ? (() => {
176
- const out = formatTable(state.lastData, {
177
- width: state.termWidth,
178
- sortBy: state.sortMode,
179
- filter: state.processFilter || undefined,
180
- processLimit: processRowBudget(),
181
- theme: state.currentTheme,
182
- visible: state.visiblePanels,
183
- treeView: state.treeView,
184
- activePanel: state.activePanel,
185
- });
186
- _cachedTableData = state.lastData;
187
- _cachedTableParams = params;
188
- _cachedTable = out;
189
- _tableDataDirty = false;
190
- return out;
191
- })()
192
- : _cachedTable;
200
+ const params = tableCacheParams();
201
+ const anomalies = detectAnomalies(state.lastData, state.history);
202
+ const anomalyCacheKey = anomalies.map(a => `${a.metric}:${a.zScore.toFixed(2)}`).join(',');
203
+ const cacheParams = params + '|' + anomalyCacheKey;
204
+ const tableStr = _tableDataDirty || state.lastData !== _cachedTableData || cacheParams !== _cachedTableParams
205
+ ? (() => {
206
+ const out = formatTable(state.lastData, {
207
+ width: state.termWidth,
208
+ sortBy: state.sortMode,
209
+ filter: state.processFilter || undefined,
210
+ processLimit: processRowBudget(),
211
+ theme: state.currentTheme,
212
+ visible: state.visiblePanels,
213
+ treeView: state.treeView,
214
+ activePanel: state.activePanel,
215
+ anomalies,
216
+ tempUnit: state.tempUnit,
217
+ history: state.history,
218
+ graphMode: state.graphMode,
219
+ });
220
+ _cachedTableData = state.lastData;
221
+ _cachedTableParams = cacheParams;
222
+ _cachedTable = applyTempUnit(out);
223
+ _tableDataDirty = false;
224
+ return _cachedTable;
225
+ })()
226
+ : _cachedTable;
193
227
 
194
228
  const bodyLines: string[] = [];
195
229
  if (state.activePanel === 'p2p') {
@@ -206,8 +240,8 @@ function render() {
206
240
  const out = formatGraphs(state.history, state.termWidth, state.currentTheme, state.graphMode);
207
241
  _cachedGraphsVersion = state.history.version;
208
242
  _cachedGraphsParams = graphsParams;
209
- _cachedGraphs = out;
210
- return out;
243
+ _cachedGraphs = applyTempUnit(out);
244
+ return _cachedGraphs;
211
245
  })()
212
246
  : _cachedGraphs;
213
247
 
@@ -216,54 +250,57 @@ function render() {
216
250
  bodyLines.push(...graphsStr.split('\n'));
217
251
  }
218
252
 
219
- const footerLines: string[] = [];
220
- footerLines.push('');
221
- footerLines.push(...footerLine().split('\n'));
253
+ const footerLine1 = footerLine()[0] || '';
254
+ const footerLine2 = footerLine()[1] || '';
255
+ const footerLine3 = footerLine()[2] || ' ';
256
+ const footerLines = [footerLine1, footerLine2, footerLine3];
222
257
 
223
258
  if (state.inputMode === 'customizer') {
224
- footerLines.push(...renderCustomizerOverlay().split('\n'));
225
- } else if (state.inputMode === 'filter') {
226
- footerLines.push(chalk.cyan(` Filter processes: ${state.inputBuffer}_`));
227
- } else if (state.inputMode === 'kill') {
228
- footerLines.push(chalk.cyan(` Kill PID: ${state.inputBuffer}_ (enter to confirm, esc to cancel)`));
229
- } else if (state.inputMode === 'signal') {
230
- const sigIdx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer as typeof state.SIGNAL_OPTIONS[number]);
231
- const sigList = state.SIGNAL_OPTIONS.map((s, i) => i === sigIdx ? chalk.yellow(s) : chalk.dim(s)).join(' ');
232
- footerLines.push(chalk.cyan(` Signal: ${state.inputBuffer}_`));
233
- footerLines.push(chalk.dim(` ${sigList}`));
234
- footerLines.push(chalk.dim(' ↑/↓ to select, Enter to confirm, Esc to cancel'));
235
- } else if (state.inputMode === 'p2p') {
236
- footerLines.push(chalk.cyan(` P2P password (${state.p2pPort}): ${state.inputBuffer}_ (enter to start, esc to cancel)`));
237
- } else if (state.statusMessage) {
238
- footerLines.push(` ${state.statusMessage}`);
239
- } else {
240
- footerLines.push('');
241
- }
242
-
243
- const maxBodyLines = Math.max(1, state.termHeight - footerLines.length);
244
- if (bodyLines.length > maxBodyLines) {
245
- bodyLines.length = maxBodyLines;
259
+ bodyLines.push('');
260
+ bodyLines.push(...renderCustomizerOverlay().split('\n'));
261
+ } else if (state.inputMode === 'filter') {
262
+ bodyLines.push(chalk.cyan(` Filter processes: ${state.inputBuffer}_`));
263
+ } else if (state.inputMode === 'kill') {
264
+ bodyLines.push(chalk.cyan(` Kill PID: ${state.inputBuffer}_ (enter to confirm, esc to cancel)`));
265
+ } else if (state.inputMode === 'signal') {
266
+ const sigIdx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer as typeof state.SIGNAL_OPTIONS[number]);
267
+ const sigList = state.SIGNAL_OPTIONS.map((s, i) => i === sigIdx ? chalk.yellow(s) : chalk.dim(s)).join(' ');
268
+ bodyLines.push(chalk.cyan(` Signal: ${state.inputBuffer}_`));
269
+ bodyLines.push(chalk.dim(` ${sigList}`));
270
+ bodyLines.push(chalk.dim(' ↑/↓ to select, Enter to confirm, Esc to cancel'));
271
+ } else if (state.inputMode === 'p2p') {
272
+ bodyLines.push(chalk.cyan(` P2P password (${state.p2pPort}): ${state.inputBuffer}_ (enter to start, esc to cancel)`));
273
+ } else if (state.statusMessage) {
274
+ bodyLines.push(` ${state.statusMessage}`);
275
+ }
276
+
277
+ const footerHeight = 3;
278
+ const targetBodyHeight = Math.max(1, state.termHeight - footerHeight);
279
+ if (bodyLines.length > targetBodyHeight) {
280
+ bodyLines.length = targetBodyHeight;
246
281
  }
247
-
248
- const frameLines = [...bodyLines, ...footerLines];
249
- if (frameLines.length > state.termHeight) {
250
- frameLines.length = state.termHeight;
282
+ const frameLines = [...bodyLines];
283
+ while (frameLines.length < targetBodyHeight) {
284
+ frameLines.push('');
251
285
  }
252
- writeFrame(frameLines);
286
+ frameLines.push(...footerLines);
287
+ const fittedLines = frameLines.map(l => fitVisible(l, cols));
288
+ writeFrame(fittedLines);
253
289
  }
254
290
 
255
- function footerLine(): string {
291
+ function footerLine(): string[] {
256
292
  const row1: [string, string][] = [
257
- ['q', 'quit'],
258
- ['c', 'customize UI'],
259
- ['←/→', 'tab'],
260
- ['1-9', 'panels'],
261
- ['g', state.showGraphs ? 'hide graphs' : 'show graphs'],
262
- ['b', `graph:${state.graphMode}`],
263
- ['t', state.treeView ? 'flat' : 'tree'],
264
- ['d', state.detailed ? 'basic' : 'detailed'],
265
- ['p', state.paused ? 'resume' : 'pause'],
266
- ];
293
+ ['q', 'quit'],
294
+ ['c', 'customize UI'],
295
+ ['←/→', 'tab'],
296
+ ['1-9', 'panels'],
297
+ ['g', state.showGraphs ? 'hide graphs' : 'show graphs'],
298
+ ['b', `graph:${state.graphMode}`],
299
+ ['t', state.treeView ? 'flat' : 'tree'],
300
+ ['d', state.detailed ? 'basic' : 'detailed'],
301
+ ['T', `temp:${state.tempUnit}`],
302
+ ['p', state.paused ? 'resume' : 'pause'],
303
+ ];
267
304
  const row2: [string, string][] = [
268
305
  ['s', `sort:${state.sortMode}`],
269
306
  ['/', 'filter'],
@@ -284,22 +321,42 @@ function footerLine(): string {
284
321
  if (state.logging) badges.push(chalk.red.bold('● REC'));
285
322
  if (state.activePanel !== 'grid') badges.push(chalk.cyan.bold(`◉ ${state.activePanel.toUpperCase()}`));
286
323
  if (state.p2pServerRunning) badges.push(chalk.green.bold(`P2P ${state.p2pBind || '0.0.0.0'}:${state.p2pPort}`));
287
- const badgeStr = badges.join(' ');
324
+ const badgeStr = badges.join(' ') || ' ';
288
325
 
289
- return badgeStr ? `${str1}\n${str2} ${badgeStr}` : `${str1}\n${str2}`;
326
+ return [str1, str2, badgeStr];
290
327
  }
291
328
 
329
+ function sendNotification(title: string, message: string) {
330
+ if (!state.notificationsEnabled) return;
331
+ execFile('osascript', ['-e', `display notification "${message}" with title "${title}"`], (err) => {
332
+ if (err) {
333
+ // Silently ignore notification failures (e.g., terminal not in focus, no notification permission)
334
+ }
335
+ });
336
+ }
337
+
292
338
  function checkAlerts(data: StatsData) {
293
- const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
294
- const hot = data.cpu.usage >= state.CPU_ALERT_PCT || (temp !== null && temp >= state.TEMP_ALERT_C);
295
- if (hot && !state.alerted) {
296
- state.alerted = true;
297
- process.stdout.write('\x07');
298
- const reason = data.cpu.usage >= state.CPU_ALERT_PCT ? `CPU at ${data.cpu.usage}%` : `Temp at ${temp}°C`;
299
- setStatus(chalk.red(`⚠ Alert: ${reason}`), 5000);
300
- } else if (!hot) {
301
- state.alerted = false;
339
+ const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
340
+ const hot = data.cpu.usage >= state.CPU_ALERT_PCT || (temp !== null && temp >= state.TEMP_ALERT_C);
341
+ const anomalies = detectAnomalies(data, state.history);
342
+ const hasCriticalAnomaly = anomalies.some(a => a.severity === 'critical');
343
+ const hasWarningAnomaly = anomalies.some(a => a.severity === 'warning');
344
+ const anomalyTriggered = hasCriticalAnomaly || hasWarningAnomaly;
345
+
346
+ if ((hot || anomalyTriggered) && !state.alerted) {
347
+ state.alerted = true;
348
+ process.stdout.write('\x07');
349
+ const reasons: string[] = [];
350
+ if (data.cpu.usage >= state.CPU_ALERT_PCT) reasons.push(`CPU at ${data.cpu.usage}%`);
351
+ if (temp !== null && temp >= state.TEMP_ALERT_C) reasons.push(`Temp at ${formatTempForUnit(temp)}`);
352
+ for (const a of anomalies) {
353
+ reasons.push(`${a.metric} anomaly (σ=${a.zScore.toFixed(1)})`);
354
+ }
355
+ setStatus(chalk.red(`⚠ Alert: ${reasons.join('; ')}`), 5000);
356
+ sendNotification('pyre Alert', reasons.join('; '));
357
+ } else if (!hot && !anomalyTriggered) {
358
+ state.alerted = false;
359
+ }
302
360
  }
303
- }
304
361
 
305
362
  export { render, footerLine, renderCustomizerOverlay, checkAlerts, processRowBudget, invalidateTableCache, invalidateFrame };
package/src/live/state.ts CHANGED
@@ -11,6 +11,7 @@ import { History } from '../history.js';
11
11
  import type { StatsData } from '../monitors/index.js';
12
12
  import type { ExportFormat, InputMode, SortMode, GraphMode } from './types.js';
13
13
  import type { ThemeName, VisibleItems } from '../formatters/types.js';
14
+ import { readConfig, type PyreConfig } from '../state/config.js';
14
15
 
15
16
  export const SIGNAL_OPTIONS = ['SIGTERM', 'SIGKILL', 'SIGINT', 'SIGHUP', 'SIGSTOP', 'SIGCONT'] as const;
16
17
 
@@ -18,54 +19,61 @@ import { type SplashColorScheme, type SplashAnimation } from '../splash.js';
18
19
 
19
20
  export { type SplashColorScheme, type SplashAnimation };
20
21
 
22
+ const config = readConfig();
23
+
21
24
  const state = {
22
25
  intervalHandle: null as NodeJS.Timeout | null,
23
26
  uiIntervalHandle: null as NodeJS.Timeout | null,
24
27
  running: false,
25
28
  paused: false,
26
- detailed: false,
27
- interval: 2,
28
- showGraphs: true,
29
- graphMode: 'spark' as GraphMode,
29
+ detailed: config.detailed,
30
+ interval: config.interval,
31
+ showGraphs: config.showGraphs,
32
+ graphMode: config.graphMode as GraphMode,
30
33
  exportFormat: 'json' as ExportFormat,
34
+ tempUnit: 'c' as 'c' | 'f',
31
35
 
32
-
33
- exportDir: './pyre-exports',
34
- currentTheme: 'default' as ThemeName,
35
- visiblePanels: {
36
- cpu: true,
37
- mem: true,
38
- gpu: true,
39
- power: true,
40
- battery: true,
41
- thermal: true,
42
- network: true,
43
- packets: true,
44
- tasks: true,
45
- disk: true,
46
- process: true,
47
- } as VisibleItems,
48
- logging: false,
49
- logStream: null as fs.WriteStream | null,
50
- statusMessage: '',
51
- statusTimer: null as NodeJS.Timeout | null,
52
- lastData: null as StatsData | null,
53
- history: new History(40),
54
- keypressHandler: null as ((str: string, key: readline.Key) => void) | null,
55
- termWidth: process.stdout.columns || 80,
56
- termHeight: process.stdout.rows || 24,
57
- sortMode: 'cpu' as SortMode,
58
- processFilter: '',
59
- inputMode: null as InputMode,
60
- inputBuffer: '',
61
- targetPid: '',
62
- selectedSignal: 'SIGTERM' as typeof SIGNAL_OPTIONS[number],
63
- treeView: false,
64
- SIGNAL_OPTIONS,
65
- customizerIndex: 0,
36
+ exportDir: config.exportDir,
37
+ currentTheme: config.theme as ThemeName,
38
+ visiblePanels: {
39
+ cpu: config.visiblePanels.cpu,
40
+ mem: config.visiblePanels.mem,
41
+ gpu: config.visiblePanels.gpu,
42
+ power: config.visiblePanels.power,
43
+ battery: config.visiblePanels.battery,
44
+ thermal: config.visiblePanels.thermal,
45
+ network: config.visiblePanels.network,
46
+ packets: config.visiblePanels.packets,
47
+ tasks: config.visiblePanels.tasks,
48
+ disk: config.visiblePanels.disk,
49
+ process: config.visiblePanels.process,
50
+ } as VisibleItems,
51
+ logging: false,
52
+ logStream: null as fs.WriteStream | null,
53
+ statusMessage: '',
54
+ statusTimer: null as NodeJS.Timeout | null,
55
+ lastData: null as StatsData | null,
56
+ history: new History(40),
57
+ keypressHandler: null as ((str: string, key: readline.Key) => void) | null,
58
+ termWidth: process.stdout.columns || 80,
59
+ termHeight: process.stdout.rows || 24,
60
+ sortMode: config.sortMode as SortMode,
61
+ processFilter: '',
62
+ inputMode: null as InputMode,
63
+ inputBuffer: '',
64
+ targetPid: '',
65
+ selectedSignal: 'SIGTERM' as typeof SIGNAL_OPTIONS[number],
66
+ treeView: config.treeView,
67
+ SIGNAL_OPTIONS,
68
+ customizerIndex: 0,
66
69
  CUSTOMIZER_OPTIONS: [
67
70
  'Theme',
68
71
  'Graph Mode',
72
+ 'Splash Screen',
73
+ 'Splash Color',
74
+ 'Splash Animation',
75
+ 'Notifications',
76
+ 'Temperature Unit',
69
77
  'Toggle CPU',
70
78
  'Toggle Memory',
71
79
  'Toggle GPU',
@@ -78,39 +86,37 @@ const state = {
78
86
  'Toggle Disk',
79
87
  'Toggle Processes',
80
88
  'Toggle Tree View',
81
- 'Splash Screen',
82
- 'Splash Color',
83
- 'Splash Animation',
84
89
  ],
85
- CPU_ALERT_PCT: 90,
86
- TEMP_ALERT_C: 95,
87
- alerted: false,
88
- activePanel: 'grid' as 'grid' | 'cpu' | 'mem' | 'gpu' | 'power' | 'battery' | 'thermal' | 'network' | 'packets' | 'tasks' | 'disk' | 'process' | 'p2p',
89
- mouseEnabled: true,
90
- PANEL_TABS: [
91
- { id: 'cpu', label: 'CPU', key: '1' },
92
- { id: 'mem', label: 'Memory', key: '2' },
93
- { id: 'gpu', label: 'GPU', key: '3' },
94
- { id: 'power', label: 'Power', key: '4' },
95
- { id: 'battery', label: 'Battery', key: '5' },
96
- { id: 'thermal', label: 'Thermal', key: '6' },
97
- { id: 'network', label: 'Network', key: '7' },
98
- { id: 'packets', label: 'Packets', key: '8' },
99
- { id: 'tasks', label: 'Tasks', key: '9' },
100
- { id: 'disk', label: 'Disk', key: '0' },
101
- { id: 'process', label: 'Process', key: 'P' },
102
- { id: 'p2p', label: 'P2P', key: 'R' },
103
- ] as const,
104
- p2pEvents: [] as { type: string; detail: string; ts: number }[],
105
- p2pBind: '',
106
- p2pServer: null as { stop: () => void; peerEventHistory: { type: string; detail: string; ts: number }[] } | null,
107
- p2pServerRunning: false,
108
- p2pPort: 9876,
109
- p2pPassword: 'pyre',
110
- splashEnabled: true,
111
- splashColorScheme: 'fire' as SplashColorScheme,
112
- splashAnimation: 'classic' as SplashAnimation,
113
- };
90
+ CPU_ALERT_PCT: config.cpuAlertPct,
91
+ TEMP_ALERT_C: config.tempAlertC,
92
+ notificationsEnabled: config.notificationsEnabled,
93
+ alerted: false,
94
+ activePanel: 'grid' as 'grid' | 'cpu' | 'mem' | 'gpu' | 'power' | 'battery' | 'thermal' | 'network' | 'packets' | 'tasks' | 'disk' | 'process' | 'p2p',
95
+ mouseEnabled: config.mouseEnabled,
96
+ PANEL_TABS: [
97
+ { id: 'cpu', label: 'CPU', key: '1' },
98
+ { id: 'mem', label: 'Memory', key: '2' },
99
+ { id: 'gpu', label: 'GPU', key: '3' },
100
+ { id: 'power', label: 'Power', key: '4' },
101
+ { id: 'battery', label: 'Battery', key: '5' },
102
+ { id: 'thermal', label: 'Thermal', key: '6' },
103
+ { id: 'network', label: 'Network', key: '7' },
104
+ { id: 'packets', label: 'Packets', key: '8' },
105
+ { id: 'tasks', label: 'Tasks', key: '9' },
106
+ { id: 'disk', label: 'Disk', key: '0' },
107
+ { id: 'process', label: 'Process', key: 'P' },
108
+ { id: 'p2p', label: 'P2P', key: 'R' },
109
+ ] as const,
110
+ p2pEvents: [] as { type: string; detail: string; ts: number }[],
111
+ p2pBind: '',
112
+ p2pServer: null as { stop: () => void; peerEventHistory: { type: string; detail: string; ts: number }[] } | null,
113
+ p2pServerRunning: false,
114
+ p2pPort: config.p2pPort,
115
+ p2pPassword: config.p2pPassword,
116
+ splashEnabled: config.splashEnabled,
117
+ splashColorScheme: config.splashColorScheme as SplashColorScheme,
118
+ splashAnimation: config.splashAnimation as SplashAnimation,
119
+ };
114
120
 
115
121
  function setStatus(msg: string, ms = 3000) {
116
122
  state.statusMessage = msg;
package/src/live/types.ts CHANGED
@@ -5,15 +5,20 @@
5
5
  * export format enum, and the input-mode union used
6
6
  * throughout the interactive session.
7
7
  */
8
+ import type { ThemeName } from '../formatters/themes.js';
9
+
8
10
  export interface LiveOptions {
9
11
  interval: number;
10
12
  detailed?: boolean;
11
13
  exportDir?: string;
12
14
  autoLog?: boolean;
13
15
  theme?: ThemeName;
16
+ alertCpu?: number;
17
+ alertTemp?: number;
18
+ tempUnit?: 'c' | 'f';
14
19
  }
15
20
 
16
- export type ExportFormat = 'json' | 'csv' | 'tsv';
21
+ export type ExportFormat = 'json' | 'csv' | 'tsv' | 'html' | 'md';
17
22
 
18
23
  type InputMode = null | 'filter' | 'kill' | 'signal' | 'customizer' | 'p2p';
19
24
 
@@ -21,4 +26,7 @@ type SortMode = 'cpu' | 'mem' | 'pid' | 'user' | 'command' | 'state' | 'threads'
21
26
 
22
27
  type GraphMode = 'spark' | 'bar';
23
28
 
24
- export type { InputMode, SortMode, GraphMode };
29
+ type ActivePanel = 'grid' | 'cpu' | 'mem' | 'gpu' | 'power' | 'battery' | 'thermal' | 'network' | 'packets' | 'tasks' | 'disk' | 'process' | 'p2p';
30
+
31
+ export type { InputMode, SortMode, GraphMode, ActivePanel, ThemeName };
32
+