@tooee/shell 0.1.9 → 0.1.12

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 (52) hide show
  1. package/dist/command-palette-provider.d.ts +5 -0
  2. package/dist/command-palette-provider.d.ts.map +1 -0
  3. package/dist/command-palette-provider.js +28 -0
  4. package/dist/command-palette-provider.js.map +1 -0
  5. package/dist/commands.d.ts +9 -0
  6. package/dist/commands.d.ts.map +1 -1
  7. package/dist/commands.js +61 -11
  8. package/dist/commands.js.map +1 -1
  9. package/dist/copy-hook.d.ts +12 -0
  10. package/dist/copy-hook.d.ts.map +1 -0
  11. package/dist/copy-hook.js +35 -0
  12. package/dist/copy-hook.js.map +1 -0
  13. package/dist/copy-on-select.d.ts.map +1 -1
  14. package/dist/copy-on-select.js +1 -3
  15. package/dist/copy-on-select.js.map +1 -1
  16. package/dist/index.d.ts +8 -5
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +5 -3
  19. package/dist/index.js.map +1 -1
  20. package/dist/navigation.d.ts +24 -0
  21. package/dist/navigation.d.ts.map +1 -0
  22. package/dist/navigation.js +239 -0
  23. package/dist/navigation.js.map +1 -0
  24. package/dist/provider.d.ts.map +1 -1
  25. package/dist/provider.js +4 -1
  26. package/dist/provider.js.map +1 -1
  27. package/dist/search-hook.d.ts +14 -0
  28. package/dist/search-hook.d.ts.map +1 -0
  29. package/dist/search-hook.js +120 -0
  30. package/dist/search-hook.js.map +1 -0
  31. package/dist/theme-picker.js +1 -1
  32. package/dist/theme-picker.js.map +1 -1
  33. package/package.json +20 -20
  34. package/src/command-palette-provider.tsx +39 -0
  35. package/src/commands.ts +64 -11
  36. package/src/copy-hook.ts +45 -0
  37. package/src/copy-on-select.ts +1 -4
  38. package/src/index.ts +15 -5
  39. package/src/navigation.ts +297 -0
  40. package/src/provider.tsx +6 -1
  41. package/src/search-hook.ts +147 -0
  42. package/src/theme-picker.ts +1 -1
  43. package/dist/command-palette.d.ts +0 -9
  44. package/dist/command-palette.d.ts.map +0 -1
  45. package/dist/command-palette.js +0 -51
  46. package/dist/command-palette.js.map +0 -1
  47. package/dist/modal.d.ts +0 -33
  48. package/dist/modal.d.ts.map +0 -1
  49. package/dist/modal.js +0 -394
  50. package/dist/modal.js.map +0 -1
  51. package/src/command-palette.ts +0 -73
  52. package/src/modal.ts +0 -449
package/src/modal.ts DELETED
@@ -1,449 +0,0 @@
1
- import { useState, useCallback, useRef, useEffect } from "react"
2
- import { useCommand, useMode, useSetMode, type Mode } from "@tooee/commands"
3
- import { copyToClipboard } from "@tooee/clipboard"
4
- import { useTerminalDimensions } from "@opentui/react"
5
- import { findMatchingLines } from "./search.js"
6
-
7
- export interface Position {
8
- line: number
9
- col: number
10
- }
11
-
12
- export interface ModalNavigationState {
13
- mode: Mode
14
- setMode: (mode: Mode) => void
15
- cursor: Position | null
16
- selection: { start: Position; end: Position } | null
17
- toggledIndices: Set<number>
18
- searchQuery: string
19
- searchActive: boolean
20
- setSearchQuery: (query: string) => void
21
- matchingLines: number[]
22
- currentMatchIndex: number
23
- submitSearch: () => void
24
- }
25
-
26
- export interface ModalNavigationOptions {
27
- totalLines: number
28
- viewportHeight?: number
29
- getText?: () => string | undefined
30
- blockCount?: number
31
- blockLineMap?: number[]
32
- /** Offset to subtract from search line numbers to get block indices (for when getText has different line structure than visual) */
33
- searchLineOffset?: number
34
- multiSelect?: boolean
35
- }
36
-
37
- export function useModalNavigationCommands(opts: ModalNavigationOptions): ModalNavigationState {
38
- const { height: terminalHeight } = useTerminalDimensions()
39
- const {
40
- totalLines,
41
- viewportHeight = terminalHeight - 2,
42
- getText,
43
- blockCount,
44
- blockLineMap,
45
- searchLineOffset = 0,
46
- multiSelect = false,
47
- } = opts
48
- const mode = useMode()
49
- const setMode = useSetMode()
50
-
51
- const [cursor, setCursor] = useState<Position | null>(null)
52
- const [selectionAnchor, setSelectionAnchor] = useState<Position | null>(null)
53
- const [toggledIndices, setToggledIndices] = useState<Set<number>>(new Set())
54
- const [searchQuery, setSearchQuery] = useState("")
55
- const [searchActive, setSearchActive] = useState(false)
56
- const [matchingLines, setMatchingLines] = useState<number[]>([])
57
- const [currentMatchIndex, setCurrentMatchIndex] = useState(0)
58
- const preSearchModeRef = useRef<Mode>("cursor")
59
-
60
- // Incremental search: recompute matches when query changes while search is active
61
- const searchQueryRef = useRef(searchQuery)
62
- searchQueryRef.current = searchQuery
63
- const matchingLinesRef = useRef(matchingLines)
64
- matchingLinesRef.current = matchingLines
65
-
66
- useEffect(() => {
67
- if (!searchActive) return
68
- const text = getText?.()
69
- if (!text || !searchQuery) {
70
- setMatchingLines([])
71
- setCurrentMatchIndex(0)
72
- return
73
- }
74
- const matches = findMatchingLines(text, searchQuery)
75
- setMatchingLines(matches)
76
- setCurrentMatchIndex(0)
77
- if (matches.length > 0) {
78
- // Auto-jump to first match
79
- const line = matches[0]
80
- if (mode === "cursor" || mode === "select") {
81
- setCursor((c) => (c ? { line, col: 0 } : c))
82
- }
83
- }
84
- }, [searchQuery, searchActive]) // eslint-disable-line react-hooks/exhaustive-deps
85
-
86
- const isBlockMode = blockCount != null
87
- const cursorMax = isBlockMode ? Math.max(0, blockCount - 1) : Math.max(0, totalLines - 1)
88
-
89
- const clampCursor = useCallback(
90
- (value: number) => Math.max(0, Math.min(value, cursorMax)),
91
- [cursorMax],
92
- )
93
-
94
- useEffect(() => {
95
- setToggledIndices((prev) => {
96
- const filtered = Array.from(prev).filter((index) => index <= cursorMax)
97
- if (filtered.length === prev.size) return prev
98
- return new Set(filtered)
99
- })
100
- }, [cursorMax])
101
-
102
- // When entering cursor mode, initialize cursor if not already set
103
- const prevMode = useRef<Mode | null>(null)
104
- useEffect(() => {
105
- if (mode === "cursor" && prevMode.current !== "cursor" && prevMode.current !== "select") {
106
- setCursor((existing) => {
107
- // If cursor already exists (e.g. set by a command via palette), preserve it
108
- if (existing) return existing
109
- // If there's an active search match, start cursor there; otherwise start at scroll position
110
- const matches = matchingLinesRef.current
111
- if (matches.length > 0) {
112
- const matchLine = matches[currentMatchIndex] ?? matches[0]
113
- // In block mode, convert search line to block index using offset
114
- if (isBlockMode) {
115
- const blockIndex = Math.max(0, matchLine - searchLineOffset)
116
- return { line: Math.min(blockIndex, cursorMax), col: 0 }
117
- }
118
- return { line: matchLine, col: 0 }
119
- }
120
- // No search match - start at top
121
- return { line: 0, col: 0 }
122
- })
123
- }
124
- if (mode === "select" && prevMode.current === "cursor" && cursor) {
125
- setSelectionAnchor({ ...cursor })
126
- }
127
- prevMode.current = mode
128
- }, [mode]) // eslint-disable-line react-hooks/exhaustive-deps
129
-
130
- // === CURSOR MODE ===
131
-
132
- useCommand({
133
- id: "cursor-down",
134
- title: "Cursor down",
135
- hotkey: "j",
136
- modes: ["cursor"],
137
- handler: () => {
138
- setCursor((c) => {
139
- if (!c) return c
140
- return { line: clampCursor(c.line + 1), col: 0 }
141
- })
142
- },
143
- })
144
-
145
- useCommand({
146
- id: "cursor-toggle",
147
- title: "Toggle selection",
148
- hotkey: "tab",
149
- modes: ["cursor"],
150
- when: () => multiSelect,
151
- handler: () => {
152
- setToggledIndices((prev) => {
153
- if (!cursor) return prev
154
- const next = new Set(prev)
155
- const idx = cursor.line
156
- if (next.has(idx)) {
157
- next.delete(idx)
158
- } else {
159
- next.add(idx)
160
- }
161
- return next
162
- })
163
- },
164
- })
165
-
166
- useCommand({
167
- id: "cursor-toggle-up",
168
- title: "Toggle and move up",
169
- hotkey: "shift+tab",
170
- modes: ["cursor"],
171
- when: () => multiSelect,
172
- handler: () => {
173
- setToggledIndices((prev) => {
174
- if (!cursor) return prev
175
- const next = new Set(prev)
176
- const idx = cursor.line
177
- if (next.has(idx)) {
178
- next.delete(idx)
179
- } else {
180
- next.add(idx)
181
- }
182
- return next
183
- })
184
- setCursor((c) => {
185
- if (!c) return c
186
- return { line: clampCursor(c.line - 1), col: 0 }
187
- })
188
- },
189
- })
190
-
191
- useCommand({
192
- id: "cursor-up",
193
- title: "Cursor up",
194
- hotkey: "k",
195
- modes: ["cursor"],
196
- handler: () => {
197
- setCursor((c) => {
198
- if (!c) return c
199
- return { line: clampCursor(c.line - 1), col: 0 }
200
- })
201
- },
202
- })
203
-
204
- useCommand({
205
- id: "cursor-half-down",
206
- title: "Cursor half page down",
207
- hotkey: "ctrl+d",
208
- modes: ["cursor"],
209
- handler: () => {
210
- setCursor((c) => {
211
- if (!c) return c
212
- const step = isBlockMode ? Math.floor(cursorMax / 4) || 1 : Math.floor(viewportHeight / 2)
213
- return { line: clampCursor(c.line + step), col: 0 }
214
- })
215
- },
216
- })
217
-
218
- useCommand({
219
- id: "cursor-half-up",
220
- title: "Cursor half page up",
221
- hotkey: "ctrl+u",
222
- modes: ["cursor"],
223
- handler: () => {
224
- setCursor((c) => {
225
- if (!c) return c
226
- const step = isBlockMode ? Math.floor(cursorMax / 4) || 1 : Math.floor(viewportHeight / 2)
227
- return { line: clampCursor(c.line - step), col: 0 }
228
- })
229
- },
230
- })
231
-
232
- useCommand({
233
- id: "cursor-top",
234
- title: "Cursor to top",
235
- hotkey: "g g",
236
- modes: ["cursor"],
237
- handler: () => {
238
- setCursor({ line: 0, col: 0 })
239
- },
240
- })
241
-
242
- useCommand({
243
- id: "cursor-bottom",
244
- title: "Cursor to bottom",
245
- hotkey: "shift+g",
246
- modes: ["cursor"],
247
- handler: () => {
248
- setCursor({ line: cursorMax, col: 0 })
249
- },
250
- })
251
-
252
- useCommand({
253
- id: "cursor-search-start",
254
- title: "Search",
255
- hotkey: "/",
256
- modes: ["cursor"],
257
- handler: () => {
258
- preSearchModeRef.current = mode
259
- setSearchActive(true)
260
- setSearchQuery("")
261
- setMode("insert")
262
- },
263
- })
264
-
265
- useCommand({
266
- id: "cursor-search-next",
267
- title: "Next match",
268
- hotkey: "n",
269
- modes: ["cursor"],
270
- when: () => !searchActive,
271
- handler: () => {
272
- const matches = matchingLinesRef.current
273
- if (matches.length === 0) return
274
- setCurrentMatchIndex((idx) => {
275
- const next = (idx + 1) % matches.length
276
- const line = matches[next]
277
- setCursor({ line, col: 0 })
278
- return next
279
- })
280
- },
281
- })
282
-
283
- useCommand({
284
- id: "cursor-search-prev",
285
- title: "Previous match",
286
- hotkey: "shift+n",
287
- modes: ["cursor"],
288
- when: () => !searchActive,
289
- handler: () => {
290
- const matches = matchingLinesRef.current
291
- if (matches.length === 0) return
292
- setCurrentMatchIndex((idx) => {
293
- const next = (idx - 1 + matches.length) % matches.length
294
- const line = matches[next]
295
- setCursor({ line, col: 0 })
296
- return next
297
- })
298
- },
299
- })
300
-
301
- // === SELECT MODE ===
302
-
303
- useCommand({
304
- id: "select-down",
305
- title: "Extend selection down",
306
- hotkey: "j",
307
- modes: ["select"],
308
- handler: () => {
309
- setCursor((c) => {
310
- if (!c) return c
311
- return { line: clampCursor(c.line + 1), col: 0 }
312
- })
313
- },
314
- })
315
-
316
- useCommand({
317
- id: "select-up",
318
- title: "Extend selection up",
319
- hotkey: "k",
320
- modes: ["select"],
321
- handler: () => {
322
- setCursor((c) => {
323
- if (!c) return c
324
- return { line: clampCursor(c.line - 1), col: 0 }
325
- })
326
- },
327
- })
328
-
329
- useCommand({
330
- id: "select-toggle",
331
- title: "Toggle selection",
332
- hotkey: "tab",
333
- modes: ["select"],
334
- when: () => multiSelect,
335
- handler: () => {
336
- setToggledIndices((prev) => {
337
- if (!cursor) return prev
338
- const next = new Set(prev)
339
- const idx = cursor.line
340
- if (next.has(idx)) {
341
- next.delete(idx)
342
- } else {
343
- next.add(idx)
344
- }
345
- return next
346
- })
347
- },
348
- })
349
-
350
- useCommand({
351
- id: "select-copy",
352
- title: "Copy selection",
353
- hotkey: "y",
354
- modes: ["select"],
355
- handler: () => {
356
- if (!getText || !selectionAnchor || !cursor) return
357
- const text = getText()
358
- if (!text) return
359
-
360
- if (isBlockMode && blockLineMap) {
361
- // Block-based copy: use blockLineMap to find line ranges
362
- const startBlock = Math.min(selectionAnchor.line, cursor.line)
363
- const endBlock = Math.max(selectionAnchor.line, cursor.line)
364
- const startLine = blockLineMap[startBlock] ?? 0
365
- const endLine =
366
- endBlock + 1 < blockLineMap.length
367
- ? (blockLineMap[endBlock + 1] ?? text.split("\n").length)
368
- : text.split("\n").length
369
- const lines = text.split("\n")
370
- const selected = lines.slice(startLine, endLine).join("\n")
371
- if (selected) {
372
- void copyToClipboard(selected)
373
- }
374
- } else {
375
- const lines = text.split("\n")
376
- const startLine = Math.min(selectionAnchor.line, cursor.line)
377
- const endLine = Math.max(selectionAnchor.line, cursor.line)
378
- const selected = lines.slice(startLine, endLine + 1).join("\n")
379
- if (selected) {
380
- void copyToClipboard(selected)
381
- }
382
- }
383
- setMode("cursor")
384
- },
385
- })
386
-
387
- useCommand({
388
- id: "select-cancel",
389
- title: "Cancel selection",
390
- hotkey: "escape",
391
- modes: ["select"],
392
- handler: () => setMode("cursor"),
393
- })
394
-
395
- // === MODE TRANSITIONS ===
396
-
397
- useCommand({
398
- id: "enter-select",
399
- title: "Enter select mode",
400
- hotkey: "v",
401
- modes: ["cursor"],
402
- handler: () => setMode("select"),
403
- })
404
-
405
- // === SEARCH CANCEL (any mode) ===
406
-
407
- useCommand({
408
- id: "search-cancel",
409
- title: "Cancel search",
410
- hotkey: "escape",
411
- modes: ["cursor", "select", "insert"],
412
- when: () => searchActive,
413
- handler: () => {
414
- setSearchActive(false)
415
- setSearchQuery("")
416
- setMatchingLines([])
417
- setCurrentMatchIndex(0)
418
- setMode(preSearchModeRef.current)
419
- },
420
- })
421
-
422
- // Compute selection from anchor + cursor
423
- const selection =
424
- selectionAnchor && cursor && mode === "select"
425
- ? {
426
- start: selectionAnchor.line <= cursor.line ? selectionAnchor : cursor,
427
- end: selectionAnchor.line <= cursor.line ? cursor : selectionAnchor,
428
- }
429
- : null
430
-
431
- const submitSearch = useCallback(() => {
432
- setSearchActive(false)
433
- setMode(preSearchModeRef.current)
434
- }, [setMode])
435
-
436
- return {
437
- mode,
438
- setMode,
439
- cursor,
440
- selection,
441
- toggledIndices,
442
- searchQuery,
443
- searchActive,
444
- setSearchQuery,
445
- matchingLines,
446
- currentMatchIndex,
447
- submitSearch,
448
- }
449
- }