dankgrinder 7.79.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 CHANGED
@@ -2761,11 +2761,6 @@ async function start(apiKey, apiUrl, opts = {}) {
2761
2761
  const CLOUD_MODE = opts.cloud === true;
2762
2762
  startTime = Date.now();
2763
2763
 
2764
- if (CLOUD_MODE) {
2765
- // In cloud mode, API_KEY is the CLOUD_ADMIN_KEY โ€” not used for user auth.
2766
- // Per-account keys are fetched per-account from /api/cloud/grinders.
2767
- console.log('๐ŸŒฅ๏ธ Starting in CLOUD MODE โ€” grinding all cloud-enabled accounts');
2768
- }
2769
2764
  REDIS_URL = process.env.REDIS_URL || '';
2770
2765
  WEBHOOK_URL = process.env.WEBHOOK_URL || '';
2771
2766
 
@@ -2773,15 +2768,14 @@ async function start(apiKey, apiUrl, opts = {}) {
2773
2768
  let hasZlib = false;
2774
2769
  try { require('zlib-sync'); hasZlib = true; } catch {}
2775
2770
 
2776
- console.log(colorBanner());
2777
- console.log(
2778
- ` ${rgb(139, 92, 246)}v${PKG_VERSION}${c.reset}` +
2779
- ` ${c.dim}ยท${c.reset} ${c.white}${AccountWorker.COMMAND_MAP.length} Commands${c.reset}` +
2780
- ` ${c.dim}ยท${c.reset} ${rgb(34, 211, 238)}${CLOUD_MODE ? 'Cloud Mode' : (CLUSTER_ENABLED ? 'Cluster Mode' : 'Standalone')}${c.reset}` +
2781
- ` ${c.dim}ยท${c.reset} ${rgb(52, 211, 153)}Auto-Recovery${c.reset}` +
2782
- ` ${c.dim}ยท${c.reset} ${rgb(251, 191, 36)}Loss Limiter${c.reset}`
2783
- );
2784
- log('info', `${c.dim}Fetching accounts...${c.reset}`);
2771
+ // Init terminal FIRST โ€” captures all subsequent console.log output
2772
+ terminal.setVersion(PKG_VERSION);
2773
+ terminal.init({ workers: [], startTime });
2774
+
2775
+ if (CLOUD_MODE) {
2776
+ console.log(`${rgb(139, 92, 246)}๐ŸŒฅ๏ธ Starting in CLOUD MODE โ€” grinding all cloud-enabled accounts${c.reset}`);
2777
+ }
2778
+ terminal.startPhase('Fetching accounts...');
2785
2779
 
2786
2780
  const fetchOpts = CLOUD_MODE ? { cloud: true } : {};
2787
2781
  let data = await fetchConfig(4, 2000, fetchOpts);
@@ -2792,9 +2786,11 @@ async function start(apiKey, apiUrl, opts = {}) {
2792
2786
  data = await fetchConfig(4, 2000, fetchOpts);
2793
2787
  }
2794
2788
  if (data && data.error) {
2789
+ terminal.endPhase(`API error: ${data.error}`, false);
2795
2790
  log('error', `${data.error}`);
2796
2791
  return;
2797
2792
  }
2793
+ terminal.endPhase(`API connected โ€” ${data.accounts?.length || 0} accounts`);
2798
2794
 
2799
2795
  // Cloud mode: post heartbeat every 30s
2800
2796
  if (CLOUD_MODE) {
@@ -2839,10 +2835,6 @@ async function start(apiKey, apiUrl, opts = {}) {
2839
2835
  }
2840
2836
  }
2841
2837
 
2842
- const checks = [];
2843
- checks.push(`${rgb(52, 211, 153)}โœ“${c.reset} ${c.white}API${c.reset}`);
2844
- if (REDIS_URL) checks.push(redis ? `${rgb(52, 211, 153)}โœ“${c.reset} ${c.white}Redis${c.reset}` : `${rgb(251, 191, 36)}โ—‹${c.reset} ${c.dim}Redis (connecting...)${c.reset}`);
2845
-
2846
2838
  // Init rawLogger Redis (uses same URL โ€” logs all raw gateway data)
2847
2839
  if (REDIS_URL) {
2848
2840
  rawLogger.init(redis);
@@ -2887,16 +2879,7 @@ async function start(apiKey, apiUrl, opts = {}) {
2887
2879
  }
2888
2880
  }
2889
2881
  });
2890
- checks.push(`${rgb(52, 211, 153)}โœ“${c.reset} ${c.white}RawLog${c.reset}`);
2891
- }
2892
- if (hasZlib) checks.push(`${rgb(52, 211, 153)}โœ“${c.reset} ${c.white}zlib${c.reset}`);
2893
- if (WEBHOOK_URL) checks.push(`${rgb(52, 211, 153)}โœ“${c.reset} ${c.white}Webhook${c.reset}`);
2894
- if (CLUSTER_ENABLED) {
2895
- checks.push(`${rgb(52, 211, 153)}โœ“${c.reset} ${rgb(34, 211, 238)}Cluster${c.reset} ${c.dim}(${NODE_ID.substring(0, 12)})${c.reset}`);
2896
2882
  }
2897
- checks.push(`${rgb(52, 211, 153)}โœ“${c.reset} ${c.white}${accounts.length} Account${accounts.length > 1 ? 's' : ''}${c.reset}`);
2898
- console.log(` ${checks.join(' ')}`);
2899
- console.log('');
2900
2883
 
2901
2884
  // โ”€โ”€ Terminal renderer init โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
2902
2885
  terminal.setVersion(PKG_VERSION);
package/lib/terminal.js CHANGED
@@ -1,154 +1,161 @@
1
1
  /**
2
- * terminal.js โ€” Modern animated terminal renderer for DankGrinder
2
+ * terminal.js โ€” Polished animated terminal renderer for DankGrinder
3
3
  *
4
- * Key design:
5
- * - stdout capture during startup โ†’ buffer prevents bleed-through
6
- * - When setActive() called: clear screen + replay buffer, then normal
7
- * - After activation: all w.log() routed through flashEvent()
8
- * - Virtual window: single-line per account (scales to 10k+)
9
- * - 4 FPS render loop with dirty-row tracking (no flicker)
10
- * - Graceful degradation: falls back to plain console.log if not TTY
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
11
12
  */
12
13
 
14
+ 'use strict';
15
+
13
16
  const READY = (() => {
14
- try {
15
- return process.stdout.isTTY && !process.env.NO_TERM;
16
- } catch (_) { return false; }
17
+ try { return !!process.stdout.isTTY && !process.env.NO_TERM; } catch (_) { return false; }
17
18
  })();
18
19
 
19
- // โ”€โ”€ ANSI Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
20
+ // โ”€โ”€ ANSI helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
20
21
 
21
22
  const A = {
22
23
  reset: '\x1b[0m',
23
- bold: '\x1b[1m',
24
- dim: '\x1b[2m',
25
- italic: '\x1b[3m',
26
-
27
- black: '\x1b[30m', red: '\x1b[31m', green: '\x1b[32m',
28
- yellow: '\x1b[33m', blue: '\x1b[34m', magenta: '\x1b[35m',
29
- cyan: '\x1b[36m', white: '\x1b[37m',
30
-
31
- bgBlack: '\x1b[40m', bgRed: '\x1b[41m', bgGreen: '\x1b[42m',
32
- bgYellow: '\x1b[43m', bgBlue: '\x1b[44m', bgMagenta: '\x1b[45m',
33
- bgCyan: '\x1b[46m', bgWhite: '\x1b[47m',
34
-
35
- // True-color RGB
24
+ bold: '\x1b[1m',
25
+ dim: '\x1b[2m',
36
26
  rgb: (r, g, b) => `\x1b[38;2;${r};${g};${b}m`,
37
-
38
- // Cursor
39
- save: '\x1b7', restore: '\x1b8',
40
- hide: '\x1b[?25l', show: '\x1b[?25h',
41
- up: (n = 1) => `\x1b[${n}A`,
42
- down: (n = 1) => `\x1b[${n}B`,
43
- clear: '\x1b[2J',
44
- clearLine: '\x1b[2K',
45
- home: '\x1b[H',
46
-
47
- // Erase in display (clears scrollback too)
48
27
  eraseAll: '\x1b[3J\x1b[2J\x1b[H',
49
-
50
- // 256-color shortcuts
51
- purple: '\x1b[38;5;141m',
52
- pink: '\x1b[38;5;205m',
53
- orange: '\x1b[38;5;214m',
54
- teal: '\x1b[38;5;44m',
55
- lime: '\x1b[38;5;82m',
56
- crimson: '\x1b[38;5;196m',
57
- slate: '\x1b[38;5;245m',
58
- gold: '\x1b[38;5;220m',
59
- emerald: '\x1b[38;5;48m',
28
+ clearLine: '\x1b[2K',
29
+ save: '\x1b7',
30
+ restore: '\x1b8',
31
+ hide: '\x1b[?25l',
32
+ show: '\x1b[?25h',
60
33
  };
61
34
 
62
- // โ”€โ”€ Color scheme โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
35
+ // โ”€โ”€ Palette โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
63
36
 
64
37
  const C = {
65
- header: A.purple,
66
- headerDim: A.rgb(100, 70, 180),
67
- border: A.rgb(55, 45, 85),
68
- borderDim: A.rgb(35, 30, 55),
69
-
70
- name: A.rgb(220, 215, 255),
71
- nameDim: A.slate,
72
- coins: A.gold,
73
- level: A.cyan,
74
- lifesavers: A.pink,
75
- lifesaversLow: A.crimson,
76
- lifesaversMid: A.orange,
77
-
78
- statusActive: A.emerald,
79
- statusPaused: A.crimson,
80
- statusWarning: A.orange,
81
- statusOffline: A.slate,
82
- statusConnecting: A.yellow,
83
-
84
- cmdSuccess: A.emerald,
85
- cmdError: A.crimson,
86
- cmdEvent: A.purple,
87
- cmdWarn: A.orange,
88
- cmdInfo: A.slate,
89
-
90
- statLabel: A.slate,
91
- statValue: A.white,
92
-
93
- // Box styles
94
- topLeft: 'โ•ญ', topRight: 'โ•ฎ',
95
- botLeft: 'โ•ฐ', botRight: 'โ•ฏ',
96
- h: 'โ”€', v: 'โ”‚',
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
+ ],
97
71
  };
98
72
 
73
+ // โ”€โ”€ Box-drawing โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
74
+
75
+ const TL='โ•ญ', TR='โ•ฎ', BL='โ•ฐ', BR='โ•ฏ', H='โ”€', V='โ”‚';
76
+
99
77
  // โ”€โ”€ Spinner frames โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
100
78
 
101
- const SPIN = ['โ ‹', 'โ ™', 'โ น', 'โ ธ', 'โ ผ', 'โ ด', 'โ ฆ', 'โ ง', 'โ ฟ'];
79
+ // Dot spinner for phase labels
80
+ const SPIN_DOTS = ['โ ‹','โ ™','โ น','โ ธ','โ ผ','โ ด','โ ฆ','โ ง','โ ฟ'];
102
81
 
103
- // โ”€โ”€ stdout capture during startup โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
82
+ // Block spinner for progress
83
+ const SPIN_BLOCK = ['โ–','โ–Ž','๏ฟฝ','โ–Œ','โ–‹','โ–Š','โ–‰','โ–Š'];
84
+
85
+ // Pulse frames for active indicators
86
+ const PULSE = ['โ—', 'โ—‰', 'โ—Ž', 'โ—‹'];
87
+
88
+ // โ”€โ”€ stdout capture โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
104
89
 
105
90
  let _origWrite = null;
106
91
  let _captureActive = false;
107
92
  let _captureBuf = [];
108
93
 
109
- function _captureWrite(chunk) {
110
- if (_captureActive) {
111
- _captureBuf.push(String(chunk));
112
- return;
113
- }
94
+ function _capWrite(chunk) {
95
+ if (_captureActive) { _captureBuf.push(String(chunk)); return; }
114
96
  return _origWrite.call(process.stdout, chunk);
115
97
  }
116
98
 
117
- // โ”€โ”€ Terminal Renderer โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
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 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
118
130
 
119
131
  class Terminal {
120
132
  constructor() {
121
- this.workers = [];
122
- this.events = [];
123
- this.MAX_EVENTS = 4;
133
+ this.workers = [];
134
+ this.events = [];
135
+ this.MAX_EVENTS = 3;
124
136
 
125
- this.phaseName = '';
137
+ this.phase = '';
126
138
  this.phaseFrame = 0;
127
- this.phaseTimer = null;
139
+ this.phaseTimer = null;
140
+ this.phaseDone = 0;
141
+ this.phaseTotal = 0;
128
142
 
129
143
  this.dirtyWorkers = new Set();
130
- this.dirtyEvents = false;
131
- this.dirtyStats = true;
144
+ this.dirtyStats = true;
132
145
  this._renderTimer = null;
133
- this._startTime = 0;
146
+ this._startTime = 0;
147
+ this._active = false;
148
+ this._shutdown = false;
149
+ this._origLog = null;
134
150
 
151
+ this._w = 110;
152
+ this._h = 35;
135
153
  this.windowStart = 0;
136
- this.windowSize = 8;
137
- this._followIdx = -1;
154
+ this.windowSize = 8;
138
155
 
139
- this._lineCount = 0;
140
- this._active = false;
141
- this._shutdown = false;
142
-
143
- this._w = 80;
144
- this._h = 24;
145
- this._resizeTimer = null;
146
-
147
- // โ”€โ”€ Startup capture โ”€โ”€
148
- this._capturing = false;
149
- this._origLog = null;
150
- this._phaseProgressDone = 0;
151
- this._phaseProgressTotal = 0;
156
+ // Pulse animation state
157
+ this._pulseFrame = 0;
158
+ this._pulseTimer = null;
152
159
  }
153
160
 
154
161
  // โ”€โ”€ Public API โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@@ -159,26 +166,21 @@ class Terminal {
159
166
  this._updateSize();
160
167
 
161
168
  if (READY) {
162
- // Override stdout.write to capture everything during startup
163
169
  _origWrite = process.stdout.write.bind(process.stdout);
164
- process.stdout.write = _captureWrite;
165
- this._capturing = true;
170
+ process.stdout.write = _capWrite;
166
171
  _captureActive = true;
167
172
  _captureBuf = [];
168
-
169
- // Also override console.log temporarily
170
173
  this._origLog = console.log;
171
174
  console.log = (...args) => {
175
+ const s = args.join(' ');
172
176
  if (this._active) {
173
- // After activation, route to flashEvent
174
- const msg = args.join(' ').replace(/\x1b\[[0-9;]*m/g, '').substring(0, 120);
175
- if (msg.trim()) this.flashEvent('info', msg);
177
+ const clean = s.replace(/\x1b\[[0-9;]*m/g, '').substring(0, 100);
178
+ if (clean.trim()) this.flashEvent('info', clean);
176
179
  } else {
177
- _captureWrite(args.join(' ') + '\n');
180
+ _capWrite(s + '\n');
178
181
  }
179
182
  };
180
-
181
- process.stdout.on('resize', this._onResize.bind(this));
183
+ process.stdout.on('resize', () => this._onResize());
182
184
  this._drawStartupScreen();
183
185
  }
184
186
  }
@@ -186,63 +188,52 @@ class Terminal {
186
188
  setVersion(v) { this._version = v; }
187
189
 
188
190
  startPhase(name) {
189
- this.phaseName = name;
191
+ this.phase = name;
192
+ this.phaseDone = 0;
193
+ this.phaseTotal = 0;
190
194
  this.phaseFrame = 0;
191
- this._phaseProgressDone = 0;
192
- this._phaseProgressTotal = 0;
193
-
194
- if (!READY) {
195
- console.log(` โŸณ ${name}...`);
196
- return;
197
- }
195
+ if (!READY) return;
198
196
  if (this.phaseTimer) clearInterval(this.phaseTimer);
199
197
  this.phaseTimer = setInterval(() => {
200
- this.phaseFrame = (this.phaseFrame + 1) % SPIN.length;
201
- this._redrawPhaseSpinner();
202
- }, 120);
203
- this._redrawPhaseSpinner();
198
+ this.phaseFrame = (this.phaseFrame + 1) % SPIN_DOTS.length;
199
+ this._renderPhase();
200
+ }, 80);
201
+ this._renderPhase();
204
202
  }
205
203
 
206
204
  updateProgress(done, total) {
207
- this._phaseProgressDone = done;
208
- this._phaseProgressTotal = total;
205
+ this.phaseDone = done;
206
+ this.phaseTotal = total;
209
207
  if (!READY) return;
210
- this._redrawProgressBar();
208
+ this._renderProgress();
211
209
  }
212
210
 
213
211
  endPhase(name, ok = true) {
214
212
  if (this.phaseTimer) { clearInterval(this.phaseTimer); this.phaseTimer = null; }
215
- this.phaseName = '';
216
- this._phaseProgressDone = 0;
217
- this._phaseProgressTotal = 0;
218
-
219
213
  if (!READY) {
220
- const icon = ok ? `โœ“ ${name}` : `โœ— ${name}`;
221
- console.log(` ${icon}`);
214
+ const icon = ok ? `${C.green}โœ“${A.reset}` : `${C.red}โœ—${A.reset}`;
215
+ console.log(` ${icon} ${name}`);
222
216
  return;
223
217
  }
224
- const icon = ok ? `${C.header}โœ“${A.reset}` : `${A.crimson}โœ—${A.reset}`;
225
- const line = ` ${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
226
221
  const w = this._w;
227
-
228
- // Clear spinner + progress rows, show result
222
+ const V = C.border;
223
+ const line = rpad(` ${label}`, w - 3);
229
224
  this._write(
230
- A.save +
231
- this._cursor(6, 1) + A.clearLine +
232
- this._cursor(7, 1) + A.clearLine +
233
- this._cursor(8, 1) + A.clearLine +
234
- this._cursor(9, 1) + A.clearLine +
235
- this._cursor(10, 1) + A.clearLine +
236
- this._cursor(11, 1) + A.clearLine +
237
- this._cursor(12, 1) + A.clearLine +
238
- `${this._rpad(line, w)}` +
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}` +
239
230
  A.restore
240
231
  );
241
232
  }
242
233
 
243
234
  flashEvent(type, msg) {
244
235
  const now = new Date();
245
- 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}`;
246
237
  this.events.unshift({ ts, type, msg, id: Date.now() });
247
238
  if (this.events.length > this.MAX_EVENTS) this.events.pop();
248
239
  this.dirtyEvents = true;
@@ -255,7 +246,8 @@ class Terminal {
255
246
  }
256
247
 
257
248
  markWorkerDirty(idx) {
258
- this.dirtyWorkers.add(idx);
249
+ this.dirtyWorkers = new Set(this.workers.map((_, i) => i));
250
+ this.dirtyStats = true;
259
251
  }
260
252
 
261
253
  setActive() {
@@ -263,464 +255,416 @@ class Terminal {
263
255
  this._active = true;
264
256
 
265
257
  if (READY) {
266
- // โ”€โ”€ Stop capturing, fully clear screen, draw live view โ”€โ”€
267
258
  _captureActive = false;
268
- this._capturing = false;
269
-
270
- // Restore stdout.write first
271
- if (_origWrite) {
272
- process.stdout.write = _origWrite;
273
- _origWrite = null;
274
- }
275
- if (this._origLog) {
276
- console.log = this._origLog;
277
- this._origLog = null;
278
- }
279
-
280
- // Discard all buffered startup output โ€” we only want the live view
259
+ if (_origWrite) { process.stdout.write = _origWrite; _origWrite = null; }
260
+ if (this._origLog) { console.log = this._origLog; this._origLog = null; }
281
261
  _captureBuf = [];
282
262
 
283
- // Full screen clear + scrollback clear
284
- this._write(A.eraseAll);
285
-
286
- // Draw live view
263
+ this._write(A.eraseAll + A.hide);
287
264
  this._drawLiveView();
288
-
289
- // Reset dirty flags so we don't redraw header every frame
290
265
  this.dirtyWorkers.clear();
291
266
  this.dirtyEvents = false;
292
267
  this.dirtyStats = false;
293
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
+
294
276
  this._startRenderLoop();
295
277
  } else {
296
- // Non-TTY: restore console.log
297
- if (this._origLog) {
298
- console.log = this._origLog;
299
- this._origLog = null;
300
- }
301
- }
302
- }
303
-
304
- scrollBy(delta) {
305
- if (!READY || this._shutdown) return;
306
- const max = Math.max(0, this.workers.length - this.windowSize);
307
- this.windowStart = Math.max(0, Math.min(max, this.windowStart + delta));
308
- this._followIdx = -1;
309
- this.dirtyWorkers = new Set();
310
- for (let i = this.windowStart; i < this.windowStart + this.windowSize; i++) {
311
- if (i < this.workers.length) this.dirtyWorkers.add(i);
278
+ if (this._origLog) { console.log = this._origLog; this._origLog = null; }
312
279
  }
313
280
  }
314
281
 
315
282
  shutdown(summary = {}) {
316
283
  this._shutdown = true;
317
284
  if (this._renderTimer) { clearInterval(this._renderTimer); this._renderTimer = null; }
318
- if (this._phaseTimer) { clearInterval(this._phaseTimer); this._phaseTimer = null; }
319
-
320
- // Restore stdout state
321
- if (READY && _origWrite) {
322
- process.stdout.write = _origWrite;
323
- _origWrite = null;
324
- }
325
- if (this._origLog) {
326
- console.log = this._origLog;
327
- this._origLog = null;
328
- }
285
+ if (this._pulseTimer) { clearInterval(this._pulseTimer); this._pulseTimer = null; }
286
+ if (this.phaseTimer) { clearInterval(this.phaseTimer); this.phaseTimer = null; }
329
287
 
288
+ if (READY && _origWrite) { process.stdout.write = _origWrite; _origWrite = null; }
289
+ if (this._origLog) { console.log = this._origLog; this._origLog = null; }
330
290
  this._write(A.show);
331
291
 
332
- const w = this._w;
333
292
  const { totalCoins = 0, totalCmds = 0, totalSuccess = 0,
334
293
  workers = [], uptime = 0, memMB = 0 } = summary;
335
294
 
336
- const b = C.border;
337
- const h = C.header;
338
- const g = C.statValue;
339
- const dim = A.dim;
340
- const r = A.reset;
341
-
342
- let out = '';
343
- out += A.eraseAll + A.home + A.save;
295
+ const w = this._w;
296
+ let out = A.eraseAll;
344
297
 
345
- // Box top
346
- out += `${this._at(1, 1)}${b}${C.topLeft}${'โ”€'.repeat(w - 2)}${C.topRight}${r}`;
347
- out += `${this._at(2, 1)}${b}${C.v} ${h}${A.bold} โฌก DANKGRINDER โ€” Session Summary ${r}${' '.repeat(Math.max(0, w - 40))}${C.v}${r}`;
348
- out += `${this._at(3, 1)}${b}${C.h}${'โ”€'.repeat(w - 2)}${C.h}${r}`;
298
+ // Top bar
299
+ out += this._boxTop();
300
+ out += this._statsBar();
301
+ out += this._sep();
349
302
 
350
303
  // Column headers
351
- const hdr = [
352
- `${h}#${r}`, `${h}ACCOUNT${r}`, `${h}COINS${r}`,
353
- `${h}LV${r}`, `${h}โ™ฅ${r}`, `${h}CMDS${r}`,
354
- `${h}OK%${r}`, `${h}STATUS${r}`,
355
- ].join(` `);
356
- out += `${this._at(4, 1)}${b} ${this._rpad(hdr, w - 4)} ${C.v}${r}`;
357
- out += `${this._at(5, 1)}${b}${C.h}${'โ”€'.repeat(w - 2)}${C.h}${r}`;
358
-
359
- // Per-account rows
360
- let row = 6;
361
- for (let i = 0; i < workers.length && row < this._h - 3; i++) {
362
- const wk = workers[i];
363
- const rate = wk.stats?.commands > 0
364
- ? `${((wk.stats.successes / wk.stats.commands) * 100).toFixed(0)}%`
365
- : '0%';
366
- const ls = wk._lifesavers ?? '?';
367
- const lsColor = ls === 0 ? C.lifesaversLow : ls <= 2 ? C.lifesaversMid : C.lifesavers;
368
- const statusIcon = (!wk.running || wk._tokenInvalid) ? 'โšซ offline'
369
- : wk.paused || wk.dashboardPaused ? '๐Ÿ”ด paused'
370
- : '๐ŸŸข active';
371
-
372
- const line = [
373
- `${g}${String(i + 1).padEnd(2)}${r}`,
374
- `${C.name}${A.bold}${(wk.username || '?').substring(0, 18).padEnd(19)}${r}`,
375
- `${C.coins}โฃ${(wk.stats?.coins || 0).toLocaleString().padStart(8)}${r}`,
376
- `${C.level}Lv.${wk._level ?? '?'}${r}`,
377
- `${lsColor}โ™ฅ${String(ls).padStart(2)}${r}`,
378
- `${g}${String(wk.stats?.commands || 0).padStart(4)}cmds${r}`,
379
- `${g}${rate.padStart(4)}${r}`,
380
- `${g}${statusIcon}${r}`,
381
- ].join(' ');
382
- out += `${this._at(row++, 1)}${b} ${this._rpad(line, w - 4)} ${C.v}${r}`;
304
+ out += `${this._row(4, this._colHdr())}`;
305
+ out += this._sep();
306
+
307
+ // Account rows
308
+ let row = 5;
309
+ for (let i = 0; i < workers.length && row < this._h - 4; i++) {
310
+ out += `${this._row(row++, this._accountLine(workers[i], i, workers))}`;
383
311
  }
384
312
 
385
- row++; // blank
386
- out += `${this._at(row++, 1)}${b}${C.h}${'โ”€'.repeat(w - 2)}${C.h}${r}`;
387
-
388
- // Totals row
389
- const totalRate = totalCmds > 0 ? `${((totalSuccess / totalCmds) * 100).toFixed(0)}%` : '0%';
390
- const uptimeStr = this._fmtUptime(uptime);
391
- const totalLine = [
392
- `${h}๐Ÿ’ฐ TOTAL:${r}`,
393
- `${C.coins}${A.bold}โฃ ${totalCoins.toLocaleString()}${r}`,
394
- `${dim}${totalCmds} cmds${r}`,
395
- `${dim}${totalRate} OK${r}`,
396
- `${dim}${uptimeStr}${r}`,
397
- `${dim}${memMB}MB RAM${r}`,
398
- ].join(' ');
399
- out += `${this._at(row++, 1)}${b} ${this._rpad(totalLine, w - 4)} ${C.v}${r}`;
400
- out += `${this._at(row, 1)}${b}${C.botLeft}${'โ”€'.repeat(w - 2)}${C.botRight}${r}`;
401
- out += A.restore + A.show;
313
+ out += this._sep();
314
+ row++;
315
+ out += `${this._row(row++, this._totalLine(totalCoins, totalCmds, totalSuccess, uptime, memMB))}`;
316
+ out += this._boxBot();
402
317
 
403
318
  this._write(out);
404
319
  }
405
320
 
406
- // โ”€โ”€ Internal โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
321
+ // โ”€โ”€ Startup Screen โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
407
322
 
408
- _updateSize() {
409
- try {
410
- this._w = process.stdout.columns || 80;
411
- this._h = process.stdout.rows || 24;
412
- this.windowSize = Math.max(3, this._h - 11);
413
- } catch (_) {
414
- this._w = 80; this._h = 24; this.windowSize = 8;
415
- }
416
- }
323
+ _drawStartupScreen() {
324
+ const w = this._w;
325
+ let out = A.eraseAll;
417
326
 
418
- _onResize() {
419
- if (this._resizeTimer) clearTimeout(this._resizeTimer);
420
- this._resizeTimer = setTimeout(() => {
421
- this._updateSize();
422
- if (this._active) {
423
- this._write(A.eraseAll);
424
- this._drawLiveView();
425
- }
426
- }, 100);
427
- }
327
+ // Top border
328
+ out += `${this._at(1,1)}${C.border}${TL}${'โ”€'.repeat(w-2)}${TR}${A.reset}\n`;
428
329
 
429
- _ansiLen(s) {
430
- let len = 0, i = 0;
431
- const str = String(s);
432
- while (i < str.length) {
433
- if (str.charCodeAt(i) === 0x1b && str[i + 1] === '[') {
434
- let j = i + 2;
435
- while (j < str.length && str[j] !== 'm') j++;
436
- i = j + 1;
437
- } else { len++; i++; }
438
- }
439
- return len;
440
- }
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`;
441
333
 
442
- _rpad(s, width) {
443
- return s + ' '.repeat(Math.max(0, width - this._ansiLen(s)));
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
- _cursor(row) { return `\x1b[${row};1H`; }
447
- _at(row, col) { return `\x1b[${row};${col}H`; }
448
- _write(str) { if (str) process.stdout.write(str); }
338
+ out += `${this._at(4,1)}${C.border}${V}${'โ”€'.repeat(w-2)}${V}${A.reset}\n`;
449
339
 
450
- _fmtUptime(ms) {
451
- if (!ms) return '0s';
452
- const s = Math.floor(ms / 1000);
453
- if (s < 60) return `${s}s`;
454
- const m = Math.floor(s / 60);
455
- if (m < 60) return `${m}m ${s % 60}s`;
456
- const h = Math.floor(m / 60);
457
- if (h < 24) return `${h}h ${m % 60}m`;
458
- return `${Math.floor(h / 24)}d ${h % 24}h`;
459
- }
340
+ // Spinner + phase label (row 5)
341
+ out += `${this._at(5,1)}${C.border}${V}${rpad('', w-2)}${V}${A.reset}\n`;
460
342
 
461
- _fmtCoins(n) {
462
- if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
463
- if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
464
- return String(n);
465
- }
343
+ // Progress bar (row 7)
344
+ out += `${this._at(7,1)}${C.border}${V}${rpad('', w-2)}${V}${A.reset}\n`;
466
345
 
467
- _buildAccountRow(wk, idx) {
468
- const w = this._w;
469
- const ls = wk._lifesavers ?? '?';
470
- const lsColor = ls === 0 ? C.lifesaversLow : ls <= 2 ? C.lifesaversMid : C.lifesavers;
471
- const rate = wk.stats?.commands > 0
472
- ? `${((wk.stats.successes / wk.stats.commands) * 100).toFixed(0)}%`
473
- : '0%';
346
+ // Checkmarks / status area (row 9)
347
+ out += `${this._at(9,1)}${C.border}${V}${rpad('', w-2)}${V}${A.reset}\n`;
474
348
 
475
- let statusDot, statusText, rowBg;
476
- if (!wk.running || wk._tokenInvalid) {
477
- statusDot = 'โšซ'; statusText = 'offline';
478
- } else if (wk.paused || wk.dashboardPaused) {
479
- statusDot = '๐Ÿ”ด'; statusText = 'paused';
480
- } else if (wk.lastStatus?.includes('claim') || wk.lastStatus?.includes('daily')) {
481
- statusDot = '๐ŸŸก'; statusText = 'claiming';
482
- } else {
483
- statusDot = '๐ŸŸข'; statusText = 'grinding';
484
- }
349
+ // Spacer
350
+ out += `${this._at(11,1)}${C.border}${V}${rpad('', w-2)}${V}${A.reset}\n`;
485
351
 
486
- const line = [
487
- `${C.header}${idx + 1}.${A.reset}`,
488
- `${C.name}${A.bold}${(wk.username || '?').substring(0, 18).padEnd(19)}${A.reset}`,
489
- `${C.coins}โฃ${this._fmtCoins(wk.stats?.coins || 0).padStart(7)}${A.reset}`,
490
- `${C.level}Lv.${String(wk._level ?? '?').padStart(3)}${A.reset}`,
491
- `${lsColor}โ™ฅ${String(ls).padStart(2)}${A.reset}`,
492
- `${C.statValue}${String(wk.stats?.commands || 0).padStart(4)}cmds${A.reset}`,
493
- `${C.statValue}${rate.padStart(5)}${A.reset}`,
494
- `${statusDot} ${statusText}`.padEnd(14),
495
- `${A.dim}${(wk.lastStatus || '').substring(0, 22).padEnd(22)}${A.reset}`,
496
- ].join(' ');
352
+ // Bottom border
353
+ out += `${this._at(12,1)}${C.border}${BL}${'โ”€'.repeat(w-2)}${BR}${A.reset}\n`;
497
354
 
498
- return this._rpad(line, w);
499
- }
500
-
501
- _drawStartupScreen() {
502
- const w = this._w;
503
- const b = C.border;
504
- const h = C.header;
505
- const dim = A.dim;
506
- const r = A.reset;
507
-
508
- let out = '';
509
- out += A.eraseAll + A.home;
510
- out += `${this._at(1, 1)}${b}${C.topLeft}${'โ”€'.repeat(w - 2)}${C.topRight}${r}`;
511
- out += `${this._at(2, 1)}${b} ${h}${A.bold} โฌก DANKGRINDER v${this._version || '?'} ${r}${'โ”€'.repeat(Math.max(0, w - 28 - (this._version || '').length))}${r}`;
512
- out += `${this._at(3, 1)}${b}${C.h}${'โ”€'.repeat(w - 2)}${C.h}${r}`;
513
- // Status bar placeholder
514
- out += `${this._at(4, 1)}${b}${' '.repeat(w - 2)}${r}`;
515
- out += `${this._at(5, 1)}${b}${C.h}${'โ”€'.repeat(w - 2)}${C.h}${r}`;
516
- // Spinner area
517
- out += `${this._at(7, 1)}${b}${' '.repeat(w - 2)}${r}`;
518
- out += `${this._at(8, 1)}${b}${' '.repeat(w - 2)}${r}`;
519
- out += `${this._at(9, 1)}${b}${' '.repeat(w - 2)}${r}`;
520
- out += `${this._at(10, 1)}${b}${' '.repeat(w - 2)}${r}`;
521
- out += `${this._at(11, 1)}${b}${' '.repeat(w - 2)}${r}`;
522
- out += `${this._at(12, 1)}${b}${' '.repeat(w - 2)}${r}`;
523
355
  // Footer
524
- out += `${this._at(14, 1)}${b}${C.botLeft}${'โ”€'.repeat(w - 2)}${C.botRight}${r}`;
525
- out += `${this._at(15, 1)}${b} ${dim}Starting up...${r}${' '.repeat(Math.max(0, w - 18))}${r}`;
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`;
526
358
 
527
359
  this._write(out);
528
360
  }
529
361
 
530
- _redrawPhaseSpinner() {
531
- if (!READY || !this._phaseName) return;
532
- const frame = SPIN[this.phaseFrame];
533
- const line = ` ${frame} ${this.phaseName}...`;
362
+ _renderPhase() {
363
+ if (!READY || !this.phase) return;
534
364
  const w = this._w;
535
- const b = C.border;
536
- 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);
537
369
  this._write(
538
- A.save +
539
- this._cursor(8, 1) + A.clearLine +
540
- `${b} ${line}${' '.repeat(Math.max(0, w - this._ansiLen(line) - 3))}${r}` +
370
+ `${A.save}` +
371
+ `${this._at(5,1)}${A.clearLine}` +
372
+ `${V} ${line} ${V}${A.reset}` +
541
373
  A.restore
542
374
  );
543
375
  }
544
376
 
545
- _redrawProgressBar() {
546
- if (!READY || !this._phaseName) return;
547
- const { done, total } = { done: this._phaseProgressDone, total: this._phaseProgressTotal };
377
+ _renderProgress() {
378
+ if (!READY) return;
548
379
  const w = this._w;
549
- const h = C.header;
550
- const dim = A.dim;
551
- const r = A.reset;
552
-
553
- const barW = Math.max(10, w - 35);
380
+ const V = C.border;
381
+ const { done, total } = { done: this.phaseDone, total: this.phaseTotal };
382
+ const barW = Math.max(16, w - 40);
554
383
  const filled = total > 0 ? Math.round((done / total) * barW) : 0;
555
- const bar = `${h}${'โ–ˆ'.repeat(filled)}${dim}${'โ–‘'.repeat(barW - filled)}${r}`;
556
- const label = ` ${done}/${total} `;
557
- const line = bar + label;
384
+ const block = SPIN_BLOCK[this.phaseFrame % SPIN_BLOCK.length];
385
+
386
+ // Filled bar with gradient: gold on left, purple on right
387
+ const filledPart = filled > 0
388
+ ? `${C.gold}${'โ–ˆ'.repeat(Math.max(1, filled - 1))}${C.green}${block}${A.reset}`
389
+ : '';
390
+ const emptyPart = barW - filled > 0
391
+ ? `${C.borderDim}${'โ–‘'.repeat(Math.max(0, barW - filled))}${A.reset}`
392
+ : '';
393
+
394
+ const pct = total > 0 ? `${Math.round((done / total) * 100)}%` : '';
395
+ const label = ` ${pct} `;
396
+ const bar = `${filledPart}${emptyPart}${C.textDim}${label}${A.reset}`;
397
+ const line = rpad(` ${bar}`, w - 3);
398
+
558
399
  this._write(
559
- A.save +
560
- this._cursor(9, 1) + A.clearLine +
561
- `${C.border} ${line}${' '.repeat(Math.max(0, w - this._ansiLen(line) - 3))}${r}` +
400
+ `${A.save}` +
401
+ `${this._at(7,1)}${A.clearLine}` +
402
+ `${V} ${line} ${V}${A.reset}` +
562
403
  A.restore
563
404
  );
564
405
  }
565
406
 
407
+ // โ”€โ”€ Live View โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
408
+
566
409
  _drawLiveView() {
567
- this._drawHeader();
568
- this._drawAccounts();
569
- this._drawEvents();
570
- 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);
571
424
  }
572
425
 
573
- _drawHeader() {
426
+ _boxTop() {
574
427
  const w = this._w;
575
- const b = C.border;
576
- const h = C.header;
577
- const g = C.statValue;
578
- const dim = A.dim;
579
- const r = A.reset;
428
+ let o = '';
429
+ o += `${this._at(1,1)}${C.border}${TL}${'โ”€'.repeat(w-2)}${TR}${A.reset}\n`;
430
+ const title = ` โฌก DANKGRINDER v${this._version || '?'} `;
431
+ o += `${this._at(2,1)}${C.border}${V} ${C.purple}${A.bold}${title}${rpad('', w - ansiLen(title) - 4)}${V}${A.reset}\n`;
432
+ o += `${this._at(3,1)}${C.border}${V}${'โ”€'.repeat(w-2)}${V}${A.reset}\n`;
433
+ return o;
434
+ }
580
435
 
581
- // Title bar
582
- const titleText = ` โฌก DANKGRINDER v${this._version || '?'} `;
583
- const titlePad = Math.max(0, w - 2 - this._ansiLen(titleText));
584
- this._write(`${this._at(1, 1)}${b}${C.topLeft}${'โ”€'.repeat(w - 2)}${C.topRight}${r}`);
585
- this._write(`${this._at(2, 1)}${b} ${h}${A.bold}${titleText}${r}${' '.repeat(titlePad)} ${C.v}${r}`);
586
- this._write(`${this._at(3, 1)}${b}${C.h}${'โ”€'.repeat(w - 2)}${C.h}${r}`);
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
+ }
587
444
 
588
- // Stats bar
589
- const stats = this._buildStatsLine();
590
- const statsPad = Math.max(0, w - 2 - this._ansiLen(stats) - 2);
591
- this._write(`${this._at(4, 1)}${b} ${stats}${' '.repeat(statsPad)} ${C.v}${r}`);
592
- this._write(`${this._at(5, 1)}${b}${C.h}${'โ”€'.repeat(w - 2)}${C.h}${r}`);
445
+ _sep() {
446
+ const w = this._w;
447
+ return `${C.border}${V}${'โ”€'.repeat(w-2)}${V}${A.reset}\n`;
448
+ }
593
449
 
594
- this._accountsRow = 6;
450
+ _row(r, content) {
451
+ const w = this._w;
452
+ return `${this._at(r,1)}${C.border}${V} ${content}${rpad('', w - ansiLen(content) - 4)} ${V}${A.reset}\n`;
453
+ }
454
+
455
+ _statsBar() {
456
+ const w = this._w;
457
+ const stats = this._buildStats();
458
+ return `${this._row(4, stats)}`;
595
459
  }
596
460
 
597
- _buildStatsLine() {
461
+ _buildStats() {
598
462
  let totalCoins = 0, totalCmds = 0, totalSuccess = 0, totalLs = 0;
599
463
  let paused = 0, active = 0;
600
464
 
601
465
  for (const wk of this.workers) {
602
- totalCoins += wk.stats?.coins || 0;
603
- totalCmds += wk.stats?.commands || 0;
604
- totalSuccess += wk.stats?.successes || 0;
466
+ totalCoins += wk.stats?.coins || 0;
467
+ totalCmds += wk.stats?.commands || 0;
468
+ totalSuccess += wk.stats?.successes|| 0;
605
469
  if (wk._lifesavers != null) totalLs += wk._lifesavers;
606
470
  if (wk.running && !wk._tokenInvalid) {
607
- if (wk.paused || wk.dashboardPaused) paused++;
608
- else active++;
471
+ if (wk.paused || wk.dashboardPaused) paused++; else active++;
609
472
  }
610
473
  }
611
-
612
474
  const uptime = this._fmtUptime(Date.now() - this._startTime);
613
- const rate = totalCmds > 0 ? ((totalSuccess / totalCmds) * 100).toFixed(0) : '0';
614
-
615
- return [
616
- `${A.dim}โฑ${A.reset} ${C.statValue}${uptime}${A.reset}`,
617
- `${A.dim}โฌก${A.reset} ${C.statValue}${this.workers.length}${A.reset} ${A.dim}accounts${A.reset}`,
618
- `${C.coins}โฃ${A.reset} ${C.statValue}${totalCoins.toLocaleString()}${A.reset}`,
619
- `${A.dim}โšก${A.reset} ${C.statValue}${totalCmds}${A.reset} ${A.dim}cmds${A.reset}`,
620
- `${A.dim}๐Ÿ“Š${A.reset} ${C.statValue}${rate}%${A.reset}`,
621
- `${C.lifesavers}โ™ฅ${A.reset} ${C.statValue}${totalLs}${A.reset}`,
622
- `${C.statusActive}๐ŸŸข${A.reset} ${C.statValue}${active}${A.reset}`,
623
- `${C.statusPaused}๐Ÿ”ด${A.reset} ${C.statValue}${paused}${A.reset}`,
624
- ].join(` โ”‚ `);
475
+ const rate = totalCmds > 0 ? `${((totalSuccess / totalCmds) * 100).toFixed(0)}%` : '0%';
476
+
477
+ const items = [
478
+ [`โฑ`, uptime, C.textDim],
479
+ [`โฌก`, `${this.workers.length} accounts`, C.textDim],
480
+ [`โฃ`, totalCoins.toLocaleString(), C.gold],
481
+ [`โšก`, `${totalCmds} cmds`, C.textDim],
482
+ [`๐Ÿ“Š`, `${rate} ok`, C.textDim],
483
+ [`โ™ฅ`, `${totalLs}`, C.pink],
484
+ [`๐ŸŸข`, `${active}`, C.green],
485
+ [`๐Ÿ”ด`, `${paused}`, C.red],
486
+ ];
487
+
488
+ return items.map(([icon, val, col]) =>
489
+ `${C.textDim}${icon}${A.reset} ${col}${val}${A.reset}`
490
+ ).join(` ${C.borderDim}โ”‚${A.reset} `);
625
491
  }
626
492
 
627
- _drawAccounts() {
628
- const w = this._w;
629
- const b = C.border;
630
- const h = C.header;
631
- const r = A.reset;
632
-
633
- // Column headers
493
+ _colHdr() {
634
494
  const cols = [
635
- `${h}#${r}`, `${h}ACCOUNT${r}`, `${h}COINS${r}`,
636
- `${h}LV${r}`, `${h}โ™ฅ${r}`, `${h}CMDS${r}`,
637
- `${h}OK%${r}`, `${h}STATUS${r}`,
638
- ].join(` `);
639
- this._write(`${this._at(this._accountsRow, 1)}${b} ${this._rpad(cols, w - 4)} ${C.v}${r}`);
640
- this._write(`${this._at(this._accountsRow + 1, 1)}${b}${C.h}${'โ”€'.repeat(w - 2)}${C.h}${r}`);
641
-
642
- const visible = this.workers.slice(this.windowStart, this.windowStart + this.windowSize);
495
+ `${C.purple}#`,
496
+ `${C.purple}ACCOUNT`,
497
+ `${C.purple}COINS`,
498
+ `${C.purple}LV`,
499
+ `${C.purple}โ™ฅ`,
500
+ `${C.purple}OK%`,
501
+ `${C.purple}STATUS`,
502
+ ];
503
+ return cols.join(' ');
504
+ }
505
+
506
+ _accountRows() {
507
+ let out = '';
643
508
  for (let i = 0; i < this.windowSize; i++) {
644
- const row = this._accountsRow + 2 + i;
645
- if (row > this._h - 4) break;
646
- if (i < visible.length) {
647
- const line = this._buildAccountRow(visible[i], this.windowStart + i);
648
- this._write(`${this._at(row, 1)}${b} ${line} ${C.v}${r}`);
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))}`;
649
514
  } else {
650
- this._write(`${this._at(row, 1)}${b}${' '.repeat(w - 2)}${r}`);
515
+ out += `${this._row(row, '')}`;
651
516
  }
652
517
  }
653
- this._eventsRow = this._accountsRow + 2 + Math.min(this.windowSize, this.workers.length);
518
+ return out;
654
519
  }
655
520
 
656
- _drawEvents() {
657
- const w = this._w;
658
- const b = C.border;
659
- const dim = A.dim;
660
- const r = A.reset;
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%';
661
529
 
662
- if (this._eventsRow > this._h - 4) return;
663
- this._write(`${this._at(this._eventsRow, 1)}${b}${C.h}${'โ”€'.repeat(w - 2)}${C.h}${r}`);
530
+ // Rank badge
531
+ const medal = pos < 3 ? MEDALS[pos] : null;
532
+ const rankColor = pos === 0 ? C.rank1 : pos === 1 ? C.rank2 : pos === 2 ? C.rank3 : null;
533
+ const acctColor = C.ACCT[idx % C.ACCT.length];
534
+
535
+ // Color based on rank (top 3 get medal color, rest get account color)
536
+ const mainColor = isActive
537
+ ? (rankColor || acctColor)
538
+ : C.textFaint;
539
+
540
+ const dimColor = isActive ? C.textDim : C.textFaint;
541
+ const goldColor = isActive ? C.gold : C.textDim;
542
+ const cyanColor = isActive ? C.cyan : C.textDim;
543
+ const lsColor = isActive
544
+ ? (ls === 0 ? C.red : ls <= 2 ? C.orange : C.pink)
545
+ : C.textDim;
546
+
547
+ // Status with pulse for active
548
+ let dot, statusText;
549
+ if (!wk.running || wk._tokenInvalid) {
550
+ dot = `${C.textFaint}โšซ${A.reset}`; statusText = `${C.textFaint}offline${A.reset}`;
551
+ } else if (wk.paused || wk.dashboardPaused) {
552
+ dot = `${C.red}๐Ÿ”ด${A.reset}`; statusText = `${C.red}paused${A.reset}`;
553
+ } else {
554
+ const pulse = PULSE[this._pulseFrame];
555
+ dot = `${C.green}${pulse}${A.reset}`; statusText = `${C.green}active${A.reset}`;
556
+ }
557
+
558
+ // Account name โ€” truncate with care
559
+ const rawName = wk.username || '?';
560
+ const nameDisplay = rawName.length > 20
561
+ ? rawName.substring(0, 17) + '...'
562
+ : rawName;
563
+
564
+ // Coins display
565
+ const coins = (wk.stats?.coins || 0).toLocaleString();
566
+ const sign = (wk.stats?.coins || 0) >= 0 ? '+' : '';
567
+ const coinDisplay = `${goldColor}${sign}โฃ${coins}${A.reset}`;
568
+
569
+ // Current command (minimal)
570
+ const cmd = (wk.lastStatus || 'โ€”').replace(/\x1b\[[0-9;]*m/g, '').substring(0, 14);
571
+
572
+ // Build rank badge
573
+ const rankBadge = medal
574
+ ? `${rankColor}${A.bold}${medal}${A.reset}`
575
+ : `${dimColor}${rank}th${A.reset}`;
576
+
577
+ // Account name with subtle glow for top 3
578
+ const nameBadge = pos < 3
579
+ ? `${mainColor}${A.bold}${nameDisplay}${A.reset}`
580
+ : `${mainColor}${nameDisplay}${A.reset}`;
581
+
582
+ const parts = [
583
+ rankBadge,
584
+ nameBadge,
585
+ coinDisplay,
586
+ `${cyanColor}Lv.${String(wk._level ?? '?').padStart(3)}${A.reset}`,
587
+ `${lsColor}โ™ฅ${String(ls).padStart(2)}${A.reset}`,
588
+ `${dimColor}${rate.padStart(5)}${A.reset}`,
589
+ `${dot} ${statusText}`,
590
+ `${dimColor}${cmd}`,
591
+ ];
592
+
593
+ return parts.join(' ');
594
+ }
664
595
 
665
- const visible = this.events.slice(0, Math.min(this.MAX_EVENTS, this._h - this._eventsRow - 3));
596
+ _eventFeed() {
597
+ let out = '';
598
+ const visible = this.events.slice(0, Math.min(this.MAX_EVENTS, this._h - this._eventRow - 3));
666
599
  for (let i = 0; i < visible.length; i++) {
667
- const row = this._eventsRow + 1 + i;
668
- if (row > this._h - 2) break;
600
+ const row = this._eventRow + i;
601
+ if (row > this._h - 3) break;
669
602
  const e = visible[i];
670
- const typeColor = e.type === 'death' ? C.cmdError
671
- : e.type === 'lowls' ? C.cmdWarn
672
- : e.type === 'levelup' ? C.cmdSuccess
673
- : e.type === 'success' ? C.cmdSuccess
674
- : C.cmdInfo;
675
- const line = ` ${dim}${e.ts}${A.reset} ${typeColor}${e.msg}${A.reset}`;
676
- this._write(`${this._at(row, 1)}${b} ${this._rpad(line, w - 4)} ${C.v}${r}`);
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}`);
677
609
  }
678
- this._footerRow = this._eventsRow + 1 + visible.length;
610
+ this._footerRow = this._eventRow + visible.length;
611
+ return out;
679
612
  }
680
613
 
681
- _drawFooter() {
682
- const w = this._w;
683
- const b = C.border;
684
- const dim = A.dim;
685
- const r = A.reset;
686
- if (this._footerRow > this._h - 1) this._footerRow = this._h - 2;
687
- this._write(`${this._at(this._footerRow, 1)}${b}${C.botLeft}${'โ”€'.repeat(w - 2)}${C.botRight}${r}`);
688
- const hint = `${dim}โ†‘โ†“ scroll${r}`;
689
- this._write(`${this._at(this._footerRow + 1, 1)}${b} ${hint}${' '.repeat(Math.max(0, w - 2 - this._ansiLen(hint)))}${r}`);
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(' ');
690
622
  }
691
623
 
624
+ // โ”€โ”€ Render loop โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
625
+
692
626
  _render() {
693
627
  if (!READY || this._shutdown || !this._active) return;
694
628
 
695
- // Only update stats line (row 4) when dirty โ€” don't redraw whole header
696
629
  if (this.dirtyStats) {
630
+ // Update stats bar (row 4)
631
+ const stats = this._buildStats();
697
632
  const w = this._w;
698
- const b = C.border;
699
- const stats = this._buildStatsLine();
700
- const statsPad = Math.max(0, w - 2 - this._ansiLen(stats) - 2);
701
- this._write(`${this._at(4, 1)}${b} ${stats}${' '.repeat(statsPad)} ${C.v}${A.reset}`);
633
+ const V = C.border;
634
+ this._write(
635
+ `${this._at(4,1)}${V} ${rpad(stats, w-4)} ${V}${A.reset}`
636
+ );
637
+ // Re-draw account rows to update pulse/status
638
+ for (let i = 0; i < this.windowSize; i++) {
639
+ const wkIdx = this.windowStart + i;
640
+ const row = this._topRow + i;
641
+ if (row > this._h - 5) break;
642
+ if (wkIdx < this.workers.length) {
643
+ const line = this._accountLine(this.workers[wkIdx], wkIdx, this.workers);
644
+ this._write(`${this._at(row,1)}${C.border}${V} ${rpad(line, w-4)} ${V}${A.reset}`);
645
+ }
646
+ }
702
647
  this.dirtyStats = false;
703
648
  }
704
649
 
705
650
  if (this.dirtyWorkers.size > 0) {
706
651
  const w = this._w;
707
- const b = C.border;
708
- const r = A.reset;
709
- for (const idx of this.dirtyWorkers) {
710
- const localIdx = idx - this.windowStart;
711
- const row = this._accountsRow + 2 + localIdx;
712
- if (row < this._accountsRow + 2 || row > this._h - 4) continue;
713
- if (idx < this.workers.length) {
714
- const line = this._buildAccountRow(this.workers[idx], idx);
715
- this._write(`${this._at(row, 1)}${b} ${line} ${C.v}${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}`);
716
660
  } else {
717
- 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}`);
718
662
  }
719
663
  }
720
664
  }
721
665
 
722
666
  if (this.dirtyEvents) {
723
- this._drawEvents();
667
+ this._write(this._eventFeed());
724
668
  }
725
669
 
726
670
  this.dirtyWorkers.clear();
@@ -729,10 +673,44 @@ class Terminal {
729
673
 
730
674
  _startRenderLoop() {
731
675
  if (this._renderTimer) clearInterval(this._renderTimer);
732
- this._renderTimer = setInterval(() => {
733
- this.dirtyStats = true; // always refresh uptime/stats
734
- this._render();
735
- }, 250);
676
+ this._renderTimer = setInterval(() => this._render(), 250);
677
+ }
678
+
679
+ // โ”€โ”€ Internals โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
680
+
681
+ _updateSize() {
682
+ try {
683
+ this._w = process.stdout.columns || 110;
684
+ this._h = process.stdout.rows || 35;
685
+ this.windowSize = Math.max(4, this._h - 11);
686
+ } catch (_) {
687
+ this._w = 110; this._h = 35; this.windowSize = 17;
688
+ }
689
+ }
690
+
691
+ _onResize() {
692
+ clearTimeout(this._resizeTimer);
693
+ this._resizeTimer = setTimeout(() => {
694
+ this._updateSize();
695
+ if (this._active) {
696
+ this._write(A.eraseAll);
697
+ this._drawLiveView();
698
+ }
699
+ }, 100);
700
+ }
701
+
702
+ _at(r, c) { return `\x1b[${r};${c}H`; }
703
+ _write(s) { if (s) process.stdout.write(s); }
704
+
705
+ _fmtUptime(ms) {
706
+ if (!ms) return '0s';
707
+ const s = Math.floor(ms / 1000);
708
+ if (s < 60) return `${s}s`;
709
+ const m = Math.floor(s / 60);
710
+ if (m < 60) return `${m}m ${s%60}s`;
711
+ const h = Math.floor(m / 60);
712
+ if (h < 24) return `${h}h ${m%60}m`;
713
+ return `${Math.floor(h/24)}d ${h%24}h`;
736
714
  }
737
715
  }
738
716
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "7.79.0",
3
+ "version": "7.81.0",
4
4
  "description": "Dank Memer automation engine โ€” grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"