dbn-cli 0.3.0 → 0.5.2
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/README.md +1 -1
- package/package.json +5 -5
- package/src/adapter/adapter.test.ts +207 -0
- package/src/adapter/base.ts +14 -1
- package/src/adapter/sqlite.ts +71 -1
- package/src/index.ts +59 -2
- package/src/repro_bug.test.ts +101 -0
- package/src/types.ts +54 -1
- package/src/ui/grit/README.md +58 -0
- package/src/ui/grit/index.test.ts +67 -0
- package/src/ui/grit/index.ts +101 -0
- package/src/ui/grit/types.ts +16 -0
- package/src/ui/grit/utils.ts +35 -0
- package/src/ui/navigator.test.ts +434 -0
- package/src/ui/navigator.ts +385 -27
- package/src/ui/navigator_sync.test.ts +95 -0
- package/src/ui/renderer.ts +247 -409
- package/src/ui/screen.ts +6 -6
- package/src/ui/theme.test.ts +30 -0
- package/src/ui/theme.ts +20 -46
- package/src/utils/format.test.ts +209 -0
- package/src/utils/format.ts +94 -21
package/src/ui/screen.ts
CHANGED
|
@@ -26,13 +26,13 @@ export class Screen extends EventEmitter {
|
|
|
26
26
|
|
|
27
27
|
// Enter alternate screen buffer
|
|
28
28
|
stdout.write('\x1b[?1049h');
|
|
29
|
-
|
|
29
|
+
|
|
30
30
|
// Hide cursor
|
|
31
31
|
stdout.write('\x1b[?25l');
|
|
32
|
-
|
|
32
|
+
|
|
33
33
|
// Clear screen
|
|
34
34
|
stdout.write('\x1b[2J\x1b[H');
|
|
35
|
-
|
|
35
|
+
|
|
36
36
|
// Listen for terminal resize
|
|
37
37
|
this.resizeHandler = () => {
|
|
38
38
|
this.width = stdout.columns || 80;
|
|
@@ -40,7 +40,7 @@ export class Screen extends EventEmitter {
|
|
|
40
40
|
this.emit('resize', { width: this.width, height: this.height } as ScreenDimensions);
|
|
41
41
|
};
|
|
42
42
|
process.on('SIGWINCH', this.resizeHandler);
|
|
43
|
-
|
|
43
|
+
|
|
44
44
|
this.isActive = true;
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -52,10 +52,10 @@ export class Screen extends EventEmitter {
|
|
|
52
52
|
|
|
53
53
|
// Show cursor
|
|
54
54
|
stdout.write('\x1b[?25h');
|
|
55
|
-
|
|
55
|
+
|
|
56
56
|
// Exit alternate screen buffer
|
|
57
57
|
stdout.write('\x1b[?1049l');
|
|
58
|
-
|
|
58
|
+
|
|
59
59
|
// Remove resize listener
|
|
60
60
|
if (this.resizeHandler) {
|
|
61
61
|
process.off('SIGWINCH', this.resizeHandler);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { THEME, UI } from '../ui/theme.ts';
|
|
4
|
+
|
|
5
|
+
describe('Theme', () => {
|
|
6
|
+
describe('THEME', () => {
|
|
7
|
+
it('should have background colors', () => {
|
|
8
|
+
assert.ok(THEME.background);
|
|
9
|
+
assert.ok(THEME.surface);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should have brand colors', () => {
|
|
13
|
+
assert.ok(THEME.primary);
|
|
14
|
+
assert.ok(THEME.secondary);
|
|
15
|
+
assert.ok(THEME.accent);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should have semantic colors', () => {
|
|
19
|
+
assert.ok(THEME.success);
|
|
20
|
+
assert.ok(THEME.warning);
|
|
21
|
+
assert.ok(THEME.error);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('UI', () => {
|
|
26
|
+
it('should have ellipsis', () => {
|
|
27
|
+
assert.strictEqual(UI.ellipsis, '...');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
});
|
package/src/ui/theme.ts
CHANGED
|
@@ -1,58 +1,32 @@
|
|
|
1
|
+
import { ANSI as GRIT_ANSI } from './grit/index.ts';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
* ANSI
|
|
4
|
+
* ANSI Escape Codes for Styling
|
|
3
5
|
*/
|
|
4
|
-
export const
|
|
5
|
-
reset: '\x1b[0m',
|
|
6
|
-
bold: '\x1b[1m',
|
|
7
|
-
dim: '\x1b[2m',
|
|
8
|
-
italic: '\x1b[3m',
|
|
9
|
-
underline: '\x1b[4m',
|
|
10
|
-
inverse: '\x1b[7m',
|
|
11
|
-
|
|
12
|
-
// Foreground colors
|
|
13
|
-
black: '\x1b[30m',
|
|
14
|
-
red: '\x1b[31m',
|
|
15
|
-
green: '\x1b[32m',
|
|
16
|
-
yellow: '\x1b[33m',
|
|
17
|
-
blue: '\x1b[34m',
|
|
18
|
-
magenta: '\x1b[35m',
|
|
19
|
-
cyan: '\x1b[36m',
|
|
20
|
-
white: '\x1b[37m',
|
|
21
|
-
gray: '\x1b[90m',
|
|
22
|
-
|
|
23
|
-
// Background colors
|
|
24
|
-
bgBlack: '\x1b[40m',
|
|
25
|
-
bgRed: '\x1b[41m',
|
|
26
|
-
bgGreen: '\x1b[42m',
|
|
27
|
-
bgYellow: '\x1b[43m',
|
|
28
|
-
bgBlue: '\x1b[44m',
|
|
29
|
-
bgMagenta: '\x1b[45m',
|
|
30
|
-
bgCyan: '\x1b[46m',
|
|
31
|
-
bgWhite: '\x1b[47m',
|
|
32
|
-
} as const;
|
|
6
|
+
export const ANSI = GRIT_ANSI;
|
|
33
7
|
|
|
34
8
|
/**
|
|
35
|
-
*
|
|
9
|
+
* Modern color palette (OpenCode style)
|
|
36
10
|
*/
|
|
37
|
-
export const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
11
|
+
export const THEME = {
|
|
12
|
+
background: '#0D0D0D',
|
|
13
|
+
surface: '#1A1A1A',
|
|
14
|
+
primary: '#00A0FF', // Brighter Blue
|
|
15
|
+
secondary: '#A259FF', // Brighter Purple
|
|
16
|
+
accent: '#FF3B30', // Red
|
|
17
|
+
text: '#FFFFFF',
|
|
18
|
+
textDim: '#8E8E93',
|
|
19
|
+
headerBg: '#242426',
|
|
20
|
+
footerBg: '#1C1C1E',
|
|
21
|
+
selectionBg: '#3A3A3C',
|
|
22
|
+
success: '#34C759',
|
|
23
|
+
warning: '#FF9500',
|
|
24
|
+
error: '#FF3B30',
|
|
25
|
+
};
|
|
50
26
|
|
|
51
27
|
/**
|
|
52
28
|
* Common UI elements
|
|
53
29
|
*/
|
|
54
30
|
export const UI = {
|
|
55
|
-
cursor: '>',
|
|
56
|
-
empty: ' ',
|
|
57
31
|
ellipsis: '...',
|
|
58
32
|
} as const;
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { formatNumber, truncate, pad, formatValue, getVisibleWidth, wrapText } from '../utils/format.ts';
|
|
4
|
+
|
|
5
|
+
describe('Format Utils', () => {
|
|
6
|
+
describe('formatNumber', () => {
|
|
7
|
+
it('should format numbers with thousand separators', () => {
|
|
8
|
+
assert.strictEqual(formatNumber(1234), '1,234');
|
|
9
|
+
assert.strictEqual(formatNumber(1234567), '1,234,567');
|
|
10
|
+
assert.strictEqual(formatNumber(123), '123');
|
|
11
|
+
assert.strictEqual(formatNumber(0), '0');
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('truncate', () => {
|
|
16
|
+
it('should truncate long strings', () => {
|
|
17
|
+
assert.strictEqual(truncate('Hello World', 8), 'Hello...');
|
|
18
|
+
assert.strictEqual(truncate('Hello World', 15), 'Hello World');
|
|
19
|
+
assert.strictEqual(truncate('Hello', 10), 'Hello');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should handle null and undefined', () => {
|
|
23
|
+
assert.strictEqual(truncate(null as any, 10), '');
|
|
24
|
+
assert.strictEqual(truncate(undefined as any, 10), '');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should handle empty strings', () => {
|
|
28
|
+
assert.strictEqual(truncate('', 10), '');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should convert non-strings', () => {
|
|
32
|
+
assert.strictEqual(truncate(123 as any, 5), '123');
|
|
33
|
+
assert.strictEqual(truncate(12345678 as any, 5), '12...');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should truncate CJK strings correctly by visible width', () => {
|
|
37
|
+
// 你好世界 = 4 chars, 8 visible width
|
|
38
|
+
assert.strictEqual(getVisibleWidth(truncate('你好世界', 5)), 5); // Should fit '你...' (2+3=5)
|
|
39
|
+
assert.strictEqual(getVisibleWidth(truncate('你好世界', 10)), 8); // Should not truncate
|
|
40
|
+
assert.strictEqual(getVisibleWidth(truncate('Hello你好', 8)), 8); // 'Hello...' = 8 width
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should truncate emoji strings correctly by visible width', () => {
|
|
44
|
+
// 👋 = 2 visible width
|
|
45
|
+
assert.strictEqual(getVisibleWidth(truncate('Hello 👋', 6)), 6); // Should fit 'Hel...' = 6 width
|
|
46
|
+
assert.strictEqual(getVisibleWidth(truncate('👋👋👋', 5)), 5); // Should fit '👋...' (2+3=5)
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('pad', () => {
|
|
51
|
+
it('should pad strings to the left by default', () => {
|
|
52
|
+
assert.strictEqual(pad('test', 10), 'test ');
|
|
53
|
+
assert.strictEqual(pad('hello', 8), 'hello ');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should pad strings to the right', () => {
|
|
57
|
+
assert.strictEqual(pad('test', 10, 'right'), ' test');
|
|
58
|
+
assert.strictEqual(pad('hello', 8, 'right'), ' hello');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should pad strings to the center', () => {
|
|
62
|
+
assert.strictEqual(pad('test', 10, 'center'), ' test ');
|
|
63
|
+
assert.strictEqual(pad('hello', 9, 'center'), ' hello ');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should truncate if string is longer than length', () => {
|
|
67
|
+
assert.strictEqual(getVisibleWidth(pad('hello world', 5)), 5);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should handle null and undefined', () => {
|
|
71
|
+
assert.strictEqual(pad(null as any, 5), ' ');
|
|
72
|
+
assert.strictEqual(pad(undefined as any, 5), ' ');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should pad CJK strings correctly by visible width', () => {
|
|
76
|
+
// 你好 = 2 chars, 4 visible width
|
|
77
|
+
const padded = pad('你好', 10);
|
|
78
|
+
assert.strictEqual(getVisibleWidth(padded), 10); // Should be 4 + 6 spaces = 10
|
|
79
|
+
|
|
80
|
+
const rightPadded = pad('你好', 10, 'right');
|
|
81
|
+
assert.strictEqual(getVisibleWidth(rightPadded), 10); // Should be 6 spaces + 4 = 10
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should truncate CJK strings correctly by visible width', () => {
|
|
85
|
+
// 你好世界 = 4 chars, 8 visible width
|
|
86
|
+
const truncated = pad('你好世界', 5);
|
|
87
|
+
assert.strictEqual(getVisibleWidth(truncated), 5); // Should fit up to 5 width
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should pad emoji strings correctly by visible width', () => {
|
|
91
|
+
// 👋 = 2 visible width
|
|
92
|
+
const padded = pad('👋', 10);
|
|
93
|
+
assert.strictEqual(getVisibleWidth(padded), 10); // Should be 2 + 8 spaces = 10
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should truncate emoji strings correctly by visible width', () => {
|
|
97
|
+
// 👋👋👋 = 6 visible width
|
|
98
|
+
const truncated = pad('👋👋👋', 5);
|
|
99
|
+
assert.strictEqual(getVisibleWidth(truncated), 5); // Should fit 2 emoji + 1 space = 5
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('formatValue', () => {
|
|
104
|
+
it('should format null as NULL', () => {
|
|
105
|
+
assert.strictEqual(formatValue(null), 'NULL');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should format undefined as empty string', () => {
|
|
109
|
+
assert.strictEqual(formatValue(undefined), '');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should format booleans', () => {
|
|
113
|
+
assert.strictEqual(formatValue(true), 'true');
|
|
114
|
+
assert.strictEqual(formatValue(false), 'false');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should format objects as JSON', () => {
|
|
118
|
+
assert.strictEqual(formatValue({ key: 'value' }), '{"key":"value"}');
|
|
119
|
+
assert.strictEqual(formatValue([1, 2, 3]), '[1,2,3]');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should format strings as-is', () => {
|
|
123
|
+
assert.strictEqual(formatValue('hello'), 'hello');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should format numbers as strings', () => {
|
|
127
|
+
assert.strictEqual(formatValue(123), '123');
|
|
128
|
+
assert.strictEqual(formatValue(45.67), '45.67');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should pretty print objects when requested', () => {
|
|
132
|
+
const obj = { a: 1, b: [2, 3] };
|
|
133
|
+
const formatted = formatValue(obj, undefined, true);
|
|
134
|
+
assert.ok(formatted.includes('\n'));
|
|
135
|
+
assert.ok(formatted.includes(' "a": 1'));
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should preserve newlines when pretty is requested', () => {
|
|
139
|
+
const str = 'line1\nline2';
|
|
140
|
+
assert.strictEqual(formatValue(str, undefined, true), str);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('wrapText', () => {
|
|
145
|
+
it('should wrap simple text', () => {
|
|
146
|
+
const text = 'hello world';
|
|
147
|
+
const lines = wrapText(text, 5);
|
|
148
|
+
assert.deepStrictEqual(lines, ['hello', ' worl', 'd']);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should handle CJK characters', () => {
|
|
152
|
+
const text = '你好世界';
|
|
153
|
+
const lines = wrapText(text, 4);
|
|
154
|
+
assert.deepStrictEqual(lines, ['你好', '世界']);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should handle existing newlines', () => {
|
|
158
|
+
const text = 'line1\nline2';
|
|
159
|
+
const lines = wrapText(text, 10);
|
|
160
|
+
assert.deepStrictEqual(lines, ['line1', 'line2']);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should handle empty lines', () => {
|
|
164
|
+
const text = 'line1\n\nline2';
|
|
165
|
+
const lines = wrapText(text, 10);
|
|
166
|
+
assert.deepStrictEqual(lines, ['line1', '', 'line2']);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('getVisibleWidth', () => {
|
|
171
|
+
it('should get width of plain strings', () => {
|
|
172
|
+
assert.strictEqual(getVisibleWidth('hello'), 5);
|
|
173
|
+
assert.strictEqual(getVisibleWidth('test string'), 11);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should ignore ANSI escape codes', () => {
|
|
177
|
+
assert.strictEqual(getVisibleWidth('\x1b[1mhello\x1b[0m'), 5);
|
|
178
|
+
assert.strictEqual(getVisibleWidth('\x1b[31mred text\x1b[0m'), 8);
|
|
179
|
+
assert.strictEqual(getVisibleWidth('\x1b[1m\x1b[31mbold red\x1b[0m\x1b[0m'), 8);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should handle empty strings', () => {
|
|
183
|
+
assert.strictEqual(getVisibleWidth(''), 0);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should count CJK characters as double-width', () => {
|
|
187
|
+
assert.strictEqual(getVisibleWidth('你好'), 4); // 2 Chinese chars = 4 width
|
|
188
|
+
assert.strictEqual(getVisibleWidth('Hello世界'), 9); // 5 + 2*2 = 9
|
|
189
|
+
assert.strictEqual(getVisibleWidth('こんにちは'), 10); // 5 Japanese chars = 10 width
|
|
190
|
+
assert.strictEqual(getVisibleWidth('안녕하세요'), 10); // 5 Korean chars = 10 width
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should count emoji as double-width', () => {
|
|
194
|
+
assert.strictEqual(getVisibleWidth('👋'), 2); // Wave emoji = 2 width
|
|
195
|
+
assert.strictEqual(getVisibleWidth('Hello! 👋'), 9); // 7 + 2 = 9
|
|
196
|
+
assert.strictEqual(getVisibleWidth('😀😁😂'), 6); // 3 emoji = 6 width
|
|
197
|
+
assert.strictEqual(getVisibleWidth('Test 🎉 OK'), 10); // 4 + 1 + 2 + 1 + 2 = 10
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should handle mixed content with CJK and ANSI codes', () => {
|
|
201
|
+
assert.strictEqual(getVisibleWidth('\x1b[1m你好\x1b[0m'), 4);
|
|
202
|
+
assert.strictEqual(getVisibleWidth('Test测试'), 8); // 4 + 2*2 = 8
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should handle mixed emoji, CJK, and ASCII', () => {
|
|
206
|
+
assert.strictEqual(getVisibleWidth('Hello 你好 👋'), 13); // 5 + 1 + 4 + 1 + 2 = 13
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
});
|
package/src/utils/format.ts
CHANGED
|
@@ -21,25 +21,41 @@ import stringWidth from 'string-width';
|
|
|
21
21
|
|
|
22
22
|
export function truncate(str: string, maxWidth: number): string {
|
|
23
23
|
if (!str) return '';
|
|
24
|
-
|
|
24
|
+
let s = String(str);
|
|
25
25
|
|
|
26
|
+
// Optimization: handle extremely large strings by pre-truncating
|
|
27
|
+
// A single visible character can't be more than 4 code points (e.g., complex emoji)
|
|
28
|
+
// and double-width characters count as 2.
|
|
29
|
+
// So taking maxWidth * 4 characters is a very safe upper bound.
|
|
30
|
+
if (s.length > maxWidth * 4) {
|
|
31
|
+
s = s.slice(0, maxWidth * 4);
|
|
32
|
+
}
|
|
33
|
+
|
|
26
34
|
const currentWidth = getVisibleWidth(s);
|
|
27
35
|
if (currentWidth <= maxWidth) return s;
|
|
28
36
|
|
|
29
|
-
// Need to truncate - build string character by character
|
|
30
|
-
let result = '';
|
|
31
|
-
let width = 0;
|
|
32
37
|
const ellipsis = '...';
|
|
33
|
-
const ellipsisWidth = 3;
|
|
38
|
+
const ellipsisWidth = 3;
|
|
34
39
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
if (maxWidth <= ellipsisWidth) {
|
|
41
|
+
return ellipsis.slice(0, maxWidth);
|
|
42
|
+
}
|
|
38
43
|
|
|
39
|
-
|
|
44
|
+
// Optimization: for plain ASCII strings, we can use slice directly
|
|
45
|
+
const isPlainASCII = /^[\x20-\x7E]*$/.test(s);
|
|
46
|
+
if (isPlainASCII) {
|
|
47
|
+
return s.slice(0, maxWidth - ellipsisWidth) + ellipsis;
|
|
48
|
+
}
|
|
40
49
|
|
|
41
|
-
|
|
42
|
-
|
|
50
|
+
// For complex strings, we still need to be careful about double-width characters
|
|
51
|
+
// but we can optimize by taking a slice that is definitely not too long
|
|
52
|
+
let result = s.slice(0, maxWidth);
|
|
53
|
+
while (getVisibleWidth(result) + ellipsisWidth > maxWidth && result.length > 0) {
|
|
54
|
+
// Remove one character (potentially a multi-byte character or emoji)
|
|
55
|
+
// Using Array.from to correctly handle surrogate pairs
|
|
56
|
+
const chars = Array.from(result);
|
|
57
|
+
chars.pop();
|
|
58
|
+
result = chars.join('');
|
|
43
59
|
}
|
|
44
60
|
|
|
45
61
|
return result + ellipsis;
|
|
@@ -95,19 +111,80 @@ export function pad(str: string, targetWidth: number, align: 'left' | 'right' |
|
|
|
95
111
|
/**
|
|
96
112
|
* Format a value for display (handle null, undefined, etc.)
|
|
97
113
|
* @param value - Value to format
|
|
114
|
+
* @param maxLen - Maximum length hint to prevent processing large strings
|
|
115
|
+
* @param pretty - Whether to use pretty printing for objects and preserve newlines
|
|
98
116
|
* @returns Formatted string
|
|
99
117
|
*/
|
|
100
|
-
export function formatValue(value: any): string {
|
|
118
|
+
export function formatValue(value: any, maxLen?: number, pretty?: boolean): string {
|
|
101
119
|
if (value === null) return 'NULL';
|
|
102
120
|
if (value === undefined) return '';
|
|
103
121
|
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
|
104
|
-
if (typeof value === 'object') return JSON.stringify(value);
|
|
122
|
+
if (typeof value === 'object') return JSON.stringify(value, null, pretty ? 2 : undefined);
|
|
105
123
|
|
|
106
|
-
// Convert to string
|
|
107
|
-
|
|
124
|
+
// Convert to string
|
|
125
|
+
let str = String(value);
|
|
126
|
+
|
|
127
|
+
if (pretty) return str;
|
|
128
|
+
|
|
129
|
+
// Pre-truncate if it's way too long
|
|
130
|
+
if (maxLen !== undefined && str.length > maxLen * 4) {
|
|
131
|
+
str = str.slice(0, maxLen * 4);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Remove control characters (newlines, tabs, etc.)
|
|
108
135
|
return str.replace(/[\n\r\t\v\f]/g, ' ').replace(/\s+/g, ' ');
|
|
109
136
|
}
|
|
110
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Wrap text into multiple lines based on visible width
|
|
140
|
+
* @param text - String to wrap
|
|
141
|
+
* @param maxWidth - Maximum visible width per line
|
|
142
|
+
* @returns Array of wrapped lines
|
|
143
|
+
*/
|
|
144
|
+
export function wrapText(text: string, maxWidth: number): string[] {
|
|
145
|
+
if (!text) return [''];
|
|
146
|
+
if (maxWidth <= 0) return [text];
|
|
147
|
+
|
|
148
|
+
const lines: string[] = [];
|
|
149
|
+
const sourceLines = text.split(/\r?\n/);
|
|
150
|
+
|
|
151
|
+
for (const sourceLine of sourceLines) {
|
|
152
|
+
if (!sourceLine) {
|
|
153
|
+
lines.push('');
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let currentLine = '';
|
|
158
|
+
let currentWidth = 0;
|
|
159
|
+
|
|
160
|
+
for (const char of Array.from(sourceLine)) {
|
|
161
|
+
const charWidth = getVisibleWidth(char);
|
|
162
|
+
|
|
163
|
+
if (currentWidth + charWidth > maxWidth) {
|
|
164
|
+
if (currentLine) {
|
|
165
|
+
lines.push(currentLine);
|
|
166
|
+
currentLine = char;
|
|
167
|
+
currentWidth = charWidth;
|
|
168
|
+
} else {
|
|
169
|
+
// Single character is wider than maxWidth, forced break
|
|
170
|
+
lines.push(char);
|
|
171
|
+
currentLine = '';
|
|
172
|
+
currentWidth = 0;
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
currentLine += char;
|
|
176
|
+
currentWidth += charWidth;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (currentLine) {
|
|
181
|
+
lines.push(currentLine);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return lines;
|
|
186
|
+
}
|
|
187
|
+
|
|
111
188
|
/**
|
|
112
189
|
* Check if a character is double-width (CJK, emoji, etc.)
|
|
113
190
|
* @param char - Single character or code point
|
|
@@ -125,13 +202,9 @@ function isDoubleWidth(_char: string): boolean {
|
|
|
125
202
|
* @returns Visible width
|
|
126
203
|
*/
|
|
127
204
|
export function getVisibleWidth(str: string): number {
|
|
205
|
+
if (!str) return 0;
|
|
128
206
|
// Remove ANSI escape codes
|
|
129
207
|
const cleanStr = str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
130
208
|
|
|
131
|
-
|
|
132
|
-
for (const ch of Array.from(cleanStr)) {
|
|
133
|
-
width += stringWidth(ch);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return width;
|
|
209
|
+
return stringWidth(cleanStr);
|
|
137
210
|
}
|