dankgrinder 7.83.0 → 8.1.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/lib/terminal.js DELETED
@@ -1,720 +0,0 @@
1
- /**
2
- * terminal.js — Polished animated terminal renderer for DankGrinder
3
- *
4
- * Features:
5
- * - Animated startup phases with multi-element spinners
6
- * - Live leaderboard with coin-based rank medals (🥇🥈🥉)
7
- * - Accounts stay in fixed positions, medals update live
8
- * - Pulsing status indicators for active accounts
9
- * - Dark theme: deep purple borders, vibrant accent colors
10
- * - 4 FPS render loop, dirty-row only, no flicker
11
- * - Graceful fallback: plain console.log if not a TTY
12
- */
13
-
14
- 'use strict';
15
-
16
- const READY = (() => {
17
- try { return !!process.stdout.isTTY && !process.env.NO_TERM; } catch (_) { return false; }
18
- })();
19
-
20
- // ── ANSI helpers ────────────────────────────────────────────────────────────
21
-
22
- const A = {
23
- reset: '\x1b[0m',
24
- bold: '\x1b[1m',
25
- dim: '\x1b[2m',
26
- rgb: (r, g, b) => `\x1b[38;2;${r};${g};${b}m`,
27
- eraseAll: '\x1b[3J\x1b[2J\x1b[H',
28
- clearLine: '\x1b[2K',
29
- save: '\x1b7',
30
- restore: '\x1b8',
31
- hide: '\x1b[?25l',
32
- show: '\x1b[?25h',
33
- };
34
-
35
- // ── Palette ─────────────────────────────────────────────────────────────────
36
-
37
- const C = {
38
- border: A.rgb(55, 42, 95),
39
- borderDim: A.rgb(38, 28, 65),
40
- borderMid: A.rgb(80, 65, 130),
41
-
42
- text: A.rgb(205, 200, 230),
43
- textDim: A.rgb(90, 85, 115),
44
- textFaint: A.rgb(55, 50, 75),
45
-
46
- purple: A.rgb(167, 139, 250), // vivid lavender
47
- cyan: A.rgb(34, 211, 238), // vivid cyan
48
- gold: A.rgb(251, 191, 36), // vivid gold
49
- green: A.rgb(52, 211, 153), // vivid green
50
- pink: A.rgb(244, 114, 182), // vivid pink
51
- orange: A.rgb(251, 146, 60), // vivid orange
52
- red: A.rgb(248, 113, 113), // vivid red
53
- blue: A.rgb(96, 165, 250), // vivid blue
54
-
55
- // Rank medal colors
56
- rank1: A.rgb(255, 215, 0),
57
- rank2: A.rgb(192, 192, 192),
58
- rank3: A.rgb(205, 127, 50),
59
-
60
- // Per-account accent colors (cycles)
61
- ACCT: [
62
- A.rgb(167, 139, 250), // lavender
63
- A.rgb(103, 232, 249), // sky cyan
64
- A.rgb(253, 186, 116), // peach
65
- A.rgb(167, 243, 208), // mint
66
- A.rgb(252, 165, 201), // rose
67
- A.rgb(165, 243, 252), // light cyan
68
- A.rgb(196, 181, 253), // light purple
69
- A.rgb(147, 226, 226), // light teal
70
- ],
71
- };
72
-
73
- // ── Box-drawing ─────────────────────────────────────────────────────────────
74
-
75
- const TL='╭', TR='╮', BL='╰', BR='╯', H='─', V='│';
76
-
77
- // ── Spinner frames ──────────────────────────────────────────────────────────
78
-
79
- // Dot spinner for phase labels
80
- const SPIN_DOTS = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠿'];
81
-
82
- // Block spinner for progress
83
- const SPIN_BLOCK = ['▏','▎','�','▌','▋','▊','▉','▊'];
84
-
85
- // Pulse frames for active indicators
86
- const PULSE = ['●', '◉', '◎', '○'];
87
-
88
- // ── stdout capture ──────────────────────────────────────────────────────────
89
-
90
- let _origWrite = null;
91
- let _captureActive = false;
92
- let _captureBuf = [];
93
-
94
- function _capWrite(chunk) {
95
- if (_captureActive) { _captureBuf.push(String(chunk)); return; }
96
- return _origWrite.call(process.stdout, chunk);
97
- }
98
-
99
- // ── ANSI utils ─────────────────────────────────────────────────────────────
100
-
101
- function ansiLen(s) {
102
- let len = 0, i = 0;
103
- const str = String(s);
104
- while (i < str.length) {
105
- if (str.charCodeAt(i) === 0x1b && str[i+1] === '[') {
106
- let j = i+2;
107
- while (j < str.length && str[j] !== 'm') j++;
108
- i = j + 1;
109
- } else { len++; i++; }
110
- }
111
- return len;
112
- }
113
-
114
- function rpad(s, w) { return s + ' '.repeat(Math.max(0, w - ansiLen(s))); }
115
-
116
- // ── Rank helpers ───────────────────────────────────────────────────────────
117
-
118
- const MEDALS = ['🥇','🥈','🥉'];
119
-
120
- function coinRank(wk, workers) {
121
- const c = wk.stats?.coins || 0;
122
- let r = 1;
123
- for (const w of workers) {
124
- if ((w.stats?.coins || 0) > c) r++;
125
- }
126
- return r;
127
- }
128
-
129
- // ── Terminal ───────────────────────────────────────────────────────────────
130
-
131
- class Terminal {
132
- constructor() {
133
- this.workers = [];
134
- this.events = [];
135
- this.MAX_EVENTS = 3;
136
-
137
- this.phase = '';
138
- this.phaseFrame = 0;
139
- this.phaseTimer = null;
140
- this.phaseDone = 0;
141
- this.phaseTotal = 0;
142
-
143
- this.dirtyWorkers = new Set();
144
- this.dirtyStats = true;
145
- this._renderTimer = null;
146
- this._startTime = 0;
147
- this._active = false;
148
- this._shutdown = false;
149
- this._origLog = null;
150
-
151
- this._w = 110;
152
- this._h = 35;
153
- this.windowStart = 0;
154
- this.windowSize = 8;
155
-
156
- // Pulse animation state
157
- this._pulseFrame = 0;
158
- this._pulseTimer = null;
159
- }
160
-
161
- // ── Public API ──────────────────────────────────────────────────────────
162
-
163
- init(opts = {}) {
164
- this._startTime = opts.startTime || Date.now();
165
- this.workers = opts.workers || [];
166
- this._updateSize();
167
-
168
- if (READY) {
169
- _origWrite = process.stdout.write.bind(process.stdout);
170
- process.stdout.write = _capWrite;
171
- _captureActive = true;
172
- _captureBuf = [];
173
- this._origLog = console.log;
174
- console.log = (...args) => {
175
- const s = args.join(' ');
176
- if (this._active) {
177
- const clean = s.replace(/\x1b\[[0-9;]*m/g, '').substring(0, 100);
178
- if (clean.trim()) this.flashEvent('info', clean);
179
- } else {
180
- _capWrite(s + '\n');
181
- }
182
- };
183
- process.stdout.on('resize', () => this._onResize());
184
- this._drawStartupScreen();
185
- }
186
- }
187
-
188
- setVersion(v) { this._version = v; }
189
-
190
- startPhase(name) {
191
- this.phase = name;
192
- this.phaseDone = 0;
193
- this.phaseTotal = 0;
194
- this.phaseFrame = 0;
195
- if (!READY) {
196
- process.stdout.write(`\n ⏳ ${name}\n`);
197
- return;
198
- }
199
- if (this.phaseTimer) clearInterval(this.phaseTimer);
200
- this.phaseTimer = setInterval(() => {
201
- this.phaseFrame = (this.phaseFrame + 1) % SPIN_DOTS.length;
202
- this._renderPhase();
203
- }, 80);
204
- this._renderPhase();
205
- }
206
-
207
- updateProgress(done, total) {
208
- this.phaseDone = done;
209
- this.phaseTotal = total;
210
- if (!READY) return;
211
- this._renderProgress();
212
- }
213
-
214
- endPhase(name, ok = true) {
215
- if (this.phaseTimer) { clearInterval(this.phaseTimer); this.phaseTimer = null; }
216
- if (!READY) {
217
- const icon = ok ? `${C.green}✓${A.reset}` : `${C.red}✗${A.reset}`;
218
- console.log(` ${icon} ${name}`);
219
- return;
220
- }
221
- const icon = ok ? `${C.green}✓${A.reset}` : `${C.red}✗${A.reset}`;
222
- const label = `${icon} ${name}`;
223
- // Write to rows 5 (phase) and 7 (progress) with result
224
- const w = this._w;
225
- const V = C.border;
226
- const line = rpad(` ${label}`, w - 3);
227
- this._write(
228
- `${A.save}` +
229
- this._at(5, 1) + A.clearLine +
230
- `${V} ${line} ${V}${A.reset}` +
231
- this._at(7, 1) + A.clearLine +
232
- `${V} ${rpad('', w - 3)} ${V}${A.reset}` +
233
- A.restore
234
- );
235
- }
236
-
237
- flashEvent(type, msg) {
238
- const now = new Date();
239
- const ts = `${C.textDim}${String(now.getMinutes()).padStart(2,'0')}:${String(now.getSeconds()).padStart(2,'0')}${A.reset}`;
240
- this.events.unshift({ ts, type, msg, id: Date.now() });
241
- if (this.events.length > this.MAX_EVENTS) this.events.pop();
242
- this.dirtyEvents = true;
243
- }
244
-
245
- setWorkers(workers) {
246
- this.workers = workers;
247
- this.dirtyWorkers = new Set(workers.map((_, i) => i));
248
- this.dirtyStats = true;
249
- }
250
-
251
- markWorkerDirty(idx) {
252
- this.dirtyWorkers = new Set(this.workers.map((_, i) => i));
253
- this.dirtyStats = true;
254
- }
255
-
256
- setActive() {
257
- if (this._active) return;
258
- this._active = true;
259
-
260
- if (READY) {
261
- _captureActive = false;
262
- if (_origWrite) { process.stdout.write = _origWrite; _origWrite = null; }
263
- if (this._origLog) { console.log = this._origLog; this._origLog = null; }
264
- _captureBuf = [];
265
-
266
- this._write(A.eraseAll + A.hide);
267
- this._drawLiveView();
268
- this.dirtyWorkers.clear();
269
- this.dirtyEvents = false;
270
- this.dirtyStats = false;
271
-
272
- // Start pulse animation for active status indicators
273
- if (this._pulseTimer) clearInterval(this._pulseTimer);
274
- this._pulseTimer = setInterval(() => {
275
- this._pulseFrame = (this._pulseFrame + 1) % PULSE.length;
276
- this.dirtyStats = true; // pulse affects account rows
277
- }, 400);
278
-
279
- this._startRenderLoop();
280
- } else {
281
- if (this._origLog) { console.log = this._origLog; this._origLog = null; }
282
- }
283
- }
284
-
285
- shutdown(summary = {}) {
286
- this._shutdown = true;
287
- if (this._renderTimer) { clearInterval(this._renderTimer); this._renderTimer = null; }
288
- if (this._pulseTimer) { clearInterval(this._pulseTimer); this._pulseTimer = null; }
289
- if (this.phaseTimer) { clearInterval(this.phaseTimer); this.phaseTimer = null; }
290
-
291
- if (READY && _origWrite) { process.stdout.write = _origWrite; _origWrite = null; }
292
- if (this._origLog) { console.log = this._origLog; this._origLog = null; }
293
- this._write(A.show);
294
-
295
- const { totalCoins = 0, totalCmds = 0, totalSuccess = 0,
296
- workers = [], uptime = 0, memMB = 0 } = summary;
297
-
298
- const w = this._w;
299
- let out = A.eraseAll;
300
-
301
- // Top bar
302
- out += this._boxTop();
303
- out += this._statsBar();
304
- out += this._sep();
305
-
306
- // Column headers
307
- out += `${this._row(4, this._colHdr())}`;
308
- out += this._sep();
309
-
310
- // Account rows
311
- let row = 5;
312
- for (let i = 0; i < workers.length && row < this._h - 4; i++) {
313
- out += `${this._row(row++, this._accountLine(workers[i], i, workers))}`;
314
- }
315
-
316
- out += this._sep();
317
- row++;
318
- out += `${this._row(row++, this._totalLine(totalCoins, totalCmds, totalSuccess, uptime, memMB))}`;
319
- out += this._boxBot();
320
-
321
- this._write(out);
322
- }
323
-
324
- // ── Startup Screen ──────────────────────────────────────────────────────
325
-
326
- _drawStartupScreen() {
327
- const w = this._w;
328
- let out = A.eraseAll;
329
-
330
- // Top border
331
- out += `${this._at(1,1)}${C.border}${TL}${'─'.repeat(w-2)}${TR}${A.reset}\n`;
332
-
333
- // Version title
334
- const title = ` ⬡ DANKGRINDER v${this._version || '?'} `;
335
- out += `${this._at(2,1)}${C.border}${V} ${C.purple}${A.bold}${title}${rpad('', w - ansiLen(title) - 4)}${V}${A.reset}\n`;
336
-
337
- // Subtitle with version info
338
- const sub = `${C.textDim}24 commands · Auto-Recovery · Loss Limiter${A.reset}`;
339
- out += `${this._at(3,1)}${C.border}${V} ${sub}${rpad('', w - ansiLen(sub) - 4)}${V}${A.reset}\n`;
340
-
341
- out += `${this._at(4,1)}${C.border}${V}${'─'.repeat(w-2)}${V}${A.reset}\n`;
342
-
343
- // Spinner + phase label (row 5)
344
- out += `${this._at(5,1)}${C.border}${V}${rpad('', w-2)}${V}${A.reset}\n`;
345
-
346
- // Progress bar (row 7)
347
- out += `${this._at(7,1)}${C.border}${V}${rpad('', w-2)}${V}${A.reset}\n`;
348
-
349
- // Checkmarks / status area (row 9)
350
- out += `${this._at(9,1)}${C.border}${V}${rpad('', w-2)}${V}${A.reset}\n`;
351
-
352
- // Spacer
353
- out += `${this._at(11,1)}${C.border}${V}${rpad('', w-2)}${V}${A.reset}\n`;
354
-
355
- // Bottom border
356
- out += `${this._at(12,1)}${C.border}${BL}${'─'.repeat(w-2)}${BR}${A.reset}\n`;
357
-
358
- // Footer
359
- const hint = `${C.textDim}Initializing...${A.reset}`;
360
- out += `${this._at(13,1)}${C.border}${V} ${hint}${rpad('', w - ansiLen(hint) - 4)}${V}${A.reset}\n`;
361
-
362
- this._write(out);
363
- }
364
-
365
- _renderPhase() {
366
- if (!READY || !this.phase) return;
367
- const w = this._w;
368
- const V = C.border;
369
- const dot = SPIN_DOTS[this.phaseFrame];
370
- const label = ` ${dot} ${this.phase} `;
371
- const line = rpad(label, w - 3);
372
- this._write(
373
- `${A.save}` +
374
- `${this._at(5,1)}${A.clearLine}` +
375
- `${V} ${line} ${V}${A.reset}` +
376
- A.restore
377
- );
378
- }
379
-
380
- _renderProgress() {
381
- if (!READY) return;
382
- const w = this._w;
383
- const V = C.border;
384
- const { done, total } = { done: this.phaseDone, total: this.phaseTotal };
385
- const barW = Math.max(16, w - 40);
386
- const filled = total > 0 ? Math.round((done / total) * barW) : 0;
387
- const block = SPIN_BLOCK[this.phaseFrame % SPIN_BLOCK.length];
388
-
389
- // Filled bar with gradient: gold on left, purple on right
390
- const filledPart = filled > 0
391
- ? `${C.gold}${'█'.repeat(Math.max(1, filled - 1))}${C.green}${block}${A.reset}`
392
- : '';
393
- const emptyPart = barW - filled > 0
394
- ? `${C.borderDim}${'░'.repeat(Math.max(0, barW - filled))}${A.reset}`
395
- : '';
396
-
397
- const pct = total > 0 ? `${Math.round((done / total) * 100)}%` : '';
398
- const label = ` ${pct} `;
399
- const bar = `${filledPart}${emptyPart}${C.textDim}${label}${A.reset}`;
400
- const line = rpad(` ${bar}`, w - 3);
401
-
402
- this._write(
403
- `${A.save}` +
404
- `${this._at(7,1)}${A.clearLine}` +
405
- `${V} ${line} ${V}${A.reset}` +
406
- A.restore
407
- );
408
- }
409
-
410
- // ── Live View ─────────────────────────────────────────────────────────
411
-
412
- _drawLiveView() {
413
- let out = A.eraseAll;
414
- out += this._boxTop();
415
- out += this._statsBar();
416
- out += this._sep();
417
- out += `${this._row(4, this._colHdr())}`;
418
- out += this._sep();
419
- this._topRow = 5;
420
- out += this._accountRows();
421
- const sepRow = this._topRow + Math.min(this.windowSize, this.workers.length);
422
- out += `${this._at(sepRow,1)}${C.border}${V}${'─'.repeat(this._w-2)}${V}${A.reset}\n`;
423
- this._eventRow = sepRow + 1;
424
- out += this._eventFeed();
425
- out += this._boxBot();
426
- this._write(out);
427
- }
428
-
429
- _boxTop() {
430
- const w = this._w;
431
- let o = '';
432
- o += `${this._at(1,1)}${C.border}${TL}${'─'.repeat(w-2)}${TR}${A.reset}\n`;
433
- const title = ` ⬡ DANKGRINDER v${this._version || '?'} `;
434
- o += `${this._at(2,1)}${C.border}${V} ${C.purple}${A.bold}${title}${rpad('', w - ansiLen(title) - 4)}${V}${A.reset}\n`;
435
- o += `${this._at(3,1)}${C.border}${V}${'─'.repeat(w-2)}${V}${A.reset}\n`;
436
- return o;
437
- }
438
-
439
- _boxBot() {
440
- const w = this._w;
441
- const hint = `${C.textDim}↑↓ scroll Ctrl+C quit${A.reset}`;
442
- let o = '';
443
- o += `${this._at(this._footerRow,1)}${C.border}${BL}${'─'.repeat(w-2)}${BR}${A.reset}\n`;
444
- o += `${this._at(this._footerRow+1,1)}${C.border}${V} ${hint}${rpad('', w - ansiLen(hint) - 4)}${V}${A.reset}\n`;
445
- return o;
446
- }
447
-
448
- _sep() {
449
- const w = this._w;
450
- return `${C.border}${V}${'─'.repeat(w-2)}${V}${A.reset}\n`;
451
- }
452
-
453
- _row(r, content) {
454
- const w = this._w;
455
- return `${this._at(r,1)}${C.border}${V} ${content}${rpad('', w - ansiLen(content) - 4)} ${V}${A.reset}\n`;
456
- }
457
-
458
- _statsBar() {
459
- const w = this._w;
460
- const stats = this._buildStats();
461
- return `${this._row(4, stats)}`;
462
- }
463
-
464
- _buildStats() {
465
- let totalCoins = 0, totalCmds = 0, totalSuccess = 0, totalLs = 0;
466
- let paused = 0, active = 0;
467
-
468
- for (const wk of this.workers) {
469
- totalCoins += wk.stats?.coins || 0;
470
- totalCmds += wk.stats?.commands || 0;
471
- totalSuccess += wk.stats?.successes|| 0;
472
- if (wk._lifesavers != null) totalLs += wk._lifesavers;
473
- if (wk.running && !wk._tokenInvalid) {
474
- if (wk.paused || wk.dashboardPaused) paused++; else active++;
475
- }
476
- }
477
- const uptime = this._fmtUptime(Date.now() - this._startTime);
478
- const rate = totalCmds > 0 ? `${((totalSuccess / totalCmds) * 100).toFixed(0)}%` : '0%';
479
-
480
- const items = [
481
- [`⏱`, uptime, C.textDim],
482
- [`⬡`, `${this.workers.length} accounts`, C.textDim],
483
- [`⏣`, totalCoins.toLocaleString(), C.gold],
484
- [`⚡`, `${totalCmds} cmds`, C.textDim],
485
- [`📊`, `${rate} ok`, C.textDim],
486
- [`♥`, `${totalLs}`, C.pink],
487
- [`🟢`, `${active}`, C.green],
488
- [`🔴`, `${paused}`, C.red],
489
- ];
490
-
491
- return items.map(([icon, val, col]) =>
492
- `${C.textDim}${icon}${A.reset} ${col}${val}${A.reset}`
493
- ).join(` ${C.borderDim}│${A.reset} `);
494
- }
495
-
496
- _colHdr() {
497
- const cols = [
498
- `${C.purple}#`,
499
- `${C.purple}ACCOUNT`,
500
- `${C.purple}COINS`,
501
- `${C.purple}LV`,
502
- `${C.purple}♥`,
503
- `${C.purple}OK%`,
504
- `${C.purple}STATUS`,
505
- ];
506
- return cols.join(' ');
507
- }
508
-
509
- _accountRows() {
510
- let out = '';
511
- for (let i = 0; i < this.windowSize; i++) {
512
- const wkIdx = this.windowStart + i;
513
- const row = this._topRow + i;
514
- if (row > this._h - 5) break;
515
- if (wkIdx < this.workers.length) {
516
- out += `${this._row(row, this._accountLine(this.workers[wkIdx], wkIdx, this.workers))}`;
517
- } else {
518
- out += `${this._row(row, '')}`;
519
- }
520
- }
521
- return out;
522
- }
523
-
524
- _accountLine(wk, idx, workers) {
525
- const rank = coinRank(wk, workers);
526
- const pos = rank - 1; // 0-indexed
527
- const isActive = wk.running && !wk._tokenInvalid && !wk.paused && !wk.dashboardPaused;
528
- const ls = wk._lifesavers ?? '?';
529
- const rate = wk.stats?.commands > 0
530
- ? `${((wk.stats.successes / wk.stats.commands) * 100).toFixed(0)}%`
531
- : '0%';
532
-
533
- // Rank badge
534
- const medal = pos < 3 ? MEDALS[pos] : null;
535
- const rankColor = pos === 0 ? C.rank1 : pos === 1 ? C.rank2 : pos === 2 ? C.rank3 : null;
536
- const acctColor = C.ACCT[idx % C.ACCT.length];
537
-
538
- // Color based on rank (top 3 get medal color, rest get account color)
539
- const mainColor = isActive
540
- ? (rankColor || acctColor)
541
- : C.textFaint;
542
-
543
- const dimColor = isActive ? C.textDim : C.textFaint;
544
- const goldColor = isActive ? C.gold : C.textDim;
545
- const cyanColor = isActive ? C.cyan : C.textDim;
546
- const lsColor = isActive
547
- ? (ls === 0 ? C.red : ls <= 2 ? C.orange : C.pink)
548
- : C.textDim;
549
-
550
- // Status with pulse for active
551
- let dot, statusText;
552
- if (!wk.running || wk._tokenInvalid) {
553
- dot = `${C.textFaint}⚫${A.reset}`; statusText = `${C.textFaint}offline${A.reset}`;
554
- } else if (wk.paused || wk.dashboardPaused) {
555
- dot = `${C.red}🔴${A.reset}`; statusText = `${C.red}paused${A.reset}`;
556
- } else {
557
- const pulse = PULSE[this._pulseFrame];
558
- dot = `${C.green}${pulse}${A.reset}`; statusText = `${C.green}active${A.reset}`;
559
- }
560
-
561
- // Account name — truncate with care
562
- const rawName = wk.username || '?';
563
- const nameDisplay = rawName.length > 20
564
- ? rawName.substring(0, 17) + '...'
565
- : rawName;
566
-
567
- // Coins display
568
- const coins = (wk.stats?.coins || 0).toLocaleString();
569
- const sign = (wk.stats?.coins || 0) >= 0 ? '+' : '';
570
- const coinDisplay = `${goldColor}${sign}⏣${coins}${A.reset}`;
571
-
572
- // Current command (minimal)
573
- const cmd = (wk.lastStatus || '—').replace(/\x1b\[[0-9;]*m/g, '').substring(0, 14);
574
-
575
- // Build rank badge
576
- const rankBadge = medal
577
- ? `${rankColor}${A.bold}${medal}${A.reset}`
578
- : `${dimColor}${rank}th${A.reset}`;
579
-
580
- // Account name with subtle glow for top 3
581
- const nameBadge = pos < 3
582
- ? `${mainColor}${A.bold}${nameDisplay}${A.reset}`
583
- : `${mainColor}${nameDisplay}${A.reset}`;
584
-
585
- const parts = [
586
- rankBadge,
587
- nameBadge,
588
- coinDisplay,
589
- `${cyanColor}Lv.${String(wk._level ?? '?').padStart(3)}${A.reset}`,
590
- `${lsColor}♥${String(ls).padStart(2)}${A.reset}`,
591
- `${dimColor}${rate.padStart(5)}${A.reset}`,
592
- `${dot} ${statusText}`,
593
- `${dimColor}${cmd}`,
594
- ];
595
-
596
- return parts.join(' ');
597
- }
598
-
599
- _eventFeed() {
600
- let out = '';
601
- const visible = this.events.slice(0, Math.min(this.MAX_EVENTS, this._h - this._eventRow - 3));
602
- for (let i = 0; i < visible.length; i++) {
603
- const row = this._eventRow + i;
604
- if (row > this._h - 3) break;
605
- const e = visible[i];
606
- const color = e.type === 'death' ? C.red
607
- : e.type === 'lowls' ? C.orange
608
- : e.type === 'levelup'? C.cyan
609
- : e.type === 'success'? C.green
610
- : C.textDim;
611
- out += this._row(row, ` ${e.ts} ${color}${e.msg}${A.reset}`);
612
- }
613
- this._footerRow = this._eventRow + visible.length;
614
- return out;
615
- }
616
-
617
- _totalLine(totalCoins, totalCmds, totalSuccess, uptime, memMB) {
618
- const rate = totalCmds > 0 ? `${((totalSuccess / totalCmds) * 100).toFixed(0)}%` : '0%';
619
- const items = [
620
- [`💰`, `${C.gold}${A.bold}TOTAL:${A.reset}`, `${C.gold}${A.bold}⏣${totalCoins.toLocaleString()}${A.reset}`],
621
- [`⚡`, `${C.textDim}${totalCmds} cmds${A.reset}`, `${C.textDim}${rate} ok${A.reset}`],
622
- [`⏱`, `${C.textDim}${this._fmtUptime(uptime)}${A.reset}`, `${C.textDim}${memMB}MB${A.reset}`],
623
- ];
624
- return items.map(([, label, val]) => `${label} ${val}`).join(' ');
625
- }
626
-
627
- // ── Render loop ───────────────────────────────────────────────────────
628
-
629
- _render() {
630
- if (!READY || this._shutdown || !this._active) return;
631
-
632
- if (this.dirtyStats) {
633
- // Update stats bar (row 4)
634
- const stats = this._buildStats();
635
- const w = this._w;
636
- const V = C.border;
637
- this._write(
638
- `${this._at(4,1)}${V} ${rpad(stats, w-4)} ${V}${A.reset}`
639
- );
640
- // Re-draw account rows to update pulse/status
641
- for (let i = 0; i < this.windowSize; i++) {
642
- const wkIdx = this.windowStart + i;
643
- const row = this._topRow + i;
644
- if (row > this._h - 5) break;
645
- if (wkIdx < this.workers.length) {
646
- const line = this._accountLine(this.workers[wkIdx], wkIdx, this.workers);
647
- this._write(`${this._at(row,1)}${C.border}${V} ${rpad(line, w-4)} ${V}${A.reset}`);
648
- }
649
- }
650
- this.dirtyStats = false;
651
- }
652
-
653
- if (this.dirtyWorkers.size > 0) {
654
- const w = this._w;
655
- const V = C.border;
656
- for (const wkIdx of this.dirtyWorkers) {
657
- const localRow = wkIdx - this.windowStart;
658
- const row = this._topRow + localRow;
659
- if (row < this._topRow || row > this._h - 5) continue;
660
- if (wkIdx < this.workers.length) {
661
- const line = this._accountLine(this.workers[wkIdx], wkIdx, this.workers);
662
- this._write(`${this._at(row,1)}${C.border}${V} ${rpad(line, w-4)} ${V}${A.reset}`);
663
- } else {
664
- this._write(`${this._at(row,1)}${C.border}${V}${rpad('', w-2)}${V}${A.reset}`);
665
- }
666
- }
667
- }
668
-
669
- if (this.dirtyEvents) {
670
- this._write(this._eventFeed());
671
- }
672
-
673
- this.dirtyWorkers.clear();
674
- this.dirtyEvents = false;
675
- }
676
-
677
- _startRenderLoop() {
678
- if (this._renderTimer) clearInterval(this._renderTimer);
679
- this._renderTimer = setInterval(() => this._render(), 250);
680
- }
681
-
682
- // ── Internals ──────────────────────────────────────────────────────────
683
-
684
- _updateSize() {
685
- try {
686
- this._w = process.stdout.columns || 110;
687
- this._h = process.stdout.rows || 35;
688
- this.windowSize = Math.max(4, this._h - 11);
689
- } catch (_) {
690
- this._w = 110; this._h = 35; this.windowSize = 17;
691
- }
692
- }
693
-
694
- _onResize() {
695
- clearTimeout(this._resizeTimer);
696
- this._resizeTimer = setTimeout(() => {
697
- this._updateSize();
698
- if (this._active) {
699
- this._write(A.eraseAll);
700
- this._drawLiveView();
701
- }
702
- }, 100);
703
- }
704
-
705
- _at(r, c) { return `\x1b[${r};${c}H`; }
706
- _write(s) { if (s) process.stdout.write(s); }
707
-
708
- _fmtUptime(ms) {
709
- if (!ms) return '0s';
710
- const s = Math.floor(ms / 1000);
711
- if (s < 60) return `${s}s`;
712
- const m = Math.floor(s / 60);
713
- if (m < 60) return `${m}m ${s%60}s`;
714
- const h = Math.floor(m / 60);
715
- if (h < 24) return `${h}h ${m%60}m`;
716
- return `${Math.floor(h/24)}d ${h%24}h`;
717
- }
718
- }
719
-
720
- module.exports = new Terminal();