@tooee/choose 0.1.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/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # @tooee/choose
2
+
3
+ Terminal selection with fuzzy filtering.
4
+
5
+ Part of the [Tooee](https://github.com/gingerhendrix/tooee) monorepo. See the main repo for documentation.
@@ -0,0 +1,14 @@
1
+ import type { ActionDefinition } from "@tooee/commands";
2
+ import type { ChooseContentProvider, ChooseOptions, ChooseResult } from "./types.js";
3
+ interface ChooseProps {
4
+ contentProvider: ChooseContentProvider;
5
+ options?: ChooseOptions;
6
+ actions?: ActionDefinition[];
7
+ /** @deprecated Use actions instead */
8
+ onConfirm?: (result: ChooseResult) => void;
9
+ /** @deprecated Use actions instead */
10
+ onCancel?: () => void;
11
+ }
12
+ export declare function Choose({ contentProvider, options, actions, onConfirm, onCancel }: ChooseProps): import("react").ReactNode;
13
+ export {};
14
+ //# sourceMappingURL=Choose.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Choose.d.ts","sourceRoot":"","sources":["../src/Choose.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AACvD,OAAO,KAAK,EAAc,qBAAqB,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAGhG,UAAU,WAAW;IACnB,eAAe,EAAE,qBAAqB,CAAA;IACtC,OAAO,CAAC,EAAE,aAAa,CAAA;IACvB,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC5B,sCAAsC;IACtC,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,CAAA;IAC1C,sCAAsC;IACtC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AAED,wBAAgB,MAAM,CAAC,EAAE,eAAe,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,WAAW,6BAwR7F"}
package/dist/Choose.js ADDED
@@ -0,0 +1,239 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
+ import { useState, useEffect, useRef, useCallback } from "react";
3
+ import { useKeyboard } from "@opentui/react";
4
+ import { AppLayout } from "@tooee/layout";
5
+ import { ThemePicker, useTheme } from "@tooee/themes";
6
+ import { useThemeCommands } from "@tooee/shell";
7
+ import { useMode, useSetMode, useCommand, useActions, useProvideCommandContext, useCommandContext } from "@tooee/commands";
8
+ import { fuzzyFilter } from "./fuzzy.js";
9
+ export function Choose({ contentProvider, options, actions, onConfirm, onCancel }) {
10
+ const { theme } = useTheme();
11
+ const [items, setItems] = useState([]);
12
+ const [filterQuery, setFilterQuery] = useState("");
13
+ const [filteredItems, setFilteredItems] = useState([]);
14
+ const [activeIndex, setActiveIndex] = useState(0);
15
+ const [selectedIndices, setSelectedIndices] = useState(new Set());
16
+ const [loading, setLoading] = useState(true);
17
+ const scrollRef = useRef(null);
18
+ const { invoke } = useCommandContext();
19
+ const multi = options?.multi ?? false;
20
+ useEffect(() => {
21
+ const result = contentProvider.load();
22
+ if (result instanceof Promise) {
23
+ result.then((loaded) => {
24
+ setItems(loaded);
25
+ setFilteredItems(fuzzyFilter(loaded, ""));
26
+ setLoading(false);
27
+ });
28
+ }
29
+ else {
30
+ setItems(result);
31
+ setFilteredItems(fuzzyFilter(result, ""));
32
+ setLoading(false);
33
+ }
34
+ }, [contentProvider]);
35
+ useEffect(() => {
36
+ const matches = fuzzyFilter(items, filterQuery);
37
+ setFilteredItems(matches);
38
+ setActiveIndex(0);
39
+ }, [filterQuery, items]);
40
+ const { name: themeName, picker: themePicker } = useThemeCommands();
41
+ const mode = useMode();
42
+ const setMode = useSetMode();
43
+ // Compute selected items for context
44
+ const getSelectedItems = useCallback(() => {
45
+ if (multi) {
46
+ const selected = Array.from(selectedIndices).map((i) => items[i]);
47
+ if (selected.length === 0 && filteredItems[activeIndex]) {
48
+ return [filteredItems[activeIndex].item];
49
+ }
50
+ return selected;
51
+ }
52
+ const match = filteredItems[activeIndex];
53
+ return match ? [match.item] : [];
54
+ }, [multi, selectedIndices, items, filteredItems, activeIndex]);
55
+ useProvideCommandContext(() => ({
56
+ choose: {
57
+ activeItem: filteredItems[activeIndex]?.item,
58
+ selectedItems: getSelectedItems(),
59
+ filterQuery,
60
+ },
61
+ exit: () => onCancel?.(),
62
+ }));
63
+ useActions(actions);
64
+ const moveUp = useCallback(() => {
65
+ setActiveIndex((i) => Math.max(0, i - 1));
66
+ }, []);
67
+ const moveDown = useCallback(() => {
68
+ setActiveIndex((i) => Math.min(filteredItems.length - 1, i + 1));
69
+ }, [filteredItems.length]);
70
+ // Register a/i to return to insert mode from cursor mode
71
+ useCommand({
72
+ id: "choose:insert-mode-a",
73
+ title: "Insert mode",
74
+ hotkey: "a",
75
+ modes: ["cursor"],
76
+ handler: () => setMode("insert"),
77
+ hidden: true,
78
+ });
79
+ useCommand({
80
+ id: "choose:insert-mode-i",
81
+ title: "Insert mode",
82
+ hotkey: "i",
83
+ modes: ["cursor"],
84
+ handler: () => setMode("insert"),
85
+ hidden: true,
86
+ });
87
+ // Register j/k for vim-style navigation in cursor mode
88
+ useCommand({
89
+ id: "choose:move-down",
90
+ title: "Move down",
91
+ hotkey: "j",
92
+ modes: ["cursor"],
93
+ handler: moveDown,
94
+ hidden: true,
95
+ });
96
+ useCommand({
97
+ id: "choose:move-up",
98
+ title: "Move up",
99
+ hotkey: "k",
100
+ modes: ["cursor"],
101
+ handler: moveUp,
102
+ hidden: true,
103
+ });
104
+ const toggleSelection = useCallback((index) => {
105
+ setSelectedIndices((prev) => {
106
+ const next = new Set(prev);
107
+ const origIndex = filteredItems[index]?.originalIndex;
108
+ if (origIndex === undefined)
109
+ return prev;
110
+ if (next.has(origIndex)) {
111
+ next.delete(origIndex);
112
+ }
113
+ else {
114
+ next.add(origIndex);
115
+ }
116
+ return next;
117
+ });
118
+ }, [filteredItems]);
119
+ const confirm = useCallback(() => {
120
+ // If there's a "submit" action, invoke it via the command system
121
+ if (actions?.some((a) => a.id === "submit")) {
122
+ invoke("submit");
123
+ return;
124
+ }
125
+ // Legacy: use onConfirm/onCancel callbacks
126
+ if (multi) {
127
+ const selected = Array.from(selectedIndices).map((i) => items[i]);
128
+ if (selected.length === 0 && filteredItems[activeIndex]) {
129
+ onConfirm?.({ items: [filteredItems[activeIndex].item] });
130
+ }
131
+ else {
132
+ onConfirm?.({ items: selected });
133
+ }
134
+ }
135
+ else {
136
+ const match = filteredItems[activeIndex];
137
+ if (match) {
138
+ onConfirm?.({ items: [match.item] });
139
+ }
140
+ else {
141
+ onCancel?.();
142
+ }
143
+ }
144
+ }, [multi, selectedIndices, items, filteredItems, activeIndex, onConfirm, onCancel, actions, invoke]);
145
+ useKeyboard((key) => {
146
+ if (key.name === "escape") {
147
+ if (mode === "insert") {
148
+ // Switch to cursor mode (allows theme switching, quit, etc.)
149
+ setMode("cursor");
150
+ }
151
+ else {
152
+ // In cursor mode, escape cancels
153
+ onCancel?.();
154
+ }
155
+ return;
156
+ }
157
+ if (key.name === "return") {
158
+ confirm();
159
+ return;
160
+ }
161
+ if (key.name === "up" || (key.ctrl && key.name === "p")) {
162
+ moveUp();
163
+ return;
164
+ }
165
+ if (key.name === "down" || (key.ctrl && key.name === "n")) {
166
+ moveDown();
167
+ return;
168
+ }
169
+ if (multi && key.name === "tab") {
170
+ if (key.shift) {
171
+ toggleSelection(activeIndex);
172
+ moveUp();
173
+ }
174
+ else {
175
+ toggleSelection(activeIndex);
176
+ moveDown();
177
+ }
178
+ return;
179
+ }
180
+ });
181
+ // Auto-scroll to keep active item visible
182
+ useEffect(() => {
183
+ if (scrollRef.current && filteredItems.length > 0) {
184
+ // Each item is ~1 line tall; approximate scroll
185
+ scrollRef.current.scrollTop = Math.max(0, activeIndex - 5);
186
+ }
187
+ }, [activeIndex, filteredItems.length]);
188
+ if (loading) {
189
+ return (_jsx("box", { children: _jsx("text", { content: "Loading...", fg: theme.textMuted }) }));
190
+ }
191
+ const selectedCount = selectedIndices.size;
192
+ const hintParts = mode === "insert"
193
+ ? ["↑↓ navigate", "Enter confirm", "Esc commands"]
194
+ : ["j/k navigate", "i insert", "Esc quit", "Enter confirm"];
195
+ if (multi && mode === "insert")
196
+ hintParts.splice(2, 0, "Tab toggle");
197
+ const hint = hintParts.join(" ");
198
+ return (_jsx(AppLayout, { titleBar: options?.prompt ? { title: options.prompt } : undefined, statusBar: {
199
+ items: [
200
+ { label: "Matches:", value: `${filteredItems.length}/${items.length}` },
201
+ ...(multi ? [{ label: "Selected:", value: String(selectedCount) }] : []),
202
+ { label: "Theme:", value: themeName },
203
+ { label: "", value: hint },
204
+ ],
205
+ }, scrollRef: scrollRef, scrollProps: { focused: false }, overlay: themePicker.isOpen ? (_jsx(ThemePicker, { entries: themePicker.entries, currentTheme: themeName, onSelect: themePicker.confirm, onClose: themePicker.close, onNavigate: themePicker.preview })) : undefined, children: _jsxs("box", { flexDirection: "column", children: [_jsxs("box", { flexDirection: "row", height: 1, style: { paddingLeft: 1, paddingRight: 1 }, children: [_jsx("text", { content: "> ", fg: theme.accent }), _jsx("input", { focused: mode === "insert", placeholder: options?.placeholder ?? "Filter...", onInput: setFilterQuery, backgroundColor: "transparent", textColor: theme.text, placeholderColor: theme.textMuted, cursorColor: theme.primary, style: { flexGrow: 1 } }), _jsx("text", { content: ` ${filteredItems.length}/${items.length}`, fg: theme.textMuted })] }), filteredItems.map((match, idx) => {
206
+ const isActive = idx === activeIndex;
207
+ const isSelected = selectedIndices.has(match.originalIndex);
208
+ return (_jsxs("box", { flexDirection: "row", height: 1, backgroundColor: isActive ? theme.backgroundElement : undefined, style: { paddingLeft: 1 }, children: [multi && (_jsx("text", { content: isSelected ? "✓ " : " ", fg: isSelected ? theme.accent : theme.textMuted })), match.item.icon && _jsx("text", { content: `${match.item.icon} `, fg: theme.textMuted }), _jsx("text", { fg: isActive ? theme.primary : theme.text, children: renderHighlightedText(match.item.text, match.positions, theme.warning) }), match.item.description && (_jsx("text", { content: ` ${match.item.description}`, fg: theme.textMuted }))] }, match.originalIndex));
209
+ })] }) }));
210
+ }
211
+ function renderHighlightedText(text, positions, highlightColor) {
212
+ if (positions.length === 0) {
213
+ return text;
214
+ }
215
+ const posSet = new Set(positions);
216
+ const parts = [];
217
+ let current = "";
218
+ let currentHighlight = false;
219
+ for (let i = 0; i < text.length; i++) {
220
+ const isHighlight = posSet.has(i);
221
+ if (i === 0) {
222
+ currentHighlight = isHighlight;
223
+ current = text[i];
224
+ }
225
+ else if (isHighlight === currentHighlight) {
226
+ current += text[i];
227
+ }
228
+ else {
229
+ parts.push({ text: current, highlight: currentHighlight });
230
+ current = text[i];
231
+ currentHighlight = isHighlight;
232
+ }
233
+ }
234
+ if (current) {
235
+ parts.push({ text: current, highlight: currentHighlight });
236
+ }
237
+ return parts.map((part, i) => part.highlight ? (_jsx("span", { fg: highlightColor, children: part.text }, i)) : (part.text));
238
+ }
239
+ //# sourceMappingURL=Choose.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Choose.js","sourceRoot":"","sources":["../src/Choose.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { ChooseItem, ChooseContentProvider } from "./types.js";
2
+ export declare function createStdinChooseProvider(): ChooseContentProvider;
3
+ export declare function createStaticProvider(items: ChooseItem[]): ChooseContentProvider;
4
+ //# sourceMappingURL=default-provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"default-provider.d.ts","sourceRoot":"","sources":["../src/default-provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAEnE,wBAAgB,yBAAyB,IAAI,qBAAqB,CAUjE;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,qBAAqB,CAI/E"}
@@ -0,0 +1,17 @@
1
+ export function createStdinChooseProvider() {
2
+ return {
3
+ async load() {
4
+ const text = await new Response(Bun.stdin.stream()).text();
5
+ return text
6
+ .split("\n")
7
+ .filter((line) => line.length > 0)
8
+ .map((line) => ({ text: line }));
9
+ },
10
+ };
11
+ }
12
+ export function createStaticProvider(items) {
13
+ return {
14
+ load: () => items,
15
+ };
16
+ }
17
+ //# sourceMappingURL=default-provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"default-provider.js","sourceRoot":"","sources":["../src/default-provider.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,yBAAyB;IACvC,OAAO;QACL,KAAK,CAAC,IAAI;YACR,MAAM,IAAI,GAAG,MAAM,IAAI,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,EAA+B,CAAC,CAAC,IAAI,EAAE,CAAA;YACvF,OAAO,IAAI;iBACR,KAAK,CAAC,IAAI,CAAC;iBACX,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;iBACjC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;QACpC,CAAC;KACF,CAAA;AACH,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,KAAmB;IACtD,OAAO;QACL,IAAI,EAAE,GAAG,EAAE,CAAC,KAAK;KAClB,CAAA;AACH,CAAC"}
@@ -0,0 +1,9 @@
1
+ import type { ChooseItem } from "./types.js";
2
+ export interface FuzzyMatch {
3
+ item: ChooseItem;
4
+ originalIndex: number;
5
+ score: number;
6
+ positions: number[];
7
+ }
8
+ export declare function fuzzyFilter(items: ChooseItem[], query: string): FuzzyMatch[];
9
+ //# sourceMappingURL=fuzzy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fuzzy.d.ts","sourceRoot":"","sources":["../src/fuzzy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,UAAU,CAAA;IAChB,aAAa,EAAE,MAAM,CAAA;IACrB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,EAAE,CAAA;CACpB;AAID,wBAAgB,WAAW,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,UAAU,EAAE,CAoB5E"}
package/dist/fuzzy.js ADDED
@@ -0,0 +1,43 @@
1
+ const WORD_BOUNDARY_CHARS = new Set([" ", "-", "_", ".", "/"]);
2
+ export function fuzzyFilter(items, query) {
3
+ if (!query) {
4
+ return items.map((item, i) => ({ item, originalIndex: i, score: 0, positions: [] }));
5
+ }
6
+ const lowerQuery = query.toLowerCase();
7
+ const results = [];
8
+ for (let i = 0; i < items.length; i++) {
9
+ const item = items[i];
10
+ const text = item.text;
11
+ const lowerText = text.toLowerCase();
12
+ const match = matchFuzzy(lowerText, lowerQuery);
13
+ if (match) {
14
+ results.push({ item, originalIndex: i, score: match.score, positions: match.positions });
15
+ }
16
+ }
17
+ results.sort((a, b) => b.score - a.score);
18
+ return results;
19
+ }
20
+ function matchFuzzy(text, query) {
21
+ const positions = [];
22
+ let score = 0;
23
+ let qi = 0;
24
+ for (let ti = 0; ti < text.length && qi < query.length; ti++) {
25
+ if (text[ti] === query[qi]) {
26
+ positions.push(ti);
27
+ if (ti === 0) {
28
+ score += 3;
29
+ }
30
+ else if (WORD_BOUNDARY_CHARS.has(text[ti - 1])) {
31
+ score += 2;
32
+ }
33
+ else if (positions.length > 1 && positions[positions.length - 2] === ti - 1) {
34
+ score += 1;
35
+ }
36
+ qi++;
37
+ }
38
+ }
39
+ if (qi < query.length)
40
+ return null;
41
+ return { score, positions };
42
+ }
43
+ //# sourceMappingURL=fuzzy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fuzzy.js","sourceRoot":"","sources":["../src/fuzzy.ts"],"names":[],"mappings":"AASA,MAAM,mBAAmB,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAA;AAE9D,MAAM,UAAU,WAAW,CAAC,KAAmB,EAAE,KAAa;IAC5D,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC,CAAC,CAAA;IACtF,CAAC;IAED,MAAM,UAAU,GAAG,KAAK,CAAC,WAAW,EAAE,CAAA;IACtC,MAAM,OAAO,GAAiB,EAAE,CAAA;IAEhC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;QACrB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAA;QACtB,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,EAAE,CAAA;QACpC,MAAM,KAAK,GAAG,UAAU,CAAC,SAAS,EAAE,UAAU,CAAC,CAAA;QAC/C,IAAI,KAAK,EAAE,CAAC;YACV,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC,CAAA;QAC1F,CAAC;IACH,CAAC;IAED,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAA;IACzC,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,SAAS,UAAU,CAAC,IAAY,EAAE,KAAa;IAC7C,MAAM,SAAS,GAAa,EAAE,CAAA;IAC9B,IAAI,KAAK,GAAG,CAAC,CAAA;IACb,IAAI,EAAE,GAAG,CAAC,CAAA;IAEV,KAAK,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,IAAI,CAAC,MAAM,IAAI,EAAE,GAAG,KAAK,CAAC,MAAM,EAAE,EAAE,EAAE,EAAE,CAAC;QAC7D,IAAI,IAAI,CAAC,EAAE,CAAC,KAAK,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC;YAC3B,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;YAElB,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC;gBACb,KAAK,IAAI,CAAC,CAAA;YACZ,CAAC;iBAAM,IAAI,mBAAmB,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;gBACjD,KAAK,IAAI,CAAC,CAAA;YACZ,CAAC;iBAAM,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,IAAI,SAAS,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,EAAE,GAAG,CAAC,EAAE,CAAC;gBAC9E,KAAK,IAAI,CAAC,CAAA;YACZ,CAAC;YAED,EAAE,EAAE,CAAA;QACN,CAAC;IACH,CAAC;IAED,IAAI,EAAE,GAAG,KAAK,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IAClC,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,CAAA;AAC7B,CAAC"}
@@ -0,0 +1,8 @@
1
+ export { Choose } from "./Choose.jsx";
2
+ export { launch } from "./launch.jsx";
3
+ export type { ChooseLaunchOptions } from "./launch.jsx";
4
+ export { createStdinChooseProvider, createStaticProvider } from "./default-provider.js";
5
+ export { fuzzyFilter } from "./fuzzy.js";
6
+ export type { FuzzyMatch } from "./fuzzy.js";
7
+ export type { ChooseItem, ChooseContentProvider, ChooseResult, ChooseOptions } from "./types.js";
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AACrC,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AACrC,YAAY,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AACvD,OAAO,EAAE,yBAAyB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AACvF,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AACxC,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAC5C,YAAY,EAAE,UAAU,EAAE,qBAAqB,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { Choose } from "./Choose.jsx";
2
+ export { launch } from "./launch.jsx";
3
+ export { createStdinChooseProvider, createStaticProvider } from "./default-provider.js";
4
+ export { fuzzyFilter } from "./fuzzy.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AACrC,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAErC,OAAO,EAAE,yBAAyB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AACvF,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA"}
@@ -0,0 +1,9 @@
1
+ import type { ActionDefinition } from "@tooee/commands";
2
+ import type { ChooseContentProvider, ChooseOptions, ChooseResult } from "./types.js";
3
+ export interface ChooseLaunchOptions {
4
+ contentProvider: ChooseContentProvider;
5
+ options?: ChooseOptions;
6
+ actions?: ActionDefinition[];
7
+ }
8
+ export declare function launch(opts: ChooseLaunchOptions): Promise<ChooseResult | null>;
9
+ //# sourceMappingURL=launch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"launch.d.ts","sourceRoot":"","sources":["../src/launch.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAEvD,OAAO,KAAK,EAAE,qBAAqB,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAEpF,MAAM,WAAW,mBAAmB;IAClC,eAAe,EAAE,qBAAqB,CAAA;IACtC,OAAO,CAAC,EAAE,aAAa,CAAA;IACvB,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAA;CAC7B;AAED,wBAAsB,MAAM,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CA0CpF"}
package/dist/launch.js ADDED
@@ -0,0 +1,38 @@
1
+ import { jsx as _jsx } from "@opentui/react/jsx-runtime";
2
+ import * as fs from "node:fs";
3
+ import * as tty from "node:tty";
4
+ import { createCliRenderer } from "@opentui/core";
5
+ import { createRoot } from "@opentui/react";
6
+ import { TooeeProvider } from "@tooee/shell";
7
+ import { Choose } from "./Choose.jsx";
8
+ export async function launch(opts) {
9
+ // When stdin is piped (not a TTY), open /dev/tty for keyboard input
10
+ // so the renderer doesn't consume the piped data stream.
11
+ const stdinIsPiped = !process.stdin.isTTY;
12
+ let ttyStdin;
13
+ if (stdinIsPiped) {
14
+ const fd = fs.openSync("/dev/tty", "r");
15
+ ttyStdin = new tty.ReadStream(fd);
16
+ }
17
+ return new Promise((resolve) => {
18
+ let renderer = null;
19
+ const cleanup = (result) => {
20
+ if (renderer) {
21
+ renderer.destroy();
22
+ }
23
+ if (ttyStdin) {
24
+ ttyStdin.destroy();
25
+ }
26
+ resolve(result);
27
+ };
28
+ createCliRenderer({
29
+ useAlternateScreen: true,
30
+ exitOnCtrlC: false,
31
+ ...(ttyStdin ? { stdin: ttyStdin } : {}),
32
+ }).then((r) => {
33
+ renderer = r;
34
+ createRoot(r).render(_jsx(TooeeProvider, { initialMode: "insert", children: _jsx(Choose, { contentProvider: opts.contentProvider, options: opts.options, actions: opts.actions, onConfirm: (result) => cleanup(result), onCancel: () => cleanup(null) }) }));
35
+ });
36
+ });
37
+ }
38
+ //# sourceMappingURL=launch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"launch.js","sourceRoot":"","sources":["../src/launch.tsx"],"names":[],"mappings":";AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAA;AAC7B,OAAO,KAAK,GAAG,MAAM,UAAU,CAAA;AAC/B,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAE5C,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AASrC,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,IAAyB;IACpD,oEAAoE;IACpE,yDAAyD;IACzD,MAAM,YAAY,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAA;IACzC,IAAI,QAAoC,CAAA;IACxC,IAAI,YAAY,EAAE,CAAC;QACjB,MAAM,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,UAAU,EAAE,GAAG,CAAC,CAAA;QACvC,QAAQ,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC,CAAA;IACnC,CAAC;IAED,OAAO,IAAI,OAAO,CAAsB,CAAC,OAAO,EAAE,EAAE;QAClD,IAAI,QAAQ,GAAyD,IAAI,CAAA;QAEzE,MAAM,OAAO,GAAG,CAAC,MAA2B,EAAE,EAAE;YAC9C,IAAI,QAAQ,EAAE,CAAC;gBACb,QAAQ,CAAC,OAAO,EAAE,CAAA;YACpB,CAAC;YACD,IAAI,QAAQ,EAAE,CAAC;gBACb,QAAQ,CAAC,OAAO,EAAE,CAAA;YACpB,CAAC;YACD,OAAO,CAAC,MAAM,CAAC,CAAA;QACjB,CAAC,CAAA;QAED,iBAAiB,CAAC;YAChB,kBAAkB,EAAE,IAAI;YACxB,WAAW,EAAE,KAAK;YAClB,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,QAAwC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACzE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE;YACZ,QAAQ,GAAG,CAAC,CAAA;YACZ,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAClB,KAAC,aAAa,IAAC,WAAW,EAAC,QAAQ,YACjC,KAAC,MAAM,IACL,eAAe,EAAE,IAAI,CAAC,eAAe,EACrC,OAAO,EAAE,IAAI,CAAC,OAAO,EACrB,OAAO,EAAE,IAAI,CAAC,OAAO,EACrB,SAAS,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,EACtC,QAAQ,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,GAC7B,GACY,CACjB,CAAA;QACH,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;AACJ,CAAC"}
@@ -0,0 +1,18 @@
1
+ export interface ChooseItem {
2
+ text: string;
3
+ value?: string;
4
+ icon?: string;
5
+ description?: string;
6
+ }
7
+ export interface ChooseContentProvider {
8
+ load(): Promise<ChooseItem[]> | ChooseItem[];
9
+ }
10
+ export interface ChooseResult {
11
+ items: ChooseItem[];
12
+ }
13
+ export interface ChooseOptions {
14
+ multi?: boolean;
15
+ prompt?: string;
16
+ placeholder?: string;
17
+ }
18
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC,GAAG,UAAU,EAAE,CAAA;CAC7C;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,UAAU,EAAE,CAAA;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@tooee/choose",
3
+ "version": "0.1.0",
4
+ "description": "Terminal selection with fuzzy filtering",
5
+ "license": "MIT",
6
+ "author": "Gareth Andrew",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/gingerhendrix/tooee.git",
10
+ "directory": "packages/choose"
11
+ },
12
+ "homepage": "https://github.com/gingerhendrix/tooee",
13
+ "bugs": "https://github.com/gingerhendrix/tooee/issues",
14
+ "keywords": ["tui", "terminal", "cli", "opentui", "fuzzy", "select", "picker"],
15
+ "type": "module",
16
+ "exports": {
17
+ ".": {
18
+ "import": {
19
+ "@tooee/source": "./src/index.ts",
20
+ "default": "./dist/index.js"
21
+ }
22
+ }
23
+ },
24
+ "files": ["dist", "src"],
25
+ "scripts": {
26
+ "typecheck": "tsc --noEmit"
27
+ },
28
+ "dependencies": {
29
+ "@tooee/commands": "0.0.0",
30
+ "@tooee/layout": "0.0.0",
31
+ "@tooee/themes": "0.0.0",
32
+ "@tooee/shell": "0.0.0"
33
+ },
34
+ "devDependencies": {
35
+ "@opentui/core": "^0.1.67",
36
+ "@opentui/react": "^0.1.67",
37
+ "@types/bun": "^1.3.5",
38
+ "@types/react": "^19.1.10",
39
+ "typescript": "^5.8.3"
40
+ },
41
+ "peerDependencies": {
42
+ "@opentui/core": "^0.1.67",
43
+ "@opentui/react": "^0.1.67",
44
+ "react": "^18.0.0 || ^19.0.0"
45
+ }
46
+ }
package/src/Choose.tsx ADDED
@@ -0,0 +1,340 @@
1
+ import { useState, useEffect, useRef, useCallback } from "react"
2
+ import type { ScrollBoxRenderable } from "@opentui/core"
3
+ import { useKeyboard } from "@opentui/react"
4
+ import { AppLayout } from "@tooee/layout"
5
+ import { ThemePicker, useTheme } from "@tooee/themes"
6
+ import { useThemeCommands } from "@tooee/shell"
7
+ import { useMode, useSetMode, useCommand, useActions, useProvideCommandContext, useCommandContext } from "@tooee/commands"
8
+ import type { ActionDefinition } from "@tooee/commands"
9
+ import type { ChooseItem, ChooseContentProvider, ChooseOptions, ChooseResult } from "./types.js"
10
+ import { fuzzyFilter, type FuzzyMatch } from "./fuzzy.js"
11
+
12
+ interface ChooseProps {
13
+ contentProvider: ChooseContentProvider
14
+ options?: ChooseOptions
15
+ actions?: ActionDefinition[]
16
+ /** @deprecated Use actions instead */
17
+ onConfirm?: (result: ChooseResult) => void
18
+ /** @deprecated Use actions instead */
19
+ onCancel?: () => void
20
+ }
21
+
22
+ export function Choose({ contentProvider, options, actions, onConfirm, onCancel }: ChooseProps) {
23
+ const { theme } = useTheme()
24
+ const [items, setItems] = useState<ChooseItem[]>([])
25
+ const [filterQuery, setFilterQuery] = useState("")
26
+ const [filteredItems, setFilteredItems] = useState<FuzzyMatch[]>([])
27
+ const [activeIndex, setActiveIndex] = useState(0)
28
+ const [selectedIndices, setSelectedIndices] = useState<Set<number>>(new Set())
29
+ const [loading, setLoading] = useState(true)
30
+ const scrollRef = useRef<ScrollBoxRenderable>(null)
31
+ const { invoke } = useCommandContext()
32
+
33
+ const multi = options?.multi ?? false
34
+
35
+ useEffect(() => {
36
+ const result = contentProvider.load()
37
+ if (result instanceof Promise) {
38
+ result.then((loaded) => {
39
+ setItems(loaded)
40
+ setFilteredItems(fuzzyFilter(loaded, ""))
41
+ setLoading(false)
42
+ })
43
+ } else {
44
+ setItems(result)
45
+ setFilteredItems(fuzzyFilter(result, ""))
46
+ setLoading(false)
47
+ }
48
+ }, [contentProvider])
49
+
50
+ useEffect(() => {
51
+ const matches = fuzzyFilter(items, filterQuery)
52
+ setFilteredItems(matches)
53
+ setActiveIndex(0)
54
+ }, [filterQuery, items])
55
+
56
+ const { name: themeName, picker: themePicker } = useThemeCommands()
57
+ const mode = useMode()
58
+ const setMode = useSetMode()
59
+
60
+ // Compute selected items for context
61
+ const getSelectedItems = useCallback((): ChooseItem[] => {
62
+ if (multi) {
63
+ const selected = Array.from(selectedIndices).map((i) => items[i])
64
+ if (selected.length === 0 && filteredItems[activeIndex]) {
65
+ return [filteredItems[activeIndex].item]
66
+ }
67
+ return selected
68
+ }
69
+ const match = filteredItems[activeIndex]
70
+ return match ? [match.item] : []
71
+ }, [multi, selectedIndices, items, filteredItems, activeIndex])
72
+
73
+ useProvideCommandContext(() => ({
74
+ choose: {
75
+ activeItem: filteredItems[activeIndex]?.item,
76
+ selectedItems: getSelectedItems(),
77
+ filterQuery,
78
+ },
79
+ exit: () => onCancel?.(),
80
+ }))
81
+
82
+ useActions(actions)
83
+
84
+ const moveUp = useCallback(() => {
85
+ setActiveIndex((i) => Math.max(0, i - 1))
86
+ }, [])
87
+
88
+ const moveDown = useCallback(() => {
89
+ setActiveIndex((i) => Math.min(filteredItems.length - 1, i + 1))
90
+ }, [filteredItems.length])
91
+
92
+ // Register a/i to return to insert mode from cursor mode
93
+ useCommand({
94
+ id: "choose:insert-mode-a",
95
+ title: "Insert mode",
96
+ hotkey: "a",
97
+ modes: ["cursor"],
98
+ handler: () => setMode("insert"),
99
+ hidden: true,
100
+ })
101
+ useCommand({
102
+ id: "choose:insert-mode-i",
103
+ title: "Insert mode",
104
+ hotkey: "i",
105
+ modes: ["cursor"],
106
+ handler: () => setMode("insert"),
107
+ hidden: true,
108
+ })
109
+
110
+ // Register j/k for vim-style navigation in cursor mode
111
+ useCommand({
112
+ id: "choose:move-down",
113
+ title: "Move down",
114
+ hotkey: "j",
115
+ modes: ["cursor"],
116
+ handler: moveDown,
117
+ hidden: true,
118
+ })
119
+ useCommand({
120
+ id: "choose:move-up",
121
+ title: "Move up",
122
+ hotkey: "k",
123
+ modes: ["cursor"],
124
+ handler: moveUp,
125
+ hidden: true,
126
+ })
127
+
128
+ const toggleSelection = useCallback(
129
+ (index: number) => {
130
+ setSelectedIndices((prev) => {
131
+ const next = new Set(prev)
132
+ const origIndex = filteredItems[index]?.originalIndex
133
+ if (origIndex === undefined) return prev
134
+ if (next.has(origIndex)) {
135
+ next.delete(origIndex)
136
+ } else {
137
+ next.add(origIndex)
138
+ }
139
+ return next
140
+ })
141
+ },
142
+ [filteredItems],
143
+ )
144
+
145
+ const confirm = useCallback(() => {
146
+ // If there's a "submit" action, invoke it via the command system
147
+ if (actions?.some((a) => a.id === "submit")) {
148
+ invoke("submit")
149
+ return
150
+ }
151
+
152
+ // Legacy: use onConfirm/onCancel callbacks
153
+ if (multi) {
154
+ const selected = Array.from(selectedIndices).map((i) => items[i])
155
+ if (selected.length === 0 && filteredItems[activeIndex]) {
156
+ onConfirm?.({ items: [filteredItems[activeIndex].item] })
157
+ } else {
158
+ onConfirm?.({ items: selected })
159
+ }
160
+ } else {
161
+ const match = filteredItems[activeIndex]
162
+ if (match) {
163
+ onConfirm?.({ items: [match.item] })
164
+ } else {
165
+ onCancel?.()
166
+ }
167
+ }
168
+ }, [multi, selectedIndices, items, filteredItems, activeIndex, onConfirm, onCancel, actions, invoke])
169
+
170
+ useKeyboard((key) => {
171
+ if (key.name === "escape") {
172
+ if (mode === "insert") {
173
+ // Switch to cursor mode (allows theme switching, quit, etc.)
174
+ setMode("cursor")
175
+ } else {
176
+ // In cursor mode, escape cancels
177
+ onCancel?.()
178
+ }
179
+ return
180
+ }
181
+ if (key.name === "return") {
182
+ confirm()
183
+ return
184
+ }
185
+ if (key.name === "up" || (key.ctrl && key.name === "p")) {
186
+ moveUp()
187
+ return
188
+ }
189
+ if (key.name === "down" || (key.ctrl && key.name === "n")) {
190
+ moveDown()
191
+ return
192
+ }
193
+ if (multi && key.name === "tab") {
194
+ if (key.shift) {
195
+ toggleSelection(activeIndex)
196
+ moveUp()
197
+ } else {
198
+ toggleSelection(activeIndex)
199
+ moveDown()
200
+ }
201
+ return
202
+ }
203
+ })
204
+
205
+ // Auto-scroll to keep active item visible
206
+ useEffect(() => {
207
+ if (scrollRef.current && filteredItems.length > 0) {
208
+ // Each item is ~1 line tall; approximate scroll
209
+ scrollRef.current.scrollTop = Math.max(0, activeIndex - 5)
210
+ }
211
+ }, [activeIndex, filteredItems.length])
212
+
213
+ if (loading) {
214
+ return (
215
+ <box>
216
+ <text content="Loading..." fg={theme.textMuted} />
217
+ </box>
218
+ )
219
+ }
220
+
221
+ const selectedCount = selectedIndices.size
222
+ const hintParts =
223
+ mode === "insert"
224
+ ? ["↑↓ navigate", "Enter confirm", "Esc commands"]
225
+ : ["j/k navigate", "i insert", "Esc quit", "Enter confirm"]
226
+ if (multi && mode === "insert") hintParts.splice(2, 0, "Tab toggle")
227
+ const hint = hintParts.join(" ")
228
+
229
+ return (
230
+ <AppLayout
231
+ titleBar={options?.prompt ? { title: options.prompt } : undefined}
232
+ statusBar={{
233
+ items: [
234
+ { label: "Matches:", value: `${filteredItems.length}/${items.length}` },
235
+ ...(multi ? [{ label: "Selected:", value: String(selectedCount) }] : []),
236
+ { label: "Theme:", value: themeName },
237
+ { label: "", value: hint },
238
+ ],
239
+ }}
240
+ scrollRef={scrollRef}
241
+ scrollProps={{ focused: false }}
242
+ overlay={
243
+ themePicker.isOpen ? (
244
+ <ThemePicker
245
+ entries={themePicker.entries}
246
+ currentTheme={themeName}
247
+ onSelect={themePicker.confirm}
248
+ onClose={themePicker.close}
249
+ onNavigate={themePicker.preview}
250
+ />
251
+ ) : undefined
252
+ }
253
+ >
254
+ <box flexDirection="column">
255
+ {/* Filter input row */}
256
+ <box flexDirection="row" height={1} style={{ paddingLeft: 1, paddingRight: 1 }}>
257
+ <text content="> " fg={theme.accent} />
258
+ <input
259
+ focused={mode === "insert"}
260
+ placeholder={options?.placeholder ?? "Filter..."}
261
+ onInput={setFilterQuery}
262
+ backgroundColor="transparent"
263
+ textColor={theme.text}
264
+ placeholderColor={theme.textMuted}
265
+ cursorColor={theme.primary}
266
+ style={{ flexGrow: 1 }}
267
+ />
268
+ <text content={` ${filteredItems.length}/${items.length}`} fg={theme.textMuted} />
269
+ </box>
270
+
271
+ {/* Item list */}
272
+ {filteredItems.map((match, idx) => {
273
+ const isActive = idx === activeIndex
274
+ const isSelected = selectedIndices.has(match.originalIndex)
275
+ return (
276
+ <box
277
+ key={match.originalIndex}
278
+ flexDirection="row"
279
+ height={1}
280
+ backgroundColor={isActive ? theme.backgroundElement : undefined}
281
+ style={{ paddingLeft: 1 }}
282
+ >
283
+ {multi && (
284
+ <text
285
+ content={isSelected ? "✓ " : " "}
286
+ fg={isSelected ? theme.accent : theme.textMuted}
287
+ />
288
+ )}
289
+ {match.item.icon && <text content={`${match.item.icon} `} fg={theme.textMuted} />}
290
+ <text fg={isActive ? theme.primary : theme.text}>
291
+ {renderHighlightedText(match.item.text, match.positions, theme.warning)}
292
+ </text>
293
+ {match.item.description && (
294
+ <text content={` ${match.item.description}`} fg={theme.textMuted} />
295
+ )}
296
+ </box>
297
+ )
298
+ })}
299
+ </box>
300
+ </AppLayout>
301
+ )
302
+ }
303
+
304
+ function renderHighlightedText(text: string, positions: number[], highlightColor: string) {
305
+ if (positions.length === 0) {
306
+ return text
307
+ }
308
+
309
+ const posSet = new Set(positions)
310
+ const parts: Array<{ text: string; highlight: boolean }> = []
311
+ let current = ""
312
+ let currentHighlight = false
313
+
314
+ for (let i = 0; i < text.length; i++) {
315
+ const isHighlight = posSet.has(i)
316
+ if (i === 0) {
317
+ currentHighlight = isHighlight
318
+ current = text[i]
319
+ } else if (isHighlight === currentHighlight) {
320
+ current += text[i]
321
+ } else {
322
+ parts.push({ text: current, highlight: currentHighlight })
323
+ current = text[i]
324
+ currentHighlight = isHighlight
325
+ }
326
+ }
327
+ if (current) {
328
+ parts.push({ text: current, highlight: currentHighlight })
329
+ }
330
+
331
+ return parts.map((part, i) =>
332
+ part.highlight ? (
333
+ <span key={i} fg={highlightColor}>
334
+ {part.text}
335
+ </span>
336
+ ) : (
337
+ part.text
338
+ ),
339
+ )
340
+ }
@@ -0,0 +1,19 @@
1
+ import type { ChooseItem, ChooseContentProvider } from "./types.js"
2
+
3
+ export function createStdinChooseProvider(): ChooseContentProvider {
4
+ return {
5
+ async load(): Promise<ChooseItem[]> {
6
+ const text = await new Response(Bun.stdin.stream() as unknown as ReadableStream).text()
7
+ return text
8
+ .split("\n")
9
+ .filter((line) => line.length > 0)
10
+ .map((line) => ({ text: line }))
11
+ },
12
+ }
13
+ }
14
+
15
+ export function createStaticProvider(items: ChooseItem[]): ChooseContentProvider {
16
+ return {
17
+ load: () => items,
18
+ }
19
+ }
package/src/fuzzy.ts ADDED
@@ -0,0 +1,57 @@
1
+ import type { ChooseItem } from "./types.js"
2
+
3
+ export interface FuzzyMatch {
4
+ item: ChooseItem
5
+ originalIndex: number
6
+ score: number
7
+ positions: number[]
8
+ }
9
+
10
+ const WORD_BOUNDARY_CHARS = new Set([" ", "-", "_", ".", "/"])
11
+
12
+ export function fuzzyFilter(items: ChooseItem[], query: string): FuzzyMatch[] {
13
+ if (!query) {
14
+ return items.map((item, i) => ({ item, originalIndex: i, score: 0, positions: [] }))
15
+ }
16
+
17
+ const lowerQuery = query.toLowerCase()
18
+ const results: FuzzyMatch[] = []
19
+
20
+ for (let i = 0; i < items.length; i++) {
21
+ const item = items[i]
22
+ const text = item.text
23
+ const lowerText = text.toLowerCase()
24
+ const match = matchFuzzy(lowerText, lowerQuery)
25
+ if (match) {
26
+ results.push({ item, originalIndex: i, score: match.score, positions: match.positions })
27
+ }
28
+ }
29
+
30
+ results.sort((a, b) => b.score - a.score)
31
+ return results
32
+ }
33
+
34
+ function matchFuzzy(text: string, query: string): { score: number; positions: number[] } | null {
35
+ const positions: number[] = []
36
+ let score = 0
37
+ let qi = 0
38
+
39
+ for (let ti = 0; ti < text.length && qi < query.length; ti++) {
40
+ if (text[ti] === query[qi]) {
41
+ positions.push(ti)
42
+
43
+ if (ti === 0) {
44
+ score += 3
45
+ } else if (WORD_BOUNDARY_CHARS.has(text[ti - 1])) {
46
+ score += 2
47
+ } else if (positions.length > 1 && positions[positions.length - 2] === ti - 1) {
48
+ score += 1
49
+ }
50
+
51
+ qi++
52
+ }
53
+ }
54
+
55
+ if (qi < query.length) return null
56
+ return { score, positions }
57
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export { Choose } from "./Choose.jsx"
2
+ export { launch } from "./launch.jsx"
3
+ export type { ChooseLaunchOptions } from "./launch.jsx"
4
+ export { createStdinChooseProvider, createStaticProvider } from "./default-provider.js"
5
+ export { fuzzyFilter } from "./fuzzy.js"
6
+ export type { FuzzyMatch } from "./fuzzy.js"
7
+ export type { ChooseItem, ChooseContentProvider, ChooseResult, ChooseOptions } from "./types.js"
package/src/launch.tsx ADDED
@@ -0,0 +1,58 @@
1
+ import * as fs from "node:fs"
2
+ import * as tty from "node:tty"
3
+ import { createCliRenderer } from "@opentui/core"
4
+ import { createRoot } from "@opentui/react"
5
+ import { TooeeProvider } from "@tooee/shell"
6
+ import type { ActionDefinition } from "@tooee/commands"
7
+ import { Choose } from "./Choose.jsx"
8
+ import type { ChooseContentProvider, ChooseOptions, ChooseResult } from "./types.js"
9
+
10
+ export interface ChooseLaunchOptions {
11
+ contentProvider: ChooseContentProvider
12
+ options?: ChooseOptions
13
+ actions?: ActionDefinition[]
14
+ }
15
+
16
+ export async function launch(opts: ChooseLaunchOptions): Promise<ChooseResult | null> {
17
+ // When stdin is piped (not a TTY), open /dev/tty for keyboard input
18
+ // so the renderer doesn't consume the piped data stream.
19
+ const stdinIsPiped = !process.stdin.isTTY
20
+ let ttyStdin: tty.ReadStream | undefined
21
+ if (stdinIsPiped) {
22
+ const fd = fs.openSync("/dev/tty", "r")
23
+ ttyStdin = new tty.ReadStream(fd)
24
+ }
25
+
26
+ return new Promise<ChooseResult | null>((resolve) => {
27
+ let renderer: Awaited<ReturnType<typeof createCliRenderer>> | null = null
28
+
29
+ const cleanup = (result: ChooseResult | null) => {
30
+ if (renderer) {
31
+ renderer.destroy()
32
+ }
33
+ if (ttyStdin) {
34
+ ttyStdin.destroy()
35
+ }
36
+ resolve(result)
37
+ }
38
+
39
+ createCliRenderer({
40
+ useAlternateScreen: true,
41
+ exitOnCtrlC: false,
42
+ ...(ttyStdin ? { stdin: ttyStdin as unknown as NodeJS.ReadStream } : {}),
43
+ }).then((r) => {
44
+ renderer = r
45
+ createRoot(r).render(
46
+ <TooeeProvider initialMode="insert">
47
+ <Choose
48
+ contentProvider={opts.contentProvider}
49
+ options={opts.options}
50
+ actions={opts.actions}
51
+ onConfirm={(result) => cleanup(result)}
52
+ onCancel={() => cleanup(null)}
53
+ />
54
+ </TooeeProvider>,
55
+ )
56
+ })
57
+ })
58
+ }
package/src/types.ts ADDED
@@ -0,0 +1,20 @@
1
+ export interface ChooseItem {
2
+ text: string
3
+ value?: string
4
+ icon?: string
5
+ description?: string
6
+ }
7
+
8
+ export interface ChooseContentProvider {
9
+ load(): Promise<ChooseItem[]> | ChooseItem[]
10
+ }
11
+
12
+ export interface ChooseResult {
13
+ items: ChooseItem[]
14
+ }
15
+
16
+ export interface ChooseOptions {
17
+ multi?: boolean
18
+ prompt?: string
19
+ placeholder?: string
20
+ }