jaz-clio 4.35.1 → 4.35.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/assets/skills/api/SKILL.md +1 -1
- package/assets/skills/cli/SKILL.md +1 -1
- package/assets/skills/conversion/SKILL.md +1 -1
- package/assets/skills/jobs/SKILL.md +1 -1
- package/assets/skills/transaction-recipes/SKILL.md +1 -1
- package/dist/commands/picker.js +88 -90
- package/dist/commands/ui/index.js +1 -0
- package/dist/commands/ui/picker.js +180 -0
- package/package.json +1 -1
package/dist/commands/picker.js
CHANGED
|
@@ -1,13 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Uses `@clack/prompts` select with numbered choices and inline category tags.
|
|
5
|
-
* Activates only in TTY terminals — non-interactive usage (pipes, --json, CI)
|
|
6
|
-
* is unaffected.
|
|
7
|
-
*/
|
|
8
|
-
import * as p from '@clack/prompts';
|
|
9
|
-
import { muted } from './ui/theme.js';
|
|
10
|
-
// ── Category map (keyed by top-level command name) ──────────────────
|
|
1
|
+
import { showPicker, isCancel } from './ui/picker.js';
|
|
2
|
+
import { accent, muted } from './ui/theme.js';
|
|
3
|
+
// ── Category map ────────────────────────────────────────────────────
|
|
11
4
|
const CATEGORIES = {
|
|
12
5
|
invoices: 'AR',
|
|
13
6
|
'customer-credit-notes': 'AR',
|
|
@@ -50,9 +43,21 @@ const CATEGORY_ORDER = [
|
|
|
50
43
|
'AR', 'AP', 'Acctg', 'Banking', 'Jobs', 'Calc', 'Recipes',
|
|
51
44
|
'Reports', 'Data', 'AI', 'Setup',
|
|
52
45
|
];
|
|
53
|
-
|
|
46
|
+
const CATEGORY_HINTS = {
|
|
47
|
+
AR: 'Invoices, credit notes, payments',
|
|
48
|
+
AP: 'Bills, supplier credit notes',
|
|
49
|
+
Acctg: 'Journals, cash in/out/transfer',
|
|
50
|
+
Banking: 'Bank, reconciliation, cashflow',
|
|
51
|
+
Jobs: 'Month-end, recon, tax filing',
|
|
52
|
+
Calc: 'Loan, lease, depreciation, ECL',
|
|
53
|
+
Recipes: 'Capsule transactions (IFRS)',
|
|
54
|
+
Reports: 'TB, P&L, balance sheet, exports',
|
|
55
|
+
Data: 'Contacts, accounts, items, tags',
|
|
56
|
+
AI: 'Magic extraction, help center',
|
|
57
|
+
Setup: 'Auth, org, init, update',
|
|
58
|
+
};
|
|
54
59
|
const SKIP = new Set(['serve', 'mcp', 'help']);
|
|
55
|
-
// ── Collect leaf commands
|
|
60
|
+
// ── Collect leaf commands ───────────────────────────────────────────
|
|
56
61
|
function collectLeaves(commands, prefix, category) {
|
|
57
62
|
const leaves = [];
|
|
58
63
|
for (const cmd of commands) {
|
|
@@ -64,11 +69,8 @@ function collectLeaves(commands, prefix, category) {
|
|
|
64
69
|
const subs = cmd.commands.filter(c => !SKIP.has(c.name()));
|
|
65
70
|
const hasOwnAction = cmd._actionHandler != null;
|
|
66
71
|
if (subs.length > 0) {
|
|
67
|
-
|
|
68
|
-
// appear as both a leaf AND parent
|
|
69
|
-
if (hasOwnAction) {
|
|
72
|
+
if (hasOwnAction)
|
|
70
73
|
leaves.push({ path, description: cmd.description(), category: cat });
|
|
71
|
-
}
|
|
72
74
|
leaves.push(...collectLeaves(subs, path, cat));
|
|
73
75
|
}
|
|
74
76
|
else {
|
|
@@ -77,28 +79,9 @@ function collectLeaves(commands, prefix, category) {
|
|
|
77
79
|
}
|
|
78
80
|
return leaves;
|
|
79
81
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const sorted = [...leaves].sort((a, b) => {
|
|
84
|
-
const idxA = CATEGORY_ORDER.indexOf(a.category);
|
|
85
|
-
const idxB = CATEGORY_ORDER.indexOf(b.category);
|
|
86
|
-
const catA = idxA === -1 ? 999 : idxA;
|
|
87
|
-
const catB = idxB === -1 ? 999 : idxB;
|
|
88
|
-
if (catA !== catB)
|
|
89
|
-
return catA - catB;
|
|
90
|
-
return a.path.localeCompare(b.path);
|
|
91
|
-
});
|
|
92
|
-
const maxPath = Math.min(Math.max(...sorted.map(l => l.path.length)), 38);
|
|
93
|
-
return sorted.map((leaf, i) => {
|
|
94
|
-
const num = String(i + 1).padStart(3);
|
|
95
|
-
const cmd = leaf.path.padEnd(maxPath);
|
|
96
|
-
return {
|
|
97
|
-
label: `${num}. ${cmd}`,
|
|
98
|
-
value: leaf.path,
|
|
99
|
-
hint: `${leaf.description} [${leaf.category}]`,
|
|
100
|
-
};
|
|
101
|
-
});
|
|
82
|
+
function truncateDesc(desc, max) {
|
|
83
|
+
const short = desc.split(/[.;—]/).shift()?.trim() ?? desc;
|
|
84
|
+
return short.length <= max ? short : short.slice(0, max - 1) + '…';
|
|
102
85
|
}
|
|
103
86
|
// ── TTY guard ───────────────────────────────────────────────────────
|
|
104
87
|
export function shouldShowPicker() {
|
|
@@ -111,52 +94,78 @@ export function shouldShowPicker() {
|
|
|
111
94
|
// ── Top-level picker ────────────────────────────────────────────────
|
|
112
95
|
export async function showCommandPicker(program, authInfo) {
|
|
113
96
|
const leaves = collectLeaves(program.commands, '', '');
|
|
114
|
-
const options = buildOptions(leaves);
|
|
115
97
|
const version = program.version() ?? '';
|
|
116
|
-
// Strip control chars / ANSI sequences from untrusted strings
|
|
117
98
|
const safe = (s) => s.replace(/[\x00-\x1F\x7F]/g, '').replace(/\x1B\[[0-9;]*[A-Za-z]/g, '');
|
|
118
|
-
//
|
|
119
|
-
let
|
|
99
|
+
// Build header
|
|
100
|
+
let headerText = `Clio v${version}`;
|
|
120
101
|
if (authInfo) {
|
|
121
|
-
|
|
122
|
-
if (authInfo.otherOrgCount > 0)
|
|
123
|
-
|
|
124
|
-
|
|
102
|
+
headerText += ` ${muted('·')} ${safe(authInfo.label)} ${muted('·')} ${safe(authInfo.orgName)} (${safe(authInfo.currency)})`;
|
|
103
|
+
if (authInfo.otherOrgCount > 0)
|
|
104
|
+
headerText += ` ${muted(`+${authInfo.otherOrgCount} more`)}`;
|
|
105
|
+
}
|
|
106
|
+
// ── Level 1: Categories ──
|
|
107
|
+
while (true) {
|
|
108
|
+
const categoryItems = CATEGORY_ORDER.map(cat => {
|
|
109
|
+
const count = leaves.filter(l => l.category === cat).length;
|
|
110
|
+
return {
|
|
111
|
+
label: cat,
|
|
112
|
+
value: cat,
|
|
113
|
+
hint: `${String(count).padStart(3)} commands ${CATEGORY_HINTS[cat] ?? ''}`,
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
const category = await showPicker({
|
|
117
|
+
header: headerText,
|
|
118
|
+
items: categoryItems,
|
|
119
|
+
});
|
|
120
|
+
if (isCancel(category))
|
|
121
|
+
process.exit(0);
|
|
122
|
+
// ── Level 2: Commands in category ──
|
|
123
|
+
const catLeaves = leaves
|
|
124
|
+
.filter(l => l.category === category)
|
|
125
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
126
|
+
const descBudget = Math.max(20, (process.stdout.columns || 80) - 44);
|
|
127
|
+
const commandItems = [
|
|
128
|
+
{ label: '← Back', value: '__back__', hint: 'Return to categories' },
|
|
129
|
+
...catLeaves.map(leaf => ({
|
|
130
|
+
label: leaf.path,
|
|
131
|
+
value: leaf.path,
|
|
132
|
+
hint: truncateDesc(leaf.description, descBudget),
|
|
133
|
+
})),
|
|
134
|
+
];
|
|
135
|
+
const selected = await showPicker({
|
|
136
|
+
header: `${accent(String(category))} ${muted('·')} ${catLeaves.length} commands`,
|
|
137
|
+
items: commandItems,
|
|
138
|
+
filterable: true,
|
|
139
|
+
});
|
|
140
|
+
if (isCancel(selected))
|
|
141
|
+
process.exit(0);
|
|
142
|
+
if (selected === '__back__')
|
|
143
|
+
continue;
|
|
144
|
+
return selected;
|
|
125
145
|
}
|
|
126
|
-
header += ` — ${options.length} commands`;
|
|
127
|
-
process.stderr.write(muted(header + '\n'));
|
|
128
|
-
process.stderr.write(muted(' Type to search, ↑↓ navigate, Enter select, Esc quit\n\n'));
|
|
129
|
-
const result = await p.autocomplete({
|
|
130
|
-
message: 'clio',
|
|
131
|
-
options,
|
|
132
|
-
});
|
|
133
|
-
if (p.isCancel(result))
|
|
134
|
-
process.exit(0);
|
|
135
|
-
return result ?? null;
|
|
136
146
|
}
|
|
137
147
|
// ── Subcommand picker ───────────────────────────────────────────────
|
|
138
148
|
async function showSubcommandPicker(parent, parentPath) {
|
|
139
149
|
const topLevel = parentPath.split(' ')[0];
|
|
140
150
|
const category = CATEGORIES[topLevel] ?? 'Other';
|
|
141
|
-
const leaves = collectLeaves(parent.commands, parentPath, category)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
151
|
+
const leaves = collectLeaves(parent.commands, parentPath, category)
|
|
152
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
153
|
+
const descBudget = Math.max(20, (process.stdout.columns || 80) - 44);
|
|
154
|
+
const items = leaves.map(leaf => ({
|
|
155
|
+
label: leaf.path,
|
|
156
|
+
value: leaf.path,
|
|
157
|
+
hint: truncateDesc(leaf.description, descBudget),
|
|
158
|
+
}));
|
|
159
|
+
const result = await showPicker({
|
|
160
|
+
header: `clio ${parentPath}`,
|
|
161
|
+
items,
|
|
162
|
+
filterable: items.length > 10,
|
|
147
163
|
});
|
|
148
|
-
if (
|
|
164
|
+
if (isCancel(result))
|
|
149
165
|
process.exit(0);
|
|
150
166
|
return result ?? null;
|
|
151
167
|
}
|
|
152
|
-
// ── Attach subcommand pickers
|
|
153
|
-
/**
|
|
154
|
-
* Recursively attach interactive pickers to group commands that have
|
|
155
|
-
* subcommands but no default action of their own.
|
|
156
|
-
*
|
|
157
|
-
* After this, bare `clio invoices` or `clio invoices draft` show a picker
|
|
158
|
-
* instead of the Commander help dump.
|
|
159
|
-
*/
|
|
168
|
+
// ── Attach subcommand pickers ───────────────────────────────────────
|
|
160
169
|
export function attachSubcommandPickers(program) {
|
|
161
170
|
function recurse(parent, parentPath) {
|
|
162
171
|
for (const cmd of parent.commands) {
|
|
@@ -176,39 +185,29 @@ export function attachSubcommandPickers(program) {
|
|
|
176
185
|
});
|
|
177
186
|
}
|
|
178
187
|
else if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
179
|
-
// Parent has both own action AND subcommands (e.g. jobs payment-run).
|
|
180
|
-
// Wrap action to show disambiguation when invoked bare from picker.
|
|
181
188
|
const originalAction = cmd._actionHandler;
|
|
182
189
|
let inDisambiguation = false;
|
|
183
190
|
cmd._actionHandler = async (...args) => {
|
|
184
|
-
// Re-entrancy guard: if we're already in disambiguation (e.g. parseAsync
|
|
185
|
-
// re-invoked this handler), run the original action directly.
|
|
186
191
|
if (inDisambiguation)
|
|
187
192
|
return originalAction.call(cmd, ...args);
|
|
188
193
|
inDisambiguation = true;
|
|
189
194
|
try {
|
|
190
|
-
const
|
|
191
|
-
{
|
|
192
|
-
label: `${name} (default blueprint)`,
|
|
193
|
-
value: '__default__',
|
|
194
|
-
hint: cmd.description(),
|
|
195
|
-
},
|
|
195
|
+
const items = [
|
|
196
|
+
{ label: `${name} (default)`, value: '__default__', hint: truncateDesc(cmd.description(), 50) },
|
|
196
197
|
...subs.map(sub => ({
|
|
197
198
|
label: `${name} ${sub.name()}`,
|
|
198
199
|
value: sub.name(),
|
|
199
|
-
hint: sub.description(),
|
|
200
|
+
hint: truncateDesc(sub.description(), 50),
|
|
200
201
|
})),
|
|
201
202
|
];
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
options: disambigOptions,
|
|
203
|
+
const action = await showPicker({
|
|
204
|
+
header: `clio ${path}`,
|
|
205
|
+
items,
|
|
206
206
|
});
|
|
207
|
-
if (
|
|
207
|
+
if (isCancel(action))
|
|
208
208
|
process.exit(0);
|
|
209
|
-
if (action === '__default__')
|
|
209
|
+
if (action === '__default__')
|
|
210
210
|
return originalAction.call(cmd, ...args);
|
|
211
|
-
}
|
|
212
211
|
await program.parseAsync(['node', 'clio', ...path.split(' '), action]);
|
|
213
212
|
}
|
|
214
213
|
finally {
|
|
@@ -216,7 +215,6 @@ export function attachSubcommandPickers(program) {
|
|
|
216
215
|
}
|
|
217
216
|
};
|
|
218
217
|
}
|
|
219
|
-
// Recurse deeper (invoices > draft, capsules > types, etc.)
|
|
220
218
|
recurse(cmd, path);
|
|
221
219
|
}
|
|
222
220
|
}
|
|
@@ -5,3 +5,4 @@ export { formatApiError, formatAuthError, formatGenericError, formatWarning } fr
|
|
|
5
5
|
export { renderOrgBanner } from './banner.js';
|
|
6
6
|
export { createProgress } from './progress.js';
|
|
7
7
|
export { formatRecord } from './record.js';
|
|
8
|
+
export { showPicker, isCancel } from './picker.js';
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom-built interactive picker — branded for Clio.
|
|
3
|
+
*
|
|
4
|
+
* Built with raw readline for full visual control.
|
|
5
|
+
* Features: type-to-filter, arrow navigation, scrollable viewport,
|
|
6
|
+
* rounded borders, brand colors, compact single-line items.
|
|
7
|
+
*/
|
|
8
|
+
import readline from 'node:readline';
|
|
9
|
+
import { accent, muted, highlight, subtle, box, sym, INDENT } from './theme.js';
|
|
10
|
+
const CANCEL = Symbol('cancel');
|
|
11
|
+
export function isCancel(value) {
|
|
12
|
+
return value === CANCEL;
|
|
13
|
+
}
|
|
14
|
+
// ── Helpers ──────────────────────────────────────────────────────
|
|
15
|
+
function stripAnsi(str) {
|
|
16
|
+
// eslint-disable-next-line no-control-regex
|
|
17
|
+
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
18
|
+
}
|
|
19
|
+
function fuzzyMatch(query, target) {
|
|
20
|
+
const q = query.toLowerCase();
|
|
21
|
+
const t = target.toLowerCase();
|
|
22
|
+
let qi = 0;
|
|
23
|
+
for (let ti = 0; ti < t.length && qi < q.length; ti++) {
|
|
24
|
+
if (t[ti] === q[qi])
|
|
25
|
+
qi++;
|
|
26
|
+
}
|
|
27
|
+
return qi === q.length;
|
|
28
|
+
}
|
|
29
|
+
function truncate(str, max) {
|
|
30
|
+
const plain = stripAnsi(str);
|
|
31
|
+
if (plain.length <= max)
|
|
32
|
+
return str;
|
|
33
|
+
return str.slice(0, max - 1) + '…';
|
|
34
|
+
}
|
|
35
|
+
// ── Render Frame ─────────────────────────────────────────────────
|
|
36
|
+
function renderFrame(header, items, cursor, filter, filterable, maxVisible) {
|
|
37
|
+
const termWidth = process.stdout.columns || 80;
|
|
38
|
+
const contentWidth = Math.min(termWidth - 4, 72);
|
|
39
|
+
const lines = [];
|
|
40
|
+
// Header box
|
|
41
|
+
const hr = box.horizontal.repeat(contentWidth);
|
|
42
|
+
lines.push(muted(`${INDENT}${box.topLeft}${hr}${box.topRight}`));
|
|
43
|
+
const headerPlain = stripAnsi(header);
|
|
44
|
+
const headerPad = Math.max(0, contentWidth - headerPlain.length - 1);
|
|
45
|
+
lines.push(muted(`${INDENT}${box.vertical}`) + ` ${header}${' '.repeat(headerPad)}` + muted(box.vertical));
|
|
46
|
+
lines.push(muted(`${INDENT}${box.bottomLeft}${hr}${box.bottomRight}`));
|
|
47
|
+
// Filter input
|
|
48
|
+
if (filterable) {
|
|
49
|
+
const display = filter ? highlight(filter) : subtle('type to filter...');
|
|
50
|
+
lines.push(`${INDENT} ${muted(sym.pointerSmall)} ${display}`);
|
|
51
|
+
lines.push('');
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
lines.push('');
|
|
55
|
+
}
|
|
56
|
+
// Items viewport
|
|
57
|
+
if (items.length === 0) {
|
|
58
|
+
lines.push(`${INDENT} ${muted('No matching commands')}`);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
const total = items.length;
|
|
62
|
+
const viewSize = Math.min(maxVisible, total);
|
|
63
|
+
let start = 0;
|
|
64
|
+
if (total > viewSize) {
|
|
65
|
+
start = Math.max(0, cursor - Math.floor(viewSize / 2));
|
|
66
|
+
start = Math.min(start, total - viewSize);
|
|
67
|
+
}
|
|
68
|
+
const end = start + viewSize;
|
|
69
|
+
const visibleItems = items.slice(start, end);
|
|
70
|
+
const maxLabel = Math.min(Math.max(...visibleItems.map(i => stripAnsi(i.label).length), 8), 36);
|
|
71
|
+
const hintBudget = Math.max(10, contentWidth - maxLabel - 8);
|
|
72
|
+
if (start > 0)
|
|
73
|
+
lines.push(`${INDENT} ${muted('↑')}`);
|
|
74
|
+
for (let i = start; i < end; i++) {
|
|
75
|
+
const item = items[i];
|
|
76
|
+
const isSelected = i === cursor;
|
|
77
|
+
const label = stripAnsi(item.label).padEnd(maxLabel);
|
|
78
|
+
const hint = item.hint ? truncate(item.hint, hintBudget) : '';
|
|
79
|
+
if (isSelected) {
|
|
80
|
+
lines.push(`${INDENT}${accent(sym.pointer)} ${highlight(label)} ${muted(hint)}`);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
lines.push(`${INDENT} ${label} ${subtle(hint)}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (end < total)
|
|
87
|
+
lines.push(`${INDENT} ${muted('↓')} ${subtle(`+${total - end} more`)}`);
|
|
88
|
+
}
|
|
89
|
+
// Footer
|
|
90
|
+
lines.push('');
|
|
91
|
+
lines.push(`${INDENT}${muted('↑↓')} ${subtle('navigate')} ${muted('·')} ${muted('enter')} ${subtle('select')} ${muted('·')} ${muted('esc')} ${subtle('quit')}`);
|
|
92
|
+
return lines.join('\n');
|
|
93
|
+
}
|
|
94
|
+
// ── Interactive Picker ───────────────────────────────────────────
|
|
95
|
+
export function showPicker(opts) {
|
|
96
|
+
return new Promise((resolve) => {
|
|
97
|
+
const { header, items, filterable = false } = opts;
|
|
98
|
+
const termRows = process.stdout.rows || 24;
|
|
99
|
+
const maxVisible = opts.maxVisible ?? Math.max(8, termRows - 10);
|
|
100
|
+
let filter = '';
|
|
101
|
+
let cursor = 0;
|
|
102
|
+
let prevLineCount = 0;
|
|
103
|
+
const getFiltered = () => {
|
|
104
|
+
if (!filter)
|
|
105
|
+
return items;
|
|
106
|
+
return items.filter(i => fuzzyMatch(filter, i.label) || (i.hint ? fuzzyMatch(filter, i.hint) : false));
|
|
107
|
+
};
|
|
108
|
+
const draw = () => {
|
|
109
|
+
const filtered = getFiltered();
|
|
110
|
+
if (cursor >= filtered.length)
|
|
111
|
+
cursor = Math.max(0, filtered.length - 1);
|
|
112
|
+
const frame = renderFrame(header, filtered, cursor, filter, filterable, maxVisible);
|
|
113
|
+
const frameLines = frame.split('\n').length;
|
|
114
|
+
// Clear previous frame
|
|
115
|
+
if (prevLineCount > 0) {
|
|
116
|
+
process.stdout.write(`\x1b[${prevLineCount}A`); // Move up
|
|
117
|
+
process.stdout.write('\x1b[J'); // Clear to end of screen
|
|
118
|
+
}
|
|
119
|
+
process.stdout.write(frame + '\n');
|
|
120
|
+
prevLineCount = frameLines;
|
|
121
|
+
};
|
|
122
|
+
// Setup readline in raw mode
|
|
123
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
124
|
+
if (process.stdin.isTTY)
|
|
125
|
+
process.stdin.setRawMode(true);
|
|
126
|
+
readline.emitKeypressEvents(process.stdin, rl);
|
|
127
|
+
// Hide cursor
|
|
128
|
+
process.stdout.write('\x1b[?25l');
|
|
129
|
+
const cleanup = () => {
|
|
130
|
+
process.stdout.write('\x1b[?25h'); // Show cursor
|
|
131
|
+
if (process.stdin.isTTY)
|
|
132
|
+
process.stdin.setRawMode(false);
|
|
133
|
+
process.stdin.removeListener('keypress', onKey);
|
|
134
|
+
rl.close();
|
|
135
|
+
};
|
|
136
|
+
const onKey = (_char, key) => {
|
|
137
|
+
const filtered = getFiltered();
|
|
138
|
+
if (key?.name === 'up') {
|
|
139
|
+
cursor = cursor > 0 ? cursor - 1 : filtered.length - 1;
|
|
140
|
+
draw();
|
|
141
|
+
}
|
|
142
|
+
else if (key?.name === 'down') {
|
|
143
|
+
cursor = cursor < filtered.length - 1 ? cursor + 1 : 0;
|
|
144
|
+
draw();
|
|
145
|
+
}
|
|
146
|
+
else if (key?.name === 'return') {
|
|
147
|
+
if (filtered.length > 0) {
|
|
148
|
+
cleanup();
|
|
149
|
+
// Clear the picker output
|
|
150
|
+
if (prevLineCount > 0) {
|
|
151
|
+
process.stdout.write(`\x1b[${prevLineCount}A\x1b[J`);
|
|
152
|
+
}
|
|
153
|
+
resolve(filtered[cursor].value);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else if (key?.name === 'escape' || (key?.ctrl && key?.name === 'c')) {
|
|
157
|
+
cleanup();
|
|
158
|
+
if (prevLineCount > 0) {
|
|
159
|
+
process.stdout.write(`\x1b[${prevLineCount}A\x1b[J`);
|
|
160
|
+
}
|
|
161
|
+
resolve(CANCEL);
|
|
162
|
+
}
|
|
163
|
+
else if (key?.name === 'backspace') {
|
|
164
|
+
if (filterable && filter.length > 0) {
|
|
165
|
+
filter = filter.slice(0, -1);
|
|
166
|
+
cursor = 0;
|
|
167
|
+
draw();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
else if (filterable && key?.sequence && key.sequence.length === 1 && !key.ctrl) {
|
|
171
|
+
filter += key.sequence;
|
|
172
|
+
cursor = 0;
|
|
173
|
+
draw();
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
process.stdin.on('keypress', onKey);
|
|
177
|
+
// Initial draw
|
|
178
|
+
draw();
|
|
179
|
+
});
|
|
180
|
+
}
|