@cosider.construction/eapp 1.0.2 → 1.0.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 +1 -1
- package/src/index.js +61 -19
- package/src/tui/engine.js +140 -105
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -39,28 +39,70 @@ Options:
|
|
|
39
39
|
--help, -h Show this help
|
|
40
40
|
`;
|
|
41
41
|
|
|
42
|
+
const MAIN_MENU = [
|
|
43
|
+
{ label: 'Entity', value: 'entity' },
|
|
44
|
+
{ label: 'View', value: 'view' },
|
|
45
|
+
{ label: 'Module', value: 'module' },
|
|
46
|
+
{ label: 'Menu', value: 'menu' },
|
|
47
|
+
{ label: 'Config', value: 'config' },
|
|
48
|
+
{ label: 'Pivot', value: 'pivot' },
|
|
49
|
+
{ label: 'Layout', value: 'layout' },
|
|
50
|
+
{ label: 'List', value: 'list' },
|
|
51
|
+
{ label: 'Quit', value: 'quit' },
|
|
52
|
+
];
|
|
53
|
+
|
|
42
54
|
async function launchInteractive() {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
console.log('');
|
|
57
|
-
if (chosen === 'list') {
|
|
58
|
-
await listModules();
|
|
55
|
+
let lastIdx = 0;
|
|
56
|
+
|
|
57
|
+
// eslint-disable-next-line no-constant-condition
|
|
58
|
+
while (true) {
|
|
59
|
+
// Clear screen so each iteration feels fresh
|
|
60
|
+
process.stdout.write('\x1B[2J\x1B[H');
|
|
61
|
+
console.log('');
|
|
62
|
+
console.log(chalk.bold.cyan(' eapp') + chalk.dim(' — what do you want to do?'));
|
|
63
|
+
console.log(chalk.dim(' ← → select enter confirm Quit to exit'));
|
|
64
|
+
|
|
65
|
+
const chosen = await radioLine('', MAIN_MENU, lastIdx);
|
|
66
|
+
lastIdx = MAIN_MENU.findIndex(m => m.value === chosen);
|
|
67
|
+
|
|
59
68
|
console.log('');
|
|
60
|
-
|
|
61
|
-
|
|
69
|
+
|
|
70
|
+
if (chosen === 'quit') {
|
|
71
|
+
console.log(chalk.dim(' Bye.\n'));
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (chosen === 'list') {
|
|
76
|
+
await listModules();
|
|
77
|
+
console.log('');
|
|
78
|
+
await listEntities([]);
|
|
79
|
+
} else {
|
|
80
|
+
await run(chosen, [], []);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Pause briefly so the user can read the result before the menu redraws
|
|
84
|
+
console.log('');
|
|
85
|
+
console.log(chalk.dim(' Press enter to return to the menu…'));
|
|
86
|
+
await waitEnter();
|
|
62
87
|
}
|
|
63
|
-
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function waitEnter() {
|
|
91
|
+
return new Promise((resolve) => {
|
|
92
|
+
process.stdin.resume();
|
|
93
|
+
process.stdin.setEncoding('utf8');
|
|
94
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
95
|
+
const onKey = (key) => {
|
|
96
|
+
if (key === '\r' || key === '\n' || key === '\x03') {
|
|
97
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
98
|
+
process.stdin.pause();
|
|
99
|
+
process.stdin.removeListener('data', onKey);
|
|
100
|
+
if (key === '\x03') process.exit(0);
|
|
101
|
+
resolve();
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
process.stdin.on('data', onKey);
|
|
105
|
+
});
|
|
64
106
|
}
|
|
65
107
|
|
|
66
108
|
async function run(command, args, rawArgs) {
|
package/src/tui/engine.js
CHANGED
|
@@ -10,42 +10,37 @@ import chalk from 'chalk';
|
|
|
10
10
|
// ─── Raw input helpers ────────────────────────────────────────────────────────
|
|
11
11
|
|
|
12
12
|
export function enableRawInput() {
|
|
13
|
-
if (process.stdin.isTTY)
|
|
14
|
-
process.stdin.setRawMode(true);
|
|
15
|
-
}
|
|
13
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
16
14
|
process.stdin.resume();
|
|
17
15
|
process.stdin.setEncoding('utf8');
|
|
18
16
|
}
|
|
19
17
|
|
|
20
18
|
export function disableRawInput() {
|
|
21
|
-
if (process.stdin.isTTY)
|
|
22
|
-
process.stdin.setRawMode(false);
|
|
23
|
-
}
|
|
19
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
24
20
|
process.stdin.pause();
|
|
25
21
|
}
|
|
26
22
|
|
|
27
23
|
export function clearLines(n) {
|
|
28
|
-
for (let i = 0; i < n; i++)
|
|
29
|
-
process.stdout.write('\x1B[1A\x1B[2K');
|
|
30
|
-
}
|
|
24
|
+
for (let i = 0; i < n; i++) process.stdout.write('\x1B[1A\x1B[2K');
|
|
31
25
|
}
|
|
32
26
|
|
|
33
|
-
export function
|
|
34
|
-
export function
|
|
27
|
+
export function clearScreen() { process.stdout.write('\x1B[2J\x1B[H'); }
|
|
28
|
+
export function hideCursor() { process.stdout.write('\x1B[?25l'); }
|
|
29
|
+
export function showCursor() { process.stdout.write('\x1B[?25h'); }
|
|
35
30
|
|
|
36
31
|
// ─── Key constants ────────────────────────────────────────────────────────────
|
|
37
32
|
|
|
38
33
|
export const KEYS = {
|
|
39
|
-
UP:
|
|
40
|
-
DOWN:
|
|
41
|
-
RIGHT:
|
|
42
|
-
LEFT:
|
|
43
|
-
ENTER:
|
|
44
|
-
SPACE:
|
|
45
|
-
PLUS:
|
|
46
|
-
MINUS:
|
|
47
|
-
ESC:
|
|
48
|
-
CTRL_C:
|
|
34
|
+
UP: '\x1B[A',
|
|
35
|
+
DOWN: '\x1B[B',
|
|
36
|
+
RIGHT: '\x1B[C',
|
|
37
|
+
LEFT: '\x1B[D',
|
|
38
|
+
ENTER: '\r',
|
|
39
|
+
SPACE: ' ',
|
|
40
|
+
PLUS: '+',
|
|
41
|
+
MINUS: '-',
|
|
42
|
+
ESC: '\x1B',
|
|
43
|
+
CTRL_C: '\x03',
|
|
49
44
|
BACKSPACE: '\x7F',
|
|
50
45
|
};
|
|
51
46
|
|
|
@@ -65,8 +60,8 @@ export async function textInput(prompt, defaultVal = '') {
|
|
|
65
60
|
// ─── Radio selector ───────────────────────────────────────────────────────────
|
|
66
61
|
|
|
67
62
|
/**
|
|
68
|
-
* Horizontal radio — one choice from options
|
|
69
|
-
* Returns selected value
|
|
63
|
+
* Horizontal radio — one choice from options.
|
|
64
|
+
* Returns selected value.
|
|
70
65
|
*/
|
|
71
66
|
export async function radioLine(prompt, options, defaultIdx = 0) {
|
|
72
67
|
let selected = defaultIdx;
|
|
@@ -75,9 +70,7 @@ export async function radioLine(prompt, options, defaultIdx = 0) {
|
|
|
75
70
|
process.stdout.clearLine?.(0);
|
|
76
71
|
process.stdout.cursorTo?.(0);
|
|
77
72
|
const opts = options.map((o, i) =>
|
|
78
|
-
i === selected
|
|
79
|
-
? chalk.green(`◉ ${o.label}`)
|
|
80
|
-
: chalk.dim(`○ ${o.label}`)
|
|
73
|
+
i === selected ? chalk.green(`◉ ${o.label}`) : chalk.dim(`○ ${o.label}`)
|
|
81
74
|
).join(' ');
|
|
82
75
|
process.stdout.write(` ${chalk.cyan(prompt)} ${opts}`);
|
|
83
76
|
};
|
|
@@ -90,9 +83,9 @@ export async function radioLine(prompt, options, defaultIdx = 0) {
|
|
|
90
83
|
|
|
91
84
|
const onKey = (key) => {
|
|
92
85
|
if (key === KEYS.CTRL_C) { showCursor(); disableRawInput(); process.exit(0); }
|
|
93
|
-
if (key === KEYS.LEFT)
|
|
94
|
-
if (key === KEYS.RIGHT)
|
|
95
|
-
if (key === KEYS.ENTER)
|
|
86
|
+
if (key === KEYS.LEFT) { selected = (selected - 1 + options.length) % options.length; render(); }
|
|
87
|
+
if (key === KEYS.RIGHT) { selected = (selected + 1) % options.length; render(); }
|
|
88
|
+
if (key === KEYS.ENTER) {
|
|
96
89
|
process.stdout.write('\n');
|
|
97
90
|
process.stdin.removeListener('data', onKey);
|
|
98
91
|
showCursor();
|
|
@@ -107,43 +100,46 @@ export async function radioLine(prompt, options, defaultIdx = 0) {
|
|
|
107
100
|
// ─── Options toggles ──────────────────────────────────────────────────────────
|
|
108
101
|
|
|
109
102
|
/**
|
|
110
|
-
* One-line toggleable options
|
|
103
|
+
* One-line toggleable options.
|
|
111
104
|
* Returns object { [key]: boolean }
|
|
112
105
|
*/
|
|
113
106
|
export async function optionsLine(options) {
|
|
114
|
-
// options = [{ key, label, default }]
|
|
115
107
|
const state = {};
|
|
116
108
|
options.forEach(o => { state[o.key] = o.default !== false; });
|
|
117
109
|
let cursor = 0;
|
|
118
110
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const
|
|
111
|
+
// Lines: 1 (options row) + 1 (hint row) — always exactly 2
|
|
112
|
+
const RENDERED_LINES = 2;
|
|
113
|
+
|
|
114
|
+
const renderRow = () => {
|
|
115
|
+
return options.map((o, i) => {
|
|
116
|
+
const on = state[o.key];
|
|
117
|
+
const box = on ? chalk.green('◼') : chalk.dim('◻');
|
|
118
|
+
const label = i === cursor
|
|
119
|
+
? chalk.bold.white(o.label)
|
|
120
|
+
: (on ? chalk.white(o.label) : chalk.dim(o.label));
|
|
125
121
|
return `${box} ${label}`;
|
|
126
122
|
}).join(' ');
|
|
127
|
-
console.log(' ' + rendered);
|
|
128
|
-
console.log(chalk.dim(' ← → navigate space toggle enter confirm'));
|
|
129
123
|
};
|
|
130
124
|
|
|
131
125
|
return new Promise((resolve) => {
|
|
132
126
|
hideCursor();
|
|
133
127
|
enableRawInput();
|
|
134
128
|
console.log('');
|
|
135
|
-
|
|
136
|
-
const on = state[o.key];
|
|
137
|
-
return `${on ? chalk.green('◼') : chalk.dim('◻')} ${on ? chalk.white(o.label) : chalk.dim(o.label)}`;
|
|
138
|
-
}).join(' ');
|
|
139
|
-
console.log(' ' + rendered);
|
|
129
|
+
console.log(' ' + renderRow());
|
|
140
130
|
console.log(chalk.dim(' ← → navigate space toggle enter confirm'));
|
|
141
131
|
|
|
142
132
|
const onKey = (key) => {
|
|
143
133
|
if (key === KEYS.CTRL_C) { showCursor(); disableRawInput(); process.exit(0); }
|
|
144
|
-
if (key === KEYS.LEFT)
|
|
145
|
-
if (key === KEYS.RIGHT)
|
|
146
|
-
if (key === KEYS.SPACE)
|
|
134
|
+
if (key === KEYS.LEFT) { cursor = (cursor - 1 + options.length) % options.length; }
|
|
135
|
+
if (key === KEYS.RIGHT) { cursor = (cursor + 1) % options.length; }
|
|
136
|
+
if (key === KEYS.SPACE) { state[options[cursor].key] = !state[options[cursor].key]; }
|
|
137
|
+
if (key === KEYS.LEFT || key === KEYS.RIGHT || key === KEYS.SPACE) {
|
|
138
|
+
clearLines(RENDERED_LINES);
|
|
139
|
+
console.log(' ' + renderRow());
|
|
140
|
+
console.log(chalk.dim(' ← → navigate space toggle enter confirm'));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
147
143
|
if (key === KEYS.ENTER) {
|
|
148
144
|
process.stdin.removeListener('data', onKey);
|
|
149
145
|
showCursor();
|
|
@@ -157,72 +153,107 @@ export async function optionsLine(options) {
|
|
|
157
153
|
|
|
158
154
|
// ─── Interactive table ────────────────────────────────────────────────────────
|
|
159
155
|
|
|
160
|
-
const COL_WIDTHS = {
|
|
161
|
-
field: 18,
|
|
162
|
-
type: 10,
|
|
163
|
-
values: 14,
|
|
164
|
-
pk: 4,
|
|
165
|
-
fk: 4,
|
|
166
|
-
group: 14,
|
|
167
|
-
};
|
|
168
|
-
|
|
169
156
|
const FIELD_TYPES = ['varchar', 'int', 'decimal', 'date', 'datetime', 'bit', 'text', 'float', 'enum'];
|
|
170
157
|
|
|
171
|
-
|
|
158
|
+
// Columns where space toggles a boolean (rendered as ◼/◻ checkboxes)
|
|
159
|
+
const BOOL_COLS = new Set(['pk', 'fk']);
|
|
160
|
+
|
|
161
|
+
// Columns where space cycles between string values
|
|
162
|
+
const CYCLE_COLS = {
|
|
163
|
+
vmenu: ['own', 'share'],
|
|
164
|
+
home: ['on', 'off'],
|
|
165
|
+
grid: ['on', 'off'],
|
|
166
|
+
};
|
|
172
167
|
|
|
168
|
+
// Strip ANSI escape codes to get the true printable length of a string.
|
|
169
|
+
const ANSI_RE = /\x1B\[[0-9;]*m/g;
|
|
170
|
+
function visibleLen(s) { return String(s).replace(ANSI_RE, '').length; }
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Pad or truncate str to exactly len visible characters.
|
|
174
|
+
* Too-long strings are cut with a trailing ellipsis so column borders stay aligned.
|
|
175
|
+
*/
|
|
173
176
|
function pad(str, len) {
|
|
174
|
-
const s
|
|
175
|
-
|
|
177
|
+
const s = String(str ?? '');
|
|
178
|
+
const vl = visibleLen(s);
|
|
179
|
+
if (vl === len) return s;
|
|
180
|
+
if (vl > len) return s.replace(ANSI_RE, '').slice(0, len - 1) + '…';
|
|
181
|
+
return s + ' '.repeat(len - vl);
|
|
176
182
|
}
|
|
177
183
|
|
|
178
184
|
function renderTableHeader(cols, widths) {
|
|
179
|
-
const header
|
|
185
|
+
const header = cols.map(c => chalk.bold.cyan(pad(c, widths[c] || 12))).join(chalk.dim('│'));
|
|
180
186
|
const divider = cols.map(c => '─'.repeat(widths[c] || 12)).join('┼');
|
|
181
187
|
return ` ┌${'─'.repeat(divider.length)}┐\n │${header}│\n ├${divider}┤`;
|
|
182
188
|
}
|
|
183
189
|
|
|
184
190
|
function renderFieldRow(row, cols, widths, isActive, activeCol) {
|
|
185
191
|
const cells = cols.map((col, ci) => {
|
|
186
|
-
|
|
187
|
-
if (col === 'pk' || col === 'fk') val = val ? chalk.green('◼') : chalk.dim('◻');
|
|
192
|
+
const colW = widths[col] || 12;
|
|
188
193
|
const isThisCell = isActive && ci === activeCol;
|
|
189
|
-
|
|
190
|
-
|
|
194
|
+
|
|
195
|
+
let cell;
|
|
196
|
+
if (BOOL_COLS.has(col)) {
|
|
197
|
+
// Boolean: pad the plain glyph first so width is exact, then colorize
|
|
198
|
+
const glyph = pad(row[col] ? '◼' : '◻', colW);
|
|
199
|
+
cell = isThisCell
|
|
200
|
+
? chalk.bgBlue.white(glyph)
|
|
201
|
+
: (row[col] ? chalk.green(glyph) : chalk.dim(glyph));
|
|
202
|
+
} else {
|
|
203
|
+
// Text / cycle: pad plain text, then colorize — ANSI codes added after padding
|
|
204
|
+
const plain = pad(String(row[col] ?? ''), colW);
|
|
205
|
+
cell = isThisCell
|
|
206
|
+
? chalk.bgBlue.white(plain)
|
|
207
|
+
: (isActive ? chalk.white(plain) : chalk.dim(plain));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return cell;
|
|
191
211
|
}).join(chalk.dim('│'));
|
|
192
212
|
return ` │${cells}│`;
|
|
193
213
|
}
|
|
194
214
|
|
|
195
215
|
/**
|
|
196
|
-
*
|
|
197
|
-
*
|
|
216
|
+
* Count of lines the table currently occupies (for accurate clearLines).
|
|
217
|
+
* 1 blank line before title
|
|
218
|
+
* 1 title line
|
|
219
|
+
* 3 header block (top border + header row + divider)
|
|
220
|
+
* N data rows
|
|
221
|
+
* 1 bottom border
|
|
222
|
+
* 1 hint line
|
|
223
|
+
* + type menu lines (when open)
|
|
224
|
+
*/
|
|
225
|
+
function tableLineCount(rowCount, typeMenuOpen) {
|
|
226
|
+
return 1 + 1 + 3 + rowCount + 1 + 1 + (typeMenuOpen ? FIELD_TYPES.length + 2 : 0);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function cycleValue(col, current) {
|
|
230
|
+
const opts = CYCLE_COLS[col];
|
|
231
|
+
if (!opts) return current;
|
|
232
|
+
return opts[(opts.indexOf(current) + 1) % opts.length];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Interactive fields table.
|
|
237
|
+
* Returns array of field objects.
|
|
198
238
|
*/
|
|
199
239
|
export async function fieldsTable(initialRows, cols, widths, title) {
|
|
200
240
|
const rows = initialRows.map(r => ({ ...r }));
|
|
201
241
|
let rowIdx = 0;
|
|
202
242
|
let colIdx = 0;
|
|
203
|
-
let editBuffer = null; // currently editing text cell
|
|
204
243
|
let typeMenuOpen = false;
|
|
205
|
-
let typeMenuIdx
|
|
206
|
-
|
|
207
|
-
const toggleCols = cols.filter(c => c === 'pk' || c === 'fk');
|
|
208
|
-
const textCols = cols.filter(c => c !== 'pk' && c !== 'fk');
|
|
209
|
-
|
|
210
|
-
function totalLines() {
|
|
211
|
-
return rows.length + 4; // header(3) + footer(1)
|
|
212
|
-
}
|
|
244
|
+
let typeMenuIdx = 0;
|
|
213
245
|
|
|
214
246
|
function render(initial = false) {
|
|
215
|
-
if (!initial) clearLines(
|
|
247
|
+
if (!initial) clearLines(tableLineCount(rows.length, typeMenuOpen));
|
|
216
248
|
|
|
217
249
|
console.log('\n ' + chalk.bold.white(title));
|
|
218
250
|
console.log(renderTableHeader(cols, widths));
|
|
219
|
-
|
|
220
251
|
rows.forEach((row, ri) => {
|
|
221
252
|
console.log(renderFieldRow(row, cols, widths, ri === rowIdx, colIdx));
|
|
222
253
|
});
|
|
223
|
-
|
|
224
|
-
console.log(` └${'─'.repeat(
|
|
225
|
-
console.log(chalk.dim(' ← → cols ↑ ↓ rows + add
|
|
254
|
+
const totalW = cols.map(c => widths[c] || 12).reduce((a, b) => a + b + 1, -1);
|
|
255
|
+
console.log(` └${'─'.repeat(totalW)}┘`);
|
|
256
|
+
console.log(chalk.dim(' ← → cols ↑ ↓ rows + add - del space toggle/cycle enter done'));
|
|
226
257
|
|
|
227
258
|
if (typeMenuOpen) {
|
|
228
259
|
console.log('');
|
|
@@ -240,30 +271,29 @@ export async function fieldsTable(initialRows, cols, widths, title) {
|
|
|
240
271
|
const onKey = (key) => {
|
|
241
272
|
if (key === KEYS.CTRL_C) { showCursor(); disableRawInput(); process.exit(0); }
|
|
242
273
|
|
|
243
|
-
// Type
|
|
274
|
+
// ── Type picker ─────────────────────────────────────────────────────────
|
|
244
275
|
if (typeMenuOpen) {
|
|
245
276
|
if (key === KEYS.UP) { typeMenuIdx = (typeMenuIdx - 1 + FIELD_TYPES.length) % FIELD_TYPES.length; render(); return; }
|
|
246
277
|
if (key === KEYS.DOWN) { typeMenuIdx = (typeMenuIdx + 1) % FIELD_TYPES.length; render(); return; }
|
|
247
|
-
if (key === KEYS.ENTER) {
|
|
248
|
-
|
|
249
|
-
typeMenuOpen = false;
|
|
250
|
-
render();
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
if (key === KEYS.ESC) { typeMenuOpen = false; render(); return; }
|
|
278
|
+
if (key === KEYS.ENTER) { rows[rowIdx].type = FIELD_TYPES[typeMenuIdx]; typeMenuOpen = false; render(); return; }
|
|
279
|
+
if (key === KEYS.ESC) { typeMenuOpen = false; render(); return; }
|
|
254
280
|
return;
|
|
255
281
|
}
|
|
256
282
|
|
|
257
|
-
// Navigation
|
|
258
|
-
if (key === KEYS.UP) { rowIdx = Math.max(0, rowIdx - 1);
|
|
259
|
-
if (key === KEYS.DOWN) { rowIdx = Math.min(rows.length - 1, rowIdx + 1);
|
|
260
|
-
if (key === KEYS.LEFT) { colIdx = Math.max(0, colIdx - 1);
|
|
261
|
-
if (key === KEYS.RIGHT) { colIdx = Math.min(cols.length - 1, colIdx + 1);
|
|
283
|
+
// ── Navigation ──────────────────────────────────────────────────────────
|
|
284
|
+
if (key === KEYS.UP) { rowIdx = Math.max(0, rowIdx - 1); render(); return; }
|
|
285
|
+
if (key === KEYS.DOWN) { rowIdx = Math.min(rows.length - 1, rowIdx + 1); render(); return; }
|
|
286
|
+
if (key === KEYS.LEFT) { colIdx = Math.max(0, colIdx - 1); render(); return; }
|
|
287
|
+
if (key === KEYS.RIGHT) { colIdx = Math.min(cols.length - 1, colIdx + 1); render(); return; }
|
|
262
288
|
|
|
263
|
-
// Add / remove rows
|
|
289
|
+
// ── Add / remove rows ───────────────────────────────────────────────────
|
|
264
290
|
if (key === KEYS.PLUS) {
|
|
265
291
|
const newRow = {};
|
|
266
|
-
cols.forEach(c => {
|
|
292
|
+
cols.forEach(c => {
|
|
293
|
+
if (BOOL_COLS.has(c)) newRow[c] = false;
|
|
294
|
+
else if (CYCLE_COLS[c]) newRow[c] = CYCLE_COLS[c][0];
|
|
295
|
+
else newRow[c] = '';
|
|
296
|
+
});
|
|
267
297
|
rows.splice(rowIdx + 1, 0, newRow);
|
|
268
298
|
rowIdx++;
|
|
269
299
|
render();
|
|
@@ -275,29 +305,35 @@ export async function fieldsTable(initialRows, cols, widths, title) {
|
|
|
275
305
|
return;
|
|
276
306
|
}
|
|
277
307
|
|
|
278
|
-
//
|
|
308
|
+
// ── Space ───────────────────────────────────────────────────────────────
|
|
279
309
|
if (key === KEYS.SPACE) {
|
|
280
310
|
const col = cols[colIdx];
|
|
281
|
-
|
|
311
|
+
|
|
312
|
+
if (BOOL_COLS.has(col)) {
|
|
282
313
|
rows[rowIdx][col] = !rows[rowIdx][col];
|
|
283
|
-
//
|
|
314
|
+
// Enforce single PK
|
|
284
315
|
if (col === 'pk' && rows[rowIdx][col]) {
|
|
285
316
|
rows.forEach((r, i) => { if (i !== rowIdx) r.pk = false; });
|
|
286
317
|
}
|
|
287
318
|
render();
|
|
288
319
|
return;
|
|
289
320
|
}
|
|
290
|
-
|
|
321
|
+
|
|
322
|
+
if (CYCLE_COLS[col]) {
|
|
323
|
+
rows[rowIdx][col] = cycleValue(col, rows[rowIdx][col]);
|
|
324
|
+
render();
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
291
328
|
if (col === 'type') {
|
|
292
329
|
typeMenuOpen = true;
|
|
293
|
-
typeMenuIdx
|
|
294
|
-
if (typeMenuIdx < 0) typeMenuIdx = 0;
|
|
330
|
+
typeMenuIdx = Math.max(0, FIELD_TYPES.indexOf(rows[rowIdx].type || 'varchar'));
|
|
295
331
|
render();
|
|
296
332
|
return;
|
|
297
333
|
}
|
|
298
334
|
}
|
|
299
335
|
|
|
300
|
-
//
|
|
336
|
+
// ── Enter ────────────────────────────────────────────────────────────────
|
|
301
337
|
if (key === KEYS.ENTER) {
|
|
302
338
|
const col = cols[colIdx];
|
|
303
339
|
if (col === 'type') { typeMenuOpen = true; typeMenuIdx = 0; render(); return; }
|
|
@@ -308,9 +344,9 @@ export async function fieldsTable(initialRows, cols, widths, title) {
|
|
|
308
344
|
return;
|
|
309
345
|
}
|
|
310
346
|
|
|
311
|
-
// Text editing
|
|
347
|
+
// ── Text editing ─────────────────────────────────────────────────────────
|
|
312
348
|
const col = cols[colIdx];
|
|
313
|
-
if (col
|
|
349
|
+
if (BOOL_COLS.has(col) || CYCLE_COLS[col]) return; // not typed
|
|
314
350
|
|
|
315
351
|
if (key === KEYS.BACKSPACE) {
|
|
316
352
|
rows[rowIdx][col] = String(rows[rowIdx][col] || '').slice(0, -1);
|
|
@@ -318,7 +354,6 @@ export async function fieldsTable(initialRows, cols, widths, title) {
|
|
|
318
354
|
return;
|
|
319
355
|
}
|
|
320
356
|
|
|
321
|
-
// Printable character
|
|
322
357
|
if (key.length === 1 && key >= ' ') {
|
|
323
358
|
rows[rowIdx][col] = (rows[rowIdx][col] || '') + key;
|
|
324
359
|
render();
|