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/grinder.js +21 -27
- package/lib/terminal.js +519 -622
- package/package.json +1 -1
package/lib/terminal.js
CHANGED
|
@@ -1,253 +1,239 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* terminal.js —
|
|
2
|
+
* terminal.js — Polished animated terminal renderer for DankGrinder
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
|
20
|
+
// ── ANSI helpers ────────────────────────────────────────────────────────────
|
|
36
21
|
|
|
37
22
|
const A = {
|
|
38
23
|
reset: '\x1b[0m',
|
|
39
|
-
bold:
|
|
40
|
-
dim:
|
|
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
|
-
|
|
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
|
-
// ──
|
|
35
|
+
// ── Palette ─────────────────────────────────────────────────────────────────
|
|
91
36
|
|
|
92
37
|
const C = {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
129
|
+
// ── Terminal ───────────────────────────────────────────────────────────────
|
|
143
130
|
|
|
144
131
|
class Terminal {
|
|
145
132
|
constructor() {
|
|
146
|
-
this.workers
|
|
147
|
-
this.events
|
|
148
|
-
this.MAX_EVENTS =
|
|
133
|
+
this.workers = [];
|
|
134
|
+
this.events = [];
|
|
135
|
+
this.MAX_EVENTS = 3;
|
|
149
136
|
|
|
150
|
-
this.
|
|
137
|
+
this.phase = '';
|
|
151
138
|
this.phaseFrame = 0;
|
|
152
|
-
this.phaseTimer
|
|
139
|
+
this.phaseTimer = null;
|
|
140
|
+
this.phaseDone = 0;
|
|
141
|
+
this.phaseTotal = 0;
|
|
153
142
|
|
|
154
143
|
this.dirtyWorkers = new Set();
|
|
155
|
-
this.
|
|
156
|
-
this.dirtyStats = true;
|
|
144
|
+
this.dirtyStats = true;
|
|
157
145
|
this._renderTimer = null;
|
|
158
|
-
this._startTime
|
|
146
|
+
this._startTime = 0;
|
|
147
|
+
this._active = false;
|
|
148
|
+
this._shutdown = false;
|
|
149
|
+
this._origLog = null;
|
|
159
150
|
|
|
160
|
-
|
|
151
|
+
this._w = 110;
|
|
152
|
+
this._h = 35;
|
|
161
153
|
this.windowStart = 0;
|
|
162
|
-
this.windowSize
|
|
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
|
-
//
|
|
170
|
-
this.
|
|
171
|
-
this.
|
|
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
|
-
|
|
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(
|
|
194
|
-
process.stdout.
|
|
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.
|
|
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) %
|
|
213
|
-
this.
|
|
214
|
-
},
|
|
215
|
-
this.
|
|
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.
|
|
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.
|
|
227
|
-
console.log(` ${icon}
|
|
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.
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
A.clearLine +
|
|
239
|
-
`${
|
|
240
|
-
|
|
241
|
-
`${
|
|
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 = `${
|
|
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.
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
this.
|
|
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.
|
|
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
|
|
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
|
|
317
|
-
|
|
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
|
-
//
|
|
353
|
-
|
|
354
|
-
out +=
|
|
355
|
-
out +=
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
303
|
+
// Column headers
|
|
304
|
+
out += `${this._row(4, this._colHdr())}`;
|
|
305
|
+
out += this._sep();
|
|
362
306
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
this.
|
|
379
|
-
|
|
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
|
-
|
|
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
|
-
|
|
417
|
-
return `\x1b[${row};${col}H`;
|
|
418
|
-
}
|
|
321
|
+
// ── Startup Screen ──────────────────────────────────────────────────────
|
|
419
322
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
323
|
+
_drawStartupScreen() {
|
|
324
|
+
const w = this._w;
|
|
325
|
+
let out = A.eraseAll;
|
|
423
326
|
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
476
|
-
|
|
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
|
-
|
|
482
|
-
|
|
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
|
-
//
|
|
494
|
-
|
|
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
|
-
|
|
499
|
-
|
|
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
|
-
|
|
352
|
+
// Bottom border
|
|
353
|
+
out += `${this._at(12,1)}${C.border}${BL}${'─'.repeat(w-2)}${BR}${A.reset}\n`;
|
|
507
354
|
|
|
508
|
-
//
|
|
509
|
-
const
|
|
510
|
-
|
|
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
|
-
|
|
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
|
|
544
|
-
const
|
|
545
|
-
const
|
|
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.
|
|
552
|
-
`${
|
|
553
|
-
|
|
371
|
+
`${this._at(5,1)}${A.clearLine}` +
|
|
372
|
+
`${V} ${line} ${V}${A.reset}` +
|
|
373
|
+
A.restore
|
|
554
374
|
);
|
|
555
375
|
}
|
|
556
376
|
|
|
557
|
-
|
|
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
|
|
564
|
-
const
|
|
565
|
-
const
|
|
566
|
-
const
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
const
|
|
574
|
-
|
|
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.
|
|
579
|
-
`${
|
|
580
|
-
|
|
401
|
+
`${this._at(7,1)}${A.clearLine}` +
|
|
402
|
+
`${V} ${line} ${V}${A.reset}` +
|
|
403
|
+
A.restore
|
|
581
404
|
);
|
|
582
405
|
}
|
|
583
406
|
|
|
584
|
-
|
|
585
|
-
this._write(`${A.clear}${A.home}`);
|
|
586
|
-
}
|
|
407
|
+
// ── Live View ─────────────────────────────────────────────────────────
|
|
587
408
|
|
|
588
409
|
_drawLiveView() {
|
|
589
|
-
|
|
590
|
-
this.
|
|
591
|
-
this.
|
|
592
|
-
this.
|
|
593
|
-
this.
|
|
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
|
-
|
|
426
|
+
_boxTop() {
|
|
597
427
|
const w = this._w;
|
|
598
|
-
|
|
599
|
-
|
|
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
|
-
|
|
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
|
-
|
|
609
|
-
this.
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
this.
|
|
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
|
-
|
|
615
|
-
const
|
|
616
|
-
|
|
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
|
-
|
|
622
|
-
const
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
|
|
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
|
|
463
|
+
let paused = 0, active = 0;
|
|
629
464
|
|
|
630
465
|
for (const wk of this.workers) {
|
|
631
|
-
totalCoins
|
|
632
|
-
totalCmds
|
|
633
|
-
totalSuccess += wk.stats?.successes
|
|
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 (
|
|
636
|
-
|
|
637
|
-
|
|
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
|
-
|
|
641
|
-
|
|
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
|
-
|
|
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.
|
|
666
|
-
`${C.
|
|
667
|
-
`${C.
|
|
668
|
-
`${C.
|
|
669
|
-
`${C.
|
|
670
|
-
`${C.
|
|
671
|
-
`${C.
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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
|
-
|
|
515
|
+
out += `${this._row(row, '')}`;
|
|
689
516
|
}
|
|
690
517
|
}
|
|
691
|
-
|
|
518
|
+
return out;
|
|
692
519
|
}
|
|
693
520
|
|
|
694
|
-
|
|
695
|
-
const
|
|
696
|
-
const
|
|
697
|
-
const
|
|
698
|
-
const
|
|
699
|
-
const
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
const
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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
|
-
|
|
721
|
-
const
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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
|
-
|
|
728
|
-
|
|
569
|
+
// Current command (minimal)
|
|
570
|
+
const cmd = (wk.lastStatus || '—').replace(/\x1b\[[0-9;]*m/g, '').substring(0, 14);
|
|
729
571
|
|
|
730
|
-
|
|
731
|
-
|
|
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
|
-
|
|
738
|
-
|
|
739
|
-
const
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
this.
|
|
749
|
-
|
|
750
|
-
|
|
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
|
|
764
|
-
const
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
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,
|
|
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.
|
|
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);
|
|
676
|
+
this._renderTimer = setInterval(() => this._render(), 250);
|
|
794
677
|
}
|
|
795
678
|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
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
|
|