keyvoid 1.0.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.
@@ -0,0 +1,223 @@
1
+ // ─── Cat Skin ────────────────────────────────────────────────────────
2
+ // Cat defense mode: ASCII cat art that reacts to keystrokes,
3
+ // warm amber/orange palette, paw swipe animations.
4
+
5
+ import { renderCounter, renderSmallCounter } from '../components/counter.js';
6
+ import { renderUnvoidButton } from '../components/unvoid-button.js';
7
+ import { renderStatusBar, getStatusBarHeight } from '../components/status-bar.js';
8
+ import { palettes, RESET, BOLD, DIM } from '../../utils/colors.js';
9
+ import { centerText } from '../../utils/terminal.js';
10
+ import { formatCount } from '../../utils/big-digits.js';
11
+
12
+ const P = palettes.cat;
13
+
14
+ // ASCII cat art — different expressions
15
+ const CAT_IDLE = [
16
+ ' /\\_____/\\ ',
17
+ ' / o o \\ ',
18
+ ' ( == ^ == ) ',
19
+ ' ) ( ',
20
+ ' ( ) ',
21
+ ' ( ( ) ( ) ) ',
22
+ '(__(__)___(__)__)',
23
+ ];
24
+
25
+ const CAT_ALERT = [
26
+ ' /\\_____/\\ ',
27
+ ' / ◉ ◉ \\ ',
28
+ ' ( == ^ == ) ',
29
+ ' ) \\_____/ ( ',
30
+ ' ( \\ / ) ',
31
+ ' ( ( ) ( ) ) ',
32
+ '(__(__)___(__)__)',
33
+ ];
34
+
35
+ const CAT_SWIPE = [
36
+ ' /\\_____/\\ ',
37
+ ' / ◉ ◉ \\ ',
38
+ ' ( == ^ == ) ',
39
+ ' ) \\_____/ ( ',
40
+ ' ( )=======| ',
41
+ ' ( ( ) ( ) ) ',
42
+ '(__(__)___(__)__)',
43
+ ];
44
+
45
+ const CAT_ANGRY = [
46
+ ' /\\_____/\\ ',
47
+ ' / \\◉ ◉/ \\ ',
48
+ ' ( == ^ == ) ',
49
+ ' ) HISSSS ( ',
50
+ ' ( \\|||||||/ ) ',
51
+ ' ( ( ) ( ) ) ',
52
+ '(__(__)___(__)__)',
53
+ ];
54
+
55
+ const PAW_FRAMES = [
56
+ ' ╲ ',
57
+ ' ╲╲ ',
58
+ ' ╲╲╲ ',
59
+ '╲╲╲╲ ',
60
+ ' 🐾 ',
61
+ ];
62
+
63
+ const REACTIONS = [
64
+ 'BLOCKED!', 'NOPE!', 'PAW DENIED!', 'NOT TODAY!',
65
+ '*hiss*', '*swipe*', 'MY KEYBOARD!', 'NICE TRY!',
66
+ ];
67
+
68
+ let catState = 'idle';
69
+ let catTimer = 0;
70
+ let lastCatKeyCount = 0;
71
+ let currentReaction = 'BLOCKED!';
72
+ let pawFrame = -1;
73
+ let streak = 0;
74
+ let maxStreak = 0;
75
+
76
+ export function renderCat(cols, rows, state, tick) {
77
+ const lines = [];
78
+ const { keyCount = 0 } = state;
79
+
80
+ // Background
81
+ const bgLine = `${P.bg}${' '.repeat(cols)}${RESET}`;
82
+ for (let i = 0; i < rows; i++) lines.push(bgLine);
83
+
84
+ // Update cat state on new key
85
+ if (keyCount > lastCatKeyCount) {
86
+ const diff = keyCount - lastCatKeyCount;
87
+ streak += diff;
88
+ if (streak > maxStreak) maxStreak = streak;
89
+
90
+ if (streak > 20) {
91
+ catState = 'angry';
92
+ } else if (streak > 5) {
93
+ catState = 'swipe';
94
+ pawFrame = 0;
95
+ } else {
96
+ catState = 'alert';
97
+ }
98
+ catTimer = tick;
99
+ currentReaction = REACTIONS[Math.floor(Math.random() * REACTIONS.length)];
100
+ lastCatKeyCount = keyCount;
101
+ }
102
+
103
+ // Reset cat state after a delay
104
+ if (tick - catTimer > 15 && catState !== 'idle') {
105
+ catState = 'idle';
106
+ if (tick - catTimer > 30) streak = 0;
107
+ }
108
+
109
+ // Advance paw animation
110
+ if (pawFrame >= 0) {
111
+ pawFrame = Math.min(pawFrame + 1, PAW_FRAMES.length - 1);
112
+ if (tick - catTimer > 10) pawFrame = -1;
113
+ }
114
+
115
+ // ── Banner ──
116
+ const bannerText = '🐱 CAT DEFENSE ACTIVE 🐱';
117
+ const bannerFlash = tick % 10 < 5 ? P.primary : P.secondary;
118
+ lines[1] = `${P.bg}${centerText(`${bannerFlash}${BOLD}${bannerText}`, cols)}${RESET}`;
119
+
120
+ // Animated banner border
121
+ const borderChar = tick % 6 < 3 ? '═' : '━';
122
+ const bannerWidth = Math.min(40, cols - 10);
123
+ lines[2] = `${P.bg}${centerText(`${P.dim}${borderChar.repeat(bannerWidth)}`, cols)}${RESET}`;
124
+
125
+ // ── Cat ASCII Art ──
126
+ let catArt;
127
+ switch (catState) {
128
+ case 'alert': catArt = CAT_ALERT; break;
129
+ case 'swipe': catArt = CAT_SWIPE; break;
130
+ case 'angry': catArt = CAT_ANGRY; break;
131
+ default: catArt = CAT_IDLE;
132
+ }
133
+
134
+ const catColor = catState === 'angry' ? P.accent : P.fur;
135
+ const catStartRow = 4;
136
+
137
+ for (let i = 0; i < catArt.length; i++) {
138
+ if (catStartRow + i < rows - 10) {
139
+ // Color the eyes green
140
+ let artLine = catArt[i];
141
+ let colored = `${catColor}${artLine}`;
142
+ lines[catStartRow + i] = `${P.bg}${centerText(colored, cols)}${RESET}`;
143
+ }
144
+ }
145
+
146
+ // ── Paw Animation ──
147
+ if (pawFrame >= 0 && pawFrame < PAW_FRAMES.length) {
148
+ const pawRow = catStartRow + 3;
149
+ const pawOffset = Math.floor(cols / 2) + 12;
150
+ if (pawRow < rows) {
151
+ const paw = `${P.primary}${BOLD}${PAW_FRAMES[pawFrame]}`;
152
+ lines[pawRow] = `${P.bg}${centerText(catArt[3] + ' ' + paw, cols)}${RESET}`;
153
+ }
154
+ }
155
+
156
+ // ── Reaction Text ──
157
+ const reactionRow = catStartRow + catArt.length + 1;
158
+ if (reactionRow < rows - 8) {
159
+ if (catState !== 'idle') {
160
+ const reactionColor = catState === 'angry' ? P.accent : P.primary;
161
+ lines[reactionRow] = `${P.bg}${centerText(
162
+ `${reactionColor}${BOLD}✦ ${currentReaction} ✦`, cols
163
+ )}${RESET}`;
164
+ } else {
165
+ lines[reactionRow] = `${P.bg}${centerText(
166
+ `${P.dim}Watching for intruders...`, cols
167
+ )}${RESET}`;
168
+ }
169
+ }
170
+
171
+ // ── Bottom Elements ──
172
+ const statusHeight = getStatusBarHeight(state);
173
+ const buttonRow = Math.max(25, rows - statusHeight - 7);
174
+ const button = renderUnvoidButton(cols, buttonRow, tick);
175
+
176
+ const streakRow = buttonRow - 3;
177
+
178
+ const counterLines = renderCounter(keyCount, cols, tick, {
179
+ r1: 255, g1: 140, b1: 0, // Orange
180
+ r2: 255, g2: 60, b2: 60, // Red
181
+ });
182
+ const counterRow = streakRow - counterLines.length - 1;
183
+
184
+ // ── Counter ──
185
+ for (let i = 0; i < counterLines.length; i++) {
186
+ if (counterRow + i < rows - 8 && counterRow + i >= reactionRow + 1) {
187
+ lines[counterRow + i] = `${P.bg}${counterLines[i]}${RESET}`;
188
+ }
189
+ }
190
+
191
+ // ── Streak indicator ──
192
+ if (streakRow < rows - 6 && streakRow >= reactionRow + 2) {
193
+ const streakBar = '🐾'.repeat(Math.min(streak, 15));
194
+ const streakText = `${P.secondary}Streak: ${streakBar} ${streak > 0 ? `(${streak})` : ''} Best: ${maxStreak}`;
195
+ lines[streakRow] = `${P.bg}${centerText(streakText, cols)}${RESET}`;
196
+ }
197
+
198
+ // ── UNVOID Button ──
199
+ for (let i = 0; i < button.lines.length; i++) {
200
+ if (buttonRow + i >= 0 && buttonRow + i < rows - statusHeight) {
201
+ lines[buttonRow + i] = `${P.bg}${button.lines[i].replace(/\x1b\[0m/g, RESET + P.bg)}${RESET}`;
202
+ }
203
+ }
204
+
205
+ // ── Status Bar ──
206
+ const statusLines = renderStatusBar(cols, { ...state, skinName: 'cat' }, tick);
207
+ const statusStartRow = rows - statusLines.length;
208
+ for (let i = 0; i < statusLines.length; i++) {
209
+ if (statusStartRow + i < rows) {
210
+ lines[statusStartRow + i] = `${P.bg}${statusLines[i].replace(/\x1b\[0m/g, RESET + P.bg)}${RESET}`;
211
+ }
212
+ }
213
+
214
+ return {
215
+ lines,
216
+ buttonRegion: {
217
+ x: button.x,
218
+ y: buttonRow + 1,
219
+ width: button.width,
220
+ height: button.height,
221
+ },
222
+ };
223
+ }
@@ -0,0 +1,155 @@
1
+ // ─── Clean Skin ──────────────────────────────────────────────────────
2
+ // Default minimalist mode: high-contrast, big keystroke counter,
3
+ // subtle animations, and a premium dark aesthetic.
4
+
5
+ import { renderHeader } from '../components/header.js';
6
+ import { renderCounter, renderCounterLabel } from '../components/counter.js';
7
+ import { renderUnvoidButton } from '../components/unvoid-button.js';
8
+ import { renderStatusBar, getStatusBarHeight } from '../components/status-bar.js';
9
+ import { palettes, RESET, BOLD, DIM } from '../../utils/colors.js';
10
+ import { centerText, horizontalLine } from '../../utils/terminal.js';
11
+
12
+ const P = palettes.clean;
13
+
14
+ const particles = [];
15
+ let lastKeyCount = 0;
16
+ const ASCII_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()';
17
+
18
+ export function renderClean(cols, rows, state, tick) {
19
+ const lines = [];
20
+ const { keyCount = 0 } = state;
21
+
22
+ // ── Physics & Particles ──
23
+ if (keyCount > lastKeyCount) {
24
+ const diff = Math.min(keyCount - lastKeyCount, 15); // Cap simultaneous spawns
25
+ lastKeyCount = keyCount;
26
+ for (let i = 0; i < diff; i++) {
27
+ particles.push({
28
+ x: Math.floor(Math.random() * (cols - 10)) + 5,
29
+ y: rows - 12, // Start rising from above the counter
30
+ char: ASCII_CHARS[Math.floor(Math.random() * ASCII_CHARS.length)],
31
+ speed: 0.15 + Math.random() * 0.4,
32
+ opacity: 1.0,
33
+ r: Math.floor(Math.random() * 80) + 120, // Cyan/Purpleish colors
34
+ g: Math.floor(Math.random() * 150) + 50,
35
+ b: Math.floor(Math.random() * 100) + 155
36
+ });
37
+ }
38
+ }
39
+
40
+ const particleMap = {};
41
+ for (let i = particles.length - 1; i >= 0; i--) {
42
+ const p = particles[i];
43
+ p.y -= p.speed;
44
+ p.opacity -= 0.015;
45
+
46
+ if (p.y <= 9 || p.opacity <= 0) {
47
+ particles.splice(i, 1);
48
+ continue;
49
+ }
50
+ const yi = Math.floor(p.y);
51
+ const xi = Math.floor(p.x);
52
+ if (yi >= 0 && yi < rows && xi >= 0 && xi < cols) {
53
+ if (!particleMap[yi]) particleMap[yi] = [];
54
+ particleMap[yi].push(p);
55
+ }
56
+ }
57
+
58
+ // ── Background ──
59
+ for (let i = 0; i < rows; i++) {
60
+ if (particleMap[i]) {
61
+ particleMap[i].sort((a, b) => a.x - b.x);
62
+ let lineStr = P.bg;
63
+ let currX = 0;
64
+ for (const p of particleMap[i]) {
65
+ if (p.x < currX) continue;
66
+ lineStr += ' '.repeat(p.x - currX);
67
+ const r = Math.floor(p.r * p.opacity);
68
+ const g = Math.floor(p.g * p.opacity);
69
+ const b = Math.floor(p.b * p.opacity);
70
+ lineStr += `\x1b[38;2;${r};${g};${b}m${p.char}${P.bg}`;
71
+ currX = p.x + 1;
72
+ }
73
+ lineStr += ' '.repeat(Math.max(0, cols - currX)) + RESET;
74
+ lines.push(lineStr);
75
+ } else {
76
+ lines.push(`${P.bg}${' '.repeat(cols)}${RESET}`);
77
+ }
78
+ }
79
+
80
+ // ── Header (ASCII art title) ──
81
+ const headerLines = renderHeader(cols, tick);
82
+ let currentRow = 2; // Start with 2 rows padding
83
+ for (let i = 0; i < headerLines.length; i++) {
84
+ if (currentRow + i < rows) {
85
+ lines[currentRow + i] = `${P.bg}${headerLines[i]}${RESET}`;
86
+ }
87
+ }
88
+ currentRow += headerLines.length + 1;
89
+
90
+ // ── Separator ──
91
+ if (currentRow < rows) {
92
+ const sepLine = centerText(
93
+ `${P.dim}${'┈'.repeat(Math.min(50, cols - 10))}`,
94
+ cols
95
+ ) + RESET;
96
+ lines[currentRow] = `${P.bg}${sepLine}`;
97
+ currentRow += 2;
98
+ }
99
+
100
+ // ── Bottom UI Elements ──
101
+ const statusHeight = getStatusBarHeight(state);
102
+
103
+ // UNVOID Button
104
+ const buttonRow = Math.max(currentRow + 8, rows - statusHeight - 7);
105
+ const button = renderUnvoidButton(cols, buttonRow, tick);
106
+
107
+ // Counter Label
108
+ const labelRow = buttonRow - 3;
109
+
110
+ // Big Counter
111
+ const counterLines = renderCounter(keyCount, cols, tick, {
112
+ r1: 0, g1: 255, b1: 224, // Cyan
113
+ r2: 191, g2: 64, b2: 255, // Purple
114
+ });
115
+ const counterRow = labelRow - counterLines.length - 1;
116
+
117
+ // Render Counter
118
+ for (let i = 0; i < counterLines.length; i++) {
119
+ if (counterRow + i < rows && counterRow + i >= currentRow) {
120
+ lines[counterRow + i] = `${P.bg}${counterLines[i]}${RESET}`;
121
+ }
122
+ }
123
+
124
+ // Render Label
125
+ if (labelRow < rows && labelRow >= currentRow) {
126
+ lines[labelRow] = `${P.bg}${renderCounterLabel(cols, tick, P.dim)}${RESET}`;
127
+ }
128
+
129
+ // Render Button
130
+ for (let i = 0; i < button.lines.length; i++) {
131
+ if (buttonRow + i < rows - statusHeight && buttonRow + i >= currentRow) {
132
+ lines[buttonRow + i] = `${P.bg}${button.lines[i]}${RESET}`;
133
+ }
134
+ }
135
+
136
+ // ── Status Bar ──
137
+ const statusLines = renderStatusBar(cols, { ...state, skinName: 'clean' }, tick);
138
+ const statusStartRow = rows - statusLines.length;
139
+ for (let i = 0; i < statusLines.length; i++) {
140
+ if (statusStartRow + i < rows) {
141
+ lines[statusStartRow + i] = `${P.bg}${statusLines[i]}${RESET}`;
142
+ }
143
+ }
144
+
145
+ // Return button position for mouse region
146
+ return {
147
+ lines,
148
+ buttonRegion: {
149
+ x: button.x,
150
+ y: buttonRow + 1, // 1-indexed for terminal
151
+ width: button.width,
152
+ height: button.height,
153
+ },
154
+ };
155
+ }
@@ -0,0 +1,194 @@
1
+ // ─── Hacker Skin ──────────────────────────────────────────────────────
2
+ // Intense cyberpunk matrix digital rain.
3
+ // Uses a highly optimized vertical rain engine and fake system errors.
4
+
5
+ import { renderUnvoidButton } from '../components/unvoid-button.js';
6
+ import { renderStatusBar, getStatusBarHeight } from '../components/status-bar.js';
7
+ import { palettes, RESET, BOLD, DIM } from '../../utils/colors.js';
8
+ import { centerText, horizontalLine } from '../../utils/terminal.js';
9
+
10
+ const P = palettes.hacker;
11
+ const ASCII_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()<>{}[]|=+_~';
12
+
13
+ // Persistent rain engine state
14
+ const rainColumns = [];
15
+ let initialized = false;
16
+ let lastKeyCount = 0;
17
+ let errors = [];
18
+
19
+ function initRain(cols, rows) {
20
+ rainColumns.length = 0;
21
+ for (let c = 0; c < cols; c++) {
22
+ rainColumns.push({
23
+ head: Math.random() * rows - Math.random() * rows,
24
+ length: Math.floor(Math.random() * 20) + 10,
25
+ speed: 0.15 + Math.random() * 0.4,
26
+ chars: Array.from({ length: rows }, () => ASCII_CHARS[Math.floor(Math.random() * ASCII_CHARS.length)])
27
+ });
28
+ }
29
+ initialized = true;
30
+ }
31
+
32
+ export function renderHacker(cols, rows, state, tick) {
33
+ const lines = [];
34
+ const { keyCount = 0 } = state;
35
+
36
+ // Handle terminal resizes naturally
37
+ if (!initialized || rainColumns.length !== cols) {
38
+ initRain(cols, rows);
39
+ }
40
+
41
+ // ── Engine Updates ──
42
+ if (keyCount > lastKeyCount) {
43
+ const diff = keyCount - lastKeyCount;
44
+ // Rapidly boost rain speeds on keystrokes
45
+ for (let i = 0; i < Math.min(diff * 5, cols); i++) {
46
+ const colIdx = Math.floor(Math.random() * cols);
47
+ rainColumns[colIdx].speed += 0.5 + Math.random() * 1.5;
48
+ }
49
+
50
+ // Add fake error
51
+ const ERROR_MSGS = [
52
+ 'FATAL: KEYBOARD INPUT BLOCKED',
53
+ 'WARNING: BRUTE FORCE DETECTED',
54
+ 'SYSTEM: UNAUTHORIZED OVERRIDE',
55
+ 'ACCESS DENIED: PROTOCOL 94',
56
+ 'ERROR: IO_EXCEPTION_0x8F',
57
+ 'SECURITY: INTRUDER LOCKDOWN'
58
+ ];
59
+ errors.push(ERROR_MSGS[Math.floor(Math.random() * ERROR_MSGS.length)]);
60
+ if (errors.length > 6) errors.shift();
61
+
62
+ lastKeyCount = keyCount;
63
+ }
64
+
65
+ // Update rain positions
66
+ for (let c = 0; c < cols; c++) {
67
+ const col = rainColumns[c];
68
+ col.head += col.speed;
69
+
70
+ // Slow down burst speeds gradually
71
+ if (col.speed > 0.6) {
72
+ col.speed *= 0.95;
73
+ }
74
+
75
+ if (col.head - col.length > rows) {
76
+ col.head = -Math.floor(Math.random() * 15);
77
+ col.speed = 0.15 + Math.random() * 0.4;
78
+ }
79
+
80
+ if (Math.random() < 0.05) {
81
+ col.chars[Math.floor(Math.random() * rows)] = ASCII_CHARS[Math.floor(Math.random() * ASCII_CHARS.length)];
82
+ }
83
+ }
84
+
85
+ // ── Render Rain Buffer ──
86
+ for (let r = 0; r < rows; r++) {
87
+ let rowStr = '';
88
+ let currColor = P.bg;
89
+ let currRun = '';
90
+
91
+ for (let c = 0; c < cols; c++) {
92
+ const col = rainColumns[c];
93
+ const headDist = Math.floor(col.head) - r;
94
+
95
+ let char = ' ';
96
+ let color = P.bg;
97
+
98
+ if (headDist === 0) {
99
+ char = col.chars[r];
100
+ color = `\x1b[38;2;200;255;200m`; // White/green flash at the head
101
+ } else if (headDist > 0 && headDist < col.length) {
102
+ char = col.chars[r];
103
+ const g = Math.max(20, 255 - (headDist * 16));
104
+ color = `\x1b[38;2;0;${g};0m`;
105
+ }
106
+
107
+ // Paint bg using the P.bg block string so gaps inherit the global terminal black
108
+ if (char === ' ') {
109
+ char = ' ';
110
+ color = P.bg;
111
+ } else {
112
+ color = P.bg + color;
113
+ }
114
+
115
+ if (color !== currColor) {
116
+ rowStr += currColor + currRun;
117
+ currRun = char;
118
+ currColor = color;
119
+ } else {
120
+ currRun += char;
121
+ }
122
+ }
123
+
124
+ rowStr += currColor + currRun + RESET;
125
+ lines.push(rowStr);
126
+ }
127
+
128
+ // ── Central Error Console ──
129
+ const consoleW = Math.min(46, cols - 4);
130
+ const consoleH = 10;
131
+ const consoleY = Math.floor(rows / 2) - Math.floor(consoleH / 2) - 3;
132
+
133
+ if (consoleY > 2 && consoleW >= 30) {
134
+ const topBorder = `${P.terminalBg}${P.dim}┌${'─'.repeat(consoleW - 2)}┐`;
135
+ const bottomBorder = `${P.terminalBg}${P.dim}└${'─'.repeat(consoleW - 2)}┘`;
136
+ const emptyLine = `${P.terminalBg}${' '.repeat(consoleW)}`;
137
+
138
+ // Draw rigid background shadow for console box overwriting the rain
139
+ lines[consoleY] = `${P.bg}${centerText(topBorder, cols)}${RESET}`;
140
+
141
+ for (let i = 1; i < consoleH - 1; i++) {
142
+ let content = '';
143
+ if (i === 1) content = `${P.terminalBg}${P.error}${BOLD} SYSTEM LOCK PROTOCOL ACTIVE `;
144
+ else if (i === 2) content = `${P.terminalBg}${P.dim} ${'━'.repeat(consoleW - 4)} `;
145
+ else if (i - 3 < errors.length && i >= 3) {
146
+ content = `${P.terminalBg}\x1b[38;2;180;0;0m > ${errors[i - 3]} `;
147
+ }
148
+
149
+ let cText = content;
150
+ if (!cText) cText = emptyLine;
151
+ else {
152
+ // Pad the content with the background color to match width perfectly
153
+ // (Since centerText relies on ANSI stripping)
154
+ cText = cText.padEnd(consoleW + (cText.length - cText.replace(/\x1b\[[0-9;]*m/g, '').length), ' ');
155
+ }
156
+
157
+ // Wrap with walls
158
+ const rowContent = `${P.terminalBg}${P.dim}│${RESET}${cText}${P.terminalBg}${P.dim}│`;
159
+ lines[consoleY + i] = `${P.bg}${centerText(rowContent, cols)}${RESET}`;
160
+ }
161
+
162
+ lines[consoleY + consoleH - 1] = `${P.bg}${centerText(bottomBorder, cols)}${RESET}`;
163
+ }
164
+
165
+ // ── UNVOID Button ──
166
+ const statusHeight = getStatusBarHeight(state);
167
+ const buttonRow = Math.max(consoleY + consoleH + 2, rows - statusHeight - 7);
168
+ const button = renderUnvoidButton(cols, buttonRow, tick);
169
+
170
+ for (let i = 0; i < button.lines.length; i++) {
171
+ if (buttonRow + i < rows - statusHeight) {
172
+ lines[buttonRow + i] = `${P.bg}${button.lines[i].replace(/\x1b\[0m/g, RESET + P.bg)}${RESET}`;
173
+ }
174
+ }
175
+
176
+ // ── Status Bar ──
177
+ const statusLines = renderStatusBar(cols, { ...state, skinName: 'hacker' }, tick);
178
+ const statusStartRow = rows - statusLines.length;
179
+ for (let i = 0; i < statusLines.length; i++) {
180
+ if (statusStartRow + i < rows) {
181
+ lines[statusStartRow + i] = `${P.bg}${statusLines[i].replace(/\x1b\[0m/g, RESET + P.bg)}${RESET}`;
182
+ }
183
+ }
184
+
185
+ return {
186
+ lines,
187
+ buttonRegion: {
188
+ x: button.x,
189
+ y: buttonRow + 1,
190
+ width: button.width,
191
+ height: button.height,
192
+ },
193
+ };
194
+ }