pyre-cli 3.3.0 → 3.5.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
@@ -10,14 +10,27 @@ import readline from 'node:readline';
10
10
  import chalk from 'chalk';
11
11
  import { collectAll, StatsData } from '../monitors/index.js';
12
12
  import { THEMES, type ThemeName, type VisibleItems, getTabHitboxes, TAB_BAR_ROW } from '../formatters/index.js';
13
- import { state, setStatus } from './state.js';
13
+ import { state, setStatus, getToggleKey } from './state.js';
14
14
  import { exportSnapshot, startLogging, stopLogging, toggleLogging, writeLogRow } from './export.js';
15
- import { render, footerLine, checkAlerts, processRowBudget } from './render.js';
15
+ import { render, footerLine, checkAlerts, invalidateTableCache, invalidateFrame } from './render.js';
16
16
  import type { LiveOptions, ExportFormat, InputMode, SortMode, GraphMode } from './types.js';
17
17
  import { startP2PServer } from '../p2p/index.js';
18
18
 
19
+ function isUpKey(key: readline.Key, str?: string): boolean {
20
+ return key.name === 'up' || str === 'k' || key.sequence === '\x1b[A' || key.sequence === '\x1bOA';
21
+ }
22
+ function isDownKey(key: readline.Key, str?: string): boolean {
23
+ return key.name === 'down' || str === 'j' || key.sequence === '\x1b[B' || key.sequence === '\x1bOB';
24
+ }
25
+ function isEnterKey(key: readline.Key, str?: string): boolean {
26
+ return key.name === 'return' || key.name === 'enter' || key.sequence === '\r' || key.sequence === '\n' || str === '\r' || str === '\n';
27
+ }
28
+ function isEscKey(key: readline.Key, str?: string): boolean {
29
+ return key.name === 'escape' || key.name === 'esc' || key.sequence === '\x1b' || str === '\x1b';
30
+ }
31
+
19
32
  function handleInputModeKey(str: string, key: readline.Key) {
20
- if (key.name === 'escape') {
33
+ if (isEscKey(key, str)) {
21
34
  state.inputMode = null;
22
35
  state.inputBuffer = '';
23
36
  render();
@@ -27,40 +40,56 @@ function handleInputModeKey(str: string, key: readline.Key) {
27
40
  if (state.inputMode === 'customizer') {
28
41
  const themesList = Object.keys(THEMES) as ThemeName[];
29
42
 
30
- if (key.name === 'up' || str === 'k') {
43
+ if (isUpKey(key, str)) {
31
44
  state.customizerIndex = (state.customizerIndex - 1 + state.CUSTOMIZER_OPTIONS.length) % state.CUSTOMIZER_OPTIONS.length;
32
- } else if (key.name === 'down' || str === 'j') {
45
+ } else if (isDownKey(key, str)) {
33
46
  state.customizerIndex = (state.customizerIndex + 1) % state.CUSTOMIZER_OPTIONS.length;
34
- } else if (key.name === 'return' || str === ' ') {
47
+ } else if (isEnterKey(key, str) || str === ' ') {
35
48
  const selected = state.CUSTOMIZER_OPTIONS[state.customizerIndex];
36
49
  if (selected === 'Theme') {
37
50
  const nextIdx = (themesList.indexOf(state.currentTheme) + 1) % themesList.length;
38
51
  state.currentTheme = themesList[nextIdx];
39
- } else if (selected === 'Toggle Tree View') {
40
- state.treeView = !state.treeView;
41
- } else if (selected === 'Graph Mode') {
42
- state.graphMode = state.graphMode === 'spark' ? 'bar' : 'spark';
43
- } else {
44
-
45
- const itemKey = selected.replace('Toggle ', '').toLowerCase() as keyof VisibleItems;
46
- state.visiblePanels[itemKey] = !state.visiblePanels[itemKey];
52
+ } else if (selected === 'Graph Mode') {
53
+ state.graphMode = state.graphMode === 'spark' ? 'bar' : 'spark';
54
+ } else {
55
+ const toggleKey = getToggleKey(selected);
56
+ if (toggleKey) {
57
+ if (toggleKey === 'tree') {
58
+ state.treeView = !state.treeView;
59
+ state.visiblePanels.tree = state.treeView;
60
+ } else {
61
+ const isVisible = state.visiblePanels[toggleKey] !== false;
62
+ state.visiblePanels[toggleKey] = !isVisible;
63
+ }
64
+ }
47
65
  }
66
+ invalidateTableCache();
67
+ invalidateFrame();
48
68
  }
49
69
  render();
50
70
  return;
51
71
  }
52
72
 
53
73
  if (state.inputMode === 'signal') {
54
- if (key.name === 'up' || str === 'k') {
74
+ if (isUpKey(key, str)) {
55
75
  const idx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer as typeof state.SIGNAL_OPTIONS[number]);
56
76
  state.inputBuffer = state.SIGNAL_OPTIONS[(idx - 1 + state.SIGNAL_OPTIONS.length) % state.SIGNAL_OPTIONS.length];
57
- } else if (key.name === 'down' || str === 'j') {
77
+ state.selectedSignal = state.inputBuffer as any;
78
+ } else if (isDownKey(key, str)) {
58
79
  const idx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer as typeof state.SIGNAL_OPTIONS[number]);
59
80
  state.inputBuffer = state.SIGNAL_OPTIONS[(idx + 1) % state.SIGNAL_OPTIONS.length];
60
- } else if (key.name === 'return' || str === ' ') {
61
- killProcess(state.inputBuffer.trim(), state.inputBuffer.trim() as NodeJS.Signals);
62
- state.inputMode = null;
63
- state.inputBuffer = '';
81
+ state.selectedSignal = state.inputBuffer as any;
82
+ } else if (isEnterKey(key, str) || str === ' ') {
83
+ state.selectedSignal = state.inputBuffer.trim() as any;
84
+ if (state.targetPid) {
85
+ killProcess(state.targetPid, state.selectedSignal);
86
+ state.inputMode = null;
87
+ state.inputBuffer = '';
88
+ } else {
89
+ state.inputMode = 'kill';
90
+ state.inputBuffer = '';
91
+ setStatus(`Signal ${state.selectedSignal} selected. Enter target PID to kill:`, 5000);
92
+ }
64
93
  render();
65
94
  return;
66
95
  }
@@ -69,13 +98,13 @@ function handleInputModeKey(str: string, key: readline.Key) {
69
98
  }
70
99
 
71
100
  if (state.inputMode === 'p2p') {
72
- if (key.name === 'escape') {
101
+ if (isEscKey(key, str)) {
73
102
  state.inputMode = null;
74
103
  state.inputBuffer = '';
75
104
  render();
76
105
  return;
77
106
  }
78
- if (key.name === 'return') {
107
+ if (isEnterKey(key, str)) {
79
108
  const password = state.inputBuffer.trim();
80
109
  if (password) {
81
110
  state.p2pPassword = password;
@@ -98,12 +127,14 @@ function handleInputModeKey(str: string, key: readline.Key) {
98
127
  return;
99
128
  }
100
129
 
101
- if (key.name === 'return') {
130
+ if (isEnterKey(key, str)) {
102
131
  if (state.inputMode === 'filter') {
103
132
  state.processFilter = state.inputBuffer.trim();
104
133
  setStatus(state.processFilter ? `Filtering: "${state.processFilter}"` : 'Filter cleared');
105
134
  } else if (state.inputMode === 'kill') {
106
- killProcess(state.inputBuffer.trim());
135
+ const pidStr = state.inputBuffer.trim();
136
+ state.targetPid = pidStr;
137
+ killProcess(pidStr, state.selectedSignal);
107
138
  }
108
139
  state.inputMode = null;
109
140
  state.inputBuffer = '';
@@ -191,6 +222,8 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
191
222
  state.termWidth = process.stdout.columns || 80;
192
223
  state.termHeight = process.stdout.rows || 24;
193
224
  state.history.setMaxLen(Math.max(20, Math.min(200, state.termWidth - 30)));
225
+ invalidateFrame();
226
+ process.stdout.write('\x1b[2J\x1b[H');
194
227
  render();
195
228
  }
196
229
 
@@ -201,8 +234,9 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
201
234
 
202
235
  async function doWarmup() {
203
236
  try {
204
- const data = await collectAll({ detailed: state.detailed, processLimit: processRowBudget() });
237
+ const data = await collectAll({ detailed: state.detailed });
205
238
  state.lastData = data;
239
+ invalidateTableCache();
206
240
 
207
241
  const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
208
242
  state.history.push({
@@ -229,8 +263,9 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
229
263
  async function tick() {
230
264
  if (state.paused) return;
231
265
  try {
232
- const data = await collectAll({ detailed: state.detailed, processLimit: processRowBudget() });
266
+ const data = await collectAll({ detailed: state.detailed });
233
267
  state.lastData = data;
268
+ invalidateTableCache();
234
269
 
235
270
  const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
236
271
  state.history.push({
@@ -267,6 +302,7 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
267
302
  state.running = true;
268
303
  state.paused = false;
269
304
  state.detailed = !!opts.detailed;
305
+ if (opts.theme) state.currentTheme = opts.theme;
270
306
  state.interval = opts.interval;
271
307
  if (opts.exportDir) state.exportDir = opts.exportDir;
272
308
  state.termWidth = process.stdout.columns || 80;
@@ -302,6 +338,11 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
302
338
 
303
339
  process.stdout.on('resize', onResize);
304
340
 
341
+ if (state.uiIntervalHandle) clearInterval(state.uiIntervalHandle);
342
+ state.uiIntervalHandle = setInterval(() => {
343
+ if (state.running && !state.paused) render();
344
+ }, 250);
345
+
305
346
  state.keypressHandler = (str: string, key: readline.Key) => {
306
347
  if (!key) return;
307
348
 
@@ -383,28 +424,12 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
383
424
  case 'q':
384
425
  stopLive();
385
426
  break;
386
- case 'left': {
387
- const currentIdx = state.PANEL_TABS.findIndex(t => t.id === state.activePanel);
388
- if (currentIdx > 0) {
389
- state.activePanel = state.PANEL_TABS[currentIdx - 1].id;
390
- } else if (currentIdx === -1) {
391
- state.activePanel = state.PANEL_TABS[state.PANEL_TABS.length - 1].id;
392
- }
393
- setStatus(`Panel: ${state.activePanel}`);
394
- render();
427
+ case 'left':
428
+ cycleTab(-1);
395
429
  break;
396
- }
397
- case 'right': {
398
- const currentIdx = state.PANEL_TABS.findIndex(t => t.id === state.activePanel);
399
- if (currentIdx < state.PANEL_TABS.length - 1) {
400
- state.activePanel = state.PANEL_TABS[currentIdx + 1].id;
401
- } else if (currentIdx === -1) {
402
- state.activePanel = state.PANEL_TABS[0].id;
403
- }
404
- setStatus(`Panel: ${state.activePanel}`);
405
- render();
430
+ case 'right':
431
+ cycleTab(1);
406
432
  break;
407
- }
408
433
  case 'c':
409
434
  state.inputMode = 'customizer';
410
435
  state.customizerIndex = 0;
@@ -426,9 +451,9 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
426
451
  break;
427
452
  case 'd':
428
453
 
429
- state.detailed = !state.detailed;
430
- setStatus(`Detailed sensor mode: ${state.detailed ? 'on' : 'off'}`);
431
- break;
454
+ state.detailed = !state.detailed;
455
+ setStatus(`Detailed sensor mode: ${state.detailed ? 'on' : 'off'}`);
456
+ break;
432
457
  case 's':
433
458
  const sortCycle: SortMode[] = ['cpu', 'mem', 'pid', 'user', 'command', 'state', 'threads', 'runtime'];
434
459
  const curIdx = sortCycle.indexOf(state.sortMode);
@@ -467,12 +492,6 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
467
492
  case 'tab':
468
493
  cycleTab(1);
469
494
  break;
470
- case 'left':
471
- cycleTab(-1);
472
- break;
473
- case 'right':
474
- cycleTab(1);
475
- break;
476
495
  default:
477
496
  if (str && str.length === 1) {
478
497
  const tabId = tabKeyToId(str);
@@ -517,8 +536,8 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
517
536
  const match = seq.match(/^\x1b\[<(\d+);(\d+);(\d+)([Mm])/);
518
537
  if (!match) return;
519
538
  const button = parseInt(match[1]);
520
- const cx = parseInt(match[2]);
521
- const cy = parseInt(match[3]);
539
+ const cx = parseInt(match[2]) - 1;
540
+ const cy = parseInt(match[3]) - 1;
522
541
  if (button === 0 || button === 1) {
523
542
  handleMouseClick(cy, cx);
524
543
  }
@@ -540,6 +559,10 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
540
559
  clearInterval(state.intervalHandle);
541
560
  state.intervalHandle = null;
542
561
  }
562
+ if (state.uiIntervalHandle) {
563
+ clearInterval(state.uiIntervalHandle);
564
+ state.uiIntervalHandle = null;
565
+ }
543
566
  if (state.statusTimer) {
544
567
  clearTimeout(state.statusTimer);
545
568
  state.statusTimer = null;
@@ -0,0 +1,282 @@
1
+ # src/live/ — Live Dashboard Module
2
+
3
+ The live module provides the interactive dashboard mode for pyre. It handles real-time data collection, keyboard/mouse input, screen rendering, snapshot export, and continuous CSV logging.
4
+
5
+ ## Files
6
+
7
+ | File | Purpose |
8
+ |------|---------|
9
+ | `index.ts` | Barrel export — re-exports `startLive`, `stopLive`, and `LiveOptions` type |
10
+ | `types.ts` | Type definitions for `LiveOptions`, `ExportFormat`, `InputMode`, `SortMode`, `GraphMode` |
11
+ | `state.ts` | Shared mutable session state consumed by all sub-modules |
12
+ | `render.ts` | Screen rendering: main `render` loop, footer key-bindings, UI customizer overlay, thermal/CPU alert checker |
13
+ | `input.ts` | Keypress handling, mouse click handling, session lifecycle (`startLive`, `stopLive`), ticker management |
14
+ | `export.ts` | Snapshot export (JSON/CSV/TSV), continuous CSV logging, file-system utilities |
15
+
16
+ ## External Dependencies
17
+
18
+ - **`src/monitors/index.ts`** — `collectAll()` and `StatsData` for data collection each tick.
19
+ - **`src/formatters/index.ts`** — `formatTable`, `formatGraphs`, `gridColumns`, `THEMES`, `getTabHitboxes`, `TAB_BAR_ROW`, `panel` for rendering.
20
+ - **`src/formatters/types.ts`** — `ThemeName`, `VisibleItems` for type safety.
21
+ - **`src/history.ts`** — `History` class for rolling-window metric storage used by graph rendering.
22
+ - **`src/p2p/index.ts`** — `startP2PServer` for P2P server lifecycle from the TUI.
23
+
24
+ ## `index.ts` — Barrel Exports
25
+
26
+ Re-exports the public API of the live module:
27
+
28
+ - `startLive(opts, splashPromise?)` — starts the interactive dashboard
29
+ - `stopLive()` — tears down the live session and restores the terminal
30
+ - `LiveOptions` type — options passed to `startLive`
31
+
32
+ ## `types.ts` — Type Definitions
33
+
34
+ ### `LiveOptions`
35
+ Options passed to `startLive`:
36
+ - `interval: number` — refresh interval in seconds
37
+ - `detailed?: boolean` — include detailed sensor readings
38
+ - `exportDir?: string` — directory for snapshot exports and logs
39
+ - `autoLog?: boolean` — start continuous CSV logging immediately
40
+ - `theme?: ThemeName` — default visual theme
41
+
42
+ ### `ExportFormat`
43
+ Union type: `'json' | 'csv' | 'tsv'` — controls snapshot export format.
44
+
45
+ ### `InputMode`
46
+ Union type: `null | 'filter' | 'kill' | 'signal' | 'customizer' | 'p2p'` — tracks the current interactive input sub-mode.
47
+
48
+ ### `SortMode`
49
+ Union type: `'cpu' | 'mem' | 'pid' | 'user' | 'command' | 'state' | 'threads' | 'runtime'` — process sort key.
50
+
51
+ ### `GraphMode`
52
+ Union type: `'spark' | 'bar'` — graph rendering style.
53
+
54
+ ## `state.ts` — Shared Session State
55
+
56
+ All mutable state for the live dashboard is defined here so that `render`, `input`, and `export` sub-modules can share it without circular dependencies.
57
+
58
+ ### State Fields
59
+
60
+ | Field | Type | Default | Description |
61
+ |-------|------|---------|-------------|
62
+ | `intervalHandle` | `NodeJS.Timeout \| null` | `null` | Active ticker interval |
63
+ | `running` | `boolean` | `false` | Whether the live session is active |
64
+ | `paused` | `boolean` | `false` | Whether data collection is paused |
65
+ | `detailed` | `boolean` | `false` | Detailed sensor mode |
66
+ | `interval` | `number` | `2` | Refresh interval in seconds |
67
+ | `showGraphs` | `boolean` | `true` | Whether graphs are displayed |
68
+ | `graphMode` | `GraphMode` | `'spark'` | Graph rendering style |
69
+ | `exportFormat` | `ExportFormat` | `'json'` | Snapshot export format |
70
+ | `exportDir` | `string` | `'./pyre-exports'` | Directory for exports/logs |
71
+ | `currentTheme` | `ThemeName` | `'default'` | Active visual theme |
72
+ | `visiblePanels` | `VisibleItems` | All `true` | Which dashboard cards are shown |
73
+ | `logging` | `boolean` | `false` | Whether continuous CSV logging is active |
74
+ | `logStream` | `fs.WriteStream \| null` | `null` | Write stream for CSV log |
75
+ | `statusMessage` | `string` | `''` | Transient status message |
76
+ | `statusTimer` | `NodeJS.Timeout \| null` | `null` | Timer for auto-clearing status |
77
+ | `lastData` | `StatsData \| null` | `null` | Most recently collected data |
78
+ | `history` | `History` | `new History(40)` | Rolling window of metric samples |
79
+ | `keypressHandler` | `Function \| null` | `null` | Registered keypress listener |
80
+ | `termWidth` | `number` | `process.stdout.columns \|\| 80` | Current terminal width |
81
+ | `termHeight` | `number` | `process.stdout.rows \|\| 24` | Current terminal height |
82
+ | `sortMode` | `SortMode` | `'cpu'` | Active process sort key |
83
+ | `processFilter` | `string` | `''` | Active process filter string |
84
+ | `inputMode` | `InputMode` | `null` | Current input sub-mode |
85
+ | `inputBuffer` | `string` | `''` | Buffer for active text input |
86
+ | `treeView` | `boolean` | `false` | Process tree view toggle |
87
+ | `customizerIndex` | `number` | `0` | Selected index in UI customizer |
88
+ | `CUSTOMIZER_OPTIONS` | `string[]` | (see below) | Ordered list of customizer items |
89
+ | `CPU_ALERT_PCT` | `number` | `90` | CPU usage threshold for alerts |
90
+ | `TEMP_ALERT_C` | `number` | `95` | Temperature threshold for alerts |
91
+ | `alerted` | `boolean` | `false` | Whether an alert has been fired |
92
+ | `activePanel` | `string` | `'grid'` | Currently focused panel |
93
+ | `mouseEnabled` | `boolean` | `true` | Whether mouse input is enabled |
94
+ | `PANEL_TABS` | `readonly array` | (see below) | Ordered list of panel tab definitions |
95
+ | `p2pEvents` | `array` | `[]` | P2P peer event history |
96
+ | `p2pBind` | `string` | `''` | P2P bind address |
97
+ | `p2pServer` | `object \| null` | `null` | P2P server instance |
98
+ | `p2pServerRunning` | `boolean` | `false` | Whether P2P server is running |
99
+ | `p2pPort` | `number` | `9876` | P2P port |
100
+ | `p2pPassword` | `string` | `'pyre'` | P2P password |
101
+
102
+ ### `PANEL_TABS`
103
+ Ordered array of 12 panel definitions: `cpu`, `mem`, `gpu`, `power`, `battery`, `thermal`, `network`, `packets`, `tasks`, `disk`, `process`, `p2p`. Each has `{ id, label, key }`.
104
+
105
+ ### `CUSTOMIZER_OPTIONS`
106
+ Ordered list of 14 items: `Theme`, `Graph Mode`, `Toggle CPU`, `Toggle Memory`, `Toggle GPU`, `Toggle Power`, `Toggle Battery`, `Toggle Thermal`, `Toggle Network`, `Toggle Packets`, `Toggle Tasks`, `Toggle Disk`, `Toggle Processes`, `Toggle Tree View`.
107
+
108
+ ### `SIGNAL_OPTIONS`
109
+ `readonly ['SIGTERM', 'SIGKILL', 'SIGINT', 'SIGHUP', 'SIGSTOP', 'SIGCONT']` — available signals for the kill signal picker.
110
+
111
+ ### `setStatus(msg, ms?)`
112
+ Sets a transient status message that auto-clears after `ms` milliseconds (default 3000).
113
+
114
+ ## `render.ts` — Screen Rendering
115
+
116
+ ### `processRowBudget(): number`
117
+ Computes how many process rows can fit in the current terminal layout. Accounts for card columns, header, cards, graphs, and footer spacing. Returns a value clamped to 3–40.
118
+
119
+ ### `p2pPanelLines(): { title: string; body: string[] }`
120
+ Renders the P2P status panel showing server state, bind address, password, and the 20 most recent peer events colour-coded by type (`auth`=green, `disconnect`=yellow, `error`/`blocked`/`rate_limited`=red, others=cyan).
121
+
122
+ ### `renderCustomizerOverlay(): string`
123
+ Renders the UI customizer overlay showing all `CUSTOMIZER_OPTIONS` with the currently selected item marked by `▶`. Each item shows its current value/state (theme name, graph mode, visibility toggles).
124
+
125
+ ### `render()`
126
+ The main render function. Called on every tick and after every input event.
127
+ 1. If `activePanel === 'p2p'`, renders the P2P panel.
128
+ 2. Otherwise, calls `formatTable()` with current state options (width, sort, filter, processLimit, theme, visible panels, treeView, activePanel).
129
+ 3. If `showGraphs` and not in P2P panel, appends `formatGraphs()` output.
130
+ 4. Appends the footer line.
131
+ 5. If an `inputMode` is active, appends the appropriate input prompt (customizer, filter, kill, signal, p2p password).
132
+ 6. If a `statusMessage` exists, appends it.
133
+ 7. Writes the full output to stdout using ANSI escape sequences to clear and redraw the screen.
134
+
135
+ ### `footerLine(): string`
136
+ Renders the bottom footer showing all key bindings with their current state (e.g. `p` shows "resume" or "pause" depending on `state.paused`). Includes badges for paused, recording, active panel, and P2P status.
137
+
138
+ ### `checkAlerts(data: StatsData)`
139
+ Checks CPU usage and temperature against alert thresholds (`CPU_ALERT_PCT` = 90%, `TEMP_ALERT_C` = 95°C). On first trigger, sounds the terminal bell (`\x07`) and shows a red status message for 5 seconds. Resets the alert flag when conditions clear.
140
+
141
+ ### Exported Functions
142
+ `render`, `footerLine`, `renderCustomizerOverlay`, `checkAlerts`, `processRowBudget`
143
+
144
+ ## `input.ts` — Input Handling & Session Lifecycle
145
+
146
+ ### `startLive(opts: LiveOptions, splashPromise?: Promise<void>)`
147
+ Starts the interactive live dashboard. Idempotent — calling while already running is a no-op.
148
+
149
+ 1. Sets `state.running = true` and resets paused/detailed/theme/interval/exportDir from options.
150
+ 2. Resets terminal dimensions and history max length.
151
+ 3. Runs a warmup tick (`doWarmup()`) to collect initial data.
152
+ 4. If a splash promise is provided, awaits it.
153
+ 5. Enters alternate screen mode (`\x1b[?1049h`), hides cursor (`\x1b[?25l`), clears screen.
154
+ 6. Enables mouse tracking if `state.mouseEnabled`.
155
+ 7. Awaits warmup, then does an initial `render()`.
156
+ 8. Starts the ticker interval (`restartTicker()`).
157
+ 9. Starts logging if `opts.autoLog`.
158
+ 10. Sets up raw TTY mode, keypress listener, resize listener, and SIGINT handler.
159
+
160
+ ### `stopLive()`
161
+ Tears down the live session:
162
+ 1. Clears the ticker interval and status timer.
163
+ 2. Ends the log stream if active.
164
+ 3. Stops the P2P server if running.
165
+ 4. Removes the keypress listener.
166
+ 5. Removes the resize listener.
167
+ 6. Sets `state.running = false` and `state.logging = false`.
168
+ 7. Restores terminal to normal mode (raw mode off, cursor shown, exit alternate screen).
169
+ 8. Clears the screen and exits the process with code 0.
170
+
171
+ ### Keypress Handler
172
+
173
+ The main keypress handler (registered on `process.stdin`) dispatches based on the current `inputMode`:
174
+
175
+ **When no input mode is active (normal mode):**
176
+
177
+ | Key | Action |
178
+ |-----|--------|
179
+ | `q` | `stopLive()` |
180
+ | `c` | Enter customizer mode |
181
+ | `r` | Toggle P2P server (or enter P2P password input) |
182
+ | `p` | Toggle pause |
183
+ | `g` | Toggle graphs |
184
+ | `b` | Cycle graph mode (spark ↔ bar) |
185
+ | `d` | Toggle detailed sensor mode |
186
+ | `←`/`→`/`tab` | Cycle to next/prev panel tab |
187
+ | `1`–`9`, `0` | Jump to panel by number (cpu, mem, gpu, power, battery, thermal, network, packets, tasks, disk) |
188
+ | `P` | Toggle process panel |
189
+ | `s` | Cycle sort mode (cpu → mem → pid → user → command → state → threads → runtime) |
190
+ | `/` | Enter filter input mode |
191
+ | `k` | Enter kill PID input mode |
192
+ | `S` | Enter signal picker (starts at SIGTERM) |
193
+ | `t` | Toggle tree view |
194
+ | `e` | Export snapshot |
195
+ | `l` | Toggle continuous CSV logging |
196
+ | `f` | Cycle export format (json → csv → tsv → json) |
197
+ | `+`/`-` | Increase/decrease refresh interval by 1s |
198
+ | Any single char | If it matches a tab key (`1`–`9`, `0`, `p`, `r`), jump to that panel |
199
+
200
+ **Filter input mode:**
201
+ - Type to build the filter string; Enter applies, Esc cancels.
202
+
203
+ **Kill input mode:**
204
+ - Type a PID; Enter sends SIGTERM, Esc cancels.
205
+
206
+ **Signal picker mode:**
207
+ - ↑/↓ or j/k to cycle through `SIGNAL_OPTIONS`; Enter sends the selected signal to the PID entered in the kill mode; Esc cancels.
208
+
209
+ **Customizer mode:**
210
+ - ↑/↓ or j/k to navigate options; Enter/Space to toggle; Esc to exit.
211
+ - `Theme` cycles through available themes.
212
+ - `Graph Mode` cycles spark ↔ bar.
213
+ - `Toggle Tree View` toggles tree view.
214
+ - Other options toggle panel visibility.
215
+
216
+ **P2P password input mode:**
217
+ - Type password; Enter starts P2P server, Esc cancels.
218
+
219
+ ### Mouse Handling
220
+ Supports both normal mouse (`\x1b[M`) and SGR mouse (`\x1b[<`) protocols. Clicks on the tab bar row (`TAB_BAR_ROW` = 4) are mapped to panel tabs via `getTabHitboxes()`.
221
+
222
+ ### `handleInputModeKey(str, key)`
223
+ Dispatches keypresses within an active input sub-mode (customizer, signal, p2p, filter, kill).
224
+
225
+ ### `killProcess(pidStr, signal?)`
226
+ Sends a signal (default SIGTERM) to the specified PID. Shows a status message on success or failure.
227
+
228
+ ### `startP2PServerFromTUI()`
229
+ Starts a P2P server from within the TUI using the current password and port. Handles errors and updates state accordingly.
230
+
231
+ ### `syncP2PEvents()`
232
+ Syncs P2P peer event history from the server instance into `state.p2pEvents`.
233
+
234
+ ### `stopP2PServer()`
235
+ Stops the running P2P server and resets P2P-related state.
236
+
237
+ ### `onResize()`
238
+ Updates terminal dimensions on resize, adjusts history max length, and re-renders.
239
+
240
+ ### `restartTicker()`
241
+ Clears and restarts the data-collection interval based on `state.interval`.
242
+
243
+ ### `doWarmup()`
244
+ Collects an initial data sample, pushes it to history, writes a log row if logging, checks alerts, and syncs P2P events.
245
+
246
+ ### `tick()`
247
+ The main data-collection tick. Runs every `state.interval` seconds when not paused. Collects data, updates history, writes log row, checks alerts, syncs P2P events, and calls `render()`.
248
+
249
+ ### `tabKeyToId(str)`
250
+ Maps a single-character tab key (`1`–`9`, `0`, `p`, `r`) to its panel ID.
251
+
252
+ ### `cycleTab(direction)`
253
+ Cycles through `PANEL_TABS` in the given direction (1 = forward, -1 = backward).
254
+
255
+ ### `handleNormalMouse(seq)` and `handleSGRMouse(seq)`
256
+ Parse mouse event sequences and delegate to `handleMouseClick()`.
257
+
258
+ ### `handleMouseClick(y, x)`
259
+ If the click is on the tab bar row, finds the hitbox at the clicked column and toggles the corresponding panel.
260
+
261
+ ## `export.ts` — Snapshot Export & CSV Logging
262
+
263
+ ### `ensureDir(dir: string)`
264
+ Creates a directory recursively if it does not exist.
265
+
266
+ ### `timestampForFile(): string`
267
+ Generates a filesystem-safe ISO timestamp string (colons and dots replaced with hyphens).
268
+
269
+ ### `exportSnapshot()`
270
+ Exports `state.lastData` to a file in `state.exportDir` using `state.exportFormat`. Filename pattern: `pyre-<timestamp>.<format>`. Shows a status message on success or failure.
271
+
272
+ ### `startLogging()`
273
+ Starts continuous CSV logging. Creates a log file `pyre-log-<timestamp>.csv` in `state.exportDir` with a header row, then sets `state.logging = true`.
274
+
275
+ ### `stopLogging()`
276
+ Stops continuous CSV logging. Ends the write stream and sets `state.logging = false`.
277
+
278
+ ### `toggleLogging()`
279
+ Toggles logging on/off by calling `stopLogging()` or `startLogging()`.
280
+
281
+ ### `writeLogRow(data)`
282
+ Writes a single CSV row to the log stream with the following columns: `timestamp`, `cpu_usage`, `mem_usage_percent`, `temp_c`, `net_rx_bytes`, `net_tx_bytes`, `net_rx_packets`, `net_tx_packets`, `connections`, `thermal_state`. No-op if logging is not active or the log stream is unavailable.