@textui/widgets 0.5.0 → 0.6.1
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.
|
@@ -58,20 +58,6 @@ export interface CommandPaletteProps extends BoxProps {
|
|
|
58
58
|
*/
|
|
59
59
|
openAt?: string;
|
|
60
60
|
}
|
|
61
|
-
/**
|
|
62
|
-
* The command palette.
|
|
63
|
-
*
|
|
64
|
-
* It searches the command registry rather than a list someone maintained, so
|
|
65
|
-
* a command registered anywhere is reachable here the moment it exists - which
|
|
66
|
-
* is the payoff for insisting actions are commands. It also *runs* what it
|
|
67
|
-
* finds, so choosing "Open dialog" here and pressing the button that opens a
|
|
68
|
-
* dialog are the same act, reaching the same code.
|
|
69
|
-
*
|
|
70
|
-
* A command that declares an argument with `choices` gets a second level:
|
|
71
|
-
* choosing it lists the choices, and picking one runs the command with it.
|
|
72
|
-
* That is where sub-items come from - the command says what it needs and the
|
|
73
|
-
* palette asks, rather than every caller inventing its own submenu.
|
|
74
|
-
*/
|
|
75
61
|
export declare const CommandPalette: (props: CommandPaletteProps) => import("@textui/core").RenderOutput;
|
|
76
62
|
/**
|
|
77
63
|
* The argument the palette should ask about, if there is one.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"command-palette.d.ts","sourceRoot":"","sources":["../../src/overlay/command-palette.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAa,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAa,MAAM,cAAc,CAAC;AAiB/F,MAAM,WAAW,mBAAoB,SAAQ,QAAQ;IACnD;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,iBAAiB,EAAE,GAAG,CAAC,MAAM,iBAAiB,EAAE,CAAC,CAAC;IAC7D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iEAAiE;IACjE,KAAK,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACzD,OAAO,CAAC,IAAI,IAAI,CAAC;IACjB,uEAAuE;IACvE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;IAClC;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;
|
|
1
|
+
{"version":3,"file":"command-palette.d.ts","sourceRoot":"","sources":["../../src/overlay/command-palette.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAa,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAa,MAAM,cAAc,CAAC;AAiB/F,MAAM,WAAW,mBAAoB,SAAQ,QAAQ;IACnD;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,iBAAiB,EAAE,GAAG,CAAC,MAAM,iBAAiB,EAAE,CAAC,CAAC;IAC7D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iEAAiE;IACjE,KAAK,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACzD,OAAO,CAAC,IAAI,IAAI,CAAC;IACjB,uEAAuE;IACvE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;IAClC;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAyBD,eAAO,MAAM,cAAc,qEAqczB,CAAC;AAEH;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,UAAU,CACxB,OAAO,EAAE,iBAAiB,EAC1B,SAAS,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACtC,OAAO,GAAG,SAAS,CAKrB;AAyCD,wBAAgB,cAAc,CAAC,QAAQ,EAAE,iBAAiB,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAehG"}
|
|
@@ -2,20 +2,6 @@ import { defineComponent, h, stringWidth, useEffect, useFocusScope, useInput, us
|
|
|
2
2
|
import { TextInput } from '../control/index.js';
|
|
3
3
|
import { Menu } from '../navigation/index.js';
|
|
4
4
|
import { hint } from './shared.js';
|
|
5
|
-
/**
|
|
6
|
-
* The command palette.
|
|
7
|
-
*
|
|
8
|
-
* It searches the command registry rather than a list someone maintained, so
|
|
9
|
-
* a command registered anywhere is reachable here the moment it exists - which
|
|
10
|
-
* is the payoff for insisting actions are commands. It also *runs* what it
|
|
11
|
-
* finds, so choosing "Open dialog" here and pressing the button that opens a
|
|
12
|
-
* dialog are the same act, reaching the same code.
|
|
13
|
-
*
|
|
14
|
-
* A command that declares an argument with `choices` gets a second level:
|
|
15
|
-
* choosing it lists the choices, and picking one runs the command with it.
|
|
16
|
-
* That is where sub-items come from - the command says what it needs and the
|
|
17
|
-
* palette asks, rather than every caller inventing its own submenu.
|
|
18
|
-
*/
|
|
19
5
|
export const CommandPalette = defineComponent('CommandPalette', (props) => {
|
|
20
6
|
const theme = useTheme();
|
|
21
7
|
const runtime = useRuntime();
|
|
@@ -99,10 +85,29 @@ export const CommandPalette = defineComponent('CommandPalette', (props) => {
|
|
|
99
85
|
? { sectionBefore: command.category }
|
|
100
86
|
: {}),
|
|
101
87
|
}));
|
|
88
|
+
/** The argument answered last, when this question came after one. */
|
|
89
|
+
const previousOf = (asked) => {
|
|
90
|
+
const last = Object.keys(asked.collected).at(-1);
|
|
91
|
+
return last === undefined ? undefined : (asked.command.args ?? []).find((arg) => arg.name === last);
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* One question back.
|
|
95
|
+
*
|
|
96
|
+
* After a first question the second is asked again from the start with
|
|
97
|
+
* the earlier answer withdrawn: a wrong provider is corrected by choosing
|
|
98
|
+
* another, not by closing the whole picker and opening it again. At the
|
|
99
|
+
* first question, the command list.
|
|
100
|
+
*/
|
|
102
101
|
const back = () => {
|
|
103
102
|
// `null` is "never mind": whatever the preview did gets undone by whoever
|
|
104
103
|
// did it, because only the command knows what it changed.
|
|
105
104
|
pending?.arg.preview?.(null);
|
|
105
|
+
const previous = pending ? previousOf(pending) : undefined;
|
|
106
|
+
if (pending && previous) {
|
|
107
|
+
const { [previous.name]: _withdrawn, ...rest } = pending.collected;
|
|
108
|
+
drillInto(pending.command, previous, rest);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
106
111
|
setPending(null);
|
|
107
112
|
setChoices([]);
|
|
108
113
|
setQuery('');
|
|
@@ -144,9 +149,16 @@ export const CommandPalette = defineComponent('CommandPalette', (props) => {
|
|
|
144
149
|
setRefresh((n) => n + 1);
|
|
145
150
|
onRun?.(id, args);
|
|
146
151
|
};
|
|
147
|
-
/**
|
|
152
|
+
/**
|
|
153
|
+
* Ask about one command. `choices` may be a function, and may be async.
|
|
154
|
+
*
|
|
155
|
+
* A resolver is handed what was answered before this argument - and an
|
|
156
|
+
* argument that was not asked because its `default` stood was answered by
|
|
157
|
+
* that default. Left out, "the models of the chosen provider" would see no
|
|
158
|
+
* provider exactly when there was only one to choose from.
|
|
159
|
+
*/
|
|
148
160
|
const drillInto = (command, arg, collected = {}) => {
|
|
149
|
-
const resolved = typeof arg.choices === 'function' ? arg.choices() : arg.choices ?? [];
|
|
161
|
+
const resolved = typeof arg.choices === 'function' ? arg.choices(answeredBefore(command, arg, collected)) : arg.choices ?? [];
|
|
150
162
|
setPending({ command, arg, collected });
|
|
151
163
|
setQuery('');
|
|
152
164
|
/*
|
|
@@ -253,8 +265,10 @@ export const CommandPalette = defineComponent('CommandPalette', (props) => {
|
|
|
253
265
|
// anybody chose to be at: the caller drilled in on their behalf, and
|
|
254
266
|
// the level underneath is a list of one. Backing out to it reads as
|
|
255
267
|
// "escape did nothing", and the second escape - the one that would
|
|
256
|
-
// close it - is spent leaving a screen nobody asked to see.
|
|
257
|
-
|
|
268
|
+
// close it - is spent leaving a screen nobody asked to see. A question
|
|
269
|
+
// that came after one is different: there the level underneath is the
|
|
270
|
+
// earlier question, which somebody did choose to be at.
|
|
271
|
+
if (pending && (!openAt || previousOf(pending) !== undefined)) {
|
|
258
272
|
back();
|
|
259
273
|
return true;
|
|
260
274
|
}
|
|
@@ -267,7 +281,7 @@ export const CommandPalette = defineComponent('CommandPalette', (props) => {
|
|
|
267
281
|
onClose?.();
|
|
268
282
|
return true;
|
|
269
283
|
}
|
|
270
|
-
if (event.name === 'left' && pending && query === '' && !openAt) {
|
|
284
|
+
if (event.name === 'left' && pending && query === '' && (!openAt || previousOf(pending) !== undefined)) {
|
|
271
285
|
back();
|
|
272
286
|
return true;
|
|
273
287
|
}
|
|
@@ -297,6 +311,8 @@ export const CommandPalette = defineComponent('CommandPalette', (props) => {
|
|
|
297
311
|
// there - and a sentence about what a mode does is exactly the thing a
|
|
298
312
|
// person needs whole. So the line under the list follows the highlight: the
|
|
299
313
|
// choice's description while there is one, the question's otherwise.
|
|
314
|
+
// It is the one line in the panel that wraps rather than truncates,
|
|
315
|
+
// because it is the one place a sentence can be read whole.
|
|
300
316
|
const chosen = pending ? offered[index] : undefined;
|
|
301
317
|
const detail = pending
|
|
302
318
|
? chosen?.description
|
|
@@ -315,7 +331,14 @@ export const CommandPalette = defineComponent('CommandPalette', (props) => {
|
|
|
315
331
|
* the things that decide it: a question with two short answers still needs
|
|
316
332
|
* somewhere to type and a line saying what the keys do.
|
|
317
333
|
*/
|
|
318
|
-
const content = Math.max(...items.map((item) => rowWidth(item, descriptions)), ...(pending ? [stringWidth(pending.command.title) + 12] : [stringWidth(placeholder ?? '') + 4])
|
|
334
|
+
const content = Math.max(...items.map((item) => rowWidth(item, descriptions)), ...(pending ? [stringWidth(pending.command.title) + 12] : [stringWidth(placeholder ?? '') + 4]),
|
|
335
|
+
// The sentences the line under the list may show, so a question with
|
|
336
|
+
// three one-word answers is not a panel 28 wide wrapping a sentence
|
|
337
|
+
// three times under them. Capped like everything else by `maxWidth`.
|
|
338
|
+
...(pending
|
|
339
|
+
? [pending.arg.description ?? '', ...offered.map((choice) => choice.description ?? '')]
|
|
340
|
+
.map((sentence) => (sentence === '' ? 0 : stringWidth(sentence) + 6))
|
|
341
|
+
: []));
|
|
319
342
|
return h('box', {
|
|
320
343
|
role: 'dialog',
|
|
321
344
|
label: 'Commands',
|
|
@@ -361,10 +384,19 @@ export const CommandPalette = defineComponent('CommandPalette', (props) => {
|
|
|
361
384
|
if (command && argumentOf(command))
|
|
362
385
|
choose();
|
|
363
386
|
},
|
|
387
|
+
// A typed answer's field is the answer, so its placeholder is the
|
|
388
|
+
// question. A list's field is a filter, and the question goes where
|
|
389
|
+
// there is room to say it once: here, while the line under the list
|
|
390
|
+
// is busy saying what each answer means; under the list, whole and
|
|
391
|
+
// wrapped, when the answers have nothing to say for themselves and
|
|
392
|
+
// that line would otherwise repeat this one, cut to whatever width
|
|
393
|
+
// three short answers gave the panel.
|
|
364
394
|
placeholder: pending
|
|
365
|
-
? (pending.arg.
|
|
366
|
-
? `${pending.command.title}${theme.glyphs.ellipsis}`
|
|
367
|
-
:
|
|
395
|
+
? (pending.arg.choices === undefined
|
|
396
|
+
? pending.arg.description ?? `${pending.command.title}${theme.glyphs.ellipsis}`
|
|
397
|
+
: offered.some((choice) => choice.description) && pending.arg.description
|
|
398
|
+
? pending.arg.description
|
|
399
|
+
: `Choose ${pending.command.title.toLowerCase()}${theme.glyphs.ellipsis}`)
|
|
368
400
|
: (placeholder ?? `Type a command${theme.glyphs.ellipsis}`),
|
|
369
401
|
search: true,
|
|
370
402
|
autoFocus: true,
|
|
@@ -386,7 +418,7 @@ export const CommandPalette = defineComponent('CommandPalette', (props) => {
|
|
|
386
418
|
}), h('box', { height: 1, fill: theme.borderChars().top, fg: 'borderSubtle' }),
|
|
387
419
|
// What the highlighted row actually is, and how to move around. A palette
|
|
388
420
|
// that shows only titles makes you run something to find out what it does.
|
|
389
|
-
h('box', { direction: 'row', gap: 1 }, h('text', { content: detail, fg: 'muted', flex: 1,
|
|
421
|
+
h('box', { direction: 'row', gap: 1, align: 'start' }, h('text', { content: detail, fg: 'muted', flex: 1, wrap: 'word' }), h('text', { content: `${rows.length}`, fg: 'subtle' })), h('box', { direction: 'row', gap: 1 }, h('text', {
|
|
390
422
|
content: pending
|
|
391
423
|
? (pending.arg.choices === undefined
|
|
392
424
|
? hint(theme, ['type it', 'enter confirm', 'esc back'])
|
|
@@ -418,6 +450,23 @@ export function argumentOf(command, collected = {}) {
|
|
|
418
450
|
return (command.args ?? []).find((arg) => collected[arg.name] === undefined &&
|
|
419
451
|
(arg.choices !== undefined || (arg.required === true && arg.default === undefined)));
|
|
420
452
|
}
|
|
453
|
+
/**
|
|
454
|
+
* What a `choices` resolver is told: the answers given, under the defaults
|
|
455
|
+
* of the arguments before this one that stood in for a question.
|
|
456
|
+
*
|
|
457
|
+
* Only the ones ahead of it in the declaration. An argument after this one
|
|
458
|
+
* has not been reached, and its default is not yet an answer to anything.
|
|
459
|
+
*/
|
|
460
|
+
function answeredBefore(command, arg, collected) {
|
|
461
|
+
const answered = {};
|
|
462
|
+
for (const spec of command.args ?? []) {
|
|
463
|
+
if (spec === arg)
|
|
464
|
+
break;
|
|
465
|
+
if (spec.default !== undefined)
|
|
466
|
+
answered[spec.name] = spec.default;
|
|
467
|
+
}
|
|
468
|
+
return { ...answered, ...collected };
|
|
469
|
+
}
|
|
421
470
|
/** The short form and the long one, as one shape. */
|
|
422
471
|
function asChoice(choice) {
|
|
423
472
|
return typeof choice === 'string' ? { value: choice } : choice;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@textui/widgets",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "The TextUI component catalog - layout, display, controls, data, overlays and charts",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"terminal",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"test": "vitest run"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@textui/core": "^0.
|
|
49
|
+
"@textui/core": "^0.6.1"
|
|
50
50
|
},
|
|
51
51
|
"publishConfig": {
|
|
52
52
|
"access": "public"
|
|
@@ -89,6 +89,15 @@ export interface CommandPaletteProps extends BoxProps {
|
|
|
89
89
|
* That is where sub-items come from - the command says what it needs and the
|
|
90
90
|
* palette asks, rather than every caller inventing its own submenu.
|
|
91
91
|
*/
|
|
92
|
+
|
|
93
|
+
/** The question being asked: the command, the argument, and the answers given before it. */
|
|
94
|
+
interface Pending {
|
|
95
|
+
command: CommandDefinition;
|
|
96
|
+
arg: ArgSpec;
|
|
97
|
+
/** Answers given so far, for a command that asks for more than one. */
|
|
98
|
+
collected: Record<string, unknown>;
|
|
99
|
+
}
|
|
100
|
+
|
|
92
101
|
export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalette', (props) => {
|
|
93
102
|
const theme = useTheme();
|
|
94
103
|
const runtime = useRuntime();
|
|
@@ -101,12 +110,7 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
|
|
|
101
110
|
const [query, setQuery] = useState('');
|
|
102
111
|
const [highlight, setHighlight] = useState(0);
|
|
103
112
|
/** The command being asked about, when the palette has drilled in. */
|
|
104
|
-
const [pending, setPending] = useState<
|
|
105
|
-
command: CommandDefinition;
|
|
106
|
-
arg: ArgSpec;
|
|
107
|
-
/** Answers given so far, for a command that asks for more than one. */
|
|
108
|
-
collected: Record<string, unknown>;
|
|
109
|
-
} | null>(null);
|
|
113
|
+
const [pending, setPending] = useState<Pending | null>(null);
|
|
110
114
|
const [choices, setChoices] = useState<ArgChoice[]>([]);
|
|
111
115
|
/**
|
|
112
116
|
* Whether the answer to "what may I choose" is still on its way.
|
|
@@ -186,10 +190,30 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
|
|
|
186
190
|
: {}),
|
|
187
191
|
}));
|
|
188
192
|
|
|
193
|
+
/** The argument answered last, when this question came after one. */
|
|
194
|
+
const previousOf = (asked: Pending): ArgSpec | undefined => {
|
|
195
|
+
const last = Object.keys(asked.collected).at(-1);
|
|
196
|
+
return last === undefined ? undefined : (asked.command.args ?? []).find((arg) => arg.name === last);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* One question back.
|
|
201
|
+
*
|
|
202
|
+
* After a first question the second is asked again from the start with
|
|
203
|
+
* the earlier answer withdrawn: a wrong provider is corrected by choosing
|
|
204
|
+
* another, not by closing the whole picker and opening it again. At the
|
|
205
|
+
* first question, the command list.
|
|
206
|
+
*/
|
|
189
207
|
const back = (): void => {
|
|
190
208
|
// `null` is "never mind": whatever the preview did gets undone by whoever
|
|
191
209
|
// did it, because only the command knows what it changed.
|
|
192
210
|
pending?.arg.preview?.(null);
|
|
211
|
+
const previous = pending ? previousOf(pending) : undefined;
|
|
212
|
+
if (pending && previous) {
|
|
213
|
+
const { [previous.name]: _withdrawn, ...rest } = pending.collected;
|
|
214
|
+
drillInto(pending.command, previous, rest);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
193
217
|
setPending(null);
|
|
194
218
|
setChoices([]);
|
|
195
219
|
setQuery('');
|
|
@@ -233,13 +257,20 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
|
|
|
233
257
|
onRun?.(id, args);
|
|
234
258
|
};
|
|
235
259
|
|
|
236
|
-
/**
|
|
260
|
+
/**
|
|
261
|
+
* Ask about one command. `choices` may be a function, and may be async.
|
|
262
|
+
*
|
|
263
|
+
* A resolver is handed what was answered before this argument - and an
|
|
264
|
+
* argument that was not asked because its `default` stood was answered by
|
|
265
|
+
* that default. Left out, "the models of the chosen provider" would see no
|
|
266
|
+
* provider exactly when there was only one to choose from.
|
|
267
|
+
*/
|
|
237
268
|
const drillInto = (
|
|
238
269
|
command: CommandDefinition,
|
|
239
270
|
arg: ArgSpec,
|
|
240
271
|
collected: Record<string, unknown> = {},
|
|
241
272
|
): void => {
|
|
242
|
-
const resolved = typeof arg.choices === 'function' ? arg.choices() : arg.choices ?? [];
|
|
273
|
+
const resolved = typeof arg.choices === 'function' ? arg.choices(answeredBefore(command, arg, collected)) : arg.choices ?? [];
|
|
243
274
|
setPending({ command, arg, collected });
|
|
244
275
|
setQuery('');
|
|
245
276
|
|
|
@@ -350,8 +381,10 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
|
|
|
350
381
|
// anybody chose to be at: the caller drilled in on their behalf, and
|
|
351
382
|
// the level underneath is a list of one. Backing out to it reads as
|
|
352
383
|
// "escape did nothing", and the second escape - the one that would
|
|
353
|
-
// close it - is spent leaving a screen nobody asked to see.
|
|
354
|
-
|
|
384
|
+
// close it - is spent leaving a screen nobody asked to see. A question
|
|
385
|
+
// that came after one is different: there the level underneath is the
|
|
386
|
+
// earlier question, which somebody did choose to be at.
|
|
387
|
+
if (pending && (!openAt || previousOf(pending) !== undefined)) { back(); return true; }
|
|
355
388
|
// Whatever a highlighted choice previewed has to be put back on the
|
|
356
389
|
// way out, and `null` is how the command is told to undo it - only it
|
|
357
390
|
// knows what it changed. `back()` did that, so closing instead of
|
|
@@ -361,7 +394,7 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
|
|
|
361
394
|
onClose?.();
|
|
362
395
|
return true;
|
|
363
396
|
}
|
|
364
|
-
if (event.name === 'left' && pending && query === '' && !openAt) { back(); return true; }
|
|
397
|
+
if (event.name === 'left' && pending && query === '' && (!openAt || previousOf(pending) !== undefined)) { back(); return true; }
|
|
365
398
|
// Wrapping, both ways. A list you can walk off the end of makes you
|
|
366
399
|
// check where you are before every press; one that comes round means the
|
|
367
400
|
// last item is one key from the first.
|
|
@@ -389,6 +422,8 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
|
|
|
389
422
|
// there - and a sentence about what a mode does is exactly the thing a
|
|
390
423
|
// person needs whole. So the line under the list follows the highlight: the
|
|
391
424
|
// choice's description while there is one, the question's otherwise.
|
|
425
|
+
// It is the one line in the panel that wraps rather than truncates,
|
|
426
|
+
// because it is the one place a sentence can be read whole.
|
|
392
427
|
const chosen = pending ? offered[index] : undefined;
|
|
393
428
|
const detail = pending
|
|
394
429
|
? chosen?.description
|
|
@@ -411,6 +446,13 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
|
|
|
411
446
|
const content = Math.max(
|
|
412
447
|
...items.map((item) => rowWidth(item, descriptions)),
|
|
413
448
|
...(pending ? [stringWidth(pending.command.title) + 12] : [stringWidth(placeholder ?? '') + 4]),
|
|
449
|
+
// The sentences the line under the list may show, so a question with
|
|
450
|
+
// three one-word answers is not a panel 28 wide wrapping a sentence
|
|
451
|
+
// three times under them. Capped like everything else by `maxWidth`.
|
|
452
|
+
...(pending
|
|
453
|
+
? [pending.arg.description ?? '', ...offered.map((choice) => choice.description ?? '')]
|
|
454
|
+
.map((sentence) => (sentence === '' ? 0 : stringWidth(sentence) + 6))
|
|
455
|
+
: []),
|
|
414
456
|
);
|
|
415
457
|
|
|
416
458
|
return h('box', {
|
|
@@ -457,10 +499,19 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
|
|
|
457
499
|
const command = matches[index];
|
|
458
500
|
if (command && argumentOf(command)) choose();
|
|
459
501
|
},
|
|
502
|
+
// A typed answer's field is the answer, so its placeholder is the
|
|
503
|
+
// question. A list's field is a filter, and the question goes where
|
|
504
|
+
// there is room to say it once: here, while the line under the list
|
|
505
|
+
// is busy saying what each answer means; under the list, whole and
|
|
506
|
+
// wrapped, when the answers have nothing to say for themselves and
|
|
507
|
+
// that line would otherwise repeat this one, cut to whatever width
|
|
508
|
+
// three short answers gave the panel.
|
|
460
509
|
placeholder: pending
|
|
461
|
-
? (pending.arg.
|
|
462
|
-
? `${pending.command.title}${theme.glyphs.ellipsis}`
|
|
463
|
-
:
|
|
510
|
+
? (pending.arg.choices === undefined
|
|
511
|
+
? pending.arg.description ?? `${pending.command.title}${theme.glyphs.ellipsis}`
|
|
512
|
+
: offered.some((choice) => choice.description) && pending.arg.description
|
|
513
|
+
? pending.arg.description
|
|
514
|
+
: `Choose ${pending.command.title.toLowerCase()}${theme.glyphs.ellipsis}`)
|
|
464
515
|
: (placeholder ?? `Type a command${theme.glyphs.ellipsis}`),
|
|
465
516
|
search: true,
|
|
466
517
|
autoFocus: true,
|
|
@@ -484,8 +535,8 @@ export const CommandPalette = defineComponent<CommandPaletteProps>('CommandPalet
|
|
|
484
535
|
h('box', { height: 1, fill: theme.borderChars().top, fg: 'borderSubtle' }),
|
|
485
536
|
// What the highlighted row actually is, and how to move around. A palette
|
|
486
537
|
// that shows only titles makes you run something to find out what it does.
|
|
487
|
-
h('box', { direction: 'row', gap: 1 },
|
|
488
|
-
h('text', { content: detail, fg: 'muted', flex: 1,
|
|
538
|
+
h('box', { direction: 'row', gap: 1, align: 'start' },
|
|
539
|
+
h('text', { content: detail, fg: 'muted', flex: 1, wrap: 'word' }),
|
|
489
540
|
h('text', { content: `${rows.length}`, fg: 'subtle' })),
|
|
490
541
|
h('box', { direction: 'row', gap: 1 },
|
|
491
542
|
h('text', {
|
|
@@ -528,6 +579,26 @@ export function argumentOf(
|
|
|
528
579
|
);
|
|
529
580
|
}
|
|
530
581
|
|
|
582
|
+
/**
|
|
583
|
+
* What a `choices` resolver is told: the answers given, under the defaults
|
|
584
|
+
* of the arguments before this one that stood in for a question.
|
|
585
|
+
*
|
|
586
|
+
* Only the ones ahead of it in the declaration. An argument after this one
|
|
587
|
+
* has not been reached, and its default is not yet an answer to anything.
|
|
588
|
+
*/
|
|
589
|
+
function answeredBefore(
|
|
590
|
+
command: CommandDefinition,
|
|
591
|
+
arg: ArgSpec,
|
|
592
|
+
collected: Record<string, unknown>,
|
|
593
|
+
): Record<string, unknown> {
|
|
594
|
+
const answered: Record<string, unknown> = {};
|
|
595
|
+
for (const spec of command.args ?? []) {
|
|
596
|
+
if (spec === arg) break;
|
|
597
|
+
if (spec.default !== undefined) answered[spec.name] = spec.default;
|
|
598
|
+
}
|
|
599
|
+
return { ...answered, ...collected };
|
|
600
|
+
}
|
|
601
|
+
|
|
531
602
|
/** The short form and the long one, as one shape. */
|
|
532
603
|
function asChoice(choice: string | ArgChoice): ArgChoice {
|
|
533
604
|
return typeof choice === 'string' ? { value: choice } : choice;
|