@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.
- package/dist/command-palette-provider.d.ts +5 -0
- package/dist/command-palette-provider.d.ts.map +1 -0
- package/dist/command-palette-provider.js +28 -0
- package/dist/command-palette-provider.js.map +1 -0
- package/dist/commands.d.ts +9 -0
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +61 -11
- package/dist/commands.js.map +1 -1
- package/dist/copy-hook.d.ts +12 -0
- package/dist/copy-hook.d.ts.map +1 -0
- package/dist/copy-hook.js +35 -0
- package/dist/copy-hook.js.map +1 -0
- package/dist/copy-on-select.d.ts.map +1 -1
- package/dist/copy-on-select.js +1 -3
- package/dist/copy-on-select.js.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/navigation.d.ts +24 -0
- package/dist/navigation.d.ts.map +1 -0
- package/dist/navigation.js +239 -0
- package/dist/navigation.js.map +1 -0
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +4 -1
- package/dist/provider.js.map +1 -1
- package/dist/search-hook.d.ts +14 -0
- package/dist/search-hook.d.ts.map +1 -0
- package/dist/search-hook.js +120 -0
- package/dist/search-hook.js.map +1 -0
- package/dist/theme-picker.js +1 -1
- package/dist/theme-picker.js.map +1 -1
- package/package.json +20 -20
- package/src/command-palette-provider.tsx +39 -0
- package/src/commands.ts +64 -11
- package/src/copy-hook.ts +45 -0
- package/src/copy-on-select.ts +1 -4
- package/src/index.ts +15 -5
- package/src/navigation.ts +297 -0
- package/src/provider.tsx +6 -1
- package/src/search-hook.ts +147 -0
- package/src/theme-picker.ts +1 -1
- package/dist/command-palette.d.ts +0 -9
- package/dist/command-palette.d.ts.map +0 -1
- package/dist/command-palette.js +0 -51
- package/dist/command-palette.js.map +0 -1
- package/dist/modal.d.ts +0 -33
- package/dist/modal.d.ts.map +0 -1
- package/dist/modal.js +0 -394
- package/dist/modal.js.map +0 -1
- package/src/command-palette.ts +0 -73
- 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
|
-
}
|