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.
- package/bin/codeep.js +1 -1
- package/dist/config/index.js +10 -10
- package/dist/renderer/App.d.ts +430 -0
- package/dist/renderer/App.js +2712 -0
- package/dist/renderer/ChatUI.d.ts +71 -0
- package/dist/renderer/ChatUI.js +286 -0
- package/dist/renderer/Input.d.ts +72 -0
- package/dist/renderer/Input.js +371 -0
- package/dist/renderer/Screen.d.ts +79 -0
- package/dist/renderer/Screen.js +278 -0
- package/dist/renderer/ansi.d.ts +99 -0
- package/dist/renderer/ansi.js +176 -0
- package/dist/renderer/components/Box.d.ts +64 -0
- package/dist/renderer/components/Box.js +90 -0
- package/dist/renderer/components/Help.d.ts +30 -0
- package/dist/renderer/components/Help.js +195 -0
- package/dist/renderer/components/Intro.d.ts +12 -0
- package/dist/renderer/components/Intro.js +128 -0
- package/dist/renderer/components/Login.d.ts +42 -0
- package/dist/renderer/components/Login.js +178 -0
- package/dist/renderer/components/Modal.d.ts +43 -0
- package/dist/renderer/components/Modal.js +207 -0
- package/dist/renderer/components/Permission.d.ts +20 -0
- package/dist/renderer/components/Permission.js +113 -0
- package/dist/renderer/components/SelectScreen.d.ts +26 -0
- package/dist/renderer/components/SelectScreen.js +101 -0
- package/dist/renderer/components/Settings.d.ts +37 -0
- package/dist/renderer/components/Settings.js +333 -0
- package/dist/renderer/components/Status.d.ts +18 -0
- package/dist/renderer/components/Status.js +78 -0
- package/dist/renderer/demo-app.d.ts +6 -0
- package/dist/renderer/demo-app.js +85 -0
- package/dist/renderer/demo.d.ts +6 -0
- package/dist/renderer/demo.js +52 -0
- package/dist/renderer/index.d.ts +16 -0
- package/dist/renderer/index.js +17 -0
- package/dist/renderer/main.d.ts +6 -0
- package/dist/renderer/main.js +1634 -0
- package/dist/utils/agent.d.ts +21 -0
- package/dist/utils/agent.js +29 -0
- package/dist/utils/clipboard.d.ts +15 -0
- package/dist/utils/clipboard.js +95 -0
- package/package.json +7 -11
- package/dist/utils/console.d.ts +0 -55
- 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
|
+
}
|