106 lines
3.6 KiB
JavaScript
106 lines
3.6 KiB
JavaScript
import React, { useEffect, useMemo, useState } from 'react';
|
|
import { Box, Text, useInput } from 'ink';
|
|
import TextInput from 'ink-text-input';
|
|
import SelectInput from 'ink-select-input';
|
|
|
|
const h = React.createElement;
|
|
|
|
const SearchOverlay = ({
|
|
isOpen = false,
|
|
initialQuery = '',
|
|
results = [],
|
|
isSearching = false,
|
|
error = null,
|
|
onClose,
|
|
onSearch,
|
|
onOpenResult,
|
|
width = 80,
|
|
height = 24
|
|
}) => {
|
|
const [query, setQuery] = useState(initialQuery || '');
|
|
const [mode, setMode] = useState('query'); // 'query' | 'results'
|
|
|
|
useEffect(() => {
|
|
if (!isOpen) return;
|
|
setQuery(initialQuery || '');
|
|
setMode('query');
|
|
}, [isOpen, initialQuery]);
|
|
|
|
useInput((input, key) => {
|
|
if (!isOpen) return;
|
|
if (key.escape) {
|
|
if (typeof onClose === 'function') onClose();
|
|
}
|
|
if (key.tab) {
|
|
setMode(m => (m === 'query' ? 'results' : 'query'));
|
|
}
|
|
if (key.ctrl && input.toLowerCase() === 'c') {
|
|
if (typeof onClose === 'function') onClose();
|
|
}
|
|
}, { isActive: isOpen });
|
|
|
|
const items = useMemo(() => {
|
|
const max = Math.max(0, height - 8);
|
|
return results.slice(0, Math.min(200, max)).map((r, idx) => ({
|
|
label: `${r.rel}:${r.line}${r.text ? ` ${r.text}` : ''}`.slice(0, Math.max(10, width - 6)),
|
|
value: idx
|
|
}));
|
|
}, [results, width, height]);
|
|
|
|
if (!isOpen) return null;
|
|
|
|
return h(Box, {
|
|
flexDirection: 'column',
|
|
width,
|
|
height,
|
|
borderStyle: 'double',
|
|
borderColor: 'magenta',
|
|
paddingX: 1,
|
|
paddingY: 0
|
|
},
|
|
h(Box, { justifyContent: 'space-between' },
|
|
h(Text, { color: 'magenta', bold: true }, 'Search (ripgrep)'),
|
|
h(Text, { color: 'gray', dimColor: true }, 'Esc close · Enter search/open · Tab switch')
|
|
),
|
|
h(Box, { marginTop: 1, flexDirection: 'row' },
|
|
h(Text, { color: 'yellow' }, 'Query: '),
|
|
h(Box, { flexGrow: 1 },
|
|
h(TextInput, {
|
|
value: query,
|
|
focus: mode === 'query',
|
|
onChange: setQuery,
|
|
onSubmit: async () => {
|
|
if (typeof onSearch === 'function') {
|
|
setMode('results');
|
|
await onSearch(query);
|
|
}
|
|
},
|
|
placeholder: 'e.g. function handleSubmit'
|
|
})
|
|
)
|
|
),
|
|
h(Box, { marginTop: 1 },
|
|
isSearching ? h(Text, { color: 'yellow' }, 'Searching...') : null,
|
|
error ? h(Text, { color: 'red' }, error) : null,
|
|
(!isSearching && !error) ? h(Text, { color: 'gray', dimColor: true }, `${results.length} result(s)`) : null
|
|
),
|
|
h(Box, { flexDirection: 'column', flexGrow: 1, marginTop: 1 },
|
|
items.length > 0
|
|
? h(SelectInput, {
|
|
items,
|
|
isFocused: mode === 'results',
|
|
onSelect: (item) => {
|
|
const r = results[item.value];
|
|
if (r && typeof onOpenResult === 'function') onOpenResult(r);
|
|
},
|
|
itemComponent: ({ isSelected, label }) =>
|
|
h(Text, { color: isSelected ? 'cyan' : 'white', bold: isSelected, wrap: 'truncate-end' }, label)
|
|
})
|
|
: h(Text, { color: 'gray', dimColor: true }, 'No results yet. Type a query and press Enter.')
|
|
)
|
|
);
|
|
};
|
|
|
|
export default SearchOverlay;
|
|
|