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/dashboard.js +269 -4
- package/lib/grinder.js +617 -157
- package/lib/rawLogger.js +2 -12
- package/package.json +1 -1
- package/lib/terminal.js +0 -720
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();
|