dankgrinder 7.78.0 → 7.81.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 CHANGED
@@ -1,253 +1,239 @@
1
1
  /**
2
- * terminal.js — Modern animated terminal renderer for DankGrinder
2
+ * terminal.js — Polished animated terminal renderer for DankGrinder
3
3
  *
4
- * Design goals:
5
- * - Single-line-per-account rows (scales to 10k+ accounts)
6
- * - Virtual window: only renders visible slice of accounts
7
- * - 4 FPS render loop with dirty-row tracking (no flicker)
8
- * - Graceful degradation: falls back to console.log if not a TTY
9
- * - Graceful mode: Ctrl+C always restores cursor + shows summary
10
- *
11
- * Layout (fixed row heights, live-updated):
12
- *
13
- * ┌─ DANKGRINDER v7.77.0 ─────────────────────────────────────────────────────┐
14
- * │ ⏱ 00:32 ⬡ 5 accounts ⏣ 47,230 ⚡ 12 cmd/m 📊 89% success │
15
- * ├─ ACCOUNTS ──────────────────────────────────────────────────────────────────┤
16
- * │ 💎 alice_99 ⏣ 12,450 L:24 ♥3 34cmds 🟢 grinding ⚔ adventure │
17
- * │ 💎 bob_trades ⏣ 8,920 L:18 ♥5 28cmds 🟢 grinding 🐟 fishing │
18
- * │ 💎 crypto_king ⏣ 5,100 L:31 ♥0 19cmds 🟡 depositing 💰 beg │
19
- * │ 💎 diamond_h.. ⏣ 3,780 L:12 ♥8 14cmds 🟢 grinding 🌾 farm │
20
- * │ 💎 moon_wallet ⏣ 1,200 L:9 ♥2 8cmds 🔴 paused ⚠ captcha │
21
- * ├─ EVENTS ────────────────────────────────────────────────────────────────────┤
22
- * │ 00:32 ⚔ alice_99 adventure: +⏣ 850 │
23
- * │ 00:31 💀 crypto_king DEATH — 0 lifesavers! crime/search disabled │
24
- * │ 00:30 ⬆️ bob_trades leveled up to Lv.19 │
25
- * │ 00:28 🌾 diamond_hands farm: +⏣ 2,100 │
26
- * └──────────────────────────────────────────────────────────────────────────────┘
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
27
12
  */
28
13
 
14
+ 'use strict';
15
+
29
16
  const READY = (() => {
30
- try {
31
- return process.stdout.isTTY && !process.env.NO_TERM;
32
- } catch (_) { return false; }
17
+ try { return !!process.stdout.isTTY && !process.env.NO_TERM; } catch (_) { return false; }
33
18
  })();
34
19
 
35
- // ── ANSI Helpers ────────────────────────────────────────────────────────────
20
+ // ── ANSI helpers ────────────────────────────────────────────────────────────
36
21
 
37
22
  const A = {
38
23
  reset: '\x1b[0m',
39
- bold: '\x1b[1m',
40
- dim: '\x1b[2m',
41
- italic: '\x1b[3m',
42
-
43
- black: '\x1b[30m',
44
- red: '\x1b[31m',
45
- green: '\x1b[32m',
46
- yellow: '\x1b[33m',
47
- blue: '\x1b[34m',
48
- magenta: '\x1b[35m',
49
- cyan: '\x1b[36m',
50
- white: '\x1b[37m',
51
-
52
- bgBlack: '\x1b[40m',
53
- bgRed: '\x1b[41m',
54
- bgGreen: '\x1b[42m',
55
- bgYellow: '\x1b[43m',
56
- bgBlue: '\x1b[44m',
57
- bgMagenta: '\x1b[45m',
58
- bgCyan: '\x1b[46m',
59
- bgWhite: '\x1b[47m',
60
-
61
- // 256-color RGB shortcuts
24
+ bold: '\x1b[1m',
25
+ dim: '\x1b[2m',
62
26
  rgb: (r, g, b) => `\x1b[38;2;${r};${g};${b}m`,
63
-
64
- // Cursor
27
+ eraseAll: '\x1b[3J\x1b[2J\x1b[H',
28
+ clearLine: '\x1b[2K',
65
29
  save: '\x1b7',
66
30
  restore: '\x1b8',
67
31
  hide: '\x1b[?25l',
68
32
  show: '\x1b[?25h',
69
- up: (n = 1) => `\x1b[${n}A`,
70
- down: (n = 1) => `\x1b[${n}B`,
71
- right: (n = 1) => `\x1b[${n}C`,
72
- left: (n = 1) => `\x1b[${n}D`,
73
- col: (n) => `\x1b[${n}G`,
74
- clear: '\x1b[2J',
75
- clearLine: '\x1b[2K',
76
- home: '\x1b[H',
77
-
78
- // 256-color palette
79
- purple: '\x1b[38;5;141m', // #8b5cf6
80
- pink: '\x1b[38;5;205m', // #ff5c93
81
- orange: '\x1b[38;5;214m', // #ff9f43
82
- teal: '\x1b[38;5;44m', // #2dd4bf
83
- lime: '\x1b[38;5;82m', // #4cd137
84
- crimson: '\x1b[38;5;196m', // #ff4757
85
- slate: '\x1b[38;5;245m', // #a0a0b0
86
- gold: '\x1b[38;5;220m', // #ffc233
87
- emerald: '\x1b[38;5;48m', // #2ed573
88
33
  };
89
34
 
90
- // ── Color scheme ────────────────────────────────────────────────────────────
35
+ // ── Palette ─────────────────────────────────────────────────────────────────
91
36
 
92
37
  const C = {
93
- header: A.purple,
94
- headerDim: A.rgb(100, 70, 180),
95
- border: A.rgb(60, 50, 90),
96
- borderDim: A.rgb(40, 35, 60),
97
-
98
- rowDefault: A.white,
99
- rowAlt: A.rgb(30, 28, 45),
100
- rowHighlight: A.rgb(25, 23, 40),
101
-
102
- name: A.rgb(255, 255, 255),
103
- nameDim: A.slate,
104
-
105
- coins: A.gold,
106
- level: A.cyan,
107
- lifesavers: A.pink,
108
- lifesaversLow: A.crimson,
109
- lifesaversMid: A.orange,
110
-
111
- statusActive: A.emerald, // 🟢 grinding
112
- statusPaused: A.crimson, // 🔴 paused
113
- statusWarning: A.orange, // 🟡 warning
114
- statusOffline: A.slate, // ⚫ offline
115
- statusConnecting: A.yellow, // 🟡 connecting
116
-
117
- cmdSuccess: A.emerald,
118
- cmdError: A.crimson,
119
- cmdEvent: A.purple,
120
- cmdWarn: A.orange,
121
-
122
- statLabel: A.slate,
123
- statValue: A.white,
124
-
125
- headerBg: A.rgb(20, 15, 35),
126
- rowBg1: '',
127
- rowBg2: A.rgb(25, 22, 38),
128
- rowBgPaused: A.rgb(40, 15, 20),
129
- rowBgWarning: A.rgb(40, 35, 15),
130
- rowBgDeath: A.rgb(50, 15, 15),
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
+ ],
131
71
  };
132
72
 
73
+ // ── Box-drawing ─────────────────────────────────────────────────────────────
74
+
75
+ const TL='╭', TR='╮', BL='╰', BR='╯', H='─', V='│';
76
+
133
77
  // ── Spinner frames ──────────────────────────────────────────────────────────
134
78
 
135
- const SPINNERS = {
136
- dots: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠿'],
137
- moon: ['🌑', '🌒', '🌓', '🌔', '🌕', '🌖', '🌗', '🌘'],
138
- arrows: ['←', '↖', '↑', '↗', '→', '↘', '↓', '↙'],
139
- pulse: ['', '', '', ''],
140
- };
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
+ }
141
128
 
142
- // ── Terminal Renderer ────────────────────────────────────────────────────────
129
+ // ── Terminal ───────────────────────────────────────────────────────────────
143
130
 
144
131
  class Terminal {
145
132
  constructor() {
146
- this.workers = [];
147
- this.events = []; // recent event feed (last N events)
148
- this.MAX_EVENTS = 5;
133
+ this.workers = [];
134
+ this.events = [];
135
+ this.MAX_EVENTS = 3;
149
136
 
150
- this.phaseName = '';
137
+ this.phase = '';
151
138
  this.phaseFrame = 0;
152
- this.phaseTimer = null;
139
+ this.phaseTimer = null;
140
+ this.phaseDone = 0;
141
+ this.phaseTotal = 0;
153
142
 
154
143
  this.dirtyWorkers = new Set();
155
- this.dirtyEvents = false;
156
- this.dirtyStats = true;
144
+ this.dirtyStats = true;
157
145
  this._renderTimer = null;
158
- this._startTime = 0;
146
+ this._startTime = 0;
147
+ this._active = false;
148
+ this._shutdown = false;
149
+ this._origLog = null;
159
150
 
160
- // Virtual window state
151
+ this._w = 110;
152
+ this._h = 35;
161
153
  this.windowStart = 0;
162
- this.windowSize = 10;
163
- this.scrollLocked = false; // true = auto-follow most active worker
164
-
165
- this._lineCount = 0;
166
- this._active = false;
167
- this._shutdown = false;
154
+ this.windowSize = 8;
168
155
 
169
- // Row offsets (set during render)
170
- this._headerRow = 0;
171
- this._statsRow = 0;
172
- this._accountsRow = 0;
173
- this._eventsRow = 0;
174
- this._footerRow = 0;
175
-
176
- // ── Scroll position state ──
177
- this._followWorkerIdx = -1; // -1 = follow newest active worker
178
-
179
- this._w = 80;
180
- this._h = 24;
181
- this._resizeTimer = null;
182
- this._col = (n) => `\x1b[${n}G`;
156
+ // Pulse animation state
157
+ this._pulseFrame = 0;
158
+ this._pulseTimer = null;
183
159
  }
184
160
 
185
161
  // ── Public API ──────────────────────────────────────────────────────────
186
162
 
187
163
  init(opts = {}) {
188
- if (opts.startTime) this._startTime = opts.startTime;
164
+ this._startTime = opts.startTime || Date.now();
189
165
  this.workers = opts.workers || [];
190
166
  this._updateSize();
191
167
 
192
168
  if (READY) {
193
- process.stdout.write(A.hide);
194
- process.stdout.on('resize', this._onResize.bind(this));
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());
195
184
  this._drawStartupScreen();
196
- } else {
197
- console.log(`${C.header}🚀 DankGrinder starting...${A.reset}`);
198
185
  }
199
186
  }
200
187
 
201
188
  setVersion(v) { this._version = v; }
202
189
 
203
190
  startPhase(name) {
204
- this.phaseName = name;
191
+ this.phase = name;
192
+ this.phaseDone = 0;
193
+ this.phaseTotal = 0;
205
194
  this.phaseFrame = 0;
206
- if (!READY) {
207
- console.log(` ${A.dim}⟳${A.reset} ${name}...`);
208
- return;
209
- }
195
+ if (!READY) return;
210
196
  if (this.phaseTimer) clearInterval(this.phaseTimer);
211
197
  this.phaseTimer = setInterval(() => {
212
- this.phaseFrame = (this.phaseFrame + 1) % SPINNERS.dots.length;
213
- this._redrawPhaseSpinner();
214
- }, 120);
215
- this._redrawPhaseSpinner();
198
+ this.phaseFrame = (this.phaseFrame + 1) % SPIN_DOTS.length;
199
+ this._renderPhase();
200
+ }, 80);
201
+ this._renderPhase();
216
202
  }
217
203
 
218
204
  updateProgress(done, total) {
205
+ this.phaseDone = done;
206
+ this.phaseTotal = total;
219
207
  if (!READY) return;
220
- this._writePhaseProgress(done, total);
208
+ this._renderProgress();
221
209
  }
222
210
 
223
211
  endPhase(name, ok = true) {
224
212
  if (this.phaseTimer) { clearInterval(this.phaseTimer); this.phaseTimer = null; }
225
213
  if (!READY) {
226
- const icon = ok ? `${C.header}✓${A.reset}` : `${A.crimson}✗${A.reset}`;
227
- console.log(` ${icon} ${name}`);
214
+ const icon = ok ? `${C.green}✓${A.reset}` : `${C.red}✗${A.reset}`;
215
+ console.log(` ${icon} ${name}`);
228
216
  return;
229
217
  }
230
- const icon = ok ? `${C.header}✓${A.reset}` : `${A.crimson}✗${A.reset}`;
231
- const frame = SPINNERS.dots[0];
232
- const cols = this._w;
233
- const label = ` ${frame} ${name}`;
234
- const pad = cols - this._ansiLen(label) - 4;
235
- process.stdout.write(
236
- A.save +
237
- this._cursor(2, 1) +
238
- A.clearLine +
239
- `${A.save}${A.home}` +
240
- `${label}${pad > 0 ? ' '.repeat(pad) : ''}` +
241
- `${this._rpad('', cols - this._ansiLen(` ${icon} ${name}`) - 2)}${icon} ${name}` +
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}` +
242
230
  A.restore
243
231
  );
244
- // Clear spinner line
245
- process.stdout.write(this._cursor(3, 1) + A.clearLine);
246
232
  }
247
233
 
248
234
  flashEvent(type, msg) {
249
235
  const now = new Date();
250
- const ts = `${A.dim}${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}${A.reset}`;
236
+ const ts = `${C.textDim}${String(now.getMinutes()).padStart(2,'0')}:${String(now.getSeconds()).padStart(2,'0')}${A.reset}`;
251
237
  this.events.unshift({ ts, type, msg, id: Date.now() });
252
238
  if (this.events.length > this.MAX_EVENTS) this.events.pop();
253
239
  this.dirtyEvents = true;
@@ -260,560 +246,471 @@ class Terminal {
260
246
  }
261
247
 
262
248
  markWorkerDirty(idx) {
263
- this.dirtyWorkers.add(idx);
249
+ this.dirtyWorkers = new Set(this.workers.map((_, i) => i));
250
+ this.dirtyStats = true;
264
251
  }
265
252
 
266
253
  setActive() {
267
254
  if (this._active) return;
268
255
  this._active = true;
269
- if (!READY) return;
270
-
271
- // Clear startup, draw live view
272
- process.stdout.write(A.clear + A.home);
273
- this._drawLiveView();
274
- this._startRenderLoop();
275
- }
276
-
277
- scrollBy(delta) {
278
- if (!READY || this._shutdown) return;
279
- const max = Math.max(0, this.workers.length - this.windowSize);
280
- this.windowStart = Math.max(0, Math.min(max, this.windowStart + delta));
281
- this._followWorkerIdx = -1; // manual scroll cancels auto-follow
282
- this.dirtyWorkers = new Set();
283
- for (let i = this.windowStart; i < this.windowStart + this.windowSize; i++) {
284
- if (i < this.workers.length) this.dirtyWorkers.add(i);
285
- }
286
- }
287
256
 
288
- followWorker(idx) {
289
- this._followWorkerIdx = idx;
290
- this.scrollLocked = true;
291
- // Ensure the worker is visible
292
- if (idx < this.windowStart) {
293
- this.windowStart = idx;
294
- } else if (idx >= this.windowStart + this.windowSize) {
295
- this.windowStart = idx - this.windowSize + 1;
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; }
296
279
  }
297
280
  }
298
281
 
299
282
  shutdown(summary = {}) {
300
283
  this._shutdown = true;
301
284
  if (this._renderTimer) { clearInterval(this._renderTimer); this._renderTimer = null; }
302
- if (this._phaseTimer) { clearInterval(this._phaseTimer); this._phaseTimer = null; }
285
+ if (this._pulseTimer) { clearInterval(this._pulseTimer); this._pulseTimer = null; }
286
+ if (this.phaseTimer) { clearInterval(this.phaseTimer); this.phaseTimer = null; }
303
287
 
304
- process.stdout.write(A.show);
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);
305
291
 
306
- if (!READY) {
307
- this._printSummaryPlain(summary);
308
- return;
309
- }
310
-
311
- // Move to clean area below any existing output
312
- const cols = this._w;
313
292
  const { totalCoins = 0, totalCmds = 0, totalSuccess = 0,
314
293
  workers = [], uptime = 0, memMB = 0 } = summary;
315
294
 
316
- const b = C.border;
317
- const h = C.header;
318
- const g = C.statValue;
319
- const dim = A.dim;
320
- const r = A.reset;
321
-
322
- const sep = (c) => `${b}${c.repeat(cols - 2)}${r}`;
323
- const bar = `${b}${'─'.repeat(cols - 2)}${r}`;
324
- const icon = (e) => `${h}${e}${r}`;
325
-
326
- let out = '';
327
- out += `${A.clear}${A.home}`;
328
- out += `${A.save}`;
329
-
330
- // Box title
331
- out += `${this._at(1, 1)}${bar}`;
332
- out += `${this._at(2, 1)}${b} ${h}${A.bold}⬡ DANKGRINDER — Session Summary${r} ${b}${'─'.repeat(Math.max(0, cols - 37))}${r}`;
333
- out += `${this._at(3, 1)}${bar}`;
334
-
335
- // Per-account summary
336
- let row = 4;
337
- for (const wk of workers) {
338
- const coins = `+⏣ ${(wk.stats?.coins || 0).toLocaleString()}`;
339
- const cmds = `${wk.stats?.commands || 0}cmds`;
340
- const rate = wk.stats?.commands > 0
341
- ? `${((wk.stats.successes / wk.stats.commands) * 100).toFixed(0)}%`
342
- : '0%';
343
- const ls = wk._lifesavers ?? '?';
344
- const lv = wk._level ?? '?';
345
- const line = ` ${icon('💎')} ${g}${A.bold}${(wk.username || '?').padEnd(18)}${r} ${C.coins}${coins}${r} ${dim}${cmds}${r} ${g}${rate} OK${r} ${C.level}Lv.${lv}${r} ${C.lifesavers}♥${ls}${r}`;
346
- out += `${this._at(row++, 1)}${b}${this._rpad(line, cols - 2)}${r}`;
347
- }
348
-
349
- row++; // blank row
350
- out += `${this._at(row++, 1)}${bar}`;
295
+ const w = this._w;
296
+ let out = A.eraseAll;
351
297
 
352
- // Totals
353
- const totalLine = ` ${icon('💰')} ${h}${A.bold}Total:${r} ${C.coins}${A.bold}⏣ ${totalCoins.toLocaleString()}${r} ${dim}${totalCmds} cmds${r} ${g}${totalSuccess}% OK${r} ${dim}${this._fmtUptime(uptime)}${r} ${dim}${memMB}MB${r}`;
354
- out += `${this._at(row++, 1)}${b}${this._rpad(totalLine, cols - 2)}${r}`;
355
- out += `${this._at(row++, 1)}${sep('─')}`;
356
- out += `${A.restore}`;
357
- out += `${A.show}`;
298
+ // Top bar
299
+ out += this._boxTop();
300
+ out += this._statsBar();
301
+ out += this._sep();
358
302
 
359
- process.stdout.write(out);
360
- setTimeout(() => {}, 100);
361
- }
303
+ // Column headers
304
+ out += `${this._row(4, this._colHdr())}`;
305
+ out += this._sep();
362
306
 
363
- // ── Internal ────────────────────────────────────────────────────────────
364
-
365
- _updateSize() {
366
- try {
367
- this._w = process.stdout.columns || 80;
368
- this._h = process.stdout.rows || 24;
369
- // Window size: leave room for header (3 rows) + stats (2) + events footer (3) + border (2)
370
- this.windowSize = Math.max(3, this._h - 12);
371
- } catch (_) {
372
- this._w = 80; this._h = 24; this.windowSize = 10;
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))}`;
373
311
  }
374
- }
375
312
 
376
- _onResize() {
377
- if (this._resizeTimer) clearTimeout(this._resizeTimer);
378
- this._resizeTimer = setTimeout(() => {
379
- this._updateSize();
380
- if (this._active) {
381
- this._clearScreen();
382
- this._drawLiveView();
383
- }
384
- }, 100);
385
- }
386
-
387
- _ansiLen(s) {
388
- // Fast ANSI strip — just count escape sequences by looking for \x1b[
389
- let len = 0;
390
- let i = 0;
391
- const str = String(s);
392
- while (i < str.length) {
393
- if (str.charCodeAt(i) === 0x1b && str[i + 1] === '[') {
394
- let j = i + 2;
395
- while (j < str.length && str[j] !== 'm') j++;
396
- i = j + 1;
397
- } else {
398
- len++;
399
- i++;
400
- }
401
- }
402
- return len;
403
- }
313
+ out += this._sep();
314
+ row++;
315
+ out += `${this._row(row++, this._totalLine(totalCoins, totalCmds, totalSuccess, uptime, memMB))}`;
316
+ out += this._boxBot();
404
317
 
405
- _rpad(s, width) {
406
- const len = this._ansiLen(s);
407
- const pad = width > len ? width - len : 0;
408
- return s + (pad > 0 ? ' '.repeat(pad) : '');
409
- }
410
-
411
- _cursor(row, col) {
412
- // Position cursor at absolute row/col (1-indexed)
413
- return `\x1b[${row};${col}H`;
318
+ this._write(out);
414
319
  }
415
320
 
416
- _at(row, col) {
417
- return `\x1b[${row};${col}H`;
418
- }
321
+ // ── Startup Screen ──────────────────────────────────────────────────────
419
322
 
420
- _write(str) {
421
- process.stdout.write(str);
422
- }
323
+ _drawStartupScreen() {
324
+ const w = this._w;
325
+ let out = A.eraseAll;
423
326
 
424
- _ansi(s, code) { return `${code}${s}${A.reset}`; }
425
- _bold(s) { return `${A.bold}${s}${A.reset}`; }
426
- _dim(s) { return `${A.dim}${s}${A.reset}`; }
327
+ // Top border
328
+ out += `${this._at(1,1)}${C.border}${TL}${'─'.repeat(w-2)}${TR}${A.reset}\n`;
427
329
 
428
- _fmtUptime(ms) {
429
- if (!ms) return '0s';
430
- const s = Math.floor(ms / 1000);
431
- if (s < 60) return `${s}s`;
432
- const m = Math.floor(s / 60);
433
- if (m < 60) return `${m}m ${s % 60}s`;
434
- const h = Math.floor(m / 60);
435
- if (h < 24) return `${h}h ${m % 60}m`;
436
- const d = Math.floor(h / 24);
437
- return `${d}d ${h % 24}h`;
438
- }
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`;
439
333
 
440
- _fmtCoins(n) {
441
- if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
442
- if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
443
- return String(n);
444
- }
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`;
445
337
 
446
- _buildAccountRow(wk, idx) {
447
- const w = this._w;
448
- // Row layout (all in one line):
449
- // #N 💎 username ⏣coins Lv.N ♥N Ncmds ● status ⚔ current_cmd
450
- // We need to fit within `w` columns, truncate username if needed
451
-
452
- const num = `${idx + 1}.`.padEnd(3);
453
- const username = (wk.username || '?').substring(0, 16).padEnd(17);
454
- const coins = `⏣${this._fmtCoins(wk.stats?.coins || 0)}`.padEnd(8);
455
- const level = `Lv.${wk._level ?? '?'}`.padEnd(5);
456
- const ls = wk._lifesavers ?? '?';
457
- const lifesaversColor = ls === 0 ? C.lifesaversLow
458
- : ls <= 2 ? C.lifesaversMid
459
- : C.lifesavers;
460
- const lifesavers = `${lifesaversColor}♥${ls}`.padEnd(4);
461
- const cmds = `${wk.stats?.commands || 0}cmds`.padEnd(7);
462
-
463
- // Status dot + text
464
- let statusDot, statusText, rowBg;
465
- if (!wk.running || wk._tokenInvalid) {
466
- statusDot = '⚫'; statusText = 'offline'; rowBg = C.rowBgPaused;
467
- } else if (wk.paused) {
468
- statusDot = '🔴'; statusText = 'paused'; rowBg = C.rowBgPaused;
469
- } else if (wk.dashboardPaused) {
470
- statusDot = '🟠'; statusText = 'dashboard'; rowBg = C.rowBgWarning;
471
- } else {
472
- statusDot = '🟢'; statusText = 'grinding'; rowBg = '';
473
- }
338
+ out += `${this._at(4,1)}${C.border}${V}${'─'.repeat(w-2)}${V}${A.reset}\n`;
474
339
 
475
- // Current command
476
- const cmd = (wk.lastStatus || '').substring(0, 20).padEnd(21);
477
- const successRate = wk.stats?.commands > 0
478
- ? `${((wk.stats.successes / wk.stats.commands) * 100).toFixed(0)}%`
479
- : '0%';
340
+ // Spinner + phase label (row 5)
341
+ out += `${this._at(5,1)}${C.border}${V}${rpad('', w-2)}${V}${A.reset}\n`;
480
342
 
481
- const parts = [
482
- `${C.header}${num}${A.reset}`,
483
- `${C.name}${username}${A.reset}`,
484
- `${C.coins}${coins}${A.reset}`,
485
- `${C.level}${level}${A.reset}`,
486
- `${lifesaversColor}${lifesavers}${A.reset}`,
487
- `${C.statValue}${cmds}${A.reset}`,
488
- `${C.statValue}${successRate}`.padEnd(5) + A.reset,
489
- `${statusDot} ${statusText}`.padEnd(14),
490
- `${this._dim(cmd)}`,
491
- ];
343
+ // Progress bar (row 7)
344
+ out += `${this._at(7,1)}${C.border}${V}${rpad('', w-2)}${V}${A.reset}\n`;
492
345
 
493
- // Join and pad to screen width
494
- const line = parts.join(' ');
495
- return this._rpad(line, w);
496
- }
346
+ // Checkmarks / status area (row 9)
347
+ out += `${this._at(9,1)}${C.border}${V}${rpad('', w-2)}${V}${A.reset}\n`;
497
348
 
498
- _drawStartupScreen() {
499
- const w = this._w;
500
- const b = C.border;
501
- const h = C.header;
502
- const g = C.statValue;
503
- const dim = A.dim;
504
- const r = A.reset;
349
+ // Spacer
350
+ out += `${this._at(11,1)}${C.border}${V}${rpad('', w-2)}${V}${A.reset}\n`;
505
351
 
506
- const sep = (c) => `${b}${c.repeat(w - 2)}${r}`;
352
+ // Bottom border
353
+ out += `${this._at(12,1)}${C.border}${BL}${'─'.repeat(w-2)}${BR}${A.reset}\n`;
507
354
 
508
- // Figure out how many rows we need for the spinner
509
- const spinnerRow = 3;
510
- const progressRow = 5;
511
- const readyRow = 7;
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`;
512
358
 
513
- let out = '';
514
- out += `${A.clear}${A.home}`;
515
-
516
- // Title box
517
- const titleText = ` ⬡ DANKGRINDER v${this._version || '?'} `;
518
- const titlePad = w - 2 - this._ansiLen(titleText);
519
- out += `${this._at(1, 1)}${sep('─')}`;
520
- out += `${this._at(2, 1)}${b} ${h}${A.bold}${titleText}${r}${b}${'─'.repeat(Math.max(0, titlePad))}${r}`;
521
- out += `${this._at(3, 1)}${sep('─')}`;
522
-
523
- // Spinner + phase text
524
- out += `${this._at(5, 1)}${b}${' '.repeat(w - 2)}${r}`;
525
- out += `${this._at(6, 1)}${b}${' '.repeat(w - 2)}${r}`;
526
-
527
- // Footer hint
528
- out += `${this._at(8, 1)}${sep('─')}`;
529
- const hint = `${dim}Starting up...${r}`;
530
- out += `${this._at(9, 1)}${b} ${hint}${' '.repeat(Math.max(0, w - 2 - this._ansiLen(hint)))}${r}`;
531
- out += `${this._at(10, 1)}${sep('─')}`;
532
-
533
- this._lineCount = 10;
534
359
  this._write(out);
535
360
  }
536
361
 
537
- _redrawPhaseSpinner() {
538
- if (!READY) return;
539
- const frame = SPINNERS.dots[this.phaseFrame];
540
- const label = this.phaseName;
362
+ _renderPhase() {
363
+ if (!READY || !this.phase) return;
541
364
  const w = this._w;
542
-
543
- const line = ` ${frame} ${label}...`;
544
- const pad = w - this._ansiLen(line) - 2;
545
- const padded = line + (pad > 0 ? ' '.repeat(pad) : '');
546
-
547
- const b = C.border;
548
- const r = A.reset;
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);
549
369
  this._write(
550
370
  `${A.save}` +
551
- `${this._cursor(6, 1)}` +
552
- `${b}${padded}${r}` +
553
- `${A.restore}`
371
+ `${this._at(5,1)}${A.clearLine}` +
372
+ `${V} ${line} ${V}${A.reset}` +
373
+ A.restore
554
374
  );
555
375
  }
556
376
 
557
- _writePhaseProgress(done, total) {
558
- if (!READY) {
559
- console.log(` ${A.dim} → ${done}/${total}${A.reset}`);
560
- return;
561
- }
377
+ _renderProgress() {
378
+ if (!READY) return;
562
379
  const w = this._w;
563
- const b = C.border;
564
- const h = C.header;
565
- const dim = A.dim;
566
- const r = A.reset;
567
- const barLen = Math.max(10, w - 30);
568
- const filled = Math.round((done / total) * barLen);
569
- const empty = barLen - filled;
570
-
571
- const bar = `${h}${'█'.repeat(filled)}${dim}${'░'.repeat(empty)}${r}`;
572
- const label = ` ${done}/${total} `;
573
- const line = `${bar} ${label}`;
574
- const pad = w - this._ansiLen(line) - 2;
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);
575
398
 
576
399
  this._write(
577
400
  `${A.save}` +
578
- `${this._cursor(6, 1)}` +
579
- `${b}${line}${' '.repeat(Math.max(0, pad))}${r}` +
580
- `${A.restore}`
401
+ `${this._at(7,1)}${A.clearLine}` +
402
+ `${V} ${line} ${V}${A.reset}` +
403
+ A.restore
581
404
  );
582
405
  }
583
406
 
584
- _clearScreen() {
585
- this._write(`${A.clear}${A.home}`);
586
- }
407
+ // ── Live View ─────────────────────────────────────────────────────────
587
408
 
588
409
  _drawLiveView() {
589
- if (!READY) return;
590
- this._drawHeader();
591
- this._drawAccounts();
592
- this._drawEvents();
593
- this._drawFooter();
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);
594
424
  }
595
425
 
596
- _drawHeader() {
426
+ _boxTop() {
597
427
  const w = this._w;
598
- const b = C.border;
599
- const h = C.header;
600
- const g = C.statValue;
601
- const dim = A.dim;
602
- const r = A.reset;
603
-
604
- const sep = `${b}${'─'.repeat(w - 2)}${r}`;
428
+ let o = '';
429
+ o += `${this._at(1,1)}${C.border}${TL}${'─'.repeat(w-2)}${TR}${A.reset}\n`;
605
430
  const title = ` ⬡ DANKGRINDER v${this._version || '?'} `;
606
- const titlePad = w - 2 - this._ansiLen(title);
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
+ }
607
435
 
608
- this._headerRow = 1;
609
- this._write(`${this._at(1, 1)}${sep}`);
610
- this._write(`${this._at(2, 1)}${b} ${h}${A.bold}${title}${r}${b}${'─'.repeat(Math.max(0, titlePad))}${r}`);
611
- this._write(`${this._at(3, 1)}${sep}`);
612
- this._statsRow = 4;
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
+ }
613
449
 
614
- // Stats bar
615
- const stats = this._buildStatsLine();
616
- this._write(`${this._at(4, 1)}${b}${this._rpad(' ' + stats, w - 2)}${r}`);
617
- this._accountsRow = 5;
618
- this._write(`${this._at(5, 1)}${sep}`);
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`;
619
453
  }
620
454
 
621
- _buildStatsLine() {
622
- const g = C.statValue;
623
- const dim = A.dim;
624
- const h = C.header;
625
- const coins = C.coins;
455
+ _statsBar() {
456
+ const w = this._w;
457
+ const stats = this._buildStats();
458
+ return `${this._row(4, stats)}`;
459
+ }
626
460
 
461
+ _buildStats() {
627
462
  let totalCoins = 0, totalCmds = 0, totalSuccess = 0, totalLs = 0;
628
- let pausedCount = 0, activeCount = 0;
463
+ let paused = 0, active = 0;
629
464
 
630
465
  for (const wk of this.workers) {
631
- totalCoins += wk.stats?.coins || 0;
632
- totalCmds += wk.stats?.commands || 0;
633
- totalSuccess += wk.stats?.successes || 0;
466
+ totalCoins += wk.stats?.coins || 0;
467
+ totalCmds += wk.stats?.commands || 0;
468
+ totalSuccess += wk.stats?.successes|| 0;
634
469
  if (wk._lifesavers != null) totalLs += wk._lifesavers;
635
- if (!wk.running || wk._tokenInvalid) {}
636
- else if (wk.paused || wk.dashboardPaused) pausedCount++;
637
- else activeCount++;
470
+ if (wk.running && !wk._tokenInvalid) {
471
+ if (wk.paused || wk.dashboardPaused) paused++; else active++;
472
+ }
638
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
+ ];
639
487
 
640
- const uptime = this._fmtUptime(Date.now() - (this._startTime || Date.now()));
641
- const rate = totalCmds > 0 ? ((totalSuccess / totalCmds) * 100).toFixed(0) : 0;
642
-
643
- return [
644
- `${dim}⏱${A.reset} ${g}${uptime}${A.reset}`,
645
- `${dim}⬡${A.reset} ${g}${this.workers.length}${A.reset} ${dim}accounts${A.reset}`,
646
- `${coins}⏣${A.reset} ${g}${totalCoins.toLocaleString()}${A.reset}`,
647
- `${dim}⚡${A.reset} ${g}${totalCmds}${A.reset} ${dim}cmds${A.reset}`,
648
- `${dim}📊${A.reset} ${g}${rate}%${A.reset}`,
649
- `${C.lifesavers}♥${A.reset} ${g}${totalLs}${A.reset}`,
650
- `${h}🟢${A.reset} ${g}${activeCount}${A.reset} ${h}🔴${A.reset} ${g}${pausedCount}${A.reset}`,
651
- ].join(' ');
488
+ return items.map(([icon, val, col]) =>
489
+ `${C.textDim}${icon}${A.reset} ${col}${val}${A.reset}`
490
+ ).join(` ${C.borderDim}│${A.reset} `);
652
491
  }
653
492
 
654
- _drawAccounts() {
655
- const w = this._w;
656
- const b = C.border;
657
- const dim = A.dim;
658
- const r = A.reset;
659
- const sep = `${b}${'─'.repeat(w - 2)}${r}`;
660
-
661
- const visible = this.workers.slice(this.windowStart, this.windowStart + this.windowSize);
662
-
663
- // Column header
493
+ _colHdr() {
664
494
  const cols = [
665
- `${C.header}#${A.reset}`,
666
- `${C.header}ACCOUNT${A.reset}`,
667
- `${C.header}COINS${A.reset}`,
668
- `${C.header}LV${A.reset}`,
669
- `${C.header}♥${A.reset}`,
670
- `${C.header}CMDS${A.reset}`,
671
- `${C.header}OK%${A.reset}`,
672
- `${C.header}STATUS${A.reset}`,
673
- `${C.header}CURRENT${A.reset}`,
674
- ].join(' ');
675
- this._write(`${this._at(this._accountsRow, 1)}${b}${this._rpad(' ' + cols, w - 2)}${r}`);
676
- this._write(`${this._at(this._accountsRow + 1, 1)}${sep}`);
677
-
678
- // Worker rows
679
- for (let i = 0; i < this.windowSize; i++) {
680
- const row = this._accountsRow + 2 + i;
681
- if (row > this._h - 4) break;
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
+ }
682
505
 
683
- if (i < visible.length) {
684
- const wk = visible[i];
685
- const line = this._buildAccountRow(wk, this.windowStart + i);
686
- this._write(`${this._at(row, 1)}${b}${line}${r}`);
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))}`;
687
514
  } else {
688
- this._write(`${this._at(row, 1)}${b}${' '.repeat(w - 2)}${r}`);
515
+ out += `${this._row(row, '')}`;
689
516
  }
690
517
  }
691
- this._eventsRow = this._accountsRow + 2 + this.windowSize + 1;
518
+ return out;
692
519
  }
693
520
 
694
- _drawEvents() {
695
- const w = this._w;
696
- const b = C.border;
697
- const dim = A.dim;
698
- const r = A.reset;
699
- const sep = `${b}${'─'.repeat(w - 2)}${r}`;
700
-
701
- if (this._eventsRow > this._h - 4) return;
702
- this._write(`${this._at(this._eventsRow, 1)}${sep}`);
703
-
704
- const visibleEvents = this.events.slice(0, Math.min(this.MAX_EVENTS, this._h - this._eventsRow - 3));
705
- for (let i = 0; i < visibleEvents.length; i++) {
706
- const row = this._eventsRow + 1 + i;
707
- if (row > this._h - 2) break;
708
- const evt = visibleEvents[i];
709
- const typeColor = evt.type === 'error' || evt.type === 'death' ? C.cmdError
710
- : evt.type === 'warn' || evt.type === 'lowls' ? C.cmdWarn
711
- : evt.type === 'success' || evt.type === 'levelup' ? C.cmdSuccess
712
- : C.cmdEvent;
713
-
714
- const line = ` ${dim}${evt.ts}${A.reset} ${typeColor}${evt.msg}${A.reset}`;
715
- this._write(`${this._at(row, 1)}${b}${this._rpad(line, w - 2)}${r}`);
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}`;
716
556
  }
717
- this._footerRow = this._eventsRow + 1 + visibleEvents.length;
718
- }
719
557
 
720
- _drawFooter() {
721
- const w = this._w;
722
- const b = C.border;
723
- const dim = A.dim;
724
- const r = A.reset;
725
- const sep = `${b}${'─'.repeat(w - 2)}${r}`;
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}`;
726
568
 
727
- if (this._footerRow > this._h - 1) this._footerRow = this._h - 2;
728
- this._write(`${this._at(this._footerRow, 1)}${sep}`);
569
+ // Current command (minimal)
570
+ const cmd = (wk.lastStatus || '—').replace(/\x1b\[[0-9;]*m/g, '').substring(0, 14);
729
571
 
730
- const hint = `${dim}↑↓ scroll · j/k navigate · Ctrl+C quit${r}`;
731
- this._write(`${this._at(this._footerRow + 1, 1)}${b} ${hint}${' '.repeat(Math.max(0, w - 2 - this._ansiLen(hint)))}${r}`);
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;
732
612
  }
733
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
+
734
626
  _render() {
735
627
  if (!READY || this._shutdown || !this._active) return;
736
628
 
737
- // Auto-follow newest active worker
738
- if (this._followWorkerIdx >= 0 && this._followWorkerIdx < this.workers.length) {
739
- const target = this._followWorkerIdx;
740
- if (target < this.windowStart) {
741
- this.windowStart = target;
742
- this.dirtyWorkers = new Set();
743
- for (let i = this.windowStart; i < this.windowStart + this.windowSize; i++) {
744
- if (i < this.workers.length) this.dirtyWorkers.add(i);
745
- }
746
- } else if (target >= this.windowStart + this.windowSize) {
747
- this.windowStart = target - this.windowSize + 1;
748
- this.dirtyWorkers = new Set();
749
- for (let i = this.windowStart; i < this.windowStart + this.windowSize; i++) {
750
- if (i < this.workers.length) this.dirtyWorkers.add(i);
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}`);
751
645
  }
752
646
  }
753
- }
754
-
755
- // Redraw everything on resize changes
756
- if (this.dirtyStats) {
757
- this._drawHeader();
758
- this.dirtyWorkers = new Set(this.workers.map((_, i) => i));
647
+ this.dirtyStats = false;
759
648
  }
760
649
 
761
650
  if (this.dirtyWorkers.size > 0) {
762
651
  const w = this._w;
763
- const b = C.border;
764
- const r = A.reset;
765
- const sep = `${b}${'─'.repeat(w - 2)}${r}`;
766
-
767
- for (const idx of this.dirtyWorkers) {
768
- const localIdx = idx - this.windowStart;
769
- const row = this._accountsRow + 2 + localIdx;
770
- if (row < this._accountsRow + 2 || row > this._h - 4) continue;
771
-
772
- if (idx < this.workers.length) {
773
- const line = this._buildAccountRow(this.workers[idx], idx);
774
- this._write(`${this._at(row, 1)}${b}${line}${r}`);
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}`);
775
660
  } else {
776
- this._write(`${this._at(row, 1)}${b}${' '.repeat(w - 2)}${r}`);
661
+ this._write(`${this._at(row,1)}${C.border}${V}${rpad('', w-2)}${V}${A.reset}`);
777
662
  }
778
663
  }
779
664
  }
780
665
 
781
666
  if (this.dirtyEvents) {
782
- this._drawEvents();
783
- this._drawFooter();
667
+ this._write(this._eventFeed());
784
668
  }
785
669
 
786
670
  this.dirtyWorkers.clear();
787
671
  this.dirtyEvents = false;
788
- this.dirtyStats = false;
789
672
  }
790
673
 
791
674
  _startRenderLoop() {
792
675
  if (this._renderTimer) clearInterval(this._renderTimer);
793
- this._renderTimer = setInterval(() => this._render(), 250); // 4 FPS
676
+ this._renderTimer = setInterval(() => this._render(), 250);
794
677
  }
795
678
 
796
- _printSummaryPlain(summary) {
797
- const { totalCoins = 0, totalCmds = 0, totalSuccess = 0,
798
- workers = [], uptime = 0, memMB = 0 } = summary;
799
- const b = '═'.repeat(60);
800
- console.log('');
801
- console.log(` ${'═'.repeat(60)}`);
802
- console.log(` ⬡ DANKGRINDER Session Summary`);
803
- console.log(` ${'─'.repeat(60)}`);
804
- for (const wk of workers) {
805
- const rate = wk.stats?.commands > 0
806
- ? `${((wk.stats.successes / wk.stats.commands) * 100).toFixed(0)}%`
807
- : '0%';
808
- console.log(
809
- ` 💎 ${(wk.username || '?').padEnd(18)} +⏣ ${(wk.stats?.coins || 0).toLocaleString().padStart(8)} ` +
810
- `${(wk.stats?.commands || 0)}cmds ${rate} ♥${wk._lifesavers ?? '?'} Lv.${wk._level ?? '?'}`
811
- );
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;
812
688
  }
813
- console.log(` ${'─'.repeat(60)}`);
814
- console.log(` 💰 Total: ⏣ ${totalCoins.toLocaleString()} ${totalCmds}cmds ${totalSuccess}%OK ${this._fmtUptime(uptime)} ${memMB}MB`);
815
- console.log(` ${'═'.repeat(60)}`);
816
- console.log('');
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`;
817
714
  }
818
715
  }
819
716