pyre-cli 4.14.0 → 4.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,6 +9,8 @@ Mac system monitoring CLI: temps, CPU, memory, disk, battery, live dashboard, pa
9
9
 
10
10
  > Note : things may not work as intended if your firewall settings do not allow inbound connections for this program
11
11
 
12
+ > Additionally, you can skip init with enter but you may encounter some UI bugs. I am working on the issue at the moment.
13
+
12
14
  ## Features
13
15
 
14
16
  - **P2P live data streaming** — send live system stats to another system over TCP with password authentication, TLS encryption, rate limiting, IP allow/deny lists, audit logging, and HMAC message signing
@@ -81,6 +83,116 @@ pyre live --interval 5 --log # Custom interval with auto-logging
81
83
  | `--log` | Start continuous CSV logging immediately when live mode starts |
82
84
  | `--packets` | Include packet monitor panel in static output |
83
85
 
86
+ ## Commands
87
+
88
+ ### Static snapshot
89
+
90
+ | Command | Description |
91
+ |---|---|
92
+ | `pyre` | Show all system stats in a formatted table |
93
+ | `pyre --detailed` | Include detailed sensor and powermetrics readings |
94
+ | `pyre --json` | Output stats as JSON |
95
+ | `pyre --csv` | Output stats as CSV |
96
+ | `pyre --tsv` | Output stats as TSV |
97
+ | `pyre --once` | Show a single static snapshot (same as default without live) |
98
+ | `pyre --out <file>` | Write snapshot output to a file |
99
+ | `pyre --packets` | Include packet monitor panel in static output |
100
+
101
+ ### Live dashboard
102
+
103
+ | Command | Description |
104
+ |---|---|
105
+ | `pyre live` | Start the interactive live dashboard |
106
+ | `pyre live --interval <seconds>` | Set refresh interval for live mode (default: `2`) |
107
+ | `pyre live --detailed` | Live mode with detailed sensor readings |
108
+ | `pyre live --export-dir <dir>` | Custom directory for snapshot exports and logs |
109
+ | `pyre live --log` | Start continuous CSV logging immediately |
110
+
111
+ ### Quick P2P setup
112
+
113
+ | Command | Description |
114
+ |---|---|
115
+ | `pyre server` | Detect the local IP and print ready-to-paste commands for starting a P2P server and connecting a client on the same LAN |
116
+
117
+ `pyre server` automatically detects the machine's non-loopback IP address and outputs two copy-paste commands — one to start the server and one for a client on the same network to connect. Default port is `9876` and default password is `mysecret`.
118
+
119
+ ### P2P server
120
+
121
+ | Command | Description |
122
+ |---|---|
123
+ | `pyre p2p server` | Start a P2P server that streams live stats to authenticated peers |
124
+ | `pyre p2p server --p2p-host <host>` | Bind address (default: `0.0.0.0`) |
125
+ | `pyre p2p server --p2p-port <port>` | Port number (default: `9876`) |
126
+ | `pyre p2p server --p2p-password <password>` | Password for authentication (required) |
127
+ | `pyre p2p server --interval <seconds>` | Data refresh interval (default: `2`) |
128
+ | `pyre p2p server --detailed` | Include detailed sensor readings |
129
+ | `pyre p2p server --p2p-cert <file>` | TLS certificate file (PEM) for encrypted connections |
130
+ | `pyre p2p server --p2p-key <file>` | TLS private key file (PEM) for the server |
131
+ | `pyre p2p server --p2p-ca <file>` | TLS CA certificate file (PEM) — required by clients using `--p2p-tls` |
132
+ | `pyre p2p server --p2p-rate-limit <n>` | Max auth attempts per IP per minute (default: `5`) |
133
+ | `pyre p2p server --p2p-allow <ips>` | Comma-separated list of allowed IPs (empty = all) |
134
+ | `pyre p2p server --p2p-deny <ips>` | Comma-separated list of denied IPs |
135
+ | `pyre p2p server --p2p-audit-log <dir>` | Directory for P2P audit logs |
136
+ | `pyre p2p server --p2p-hmac-key <key>` | HMAC key for message signing (default: derived from password) |
137
+
138
+ ### P2P client
139
+
140
+ | Command | Description |
141
+ |---|---|
142
+ | `pyre p2p connect` | Connect to a P2P server and display live stats |
143
+ | `pyre p2p connect --p2p-host <host>` | Server address (default: `127.0.0.1`) |
144
+ | `pyre p2p connect --p2p-port <port>` | Server port (default: `9876`) |
145
+ | `pyre p2p connect --p2p-password <password>` | Password for authentication (required) |
146
+ | `pyre p2p connect --p2p-tls` | Enable TLS encryption for the connection |
147
+ | `pyre p2p connect --p2p-ca <file>` | TLS CA certificate file (PEM) — required when using `--p2p-tls` |
148
+ | `pyre p2p connect --p2p-insecure` | Skip TLS certificate verification (client only) |
149
+ | `pyre p2p connect --p2p-audit-log <dir>` | Directory for P2P client audit logs |
150
+ | `pyre p2p connect --p2p-hmac-key <key>` | HMAC key for message signing (must match server) |
151
+
152
+ ### Options (shared)
153
+
154
+ | Option | Description |
155
+ |---|---|
156
+ | `-j, --json` | Output as JSON |
157
+ | `-c, --csv` | Output as CSV |
158
+ | `-t, --tsv` | Output as TSV |
159
+ | `--detailed` | Include detailed system info and sensor readings |
160
+ | `--interval <seconds>` | Refresh interval for live mode (default: `2`) |
161
+ | `--once` | Show a single static snapshot instead of live feed |
162
+ | `--out <file>` | Write snapshot output to a file |
163
+ | `--export-dir <dir>` | Directory for live-mode snapshot exports and logs (default: `./pyre-exports`) |
164
+ | `--log` | Start continuous CSV logging immediately when live mode starts |
165
+ | `--packets` | Include packet monitor panel in static output |
166
+
167
+ ### Live dashboard keyboard controls
168
+
169
+ | Key | Action |
170
+ |---|---|
171
+ | `q` | Quit the dashboard |
172
+ | `p` | Pause / resume the refresh cycle |
173
+ | `d` | Toggle detailed sensor mode |
174
+ | `g` | Show / hide sparkline graphs |
175
+ | `s` | Cycle sort order (CPU → memory → PID) |
176
+ | `f` | Cycle export format (JSON → CSV → TSV) |
177
+ | `e` | Export a snapshot to the export directory |
178
+ | `l` | Toggle continuous CSV logging |
179
+ | `c` | Open the UI customizer |
180
+ | `/` | Open process filter |
181
+ | `k` | Open process kill (enter PID, then Enter to confirm) |
182
+ | `+` | Increase refresh interval by 1 second |
183
+ | `-` | Decrease refresh interval by 1 second |
184
+ | `↑` / `k` | Navigate customizer options |
185
+ | `↓` / `j` | Navigate customizer options |
186
+ | `Enter` / `Space` | Toggle visibility or select theme in customizer |
187
+ | `Esc` | Close customizer or clear filter/kill input |
188
+
189
+ ### P2P server controls
190
+
191
+ | Command | Action |
192
+ |---|---|
193
+ | `status` | Print the number of currently connected peers |
194
+ | `q` / `quit` / `exit` | Shut down the server gracefully |
195
+
84
196
  ## P2P Live Data Streaming
85
197
 
86
198
  Send live system stats to another system over a TCP connection with password authentication. The server streams `StatsData` snapshots to authenticated peers at the configured interval.
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import fs4 from "fs";
5
+ import os from "os";
5
6
  import readline2 from "readline";
6
7
  import { Command } from "commander";
7
8
  import chalk9 from "chalk";
@@ -1549,7 +1550,10 @@ var state = {
1549
1550
  "Toggle Tasks",
1550
1551
  "Toggle Disk",
1551
1552
  "Toggle Processes",
1552
- "Toggle Tree View"
1553
+ "Toggle Tree View",
1554
+ "Splash Screen",
1555
+ "Splash Color",
1556
+ "Splash Animation"
1553
1557
  ],
1554
1558
  CPU_ALERT_PCT: 90,
1555
1559
  TEMP_ALERT_C: 95,
@@ -1575,7 +1579,10 @@ var state = {
1575
1579
  p2pServer: null,
1576
1580
  p2pServerRunning: false,
1577
1581
  p2pPort: 9876,
1578
- p2pPassword: "pyre"
1582
+ p2pPassword: "pyre",
1583
+ splashEnabled: true,
1584
+ splashColorScheme: "fire",
1585
+ splashAnimation: "classic"
1579
1586
  };
1580
1587
  function setStatus(msg, ms = 3e3) {
1581
1588
  state.statusMessage = msg;
@@ -1740,6 +1747,13 @@ function renderCustomizerOverlay() {
1740
1747
  lines.push(`${prefix}${opt}: ${chalk5.bold.green(state.currentTheme)} [${themesList.join(", ")}]`);
1741
1748
  } else if (opt === "Graph Mode") {
1742
1749
  lines.push(`${prefix}${opt}: ${chalk5.bold.green(state.graphMode)} [spark, bar]`);
1750
+ } else if (opt === "Splash Screen") {
1751
+ const status = state.splashEnabled ? chalk5.green("[ON]") : chalk5.red("[OFF]");
1752
+ lines.push(`${prefix}${opt}: ${status}`);
1753
+ } else if (opt === "Splash Color") {
1754
+ lines.push(`${prefix}${opt}: ${chalk5.bold.green(state.splashColorScheme)} [fire, ocean, forest, purple, monochrome]`);
1755
+ } else if (opt === "Splash Animation") {
1756
+ lines.push(`${prefix}${opt}: ${chalk5.bold.green(state.splashAnimation)} [classic, wave, sparks]`);
1743
1757
  } else {
1744
1758
  const toggleKey = getToggleKey(opt);
1745
1759
  const isVisible = toggleKey ? toggleKey === "tree" ? state.treeView : state.visiblePanels[toggleKey] !== false : true;
@@ -2590,6 +2604,19 @@ function handleInputModeKey(str, key) {
2590
2604
  state.currentTheme = themesList[nextIdx];
2591
2605
  } else if (selected === "Graph Mode") {
2592
2606
  state.graphMode = state.graphMode === "spark" ? "bar" : "spark";
2607
+ } else if (selected === "Splash Screen") {
2608
+ state.splashEnabled = !state.splashEnabled;
2609
+ setStatus(state.splashEnabled ? "Splash screen enabled" : "Splash screen disabled");
2610
+ } else if (selected === "Splash Color") {
2611
+ const schemes = ["fire", "ocean", "forest", "purple", "monochrome"];
2612
+ const curIdx = schemes.indexOf(state.splashColorScheme);
2613
+ state.splashColorScheme = schemes[(curIdx + 1) % schemes.length];
2614
+ setStatus(`Splash color: ${state.splashColorScheme}`);
2615
+ } else if (selected === "Splash Animation") {
2616
+ const animations = ["classic", "wave", "sparks"];
2617
+ const curIdx = animations.indexOf(state.splashAnimation);
2618
+ state.splashAnimation = animations[(curIdx + 1) % animations.length];
2619
+ setStatus(`Splash animation: ${state.splashAnimation}`);
2593
2620
  } else {
2594
2621
  const toggleKey = getToggleKey(selected);
2595
2622
  if (toggleKey) {
@@ -3000,6 +3027,12 @@ async function startLive(opts2, splashPromise) {
3000
3027
  case "tab":
3001
3028
  cycleTab(1);
3002
3029
  break;
3030
+ case "escape":
3031
+ case "esc":
3032
+ state.activePanel = "grid";
3033
+ setStatus("Grid view");
3034
+ render();
3035
+ break;
3003
3036
  default:
3004
3037
  if (str && str.length === 1) {
3005
3038
  const tabId = tabKeyToId(str);
@@ -3099,59 +3132,286 @@ function stopLive() {
3099
3132
  import chalk8 from "chalk";
3100
3133
  var MAXV = 36;
3101
3134
  var CHAR_RAMP = [" ", ".", "`", ",", ":", ";", "+", "*", "?", "%", "#", "@"];
3102
- var palette = [
3103
- "#0a0505",
3104
- "#1a0806",
3105
- "#2a0b06",
3106
- "#3f1004",
3107
- "#591404",
3108
- "#791804",
3109
- "#9e1c04",
3110
- "#c92004",
3111
- "#d63004",
3112
- "#e34004",
3113
- "#f05004",
3114
- "#fd6004",
3115
- "#ff7004",
3116
- "#ff8004",
3117
- "#ff9004",
3118
- "#ffa004",
3119
- "#ffb004",
3120
- "#ffc004",
3121
- "#ffd004",
3122
- "#ffe004",
3123
- "#fff004",
3124
- "#fff820",
3125
- "#ffff40",
3126
- "#ffff60",
3127
- "#ffff80",
3128
- "#ffffa0",
3129
- "#ffffc0",
3130
- "#ffffe0",
3131
- "#ffffff",
3132
- "#ffffff",
3133
- "#ffffff",
3134
- "#ffffff",
3135
- "#ffffff",
3136
- "#ffffff",
3137
- "#ffffff",
3138
- "#ffffff"
3139
- ];
3135
+ var PALETTES = {
3136
+ fire: [
3137
+ "#0a0505",
3138
+ "#1a0806",
3139
+ "#2a0b06",
3140
+ "#3f1004",
3141
+ "#591404",
3142
+ "#791804",
3143
+ "#9e1c04",
3144
+ "#c92004",
3145
+ "#d63004",
3146
+ "#e34004",
3147
+ "#f05004",
3148
+ "#fd6004",
3149
+ "#ff7004",
3150
+ "#ff8004",
3151
+ "#ff9004",
3152
+ "#ffa004",
3153
+ "#ffb004",
3154
+ "#ffc004",
3155
+ "#ffd004",
3156
+ "#ffe004",
3157
+ "#fff004",
3158
+ "#fff820",
3159
+ "#ffff40",
3160
+ "#ffff60",
3161
+ "#ffff80",
3162
+ "#ffffa0",
3163
+ "#ffffc0",
3164
+ "#ffffe0",
3165
+ "#ffffff",
3166
+ "#ffffff",
3167
+ "#ffffff",
3168
+ "#ffffff",
3169
+ "#ffffff",
3170
+ "#ffffff",
3171
+ "#ffffff",
3172
+ "#ffffff"
3173
+ ],
3174
+ ocean: [
3175
+ "#020a1a",
3176
+ "#041428",
3177
+ "#061e36",
3178
+ "#0a2844",
3179
+ "#0e3252",
3180
+ "#123c60",
3181
+ "#16466e",
3182
+ "#1a507c",
3183
+ "#1e5a8a",
3184
+ "#226498",
3185
+ "#266ea6",
3186
+ "#2a78b4",
3187
+ "#2e82c2",
3188
+ "#328cd0",
3189
+ "#3696de",
3190
+ "#3aa0ec",
3191
+ "#3eaae8",
3192
+ "#42b4e4",
3193
+ "#46bee0",
3194
+ "#4ac8dc",
3195
+ "#4fd2d8",
3196
+ "#53dcd4",
3197
+ "#57e6d0",
3198
+ "#5befcc",
3199
+ "#5ff6c8",
3200
+ "#63ffc4",
3201
+ "#67ffc0",
3202
+ "#6bffbc",
3203
+ "#6fffB8",
3204
+ "#73ffb4",
3205
+ "#77ffb0",
3206
+ "#7bffac",
3207
+ "#7fffA8",
3208
+ "#83ffa4",
3209
+ "#87ffa0",
3210
+ "#8bff9c"
3211
+ ],
3212
+ forest: [
3213
+ "#020a02",
3214
+ "#041404",
3215
+ "#061e06",
3216
+ "#0a280a",
3217
+ "#0e320e",
3218
+ "#123c12",
3219
+ "#164616",
3220
+ "#1a501a",
3221
+ "#1e5a1e",
3222
+ "#226422",
3223
+ "#266e26",
3224
+ "#2a782a",
3225
+ "#2e822e",
3226
+ "#328c32",
3227
+ "#369636",
3228
+ "#3aa03a",
3229
+ "#3eaa3e",
3230
+ "#42b442",
3231
+ "#46be46",
3232
+ "#4ac84a",
3233
+ "#4fd24f",
3234
+ "#53dc53",
3235
+ "#57e657",
3236
+ "#5bf05b",
3237
+ "#5ff65f",
3238
+ "#63ff63",
3239
+ "#67ff67",
3240
+ "#6bff6b",
3241
+ "#6fff6f",
3242
+ "#73ff73",
3243
+ "#77ff77",
3244
+ "#7bff7b",
3245
+ "#7fff7f",
3246
+ "#83ff83",
3247
+ "#87ff87",
3248
+ "#8bff8b"
3249
+ ],
3250
+ purple: [
3251
+ "#0a021a",
3252
+ "#140428",
3253
+ "#1e0636",
3254
+ "#280844",
3255
+ "#320a52",
3256
+ "#3c0c60",
3257
+ "#460e6e",
3258
+ "#50107c",
3259
+ "#5a128a",
3260
+ "#641498",
3261
+ "#6e16a6",
3262
+ "#7818b4",
3263
+ "#821ac2",
3264
+ "#8c1cd0",
3265
+ "#961ede",
3266
+ "#a020ec",
3267
+ "#aa22e8",
3268
+ "#b424e4",
3269
+ "#be26e0",
3270
+ "#c828dc",
3271
+ "#d22ad8",
3272
+ "#dc2cd4",
3273
+ "#e62ed0",
3274
+ "#f030cc",
3275
+ "#f432c8",
3276
+ "#f834c4",
3277
+ "#fc36c0",
3278
+ "#ff38bc",
3279
+ "#ff3ab8",
3280
+ "#ff3cb4",
3281
+ "#ff3eb0",
3282
+ "#ff40ac",
3283
+ "#ff42a8",
3284
+ "#ff44a4",
3285
+ "#ff46a0",
3286
+ "#ff489c"
3287
+ ],
3288
+ monochrome: [
3289
+ "#0a0a0a",
3290
+ "#1a1a1a",
3291
+ "#2a2a2a",
3292
+ "#3a3a3a",
3293
+ "#4a4a4a",
3294
+ "#5a5a5a",
3295
+ "#6a6a6a",
3296
+ "#7a7a7a",
3297
+ "#8a8a8a",
3298
+ "#9a9a9a",
3299
+ "#aaaaaa",
3300
+ "#bAbAbA",
3301
+ "#cacaca",
3302
+ "#dAdAdA",
3303
+ "#eaeaea",
3304
+ "#f4f4f4",
3305
+ "#f8f8f8",
3306
+ "#fcfcfc",
3307
+ "#ffffff",
3308
+ "#ffffff",
3309
+ "#ffffff",
3310
+ "#ffffff",
3311
+ "#ffffff",
3312
+ "#ffffff",
3313
+ "#ffffff",
3314
+ "#ffffff",
3315
+ "#ffffff",
3316
+ "#ffffff",
3317
+ "#ffffff",
3318
+ "#ffffff",
3319
+ "#ffffff",
3320
+ "#ffffff",
3321
+ "#ffffff",
3322
+ "#ffffff",
3323
+ "#ffffff",
3324
+ "#ffffff"
3325
+ ]
3326
+ };
3140
3327
  function getRgb(hex) {
3141
3328
  const r = parseInt(hex.slice(1, 3), 16);
3142
3329
  const g = parseInt(hex.slice(3, 5), 16);
3143
3330
  const b = parseInt(hex.slice(5, 7), 16);
3144
3331
  return { r, g, b };
3145
3332
  }
3146
- var rgbPalette = palette.map(getRgb);
3333
+ function getPalette(scheme) {
3334
+ return (PALETTES[scheme] ?? PALETTES.fire).map(getRgb);
3335
+ }
3147
3336
  function getChar(val) {
3148
3337
  const idx = Math.min(CHAR_RAMP.length - 1, Math.floor(val / (MAXV + 1) * CHAR_RAMP.length));
3149
3338
  return CHAR_RAMP[idx];
3150
3339
  }
3151
- async function showSplash() {
3340
+ function initFire(cols, rows) {
3341
+ return new Uint8Array(cols * rows);
3342
+ }
3343
+ function spreadFireClassic(fire, cols, rows) {
3344
+ for (let x = 0; x < cols; x++) {
3345
+ fire[(rows - 1) * cols + x] = MAXV - Math.floor(Math.random() * 4);
3346
+ }
3347
+ for (let y = rows - 1; y >= 1; y--) {
3348
+ for (let x = 0; x < cols; x++) {
3349
+ const src = y * cols + x;
3350
+ const val = fire[src];
3351
+ if (val === 0) {
3352
+ fire[(y - 1) * cols + x] = 0;
3353
+ continue;
3354
+ }
3355
+ const decay = Math.floor(Math.random() * 3);
3356
+ const drift = Math.floor(Math.random() * 3) - 1;
3357
+ let nx = x + drift;
3358
+ if (nx < 0) nx = 0;
3359
+ else if (nx >= cols) nx = cols - 1;
3360
+ fire[(y - 1) * cols + nx] = Math.max(0, val - decay);
3361
+ }
3362
+ }
3363
+ }
3364
+ function spreadFireWave(fire, cols, rows, time) {
3365
+ for (let x = 0; x < cols; x++) {
3366
+ const wave = Math.sin((x + time) * 0.3) * 0.5 + 0.5;
3367
+ fire[(rows - 1) * cols + x] = Math.floor(MAXV * wave);
3368
+ }
3369
+ for (let y = rows - 1; y >= 1; y--) {
3370
+ for (let x = 0; x < cols; x++) {
3371
+ const src = y * cols + x;
3372
+ const val = fire[src];
3373
+ if (val === 0) {
3374
+ fire[(y - 1) * cols + x] = 0;
3375
+ continue;
3376
+ }
3377
+ const decay = Math.floor(Math.random() * 2);
3378
+ const nx = x;
3379
+ fire[(y - 1) * cols + nx] = Math.max(0, val - decay);
3380
+ }
3381
+ }
3382
+ }
3383
+ function spreadFireSparks(fire, cols, rows) {
3384
+ for (let x = 0; x < cols; x++) {
3385
+ fire[(rows - 1) * cols + x] = Math.random() < 0.3 ? MAXV - Math.floor(Math.random() * 4) : 0;
3386
+ }
3387
+ for (let y = rows - 1; y >= 1; y--) {
3388
+ for (let x = 0; x < cols; x++) {
3389
+ const src = y * cols + x;
3390
+ const val = fire[src];
3391
+ if (val === 0) {
3392
+ fire[(y - 1) * cols + x] = 0;
3393
+ continue;
3394
+ }
3395
+ const decay = Math.floor(Math.random() * 4);
3396
+ const drift = Math.floor(Math.random() * 5) - 2;
3397
+ let nx = x + drift;
3398
+ if (nx < 0) nx = 0;
3399
+ else if (nx >= cols) nx = cols - 1;
3400
+ fire[(y - 1) * cols + nx] = Math.max(0, val - decay);
3401
+ }
3402
+ }
3403
+ }
3404
+ async function showSplash(config) {
3405
+ const enabled = config?.enabled ?? true;
3406
+ if (!enabled) return;
3407
+ const colorScheme = config?.colorScheme ?? "fire";
3408
+ const animation = config?.animation ?? "classic";
3409
+ const palette = getPalette(colorScheme);
3410
+ const rgbPalette = palette.map(getRgb);
3152
3411
  const cols = 60;
3153
3412
  const rows = 15;
3154
- let fire = new Uint8Array(cols * rows);
3413
+ let fire = initFire(cols, rows);
3414
+ let time = 0;
3155
3415
  process.stdout.write("\x1B[2J");
3156
3416
  const termRows = process.stdout.rows || 24;
3157
3417
  const termCols = process.stdout.columns || 80;
@@ -3159,24 +3419,13 @@ async function showSplash() {
3159
3419
  const startCol = Math.max(0, Math.floor((termCols - cols) / 2));
3160
3420
  return new Promise((resolve) => {
3161
3421
  const interval = setInterval(() => {
3162
- for (let x = 0; x < cols; x++) {
3163
- fire[(rows - 1) * cols + x] = MAXV - Math.floor(Math.random() * 4);
3164
- }
3165
- for (let y = rows - 1; y >= 1; y--) {
3166
- for (let x = 0; x < cols; x++) {
3167
- const src = y * cols + x;
3168
- const val = fire[src];
3169
- if (val === 0) {
3170
- fire[(y - 1) * cols + x] = 0;
3171
- continue;
3172
- }
3173
- const decay = Math.floor(Math.random() * 3);
3174
- const drift = Math.floor(Math.random() * 3) - 1;
3175
- let nx = x + drift;
3176
- if (nx < 0) nx = 0;
3177
- else if (nx >= cols) nx = cols - 1;
3178
- fire[(y - 1) * cols + nx] = Math.max(0, val - decay);
3179
- }
3422
+ time++;
3423
+ if (animation === "wave") {
3424
+ spreadFireWave(fire, cols, rows, time);
3425
+ } else if (animation === "sparks") {
3426
+ spreadFireSparks(fire, cols, rows);
3427
+ } else {
3428
+ spreadFireClassic(fire, cols, rows);
3180
3429
  }
3181
3430
  let output = "";
3182
3431
  for (let i = 0; i < startRow; i++) output += "\n";
@@ -3221,8 +3470,45 @@ function stripAnsi(s) {
3221
3470
  function sanitizeHost2(host) {
3222
3471
  return host.trim().replace(/%$/, "");
3223
3472
  }
3473
+ function detectLocalIP() {
3474
+ const ifaces = os.networkInterfaces();
3475
+ for (const [, addrs] of Object.entries(ifaces)) {
3476
+ if (!addrs) continue;
3477
+ for (const addr of addrs) {
3478
+ if (addr.family === "IPv4" && !addr.internal) {
3479
+ return addr.address;
3480
+ }
3481
+ }
3482
+ }
3483
+ return null;
3484
+ }
3485
+ async function runServerCommand() {
3486
+ const ip = detectLocalIP();
3487
+ if (!ip) {
3488
+ console.log(chalk9.red("Could not detect a non-loopback IP address. Connect to a network first."));
3489
+ process.exit(1);
3490
+ }
3491
+ const port = 9876;
3492
+ const password = "mysecret";
3493
+ console.log(chalk9.bold(`
3494
+ pyre server`));
3495
+ console.log(chalk9.dim(` Detected IP: ${ip}`));
3496
+ console.log(chalk9.dim(` Port: ${port}`));
3497
+ console.log(chalk9.dim(` Password: ${password}`));
3498
+ console.log();
3499
+ console.log(chalk9.bold("Run this on THIS machine (server):"));
3500
+ console.log(chalk9.hex("#ff6a39")(`pyre p2p server --p2p-host 0.0.0.0 --p2p-port ${port} --p2p-password ${password}`));
3501
+ console.log();
3502
+ console.log(chalk9.bold("Run this on the OTHER machine (client):"));
3503
+ console.log(chalk9.hex("#ff6a39")(`pyre p2p connect --p2p-host ${ip} --p2p-port ${port} --p2p-password ${password}`));
3504
+ console.log();
3505
+ }
3224
3506
  async function main() {
3225
3507
  const cmd = program.args[0];
3508
+ if (cmd === "server") {
3509
+ await runServerCommand();
3510
+ return;
3511
+ }
3226
3512
  if (cmd === "live" || !isExportMode() && !opts.once && cmd !== "p2p") {
3227
3513
  if (cmd === "live") {
3228
3514
  program.args.shift();
@@ -3343,6 +3629,22 @@ async function runP2PServer() {
3343
3629
  process.exit(0);
3344
3630
  });
3345
3631
  }
3632
+ var _prevFrameLines2 = [];
3633
+ function writeFrame2(lines) {
3634
+ if (_prevFrameLines2.length === 0 || lines.length !== _prevFrameLines2.length) {
3635
+ process.stdout.write("\x1B[?25l\x1B[H" + lines.map((l) => `\x1B[2K${l}`).join("\r\n") + "\x1B[0J");
3636
+ _prevFrameLines2 = lines;
3637
+ return;
3638
+ }
3639
+ let out = "";
3640
+ for (let i = 0; i < lines.length; i++) {
3641
+ if (lines[i] !== _prevFrameLines2[i]) {
3642
+ out += `\x1B[${i + 1};1H\x1B[2K${lines[i]}`;
3643
+ }
3644
+ }
3645
+ if (out) process.stdout.write(out);
3646
+ _prevFrameLines2 = lines;
3647
+ }
3346
3648
  async function runP2PConnect() {
3347
3649
  const host = sanitizeHost2(opts.p2pHost || "127.0.0.1");
3348
3650
  const port = parseInt(opts.p2pPort, 10) || 9876;
@@ -3368,16 +3670,23 @@ async function runP2PConnect() {
3368
3670
  process.stdout.write(`\r${chalk9.dim(msg)}${" ".repeat(40)}\r`);
3369
3671
  });
3370
3672
  client.setOnData((data) => {
3371
- process.stdout.write("\x1B[2J\x1B[H");
3372
- console.log(chalk9.bold(` pyre P2P Client \u2014 ${data.header.hostname}`));
3373
- console.log(chalk9.dim(` ${data.timestamp}`));
3374
- console.log();
3375
- console.log(formatTable(data, {
3673
+ const lines = [];
3674
+ lines.push(chalk9.bold(` pyre P2P Client \u2014 ${data.header.hostname}`));
3675
+ lines.push(chalk9.dim(` ${data.timestamp}`));
3676
+ lines.push("");
3677
+ lines.push(...formatTable(data, {
3376
3678
  width: process.stdout.columns || 80,
3377
3679
  sortBy: "cpu",
3378
3680
  treeView: false,
3379
3681
  visible: { packets: true }
3380
- }));
3682
+ }).split("\n"));
3683
+ while (lines.length < process.stdout.rows) {
3684
+ lines.push("");
3685
+ }
3686
+ if (lines.length > process.stdout.rows) {
3687
+ lines.length = process.stdout.rows;
3688
+ }
3689
+ writeFrame2(lines);
3381
3690
  });
3382
3691
  const protocolLabel = useTLS ? "TLS" : "TCP";
3383
3692
  console.log(chalk9.bold(`
@@ -3386,6 +3695,7 @@ async function runP2PConnect() {
3386
3695
  console.log(chalk9.dim(` Connecting to ${host}:${port}...`));
3387
3696
  console.log(chalk9.dim(` Press Ctrl+C to disconnect
3388
3697
  `));
3698
+ process.stdout.write("\x1B[?25l");
3389
3699
  try {
3390
3700
  await client.connect();
3391
3701
  } catch (err) {
@@ -3409,10 +3719,13 @@ async function runP2PConnect() {
3409
3719
  console.log(chalk9.dim(" 3. Check connectivity: ping " + host));
3410
3720
  console.log(chalk9.dim(" 4. Ensure both machines are on the same LAN (ping each other first)"));
3411
3721
  }
3722
+ process.stdout.write("\x1B[?25h");
3412
3723
  process.exit(1);
3413
3724
  }
3414
3725
  process.once("SIGINT", () => {
3726
+ _prevFrameLines2 = [];
3415
3727
  client.disconnect();
3728
+ process.stdout.write("\x1B[?25h\x1B[2J\x1B[H");
3416
3729
  process.exit(0);
3417
3730
  });
3418
3731
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pyre-cli",
3
- "version": "4.14.0",
3
+ "version": "4.16.0",
4
4
  "description": "CLI Mac monitoring tool: temps, system stats, live dashboard, packet monitor, battery predictor, export",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  * P2P mode enables live data streaming between two systems over TCP.
8
8
  */
9
9
  import fs from 'node:fs';
10
+ import os from 'node:os';
10
11
  import readline from 'node:readline';
11
12
  import { Command } from 'commander';
12
13
  import chalk from 'chalk';
@@ -69,9 +70,53 @@ function sanitizeHost(host: string): string {
69
70
  return host.trim().replace(/%$/, '');
70
71
  }
71
72
 
73
+ function detectLocalIP(): string | null {
74
+ const ifaces = os.networkInterfaces();
75
+ for (const [, addrs] of Object.entries(ifaces)) {
76
+ if (!addrs) continue;
77
+ for (const addr of addrs) {
78
+ if (addr.family === 'IPv4' && !addr.internal) {
79
+ return addr.address;
80
+ }
81
+ }
82
+ }
83
+ return null;
84
+ }
85
+
86
+ async function runServerCommand(): Promise<void> {
87
+ const ip = detectLocalIP();
88
+
89
+ if (!ip) {
90
+ console.log(chalk.red('Could not detect a non-loopback IP address. Connect to a network first.'));
91
+ process.exit(1);
92
+ }
93
+
94
+ const port = 9876;
95
+ const password = 'mysecret';
96
+
97
+ console.log(chalk.bold(`\n pyre server`));
98
+ console.log(chalk.dim(` Detected IP: ${ip}`));
99
+ console.log(chalk.dim(` Port: ${port}`));
100
+ console.log(chalk.dim(` Password: ${password}`));
101
+ console.log();
102
+
103
+ console.log(chalk.bold('Run this on THIS machine (server):'));
104
+ console.log(chalk.hex('#ff6a39')(`pyre p2p server --p2p-host 0.0.0.0 --p2p-port ${port} --p2p-password ${password}`));
105
+ console.log();
106
+
107
+ console.log(chalk.bold('Run this on the OTHER machine (client):'));
108
+ console.log(chalk.hex('#ff6a39')(`pyre p2p connect --p2p-host ${ip} --p2p-port ${port} --p2p-password ${password}`));
109
+ console.log();
110
+ }
111
+
72
112
  async function main() {
73
113
  const cmd = program.args[0];
74
114
 
115
+ if (cmd === 'server') {
116
+ await runServerCommand();
117
+ return;
118
+ }
119
+
75
120
  if (cmd === 'live' || (!isExportMode() && !opts.once && cmd !== 'p2p')) {
76
121
  if (cmd === 'live') {
77
122
  program.args.shift();
@@ -208,6 +253,25 @@ async function runP2PServer(): Promise<void> {
208
253
  });
209
254
  }
210
255
 
256
+ let _prevFrameLines: string[] = [];
257
+
258
+ function writeFrame(lines: string[]) {
259
+ if (_prevFrameLines.length === 0 || lines.length !== _prevFrameLines.length) {
260
+ process.stdout.write('\x1b[?25l\x1b[H' + lines.map(l => `\x1b[2K${l}`).join('\r\n') + '\x1b[0J');
261
+ _prevFrameLines = lines;
262
+ return;
263
+ }
264
+
265
+ let out = '';
266
+ for (let i = 0; i < lines.length; i++) {
267
+ if (lines[i] !== _prevFrameLines[i]) {
268
+ out += `\x1b[${i + 1};1H\x1b[2K${lines[i]}`;
269
+ }
270
+ }
271
+ if (out) process.stdout.write(out);
272
+ _prevFrameLines = lines;
273
+ }
274
+
211
275
  async function runP2PConnect(): Promise<void> {
212
276
  const host = sanitizeHost(opts.p2pHost || '127.0.0.1');
213
277
  const port = parseInt(opts.p2pPort, 10) || 9876;
@@ -237,16 +301,25 @@ async function runP2PConnect(): Promise<void> {
237
301
  });
238
302
 
239
303
  client.setOnData((data) => {
240
- process.stdout.write('\x1b[2J\x1b[H');
241
- console.log(chalk.bold(` pyre P2P Client — ${data.header.hostname}`));
242
- console.log(chalk.dim(` ${data.timestamp}`));
243
- console.log();
244
- console.log(formatTable(data, {
304
+ const lines: string[] = [];
305
+ lines.push(chalk.bold(` pyre P2P Client — ${data.header.hostname}`));
306
+ lines.push(chalk.dim(` ${data.timestamp}`));
307
+ lines.push('');
308
+ lines.push(...formatTable(data, {
245
309
  width: process.stdout.columns || 80,
246
310
  sortBy: 'cpu',
247
311
  treeView: false,
248
312
  visible: { packets: true },
249
- }));
313
+ }).split('\n'));
314
+
315
+ while (lines.length < process.stdout.rows) {
316
+ lines.push('');
317
+ }
318
+ if (lines.length > process.stdout.rows) {
319
+ lines.length = process.stdout.rows;
320
+ }
321
+
322
+ writeFrame(lines);
250
323
  });
251
324
 
252
325
  const protocolLabel = useTLS ? 'TLS' : 'TCP';
@@ -255,6 +328,8 @@ async function runP2PConnect(): Promise<void> {
255
328
  console.log(chalk.dim(` Connecting to ${host}:${port}...`));
256
329
  console.log(chalk.dim(` Press Ctrl+C to disconnect\n`));
257
330
 
331
+ process.stdout.write('\x1b[?25l');
332
+
258
333
  try {
259
334
  await client.connect();
260
335
  } catch (err: any) {
@@ -278,11 +353,14 @@ async function runP2PConnect(): Promise<void> {
278
353
  console.log(chalk.dim(' 3. Check connectivity: ping ' + host));
279
354
  console.log(chalk.dim(' 4. Ensure both machines are on the same LAN (ping each other first)'));
280
355
  }
356
+ process.stdout.write('\x1b[?25h');
281
357
  process.exit(1);
282
358
  }
283
359
 
284
360
  process.once('SIGINT', () => {
361
+ _prevFrameLines = [];
285
362
  client.disconnect();
363
+ process.stdout.write('\x1b[?25h\x1b[2J\x1b[H');
286
364
  process.exit(0);
287
365
  });
288
366
  }
package/src/live/input.ts CHANGED
@@ -10,7 +10,7 @@ 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, getToggleKey } from './state.js';
13
+ import { state, setStatus, getToggleKey, type SplashColorScheme, type SplashAnimation } from './state.js';
14
14
  import { exportSnapshot, startLogging, stopLogging, toggleLogging, writeLogRow } from './export.js';
15
15
  import { render, footerLine, checkAlerts, invalidateTableCache, invalidateFrame } from './render.js';
16
16
  import type { LiveOptions, ExportFormat, InputMode, SortMode, GraphMode } from './types.js';
@@ -51,18 +51,31 @@ function handleInputModeKey(str: string, key: readline.Key) {
51
51
  state.currentTheme = themesList[nextIdx];
52
52
  } else if (selected === 'Graph Mode') {
53
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
- }
65
- }
54
+ } else if (selected === 'Splash Screen') {
55
+ state.splashEnabled = !state.splashEnabled;
56
+ setStatus(state.splashEnabled ? 'Splash screen enabled' : 'Splash screen disabled');
57
+ } else if (selected === 'Splash Color') {
58
+ const schemes: SplashColorScheme[] = ['fire', 'ocean', 'forest', 'purple', 'monochrome'];
59
+ const curIdx = schemes.indexOf(state.splashColorScheme);
60
+ state.splashColorScheme = schemes[(curIdx + 1) % schemes.length];
61
+ setStatus(`Splash color: ${state.splashColorScheme}`);
62
+ } else if (selected === 'Splash Animation') {
63
+ const animations: SplashAnimation[] = ['classic', 'wave', 'sparks'];
64
+ const curIdx = animations.indexOf(state.splashAnimation);
65
+ state.splashAnimation = animations[(curIdx + 1) % animations.length];
66
+ setStatus(`Splash animation: ${state.splashAnimation}`);
67
+ } else {
68
+ const toggleKey = getToggleKey(selected);
69
+ if (toggleKey) {
70
+ if (toggleKey === 'tree') {
71
+ state.treeView = !state.treeView;
72
+ state.visiblePanels.tree = state.treeView;
73
+ } else {
74
+ const isVisible = state.visiblePanels[toggleKey] !== false;
75
+ state.visiblePanels[toggleKey] = !isVisible;
76
+ }
77
+ }
78
+ }
66
79
  invalidateTableCache();
67
80
  invalidateFrame();
68
81
  }
@@ -492,6 +505,12 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
492
505
  case 'tab':
493
506
  cycleTab(1);
494
507
  break;
508
+ case 'escape':
509
+ case 'esc':
510
+ state.activePanel = 'grid';
511
+ setStatus('Grid view');
512
+ render();
513
+ break;
495
514
  default:
496
515
  if (str && str.length === 1) {
497
516
  const tabId = tabKeyToId(str);
@@ -103,21 +103,28 @@ function renderCustomizerOverlay(): string {
103
103
  chalk.bgCyan.black.bold(' UI CUSTOMIZER (Use ↑/↓ or J/K to navigate, Enter/Space to toggle, Esc to exit) '),
104
104
  ];
105
105
 
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 {
115
- const toggleKey = getToggleKey(opt);
116
- const isVisible = toggleKey ? (toggleKey === 'tree' ? state.treeView : state.visiblePanels[toggleKey] !== false) : true;
117
- const status = isVisible ? chalk.green('[VISIBLE]') : chalk.red('[HIDDEN]');
118
- lines.push(`${prefix}${opt}: ${status}`);
119
- }
120
- });
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
+ });
121
128
 
122
129
  return lines.map(l => ` ${l}`).join('\n');
123
130
  }
package/src/live/state.ts CHANGED
@@ -14,6 +14,10 @@ import type { ThemeName, VisibleItems } from '../formatters/types.js';
14
14
 
15
15
  export const SIGNAL_OPTIONS = ['SIGTERM', 'SIGKILL', 'SIGINT', 'SIGHUP', 'SIGSTOP', 'SIGCONT'] as const;
16
16
 
17
+ import { type SplashColorScheme, type SplashAnimation } from '../splash.js';
18
+
19
+ export { type SplashColorScheme, type SplashAnimation };
20
+
17
21
  const state = {
18
22
  intervalHandle: null as NodeJS.Timeout | null,
19
23
  uiIntervalHandle: null as NodeJS.Timeout | null,
@@ -59,48 +63,54 @@ const state = {
59
63
  treeView: false,
60
64
  SIGNAL_OPTIONS,
61
65
  customizerIndex: 0,
62
- CUSTOMIZER_OPTIONS: [
63
- 'Theme',
64
- 'Graph Mode',
65
- 'Toggle CPU',
66
- 'Toggle Memory',
67
- 'Toggle GPU',
68
- 'Toggle Power',
69
- 'Toggle Battery',
70
- 'Toggle Thermal',
71
- 'Toggle Network',
72
- 'Toggle Packets',
73
- 'Toggle Tasks',
74
- 'Toggle Disk',
75
- 'Toggle Processes',
76
- 'Toggle Tree View',
77
- ],
78
- CPU_ALERT_PCT: 90,
79
- TEMP_ALERT_C: 95,
80
- alerted: false,
81
- activePanel: 'grid' as 'grid' | 'cpu' | 'mem' | 'gpu' | 'power' | 'battery' | 'thermal' | 'network' | 'packets' | 'tasks' | 'disk' | 'process' | 'p2p',
82
- mouseEnabled: true,
83
- PANEL_TABS: [
84
- { id: 'cpu', label: 'CPU', key: '1' },
85
- { id: 'mem', label: 'Memory', key: '2' },
86
- { id: 'gpu', label: 'GPU', key: '3' },
87
- { id: 'power', label: 'Power', key: '4' },
88
- { id: 'battery', label: 'Battery', key: '5' },
89
- { id: 'thermal', label: 'Thermal', key: '6' },
90
- { id: 'network', label: 'Network', key: '7' },
91
- { id: 'packets', label: 'Packets', key: '8' },
92
- { id: 'tasks', label: 'Tasks', key: '9' },
93
- { id: 'disk', label: 'Disk', key: '0' },
94
- { id: 'process', label: 'Process', key: 'P' },
95
- { id: 'p2p', label: 'P2P', key: 'R' },
96
- ] as const,
97
- p2pEvents: [] as { type: string; detail: string; ts: number }[],
98
- p2pBind: '',
99
- p2pServer: null as { stop: () => void; peerEventHistory: { type: string; detail: string; ts: number }[] } | null,
100
- p2pServerRunning: false,
101
- p2pPort: 9876,
102
- p2pPassword: 'pyre',
103
- };
66
+ CUSTOMIZER_OPTIONS: [
67
+ 'Theme',
68
+ 'Graph Mode',
69
+ 'Toggle CPU',
70
+ 'Toggle Memory',
71
+ 'Toggle GPU',
72
+ 'Toggle Power',
73
+ 'Toggle Battery',
74
+ 'Toggle Thermal',
75
+ 'Toggle Network',
76
+ 'Toggle Packets',
77
+ 'Toggle Tasks',
78
+ 'Toggle Disk',
79
+ 'Toggle Processes',
80
+ 'Toggle Tree View',
81
+ 'Splash Screen',
82
+ 'Splash Color',
83
+ 'Splash Animation',
84
+ ],
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
+ };
104
114
 
105
115
  function setStatus(msg: string, ms = 3000) {
106
116
  state.statusMessage = msg;
package/src/splash.ts CHANGED
@@ -1,19 +1,60 @@
1
1
  /**
2
2
  * ASCII Splash Screen implementation based on the doom-fire effect.
3
+ *
4
+ * Supports configurable colour schemes and animation types via
5
+ * the {@link SplashConfig} interface.
3
6
  */
4
7
  import chalk from 'chalk';
5
8
 
6
9
  const MAXV = 36;
7
10
  const CHAR_RAMP = [' ', '.', '`', ',', ':', ';', '+', '*', '?', '%', '#', '@'];
8
11
 
9
- // Colors for the fire effect
10
- const palette = [
11
- '#0a0505', '#1a0806', '#2a0b06', '#3f1004', '#591404', '#791804', '#9e1c04', '#c92004',
12
- '#d63004', '#e34004', '#f05004', '#fd6004', '#ff7004', '#ff8004', '#ff9004', '#ffa004',
13
- '#ffb004', '#ffc004', '#ffd004', '#ffe004', '#fff004', '#fff820', '#ffff40', '#ffff60',
14
- '#ffff80', '#ffffa0', '#ffffc0', '#ffffe0', '#ffffff', '#ffffff', '#ffffff', '#ffffff',
15
- '#ffffff', '#ffffff', '#ffffff', '#ffffff'
16
- ];
12
+ export type SplashColorScheme = 'fire' | 'ocean' | 'forest' | 'purple' | 'monochrome';
13
+ export type SplashAnimation = 'classic' | 'wave' | 'sparks';
14
+
15
+ export interface SplashConfig {
16
+ enabled?: boolean;
17
+ colorScheme?: SplashColorScheme;
18
+ animation?: SplashAnimation;
19
+ }
20
+
21
+ const PALETTES: Record<SplashColorScheme, string[]> = {
22
+ fire: [
23
+ '#0a0505', '#1a0806', '#2a0b06', '#3f1004', '#591404', '#791804', '#9e1c04', '#c92004',
24
+ '#d63004', '#e34004', '#f05004', '#fd6004', '#ff7004', '#ff8004', '#ff9004', '#ffa004',
25
+ '#ffb004', '#ffc004', '#ffd004', '#ffe004', '#fff004', '#fff820', '#ffff40', '#ffff60',
26
+ '#ffff80', '#ffffa0', '#ffffc0', '#ffffe0', '#ffffff', '#ffffff', '#ffffff', '#ffffff',
27
+ '#ffffff', '#ffffff', '#ffffff', '#ffffff',
28
+ ],
29
+ ocean: [
30
+ '#020a1a', '#041428', '#061e36', '#0a2844', '#0e3252', '#123c60', '#16466e', '#1a507c',
31
+ '#1e5a8a', '#226498', '#266ea6', '#2a78b4', '#2e82c2', '#328cd0', '#3696de', '#3aa0ec',
32
+ '#3eaae8', '#42b4e4', '#46bee0', '#4ac8dc', '#4fd2d8', '#53dcd4', '#57e6d0', '#5befcc',
33
+ '#5ff6c8', '#63ffc4', '#67ffc0', '#6bffbc', '#6fffB8', '#73ffb4', '#77ffb0', '#7bffac',
34
+ '#7fffA8', '#83ffa4', '#87ffa0', '#8bff9c',
35
+ ],
36
+ forest: [
37
+ '#020a02', '#041404', '#061e06', '#0a280a', '#0e320e', '#123c12', '#164616', '#1a501a',
38
+ '#1e5a1e', '#226422', '#266e26', '#2a782a', '#2e822e', '#328c32', '#369636', '#3aa03a',
39
+ '#3eaa3e', '#42b442', '#46be46', '#4ac84a', '#4fd24f', '#53dc53', '#57e657', '#5bf05b',
40
+ '#5ff65f', '#63ff63', '#67ff67', '#6bff6b', '#6fff6f', '#73ff73', '#77ff77', '#7bff7b',
41
+ '#7fff7f', '#83ff83', '#87ff87', '#8bff8b',
42
+ ],
43
+ purple: [
44
+ '#0a021a', '#140428', '#1e0636', '#280844', '#320a52', '#3c0c60', '#460e6e', '#50107c',
45
+ '#5a128a', '#641498', '#6e16a6', '#7818b4', '#821ac2', '#8c1cd0', '#961ede', '#a020ec',
46
+ '#aa22e8', '#b424e4', '#be26e0', '#c828dc', '#d22ad8', '#dc2cd4', '#e62ed0', '#f030cc',
47
+ '#f432c8', '#f834c4', '#fc36c0', '#ff38bc', '#ff3ab8', '#ff3cb4', '#ff3eb0', '#ff40ac',
48
+ '#ff42a8', '#ff44a4', '#ff46a0', '#ff489c',
49
+ ],
50
+ monochrome: [
51
+ '#0a0a0a', '#1a1a1a', '#2a2a2a', '#3a3a3a', '#4a4a4a', '#5a5a5a', '#6a6a6a', '#7a7a7a',
52
+ '#8a8a8a', '#9a9a9a', '#aaaaaa', '#bAbAbA', '#cacaca', '#dAdAdA', '#eaeaea', '#f4f4f4',
53
+ '#f8f8f8', '#fcfcfc', '#ffffff', '#ffffff', '#ffffff', '#ffffff', '#ffffff', '#ffffff',
54
+ '#ffffff', '#ffffff', '#ffffff', '#ffffff', '#ffffff', '#ffffff', '#ffffff', '#ffffff',
55
+ '#ffffff', '#ffffff', '#ffffff', '#ffffff',
56
+ ],
57
+ };
17
58
 
18
59
  function getRgb(hex: string) {
19
60
  const r = parseInt(hex.slice(1, 3), 16);
@@ -22,19 +63,95 @@ function getRgb(hex: string) {
22
63
  return { r, g, b };
23
64
  }
24
65
 
25
- const rgbPalette = palette.map(getRgb);
66
+ function getPalette(scheme: SplashColorScheme) {
67
+ return (PALETTES[scheme] ?? PALETTES.fire).map(getRgb);
68
+ }
26
69
 
27
70
  function getChar(val: number) {
28
71
  const idx = Math.min(CHAR_RAMP.length - 1, Math.floor((val / (MAXV + 1)) * CHAR_RAMP.length));
29
72
  return CHAR_RAMP[idx];
30
73
  }
31
74
 
32
- export async function showSplash() {
75
+ function initFire(cols: number, rows: number): Uint8Array {
76
+ return new Uint8Array(cols * rows);
77
+ }
78
+
79
+ function spreadFireClassic(fire: Uint8Array, cols: number, rows: number) {
80
+ for (let x = 0; x < cols; x++) {
81
+ fire[(rows - 1) * cols + x] = MAXV - Math.floor(Math.random() * 4);
82
+ }
83
+ for (let y = rows - 1; y >= 1; y--) {
84
+ for (let x = 0; x < cols; x++) {
85
+ const src = y * cols + x;
86
+ const val = fire[src];
87
+ if (val === 0) {
88
+ fire[(y - 1) * cols + x] = 0;
89
+ continue;
90
+ }
91
+ const decay = Math.floor(Math.random() * 3);
92
+ const drift = Math.floor(Math.random() * 3) - 1;
93
+ let nx = x + drift;
94
+ if (nx < 0) nx = 0; else if (nx >= cols) nx = cols - 1;
95
+ fire[(y - 1) * cols + nx] = Math.max(0, val - decay);
96
+ }
97
+ }
98
+ }
99
+
100
+ function spreadFireWave(fire: Uint8Array, cols: number, rows: number, time: number) {
101
+ for (let x = 0; x < cols; x++) {
102
+ const wave = Math.sin((x + time) * 0.3) * 0.5 + 0.5;
103
+ fire[(rows - 1) * cols + x] = Math.floor(MAXV * wave);
104
+ }
105
+ for (let y = rows - 1; y >= 1; y--) {
106
+ for (let x = 0; x < cols; x++) {
107
+ const src = y * cols + x;
108
+ const val = fire[src];
109
+ if (val === 0) {
110
+ fire[(y - 1) * cols + x] = 0;
111
+ continue;
112
+ }
113
+ const decay = Math.floor(Math.random() * 2);
114
+ const nx = x;
115
+ fire[(y - 1) * cols + nx] = Math.max(0, val - decay);
116
+ }
117
+ }
118
+ }
119
+
120
+ function spreadFireSparks(fire: Uint8Array, cols: number, rows: number) {
121
+ for (let x = 0; x < cols; x++) {
122
+ fire[(rows - 1) * cols + x] = Math.random() < 0.3 ? MAXV - Math.floor(Math.random() * 4) : 0;
123
+ }
124
+ for (let y = rows - 1; y >= 1; y--) {
125
+ for (let x = 0; x < cols; x++) {
126
+ const src = y * cols + x;
127
+ const val = fire[src];
128
+ if (val === 0) {
129
+ fire[(y - 1) * cols + x] = 0;
130
+ continue;
131
+ }
132
+ const decay = Math.floor(Math.random() * 4);
133
+ const drift = Math.floor(Math.random() * 5) - 2;
134
+ let nx = x + drift;
135
+ if (nx < 0) nx = 0; else if (nx >= cols) nx = cols - 1;
136
+ fire[(y - 1) * cols + nx] = Math.max(0, val - decay);
137
+ }
138
+ }
139
+ }
140
+
141
+ export async function showSplash(config?: SplashConfig): Promise<void> {
142
+ const enabled = config?.enabled ?? true;
143
+ if (!enabled) return;
144
+
145
+ const colorScheme = config?.colorScheme ?? 'fire';
146
+ const animation = config?.animation ?? 'classic';
147
+ const palette = getPalette(colorScheme);
148
+ const rgbPalette = palette.map(getRgb);
149
+
33
150
  const cols = 60;
34
151
  const rows = 15;
35
- let fire = new Uint8Array(cols * rows);
152
+ let fire = initFire(cols, rows);
153
+ let time = 0;
36
154
 
37
- // Clear screen
38
155
  process.stdout.write('\x1b[2J');
39
156
  const termRows = process.stdout.rows || 24;
40
157
  const termCols = process.stdout.columns || 80;
@@ -43,29 +160,16 @@ export async function showSplash() {
43
160
 
44
161
  return new Promise<void>((resolve) => {
45
162
  const interval = setInterval(() => {
46
- // Bottom row heat
47
- for (let x = 0; x < cols; x++) {
48
- fire[(rows - 1) * cols + x] = MAXV - Math.floor(Math.random() * 4);
49
- }
163
+ time++;
50
164
 
51
- // Spread fire
52
- for (let y = rows - 1; y >= 1; y--) {
53
- for (let x = 0; x < cols; x++) {
54
- const src = y * cols + x;
55
- const val = fire[src];
56
- if (val === 0) {
57
- fire[(y - 1) * cols + x] = 0;
58
- continue;
59
- }
60
- const decay = Math.floor(Math.random() * 3);
61
- const drift = Math.floor(Math.random() * 3) - 1;
62
- let nx = x + drift;
63
- if (nx < 0) nx = 0; else if (nx >= cols) nx = cols - 1;
64
- fire[(y - 1) * cols + nx] = Math.max(0, val - decay);
65
- }
165
+ if (animation === 'wave') {
166
+ spreadFireWave(fire, cols, rows, time);
167
+ } else if (animation === 'sparks') {
168
+ spreadFireSparks(fire, cols, rows);
169
+ } else {
170
+ spreadFireClassic(fire, cols, rows);
66
171
  }
67
172
 
68
- // Draw
69
173
  let output = '';
70
174
  for (let i = 0; i < startRow; i++) output += '\n';
71
175
 
@@ -80,8 +184,7 @@ export async function showSplash() {
80
184
  }
81
185
  output += ' '.repeat(Math.max(0, startCol + (cols - 45) / 2)) + chalk.bold.white('🔥 pyre — system monitor (warming up sensors...)\n');
82
186
  output += ' '.repeat(Math.max(0, startCol + (cols - 25) / 2)) + chalk.dim('[Press Enter to continue]');
83
-
84
- // Clear viewport area and redraw
187
+
85
188
  process.stdout.write(`\x1b[H${output}`);
86
189
  }, 65);
87
190
 
@@ -96,7 +199,6 @@ export async function showSplash() {
96
199
  process.stdin.resume();
97
200
  process.stdin.on('data', onEnter);
98
201
 
99
- // Auto-exit after 5s
100
202
  setTimeout(onEnter, 5000);
101
203
  });
102
- }
204
+ }