@tooee/choose 0.2.3 → 0.3.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.
Files changed (94) hide show
  1. package/README.md +2 -2
  2. package/dist/{ChooseFilter.d.ts → choose-filter.d.ts} +2 -2
  3. package/dist/choose-filter.d.ts.map +1 -0
  4. package/dist/choose-filter.js +11 -0
  5. package/dist/choose-filter.js.map +1 -0
  6. package/dist/choose-highlighted-text.d.ts +9 -0
  7. package/dist/choose-highlighted-text.d.ts.map +1 -0
  8. package/dist/{ChooseHighlightedText.js → choose-highlighted-text.js} +10 -8
  9. package/dist/choose-highlighted-text.js.map +1 -0
  10. package/dist/{ChooseList.d.ts → choose-list.d.ts} +3 -3
  11. package/dist/choose-list.d.ts.map +1 -0
  12. package/dist/{ChooseList.js → choose-list.js} +20 -15
  13. package/dist/choose-list.js.map +1 -0
  14. package/dist/{ChooseOverlay.d.ts → choose-overlay.d.ts} +6 -6
  15. package/dist/choose-overlay.d.ts.map +1 -0
  16. package/dist/{ChooseOverlay.js → choose-overlay.js} +15 -13
  17. package/dist/choose-overlay.js.map +1 -0
  18. package/dist/{ChoosePanel.d.ts → choose-panel.d.ts} +5 -5
  19. package/dist/choose-panel.d.ts.map +1 -0
  20. package/dist/choose-panel.js +31 -0
  21. package/dist/choose-panel.js.map +1 -0
  22. package/dist/choose.d.ts +28 -0
  23. package/dist/choose.d.ts.map +1 -0
  24. package/dist/{Choose.js → choose.js} +25 -18
  25. package/dist/choose.js.map +1 -0
  26. package/dist/default-provider.d.ts +2 -2
  27. package/dist/default-provider.d.ts.map +1 -1
  28. package/dist/default-provider.js +4 -4
  29. package/dist/default-provider.js.map +1 -1
  30. package/dist/fuzzy.d.ts +1 -1
  31. package/dist/fuzzy.d.ts.map +1 -1
  32. package/dist/fuzzy.js +5 -5
  33. package/dist/fuzzy.js.map +1 -1
  34. package/dist/index.d.ts +12 -12
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +6 -6
  37. package/dist/index.js.map +1 -1
  38. package/dist/launch.d.ts +1 -1
  39. package/dist/launch.d.ts.map +1 -1
  40. package/dist/launch.js +5 -4
  41. package/dist/launch.js.map +1 -1
  42. package/dist/source.d.ts +2 -2
  43. package/dist/source.d.ts.map +1 -1
  44. package/dist/source.js +8 -6
  45. package/dist/source.js.map +1 -1
  46. package/dist/types.d.ts +1 -1
  47. package/dist/types.d.ts.map +1 -1
  48. package/dist/use-choose-dialog.d.ts +12 -10
  49. package/dist/use-choose-dialog.d.ts.map +1 -1
  50. package/dist/use-choose-dialog.js +98 -77
  51. package/dist/use-choose-dialog.js.map +1 -1
  52. package/dist/use-choose-dialog.typecheck.d.ts +1 -1
  53. package/dist/use-choose-dialog.typecheck.d.ts.map +1 -1
  54. package/dist/use-choose-dialog.typecheck.js +5 -5
  55. package/dist/use-choose-dialog.typecheck.js.map +1 -1
  56. package/dist/use-choose.d.ts +16 -16
  57. package/dist/use-choose.d.ts.map +1 -1
  58. package/dist/use-choose.js +176 -138
  59. package/dist/use-choose.js.map +1 -1
  60. package/package.json +7 -7
  61. package/src/{ChooseFilter.tsx → choose-filter.tsx} +18 -16
  62. package/src/choose-highlighted-text.tsx +49 -0
  63. package/src/{ChooseList.tsx → choose-list.tsx} +73 -60
  64. package/src/choose-overlay.tsx +96 -0
  65. package/src/{ChoosePanel.tsx → choose-panel.tsx} +41 -34
  66. package/src/choose.tsx +142 -0
  67. package/src/default-provider.ts +18 -15
  68. package/src/fuzzy.ts +16 -16
  69. package/src/index.ts +23 -23
  70. package/src/launch.tsx +15 -12
  71. package/src/source.ts +15 -9
  72. package/src/types.ts +12 -12
  73. package/src/use-choose-dialog.tsx +168 -139
  74. package/src/use-choose-dialog.typecheck.ts +19 -18
  75. package/src/use-choose.ts +349 -302
  76. package/dist/Choose.d.ts +0 -21
  77. package/dist/Choose.d.ts.map +0 -1
  78. package/dist/Choose.js.map +0 -1
  79. package/dist/ChooseFilter.d.ts.map +0 -1
  80. package/dist/ChooseFilter.js +0 -9
  81. package/dist/ChooseFilter.js.map +0 -1
  82. package/dist/ChooseHighlightedText.d.ts +0 -9
  83. package/dist/ChooseHighlightedText.d.ts.map +0 -1
  84. package/dist/ChooseHighlightedText.js.map +0 -1
  85. package/dist/ChooseList.d.ts.map +0 -1
  86. package/dist/ChooseList.js.map +0 -1
  87. package/dist/ChooseOverlay.d.ts.map +0 -1
  88. package/dist/ChooseOverlay.js.map +0 -1
  89. package/dist/ChoosePanel.d.ts.map +0 -1
  90. package/dist/ChoosePanel.js +0 -25
  91. package/dist/ChoosePanel.js.map +0 -1
  92. package/src/Choose.tsx +0 -126
  93. package/src/ChooseHighlightedText.tsx +0 -42
  94. package/src/ChooseOverlay.tsx +0 -88
package/src/use-choose.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { useCallback, useEffect, useMemo, useRef, useState } from "react"
2
- import type { InputRenderable, KeyEvent } from "@opentui/core"
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import type { InputRenderable, KeyEvent } from "@opentui/core";
3
3
  import {
4
4
  useActiveCommandSurface,
5
5
  useActions,
@@ -8,155 +8,164 @@ import {
8
8
  useMode,
9
9
  useProvideCommandContext,
10
10
  useSetMode,
11
- type ActionDefinition,
12
- type Mode,
13
- } from "@tooee/commands"
14
- import { fuzzyFilter, type FuzzyMatch } from "./fuzzy.js"
15
- import { chooseSourceError, loadChooseSource } from "./source.js"
16
- import type { ChooseItem, ChooseResult, ChooseSource } from "./types.js"
11
+ } from "@tooee/commands";
12
+ import type { ActionDefinition, Mode } from "@tooee/commands";
13
+ import { fuzzyFilter } from "./fuzzy.js";
14
+ import type { FuzzyMatch } from "./fuzzy.js";
15
+ import { chooseSourceError, loadChooseSource } from "./source.js";
16
+ import type { ChooseItem, ChooseResult, ChooseSource } from "./types.js";
17
17
 
18
18
  declare module "@tooee/commands" {
19
19
  interface CommandContext {
20
20
  /** Contributed by chooser primitives: current filtering and selection state. */
21
21
  choose: {
22
- activeItem: ChooseItem | undefined
23
- selectedItems: ChooseItem[]
24
- filterQuery: string
25
- }
22
+ activeItem: ChooseItem | undefined;
23
+ selectedItems: ChooseItem[];
24
+ filterQuery: string;
25
+ };
26
26
  }
27
27
  }
28
28
 
29
- export type ChooseCommandGroup = "navigation" | "mode" | "submit" | "cancel" | "multi-select"
29
+ export type ChooseCommandGroup = "navigation" | "mode" | "submit" | "cancel" | "multi-select";
30
30
 
31
31
  export interface UseChooseOptions {
32
- source: ChooseSource
33
- multi?: boolean
34
- initialFilter?: string
35
- onSubmit: (result: ChooseResult) => void | Promise<void>
36
- onCancel?: () => void
37
- commands?: ActionDefinition[]
32
+ source: ChooseSource;
33
+ multi?: boolean;
34
+ initialFilter?: string;
35
+ onSubmit: (result: ChooseResult) => void | Promise<void>;
36
+ onCancel?: () => void;
37
+ commands?: ActionDefinition[];
38
38
  /** Prefix for built-in command ids (default `choose`). */
39
- commandScope?: string
39
+ commandScope?: string;
40
40
  /** Disable a built-in command group when a host owns those keys. */
41
- disable?: ChooseCommandGroup[]
41
+ disable?: ChooseCommandGroup[];
42
42
  /** Explicitly blur/disable mouse views while a legacy covering surface is open. */
43
- suspended?: boolean
43
+ suspended?: boolean;
44
44
  }
45
45
 
46
46
  export interface ChooseController {
47
- getFilter(): string
48
- setFilter(query: string): void
49
- clearFilter(): void
50
- moveUp(): void
51
- moveDown(): void
52
- setActiveIndex(index: number): void
53
- getActiveItem(): ChooseItem | undefined
54
- getSelectedItems(): ChooseItem[]
55
- toggleActive(): void
56
- submit(): void
57
- cancel(): void
58
- reload(): void
59
- readonly mode: Mode
60
- setMode(mode: Mode): void
47
+ getFilter: () => string;
48
+ setFilter: (query: string) => void;
49
+ clearFilter: () => void;
50
+ moveUp: () => void;
51
+ moveDown: () => void;
52
+ setActiveIndex: (index: number) => void;
53
+ getActiveItem: () => ChooseItem | undefined;
54
+ getSelectedItems: () => ChooseItem[];
55
+ toggleActive: () => void;
56
+ submit: () => void;
57
+ cancel: () => void;
58
+ reload: () => void;
59
+ readonly mode: Mode;
60
+ setMode: (mode: Mode) => void;
61
61
  }
62
62
 
63
63
  export interface ChooseState {
64
- items: ChooseItem[]
65
- matches: FuzzyMatch[]
66
- filterQuery: string
67
- activeIndex: number
68
- activeItem: ChooseItem | undefined
69
- selectedOriginalIndices: ReadonlySet<number>
70
- selectedItems: ChooseItem[]
71
- loading: boolean
72
- error: string | null
73
- multi: boolean
64
+ items: ChooseItem[];
65
+ matches: FuzzyMatch[];
66
+ filterQuery: string;
67
+ activeIndex: number;
68
+ activeItem: ChooseItem | undefined;
69
+ selectedOriginalIndices: ReadonlySet<number>;
70
+ selectedItems: ChooseItem[];
71
+ loading: boolean;
72
+ error: string | null;
73
+ multi: boolean;
74
74
  }
75
75
 
76
76
  export interface ChooseViewModel {
77
- filterRef: { current: InputRenderable | null }
78
- mode: Mode
77
+ filterRef: { current: InputRenderable | null };
78
+ mode: Mode;
79
79
  /** True while another modal surface (or an explicit host guard) owns interaction. */
80
- suspended: boolean
81
- filterFocused: boolean
82
- onFilterInput: (query: string) => void
80
+ suspended: boolean;
81
+ filterFocused: boolean;
82
+ onFilterInput: (query: string) => void;
83
83
  /** Input-level Tab bridge; OpenTUI consumes Tab before global dispatch. */
84
- onFilterKeyDown: (event: KeyEvent) => void
84
+ onFilterKeyDown: (event: KeyEvent) => void;
85
85
  }
86
86
 
87
87
  export interface UseChooseResult {
88
- controller: ChooseController
89
- state: ChooseState
90
- view: ChooseViewModel
88
+ controller: ChooseController;
89
+ state: ChooseState;
90
+ view: ChooseViewModel;
91
91
  }
92
92
 
93
93
  /**
94
94
  * Shared headless chooser: source lifecycle, fuzzy matches, selection,
95
95
  * command context, controller, and all built-in keyboard commands.
96
96
  */
97
- export function useChoose(options: UseChooseOptions): UseChooseResult {
98
- const { source, multi = false, initialFilter = "", commandScope = "choose" } = options
99
- const initialItems = Array.isArray(source) ? source : []
100
-
101
- const [items, setItems] = useState<ChooseItem[]>(initialItems)
102
- const [filterQuery, setFilterQuery] = useState(initialFilter)
103
- const [activeIndex, setActiveIndexState] = useState(0)
104
- const [selectedOriginalIndices, setSelectedOriginalIndices] = useState<Set<number>>(new Set())
105
- const [loading, setLoading] = useState(!Array.isArray(source))
106
- const [error, setError] = useState<string | null>(null)
107
- const [reloadRevision, setReloadRevision] = useState(0)
108
- const filterRef = useRef<InputRenderable>(null)
109
- const requestIdRef = useRef(0)
110
- const initialArraySourceRef = useRef<ChooseItem[] | null>(Array.isArray(source) ? source : null)
111
- const didHandleInitialSourceRef = useRef(false)
112
-
113
- const matches = useMemo(() => fuzzyFilter(items, filterQuery), [items, filterQuery])
114
- const activeItem = matches[activeIndex]?.item
97
+ export const useChoose = function useChoose(options: UseChooseOptions): UseChooseResult {
98
+ const { source, multi = false, initialFilter = "", commandScope = "choose" } = options;
99
+ const initialItems = Array.isArray(source) ? source : [];
100
+
101
+ const [items, setItems] = useState<ChooseItem[]>(initialItems);
102
+ const [filterQuery, setFilterQuery] = useState(initialFilter);
103
+ const [activeIndex, setActiveIndex] = useState(0);
104
+ const [selectedOriginalIndices, setSelectedOriginalIndices] = useState<Set<number>>(new Set());
105
+ const [loading, setLoading] = useState(!Array.isArray(source));
106
+ const [error, setError] = useState<string | null>(null);
107
+ const [reloadRevision, setReloadRevision] = useState(0);
108
+ const filterRef = useRef<InputRenderable>(null);
109
+ const requestIdRef = useRef(0);
110
+ const initialArraySourceRef = useRef<ChooseItem[] | null>(Array.isArray(source) ? source : null);
111
+ const didHandleInitialSourceRef = useRef(false);
112
+
113
+ const matches = useMemo(() => fuzzyFilter(items, filterQuery), [items, filterQuery]);
114
+ const activeItem = matches[activeIndex]?.item;
115
115
  const selectedItems = useMemo(() => {
116
- if (!multi) return activeItem ? [activeItem] : []
117
- const selected = Array.from(selectedOriginalIndices).flatMap((index) => {
118
- const item = items[index]
119
- return item ? [item] : []
120
- })
121
- return selected.length > 0 ? selected : activeItem ? [activeItem] : []
122
- }, [multi, activeItem, selectedOriginalIndices, items])
116
+ if (!multi) {
117
+ return activeItem === undefined ? [] : [activeItem];
118
+ }
119
+ const selected = [...selectedOriginalIndices].flatMap((index) => {
120
+ const item = items[index];
121
+ return item === undefined ? [] : [item];
122
+ });
123
+ if (selected.length > 0) {
124
+ return selected;
125
+ }
126
+ return activeItem === undefined ? [] : [activeItem];
127
+ }, [multi, activeItem, selectedOriginalIndices, items]);
123
128
 
124
- const mode = useMode()
125
- const setMode = useSetMode()
129
+ const mode = useMode();
130
+ const setMode = useSetMode();
126
131
 
127
132
  // Stable controller methods read live values through refs, including when
128
133
  // multiple imperative operations happen before React renders again.
129
- const optionsRef = useRef(options)
130
- optionsRef.current = options
131
- const itemsRef = useRef(items)
132
- itemsRef.current = items
133
- const matchesRef = useRef(matches)
134
- matchesRef.current = matches
135
- const filterQueryRef = useRef(filterQuery)
136
- filterQueryRef.current = filterQuery
137
- const activeIndexRef = useRef(activeIndex)
138
- activeIndexRef.current = activeIndex
139
- const selectedRef = useRef(selectedOriginalIndices)
140
- selectedRef.current = selectedOriginalIndices
141
- const multiRef = useRef(multi)
142
- multiRef.current = multi
143
- const modeRef = useRef(mode)
144
- modeRef.current = mode
134
+ const optionsRef = useRef(options);
135
+ optionsRef.current = options;
136
+ const itemsRef = useRef(items);
137
+ itemsRef.current = items;
138
+ const matchesRef = useRef(matches);
139
+ matchesRef.current = matches;
140
+ const filterQueryRef = useRef(filterQuery);
141
+ filterQueryRef.current = filterQuery;
142
+ const activeIndexRef = useRef(activeIndex);
143
+ activeIndexRef.current = activeIndex;
144
+ const selectedRef = useRef(selectedOriginalIndices);
145
+ selectedRef.current = selectedOriginalIndices;
146
+ const multiRef = useRef(multi);
147
+ multiRef.current = multi;
148
+ const modeRef = useRef(mode);
149
+ modeRef.current = mode;
145
150
 
146
151
  const replaceItems = useCallback((nextItems: ChooseItem[]) => {
147
- itemsRef.current = nextItems
148
- matchesRef.current = fuzzyFilter(nextItems, filterQueryRef.current)
149
- activeIndexRef.current = 0
150
- selectedRef.current = new Set()
151
- setItems(nextItems)
152
- setActiveIndexState(0)
153
- setSelectedOriginalIndices(selectedRef.current)
154
- }, [])
152
+ itemsRef.current = nextItems;
153
+ matchesRef.current = fuzzyFilter(nextItems, filterQueryRef.current);
154
+ activeIndexRef.current = 0;
155
+ selectedRef.current = new Set();
156
+ setItems(nextItems);
157
+ setActiveIndex(0);
158
+ setSelectedOriginalIndices(selectedRef.current);
159
+ }, []);
155
160
 
156
161
  useEffect(() => {
157
- const requestId = ++requestIdRef.current
158
- let active = true
159
- let result: ChooseItem[] | Promise<ChooseItem[]>
162
+ requestIdRef.current += 1;
163
+ const requestId = requestIdRef.current;
164
+ let active = true;
165
+ const deactivate = () => {
166
+ active = false;
167
+ };
168
+ let result: ChooseItem[] | Promise<ChooseItem[]>;
160
169
 
161
170
  // Direct arrays seed state during the first render. Avoid a redundant
162
171
  // post-mount reset that could overwrite immediate controller operations.
@@ -165,253 +174,288 @@ export function useChoose(options: UseChooseOptions): UseChooseResult {
165
174
  Array.isArray(source) &&
166
175
  source === initialArraySourceRef.current
167
176
  ) {
168
- didHandleInitialSourceRef.current = true
169
- setLoading(false)
170
- return
177
+ didHandleInitialSourceRef.current = true;
178
+ setLoading(false);
179
+ return deactivate;
171
180
  }
172
- didHandleInitialSourceRef.current = true
181
+ didHandleInitialSourceRef.current = true;
173
182
 
174
- setError(null)
183
+ setError(null);
175
184
  try {
176
- result = loadChooseSource(source)
185
+ result = loadChooseSource(source);
177
186
  } catch (loadError) {
178
- if (requestId !== requestIdRef.current) return
179
- replaceItems([])
180
- setError(chooseSourceError(loadError))
181
- setLoading(false)
182
- return
187
+ if (requestId !== requestIdRef.current) {
188
+ return deactivate;
189
+ }
190
+ replaceItems([]);
191
+ setError(chooseSourceError(loadError));
192
+ setLoading(false);
193
+ return deactivate;
183
194
  }
184
195
 
185
196
  if (result instanceof Promise) {
186
- setLoading(true)
187
- void result.then(
188
- (loaded) => {
189
- if (!active || requestId !== requestIdRef.current) return
190
- replaceItems(loaded)
191
- setError(null)
192
- setLoading(false)
193
- },
194
- (loadError: unknown) => {
195
- if (!active || requestId !== requestIdRef.current) return
196
- replaceItems([])
197
- setError(chooseSourceError(loadError))
198
- setLoading(false)
199
- },
200
- )
201
- return () => {
202
- active = false
203
- }
197
+ setLoading(true);
198
+ void (async () => {
199
+ try {
200
+ const loaded = await result;
201
+ if (!active || requestId !== requestIdRef.current) {
202
+ return;
203
+ }
204
+ replaceItems(loaded);
205
+ setError(null);
206
+ setLoading(false);
207
+ } catch (loadError) {
208
+ if (!active || requestId !== requestIdRef.current) {
209
+ return;
210
+ }
211
+ replaceItems([]);
212
+ setError(chooseSourceError(loadError));
213
+ setLoading(false);
214
+ }
215
+ })();
216
+ return deactivate;
204
217
  }
205
218
 
206
- replaceItems(result)
207
- setLoading(false)
208
- }, [source, reloadRevision, replaceItems])
219
+ replaceItems(result);
220
+ setLoading(false);
221
+ return deactivate;
222
+ }, [source, reloadRevision, replaceItems]);
209
223
 
210
224
  const setFilter = useCallback((query: string) => {
211
225
  // Controlled OpenTUI inputs may echo a programmatic `value` update through
212
226
  // onInput. Only a real query change resets navigation.
213
227
  if (query === filterQueryRef.current) {
214
- setFilterQuery(query)
215
- return
228
+ setFilterQuery(query);
229
+ return;
216
230
  }
217
- filterQueryRef.current = query
218
- matchesRef.current = fuzzyFilter(itemsRef.current, query)
219
- activeIndexRef.current = 0
220
- setFilterQuery(query)
221
- setActiveIndexState(0)
222
- }, [])
223
-
224
- const setActiveIndex = useCallback((index: number) => {
225
- const lastIndex = Math.max(0, matchesRef.current.length - 1)
226
- const next = Math.min(lastIndex, Math.max(0, index))
227
- activeIndexRef.current = next
228
- setActiveIndexState(next)
229
- }, [])
230
-
231
- const moveUp = useCallback(() => setActiveIndex(activeIndexRef.current - 1), [setActiveIndex])
232
- const moveDown = useCallback(() => setActiveIndex(activeIndexRef.current + 1), [setActiveIndex])
233
-
234
- const getActiveItem = useCallback(() => matchesRef.current[activeIndexRef.current]?.item, [])
231
+ filterQueryRef.current = query;
232
+ matchesRef.current = fuzzyFilter(itemsRef.current, query);
233
+ activeIndexRef.current = 0;
234
+ setFilterQuery(query);
235
+ setActiveIndex(0);
236
+ }, []);
237
+
238
+ const updateActiveIndex = useCallback((index: number) => {
239
+ const lastIndex = Math.max(0, matchesRef.current.length - 1);
240
+ const next = Math.min(lastIndex, Math.max(0, index));
241
+ activeIndexRef.current = next;
242
+ setActiveIndex(next);
243
+ }, []);
244
+
245
+ const moveUp = useCallback(() => {
246
+ updateActiveIndex(activeIndexRef.current - 1);
247
+ }, [updateActiveIndex]);
248
+ const moveDown = useCallback(() => {
249
+ updateActiveIndex(activeIndexRef.current + 1);
250
+ }, [updateActiveIndex]);
251
+
252
+ const getActiveItem = useCallback(() => matchesRef.current[activeIndexRef.current]?.item, []);
235
253
 
236
254
  const getSelectedItems = useCallback((): ChooseItem[] => {
237
- const active = getActiveItem()
238
- if (!multiRef.current) return active ? [active] : []
239
- const selected = Array.from(selectedRef.current).flatMap((index) => {
240
- const item = itemsRef.current[index]
241
- return item ? [item] : []
242
- })
243
- return selected.length > 0 ? selected : active ? [active] : []
244
- }, [getActiveItem])
255
+ const active = getActiveItem();
256
+ if (!multiRef.current) {
257
+ return active === undefined ? [] : [active];
258
+ }
259
+ const selected = [...selectedRef.current].flatMap((index) => {
260
+ const item = itemsRef.current[index];
261
+ return item === undefined ? [] : [item];
262
+ });
263
+ if (selected.length > 0) {
264
+ return selected;
265
+ }
266
+ return active === undefined ? [] : [active];
267
+ }, [getActiveItem]);
245
268
 
246
269
  const toggleActive = useCallback(() => {
247
- if (!multiRef.current) return
248
- const originalIndex = matchesRef.current[activeIndexRef.current]?.originalIndex
249
- if (originalIndex === undefined) return
250
- const next = new Set(selectedRef.current)
251
- if (next.has(originalIndex)) next.delete(originalIndex)
252
- else next.add(originalIndex)
253
- selectedRef.current = next
254
- setSelectedOriginalIndices(next)
255
- }, [])
270
+ if (!multiRef.current) {
271
+ return;
272
+ }
273
+ const originalIndex = matchesRef.current[activeIndexRef.current]?.originalIndex;
274
+ if (originalIndex === undefined) {
275
+ return;
276
+ }
277
+ const next = new Set(selectedRef.current);
278
+ if (next.has(originalIndex)) {
279
+ next.delete(originalIndex);
280
+ } else {
281
+ next.add(originalIndex);
282
+ }
283
+ selectedRef.current = next;
284
+ setSelectedOriginalIndices(next);
285
+ }, []);
256
286
 
257
287
  const submit = useCallback(() => {
258
- void optionsRef.current.onSubmit({ items: getSelectedItems() })
259
- }, [getSelectedItems])
260
-
261
- const cancel = useCallback(() => optionsRef.current.onCancel?.(), [])
262
- const reload = useCallback(() => setReloadRevision((revision) => revision + 1), [])
263
- const setModeExternal = useCallback((nextMode: Mode) => setMode(nextMode), [setMode])
288
+ void optionsRef.current.onSubmit({ items: getSelectedItems() });
289
+ }, [getSelectedItems]);
290
+
291
+ const cancel = useCallback(() => optionsRef.current.onCancel?.(), []);
292
+ const reload = useCallback(() => {
293
+ setReloadRevision((revision) => revision + 1);
294
+ }, []);
295
+ const setModeExternal = useCallback(
296
+ (nextMode: Mode) => {
297
+ setMode(nextMode);
298
+ },
299
+ [setMode],
300
+ );
264
301
 
265
302
  useProvideCommandContext(() => ({
266
303
  choose: {
267
304
  activeItem: getActiveItem(),
268
- selectedItems: getSelectedItems(),
269
305
  filterQuery: filterQueryRef.current,
306
+ selectedItems: getSelectedItems(),
270
307
  },
271
308
  ...(optionsRef.current.onCancel ? { exit: cancel } : {}),
272
- }))
309
+ }));
273
310
 
274
- useActions(options.commands)
311
+ useActions(options.commands);
275
312
 
276
313
  const enabled = useCallback(
277
314
  (group: ChooseCommandGroup) => !(optionsRef.current.disable?.includes(group) ?? false),
278
315
  [],
279
- )
316
+ );
280
317
 
281
318
  useCommand({
282
- id: `${commandScope}:escape`,
283
- title: "Back / cancel",
319
+ handler: () => {
320
+ if (modeRef.current === "insert") {
321
+ setMode("cursor");
322
+ } else {
323
+ cancel();
324
+ }
325
+ },
326
+ hidden: true,
284
327
  hotkey: "Escape",
328
+ id: `${commandScope}:escape`,
285
329
  modes: ["insert", "cursor"],
286
- hidden: true,
330
+ title: "Back / cancel",
287
331
  when: () =>
288
332
  modeRef.current === "insert"
289
333
  ? enabled("mode")
290
334
  : enabled("cancel") && optionsRef.current.onCancel !== undefined,
291
- handler: () => {
292
- if (modeRef.current === "insert") setMode("cursor")
293
- else cancel()
294
- },
295
- })
335
+ });
296
336
  useCommand({
297
- id: `${commandScope}:cancel`,
298
- title: "Cancel",
337
+ handler: cancel,
338
+ hidden: true,
299
339
  hotkey: "q",
340
+ id: `${commandScope}:cancel`,
300
341
  modes: ["cursor"],
301
- hidden: true,
342
+ title: "Cancel",
302
343
  when: () => enabled("cancel") && optionsRef.current.onCancel !== undefined,
303
- handler: cancel,
304
- })
344
+ });
305
345
  useCommand({
306
- id: `${commandScope}:insert-mode-i`,
307
- title: "Insert mode",
346
+ handler: () => {
347
+ setMode("insert");
348
+ },
349
+ hidden: true,
308
350
  hotkey: "i",
351
+ id: `${commandScope}:insert-mode-i`,
309
352
  modes: ["cursor"],
310
- hidden: true,
353
+ title: "Insert mode",
311
354
  when: () => enabled("mode"),
312
- handler: () => setMode("insert"),
313
- })
355
+ });
314
356
  useCommand({
315
- id: `${commandScope}:insert-mode-a`,
316
- title: "Insert mode",
357
+ handler: () => {
358
+ setMode("insert");
359
+ },
360
+ hidden: true,
317
361
  hotkey: "a",
362
+ id: `${commandScope}:insert-mode-a`,
318
363
  modes: ["cursor"],
319
- hidden: true,
364
+ title: "Insert mode",
320
365
  when: () => enabled("mode"),
321
- handler: () => setMode("insert"),
322
- })
366
+ });
323
367
  useCommand({
324
- id: `${commandScope}:move-down-vim`,
325
- title: "Move down",
368
+ handler: moveDown,
369
+ hidden: true,
326
370
  hotkey: "j",
371
+ id: `${commandScope}:move-down-vim`,
327
372
  modes: ["cursor"],
328
- hidden: true,
373
+ title: "Move down",
329
374
  when: () => enabled("navigation"),
330
- handler: moveDown,
331
- })
375
+ });
332
376
  useCommand({
333
- id: `${commandScope}:move-up-vim`,
334
- title: "Move up",
377
+ handler: moveUp,
378
+ hidden: true,
335
379
  hotkey: "k",
380
+ id: `${commandScope}:move-up-vim`,
336
381
  modes: ["cursor"],
337
- hidden: true,
382
+ title: "Move up",
338
383
  when: () => enabled("navigation"),
339
- handler: moveUp,
340
- })
384
+ });
341
385
  useCommand({
342
- id: `${commandScope}:confirm`,
343
- title: "Confirm",
386
+ handler: submit,
387
+ hidden: true,
344
388
  hotkey: "Enter",
389
+ id: `${commandScope}:confirm`,
345
390
  modes: ["insert", "cursor"],
346
- hidden: true,
391
+ title: "Confirm",
347
392
  when: () => enabled("submit"),
348
- handler: submit,
349
- })
393
+ });
350
394
  useCommand({
351
- id: `${commandScope}:move-up`,
352
- title: "Move up",
395
+ handler: moveUp,
396
+ hidden: true,
353
397
  hotkey: "up",
398
+ id: `${commandScope}:move-up`,
354
399
  modes: ["insert", "cursor"],
355
- hidden: true,
400
+ title: "Move up",
356
401
  when: () => enabled("navigation"),
357
- handler: moveUp,
358
- })
402
+ });
359
403
  useCommand({
360
- id: `${commandScope}:move-up-ctrl-p`,
361
- title: "Move up",
404
+ handler: moveUp,
405
+ hidden: true,
362
406
  hotkey: "ctrl+p",
407
+ id: `${commandScope}:move-up-ctrl-p`,
363
408
  modes: ["insert", "cursor"],
364
- hidden: true,
409
+ title: "Move up",
365
410
  when: () => enabled("navigation"),
366
- handler: moveUp,
367
- })
411
+ });
368
412
  useCommand({
369
- id: `${commandScope}:move-down`,
370
- title: "Move down",
413
+ handler: moveDown,
414
+ hidden: true,
371
415
  hotkey: "down",
416
+ id: `${commandScope}:move-down`,
372
417
  modes: ["insert", "cursor"],
373
- hidden: true,
418
+ title: "Move down",
374
419
  when: () => enabled("navigation"),
375
- handler: moveDown,
376
- })
420
+ });
377
421
  useCommand({
378
- id: `${commandScope}:move-down-ctrl-n`,
379
- title: "Move down",
422
+ handler: moveDown,
423
+ hidden: true,
380
424
  hotkey: "ctrl+n",
425
+ id: `${commandScope}:move-down-ctrl-n`,
381
426
  modes: ["insert", "cursor"],
382
- hidden: true,
427
+ title: "Move down",
383
428
  when: () => enabled("navigation"),
384
- handler: moveDown,
385
- })
429
+ });
386
430
  useCommand({
387
- id: `${commandScope}:toggle-next`,
388
- title: "Toggle selection and move down",
431
+ handler: () => {
432
+ toggleActive();
433
+ moveDown();
434
+ },
435
+ hidden: true,
389
436
  hotkey: "Tab",
437
+ id: `${commandScope}:toggle-next`,
390
438
  modes: ["insert", "cursor"],
391
- hidden: true,
439
+ title: "Toggle selection and move down",
392
440
  when: () => enabled("multi-select") && multiRef.current,
441
+ });
442
+ useCommand({
393
443
  handler: () => {
394
- toggleActive()
395
- moveDown()
444
+ toggleActive();
445
+ moveUp();
396
446
  },
397
- })
398
- useCommand({
399
- id: `${commandScope}:toggle-previous`,
400
- title: "Toggle selection and move up",
447
+ hidden: true,
401
448
  hotkey: "shift+Tab",
449
+ id: `${commandScope}:toggle-previous`,
402
450
  modes: ["insert", "cursor"],
403
- hidden: true,
451
+ title: "Toggle selection and move up",
404
452
  when: () => enabled("multi-select") && multiRef.current,
405
- handler: () => {
406
- toggleActive()
407
- moveUp()
408
- },
409
- })
453
+ });
410
454
 
411
- const surfaceId = useCommandSurfaceId()
412
- const activeSurface = useActiveCommandSurface()
455
+ const surfaceId = useCommandSurfaceId();
456
+ const activeSurface = useActiveCommandSurface();
413
457
  const suspended =
414
- (options.suspended ?? false) || (activeSurface !== null && activeSurface.id !== surfaceId)
458
+ (options.suspended ?? false) || (activeSurface !== null && activeSurface.id !== surfaceId);
415
459
 
416
460
  const onFilterKeyDown = useCallback(
417
461
  (event: KeyEvent) => {
@@ -421,59 +465,62 @@ export function useChoose(options: UseChooseOptions): UseChooseResult {
421
465
  !enabled("multi-select") ||
422
466
  modeRef.current !== "insert"
423
467
  ) {
424
- return
468
+ return;
469
+ }
470
+ event.preventDefault();
471
+ toggleActive();
472
+ if (event.shift) {
473
+ moveUp();
474
+ } else {
475
+ moveDown();
425
476
  }
426
- event.preventDefault()
427
- toggleActive()
428
- if (event.shift) moveUp()
429
- else moveDown()
430
477
  },
431
478
  [enabled, moveDown, moveUp, toggleActive],
432
- )
433
-
434
- const controllerRef = useRef<ChooseController | null>(null)
435
- if (controllerRef.current === null) {
436
- controllerRef.current = {
437
- getFilter: () => filterQueryRef.current,
438
- setFilter,
439
- clearFilter: () => setFilter(""),
440
- moveUp,
441
- moveDown,
442
- setActiveIndex,
443
- getActiveItem,
444
- getSelectedItems,
445
- toggleActive,
446
- submit,
447
- cancel,
448
- reload,
449
- get mode() {
450
- return modeRef.current
451
- },
452
- setMode: setModeExternal,
453
- }
454
- }
479
+ );
480
+
481
+ const controllerRef = useRef<ChooseController | null>(null);
482
+ controllerRef.current ??= {
483
+ cancel,
484
+ clearFilter: () => {
485
+ setFilter("");
486
+ },
487
+ getActiveItem,
488
+ getFilter: () => filterQueryRef.current,
489
+ getSelectedItems,
490
+ get mode() {
491
+ return modeRef.current;
492
+ },
493
+ moveDown,
494
+ moveUp,
495
+ reload,
496
+ setActiveIndex: updateActiveIndex,
497
+ setFilter,
498
+ setMode: setModeExternal,
499
+ submit,
500
+ toggleActive,
501
+ };
455
502
 
456
503
  return {
457
504
  controller: controllerRef.current,
458
505
  state: {
459
- items,
460
- matches,
461
- filterQuery,
462
506
  activeIndex,
463
507
  activeItem,
464
- selectedOriginalIndices,
465
- selectedItems,
466
- loading,
467
508
  error,
509
+ filterQuery,
510
+ items,
511
+ loading,
512
+ matches,
468
513
  multi,
514
+ selectedItems,
515
+ selectedOriginalIndices,
469
516
  },
470
517
  view: {
518
+ filterFocused: mode === "insert" && !suspended,
471
519
  filterRef,
472
520
  mode,
473
- suspended,
474
- filterFocused: mode === "insert" && !suspended,
475
521
  onFilterInput: setFilter,
476
522
  onFilterKeyDown,
523
+ suspended,
477
524
  },
478
- }
479
- }
525
+ };
526
+ };