codeep 1.1.12 → 1.1.13

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.
Files changed (45) hide show
  1. package/bin/codeep.js +1 -1
  2. package/dist/config/index.js +10 -10
  3. package/dist/renderer/App.d.ts +430 -0
  4. package/dist/renderer/App.js +2712 -0
  5. package/dist/renderer/ChatUI.d.ts +71 -0
  6. package/dist/renderer/ChatUI.js +286 -0
  7. package/dist/renderer/Input.d.ts +72 -0
  8. package/dist/renderer/Input.js +371 -0
  9. package/dist/renderer/Screen.d.ts +79 -0
  10. package/dist/renderer/Screen.js +278 -0
  11. package/dist/renderer/ansi.d.ts +99 -0
  12. package/dist/renderer/ansi.js +176 -0
  13. package/dist/renderer/components/Box.d.ts +64 -0
  14. package/dist/renderer/components/Box.js +90 -0
  15. package/dist/renderer/components/Help.d.ts +30 -0
  16. package/dist/renderer/components/Help.js +195 -0
  17. package/dist/renderer/components/Intro.d.ts +12 -0
  18. package/dist/renderer/components/Intro.js +128 -0
  19. package/dist/renderer/components/Login.d.ts +42 -0
  20. package/dist/renderer/components/Login.js +178 -0
  21. package/dist/renderer/components/Modal.d.ts +43 -0
  22. package/dist/renderer/components/Modal.js +207 -0
  23. package/dist/renderer/components/Permission.d.ts +20 -0
  24. package/dist/renderer/components/Permission.js +113 -0
  25. package/dist/renderer/components/SelectScreen.d.ts +26 -0
  26. package/dist/renderer/components/SelectScreen.js +101 -0
  27. package/dist/renderer/components/Settings.d.ts +37 -0
  28. package/dist/renderer/components/Settings.js +333 -0
  29. package/dist/renderer/components/Status.d.ts +18 -0
  30. package/dist/renderer/components/Status.js +78 -0
  31. package/dist/renderer/demo-app.d.ts +6 -0
  32. package/dist/renderer/demo-app.js +85 -0
  33. package/dist/renderer/demo.d.ts +6 -0
  34. package/dist/renderer/demo.js +52 -0
  35. package/dist/renderer/index.d.ts +16 -0
  36. package/dist/renderer/index.js +17 -0
  37. package/dist/renderer/main.d.ts +6 -0
  38. package/dist/renderer/main.js +1634 -0
  39. package/dist/utils/agent.d.ts +21 -0
  40. package/dist/utils/agent.js +29 -0
  41. package/dist/utils/clipboard.d.ts +15 -0
  42. package/dist/utils/clipboard.js +95 -0
  43. package/package.json +7 -11
  44. package/dist/utils/console.d.ts +0 -55
  45. package/dist/utils/console.js +0 -188
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Screen buffer with diff-based rendering
3
+ * Only writes changes to terminal - minimizes flickering
4
+ */
5
+ import { cursor, screen, style, visibleLength } from './ansi.js';
6
+ export class Screen {
7
+ width;
8
+ height;
9
+ buffer;
10
+ rendered;
11
+ cursorX = 0;
12
+ cursorY = 0;
13
+ cursorVisible = true;
14
+ constructor() {
15
+ this.width = process.stdout.columns || 80;
16
+ this.height = process.stdout.rows || 24;
17
+ this.buffer = this.createEmptyBuffer();
18
+ this.rendered = this.createEmptyBuffer();
19
+ // Handle resize
20
+ process.stdout.on('resize', () => {
21
+ this.width = process.stdout.columns || 80;
22
+ this.height = process.stdout.rows || 24;
23
+ this.buffer = this.createEmptyBuffer();
24
+ this.rendered = this.createEmptyBuffer();
25
+ this.fullRender();
26
+ });
27
+ }
28
+ createEmptyBuffer() {
29
+ const buffer = [];
30
+ for (let y = 0; y < this.height; y++) {
31
+ const row = [];
32
+ for (let x = 0; x < this.width; x++) {
33
+ row.push({ char: ' ', style: '' });
34
+ }
35
+ buffer.push(row);
36
+ }
37
+ return buffer;
38
+ }
39
+ /**
40
+ * Get terminal dimensions
41
+ */
42
+ getSize() {
43
+ return { width: this.width, height: this.height };
44
+ }
45
+ /**
46
+ * Clear the buffer
47
+ */
48
+ clear() {
49
+ this.buffer = this.createEmptyBuffer();
50
+ }
51
+ /**
52
+ * Write text at position
53
+ */
54
+ write(x, y, text, textStyle = '') {
55
+ if (y < 0 || y >= this.height)
56
+ return;
57
+ let col = x;
58
+ let inEscape = false;
59
+ let currentStyle = textStyle;
60
+ for (const char of text) {
61
+ if (char === '\x1b') {
62
+ inEscape = true;
63
+ currentStyle += char;
64
+ }
65
+ else if (inEscape) {
66
+ currentStyle += char;
67
+ if (char.match(/[a-zA-Z]/)) {
68
+ inEscape = false;
69
+ }
70
+ }
71
+ else if (char === '\n') {
72
+ // Newline - would need to handle multi-line writes
73
+ break;
74
+ }
75
+ else {
76
+ if (col >= 0 && col < this.width) {
77
+ this.buffer[y][col] = { char, style: currentStyle };
78
+ }
79
+ col++;
80
+ }
81
+ }
82
+ }
83
+ /**
84
+ * Write a line, clearing rest of line
85
+ */
86
+ writeLine(y, text, textStyle = '') {
87
+ if (y < 0 || y >= this.height)
88
+ return;
89
+ // Clear the line first
90
+ for (let x = 0; x < this.width; x++) {
91
+ this.buffer[y][x] = { char: ' ', style: '' };
92
+ }
93
+ this.write(0, y, text, textStyle);
94
+ }
95
+ /**
96
+ * Write raw line with pre-formatted ANSI codes (for syntax highlighted content)
97
+ * This writes directly without parsing for styles since the text already contains ANSI escapes
98
+ */
99
+ writeRaw(y, text, prefixStyle = '') {
100
+ if (y < 0 || y >= this.height)
101
+ return;
102
+ // Clear the line first
103
+ for (let x = 0; x < this.width; x++) {
104
+ this.buffer[y][x] = { char: ' ', style: '' };
105
+ }
106
+ // Parse text character by character, tracking ANSI escape sequences
107
+ let col = 0;
108
+ let i = 0;
109
+ let currentStyle = prefixStyle;
110
+ while (i < text.length && col < this.width) {
111
+ // Check for ANSI escape sequence
112
+ if (text[i] === '\x1b' && text[i + 1] === '[') {
113
+ // Find end of escape sequence (ends with letter)
114
+ let escEnd = i + 2;
115
+ while (escEnd < text.length && !text[escEnd].match(/[a-zA-Z]/)) {
116
+ escEnd++;
117
+ }
118
+ if (escEnd < text.length) {
119
+ const escSeq = text.slice(i, escEnd + 1);
120
+ // Reset code clears style, otherwise accumulate
121
+ if (escSeq === '\x1b[0m') {
122
+ currentStyle = prefixStyle;
123
+ }
124
+ else {
125
+ currentStyle += escSeq;
126
+ }
127
+ i = escEnd + 1;
128
+ }
129
+ else {
130
+ i++;
131
+ }
132
+ }
133
+ else {
134
+ // Regular character
135
+ if (col < this.width) {
136
+ this.buffer[y][col] = { char: text[i], style: currentStyle };
137
+ col++;
138
+ }
139
+ i++;
140
+ }
141
+ }
142
+ }
143
+ /**
144
+ * Write multiple lines starting at y
145
+ */
146
+ writeLines(startY, lines, textStyle = '') {
147
+ let y = startY;
148
+ for (const line of lines) {
149
+ if (y >= this.height)
150
+ break;
151
+ this.writeLine(y, line, textStyle);
152
+ y++;
153
+ }
154
+ return y; // Return next available line
155
+ }
156
+ /**
157
+ * Write text with word wrapping
158
+ */
159
+ writeWrapped(x, y, text, maxWidth, textStyle = '') {
160
+ const words = text.split(' ');
161
+ let line = '';
162
+ let lineLength = 0;
163
+ let currentY = y;
164
+ for (const word of words) {
165
+ const wordLength = visibleLength(word);
166
+ if (lineLength + wordLength + 1 > maxWidth && line) {
167
+ this.write(x, currentY, line, textStyle);
168
+ currentY++;
169
+ if (currentY >= this.height)
170
+ break;
171
+ line = word;
172
+ lineLength = wordLength;
173
+ }
174
+ else {
175
+ line += (line ? ' ' : '') + word;
176
+ lineLength += wordLength + (line.length > wordLength ? 1 : 0);
177
+ }
178
+ }
179
+ if (line && currentY < this.height) {
180
+ this.write(x, currentY, line, textStyle);
181
+ currentY++;
182
+ }
183
+ return currentY; // Return next available line
184
+ }
185
+ /**
186
+ * Draw a horizontal line
187
+ */
188
+ horizontalLine(y, char = '─', textStyle = '') {
189
+ const line = char.repeat(this.width);
190
+ this.writeLine(y, line, textStyle);
191
+ }
192
+ /**
193
+ * Set cursor position for input
194
+ */
195
+ setCursor(x, y) {
196
+ this.cursorX = Math.max(0, Math.min(x, this.width - 1));
197
+ this.cursorY = Math.max(0, Math.min(y, this.height - 1));
198
+ }
199
+ /**
200
+ * Show/hide cursor
201
+ */
202
+ showCursor(visible) {
203
+ this.cursorVisible = visible;
204
+ }
205
+ /**
206
+ * Render only changed cells (diff render)
207
+ */
208
+ render() {
209
+ let output = '';
210
+ let lastStyle = '';
211
+ for (let y = 0; y < this.height; y++) {
212
+ for (let x = 0; x < this.width; x++) {
213
+ const cell = this.buffer[y][x];
214
+ const renderedCell = this.rendered[y][x];
215
+ // Skip if unchanged
216
+ if (cell.char === renderedCell.char && cell.style === renderedCell.style) {
217
+ continue;
218
+ }
219
+ // Move cursor and write
220
+ output += cursor.to(y + 1, x + 1);
221
+ if (cell.style !== lastStyle) {
222
+ output += style.reset + cell.style;
223
+ lastStyle = cell.style;
224
+ }
225
+ output += cell.char;
226
+ // Update rendered buffer
227
+ this.rendered[y][x] = { ...cell };
228
+ }
229
+ }
230
+ // Reset style and position cursor
231
+ output += style.reset;
232
+ output += cursor.to(this.cursorY + 1, this.cursorX + 1);
233
+ output += this.cursorVisible ? cursor.show : cursor.hide;
234
+ if (output) {
235
+ process.stdout.write(output);
236
+ }
237
+ }
238
+ /**
239
+ * Full render (no diff, redraw everything)
240
+ */
241
+ fullRender() {
242
+ let output = cursor.hide + cursor.home;
243
+ let lastStyle = '';
244
+ for (let y = 0; y < this.height; y++) {
245
+ for (let x = 0; x < this.width; x++) {
246
+ const cell = this.buffer[y][x];
247
+ if (cell.style !== lastStyle) {
248
+ output += style.reset + cell.style;
249
+ lastStyle = cell.style;
250
+ }
251
+ output += cell.char;
252
+ // Update rendered buffer
253
+ this.rendered[y][x] = { ...cell };
254
+ }
255
+ // Don't add newline after last row
256
+ if (y < this.height - 1) {
257
+ output += '\r\n';
258
+ }
259
+ }
260
+ // Reset style and position cursor
261
+ output += style.reset;
262
+ output += cursor.to(this.cursorY + 1, this.cursorX + 1);
263
+ output += this.cursorVisible ? cursor.show : cursor.hide;
264
+ process.stdout.write(output);
265
+ }
266
+ /**
267
+ * Initialize screen (hide cursor, clear)
268
+ */
269
+ init() {
270
+ process.stdout.write(cursor.hide + screen.clear + cursor.home);
271
+ }
272
+ /**
273
+ * Cleanup (show cursor, clear)
274
+ */
275
+ cleanup() {
276
+ process.stdout.write(style.reset + screen.clear + cursor.home + cursor.show);
277
+ }
278
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * ANSI escape codes for terminal control
3
+ * Low-level building blocks for custom renderer
4
+ */
5
+ export declare const cursor: {
6
+ hide: string;
7
+ show: string;
8
+ home: string;
9
+ to: (row: number, col: number) => string;
10
+ up: (n?: number) => string;
11
+ down: (n?: number) => string;
12
+ right: (n?: number) => string;
13
+ left: (n?: number) => string;
14
+ save: string;
15
+ restore: string;
16
+ getPosition: string;
17
+ };
18
+ export declare const screen: {
19
+ clear: string;
20
+ clearLine: string;
21
+ clearToEnd: string;
22
+ clearToLineEnd: string;
23
+ setScrollRegion: (top: number, bottom: number) => string;
24
+ resetScrollRegion: string;
25
+ enterAltBuffer: string;
26
+ exitAltBuffer: string;
27
+ };
28
+ export declare const fg: {
29
+ black: string;
30
+ red: string;
31
+ green: string;
32
+ yellow: string;
33
+ blue: string;
34
+ magenta: string;
35
+ cyan: string;
36
+ white: string;
37
+ gray: string;
38
+ brightRed: string;
39
+ brightGreen: string;
40
+ brightYellow: string;
41
+ brightBlue: string;
42
+ brightMagenta: string;
43
+ brightCyan: string;
44
+ brightWhite: string;
45
+ color256: (n: number) => string;
46
+ rgb: (r: number, g: number, b: number) => string;
47
+ };
48
+ export declare const bg: {
49
+ black: string;
50
+ red: string;
51
+ green: string;
52
+ yellow: string;
53
+ blue: string;
54
+ magenta: string;
55
+ cyan: string;
56
+ white: string;
57
+ gray: string;
58
+ color256: (n: number) => string;
59
+ rgb: (r: number, g: number, b: number) => string;
60
+ };
61
+ export declare const style: {
62
+ reset: string;
63
+ bold: string;
64
+ dim: string;
65
+ italic: string;
66
+ underline: string;
67
+ blink: string;
68
+ inverse: string;
69
+ hidden: string;
70
+ strikethrough: string;
71
+ resetBold: string;
72
+ resetDim: string;
73
+ resetItalic: string;
74
+ resetUnderline: string;
75
+ resetBlink: string;
76
+ resetInverse: string;
77
+ resetHidden: string;
78
+ resetStrikethrough: string;
79
+ };
80
+ /**
81
+ * Helper to create styled text
82
+ */
83
+ export declare function styled(text: string, ...styles: string[]): string;
84
+ /**
85
+ * Strip ANSI codes from string (for length calculation)
86
+ */
87
+ export declare function stripAnsi(str: string): string;
88
+ /**
89
+ * Get visible length of string (excluding ANSI codes)
90
+ */
91
+ export declare function visibleLength(str: string): number;
92
+ /**
93
+ * Truncate string to visible length, preserving ANSI codes
94
+ */
95
+ export declare function truncate(str: string, maxLength: number, suffix?: string): string;
96
+ /**
97
+ * Wrap text to fit width, respecting ANSI codes
98
+ */
99
+ export declare function wordWrap(str: string, width: number): string[];
@@ -0,0 +1,176 @@
1
+ /**
2
+ * ANSI escape codes for terminal control
3
+ * Low-level building blocks for custom renderer
4
+ */
5
+ // Cursor control
6
+ export const cursor = {
7
+ hide: '\x1b[?25l',
8
+ show: '\x1b[?25h',
9
+ home: '\x1b[H',
10
+ // Move cursor to position (1-indexed)
11
+ to: (row, col) => `\x1b[${row};${col}H`,
12
+ // Move cursor relative
13
+ up: (n = 1) => `\x1b[${n}A`,
14
+ down: (n = 1) => `\x1b[${n}B`,
15
+ right: (n = 1) => `\x1b[${n}C`,
16
+ left: (n = 1) => `\x1b[${n}D`,
17
+ // Save/restore position
18
+ save: '\x1b[s',
19
+ restore: '\x1b[u',
20
+ // Get position (requires reading response)
21
+ getPosition: '\x1b[6n',
22
+ };
23
+ // Screen control
24
+ export const screen = {
25
+ clear: '\x1b[2J',
26
+ clearLine: '\x1b[2K',
27
+ clearToEnd: '\x1b[J',
28
+ clearToLineEnd: '\x1b[K',
29
+ // Scroll region
30
+ setScrollRegion: (top, bottom) => `\x1b[${top};${bottom}r`,
31
+ resetScrollRegion: '\x1b[r',
32
+ // Alternative screen buffer (like vim uses)
33
+ enterAltBuffer: '\x1b[?1049h',
34
+ exitAltBuffer: '\x1b[?1049l',
35
+ };
36
+ // Colors - basic 16 colors
37
+ export const fg = {
38
+ black: '\x1b[30m',
39
+ red: '\x1b[31m',
40
+ green: '\x1b[32m',
41
+ yellow: '\x1b[33m',
42
+ blue: '\x1b[34m',
43
+ magenta: '\x1b[35m',
44
+ cyan: '\x1b[36m',
45
+ white: '\x1b[37m',
46
+ gray: '\x1b[90m',
47
+ // Bright variants
48
+ brightRed: '\x1b[91m',
49
+ brightGreen: '\x1b[92m',
50
+ brightYellow: '\x1b[93m',
51
+ brightBlue: '\x1b[94m',
52
+ brightMagenta: '\x1b[95m',
53
+ brightCyan: '\x1b[96m',
54
+ brightWhite: '\x1b[97m',
55
+ // 256 color
56
+ color256: (n) => `\x1b[38;5;${n}m`,
57
+ // RGB
58
+ rgb: (r, g, b) => `\x1b[38;2;${r};${g};${b}m`,
59
+ };
60
+ export const bg = {
61
+ black: '\x1b[40m',
62
+ red: '\x1b[41m',
63
+ green: '\x1b[42m',
64
+ yellow: '\x1b[43m',
65
+ blue: '\x1b[44m',
66
+ magenta: '\x1b[45m',
67
+ cyan: '\x1b[46m',
68
+ white: '\x1b[47m',
69
+ gray: '\x1b[100m',
70
+ // 256 color
71
+ color256: (n) => `\x1b[48;5;${n}m`,
72
+ // RGB
73
+ rgb: (r, g, b) => `\x1b[48;2;${r};${g};${b}m`,
74
+ };
75
+ // Text styles
76
+ export const style = {
77
+ reset: '\x1b[0m',
78
+ bold: '\x1b[1m',
79
+ dim: '\x1b[2m',
80
+ italic: '\x1b[3m',
81
+ underline: '\x1b[4m',
82
+ blink: '\x1b[5m',
83
+ inverse: '\x1b[7m',
84
+ hidden: '\x1b[8m',
85
+ strikethrough: '\x1b[9m',
86
+ // Reset specific
87
+ resetBold: '\x1b[22m',
88
+ resetDim: '\x1b[22m',
89
+ resetItalic: '\x1b[23m',
90
+ resetUnderline: '\x1b[24m',
91
+ resetBlink: '\x1b[25m',
92
+ resetInverse: '\x1b[27m',
93
+ resetHidden: '\x1b[28m',
94
+ resetStrikethrough: '\x1b[29m',
95
+ };
96
+ /**
97
+ * Helper to create styled text
98
+ */
99
+ export function styled(text, ...styles) {
100
+ if (styles.length === 0)
101
+ return text;
102
+ return styles.join('') + text + style.reset;
103
+ }
104
+ /**
105
+ * Strip ANSI codes from string (for length calculation)
106
+ */
107
+ export function stripAnsi(str) {
108
+ // eslint-disable-next-line no-control-regex
109
+ return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
110
+ }
111
+ /**
112
+ * Get visible length of string (excluding ANSI codes)
113
+ */
114
+ export function visibleLength(str) {
115
+ return stripAnsi(str).length;
116
+ }
117
+ /**
118
+ * Truncate string to visible length, preserving ANSI codes
119
+ */
120
+ export function truncate(str, maxLength, suffix = '...') {
121
+ const visible = stripAnsi(str);
122
+ if (visible.length <= maxLength)
123
+ return str;
124
+ // Simple truncation - may cut ANSI codes
125
+ // For proper handling, we'd need to parse ANSI sequences
126
+ let visibleCount = 0;
127
+ let result = '';
128
+ let inEscape = false;
129
+ for (const char of str) {
130
+ if (char === '\x1b') {
131
+ inEscape = true;
132
+ result += char;
133
+ }
134
+ else if (inEscape) {
135
+ result += char;
136
+ if (char.match(/[a-zA-Z]/)) {
137
+ inEscape = false;
138
+ }
139
+ }
140
+ else {
141
+ if (visibleCount < maxLength - suffix.length) {
142
+ result += char;
143
+ visibleCount++;
144
+ }
145
+ else {
146
+ break;
147
+ }
148
+ }
149
+ }
150
+ return result + style.reset + suffix;
151
+ }
152
+ /**
153
+ * Wrap text to fit width, respecting ANSI codes
154
+ */
155
+ export function wordWrap(str, width) {
156
+ const lines = [];
157
+ const words = str.split(' ');
158
+ let currentLine = '';
159
+ let currentLength = 0;
160
+ for (const word of words) {
161
+ const wordLength = visibleLength(word);
162
+ if (currentLength + wordLength + 1 > width && currentLine) {
163
+ lines.push(currentLine);
164
+ currentLine = word;
165
+ currentLength = wordLength;
166
+ }
167
+ else {
168
+ currentLine += (currentLine ? ' ' : '') + word;
169
+ currentLength += wordLength + (currentLine ? 1 : 0);
170
+ }
171
+ }
172
+ if (currentLine) {
173
+ lines.push(currentLine);
174
+ }
175
+ return lines;
176
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Box drawing utilities for borders, frames, modals
3
+ */
4
+ export declare const boxChars: {
5
+ single: {
6
+ topLeft: string;
7
+ topRight: string;
8
+ bottomLeft: string;
9
+ bottomRight: string;
10
+ horizontal: string;
11
+ vertical: string;
12
+ };
13
+ double: {
14
+ topLeft: string;
15
+ topRight: string;
16
+ bottomLeft: string;
17
+ bottomRight: string;
18
+ horizontal: string;
19
+ vertical: string;
20
+ };
21
+ rounded: {
22
+ topLeft: string;
23
+ topRight: string;
24
+ bottomLeft: string;
25
+ bottomRight: string;
26
+ horizontal: string;
27
+ vertical: string;
28
+ };
29
+ heavy: {
30
+ topLeft: string;
31
+ topRight: string;
32
+ bottomLeft: string;
33
+ bottomRight: string;
34
+ horizontal: string;
35
+ vertical: string;
36
+ };
37
+ };
38
+ export type BoxStyle = keyof typeof boxChars;
39
+ export interface BoxOptions {
40
+ x: number;
41
+ y: number;
42
+ width: number;
43
+ height: number;
44
+ style?: BoxStyle;
45
+ title?: string;
46
+ titleAlign?: 'left' | 'center' | 'right';
47
+ borderColor?: string;
48
+ titleColor?: string;
49
+ }
50
+ /**
51
+ * Generate box lines for rendering
52
+ */
53
+ export declare function createBox(options: BoxOptions): Array<{
54
+ y: number;
55
+ text: string;
56
+ style: string;
57
+ }>;
58
+ /**
59
+ * Center a box on screen
60
+ */
61
+ export declare function centerBox(screenWidth: number, screenHeight: number, boxWidth: number, boxHeight: number): {
62
+ x: number;
63
+ y: number;
64
+ };
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Box drawing utilities for borders, frames, modals
3
+ */
4
+ // Box drawing characters (Unicode)
5
+ export const boxChars = {
6
+ // Single line
7
+ single: {
8
+ topLeft: '┌',
9
+ topRight: '┐',
10
+ bottomLeft: '└',
11
+ bottomRight: '┘',
12
+ horizontal: '─',
13
+ vertical: '│',
14
+ },
15
+ // Double line
16
+ double: {
17
+ topLeft: '╔',
18
+ topRight: '╗',
19
+ bottomLeft: '╚',
20
+ bottomRight: '╝',
21
+ horizontal: '═',
22
+ vertical: '║',
23
+ },
24
+ // Rounded
25
+ rounded: {
26
+ topLeft: '╭',
27
+ topRight: '╮',
28
+ bottomLeft: '╰',
29
+ bottomRight: '╯',
30
+ horizontal: '─',
31
+ vertical: '│',
32
+ },
33
+ // Heavy
34
+ heavy: {
35
+ topLeft: '┏',
36
+ topRight: '┓',
37
+ bottomLeft: '┗',
38
+ bottomRight: '┛',
39
+ horizontal: '━',
40
+ vertical: '┃',
41
+ },
42
+ };
43
+ /**
44
+ * Generate box lines for rendering
45
+ */
46
+ export function createBox(options) {
47
+ const { x, y, width, height, style: boxStyle = 'single', title, titleAlign = 'center', borderColor = '', titleColor = '', } = options;
48
+ const chars = boxChars[boxStyle];
49
+ const lines = [];
50
+ // Top border with optional title
51
+ let topLine = chars.topLeft + chars.horizontal.repeat(width - 2) + chars.topRight;
52
+ if (title && width > 4) {
53
+ const maxTitleLen = width - 4;
54
+ const displayTitle = title.length > maxTitleLen ? title.slice(0, maxTitleLen - 1) + '…' : title;
55
+ const titleWithPadding = ` ${displayTitle} `;
56
+ let titlePos;
57
+ if (titleAlign === 'left') {
58
+ titlePos = 2;
59
+ }
60
+ else if (titleAlign === 'right') {
61
+ titlePos = width - titleWithPadding.length - 2;
62
+ }
63
+ else {
64
+ titlePos = Math.floor((width - titleWithPadding.length) / 2);
65
+ }
66
+ // Insert title into top line
67
+ const before = chars.topLeft + chars.horizontal.repeat(titlePos - 1);
68
+ const after = chars.horizontal.repeat(width - titlePos - titleWithPadding.length - 1) + chars.topRight;
69
+ topLine = before + (titleColor || borderColor) + titleWithPadding + borderColor + after;
70
+ }
71
+ lines.push({ y, text: ' '.repeat(x) + topLine, style: borderColor });
72
+ // Middle lines (empty content area)
73
+ const emptyLine = chars.vertical + ' '.repeat(width - 2) + chars.vertical;
74
+ for (let row = 1; row < height - 1; row++) {
75
+ lines.push({ y: y + row, text: ' '.repeat(x) + emptyLine, style: borderColor });
76
+ }
77
+ // Bottom border
78
+ const bottomLine = chars.bottomLeft + chars.horizontal.repeat(width - 2) + chars.bottomRight;
79
+ lines.push({ y: y + height - 1, text: ' '.repeat(x) + bottomLine, style: borderColor });
80
+ return lines;
81
+ }
82
+ /**
83
+ * Center a box on screen
84
+ */
85
+ export function centerBox(screenWidth, screenHeight, boxWidth, boxHeight) {
86
+ return {
87
+ x: Math.floor((screenWidth - boxWidth) / 2),
88
+ y: Math.floor((screenHeight - boxHeight) / 2),
89
+ };
90
+ }