mirror of
https://github.com/Kaelio/ktx.git
synced 2026-07-01 08:59:39 +02:00
feat(cli): skip-context-sources menu + clack-style tree picker UX (#213)
* feat(cli): add 'skip context sources' option to database setup menu After databases are configured, the post-setup menu now offers a 'Skip context sources' choice equivalent to passing --skip-sources, which plumbs through KtxSetupDatabasesResult.skipSources to bypass the context-source step in the same run. * feat(cli): standardize tree picker UX after clack autocomplete-multiselect Search is always on (no '/' to enter): typed printable chars feed the query, Tab toggles selection on the focused node without leaving the search bar, and Space toggles only after arrow-key navigation (isNavigating); otherwise it is appended to the query. Esc clears a non-empty query before quitting, Ctrl+A and Ctrl+N replace bare-letter bulk bindings, and the cursor refocuses on the first match when the query change would hide it.
This commit is contained in:
parent
96952fb43c
commit
cfd1749ab9
9 changed files with 292 additions and 83 deletions
|
|
@ -25,7 +25,8 @@ export interface PickerState {
|
|||
expanded: Set<string>;
|
||||
checked: Set<string>;
|
||||
cursorId: string;
|
||||
search: { editing: boolean; query: string };
|
||||
search: { query: string };
|
||||
isNavigating: boolean;
|
||||
pendingConfirm: PendingConfirmKind | null;
|
||||
preLoadWarnings: string[];
|
||||
transientHint: { text: string; expiresAt: number } | null;
|
||||
|
|
@ -47,9 +48,7 @@ export type PickerCommand =
|
|||
| 'toggle-select-all-visible'
|
||||
| 'select-none'
|
||||
| 'clear-transient-hint'
|
||||
| 'search-start'
|
||||
| 'search-cancel'
|
||||
| 'search-submit'
|
||||
| 'search-clear'
|
||||
| 'search-backspace'
|
||||
| { type: 'search-input'; value: string }
|
||||
| 'save-request'
|
||||
|
|
@ -464,7 +463,8 @@ export function buildInitialState(args: {
|
|||
expanded,
|
||||
checked: minimalChecked,
|
||||
cursorId: args.tree[0]?.id ?? '',
|
||||
search: { editing: false, query: '' },
|
||||
search: { query: '' },
|
||||
isNavigating: false,
|
||||
pendingConfirm: null,
|
||||
preLoadWarnings,
|
||||
transientHint: null,
|
||||
|
|
@ -473,6 +473,14 @@ export function buildInitialState(args: {
|
|||
};
|
||||
}
|
||||
|
||||
function refocusVisibleCursor(state: PickerState): PickerState {
|
||||
const ids = visibleNodeIds(state);
|
||||
if (ids.length === 0 || ids.includes(state.cursorId)) {
|
||||
return state;
|
||||
}
|
||||
return cloneState(state, { cursorId: ids[0]! });
|
||||
}
|
||||
|
||||
export function reducer(state: PickerState, cmd: PickerCommand, now = Date.now()): { next: PickerState; effect: PickerEffect } {
|
||||
if (state.pendingConfirm) {
|
||||
if (cmd === 'save-confirm') {
|
||||
|
|
@ -491,13 +499,13 @@ export function reducer(state: PickerState, cmd: PickerCommand, now = Date.now()
|
|||
|
||||
switch (cmd) {
|
||||
case 'cursor-up':
|
||||
return { next: moveCursor(state, 'up'), effect: null };
|
||||
return { next: cloneState(moveCursor(state, 'up'), { isNavigating: true }), effect: null };
|
||||
case 'cursor-down':
|
||||
return { next: moveCursor(state, 'down'), effect: null };
|
||||
return { next: cloneState(moveCursor(state, 'down'), { isNavigating: true }), effect: null };
|
||||
case 'cursor-left':
|
||||
return { next: moveCursor(state, 'left'), effect: null };
|
||||
return { next: cloneState(moveCursor(state, 'left'), { isNavigating: true }), effect: null };
|
||||
case 'cursor-right':
|
||||
return { next: moveCursor(state, 'right'), effect: null };
|
||||
return { next: cloneState(moveCursor(state, 'right'), { isNavigating: true }), effect: null };
|
||||
case 'expand':
|
||||
return { next: setExpanded(state, state.cursorId, 'toggle'), effect: null };
|
||||
case 'collapse':
|
||||
|
|
@ -521,15 +529,19 @@ export function reducer(state: PickerState, cmd: PickerCommand, now = Date.now()
|
|||
return { next: selectNone(state), effect: null };
|
||||
case 'clear-transient-hint':
|
||||
return { next: clearExpiredTransientHint(state, now), effect: null };
|
||||
case 'search-start':
|
||||
return { next: cloneState(state, { search: { ...state.search, editing: true } }), effect: null };
|
||||
case 'search-cancel':
|
||||
return { next: cloneState(state, { search: { editing: false, query: '' } }), effect: null };
|
||||
case 'search-submit':
|
||||
return { next: cloneState(state, { search: { ...state.search, editing: false } }), effect: null };
|
||||
case 'search-clear':
|
||||
return {
|
||||
next: cloneState(state, { search: { query: '' }, isNavigating: false }),
|
||||
effect: null,
|
||||
};
|
||||
case 'search-backspace':
|
||||
return {
|
||||
next: cloneState(state, { search: { ...state.search, query: state.search.query.slice(0, -1) } }),
|
||||
next: refocusVisibleCursor(
|
||||
cloneState(state, {
|
||||
search: { query: state.search.query.slice(0, -1) },
|
||||
isNavigating: false,
|
||||
}),
|
||||
),
|
||||
effect: null,
|
||||
};
|
||||
case 'save-request':
|
||||
|
|
@ -546,6 +558,14 @@ export function reducer(state: PickerState, cmd: PickerCommand, now = Date.now()
|
|||
case 'quit':
|
||||
return { next: state, effect: 'quit-without-save' };
|
||||
default:
|
||||
return { next: cloneState(state, { search: { ...state.search, query: state.search.query + cmd.value } }), effect: null };
|
||||
return {
|
||||
next: refocusVisibleCursor(
|
||||
cloneState(state, {
|
||||
search: { query: state.search.query + cmd.value },
|
||||
isNavigating: false,
|
||||
}),
|
||||
),
|
||||
effect: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue