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 +112 -0
- package/dist/index.js +380 -67
- package/package.json +1 -1
- package/src/index.ts +84 -6
- package/src/live/input.ts +32 -13
- package/src/live/render.ts +22 -15
- package/src/live/state.ts +52 -42
- package/src/splash.ts +138 -36
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
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
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
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
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
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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);
|
package/src/live/render.ts
CHANGED
|
@@ -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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
+
}
|