feat(TUI): add autocomplete readline style keybinds (#3717)

Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
This commit is contained in:
Timo Clasen
2025-11-04 20:28:03 +01:00
committed by GitHub
parent 52e2b40610
commit 8a9a474df6
2 changed files with 34 additions and 9 deletions

View File

@@ -22,15 +22,16 @@ export type CommandOption = DialogSelectOption & {
function init() { function init() {
const [registrations, setRegistrations] = createSignal<Accessor<CommandOption[]>[]>([]) const [registrations, setRegistrations] = createSignal<Accessor<CommandOption[]>[]>([])
const [suspendCount, setSuspendCount] = createSignal(0)
const dialog = useDialog() const dialog = useDialog()
const keybind = useKeybind() const keybind = useKeybind()
const options = createMemo(() => { const options = createMemo(() => {
return registrations().flatMap((x) => x()) return registrations().flatMap((x) => x())
}) })
const suspended = () => suspendCount() > 0
let keybinds = true
useKeyboard((evt) => { useKeyboard((evt) => {
if (!keybinds) return if (suspended()) return
for (const option of options()) { for (const option of options()) {
if (option.keybind && keybind.match(option.keybind, evt)) { if (option.keybind && keybind.match(option.keybind, evt)) {
evt.preventDefault() evt.preventDefault()
@@ -50,8 +51,9 @@ function init() {
} }
}, },
keybinds(enabled: boolean) { keybinds(enabled: boolean) {
keybinds = enabled setSuspendCount((count) => count + (enabled ? -1 : 1))
}, },
suspended,
show() { show() {
dialog.replace(() => <DialogCommand options={options()} />) dialog.replace(() => <DialogCommand options={options()} />)
}, },
@@ -83,7 +85,10 @@ export function CommandProvider(props: ParentProps) {
const keybind = useKeybind() const keybind = useKeybind()
useKeyboard((evt) => { useKeyboard((evt) => {
if (keybind.match("command_list", evt) && dialog.stack.length === 0) { if (value.suspended()) return
if (dialog.stack.length > 0) return
if (evt.defaultPrevented) return
if (keybind.match("command_list", evt)) {
evt.preventDefault() evt.preventDefault()
dialog.replace(() => <DialogCommand options={value.options} />) dialog.replace(() => <DialogCommand options={value.options} />)
return return

View File

@@ -393,11 +393,31 @@ export function Autocomplete(props: {
}, },
onKeyDown(e: KeyEvent) { onKeyDown(e: KeyEvent) {
if (store.visible) { if (store.visible) {
if (e.name === "up") move(-1) const name = e.name?.toLowerCase()
if (e.name === "down") move(1) const ctrlOnly = e.ctrl && !e.meta && !e.shift
if (e.name === "escape") hide() const isNavUp = name === "up" || (ctrlOnly && name === "p")
if (e.name === "return" || e.name === "tab") select() const isNavDown = name === "down" || (ctrlOnly && name === "n")
if (["up", "down", "return", "tab", "escape"].includes(e.name)) e.preventDefault()
if (isNavUp) {
move(-1)
e.preventDefault()
return
}
if (isNavDown) {
move(1)
e.preventDefault()
return
}
if (name === "escape") {
hide()
e.preventDefault()
return
}
if (name === "return" || name === "tab") {
select()
e.preventDefault()
return
}
} }
if (!store.visible) { if (!store.visible) {
if (e.name === "@") { if (e.name === "@") {