dankgrinder 7.82.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,717 +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) return;
196
- if (this.phaseTimer) clearInterval(this.phaseTimer);
197
- this.phaseTimer = setInterval(() => {
198
- this.phaseFrame = (this.phaseFrame + 1) % SPIN_DOTS.length;
199
- this._renderPhase();
200
- }, 80);
201
- this._renderPhase();
202
- }
203
-
204
- updateProgress(done, total) {
205
- this.phaseDone = done;
206
- this.phaseTotal = total;
207
- if (!READY) return;
208
- this._renderProgress();
209
- }
210
-
211
- endPhase(name, ok = true) {
212
- if (this.phaseTimer) { clearInterval(this.phaseTimer); this.phaseTimer = null; }
213
- if (!READY) {
214
- const icon = ok ? `${C.green}✓${A.reset}` : `${C.red}✗${A.reset}`;
215
- console.log(` ${icon} ${name}`);
216
- return;
217
- }
218
- const icon = ok ? `${C.green}✓${A.reset}` : `${C.red}✗${A.reset}`;
219
- const label = `${icon} ${name}`;
220
- // Write to rows 5 (phase) and 7 (progress) with result
221
- const w = this._w;
222
- const V = C.border;
223
- const line = rpad(` ${label}`, w - 3);
224
- this._write(
225
- `${A.save}` +
226
- this._at(5, 1) + A.clearLine +
227
- `${V} ${line} ${V}${A.reset}` +
228
- this._at(7, 1) + A.clearLine +
229
- `${V} ${rpad('', w - 3)} ${V}${A.reset}` +
230
- A.restore
231
- );
232
- }
233
-
234
- flashEvent(type, msg) {
235
- const now = new Date();
236
- const ts = `${C.textDim}${String(now.getMinutes()).padStart(2,'0')}:${String(now.getSeconds()).padStart(2,'0')}${A.reset}`;
237
- this.events.unshift({ ts, type, msg, id: Date.now() });
238
- if (this.events.length > this.MAX_EVENTS) this.events.pop();
239
- this.dirtyEvents = true;
240
- }
241
-
242
- setWorkers(workers) {
243
- this.workers = workers;
244
- this.dirtyWorkers = new Set(workers.map((_, i) => i));
245
- this.dirtyStats = true;
246
- }
247
-
248
- markWorkerDirty(idx) {
249
- this.dirtyWorkers = new Set(this.workers.map((_, i) => i));
250
- this.dirtyStats = true;
251
- }
252
-
253
- setActive() {
254
- if (this._active) return;
255
- this._active = true;
256
-
257
- if (READY) {
258
- _captureActive = false;
259
- if (_origWrite) { process.stdout.write = _origWrite; _origWrite = null; }
260
- if (this._origLog) { console.log = this._origLog; this._origLog = null; }
261
- _captureBuf = [];
262
-
263
- this._write(A.eraseAll + A.hide);
264
- this._drawLiveView();
265
- this.dirtyWorkers.clear();
266
- this.dirtyEvents = false;
267
- this.dirtyStats = false;
268
-
269
- // Start pulse animation for active status indicators
270
- if (this._pulseTimer) clearInterval(this._pulseTimer);
271
- this._pulseTimer = setInterval(() => {
272
- this._pulseFrame = (this._pulseFrame + 1) % PULSE.length;
273
- this.dirtyStats = true; // pulse affects account rows
274
- }, 400);
275
-
276
- this._startRenderLoop();
277
- } else {
278
- if (this._origLog) { console.log = this._origLog; this._origLog = null; }
279
- }
280
- }
281
-
282
- shutdown(summary = {}) {
283
- this._shutdown = true;
284
- if (this._renderTimer) { clearInterval(this._renderTimer); this._renderTimer = null; }
285
- if (this._pulseTimer) { clearInterval(this._pulseTimer); this._pulseTimer = null; }
286
- if (this.phaseTimer) { clearInterval(this.phaseTimer); this.phaseTimer = null; }
287
-
288
- if (READY && _origWrite) { process.stdout.write = _origWrite; _origWrite = null; }
289
- if (this._origLog) { console.log = this._origLog; this._origLog = null; }
290
- this._write(A.show);
291
-
292
- const { totalCoins = 0, totalCmds = 0, totalSuccess = 0,
293
- workers = [], uptime = 0, memMB = 0 } = summary;
294
-
295
- const w = this._w;
296
- let out = A.eraseAll;
297
-
298
- // Top bar
299
- out += this._boxTop();
300
- out += this._statsBar();
301
- out += this._sep();
302
-
303
- // Column headers
304
- out += `${this._row(4, this._colHdr())}`;
305
- out += this._sep();
306
-
307
- // Account rows
308
- let row = 5;
309
- for (let i = 0; i < workers.length && row < this._h - 4; i++) {
310
- out += `${this._row(row++, this._accountLine(workers[i], i, workers))}`;
311
- }
312
-
313
- out += this._sep();
314
- row++;
315
- out += `${this._row(row++, this._totalLine(totalCoins, totalCmds, totalSuccess, uptime, memMB))}`;
316
- out += this._boxBot();
317
-
318
- this._write(out);
319
- }
320
-
321
- // ── Startup Screen ──────────────────────────────────────────────────────
322
-
323
- _drawStartupScreen() {
324
- const w = this._w;
325
- let out = A.eraseAll;
326
-
327
- // Top border
328
- out += `${this._at(1,1)}${C.border}${TL}${'─'.repeat(w-2)}${TR}${A.reset}\n`;
329
-
330
- // Version title
331
- const title = ` ⬡ DANKGRINDER v${this._version || '?'} `;
332
- out += `${this._at(2,1)}${C.border}${V} ${C.purple}${A.bold}${title}${rpad('', w - ansiLen(title) - 4)}${V}${A.reset}\n`;
333
-
334
- // Subtitle with version info
335
- const sub = `${C.textDim}24 commands · Auto-Recovery · Loss Limiter${A.reset}`;
336
- out += `${this._at(3,1)}${C.border}${V} ${sub}${rpad('', w - ansiLen(sub) - 4)}${V}${A.reset}\n`;
337
-
338
- out += `${this._at(4,1)}${C.border}${V}${'─'.repeat(w-2)}${V}${A.reset}\n`;
339
-
340
- // Spinner + phase label (row 5)
341
- out += `${this._at(5,1)}${C.border}${V}${rpad('', w-2)}${V}${A.reset}\n`;
342
-
343
- // Progress bar (row 7)
344
- out += `${this._at(7,1)}${C.border}${V}${rpad('', w-2)}${V}${A.reset}\n`;
345
-
346
- // Checkmarks / status area (row 9)
347
- out += `${this._at(9,1)}${C.border}${V}${rpad('', w-2)}${V}${A.reset}\n`;
348
-
349
- // Spacer
350
- out += `${this._at(11,1)}${C.border}${V}${rpad('', w-2)}${V}${A.reset}\n`;
351
-
352
- // Bottom border
353
- out += `${this._at(12,1)}${C.border}${BL}${'─'.repeat(w-2)}${BR}${A.reset}\n`;
354
-
355
- // Footer
356
- const hint = `${C.textDim}Initializing...${A.reset}`;
357
- out += `${this._at(13,1)}${C.border}${V} ${hint}${rpad('', w - ansiLen(hint) - 4)}${V}${A.reset}\n`;
358
-
359
- this._write(out);
360
- }
361
-
362
- _renderPhase() {
363
- if (!READY || !this.phase) return;
364
- const w = this._w;
365
- const V = C.border;
366
- const dot = SPIN_DOTS[this.phaseFrame];
367
- const label = ` ${dot} ${this.phase} `;
368
- const line = rpad(label, w - 3);
369
- this._write(
370
- `${A.save}` +
371
- `${this._at(5,1)}${A.clearLine}` +
372
- `${V} ${line} ${V}${A.reset}` +
373
- A.restore
374
- );
375
- }
376
-
377
- _renderProgress() {
378
- if (!READY) return;
379
- const w = this._w;
380
- const V = C.border;
381
- const { done, total } = { done: this.phaseDone, total: this.phaseTotal };
382
- const barW = Math.max(16, w - 40);
383
- const filled = total > 0 ? Math.round((done / total) * barW) : 0;
384
- const block = SPIN_BLOCK[this.phaseFrame % SPIN_BLOCK.length];
385
-
386
- // Filled bar with gradient: gold on left, purple on right
387
- const filledPart = filled > 0
388
- ? `${C.gold}${'█'.repeat(Math.max(1, filled - 1))}${C.green}${block}${A.reset}`
389
- : '';
390
- const emptyPart = barW - filled > 0
391
- ? `${C.borderDim}${'░'.repeat(Math.max(0, barW - filled))}${A.reset}`
392
- : '';
393
-
394
- const pct = total > 0 ? `${Math.round((done / total) * 100)}%` : '';
395
- const label = ` ${pct} `;
396
- const bar = `${filledPart}${emptyPart}${C.textDim}${label}${A.reset}`;
397
- const line = rpad(` ${bar}`, w - 3);
398
-
399
- this._write(
400
- `${A.save}` +
401
- `${this._at(7,1)}${A.clearLine}` +
402
- `${V} ${line} ${V}${A.reset}` +
403
- A.restore
404
- );
405
- }
406
-
407
- // ── Live View ─────────────────────────────────────────────────────────
408
-
409
- _drawLiveView() {
410
- let out = A.eraseAll;
411
- out += this._boxTop();
412
- out += this._statsBar();
413
- out += this._sep();
414
- out += `${this._row(4, this._colHdr())}`;
415
- out += this._sep();
416
- this._topRow = 5;
417
- out += this._accountRows();
418
- const sepRow = this._topRow + Math.min(this.windowSize, this.workers.length);
419
- out += `${this._at(sepRow,1)}${C.border}${V}${'─'.repeat(this._w-2)}${V}${A.reset}\n`;
420
- this._eventRow = sepRow + 1;
421
- out += this._eventFeed();
422
- out += this._boxBot();
423
- this._write(out);
424
- }
425
-
426
- _boxTop() {
427
- const w = this._w;
428
- let o = '';
429
- o += `${this._at(1,1)}${C.border}${TL}${'─'.repeat(w-2)}${TR}${A.reset}\n`;
430
- const title = ` ⬡ DANKGRINDER v${this._version || '?'} `;
431
- o += `${this._at(2,1)}${C.border}${V} ${C.purple}${A.bold}${title}${rpad('', w - ansiLen(title) - 4)}${V}${A.reset}\n`;
432
- o += `${this._at(3,1)}${C.border}${V}${'─'.repeat(w-2)}${V}${A.reset}\n`;
433
- return o;
434
- }
435
-
436
- _boxBot() {
437
- const w = this._w;
438
- const hint = `${C.textDim}↑↓ scroll Ctrl+C quit${A.reset}`;
439
- let o = '';
440
- o += `${this._at(this._footerRow,1)}${C.border}${BL}${'─'.repeat(w-2)}${BR}${A.reset}\n`;
441
- o += `${this._at(this._footerRow+1,1)}${C.border}${V} ${hint}${rpad('', w - ansiLen(hint) - 4)}${V}${A.reset}\n`;
442
- return o;
443
- }
444
-
445
- _sep() {
446
- const w = this._w;
447
- return `${C.border}${V}${'─'.repeat(w-2)}${V}${A.reset}\n`;
448
- }
449
-
450
- _row(r, content) {
451
- const w = this._w;
452
- return `${this._at(r,1)}${C.border}${V} ${content}${rpad('', w - ansiLen(content) - 4)} ${V}${A.reset}\n`;
453
- }
454
-
455
- _statsBar() {
456
- const w = this._w;
457
- const stats = this._buildStats();
458
- return `${this._row(4, stats)}`;
459
- }
460
-
461
- _buildStats() {
462
- let totalCoins = 0, totalCmds = 0, totalSuccess = 0, totalLs = 0;
463
- let paused = 0, active = 0;
464
-
465
- for (const wk of this.workers) {
466
- totalCoins += wk.stats?.coins || 0;
467
- totalCmds += wk.stats?.commands || 0;
468
- totalSuccess += wk.stats?.successes|| 0;
469
- if (wk._lifesavers != null) totalLs += wk._lifesavers;
470
- if (wk.running && !wk._tokenInvalid) {
471
- if (wk.paused || wk.dashboardPaused) paused++; else active++;
472
- }
473
- }
474
- const uptime = this._fmtUptime(Date.now() - this._startTime);
475
- const rate = totalCmds > 0 ? `${((totalSuccess / totalCmds) * 100).toFixed(0)}%` : '0%';
476
-
477
- const items = [
478
- [`⏱`, uptime, C.textDim],
479
- [`⬡`, `${this.workers.length} accounts`, C.textDim],
480
- [`⏣`, totalCoins.toLocaleString(), C.gold],
481
- [`⚡`, `${totalCmds} cmds`, C.textDim],
482
- [`📊`, `${rate} ok`, C.textDim],
483
- [`♥`, `${totalLs}`, C.pink],
484
- [`🟢`, `${active}`, C.green],
485
- [`🔴`, `${paused}`, C.red],
486
- ];
487
-
488
- return items.map(([icon, val, col]) =>
489
- `${C.textDim}${icon}${A.reset} ${col}${val}${A.reset}`
490
- ).join(` ${C.borderDim}│${A.reset} `);
491
- }
492
-
493
- _colHdr() {
494
- const cols = [
495
- `${C.purple}#`,
496
- `${C.purple}ACCOUNT`,
497
- `${C.purple}COINS`,
498
- `${C.purple}LV`,
499
- `${C.purple}♥`,
500
- `${C.purple}OK%`,
501
- `${C.purple}STATUS`,
502
- ];
503
- return cols.join(' ');
504
- }
505
-
506
- _accountRows() {
507
- let out = '';
508
- for (let i = 0; i < this.windowSize; i++) {
509
- const wkIdx = this.windowStart + i;
510
- const row = this._topRow + i;
511
- if (row > this._h - 5) break;
512
- if (wkIdx < this.workers.length) {
513
- out += `${this._row(row, this._accountLine(this.workers[wkIdx], wkIdx, this.workers))}`;
514
- } else {
515
- out += `${this._row(row, '')}`;
516
- }
517
- }
518
- return out;
519
- }
520
-
521
- _accountLine(wk, idx, workers) {
522
- const rank = coinRank(wk, workers);
523
- const pos = rank - 1; // 0-indexed
524
- const isActive = wk.running && !wk._tokenInvalid && !wk.paused && !wk.dashboardPaused;
525
- const ls = wk._lifesavers ?? '?';
526
- const rate = wk.stats?.commands > 0
527
- ? `${((wk.stats.successes / wk.stats.commands) * 100).toFixed(0)}%`
528
- : '0%';
529
-
530
- // Rank badge
531
- const medal = pos < 3 ? MEDALS[pos] : null;
532
- const rankColor = pos === 0 ? C.rank1 : pos === 1 ? C.rank2 : pos === 2 ? C.rank3 : null;
533
- const acctColor = C.ACCT[idx % C.ACCT.length];
534
-
535
- // Color based on rank (top 3 get medal color, rest get account color)
536
- const mainColor = isActive
537
- ? (rankColor || acctColor)
538
- : C.textFaint;
539
-
540
- const dimColor = isActive ? C.textDim : C.textFaint;
541
- const goldColor = isActive ? C.gold : C.textDim;
542
- const cyanColor = isActive ? C.cyan : C.textDim;
543
- const lsColor = isActive
544
- ? (ls === 0 ? C.red : ls <= 2 ? C.orange : C.pink)
545
- : C.textDim;
546
-
547
- // Status with pulse for active
548
- let dot, statusText;
549
- if (!wk.running || wk._tokenInvalid) {
550
- dot = `${C.textFaint}⚫${A.reset}`; statusText = `${C.textFaint}offline${A.reset}`;
551
- } else if (wk.paused || wk.dashboardPaused) {
552
- dot = `${C.red}🔴${A.reset}`; statusText = `${C.red}paused${A.reset}`;
553
- } else {
554
- const pulse = PULSE[this._pulseFrame];
555
- dot = `${C.green}${pulse}${A.reset}`; statusText = `${C.green}active${A.reset}`;
556
- }
557
-
558
- // Account name — truncate with care
559
- const rawName = wk.username || '?';
560
- const nameDisplay = rawName.length > 20
561
- ? rawName.substring(0, 17) + '...'
562
- : rawName;
563
-
564
- // Coins display
565
- const coins = (wk.stats?.coins || 0).toLocaleString();
566
- const sign = (wk.stats?.coins || 0) >= 0 ? '+' : '';
567
- const coinDisplay = `${goldColor}${sign}⏣${coins}${A.reset}`;
568
-
569
- // Current command (minimal)
570
- const cmd = (wk.lastStatus || '—').replace(/\x1b\[[0-9;]*m/g, '').substring(0, 14);
571
-
572
- // Build rank badge
573
- const rankBadge = medal
574
- ? `${rankColor}${A.bold}${medal}${A.reset}`
575
- : `${dimColor}${rank}th${A.reset}`;
576
-
577
- // Account name with subtle glow for top 3
578
- const nameBadge = pos < 3
579
- ? `${mainColor}${A.bold}${nameDisplay}${A.reset}`
580
- : `${mainColor}${nameDisplay}${A.reset}`;
581
-
582
- const parts = [
583
- rankBadge,
584
- nameBadge,
585
- coinDisplay,
586
- `${cyanColor}Lv.${String(wk._level ?? '?').padStart(3)}${A.reset}`,
587
- `${lsColor}♥${String(ls).padStart(2)}${A.reset}`,
588
- `${dimColor}${rate.padStart(5)}${A.reset}`,
589
- `${dot} ${statusText}`,
590
- `${dimColor}${cmd}`,
591
- ];
592
-
593
- return parts.join(' ');
594
- }
595
-
596
- _eventFeed() {
597
- let out = '';
598
- const visible = this.events.slice(0, Math.min(this.MAX_EVENTS, this._h - this._eventRow - 3));
599
- for (let i = 0; i < visible.length; i++) {
600
- const row = this._eventRow + i;
601
- if (row > this._h - 3) break;
602
- const e = visible[i];
603
- const color = e.type === 'death' ? C.red
604
- : e.type === 'lowls' ? C.orange
605
- : e.type === 'levelup'? C.cyan
606
- : e.type === 'success'? C.green
607
- : C.textDim;
608
- out += this._row(row, ` ${e.ts} ${color}${e.msg}${A.reset}`);
609
- }
610
- this._footerRow = this._eventRow + visible.length;
611
- return out;
612
- }
613
-
614
- _totalLine(totalCoins, totalCmds, totalSuccess, uptime, memMB) {
615
- const rate = totalCmds > 0 ? `${((totalSuccess / totalCmds) * 100).toFixed(0)}%` : '0%';
616
- const items = [
617
- [`💰`, `${C.gold}${A.bold}TOTAL:${A.reset}`, `${C.gold}${A.bold}⏣${totalCoins.toLocaleString()}${A.reset}`],
618
- [`⚡`, `${C.textDim}${totalCmds} cmds${A.reset}`, `${C.textDim}${rate} ok${A.reset}`],
619
- [`⏱`, `${C.textDim}${this._fmtUptime(uptime)}${A.reset}`, `${C.textDim}${memMB}MB${A.reset}`],
620
- ];
621
- return items.map(([, label, val]) => `${label} ${val}`).join(' ');
622
- }
623
-
624
- // ── Render loop ───────────────────────────────────────────────────────
625
-
626
- _render() {
627
- if (!READY || this._shutdown || !this._active) return;
628
-
629
- if (this.dirtyStats) {
630
- // Update stats bar (row 4)
631
- const stats = this._buildStats();
632
- const w = this._w;
633
- const V = C.border;
634
- this._write(
635
- `${this._at(4,1)}${V} ${rpad(stats, w-4)} ${V}${A.reset}`
636
- );
637
- // Re-draw account rows to update pulse/status
638
- for (let i = 0; i < this.windowSize; i++) {
639
- const wkIdx = this.windowStart + i;
640
- const row = this._topRow + i;
641
- if (row > this._h - 5) break;
642
- if (wkIdx < this.workers.length) {
643
- const line = this._accountLine(this.workers[wkIdx], wkIdx, this.workers);
644
- this._write(`${this._at(row,1)}${C.border}${V} ${rpad(line, w-4)} ${V}${A.reset}`);
645
- }
646
- }
647
- this.dirtyStats = false;
648
- }
649
-
650
- if (this.dirtyWorkers.size > 0) {
651
- const w = this._w;
652
- const V = C.border;
653
- for (const wkIdx of this.dirtyWorkers) {
654
- const localRow = wkIdx - this.windowStart;
655
- const row = this._topRow + localRow;
656
- if (row < this._topRow || row > this._h - 5) continue;
657
- if (wkIdx < this.workers.length) {
658
- const line = this._accountLine(this.workers[wkIdx], wkIdx, this.workers);
659
- this._write(`${this._at(row,1)}${C.border}${V} ${rpad(line, w-4)} ${V}${A.reset}`);
660
- } else {
661
- this._write(`${this._at(row,1)}${C.border}${V}${rpad('', w-2)}${V}${A.reset}`);
662
- }
663
- }
664
- }
665
-
666
- if (this.dirtyEvents) {
667
- this._write(this._eventFeed());
668
- }
669
-
670
- this.dirtyWorkers.clear();
671
- this.dirtyEvents = false;
672
- }
673
-
674
- _startRenderLoop() {
675
- if (this._renderTimer) clearInterval(this._renderTimer);
676
- this._renderTimer = setInterval(() => this._render(), 250);
677
- }
678
-
679
- // ── Internals ──────────────────────────────────────────────────────────
680
-
681
- _updateSize() {
682
- try {
683
- this._w = process.stdout.columns || 110;
684
- this._h = process.stdout.rows || 35;
685
- this.windowSize = Math.max(4, this._h - 11);
686
- } catch (_) {
687
- this._w = 110; this._h = 35; this.windowSize = 17;
688
- }
689
- }
690
-
691
- _onResize() {
692
- clearTimeout(this._resizeTimer);
693
- this._resizeTimer = setTimeout(() => {
694
- this._updateSize();
695
- if (this._active) {
696
- this._write(A.eraseAll);
697
- this._drawLiveView();
698
- }
699
- }, 100);
700
- }
701
-
702
- _at(r, c) { return `\x1b[${r};${c}H`; }
703
- _write(s) { if (s) process.stdout.write(s); }
704
-
705
- _fmtUptime(ms) {
706
- if (!ms) return '0s';
707
- const s = Math.floor(ms / 1000);
708
- if (s < 60) return `${s}s`;
709
- const m = Math.floor(s / 60);
710
- if (m < 60) return `${m}m ${s%60}s`;
711
- const h = Math.floor(m / 60);
712
- if (h < 24) return `${h}h ${m%60}m`;
713
- return `${Math.floor(h/24)}d ${h%24}h`;
714
- }
715
- }
716
-
717
- module.exports = new Terminal();