feat(cli): improve search command and user interaction

The search command in the CLI is enhanced to recognize search queries as command arguments. An improved interaction with the search box is also introduced, including key event handlers for up and down arrows and  escape key, allowing users to navigate more intuitively. keyboard-based selection of suggestions is now implemented and the helper message is updated to match these interaction changes.
This commit is contained in:
p-sw 2024-06-07 21:54:34 +09:00
parent 3e0d29fd56
commit e222b9f7a2
2 changed files with 33 additions and 7 deletions

View File

@ -1,17 +1,21 @@
import {Command} from '@oclif/core' import {Command, Args} from '@oclif/core'
import {render} from 'ink' import {render} from 'ink'
import {SearchBox} from '../components/SearchBox.js' import {SearchBox} from '../components/SearchBox.js'
import {getAvailableComponentNames, getRegistry} from '../helpers/registry.js' import {getAvailableComponentNames, getRegistry} from '../helpers/registry.js'
import React from 'react' import React from 'react'
export default class Search extends Command { export default class Search extends Command {
static override args = {
query: Args.string({description: 'search query'}),
}
static override description = 'Search components.' static override description = 'Search components.'
static override examples = ['<%= config.bin %> <%= command.id %>'] static override examples = ['<%= config.bin %> <%= command.id %>']
static override flags = {}
public async run(): Promise<void> { public async run(): Promise<void> {
const {args} = await this.parse(Search)
const registryResult = await getRegistry() const registryResult = await getRegistry()
if (!registryResult.ok) { if (!registryResult.ok) {
this.error(registryResult.message) this.error(registryResult.message)
@ -19,6 +23,13 @@ export default class Search extends Command {
const registry = registryResult.registry const registry = registryResult.registry
const componentNames = await getAvailableComponentNames(registry) const componentNames = await getAvailableComponentNames(registry)
render(<SearchBox components={componentNames} helper={'Press Ctrl+C to quit'} />) await render(
<SearchBox
components={componentNames}
initialQuery={args.query}
helper={'Press ESC to quit'}
onKeyDown={(_, k, app) => k.escape && app.exit()}
/>,
).waitUntilExit()
} }
} }

View File

@ -3,20 +3,23 @@ import {getSuggestion} from '../helpers/search.js'
import Input from 'ink-text-input' import Input from 'ink-text-input'
import {Divider} from './Divider.js' import {Divider} from './Divider.js'
import Spinner from 'ink-spinner' import Spinner from 'ink-spinner'
import {Box, Text} from 'ink' import {Box, Text, useInput, useApp, type Key} from 'ink'
export function SearchBox({ export function SearchBox({
components, components,
helper, helper,
initialQuery, initialQuery,
onKeyDown,
}: { }: {
components: string[] components: string[]
helper: string helper: string
initialQuery?: string initialQuery?: string
onKeyDown?: (i: string, k: Key, app: ReturnType<typeof useApp>) => void
}) { }) {
const [query, setQuery] = useState<string>(initialQuery ?? '') const [query, setQuery] = useState<string>(initialQuery ?? '')
const [isLoading, setLoading] = useState<boolean>(false) const [isLoading, setLoading] = useState<boolean>(false)
const [suggestions, setSuggestions] = useState<string[]>([]) const [suggestions, setSuggestions] = useState<string[]>([])
const [selected, setSelected] = useState<number>(0)
useEffect(() => { useEffect(() => {
setLoading(true) setLoading(true)
@ -26,6 +29,18 @@ export function SearchBox({
}) })
}, [query]) }, [query])
const app = useApp()
useInput((i, k) => {
if (k.downArrow) {
setSelected((p) => (p >= suggestions.length - 1 ? 0 : p + 1))
}
if (k.upArrow) {
setSelected((p) => (p <= 0 ? suggestions.length - 1 : p - 1))
}
onKeyDown?.(i, k, app)
})
return ( return (
<Box width={50} display={'flex'} flexDirection={'column'} columnGap={4}> <Box width={50} display={'flex'} flexDirection={'column'} columnGap={4}>
<Text color={'gray'}>{helper}</Text> <Text color={'gray'}>{helper}</Text>
@ -40,10 +55,10 @@ export function SearchBox({
<Spinner /> <Spinner />
) : ( ) : (
<Box display={'flex'} flexDirection={'column'} columnGap={1}> <Box display={'flex'} flexDirection={'column'} columnGap={1}>
{suggestions.map((name) => { {suggestions.map((name, index) => {
return ( return (
<Box borderStyle={'round'} key={name}> <Box borderStyle={'round'} key={name}>
<Text>{name}</Text> <Text backgroundColor={selected === index ? 'white' : undefined}>{name}</Text>
</Box> </Box>
) )
})} })}