termcast 1.3.30 → 1.3.32
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/apis/cache.d.ts.map +1 -1
- package/dist/apis/cache.js +4 -39
- package/dist/apis/cache.js.map +1 -1
- package/dist/apis/hud.d.ts.map +1 -1
- package/dist/apis/hud.js +13 -31
- package/dist/apis/hud.js.map +1 -1
- package/dist/apis/localstorage.d.ts.map +1 -1
- package/dist/apis/localstorage.js +3 -27
- package/dist/apis/localstorage.js.map +1 -1
- package/dist/apis/toast.d.ts +16 -43
- package/dist/apis/toast.d.ts.map +1 -1
- package/dist/apis/toast.js +78 -177
- package/dist/apis/toast.js.map +1 -1
- package/dist/build.d.ts +3 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +52 -2
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +206 -25
- package/dist/cli.js.map +1 -1
- package/dist/colors.d.ts.map +1 -1
- package/dist/colors.js +1 -0
- package/dist/colors.js.map +1 -1
- package/dist/compile.d.ts +0 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +18 -23
- package/dist/compile.js.map +1 -1
- package/dist/components/actions.d.ts.map +1 -1
- package/dist/components/actions.js +30 -15
- package/dist/components/actions.js.map +1 -1
- package/dist/components/animation-tick.d.ts +12 -0
- package/dist/components/animation-tick.d.ts.map +1 -0
- package/dist/components/animation-tick.js +63 -0
- package/dist/components/animation-tick.js.map +1 -0
- package/dist/components/detail.d.ts.map +1 -1
- package/dist/components/detail.js +10 -13
- package/dist/components/detail.js.map +1 -1
- package/dist/components/dropdown.d.ts +1 -0
- package/dist/components/dropdown.d.ts.map +1 -1
- package/dist/components/dropdown.js +27 -26
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/extension-preferences.d.ts.map +1 -1
- package/dist/components/extension-preferences.js +15 -10
- package/dist/components/extension-preferences.js.map +1 -1
- package/dist/components/footer.d.ts +13 -0
- package/dist/components/footer.d.ts.map +1 -0
- package/dist/components/footer.js +106 -0
- package/dist/components/footer.js.map +1 -0
- package/dist/components/form/file-autocomplete.d.ts +19 -4
- package/dist/components/form/file-autocomplete.d.ts.map +1 -1
- package/dist/components/form/file-autocomplete.js +56 -55
- package/dist/components/form/file-autocomplete.js.map +1 -1
- package/dist/components/form/file-picker.d.ts.map +1 -1
- package/dist/components/form/file-picker.js +26 -15
- package/dist/components/form/file-picker.js.map +1 -1
- package/dist/components/form/index.d.ts.map +1 -1
- package/dist/components/form/index.js +17 -15
- package/dist/components/form/index.js.map +1 -1
- package/dist/components/form/with-left-border.d.ts.map +1 -1
- package/dist/components/form/with-left-border.js +4 -12
- package/dist/components/form/with-left-border.js.map +1 -1
- package/dist/components/list.d.ts.map +1 -1
- package/dist/components/list.js +126 -86
- package/dist/components/list.js.map +1 -1
- package/dist/components/loading-bar.d.ts.map +1 -1
- package/dist/components/loading-bar.js +5 -22
- package/dist/components/loading-bar.js.map +1 -1
- package/dist/components/loading-text.d.ts.map +1 -1
- package/dist/components/loading-text.js +3 -22
- package/dist/components/loading-text.js.map +1 -1
- package/dist/components/theme-picker.d.ts +2 -0
- package/dist/components/theme-picker.d.ts.map +1 -0
- package/dist/components/theme-picker.js +37 -0
- package/dist/components/theme-picker.js.map +1 -0
- package/dist/descendants.d.ts +6 -0
- package/dist/descendants.d.ts.map +1 -1
- package/dist/descendants.js +74 -8
- package/dist/descendants.js.map +1 -1
- package/dist/examples/internal/descendants-rerender.d.ts +14 -0
- package/dist/examples/internal/descendants-rerender.d.ts.map +1 -0
- package/dist/examples/internal/descendants-rerender.js +145 -0
- package/dist/examples/internal/descendants-rerender.js.map +1 -0
- package/dist/examples/internal/simple-dialog.js +4 -1
- package/dist/examples/internal/simple-dialog.js.map +1 -1
- package/dist/examples/internal/simple-scrollbox.js +1 -1
- package/dist/examples/internal/simple-scrollbox.js.map +1 -1
- package/dist/examples/list-with-dropdown.js +1 -1
- package/dist/examples/list-with-dropdown.js.map +1 -1
- package/dist/examples/miscellaneous.js +1 -1
- package/dist/examples/miscellaneous.js.map +1 -1
- package/dist/examples/toast-action.d.ts +2 -0
- package/dist/examples/toast-action.d.ts.map +1 -0
- package/dist/examples/toast-action.js +76 -0
- package/dist/examples/toast-action.js.map +1 -0
- package/dist/examples/toast-variations.js +38 -36
- package/dist/examples/toast-variations.js.map +1 -1
- package/dist/extensions/dev.d.ts +1 -1
- package/dist/extensions/dev.d.ts.map +1 -1
- package/dist/extensions/dev.js +62 -30
- package/dist/extensions/dev.js.map +1 -1
- package/dist/extensions/home.d.ts.map +1 -1
- package/dist/extensions/home.js +4 -3
- package/dist/extensions/home.js.map +1 -1
- package/dist/extensions/react-refresh-init.d.ts +5 -0
- package/dist/extensions/react-refresh-init.d.ts.map +1 -0
- package/dist/extensions/react-refresh-init.js +52 -0
- package/dist/extensions/react-refresh-init.js.map +1 -0
- package/dist/internal/date-picker-widget.js +1 -1
- package/dist/internal/date-picker-widget.js.map +1 -1
- package/dist/internal/dialog.d.ts +8 -3
- package/dist/internal/dialog.d.ts.map +1 -1
- package/dist/internal/dialog.js +37 -53
- package/dist/internal/dialog.js.map +1 -1
- package/dist/internal/navigation.d.ts +1 -0
- package/dist/internal/navigation.d.ts.map +1 -1
- package/dist/internal/navigation.js +25 -1
- package/dist/internal/navigation.js.map +1 -1
- package/dist/internal/providers.d.ts.map +1 -1
- package/dist/internal/providers.js +9 -197
- package/dist/internal/providers.js.map +1 -1
- package/dist/internal/scrollbox.d.ts.map +1 -1
- package/dist/internal/scrollbox.js +1 -0
- package/dist/internal/scrollbox.js.map +1 -1
- package/dist/release.d.ts +1 -0
- package/dist/release.d.ts.map +1 -1
- package/dist/release.js +16 -9
- package/dist/release.js.map +1 -1
- package/dist/state.d.ts +27 -1
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +6 -0
- package/dist/state.js.map +1 -1
- package/dist/theme.d.ts +6 -19
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +76 -45
- package/dist/theme.js.map +1 -1
- package/dist/themes/aura.json +69 -0
- package/dist/themes/ayu.json +80 -0
- package/dist/themes/catppuccin-frappe.json +233 -0
- package/dist/themes/catppuccin-macchiato.json +233 -0
- package/dist/themes/catppuccin.json +112 -0
- package/dist/themes/cobalt2.json +228 -0
- package/dist/themes/cursor.json +249 -0
- package/dist/themes/dracula.json +219 -0
- package/dist/themes/everforest.json +241 -0
- package/dist/themes/flexoki.json +237 -0
- package/dist/themes/github-light.json +56 -0
- package/dist/themes/github.json +241 -0
- package/dist/themes/gruvbox.json +95 -0
- package/dist/themes/kanagawa.json +77 -0
- package/dist/themes/lucent-orng.json +227 -0
- package/dist/themes/material.json +235 -0
- package/dist/themes/matrix.json +77 -0
- package/dist/themes/mercury.json +245 -0
- package/dist/themes/monokai.json +221 -0
- package/dist/themes/nightowl.json +221 -0
- package/dist/themes/nord.json +223 -0
- package/dist/themes/one-dark.json +84 -0
- package/dist/themes/opencode-light.json +62 -0
- package/dist/themes/opencode.json +245 -0
- package/dist/themes/orng.json +245 -0
- package/dist/themes/palenight.json +222 -0
- package/dist/themes/rosepine.json +234 -0
- package/dist/themes/solarized.json +223 -0
- package/dist/themes/synthwave84.json +226 -0
- package/dist/themes/termcast.json +226 -0
- package/dist/themes/tokyonight.json +243 -0
- package/dist/themes/vercel.json +255 -0
- package/dist/themes/vesper.json +218 -0
- package/dist/themes/zenburn.json +223 -0
- package/dist/themes.d.ts +57 -0
- package/dist/themes.d.ts.map +1 -0
- package/dist/themes.js +181 -0
- package/dist/themes.js.map +1 -0
- package/dist/utils/run-command.d.ts +2 -1
- package/dist/utils/run-command.d.ts.map +1 -1
- package/dist/utils/run-command.js +20 -10
- package/dist/utils/run-command.js.map +1 -1
- package/dist/utils.d.ts +2 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +90 -17
- package/dist/utils.js.map +1 -1
- package/dist/watcher.d.ts +3 -0
- package/dist/watcher.d.ts.map +1 -0
- package/dist/watcher.js +16 -0
- package/dist/watcher.js.map +1 -0
- package/package.json +16 -10
- package/src/apis/cache.tsx +5 -44
- package/src/apis/hud.tsx +17 -62
- package/src/apis/localstorage.tsx +3 -32
- package/src/apis/toast.tsx +91 -275
- package/src/build.test.tsx +10 -0
- package/src/build.tsx +61 -1
- package/src/cli.tsx +365 -103
- package/src/colors.tsx +1 -0
- package/src/compile.tsx +21 -29
- package/src/compile.vitest.tsx +300 -0
- package/src/components/actions.tsx +64 -45
- package/src/components/animation-tick.tsx +85 -0
- package/src/components/detail.tsx +31 -35
- package/src/components/dropdown.tsx +32 -21
- package/src/components/extension-preferences.tsx +14 -10
- package/src/components/footer.tsx +241 -0
- package/src/components/form/file-autocomplete.tsx +80 -60
- package/src/components/form/file-picker.tsx +37 -25
- package/src/components/form/index.tsx +45 -41
- package/src/components/form/with-left-border.tsx +4 -14
- package/src/components/list.tsx +181 -121
- package/src/components/loading-bar.tsx +5 -25
- package/src/components/loading-text.tsx +4 -23
- package/src/components/theme-picker.tsx +57 -0
- package/src/descendants.tsx +98 -9
- package/src/examples/actions-dialog-layout.vitest.tsx +112 -0
- package/src/examples/file-autocomplete.vitest.tsx +131 -122
- package/src/examples/form-basic.vitest.tsx +463 -644
- package/src/examples/form-dropdown.vitest.tsx +553 -571
- package/src/examples/form-scroll.vitest.tsx +112 -102
- package/src/examples/form-tagpicker.vitest.tsx +364 -338
- package/src/examples/internal/descendants-rerender.tsx +273 -0
- package/src/examples/internal/descendants-rerender.vitest.tsx +194 -0
- package/src/examples/internal/simple-dialog.tsx +4 -4
- package/src/examples/internal/simple-scrollbox.tsx +2 -2
- package/src/examples/internal/simple-scrollbox.vitest.tsx +43 -31
- package/src/examples/list-detail-metadata.vitest.tsx +34 -30
- package/src/examples/list-dropdown-default.vitest.tsx +84 -72
- package/src/examples/list-empty-view.vitest.tsx +93 -0
- package/src/examples/list-fetch-data.vitest.tsx +36 -30
- package/src/examples/list-scrollbox.vitest.tsx +59 -39
- package/src/examples/list-with-detail.vitest.tsx +339 -314
- package/src/examples/list-with-dropdown.tsx +1 -0
- package/src/examples/list-with-dropdown.vitest.tsx +176 -150
- package/src/examples/list-with-sections.vitest.tsx +289 -270
- package/src/examples/list-with-toast.vitest.tsx +44 -44
- package/src/examples/miscellaneous.tsx +10 -0
- package/src/examples/simple-file-picker.vitest.tsx +90 -86
- package/src/examples/simple-grid.vitest.tsx +275 -249
- package/src/examples/simple-navigation.vitest.tsx +192 -168
- package/src/examples/store.vitest.tsx +6 -4
- package/src/examples/swift-extension.vitest.tsx +31 -19
- package/src/examples/synonyms.vitest.tsx +93 -83
- package/src/examples/toast-action.tsx +160 -0
- package/src/examples/toast-action.vitest.tsx +404 -0
- package/src/examples/toast-variations.tsx +58 -57
- package/src/examples/toast-variations.vitest.tsx +186 -166
- package/src/extensions/dev.tsx +74 -33
- package/src/extensions/dev.vitest.tsx +162 -69
- package/src/extensions/home.tsx +5 -6
- package/src/extensions/react-refresh-init.tsx +59 -0
- package/src/internal/date-picker-widget.tsx +1 -1
- package/src/internal/dialog.tsx +59 -83
- package/src/internal/navigation.tsx +37 -4
- package/src/internal/providers.tsx +27 -315
- package/src/internal/scrollbox.tsx +1 -0
- package/src/release.tsx +16 -10
- package/src/state.tsx +36 -3
- package/src/theme.tsx +82 -51
- package/src/themes/aura.json +69 -0
- package/src/themes/ayu.json +80 -0
- package/src/themes/catppuccin-frappe.json +233 -0
- package/src/themes/catppuccin-macchiato.json +233 -0
- package/src/themes/catppuccin.json +112 -0
- package/src/themes/cobalt2.json +228 -0
- package/src/themes/cursor.json +249 -0
- package/src/themes/dracula.json +219 -0
- package/src/themes/everforest.json +241 -0
- package/src/themes/flexoki.json +237 -0
- package/src/themes/github-light.json +56 -0
- package/src/themes/github.json +241 -0
- package/src/themes/gruvbox.json +95 -0
- package/src/themes/kanagawa.json +77 -0
- package/src/themes/lucent-orng.json +227 -0
- package/src/themes/material.json +235 -0
- package/src/themes/matrix.json +77 -0
- package/src/themes/mercury.json +252 -0
- package/src/themes/monokai.json +221 -0
- package/src/themes/nightowl.json +221 -0
- package/src/themes/nord.json +223 -0
- package/src/themes/one-dark.json +84 -0
- package/src/themes/opencode-light.json +62 -0
- package/src/themes/opencode.json +245 -0
- package/src/themes/orng.json +245 -0
- package/src/themes/palenight.json +222 -0
- package/src/themes/rosepine.json +234 -0
- package/src/themes/solarized.json +223 -0
- package/src/themes/synthwave84.json +226 -0
- package/src/themes/termcast.json +227 -0
- package/src/themes/tokyonight.json +243 -0
- package/src/themes/vercel.json +255 -0
- package/src/themes/vesper.json +218 -0
- package/src/themes/zenburn.json +223 -0
- package/src/themes.ts +291 -0
- package/src/utils/run-command.tsx +23 -12
- package/src/utils.tsx +115 -18
- package/src/watcher.tsx +19 -0
|
@@ -1,71 +1,90 @@
|
|
|
1
1
|
import React, { useState } from 'react'
|
|
2
2
|
import { useQuery } from '@tanstack/react-query'
|
|
3
3
|
import { Theme } from 'termcast/src/theme'
|
|
4
|
-
import {
|
|
5
|
-
import { listAllFiles } from '../../utils/file-system'
|
|
4
|
+
import { searchFiles, parsePath } from '../../utils/file-system'
|
|
6
5
|
import { useKeyboard } from '@opentui/react'
|
|
7
6
|
import { useIsInFocus } from 'termcast/src/internal/focus-context'
|
|
7
|
+
import { createStore, useStore } from 'zustand'
|
|
8
|
+
import os from 'os'
|
|
9
|
+
|
|
10
|
+
const homedir = os.homedir()
|
|
11
|
+
|
|
12
|
+
interface FileAutocompleteState {
|
|
13
|
+
filter: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Creates a local zustand store for sharing state between FilePicker and FileAutocompleteDialog.
|
|
18
|
+
* This is necessary because dialog.push() freezes props at creation time - props won't update.
|
|
19
|
+
* Using zustand allows the dialog to subscribe to state changes reactively.
|
|
20
|
+
*/
|
|
21
|
+
export function createFileAutocompleteStore() {
|
|
22
|
+
return createStore<FileAutocompleteState>(() => ({
|
|
23
|
+
filter: '',
|
|
24
|
+
}))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type FileAutocompleteStore = ReturnType<typeof createFileAutocompleteStore>
|
|
8
28
|
|
|
9
29
|
export interface FileAutocompleteDialogProps {
|
|
30
|
+
/**
|
|
31
|
+
* NOTE: Do not pass frequently-changing values as props here.
|
|
32
|
+
* dialog.push() freezes props at creation time - they won't update.
|
|
33
|
+
* Use the `store` prop for reactive state instead.
|
|
34
|
+
*/
|
|
35
|
+
store: FileAutocompleteStore
|
|
10
36
|
onSelect: (path: string) => void
|
|
11
37
|
onClose: () => void
|
|
12
|
-
|
|
38
|
+
onNavigate: (path: string) => void
|
|
13
39
|
canChooseFiles?: boolean
|
|
14
40
|
canChooseDirectories?: boolean
|
|
15
41
|
initialDirectory?: string
|
|
16
42
|
}
|
|
17
43
|
|
|
18
|
-
function fuzzyMatch(text: string, query: string): boolean {
|
|
19
|
-
if (!query) return true
|
|
20
|
-
const lowerText = text.toLowerCase()
|
|
21
|
-
const lowerQuery = query.toLowerCase()
|
|
22
|
-
|
|
23
|
-
let queryIndex = 0
|
|
24
|
-
for (let i = 0; i < lowerText.length && queryIndex < lowerQuery.length; i++) {
|
|
25
|
-
if (lowerText[i] === lowerQuery[queryIndex]) {
|
|
26
|
-
queryIndex++
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return queryIndex === lowerQuery.length
|
|
30
|
-
}
|
|
31
|
-
|
|
32
44
|
export const FileAutocompleteDialog = ({
|
|
45
|
+
store,
|
|
33
46
|
onSelect,
|
|
34
47
|
onClose,
|
|
35
|
-
|
|
48
|
+
onNavigate,
|
|
36
49
|
canChooseFiles = true,
|
|
37
50
|
canChooseDirectories = false,
|
|
38
51
|
initialDirectory,
|
|
39
52
|
}: FileAutocompleteDialogProps): any => {
|
|
40
53
|
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
41
|
-
const [filter, setFilter] = useState(() => inputRef.current?.plainText || '')
|
|
42
54
|
const isInFocus = useIsInFocus()
|
|
55
|
+
|
|
56
|
+
// Subscribe to filter from zustand store (reactive)
|
|
57
|
+
const filter = useStore(store, (s) => s.filter)
|
|
58
|
+
|
|
59
|
+
// Reset selection when filter changes
|
|
60
|
+
const prevFilterRef = React.useRef(filter)
|
|
61
|
+
if (prevFilterRef.current !== filter) {
|
|
62
|
+
prevFilterRef.current = filter
|
|
63
|
+
setSelectedIndex(0)
|
|
64
|
+
}
|
|
43
65
|
|
|
44
|
-
|
|
45
|
-
|
|
66
|
+
// Parse the filter to extract base path and prefix
|
|
67
|
+
// When filter is empty, use initialDirectory with empty prefix to show all files
|
|
68
|
+
const { basePath, prefix } = filter
|
|
69
|
+
? parsePath(filter)
|
|
70
|
+
: { basePath: initialDirectory || '.', prefix: '' }
|
|
71
|
+
|
|
72
|
+
const { data: files = [], isLoading } = useQuery({
|
|
73
|
+
queryKey: ['file-search', basePath, prefix, canChooseFiles],
|
|
46
74
|
queryFn: async () => {
|
|
47
|
-
|
|
48
|
-
const files = await listAllFiles({
|
|
49
|
-
basePath: initialDirectory || '.',
|
|
50
|
-
maxDepth: 4,
|
|
51
|
-
maxFiles: 500,
|
|
52
|
-
includeDirectories: true,
|
|
53
|
-
})
|
|
75
|
+
const items = await searchFiles(basePath, prefix)
|
|
54
76
|
|
|
55
|
-
//
|
|
56
|
-
return
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return canChooseDirectories
|
|
77
|
+
// Always show directories for navigation, filter files based on canChooseFiles
|
|
78
|
+
return items.filter((item) => {
|
|
79
|
+
if (item.isDirectory) {
|
|
80
|
+
return true
|
|
60
81
|
}
|
|
61
82
|
return canChooseFiles
|
|
62
83
|
})
|
|
63
84
|
},
|
|
64
85
|
})
|
|
65
86
|
|
|
66
|
-
|
|
67
|
-
const filteredFiles = allFiles.filter((file) => fuzzyMatch(file, filter))
|
|
68
|
-
const visibleFiles = filteredFiles.slice(0, 10)
|
|
87
|
+
const visibleFiles = files.slice(0, 10)
|
|
69
88
|
|
|
70
89
|
useKeyboard((evt) => {
|
|
71
90
|
if (!isInFocus) return
|
|
@@ -77,32 +96,33 @@ export const FileAutocompleteDialog = ({
|
|
|
77
96
|
} else if (evt.name === 'return' || evt.name === 'tab') {
|
|
78
97
|
const selected = visibleFiles[selectedIndex]
|
|
79
98
|
if (selected) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
99
|
+
if (selected.isDirectory) {
|
|
100
|
+
if (canChooseDirectories) {
|
|
101
|
+
// Select directory
|
|
102
|
+
onSelect(selected.path)
|
|
103
|
+
onClose()
|
|
104
|
+
} else {
|
|
105
|
+
// Navigate into directory
|
|
106
|
+
onNavigate(selected.path + '/')
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
// Select file
|
|
110
|
+
onSelect(selected.path)
|
|
111
|
+
onClose()
|
|
112
|
+
}
|
|
90
113
|
}
|
|
91
|
-
} else if (evt.name && evt.name.length === 1 && !evt.ctrl && !evt.meta) {
|
|
92
|
-
// Single character input
|
|
93
|
-
const newFilter = filter + evt.name
|
|
94
|
-
setFilter(newFilter)
|
|
95
|
-
setSelectedIndex(0)
|
|
96
114
|
}
|
|
97
115
|
})
|
|
98
116
|
|
|
99
117
|
const hintText = '↑↓ navigate ⏎/tab select esc close'
|
|
100
118
|
|
|
101
119
|
return (
|
|
102
|
-
<box flexDirection='column' paddingLeft={1} paddingRight={1}>
|
|
120
|
+
<box flexDirection='column' paddingLeft={1} paddingRight={1} overflow='hidden'>
|
|
103
121
|
<box flexDirection='row'>
|
|
104
|
-
<text fg={Theme.textMuted}>Filter: </text>
|
|
105
|
-
<text fg={Theme.primary}
|
|
122
|
+
<text fg={Theme.textMuted} wrapMode='none'>Filter: </text>
|
|
123
|
+
<text fg={Theme.primary} wrapMode='none'>
|
|
124
|
+
{filter ? filter.replace(homedir, '~') : '(type to filter)'}
|
|
125
|
+
</text>
|
|
106
126
|
</box>
|
|
107
127
|
<box height={1} />
|
|
108
128
|
{isLoading ? (
|
|
@@ -111,23 +131,23 @@ export const FileAutocompleteDialog = ({
|
|
|
111
131
|
<text fg={Theme.textMuted}>No files found</text>
|
|
112
132
|
) : (
|
|
113
133
|
<>
|
|
114
|
-
{visibleFiles.map((
|
|
115
|
-
const
|
|
116
|
-
const icon = isDir ? '📁 ' : '📄 '
|
|
134
|
+
{visibleFiles.map((item, index) => {
|
|
135
|
+
const icon = item.isDirectory ? '▫ ' : '▪ '
|
|
117
136
|
return (
|
|
118
137
|
<text
|
|
119
|
-
key={
|
|
138
|
+
key={item.path}
|
|
120
139
|
fg={index === selectedIndex ? Theme.background : Theme.text}
|
|
121
140
|
bg={index === selectedIndex ? Theme.primary : Theme.backgroundPanel}
|
|
141
|
+
wrapMode='none'
|
|
122
142
|
>
|
|
123
|
-
{' '}{icon}{
|
|
143
|
+
{' '}{icon}{item.name}{item.isDirectory ? '/' : ''}
|
|
124
144
|
</text>
|
|
125
145
|
)
|
|
126
146
|
})}
|
|
127
147
|
</>
|
|
128
148
|
)}
|
|
129
149
|
<box height={1} />
|
|
130
|
-
<text fg={Theme.textMuted}>{hintText}</text>
|
|
150
|
+
<text fg={Theme.textMuted} wrapMode='none'>{hintText}</text>
|
|
131
151
|
</box>
|
|
132
152
|
)
|
|
133
153
|
}
|
|
@@ -7,7 +7,7 @@ import { useFormContext, Controller } from 'react-hook-form'
|
|
|
7
7
|
import { useFocusContext, useFormFieldDescendant } from './index'
|
|
8
8
|
import { useKeyboard } from '@opentui/react'
|
|
9
9
|
import { useIsInFocus } from 'termcast/src/internal/focus-context'
|
|
10
|
-
import { FileAutocompleteDialog } from './file-autocomplete'
|
|
10
|
+
import { FileAutocompleteDialog, createFileAutocompleteStore } from './file-autocomplete'
|
|
11
11
|
import { useFormNavigationHelpers } from './use-form-navigation'
|
|
12
12
|
import { useDialog } from 'termcast/src/internal/dialog'
|
|
13
13
|
import { LoadingText } from 'termcast/src/components/loading-text'
|
|
@@ -68,38 +68,47 @@ const FilePickerField = ({
|
|
|
68
68
|
const inputRef = React.useRef<TextareaRenderable>(null)
|
|
69
69
|
const dialog = useDialog()
|
|
70
70
|
|
|
71
|
+
// Create store once for sharing state with dialog
|
|
72
|
+
const [store] = React.useState(() => createFileAutocompleteStore())
|
|
73
|
+
|
|
71
74
|
const showAutocomplete = () => {
|
|
72
75
|
if (dialog.stack.length > 0) return
|
|
73
76
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
77
|
+
dialog.push({
|
|
78
|
+
element: (
|
|
79
|
+
<FileAutocompleteDialog
|
|
80
|
+
store={store}
|
|
81
|
+
onSelect={(path) => {
|
|
82
|
+
const currentFiles = field.value || []
|
|
83
|
+
const newFiles =
|
|
84
|
+
props.allowMultipleSelection !== false ? [...currentFiles, path] : [path]
|
|
85
|
+
field.onChange(newFiles)
|
|
86
|
+
if (props.onChange) {
|
|
87
|
+
props.onChange(newFiles)
|
|
88
|
+
}
|
|
89
|
+
inputRef.current?.setText('')
|
|
90
|
+
store.setState({ filter: '' })
|
|
91
|
+
dialog.clear()
|
|
92
|
+
}}
|
|
93
|
+
onNavigate={(path) => {
|
|
94
|
+
inputRef.current?.setText(path)
|
|
95
|
+
store.setState({ filter: path })
|
|
96
|
+
}}
|
|
97
|
+
onClose={() => {
|
|
98
|
+
dialog.clear()
|
|
99
|
+
}}
|
|
100
|
+
canChooseFiles={props.canChooseFiles}
|
|
101
|
+
canChooseDirectories={props.canChooseDirectories}
|
|
102
|
+
initialDirectory={props.initialDirectory}
|
|
103
|
+
/>
|
|
104
|
+
),
|
|
105
|
+
})
|
|
98
106
|
}
|
|
99
107
|
|
|
100
108
|
// Handle Enter key and left arrow for removing last file
|
|
101
109
|
useKeyboard((evt) => {
|
|
102
110
|
if (!isFocused || !isInFocus) return
|
|
111
|
+
if (dialog.stack.length > 0) return // Let autocomplete handle keys
|
|
103
112
|
|
|
104
113
|
// Left arrow removes last selected file when input is empty
|
|
105
114
|
if (evt.name === 'left') {
|
|
@@ -132,6 +141,7 @@ const FilePickerField = ({
|
|
|
132
141
|
props.onChange(newFiles)
|
|
133
142
|
}
|
|
134
143
|
inputRef.current?.setText('')
|
|
144
|
+
store.setState({ filter: '' })
|
|
135
145
|
}
|
|
136
146
|
}
|
|
137
147
|
})
|
|
@@ -167,9 +177,11 @@ const FilePickerField = ({
|
|
|
167
177
|
initialValue=""
|
|
168
178
|
placeholder={props.placeholder || 'Enter file path...'}
|
|
169
179
|
focused={isFocused}
|
|
180
|
+
showCursor={dialog.stack.length === 0}
|
|
170
181
|
onMouseDown={() => setFocusedField(props.id)}
|
|
171
182
|
onContentChange={() => {
|
|
172
183
|
const value = inputRef.current?.plainText || ''
|
|
184
|
+
store.setState({ filter: value })
|
|
173
185
|
if (value && isFocused) {
|
|
174
186
|
showAutocomplete()
|
|
175
187
|
}
|
|
@@ -14,6 +14,7 @@ import { InFocus, useIsInFocus } from 'termcast/src/internal/focus-context'
|
|
|
14
14
|
import { useDialog } from 'termcast/src/internal/dialog'
|
|
15
15
|
import { Theme } from 'termcast/src/theme'
|
|
16
16
|
import { useStore } from 'termcast/src/state'
|
|
17
|
+
import { Footer } from 'termcast/src/components/footer'
|
|
17
18
|
import {
|
|
18
19
|
TextAttributes,
|
|
19
20
|
ScrollBoxRenderable,
|
|
@@ -105,37 +106,29 @@ function FormFooter(): any {
|
|
|
105
106
|
const hasToast = useStore((s) => s.toast !== null)
|
|
106
107
|
|
|
107
108
|
const content = hasToast ? null : (
|
|
108
|
-
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
height={1}
|
|
128
|
-
style={{
|
|
129
|
-
paddingLeft: 1,
|
|
130
|
-
paddingRight: 1,
|
|
131
|
-
paddingTop: 1,
|
|
132
|
-
marginTop: 1,
|
|
133
|
-
flexDirection: 'row',
|
|
134
|
-
}}
|
|
135
|
-
>
|
|
136
|
-
{content}
|
|
109
|
+
<box style={{ flexDirection: 'row', gap: 3 }}>
|
|
110
|
+
<box style={{ flexDirection: 'row', gap: 1 }}>
|
|
111
|
+
<text flexShrink={0} fg={Theme.text} attributes={TextAttributes.BOLD}>
|
|
112
|
+
ctrl ↵
|
|
113
|
+
</text>
|
|
114
|
+
<text flexShrink={0} fg={Theme.textMuted}>submit</text>
|
|
115
|
+
</box>
|
|
116
|
+
<box style={{ flexDirection: 'row', gap: 1 }}>
|
|
117
|
+
<text flexShrink={0} fg={Theme.text} attributes={TextAttributes.BOLD}>
|
|
118
|
+
tab
|
|
119
|
+
</text>
|
|
120
|
+
<text flexShrink={0} fg={Theme.textMuted}>navigate</text>
|
|
121
|
+
</box>
|
|
122
|
+
<box style={{ flexDirection: 'row', gap: 1 }}>
|
|
123
|
+
<text flexShrink={0} fg={Theme.text} attributes={TextAttributes.BOLD}>
|
|
124
|
+
^k
|
|
125
|
+
</text>
|
|
126
|
+
<text flexShrink={0} fg={Theme.textMuted}>actions</text>
|
|
127
|
+
</box>
|
|
137
128
|
</box>
|
|
138
129
|
)
|
|
130
|
+
|
|
131
|
+
return <Footer>{content}</Footer>
|
|
139
132
|
}
|
|
140
133
|
|
|
141
134
|
import type { TextFieldProps, TextFieldRef } from './text-field'
|
|
@@ -334,31 +327,31 @@ export const Form: FormType = ((props) => {
|
|
|
334
327
|
return
|
|
335
328
|
}
|
|
336
329
|
|
|
337
|
-
if (evt.name === 'k' && evt.ctrl
|
|
338
|
-
// Ctrl+K shows actions (always show
|
|
339
|
-
dialog.
|
|
330
|
+
if (evt.name === 'k' && evt.ctrl) {
|
|
331
|
+
// Ctrl+K shows actions (always show panel, even without actions)
|
|
332
|
+
dialog.pushActions(
|
|
340
333
|
<FormSubmitContext.Provider value={submitContextValue}>
|
|
341
|
-
{props.actions}
|
|
334
|
+
{props.actions || <ActionPanel />}
|
|
342
335
|
</FormSubmitContext.Provider>,
|
|
343
|
-
'
|
|
336
|
+
'center',
|
|
344
337
|
)
|
|
345
338
|
} else if (evt.name === 'return' && evt.ctrl && props.actions) {
|
|
346
339
|
// Ctrl+Return executes first action directly
|
|
347
340
|
useStore.setState({ shouldAutoExecuteFirstAction: true })
|
|
348
|
-
dialog.
|
|
341
|
+
dialog.pushActions(
|
|
349
342
|
<FormSubmitContext.Provider value={submitContextValue}>
|
|
350
343
|
{props.actions}
|
|
351
344
|
</FormSubmitContext.Provider>,
|
|
352
|
-
'
|
|
345
|
+
'center',
|
|
353
346
|
)
|
|
354
347
|
} else if (evt.name === 'return' && evt.meta && props.actions) {
|
|
355
348
|
// Cmd+Return also executes first action directly
|
|
356
349
|
useStore.setState({ shouldAutoExecuteFirstAction: true })
|
|
357
|
-
dialog.
|
|
350
|
+
dialog.pushActions(
|
|
358
351
|
<FormSubmitContext.Provider value={submitContextValue}>
|
|
359
352
|
{props.actions}
|
|
360
353
|
</FormSubmitContext.Provider>,
|
|
361
|
-
'
|
|
354
|
+
'center',
|
|
362
355
|
)
|
|
363
356
|
}
|
|
364
357
|
})
|
|
@@ -376,12 +369,23 @@ export const Form: FormType = ((props) => {
|
|
|
376
369
|
<FormProvider {...methods}>
|
|
377
370
|
<FormSubmitContext.Provider value={submitContextValue}>
|
|
378
371
|
<FormScrollContext.Provider value={scrollContextValue}>
|
|
379
|
-
<FocusContext.Provider
|
|
380
|
-
|
|
372
|
+
<FocusContext.Provider
|
|
373
|
+
value={{
|
|
374
|
+
focusedField,
|
|
375
|
+
setFocusedField,
|
|
376
|
+
isLoading: isLoading || false,
|
|
377
|
+
}}
|
|
378
|
+
>
|
|
379
|
+
<box
|
|
380
|
+
flexDirection='row'
|
|
381
|
+
flexGrow={1}
|
|
382
|
+
paddingTop={2}
|
|
383
|
+
justifyContent='center'
|
|
384
|
+
>
|
|
381
385
|
<box flexGrow={0} flexDirection='column'>
|
|
382
386
|
<ScrollBox
|
|
383
387
|
ref={scrollBoxRef}
|
|
384
|
-
flexGrow={1}
|
|
388
|
+
// flexGrow={1}
|
|
385
389
|
style={{
|
|
386
390
|
rootOptions: {
|
|
387
391
|
maxWidth: FORM_MAX_WIDTH,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { TextAttributes } from '@opentui/core'
|
|
3
2
|
import { Theme } from 'termcast/src/theme'
|
|
4
3
|
import { colord } from 'colord'
|
|
4
|
+
import { useAnimationTick, TICK_DIVISORS } from 'termcast/src/components/animation-tick'
|
|
5
5
|
|
|
6
6
|
const spinnerFrames = [
|
|
7
7
|
{ char: ' ', color: Theme.accent },
|
|
@@ -11,18 +11,8 @@ const spinnerFrames = [
|
|
|
11
11
|
]
|
|
12
12
|
|
|
13
13
|
function Spinner(): any {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
React.useEffect(() => {
|
|
17
|
-
const interval = setInterval(() => {
|
|
18
|
-
setIndex((i) => (i + 1) % spinnerFrames.length)
|
|
19
|
-
}, 200)
|
|
20
|
-
return () => {
|
|
21
|
-
clearInterval(interval)
|
|
22
|
-
}
|
|
23
|
-
}, [])
|
|
24
|
-
|
|
25
|
-
const frame = spinnerFrames[index]
|
|
14
|
+
const tick = useAnimationTick(TICK_DIVISORS.SPINNER)
|
|
15
|
+
const frame = spinnerFrames[tick % spinnerFrames.length]
|
|
26
16
|
return (
|
|
27
17
|
<text flexShrink={0} fg={frame.color}>
|
|
28
18
|
<b>{frame.char}</b>
|
|
@@ -75,7 +65,7 @@ export const WithLeftBorder = ({
|
|
|
75
65
|
paddingLeft={paddingLeft}
|
|
76
66
|
border={['left']}
|
|
77
67
|
// borderStyle={isFocused ? 'heavy' : 'single'}
|
|
78
|
-
borderColor={isFocused ? Theme.accent :
|
|
68
|
+
borderColor={isFocused ? Theme.accent : Theme.text}
|
|
79
69
|
flexShrink={0}
|
|
80
70
|
flexDirection='row'
|
|
81
71
|
>
|