dbn-cli 0.4.0 → 0.5.3
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/package.json +5 -5
- package/src/adapter/adapter.test.ts +207 -0
- package/src/index.ts +24 -6
- package/src/repro_bug.test.ts +101 -0
- package/src/types.ts +8 -0
- package/src/ui/grit/README.md +58 -0
- package/src/ui/grit/index.test.ts +67 -0
- package/src/ui/grit/index.ts +107 -0
- package/src/ui/grit/types.ts +16 -0
- package/src/ui/grit/utils.ts +37 -0
- package/src/ui/navigator.test.ts +434 -0
- package/src/ui/navigator.ts +199 -36
- package/src/ui/navigator_sync.test.ts +95 -0
- package/src/ui/renderer.ts +268 -493
- 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/debounce.ts +16 -0
- package/src/utils/format.test.ts +209 -0
- package/src/utils/format.ts +94 -21
package/src/ui/renderer.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { formatNumber, truncate, pad, formatValue, getVisibleWidth } from '../utils/format.ts';
|
|
1
|
+
import { THEME, ANSI } from './theme.ts';
|
|
2
|
+
import { formatNumber, truncate, pad, formatValue, getVisibleWidth, wrapText } from '../utils/format.ts';
|
|
3
|
+
import { Box, Transition, Grid, type ColumnConfig } from './grit/index.ts';
|
|
3
4
|
import type { Screen } from './screen.ts';
|
|
4
5
|
import type { ViewState, TablesViewState, TableDetailViewState, SchemaViewState, RowDetailViewState, HealthViewState } from '../types.ts';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
* Renderer
|
|
8
|
+
* Modern Renderer (Manual ANSI Implementation)
|
|
9
|
+
* Emulates OpenTUI/OpenCode design with color blocks and no lines.
|
|
8
10
|
*/
|
|
9
11
|
export class Renderer {
|
|
10
12
|
private screen: Screen;
|
|
13
|
+
private lastLines: string[] = [];
|
|
14
|
+
private lastWidth: number = 0;
|
|
15
|
+
private lastHeight: number = 0;
|
|
11
16
|
|
|
12
17
|
constructor(screen: Screen) {
|
|
13
18
|
this.screen = screen;
|
|
@@ -15,571 +20,341 @@ export class Renderer {
|
|
|
15
20
|
|
|
16
21
|
/**
|
|
17
22
|
* Render the current state to screen
|
|
18
|
-
* @param state - Current navigation state
|
|
19
|
-
* @param dbPath - Database file path
|
|
20
23
|
*/
|
|
21
24
|
render(state: ViewState, dbPath: string): void {
|
|
22
25
|
const { width, height } = this.screen;
|
|
23
26
|
const lines: string[] = [];
|
|
24
27
|
|
|
25
|
-
//
|
|
28
|
+
// Header transition color can match table header if in table-detail
|
|
29
|
+
const contentTopBg = state.type === 'table-detail' ? THEME.surface : THEME.background;
|
|
30
|
+
|
|
31
|
+
// 1. Title Bar (Header Block)
|
|
26
32
|
lines.push(this.buildTitleBar(state, dbPath, width));
|
|
27
|
-
lines.push(
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
lines.push(Transition.draw(width, THEME.headerBg, contentTopBg));
|
|
34
|
+
|
|
35
|
+
// 2. Main Content area
|
|
36
|
+
const contentHeight = height - 4; // 1 header, 1 footer, 2 transitions
|
|
37
|
+
const contentLines = this.buildContent(state, contentHeight, width);
|
|
30
38
|
lines.push(...contentLines);
|
|
31
|
-
|
|
39
|
+
|
|
40
|
+
// 3. Help/Status Bar (Footer Block)
|
|
41
|
+
lines.push(Transition.draw(width, THEME.background, THEME.footerBg));
|
|
32
42
|
lines.push(this.buildHelpBar(state, width));
|
|
33
43
|
|
|
34
|
-
//
|
|
35
|
-
this.
|
|
36
|
-
|
|
44
|
+
// Incremental Render Logic
|
|
45
|
+
if (width !== this.lastWidth || height !== this.lastHeight) {
|
|
46
|
+
// Screen resized: Full redraw from top, then clear remainder of screen
|
|
47
|
+
this.screen.moveCursor(1, 1);
|
|
48
|
+
this.screen.write(lines.join('\n'));
|
|
49
|
+
this.screen.write('\x1b[J'); // Clear remaining lines if new height is smaller
|
|
50
|
+
} else {
|
|
51
|
+
// Incremental update: Only write changed lines
|
|
52
|
+
for (let i = 0; i < lines.length; i++) {
|
|
53
|
+
if (lines[i] !== this.lastLines[i]) {
|
|
54
|
+
this.screen.moveCursor(i + 1, 1);
|
|
55
|
+
// Write line and clear to end of line to prevent ghosting
|
|
56
|
+
this.screen.write(lines[i] + '\x1b[K');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.lastLines = lines;
|
|
62
|
+
this.lastWidth = width;
|
|
63
|
+
this.lastHeight = height;
|
|
37
64
|
}
|
|
38
65
|
|
|
39
|
-
/**
|
|
40
|
-
* Build title bar (top line)
|
|
41
|
-
*/
|
|
42
66
|
private buildTitleBar(state: ViewState, dbPath: string, width: number): string {
|
|
43
67
|
const fileName = dbPath.split('/').pop() || dbPath;
|
|
44
|
-
let
|
|
68
|
+
let breadcrumb = ` ${fileName}`;
|
|
45
69
|
|
|
46
|
-
if (state.type === 'table-detail') {
|
|
47
|
-
|
|
48
|
-
} else if (state.type === 'schema-view') {
|
|
49
|
-
title += ` ${COLORS.dim}>${COLORS.reset} ${state.tableName} ${COLORS.dim}> schema${COLORS.reset}`;
|
|
50
|
-
} else if (state.type === 'row-detail') {
|
|
51
|
-
title += ` ${COLORS.dim}>${COLORS.reset} ${state.tableName} ${COLORS.dim}> row ${state.rowIndex + 1}${COLORS.reset}`;
|
|
52
|
-
} else if (state.type === 'health') {
|
|
53
|
-
title += ` ${COLORS.dim}>${COLORS.reset} health`;
|
|
70
|
+
if (state.type === 'table-detail' || state.type === 'schema-view' || state.type === 'row-detail') {
|
|
71
|
+
breadcrumb += ` > ${state.tableName}`;
|
|
54
72
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
73
|
+
if (state.type === 'schema-view') breadcrumb += ` > schema`;
|
|
74
|
+
if (state.type === 'row-detail') breadcrumb += ` > row ${state.rowIndex + 1}`;
|
|
75
|
+
if (state.type === 'health') breadcrumb += ` > health`;
|
|
76
|
+
|
|
77
|
+
const leftPart = `${ANSI.bold}${ANSI.fg(THEME.primary)}${breadcrumb}${ANSI.reset}`;
|
|
78
|
+
|
|
79
|
+
let rightPart = '';
|
|
60
80
|
if (state.type === 'tables') {
|
|
61
|
-
|
|
62
|
-
const currentPos = state.cursor + 1;
|
|
63
|
-
rightInfo = `${COLORS.dim}${currentPos}/${totalTables} tables${COLORS.reset}`;
|
|
81
|
+
rightPart = `${state.cursor + 1}/${state.tables.length} tables`;
|
|
64
82
|
} else if (state.type === 'table-detail') {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
rightInfo = `${COLORS.dim}row ${formatNumber(currentRow)}/${totalRows}${COLORS.reset}`;
|
|
83
|
+
const current = state.dataOffset + state.dataCursor + 1;
|
|
84
|
+
rightPart = `row ${formatNumber(current)}/${formatNumber(state.totalRows)}`;
|
|
68
85
|
} else if (state.type === 'schema-view') {
|
|
69
|
-
|
|
70
|
-
const currentPos = state.cursor + 1;
|
|
71
|
-
rightInfo = `${COLORS.dim}${currentPos}/${totalCols} columns${COLORS.reset}`;
|
|
86
|
+
rightPart = `${state.cursor + 1}/${state.schema.length} columns`;
|
|
72
87
|
} else if (state.type === 'row-detail') {
|
|
73
|
-
|
|
74
|
-
rightInfo = `${COLORS.dim}${colCount} fields${COLORS.reset}`;
|
|
75
|
-
} else if (state.type === 'health') {
|
|
76
|
-
rightInfo = `${COLORS.dim}overview${COLORS.reset}`;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Calculate spacing using visible width (accounts for CJK double-width)
|
|
80
|
-
const titleWidth = getVisibleWidth(title);
|
|
81
|
-
const rightInfoWidth = getVisibleWidth(rightInfo);
|
|
82
|
-
const availableSpace = width - titleWidth - rightInfoWidth;
|
|
83
|
-
|
|
84
|
-
// Adjust width to prevent wrapping issues with ambiguous width characters
|
|
85
|
-
const safeWidth = width - 1;
|
|
86
|
-
|
|
87
|
-
if (availableSpace > 0) {
|
|
88
|
-
// Ensure we don't exceed safeWidth
|
|
89
|
-
let result = title + ' '.repeat(availableSpace) + rightInfo;
|
|
90
|
-
const resultWidth = getVisibleWidth(result);
|
|
91
|
-
if (resultWidth > safeWidth) {
|
|
92
|
-
return truncate(result, safeWidth);
|
|
93
|
-
}
|
|
94
|
-
return result;
|
|
95
|
-
} else {
|
|
96
|
-
// Not enough space, truncate title
|
|
97
|
-
const maxTitleWidth = safeWidth - rightInfoWidth - 3; // Reserve space for "..."
|
|
98
|
-
if (maxTitleWidth > 10) {
|
|
99
|
-
const truncatedTitle = truncate(title.replace(/\x1b\[[0-9;]*m/g, ''), maxTitleWidth);
|
|
100
|
-
const truncatedWidth = getVisibleWidth(truncatedTitle);
|
|
101
|
-
const padding = safeWidth - truncatedWidth - rightInfoWidth;
|
|
102
|
-
return `${COLORS.bold}${truncatedTitle}${COLORS.reset}` + ' '.repeat(Math.max(0, padding)) + rightInfo;
|
|
103
|
-
} else {
|
|
104
|
-
// Very narrow screen, just show title
|
|
105
|
-
return truncate(title.replace(/\x1b\[[0-9;]*m/g, ''), safeWidth);
|
|
106
|
-
}
|
|
88
|
+
rightPart = `${state.schema.length} fields`;
|
|
107
89
|
}
|
|
108
|
-
}
|
|
109
90
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
91
|
+
const rightPartStyled = `${ANSI.fg(THEME.textDim)}${rightPart}${ANSI.reset}`;
|
|
92
|
+
|
|
93
|
+
const padding = ' '.repeat(Math.max(0, width - 2 - getVisibleWidth(leftPart) - getVisibleWidth(rightPartStyled)));
|
|
94
|
+
const rowContent = `${leftPart}${padding}${rightPartStyled}`;
|
|
95
|
+
|
|
96
|
+
const box = new Box({ width, padding: 1, background: THEME.headerBg });
|
|
97
|
+
return box.render(rowContent);
|
|
115
98
|
}
|
|
116
99
|
|
|
117
|
-
/**
|
|
118
|
-
* Build main content area
|
|
119
|
-
*/
|
|
120
100
|
private buildContent(state: ViewState, height: number, width: number): string[] {
|
|
101
|
+
let content: string[] = [];
|
|
102
|
+
const bgBox = new Box({ width, background: THEME.background, padding: 1 });
|
|
103
|
+
|
|
121
104
|
if (state.type === 'tables') {
|
|
122
|
-
|
|
105
|
+
content = this.renderTables(state, height, width);
|
|
123
106
|
} else if (state.type === 'table-detail') {
|
|
124
|
-
|
|
107
|
+
content = this.renderTableDetail(state, height, width);
|
|
125
108
|
} else if (state.type === 'schema-view') {
|
|
126
|
-
|
|
109
|
+
content = this.renderSchema(state, height, width);
|
|
127
110
|
} else if (state.type === 'row-detail') {
|
|
128
|
-
|
|
111
|
+
content = this.renderRowDetail(state, height, width);
|
|
129
112
|
} else if (state.type === 'health') {
|
|
130
|
-
|
|
113
|
+
content = this.renderHealth(state, height, width);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Fill remaining lines with background
|
|
117
|
+
while (content.length < height) {
|
|
118
|
+
content.push(bgBox.render(''));
|
|
131
119
|
}
|
|
132
|
-
return
|
|
120
|
+
return content;
|
|
133
121
|
}
|
|
134
122
|
|
|
135
|
-
|
|
136
|
-
* Build tables list view
|
|
137
|
-
*/
|
|
138
|
-
private buildTablesList(state: TablesViewState, height: number, width: number): string[] {
|
|
123
|
+
private renderTables(state: TablesViewState, height: number, width: number): string[] {
|
|
139
124
|
const lines: string[] = [];
|
|
140
125
|
const { tables, cursor } = state;
|
|
126
|
+
const box = new Box({ width, padding: 1 });
|
|
141
127
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
lines.push('');
|
|
147
|
-
}
|
|
148
|
-
return lines;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Calculate visible window
|
|
152
|
-
const halfHeight = Math.floor(height / 2);
|
|
153
|
-
let startIdx = Math.max(0, cursor - halfHeight);
|
|
154
|
-
let endIdx = Math.min(tables.length, startIdx + height);
|
|
155
|
-
|
|
156
|
-
// Adjust if at the end
|
|
157
|
-
if (endIdx - startIdx < height) {
|
|
158
|
-
startIdx = Math.max(0, endIdx - height);
|
|
159
|
-
}
|
|
128
|
+
const half = Math.floor(height / 2);
|
|
129
|
+
let start = Math.max(0, cursor - half);
|
|
130
|
+
let end = Math.min(tables.length, start + height);
|
|
131
|
+
if (end - start < height) start = Math.max(0, end - height);
|
|
160
132
|
|
|
161
|
-
for (let i =
|
|
162
|
-
const table = tables[i];
|
|
133
|
+
for (let i = start; i < end; i++) {
|
|
163
134
|
const isSelected = i === cursor;
|
|
135
|
+
const table = tables[i];
|
|
164
136
|
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
const maxNameWidth = width - 20; // Reserve space for row count
|
|
168
|
-
const name = truncate(table.name, maxNameWidth - 5);
|
|
169
|
-
const namePadded = pad(name, maxNameWidth - 3);
|
|
170
|
-
const count = formatNumber(table.row_count) + ' rows';
|
|
171
|
-
const countPadded = pad(count, 15, 'right');
|
|
172
|
-
|
|
173
|
-
let content = `${cursorChar} ${namePadded} ${countPadded}`;
|
|
174
|
-
|
|
175
|
-
// Ensure exact width before adding color codes
|
|
176
|
-
const safeWidth = width - 1; // Subtract 1 to prevent wrapping issues
|
|
177
|
-
const contentWidth = getVisibleWidth(content);
|
|
178
|
-
if (contentWidth > safeWidth) {
|
|
179
|
-
content = truncate(content, safeWidth);
|
|
180
|
-
} else if (contentWidth < safeWidth) {
|
|
181
|
-
content += ' '.repeat(safeWidth - contentWidth);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Apply color codes to properly sized line
|
|
185
|
-
let line: string;
|
|
186
|
-
if (isSelected) {
|
|
187
|
-
line = COLORS.inverse + content + COLORS.reset;
|
|
188
|
-
} else {
|
|
189
|
-
line = content;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
lines.push(line);
|
|
193
|
-
}
|
|
137
|
+
const name = isSelected ? `${ANSI.bold}${table.name}` : `${table.name}`;
|
|
138
|
+
const count = `${formatNumber(table.row_count)} rows`;
|
|
194
139
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
lines.push(' '.repeat(width));
|
|
198
|
-
}
|
|
140
|
+
const bg = isSelected ? THEME.selectionBg : THEME.background;
|
|
141
|
+
const fg = isSelected ? THEME.primary : THEME.text;
|
|
199
142
|
|
|
143
|
+
const leftPart = `${ANSI.fg(fg)}${name}${ANSI.reset}`;
|
|
144
|
+
const rightPart = `${ANSI.fg(isSelected ? fg : THEME.textDim)}${count}${ANSI.reset}`;
|
|
145
|
+
|
|
146
|
+
const padding = ' '.repeat(Math.max(0, width - 2 - getVisibleWidth(leftPart) - getVisibleWidth(rightPart)));
|
|
147
|
+
const rowContent = `${leftPart}${padding}${rightPart}`;
|
|
148
|
+
lines.push(box.render(rowContent, { background: bg }));
|
|
149
|
+
}
|
|
200
150
|
return lines;
|
|
201
151
|
}
|
|
202
152
|
|
|
203
|
-
|
|
204
|
-
* Build table detail view
|
|
205
|
-
*/
|
|
206
|
-
private buildTableDetail(state: TableDetailViewState, height: number, width: number): string[] {
|
|
153
|
+
private renderTableDetail(state: TableDetailViewState, height: number, width: number): string[] {
|
|
207
154
|
const lines: string[] = [];
|
|
208
|
-
const { data,
|
|
155
|
+
const { data, dataOffset, dataCursor, bufferOffset } = state;
|
|
156
|
+
const box = new Box({ width, padding: 1 });
|
|
209
157
|
|
|
210
|
-
if (data.length === 0) {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
158
|
+
if (data.length === 0) return [box.render('No data', { align: 'center' })];
|
|
159
|
+
|
|
160
|
+
const columns = Object.keys(data[0]).slice(0, 8);
|
|
161
|
+
|
|
162
|
+
// Calculate or use cached column widths
|
|
163
|
+
if (!state.cachedColWidths || state.cachedScreenWidth !== width) {
|
|
164
|
+
const numCols = columns.length;
|
|
217
165
|
const minColWidth = 8;
|
|
218
|
-
const maxColWidth = 50; // Maximum width for any single column
|
|
219
|
-
const maxVisibleCols = 8;
|
|
220
|
-
const visibleColumns = columns.slice(0, maxVisibleCols);
|
|
221
|
-
const availableWidth = width - 4; // Reserve space for padding and cursor
|
|
222
|
-
const spacingWidth = visibleColumns.length - 1; // Space between columns
|
|
223
|
-
const usableWidth = availableWidth - spacingWidth;
|
|
224
|
-
|
|
225
|
-
// Calculate ideal width for each column based on content
|
|
226
|
-
const idealWidths = visibleColumns.map(col => {
|
|
227
|
-
// Check column name length
|
|
228
|
-
let maxWidth = col.length;
|
|
229
|
-
|
|
230
|
-
// Check data values length (sample first few rows for performance)
|
|
231
|
-
const sampleSize = Math.min(data.length, 20);
|
|
232
|
-
for (let i = 0; i < sampleSize; i++) {
|
|
233
|
-
const value = formatValue(data[i][col]);
|
|
234
|
-
maxWidth = Math.max(maxWidth, value.length);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// Apply constraints: add 2 for padding, cap at maxColWidth
|
|
238
|
-
return Math.max(minColWidth, Math.min(maxWidth + 2, maxColWidth));
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
// Calculate total ideal width
|
|
242
|
-
let totalIdealWidth = idealWidths.reduce((sum, w) => sum + w, 0);
|
|
243
|
-
|
|
244
|
-
// Allocate widths
|
|
245
|
-
const colWidths: number[] = [];
|
|
246
|
-
|
|
247
|
-
if (totalIdealWidth <= usableWidth) {
|
|
248
|
-
// We have extra space - distribute it intelligently
|
|
249
|
-
const extraSpace = usableWidth - totalIdealWidth;
|
|
250
|
-
|
|
251
|
-
// Find columns that could use more space (those at maxColWidth)
|
|
252
|
-
const expandableIndices = idealWidths
|
|
253
|
-
.map((w, i) => ({ width: w, index: i }))
|
|
254
|
-
.filter(item => item.width === maxColWidth)
|
|
255
|
-
.map(item => item.index);
|
|
256
|
-
|
|
257
|
-
// Distribute extra space only to expandable columns
|
|
258
|
-
if (expandableIndices.length > 0) {
|
|
259
|
-
const extraPerCol = Math.floor(extraSpace / expandableIndices.length);
|
|
260
|
-
for (let i = 0; i < visibleColumns.length; i++) {
|
|
261
|
-
if (expandableIndices.includes(i)) {
|
|
262
|
-
colWidths[i] = idealWidths[i] + extraPerCol;
|
|
263
|
-
} else {
|
|
264
|
-
colWidths[i] = idealWidths[i];
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
// Add remainder to last expandable column
|
|
268
|
-
const remainder = extraSpace - (extraPerCol * expandableIndices.length);
|
|
269
|
-
colWidths[expandableIndices[expandableIndices.length - 1]] += remainder;
|
|
270
|
-
} else {
|
|
271
|
-
// No expandable columns, distribute evenly to all
|
|
272
|
-
const extraPerCol = Math.floor(extraSpace / visibleColumns.length);
|
|
273
|
-
for (let i = 0; i < visibleColumns.length; i++) {
|
|
274
|
-
colWidths[i] = idealWidths[i] + extraPerCol;
|
|
275
|
-
}
|
|
276
|
-
const remainder = extraSpace - (extraPerCol * visibleColumns.length);
|
|
277
|
-
colWidths[colWidths.length - 1] += remainder;
|
|
278
|
-
}
|
|
279
|
-
} else {
|
|
280
|
-
// Need to scale down - use proportional scaling
|
|
281
|
-
const scale = usableWidth / totalIdealWidth;
|
|
282
|
-
for (let i = 0; i < visibleColumns.length; i++) {
|
|
283
|
-
colWidths[i] = Math.max(minColWidth, Math.floor(idealWidths[i] * scale));
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Adjust to fill exact width
|
|
287
|
-
const totalWidth = colWidths.reduce((sum, w) => sum + w, 0);
|
|
288
|
-
const diff = usableWidth - totalWidth;
|
|
289
|
-
if (diff !== 0) {
|
|
290
|
-
colWidths[colWidths.length - 1] += diff;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Render table header
|
|
295
|
-
const headerCells = visibleColumns.map((col, idx) => {
|
|
296
|
-
const truncatedCol = truncate(col, colWidths[idx] - 1);
|
|
297
|
-
return pad(truncatedCol, colWidths[idx] - 1);
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
let headerLine = ` ${COLORS.dim} ${headerCells.join(' ')}${COLORS.reset}`;
|
|
301
|
-
const headerWidth = getVisibleWidth(headerLine);
|
|
302
|
-
|
|
303
|
-
if (headerWidth < width) {
|
|
304
|
-
headerLine += ' '.repeat(width - headerWidth);
|
|
305
|
-
} else if (headerWidth > width) {
|
|
306
|
-
// Truncate if somehow too wide
|
|
307
|
-
const plainHeader = ` ${headerCells.map(c => c.replace(/\x1b\[[0-9;]*m/g, '')).join(' ')}`;
|
|
308
|
-
headerLine = truncate(plainHeader, width);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
lines.push(headerLine);
|
|
312
|
-
|
|
313
|
-
// Update visible rows for navigator (subtract 1 for header)
|
|
314
|
-
state.visibleRows = height - 1;
|
|
315
|
-
|
|
316
|
-
// Render data rows
|
|
317
|
-
const maxRows = Math.min(data.length, height - 1);
|
|
318
|
-
|
|
319
|
-
for (let i = 0; i < maxRows; i++) {
|
|
320
|
-
const row = data[i];
|
|
321
|
-
const isSelected = i === dataCursor;
|
|
322
|
-
|
|
323
|
-
const cells = visibleColumns.map((col, idx) => {
|
|
324
|
-
const value = formatValue(row[col]);
|
|
325
|
-
return truncate(value, colWidths[idx] - 1);
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
// Build content without color codes first
|
|
329
|
-
const prefix = isSelected ? ` ${UI.cursor}` : ' ';
|
|
330
|
-
const contentWithoutPrefix = cells.map((cell, idx) => pad(cell, colWidths[idx] - 1)).join(' ');
|
|
331
|
-
|
|
332
|
-
// Ensure exact width before adding color codes (using visible width for CJK)
|
|
333
|
-
const safeWidth = width - 1; // Subtract 1 to prevent wrapping issues
|
|
334
|
-
let content = prefix + ' ' + contentWithoutPrefix;
|
|
335
|
-
const contentWidth = getVisibleWidth(content);
|
|
336
|
-
|
|
337
|
-
if (contentWidth > safeWidth) {
|
|
338
|
-
content = truncate(content, safeWidth);
|
|
339
|
-
} else if (contentWidth < safeWidth) {
|
|
340
|
-
content += ' '.repeat(safeWidth - contentWidth);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// Apply color codes to properly sized line
|
|
344
|
-
let line: string;
|
|
345
|
-
if (isSelected) {
|
|
346
|
-
line = COLORS.inverse + content + COLORS.reset;
|
|
347
|
-
} else {
|
|
348
|
-
line = content;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
lines.push(line);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
166
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
167
|
+
const configs: ColumnConfig[] = columns.map((_, i) => ({
|
|
168
|
+
weight: (state.columnWeights && state.columnWeights[i]) || 1,
|
|
169
|
+
minWidth: minColWidth
|
|
170
|
+
}));
|
|
171
|
+
|
|
172
|
+
state.cachedColWidths = Grid.calculateWidths(width - 2, configs);
|
|
173
|
+
state.cachedScreenWidth = width;
|
|
358
174
|
}
|
|
359
175
|
|
|
176
|
+
const colWidths = state.cachedColWidths!;
|
|
177
|
+
|
|
178
|
+
// Table Header Block
|
|
179
|
+
let headerContent = `${ANSI.fg(THEME.textDim)}${ANSI.bold}`;
|
|
180
|
+
columns.forEach((col, i) => {
|
|
181
|
+
const w = colWidths[i];
|
|
182
|
+
headerContent += pad(col, w - 1).slice(0, w - 1) + ' ';
|
|
183
|
+
});
|
|
184
|
+
lines.push(box.render(headerContent, { background: THEME.surface }));
|
|
185
|
+
lines.push(Transition.draw(width, THEME.surface, THEME.background));
|
|
186
|
+
|
|
187
|
+
// Data Rows
|
|
188
|
+
const relativeOffset = dataOffset - bufferOffset;
|
|
189
|
+
const displayData = data.slice(relativeOffset, relativeOffset + height - 2);
|
|
190
|
+
state.visibleRows = height - 2;
|
|
191
|
+
|
|
192
|
+
displayData.forEach((row, idx) => {
|
|
193
|
+
const isSelected = idx === dataCursor;
|
|
194
|
+
const rowBg = isSelected ? THEME.selectionBg : THEME.background;
|
|
195
|
+
const rowFg = isSelected ? THEME.primary : THEME.text;
|
|
196
|
+
|
|
197
|
+
let rowContent = `${ANSI.fg(rowFg)}`;
|
|
198
|
+
columns.forEach((col, i) => {
|
|
199
|
+
const w = colWidths[i];
|
|
200
|
+
const val = formatValue(row[col], w - 1);
|
|
201
|
+
rowContent += pad(val, w - 1).slice(0, w - 1) + ' ';
|
|
202
|
+
});
|
|
203
|
+
lines.push(box.render(rowContent, { background: rowBg }));
|
|
204
|
+
});
|
|
205
|
+
|
|
360
206
|
return lines;
|
|
361
207
|
}
|
|
362
208
|
|
|
363
|
-
|
|
364
|
-
* Build schema view (full screen)
|
|
365
|
-
*/
|
|
366
|
-
private buildSchemaView(state: SchemaViewState, height: number, width: number): string[] {
|
|
209
|
+
private renderSchema(state: SchemaViewState, height: number, width: number): string[] {
|
|
367
210
|
const lines: string[] = [];
|
|
368
|
-
const { schema, cursor
|
|
369
|
-
|
|
370
|
-
if (schema.length === 0) {
|
|
371
|
-
lines.push(pad('No schema information', width, 'center'));
|
|
372
|
-
while (lines.length < height) {
|
|
373
|
-
lines.push(' '.repeat(width));
|
|
374
|
-
}
|
|
375
|
-
return lines;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// Calculate visible window
|
|
379
|
-
const contentHeight = height; // Use full height
|
|
380
|
-
const halfHeight = Math.floor(contentHeight / 2);
|
|
381
|
-
let startIdx = Math.max(0, cursor - halfHeight);
|
|
382
|
-
let endIdx = Math.min(schema.length, startIdx + contentHeight);
|
|
383
|
-
|
|
384
|
-
// Adjust if at the end
|
|
385
|
-
if (endIdx - startIdx < contentHeight) {
|
|
386
|
-
startIdx = Math.max(0, endIdx - contentHeight);
|
|
387
|
-
}
|
|
211
|
+
const { schema, cursor } = state;
|
|
212
|
+
const box = new Box({ width, padding: 1 });
|
|
388
213
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
214
|
+
const half = Math.floor(height / 2);
|
|
215
|
+
let start = Math.max(0, cursor - half);
|
|
216
|
+
let end = Math.min(schema.length, start + height);
|
|
217
|
+
if (end - start < height) start = Math.max(0, end - height);
|
|
393
218
|
|
|
394
|
-
|
|
395
|
-
for (let i = startIdx; i < endIdx; i++) {
|
|
219
|
+
for (let i = start; i < end; i++) {
|
|
396
220
|
const col = schema[i];
|
|
397
221
|
const isSelected = i === cursor;
|
|
222
|
+
const rowBg = isSelected ? THEME.selectionBg : THEME.background;
|
|
398
223
|
|
|
399
|
-
const name = pad(
|
|
400
|
-
const type = pad(
|
|
401
|
-
|
|
402
|
-
// Build attributes string
|
|
403
|
-
const attrs: string[] = [];
|
|
224
|
+
const name = pad(col.name, 25);
|
|
225
|
+
const type = pad(col.type, 15);
|
|
226
|
+
const attrs = [];
|
|
404
227
|
if (col.pk) attrs.push('PK');
|
|
405
228
|
if (col.notnull) attrs.push('NOT NULL');
|
|
406
|
-
if (col.dflt_value !== null) attrs.push(`DEFAULT ${col.dflt_value}`);
|
|
407
|
-
const attrStr = attrs.length > 0 ? truncate(attrs.join(', '), attrWidth) : '';
|
|
408
|
-
|
|
409
|
-
// Build content without selection color first
|
|
410
|
-
const cursorChar = isSelected ? ` ${UI.cursor}` : ' ';
|
|
411
|
-
const content = `${cursorChar} ${name} ${COLORS.cyan}${type}${COLORS.reset} ${COLORS.dim}${attrStr}${COLORS.reset}`;
|
|
412
|
-
|
|
413
|
-
// Calculate visible width (accounting for CJK and ANSI codes)
|
|
414
|
-
const contentWidth = getVisibleWidth(content);
|
|
415
229
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
230
|
+
let rowContent = `${ANSI.fg(isSelected ? THEME.primary : THEME.text)}${name}`;
|
|
231
|
+
rowContent += `${ANSI.fg(THEME.secondary)}${type}`;
|
|
232
|
+
rowContent += `${ANSI.fg(THEME.textDim)}${attrs.join(', ')}`;
|
|
233
|
+
lines.push(box.render(rowContent, { background: rowBg }));
|
|
234
|
+
}
|
|
235
|
+
return lines;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private renderRowDetail(state: RowDetailViewState, height: number, width: number): string[] {
|
|
239
|
+
const allLines: string[] = [];
|
|
240
|
+
const { row, schema } = state;
|
|
241
|
+
const innerWidth = width - 2;
|
|
242
|
+
const box = new Box({ width, padding: 1, background: THEME.background });
|
|
243
|
+
|
|
244
|
+
// Calculate max label width for alignment
|
|
245
|
+
let maxLabelWidth = 0;
|
|
246
|
+
schema.forEach(col => {
|
|
247
|
+
maxLabelWidth = Math.max(maxLabelWidth, getVisibleWidth(col.name));
|
|
248
|
+
});
|
|
249
|
+
const labelPad = maxLabelWidth + 2; // +2 for ": "
|
|
250
|
+
|
|
251
|
+
schema.forEach((col) => {
|
|
252
|
+
const label = `${ANSI.bold}${ANSI.fg(THEME.secondary)}${pad(col.name, maxLabelWidth)}${ANSI.reset}: `;
|
|
253
|
+
const val = formatValue(row[col.name], undefined, true);
|
|
254
|
+
|
|
255
|
+
if (labelPad > innerWidth * 0.4) {
|
|
256
|
+
// Label too long, fallback to simpler layout
|
|
257
|
+
const simpleLabel = `${ANSI.bold}${ANSI.fg(THEME.secondary)}${col.name}${ANSI.reset}: `;
|
|
258
|
+
allLines.push(box.render(simpleLabel));
|
|
259
|
+
const wrappedLines = wrapText(val, innerWidth);
|
|
260
|
+
wrappedLines.forEach(line => {
|
|
261
|
+
allLines.push(box.render(`${ANSI.fg(THEME.text)}${line}`));
|
|
262
|
+
});
|
|
426
263
|
} else {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
264
|
+
const firstLineMax = innerWidth - labelPad;
|
|
265
|
+
const wrappedLines = wrapText(val, firstLineMax);
|
|
266
|
+
|
|
267
|
+
if (wrappedLines.length === 0 || (wrappedLines.length === 1 && wrappedLines[0] === '')) {
|
|
268
|
+
allLines.push(box.render(`${label}`));
|
|
269
|
+
} else {
|
|
270
|
+
allLines.push(box.render(`${label}${ANSI.fg(THEME.text)}${wrappedLines[0]}`));
|
|
271
|
+
|
|
272
|
+
if (wrappedLines.length > 1) {
|
|
273
|
+
wrappedLines.slice(1).forEach(line => {
|
|
274
|
+
allLines.push(box.render(`${' '.repeat(labelPad)}${ANSI.fg(THEME.text)}${line}`));
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
433
278
|
}
|
|
434
|
-
|
|
435
|
-
lines.push(line);
|
|
436
|
-
}
|
|
279
|
+
});
|
|
437
280
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
lines.push(' '.repeat(width));
|
|
441
|
-
}
|
|
281
|
+
state.totalLines = allLines.length;
|
|
282
|
+
state.visibleHeight = height;
|
|
442
283
|
|
|
443
|
-
|
|
284
|
+
// Apply scroll offset
|
|
285
|
+
return allLines.slice(state.scrollOffset, state.scrollOffset + height);
|
|
444
286
|
}
|
|
445
287
|
|
|
446
|
-
|
|
447
|
-
* Build row detail view
|
|
448
|
-
*/
|
|
449
|
-
private buildRowDetail(state: RowDetailViewState, height: number, width: number): string[] {
|
|
288
|
+
private renderHealth(state: HealthViewState, height: number, width: number): string[] {
|
|
450
289
|
const lines: string[] = [];
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
for (const col of schema) {
|
|
461
|
-
if (lineCount >= height) break;
|
|
462
|
-
|
|
463
|
-
const colName = `${COLORS.cyan}${pad(col.name, maxColNameLength)}${COLORS.reset}`;
|
|
464
|
-
const value = formatValue(row[col.name]);
|
|
465
|
-
|
|
466
|
-
// Truncate value to fit in single line
|
|
467
|
-
const truncatedValue = truncate(value, valueWidth);
|
|
468
|
-
|
|
469
|
-
let line = ` ${colName} ${COLORS.dim}:${COLORS.reset} ${truncatedValue}`;
|
|
470
|
-
const lineWidth = getVisibleWidth(line);
|
|
471
|
-
if (lineWidth < width - rightMargin) {
|
|
472
|
-
line += ' '.repeat(width - rightMargin - lineWidth);
|
|
473
|
-
}
|
|
474
|
-
lines.push(line);
|
|
475
|
-
lineCount++;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// Fill remaining lines
|
|
479
|
-
while (lines.length < height) {
|
|
480
|
-
lines.push(' '.repeat(width));
|
|
481
|
-
}
|
|
482
|
-
|
|
290
|
+
const entries = Object.entries(state.info);
|
|
291
|
+
const box = new Box({ width, padding: 1, background: THEME.background });
|
|
292
|
+
|
|
293
|
+
entries.forEach(([key, val], idx) => {
|
|
294
|
+
if (idx >= height) return;
|
|
295
|
+
const label = pad(key.replace(/_/g, ' '), 25);
|
|
296
|
+
const rowContent = `${ANSI.fg(THEME.secondary)}${label}${ANSI.reset} : ${val}`;
|
|
297
|
+
lines.push(box.render(rowContent));
|
|
298
|
+
});
|
|
483
299
|
return lines;
|
|
484
300
|
}
|
|
485
301
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
const { info } = state;
|
|
492
|
-
|
|
493
|
-
const title = `${COLORS.bold}Core health overview${COLORS.reset}`;
|
|
494
|
-
lines.push(pad(title, width));
|
|
495
|
-
|
|
496
|
-
const formatBool = (value: string): string => (value === '1' ? 'on' : value === '0' ? 'off' : value);
|
|
497
|
-
const formatAutoVacuum = (value: string): string => {
|
|
498
|
-
if (value === '0') return 'none';
|
|
499
|
-
if (value === '1') return 'full';
|
|
500
|
-
if (value === '2') return 'incremental';
|
|
501
|
-
return value;
|
|
502
|
-
};
|
|
503
|
-
|
|
504
|
-
const entries: Array<[string, string]> = [
|
|
505
|
-
['SQLite version', info.sqlite_version],
|
|
506
|
-
['Journal mode', info.journal_mode],
|
|
507
|
-
['Synchronous', info.synchronous],
|
|
508
|
-
['Locking mode', info.locking_mode],
|
|
509
|
-
['Page size', formatNumber(info.page_size)],
|
|
510
|
-
['Page count', formatNumber(info.page_count)],
|
|
511
|
-
['Freelist pages', formatNumber(info.freelist_count)],
|
|
512
|
-
['Cache size', formatNumber(info.cache_size)],
|
|
513
|
-
['WAL autocheckpoint', formatNumber(info.wal_autocheckpoint)],
|
|
514
|
-
['Auto vacuum', formatAutoVacuum(info.auto_vacuum)],
|
|
515
|
-
['User version', String(info.user_version)],
|
|
516
|
-
['Application id', String(info.application_id)],
|
|
517
|
-
['Encoding', info.encoding],
|
|
518
|
-
['Foreign keys', formatBool(info.foreign_keys)],
|
|
519
|
-
['Temp store', info.temp_store],
|
|
520
|
-
['Mmap size', formatNumber(info.mmap_size)],
|
|
521
|
-
['Busy timeout', `${formatNumber(info.busy_timeout)} ms`]
|
|
522
|
-
];
|
|
523
|
-
|
|
524
|
-
const labelWidth = Math.min(24, Math.max(...entries.map(([label]) => label.length)) + 1);
|
|
525
|
-
const contentWidth = width - labelWidth - 4;
|
|
526
|
-
|
|
527
|
-
for (const [label, value] of entries) {
|
|
528
|
-
if (lines.length >= height) break;
|
|
529
|
-
const labelText = pad(label, labelWidth, 'right');
|
|
530
|
-
const valueText = truncate(value, contentWidth);
|
|
531
|
-
const line = ` ${COLORS.cyan}${labelText}${COLORS.reset} ${COLORS.dim}:${COLORS.reset} ${valueText}`;
|
|
532
|
-
const lineWidth = getVisibleWidth(line);
|
|
533
|
-
if (lineWidth < width) {
|
|
534
|
-
lines.push(line + ' '.repeat(width - lineWidth));
|
|
535
|
-
} else {
|
|
536
|
-
const basicLine = ` ${labelText} : ${valueText}`;
|
|
537
|
-
lines.push(truncate(basicLine, width - 1));
|
|
538
|
-
}
|
|
302
|
+
private buildHelpBar(state: ViewState, width: number): string {
|
|
303
|
+
const box = new Box({ width, padding: 1, background: THEME.footerBg });
|
|
304
|
+
|
|
305
|
+
if ((state as any).notice) {
|
|
306
|
+
return box.render(`${ANSI.fg(THEME.textDim)}${(state as any).notice}`);
|
|
539
307
|
}
|
|
540
308
|
|
|
541
|
-
|
|
542
|
-
|
|
309
|
+
let helpItems: { key: string; label: string }[] = [];
|
|
310
|
+
switch (state.type) {
|
|
311
|
+
case 'tables':
|
|
312
|
+
helpItems = [
|
|
313
|
+
{ key: 'j/k', label: 'select' },
|
|
314
|
+
{ key: 'g/G', label: 'first/last' },
|
|
315
|
+
{ key: 'Enter/l', label: 'open' },
|
|
316
|
+
{ key: 'i', label: 'info' },
|
|
317
|
+
{ key: 'q', label: 'quit' }
|
|
318
|
+
];
|
|
319
|
+
break;
|
|
320
|
+
case 'table-detail':
|
|
321
|
+
helpItems = [
|
|
322
|
+
{ key: 'j/k', label: 'scroll' },
|
|
323
|
+
{ key: 'g/G', label: 'first/last' },
|
|
324
|
+
{ key: 'Enter/l', label: 'row' },
|
|
325
|
+
{ key: 's', label: 'schema' },
|
|
326
|
+
{ key: 'h', label: 'back' },
|
|
327
|
+
{ key: 'q', label: 'quit' }
|
|
328
|
+
];
|
|
329
|
+
break;
|
|
330
|
+
case 'schema-view':
|
|
331
|
+
helpItems = [
|
|
332
|
+
{ key: 'j/k', label: 'scroll' },
|
|
333
|
+
{ key: 'g/G', label: 'first/last' },
|
|
334
|
+
{ key: 's/h', label: 'back' },
|
|
335
|
+
{ key: 'q', label: 'quit' }
|
|
336
|
+
];
|
|
337
|
+
break;
|
|
338
|
+
case 'row-detail':
|
|
339
|
+
helpItems = [
|
|
340
|
+
{ key: 'j/k', label: 'switch' },
|
|
341
|
+
{ key: '↑/↓', label: 'scroll' },
|
|
342
|
+
{ key: 'h', label: 'back' },
|
|
343
|
+
{ key: 'q', label: 'quit' }
|
|
344
|
+
];
|
|
345
|
+
break;
|
|
346
|
+
default:
|
|
347
|
+
helpItems = [
|
|
348
|
+
{ key: 'h', label: 'back' },
|
|
349
|
+
{ key: 'q', label: 'quit' }
|
|
350
|
+
];
|
|
351
|
+
break;
|
|
543
352
|
}
|
|
544
353
|
|
|
545
|
-
|
|
546
|
-
|
|
354
|
+
const styledHelp = helpItems
|
|
355
|
+
.map(item => `${ANSI.fg(THEME.text)}${item.key} ${ANSI.fg(THEME.textDim)}${item.label}`)
|
|
356
|
+
.join(' ');
|
|
547
357
|
|
|
548
|
-
|
|
549
|
-
* Build help bar (bottom line)
|
|
550
|
-
*/
|
|
551
|
-
private buildHelpBar(state: ViewState, width: number): string {
|
|
552
|
-
let help = '';
|
|
553
|
-
const notice =
|
|
554
|
-
(state.type === 'table-detail' || state.type === 'row-detail') ? state.notice : undefined;
|
|
555
|
-
const deleteStep =
|
|
556
|
-
(state.type === 'table-detail' || state.type === 'row-detail') ? state.deleteConfirm?.step : undefined;
|
|
557
|
-
const isDeleteConfirm =
|
|
558
|
-
(state.type === 'table-detail' || state.type === 'row-detail') ? Boolean(state.deleteConfirm) : false;
|
|
559
|
-
|
|
560
|
-
if (notice && isDeleteConfirm) {
|
|
561
|
-
const stepLabel = deleteStep ? `Step ${deleteStep}/2` : '';
|
|
562
|
-
const actionLabel = deleteStep === 2 ? 'delete' : 'confirm';
|
|
563
|
-
help = ` ${COLORS.red}${COLORS.bold}${stepLabel ? `${stepLabel} ` : ''}${notice}${COLORS.reset} ${COLORS.yellow}[y] ${actionLabel}${COLORS.reset} [h/Esc] cancel`;
|
|
564
|
-
const paddedHelp = pad(help, width);
|
|
565
|
-
return paddedHelp;
|
|
566
|
-
} else if (notice) {
|
|
567
|
-
help = ` ${COLORS.yellow}${notice}${COLORS.reset}`;
|
|
568
|
-
const paddedHelp = pad(help, width);
|
|
569
|
-
return paddedHelp;
|
|
570
|
-
} else if (state.type === 'tables') {
|
|
571
|
-
help = ' [j/k] select [Enter/l] open [i] info [g/G] top/bottom [q] quit';
|
|
572
|
-
} else if (state.type === 'table-detail') {
|
|
573
|
-
help = ' [j/k] scroll [Enter/l] view row [Backspace] delete [s] toggle schema [h/Esc] back [q] quit';
|
|
574
|
-
} else if (state.type === 'schema-view') {
|
|
575
|
-
help = ' [j/k] scroll [g/G] top/bottom [s/h/Esc] back [q] quit';
|
|
576
|
-
} else if (state.type === 'row-detail') {
|
|
577
|
-
help = ' [Backspace] delete [h/Esc] back [q] quit';
|
|
578
|
-
} else if (state.type === 'health') {
|
|
579
|
-
help = ' [i] back [h/Esc] back [q] quit';
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
const paddedHelp = pad(help, width);
|
|
583
|
-
return `${COLORS.dim}${paddedHelp}${COLORS.reset}`;
|
|
358
|
+
return box.render(styledHelp, { align: 'right' });
|
|
584
359
|
}
|
|
585
360
|
}
|