@tooee/choose 0.7.3 → 0.8.0
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/dist/choose-filter.d.ts.map +1 -1
- package/dist/choose-filter.js +11 -2
- package/dist/choose-filter.js.map +1 -1
- package/dist/choose-list.d.ts.map +1 -1
- package/dist/choose-list.js +10 -1
- package/dist/choose-list.js.map +1 -1
- package/dist/choose-panel.d.ts +7 -9
- package/dist/choose-panel.d.ts.map +1 -1
- package/dist/choose-panel.js +5 -8
- package/dist/choose-panel.js.map +1 -1
- package/dist/choose-store.d.ts +57 -0
- package/dist/choose-store.d.ts.map +1 -0
- package/dist/choose-store.js +102 -0
- package/dist/choose-store.js.map +1 -0
- package/dist/choose.d.ts +5 -5
- package/dist/choose.d.ts.map +1 -1
- package/dist/choose.js +26 -12
- package/dist/choose.js.map +1 -1
- package/dist/fuzzy.d.ts.map +1 -1
- package/dist/fuzzy.js +2 -14
- package/dist/fuzzy.js.map +1 -1
- package/dist/launch.d.ts.map +1 -1
- package/dist/launch.js +1 -1
- package/dist/launch.js.map +1 -1
- package/dist/source.d.ts.map +1 -1
- package/dist/source.js +17 -5
- package/dist/source.js.map +1 -1
- package/dist/use-choose-dialog.d.ts.map +1 -1
- package/dist/use-choose-dialog.js +22 -84
- package/dist/use-choose-dialog.js.map +1 -1
- package/dist/use-choose.d.ts +1 -1
- package/dist/use-choose.d.ts.map +1 -1
- package/dist/use-choose.js +236 -264
- package/dist/use-choose.js.map +1 -1
- package/package.json +10 -7
- package/src/choose-filter.tsx +10 -2
- package/src/choose-list.tsx +9 -1
- package/src/choose-panel.tsx +18 -71
- package/src/choose-store.ts +146 -0
- package/src/choose.tsx +43 -27
- package/src/fuzzy.ts +2 -17
- package/src/launch.tsx +7 -2
- package/src/source.ts +24 -6
- package/src/use-choose-dialog.tsx +30 -98
- package/src/use-choose.ts +273 -278
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tooee/choose",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Terminal selection with fuzzy filtering",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -40,16 +40,19 @@
|
|
|
40
40
|
"typecheck": "tsc --noEmit"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@tooee/commands": "0.
|
|
44
|
-
"@tooee/fuzzy": "0.
|
|
45
|
-
"@tooee/layout": "0.
|
|
46
|
-
"@tooee/overlays": "0.
|
|
47
|
-
"@tooee/shell": "0.
|
|
48
|
-
"@tooee/themes": "0.
|
|
43
|
+
"@tooee/commands": "0.8.0",
|
|
44
|
+
"@tooee/fuzzy": "0.8.0",
|
|
45
|
+
"@tooee/layout": "0.8.0",
|
|
46
|
+
"@tooee/overlays": "0.8.0",
|
|
47
|
+
"@tooee/shell": "0.8.0",
|
|
48
|
+
"@tooee/themes": "0.8.0",
|
|
49
|
+
"@xstate/store": "^4",
|
|
50
|
+
"@xstate/store-react": "^2"
|
|
49
51
|
},
|
|
50
52
|
"devDependencies": {
|
|
51
53
|
"@opentui/core": "^0.5.1",
|
|
52
54
|
"@opentui/react": "^0.5.1",
|
|
55
|
+
"@tooee/test-support": "0.7.3",
|
|
53
56
|
"@types/bun": "^1.3.10",
|
|
54
57
|
"@types/react": "^19.2.14",
|
|
55
58
|
"typescript": "^5.9.3"
|
package/src/choose-filter.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
import { CloseButton,
|
|
2
|
+
import { CloseButton, decodeReactContent } from "@tooee/layout";
|
|
3
|
+
import { useTheme } from "@tooee/themes";
|
|
3
4
|
import type { UseChooseResult } from "./use-choose.js";
|
|
4
5
|
|
|
5
6
|
export interface ChooseFilterProps {
|
|
@@ -22,10 +23,17 @@ export const ChooseFilter = function ChooseFilter({
|
|
|
22
23
|
const { state, view } = choose;
|
|
23
24
|
const handleFilterInput = view.onFilterInput;
|
|
24
25
|
const handleFilterKeyDown = view.onFilterKeyDown;
|
|
26
|
+
const promptContent = decodeReactContent(prompt);
|
|
27
|
+
let renderedPrompt: ReactNode = null;
|
|
28
|
+
if (promptContent.kind === "string") {
|
|
29
|
+
renderedPrompt = <text content={promptContent.value} fg={theme.accent} />;
|
|
30
|
+
} else if (promptContent.kind === "node") {
|
|
31
|
+
renderedPrompt = promptContent.value;
|
|
32
|
+
}
|
|
25
33
|
|
|
26
34
|
return (
|
|
27
35
|
<box flexDirection="row" height={1} style={{ paddingLeft: 1, paddingRight: 1 }}>
|
|
28
|
-
{
|
|
36
|
+
{renderedPrompt}
|
|
29
37
|
<input
|
|
30
38
|
ref={view.filterRef}
|
|
31
39
|
focused={view.filterFocused}
|
package/src/choose-list.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEffect, useRef } from "react";
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
|
+
import { decodeReactContent } from "@tooee/layout";
|
|
3
4
|
import type { ScrollBoxRenderable } from "@opentui/core";
|
|
4
5
|
import { useTheme } from "@tooee/themes";
|
|
5
6
|
import { ChooseHighlightedText } from "./choose-highlighted-text.js";
|
|
@@ -37,9 +38,16 @@ const ThemedLine = function ThemedLine({
|
|
|
37
38
|
content: ReactNode;
|
|
38
39
|
color: string;
|
|
39
40
|
}): ReactNode {
|
|
41
|
+
const decoded = decodeReactContent(content);
|
|
42
|
+
let rendered: ReactNode = null;
|
|
43
|
+
if (decoded.kind === "string") {
|
|
44
|
+
rendered = <text content={decoded.value} fg={color} />;
|
|
45
|
+
} else if (decoded.kind === "node") {
|
|
46
|
+
rendered = decoded.value;
|
|
47
|
+
}
|
|
40
48
|
return (
|
|
41
49
|
<box height={1} style={{ paddingLeft: 2 }}>
|
|
42
|
-
{
|
|
50
|
+
{rendered}
|
|
43
51
|
</box>
|
|
44
52
|
);
|
|
45
53
|
};
|
package/src/choose-panel.tsx
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
+
import type { MouseEvent } from "@opentui/core";
|
|
2
3
|
import { useMode } from "@tooee/commands";
|
|
3
4
|
import type { Mode } from "@tooee/commands";
|
|
4
|
-
import {
|
|
5
|
+
import { OverlayPanel } from "@tooee/layout";
|
|
6
|
+
import type { PanelInset, PanelInsetValue } from "@tooee/layout";
|
|
5
7
|
|
|
6
8
|
export const buildChooseHints = function buildChooseHints(
|
|
7
9
|
mode: Mode,
|
|
@@ -25,14 +27,8 @@ export const buildChooseHints = function buildChooseHints(
|
|
|
25
27
|
return options.extra ? [...base, ...options.extra] : base;
|
|
26
28
|
};
|
|
27
29
|
|
|
28
|
-
export type ChoosePanelInsetValue =
|
|
29
|
-
|
|
30
|
-
export interface ChoosePanelInset {
|
|
31
|
-
left?: ChoosePanelInsetValue;
|
|
32
|
-
right?: ChoosePanelInsetValue;
|
|
33
|
-
top?: ChoosePanelInsetValue;
|
|
34
|
-
bottom?: ChoosePanelInsetValue;
|
|
35
|
-
}
|
|
30
|
+
export type ChoosePanelInsetValue = PanelInsetValue;
|
|
31
|
+
export type ChoosePanelInset = PanelInset;
|
|
36
32
|
|
|
37
33
|
export interface ChoosePanelProps {
|
|
38
34
|
title?: ReactNode;
|
|
@@ -44,9 +40,10 @@ export interface ChoosePanelProps {
|
|
|
44
40
|
footer?: ReactNode;
|
|
45
41
|
onClose?: () => void;
|
|
46
42
|
inset?: ChoosePanelInset;
|
|
43
|
+
onMouseDown?: (event: MouseEvent) => void;
|
|
47
44
|
}
|
|
48
45
|
|
|
49
|
-
/**
|
|
46
|
+
/** Chooser-specific overlay panel with mode-aware default hints. */
|
|
50
47
|
export const ChoosePanel = function ChoosePanel({
|
|
51
48
|
title,
|
|
52
49
|
filter,
|
|
@@ -57,74 +54,24 @@ export const ChoosePanel = function ChoosePanel({
|
|
|
57
54
|
footer,
|
|
58
55
|
onClose,
|
|
59
56
|
inset,
|
|
57
|
+
onMouseDown,
|
|
60
58
|
}: ChoosePanelProps): ReactNode {
|
|
61
|
-
const { theme } = useTheme();
|
|
62
59
|
const mode = useMode();
|
|
63
60
|
const defaults = buildChooseHints(mode, { multi });
|
|
64
61
|
const hintContent = hints ? hints({ defaults, mode }) : defaults.join(" ");
|
|
65
62
|
|
|
66
63
|
return (
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
borderColor={theme.borderActive}
|
|
64
|
+
<OverlayPanel
|
|
65
|
+
title={title}
|
|
66
|
+
prompt={filter}
|
|
67
|
+
hints={hintContent}
|
|
68
|
+
statusRight={statusRight}
|
|
69
|
+
footer={footer}
|
|
70
|
+
onClose={onClose}
|
|
71
|
+
inset={inset}
|
|
72
|
+
onMouseDown={onMouseDown}
|
|
77
73
|
>
|
|
78
|
-
{title !== null && title !== undefined && (
|
|
79
|
-
<box
|
|
80
|
-
flexDirection="row"
|
|
81
|
-
height={1}
|
|
82
|
-
paddingLeft={1}
|
|
83
|
-
paddingRight={1}
|
|
84
|
-
backgroundColor={theme.backgroundElement}
|
|
85
|
-
>
|
|
86
|
-
{typeof title === "string" ? (
|
|
87
|
-
<text content={title} fg={theme.accent} style={{ flexGrow: 1 }} />
|
|
88
|
-
) : (
|
|
89
|
-
<box flexDirection="row" style={{ flexGrow: 1 }}>
|
|
90
|
-
{title}
|
|
91
|
-
</box>
|
|
92
|
-
)}
|
|
93
|
-
{onClose && <CloseButton onClose={onClose} />}
|
|
94
|
-
</box>
|
|
95
|
-
)}
|
|
96
|
-
|
|
97
|
-
{filter}
|
|
98
74
|
{children}
|
|
99
|
-
|
|
100
|
-
{footer !== null && footer !== undefined && (
|
|
101
|
-
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
102
|
-
{footer}
|
|
103
|
-
</box>
|
|
104
|
-
)}
|
|
105
|
-
|
|
106
|
-
<box
|
|
107
|
-
flexDirection="row"
|
|
108
|
-
height={1}
|
|
109
|
-
paddingLeft={1}
|
|
110
|
-
paddingRight={1}
|
|
111
|
-
backgroundColor={theme.backgroundElement}
|
|
112
|
-
>
|
|
113
|
-
<box flexDirection="row" style={{ flexGrow: 1 }}>
|
|
114
|
-
{typeof hintContent === "string" ? (
|
|
115
|
-
<text content={hintContent} fg={theme.textMuted} />
|
|
116
|
-
) : (
|
|
117
|
-
hintContent
|
|
118
|
-
)}
|
|
119
|
-
</box>
|
|
120
|
-
{statusRight !== null &&
|
|
121
|
-
statusRight !== undefined &&
|
|
122
|
-
(typeof statusRight === "string" ? (
|
|
123
|
-
<text content={statusRight} fg={theme.textMuted} />
|
|
124
|
-
) : (
|
|
125
|
-
statusRight
|
|
126
|
-
))}
|
|
127
|
-
</box>
|
|
128
|
-
</box>
|
|
75
|
+
</OverlayPanel>
|
|
129
76
|
);
|
|
130
77
|
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { createStore } from "@xstate/store";
|
|
2
|
+
import { fuzzyFilter } from "./fuzzy.js";
|
|
3
|
+
import type { FuzzyMatch } from "./fuzzy.js";
|
|
4
|
+
import type { ChooseItem } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export interface ChooseStoreContext {
|
|
7
|
+
items: ChooseItem[];
|
|
8
|
+
matches: FuzzyMatch[];
|
|
9
|
+
filterQuery: string;
|
|
10
|
+
activeIndex: number;
|
|
11
|
+
selectedOriginalIndices: ReadonlySet<number>;
|
|
12
|
+
loading: boolean;
|
|
13
|
+
error: string | null;
|
|
14
|
+
requestId: number;
|
|
15
|
+
reloadRevision: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ChooseStoreEventDefinitions {
|
|
19
|
+
filterChanged: { query: string };
|
|
20
|
+
activeIndexSet: { index: number };
|
|
21
|
+
moved: { delta: number };
|
|
22
|
+
activeToggled: Record<never, never>;
|
|
23
|
+
requestStarted: Record<never, never>;
|
|
24
|
+
loadingStarted: { requestId: number };
|
|
25
|
+
loadSucceeded: { requestId: number; items: ChooseItem[] };
|
|
26
|
+
loadFailed: { requestId: number; error: string };
|
|
27
|
+
reloadRequested: Record<never, never>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type ChooseStoreEvents = {
|
|
31
|
+
[EventName in keyof ChooseStoreEventDefinitions]: ChooseStoreEventDefinitions[EventName];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const replaceItems = function replaceItems(
|
|
35
|
+
ctx: ChooseStoreContext,
|
|
36
|
+
items: ChooseItem[],
|
|
37
|
+
): ChooseStoreContext {
|
|
38
|
+
return {
|
|
39
|
+
...ctx,
|
|
40
|
+
activeIndex: 0,
|
|
41
|
+
items,
|
|
42
|
+
matches: fuzzyFilter(items, ctx.filterQuery),
|
|
43
|
+
selectedOriginalIndices: new Set(),
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const clampActiveIndex = function clampActiveIndex(index: number, matchCount: number): number {
|
|
48
|
+
return Math.min(Math.max(0, matchCount - 1), Math.max(0, index));
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const createChooseStore = function createChooseStore(options: {
|
|
52
|
+
items?: ChooseItem[];
|
|
53
|
+
initialFilter?: string;
|
|
54
|
+
loading?: boolean;
|
|
55
|
+
}) {
|
|
56
|
+
const items = options.items ?? [];
|
|
57
|
+
const filterQuery = options.initialFilter ?? "";
|
|
58
|
+
|
|
59
|
+
return createStore<ChooseStoreContext, ChooseStoreEvents>({
|
|
60
|
+
context: {
|
|
61
|
+
activeIndex: 0,
|
|
62
|
+
error: null,
|
|
63
|
+
filterQuery,
|
|
64
|
+
items,
|
|
65
|
+
loading: options.loading ?? false,
|
|
66
|
+
matches: fuzzyFilter(items, filterQuery),
|
|
67
|
+
reloadRevision: 0,
|
|
68
|
+
requestId: 0,
|
|
69
|
+
selectedOriginalIndices: new Set(),
|
|
70
|
+
},
|
|
71
|
+
on: {
|
|
72
|
+
activeIndexSet: (ctx, event) => {
|
|
73
|
+
const activeIndex = clampActiveIndex(event.index, ctx.matches.length);
|
|
74
|
+
return activeIndex === ctx.activeIndex ? ctx : { ...ctx, activeIndex };
|
|
75
|
+
},
|
|
76
|
+
activeToggled: (ctx) => {
|
|
77
|
+
const originalIndex = ctx.matches[ctx.activeIndex]?.originalIndex;
|
|
78
|
+
if (originalIndex === undefined) {
|
|
79
|
+
return ctx;
|
|
80
|
+
}
|
|
81
|
+
const selectedOriginalIndices = new Set(ctx.selectedOriginalIndices);
|
|
82
|
+
if (selectedOriginalIndices.has(originalIndex)) {
|
|
83
|
+
selectedOriginalIndices.delete(originalIndex);
|
|
84
|
+
} else {
|
|
85
|
+
selectedOriginalIndices.add(originalIndex);
|
|
86
|
+
}
|
|
87
|
+
return { ...ctx, selectedOriginalIndices };
|
|
88
|
+
},
|
|
89
|
+
filterChanged: (ctx, event) => {
|
|
90
|
+
if (event.query === ctx.filterQuery) {
|
|
91
|
+
return ctx;
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
...ctx,
|
|
95
|
+
activeIndex: 0,
|
|
96
|
+
filterQuery: event.query,
|
|
97
|
+
matches: fuzzyFilter(ctx.items, event.query),
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
loadFailed: (ctx, event) => {
|
|
101
|
+
if (event.requestId !== ctx.requestId) {
|
|
102
|
+
return ctx;
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
...replaceItems(ctx, []),
|
|
106
|
+
error: event.error,
|
|
107
|
+
loading: false,
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
loadSucceeded: (ctx, event) => {
|
|
111
|
+
if (event.requestId !== ctx.requestId) {
|
|
112
|
+
return ctx;
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
...replaceItems(ctx, event.items),
|
|
116
|
+
error: null,
|
|
117
|
+
loading: false,
|
|
118
|
+
};
|
|
119
|
+
},
|
|
120
|
+
loadingStarted: (ctx, event) =>
|
|
121
|
+
event.requestId === ctx.requestId && !ctx.loading ? { ...ctx, loading: true } : ctx,
|
|
122
|
+
moved: (ctx, event) => {
|
|
123
|
+
const activeIndex = clampActiveIndex(ctx.activeIndex + event.delta, ctx.matches.length);
|
|
124
|
+
return activeIndex === ctx.activeIndex ? ctx : { ...ctx, activeIndex };
|
|
125
|
+
},
|
|
126
|
+
reloadRequested: (ctx) => ({ ...ctx, reloadRevision: ctx.reloadRevision + 1 }),
|
|
127
|
+
requestStarted: (ctx) => ({
|
|
128
|
+
...ctx,
|
|
129
|
+
error: null,
|
|
130
|
+
requestId: ctx.requestId + 1,
|
|
131
|
+
}),
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export type ChooseStore = ReturnType<typeof createChooseStore>;
|
|
137
|
+
|
|
138
|
+
export const selectItems = (ctx: ChooseStoreContext): ChooseItem[] => ctx.items;
|
|
139
|
+
export const selectMatches = (ctx: ChooseStoreContext): FuzzyMatch[] => ctx.matches;
|
|
140
|
+
export const selectFilterQuery = (ctx: ChooseStoreContext): string => ctx.filterQuery;
|
|
141
|
+
export const selectActiveIndex = (ctx: ChooseStoreContext): number => ctx.activeIndex;
|
|
142
|
+
export const selectSelectedOriginalIndices = (ctx: ChooseStoreContext): ReadonlySet<number> =>
|
|
143
|
+
ctx.selectedOriginalIndices;
|
|
144
|
+
export const selectLoading = (ctx: ChooseStoreContext): boolean => ctx.loading;
|
|
145
|
+
export const selectError = (ctx: ChooseStoreContext): string | null => ctx.error;
|
|
146
|
+
export const selectReloadRevision = (ctx: ChooseStoreContext): number => ctx.reloadRevision;
|
package/src/choose.tsx
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { useImperativeHandle } from "react";
|
|
2
|
-
import type { Ref } from "react";
|
|
2
|
+
import type { ReactNode, Ref } from "react";
|
|
3
3
|
import { AppLayout } from "@tooee/layout";
|
|
4
4
|
import { useHasOverlay, useHasModalOverlay } from "@tooee/overlays";
|
|
5
5
|
import { useTheme } from "@tooee/themes";
|
|
6
6
|
import { useThemeCommands, useQuitCommand } from "@tooee/shell";
|
|
7
|
-
import {
|
|
7
|
+
import { useSurfaceInvoke } from "@tooee/commands";
|
|
8
8
|
import type { ActionDefinition } from "@tooee/commands";
|
|
9
9
|
import { ChooseFilter } from "./choose-filter.js";
|
|
10
10
|
import { ChooseList } from "./choose-list.js";
|
|
@@ -14,12 +14,12 @@ import type { ChooseContentProvider, ChooseOptions, ChooseResult } from "./types
|
|
|
14
14
|
import { useChoose } from "./use-choose.js";
|
|
15
15
|
import type { ChooseController } from "./use-choose.js";
|
|
16
16
|
|
|
17
|
-
export interface ChooseProps {
|
|
17
|
+
export interface ChooseProps extends ChooseOptions {
|
|
18
18
|
contentProvider: ChooseContentProvider;
|
|
19
|
+
/** @deprecated Pass chooser options as top-level props. This alias will be removed in 0.9.0. */
|
|
19
20
|
options?: ChooseOptions;
|
|
20
|
-
/** Legacy name retained for source compatibility. */
|
|
21
21
|
actions?: ActionDefinition[];
|
|
22
|
-
/**
|
|
22
|
+
/** @deprecated Use `actions`. This alias will be removed in 0.9.0. */
|
|
23
23
|
commands?: ActionDefinition[];
|
|
24
24
|
controllerRef?: Ref<ChooseController>;
|
|
25
25
|
renderItem?: ChooseListProps["renderItem"];
|
|
@@ -36,20 +36,37 @@ export interface ChooseProps {
|
|
|
36
36
|
onCancel?: () => void;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
39
|
+
interface ResolvedChooseProps extends ChooseOptions {
|
|
40
|
+
actions?: ActionDefinition[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const resolveChooseProps = function resolveChooseProps(props: ChooseProps): ResolvedChooseProps {
|
|
44
|
+
// oxlint-disable-next-line typescript/no-deprecated -- compatibility alias remains supported until 0.9.0
|
|
45
|
+
const { options } = props;
|
|
46
|
+
// oxlint-disable-next-line typescript/no-deprecated -- compatibility alias remains supported until 0.9.0
|
|
47
|
+
const { commands } = props;
|
|
48
|
+
return {
|
|
49
|
+
actions: props.actions ?? commands,
|
|
50
|
+
emptyMessage: props.emptyMessage ?? options?.emptyMessage,
|
|
51
|
+
multi: props.multi ?? options?.multi,
|
|
52
|
+
placeholder: props.placeholder ?? options?.placeholder,
|
|
53
|
+
prompt: props.prompt ?? options?.prompt,
|
|
54
|
+
title: props.title ?? options?.title,
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const Choose = function Choose(props: ChooseProps): ReactNode {
|
|
59
|
+
const { contentProvider, controllerRef, renderItem, onConfirm, onCancel } = props;
|
|
60
|
+
const {
|
|
61
|
+
actions: effectiveActions,
|
|
62
|
+
emptyMessage,
|
|
63
|
+
multi = false,
|
|
64
|
+
placeholder,
|
|
65
|
+
prompt,
|
|
66
|
+
title,
|
|
67
|
+
} = resolveChooseProps(props);
|
|
49
68
|
const { theme } = useTheme();
|
|
50
|
-
const { invoke } =
|
|
51
|
-
const effectiveCommands = commands ?? actions;
|
|
52
|
-
const multi = options?.multi ?? false;
|
|
69
|
+
const { invoke } = useSurfaceInvoke();
|
|
53
70
|
|
|
54
71
|
const { name: themeName } = useThemeCommands();
|
|
55
72
|
useQuitCommand({ onQuit: () => onCancel?.() });
|
|
@@ -58,13 +75,13 @@ export const Choose = function Choose({
|
|
|
58
75
|
const hasModalOverlay = useHasModalOverlay();
|
|
59
76
|
|
|
60
77
|
const choose = useChoose({
|
|
61
|
-
commands:
|
|
78
|
+
commands: effectiveActions,
|
|
62
79
|
multi,
|
|
63
80
|
onCancel,
|
|
64
81
|
onSubmit: (result) => {
|
|
65
82
|
// Standalone behaviour: a command named `submit` wins over `onConfirm`, so
|
|
66
83
|
// action-driven CLIs keep their own submit semantics.
|
|
67
|
-
if (
|
|
84
|
+
if (effectiveActions?.some((action) => action.id === "submit") === true) {
|
|
68
85
|
invoke("submit");
|
|
69
86
|
return;
|
|
70
87
|
}
|
|
@@ -98,10 +115,10 @@ export const Choose = function Choose({
|
|
|
98
115
|
|
|
99
116
|
const hints = buildChooseHints(choose.view.mode, { multi });
|
|
100
117
|
let titleBar: { title: string } | undefined;
|
|
101
|
-
if (
|
|
102
|
-
titleBar = { title
|
|
103
|
-
} else if (
|
|
104
|
-
titleBar = { title:
|
|
118
|
+
if (title !== undefined && title !== "") {
|
|
119
|
+
titleBar = { title };
|
|
120
|
+
} else if (prompt !== undefined && prompt !== "") {
|
|
121
|
+
titleBar = { title: prompt };
|
|
105
122
|
}
|
|
106
123
|
|
|
107
124
|
return (
|
|
@@ -125,15 +142,14 @@ export const Choose = function Choose({
|
|
|
125
142
|
{ label: "", value: hints.join(" ") },
|
|
126
143
|
],
|
|
127
144
|
}}
|
|
128
|
-
scrollProps={{ focused: false }}
|
|
129
145
|
>
|
|
130
146
|
<box flexDirection="column" style={{ flexGrow: 1 }}>
|
|
131
|
-
<ChooseFilter choose={choose} placeholder={
|
|
147
|
+
<ChooseFilter choose={choose} placeholder={placeholder} />
|
|
132
148
|
<ChooseList
|
|
133
149
|
choose={choose}
|
|
134
150
|
rowClick="activate"
|
|
135
151
|
renderItem={renderItem}
|
|
136
|
-
emptyContent={
|
|
152
|
+
emptyContent={emptyMessage}
|
|
137
153
|
suspended={hasModalOverlay}
|
|
138
154
|
/>
|
|
139
155
|
</box>
|
package/src/fuzzy.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { rankBy } from "@tooee/fuzzy";
|
|
2
2
|
import type { ChooseItem } from "./types.js";
|
|
3
3
|
|
|
4
4
|
export interface FuzzyMatch {
|
|
@@ -9,20 +9,5 @@ export interface FuzzyMatch {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export const fuzzyFilter = function fuzzyFilter(items: ChooseItem[], query: string): FuzzyMatch[] {
|
|
12
|
-
|
|
13
|
-
return items.map((item, i) => ({ item, originalIndex: i, positions: [], score: 0 }));
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const results: FuzzyMatch[] = [];
|
|
17
|
-
|
|
18
|
-
for (let i = 0; i < items.length; i += 1) {
|
|
19
|
-
const item = items[i];
|
|
20
|
-
const match = fuzzyMatchPositions(query, item.text);
|
|
21
|
-
if (match) {
|
|
22
|
-
results.push({ item, originalIndex: i, positions: match.positions, score: match.score });
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
results.sort((a, b) => b.score - a.score);
|
|
27
|
-
return results;
|
|
12
|
+
return rankBy(items, query, (item) => item.text);
|
|
28
13
|
};
|
package/src/launch.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { runCliSession } from "@tooee/shell";
|
|
|
2
2
|
import type { ActionDefinition } from "@tooee/commands";
|
|
3
3
|
import { Choose } from "./choose.js";
|
|
4
4
|
import type { ChooseContentProvider, ChooseOptions, ChooseResult } from "./types.js";
|
|
5
|
+
import type { ReactNode } from "react";
|
|
5
6
|
|
|
6
7
|
export interface ChooseLaunchOptions {
|
|
7
8
|
contentProvider: ChooseContentProvider;
|
|
@@ -13,10 +14,14 @@ export const launch = async function launch(
|
|
|
13
14
|
opts: ChooseLaunchOptions,
|
|
14
15
|
): Promise<ChooseResult | null> {
|
|
15
16
|
const result = await runCliSession<ChooseResult>(
|
|
16
|
-
({ resolve, cancel }):
|
|
17
|
+
({ resolve, cancel }): ReactNode => (
|
|
17
18
|
<Choose
|
|
18
19
|
contentProvider={opts.contentProvider}
|
|
19
|
-
|
|
20
|
+
title={opts.options?.title}
|
|
21
|
+
prompt={opts.options?.prompt}
|
|
22
|
+
placeholder={opts.options?.placeholder}
|
|
23
|
+
multi={opts.options?.multi}
|
|
24
|
+
emptyMessage={opts.options?.emptyMessage}
|
|
20
25
|
actions={opts.actions}
|
|
21
26
|
onConfirm={resolve}
|
|
22
27
|
onCancel={cancel}
|
package/src/source.ts
CHANGED
|
@@ -1,18 +1,36 @@
|
|
|
1
|
-
import type { ChooseItem, ChooseSource } from "./types.js";
|
|
1
|
+
import type { ChooseContentProvider, ChooseItem, ChooseSource } from "./types.js";
|
|
2
|
+
|
|
3
|
+
type DecodedChooseSource =
|
|
4
|
+
| { kind: "items"; items: ChooseItem[] }
|
|
5
|
+
| { kind: "loader"; load: () => ChooseItem[] | Promise<ChooseItem[]> }
|
|
6
|
+
| { kind: "provider"; provider: ChooseContentProvider };
|
|
7
|
+
|
|
8
|
+
const decodeChooseSource = function decodeChooseSource(source: ChooseSource): DecodedChooseSource {
|
|
9
|
+
if (Array.isArray(source)) {
|
|
10
|
+
return { items: source, kind: "items" };
|
|
11
|
+
}
|
|
12
|
+
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- public source boundary distinguishes the documented loader callback from a provider
|
|
13
|
+
if (typeof source === "function") {
|
|
14
|
+
return { kind: "loader", load: source };
|
|
15
|
+
}
|
|
16
|
+
return { kind: "provider", provider: source };
|
|
17
|
+
};
|
|
2
18
|
|
|
3
19
|
/** Resolve every public Choose source shape through one compatibility boundary. */
|
|
4
20
|
export const loadChooseSource = function loadChooseSource(
|
|
5
21
|
source: ChooseSource,
|
|
6
22
|
): ChooseItem[] | Promise<ChooseItem[]> {
|
|
7
|
-
|
|
8
|
-
|
|
23
|
+
const decoded = decodeChooseSource(source);
|
|
24
|
+
if (decoded.kind === "items") {
|
|
25
|
+
return decoded.items;
|
|
9
26
|
}
|
|
10
|
-
if (
|
|
11
|
-
return
|
|
27
|
+
if (decoded.kind === "loader") {
|
|
28
|
+
return decoded.load();
|
|
12
29
|
}
|
|
13
|
-
return
|
|
30
|
+
return decoded.provider.load();
|
|
14
31
|
};
|
|
15
32
|
|
|
33
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- caught loader failures enter here before conversion to display text
|
|
16
34
|
export const chooseSourceError = function chooseSourceError(error: unknown): string {
|
|
17
35
|
return error instanceof Error ? error.message : String(error);
|
|
18
36
|
};
|