diff --git a/internal/ui/app.go b/internal/ui/app.go index 10c909a..1b8e97c 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -202,7 +202,7 @@ func (a *App) openModal(p tview.Primitive, width, height int) { AddItem(p, height, 0, true). AddItem(nil, 0, 1, false), width, 0, true). AddItem(nil, 0, 1, false) - a.pushOverlay(box, p, "Tab/arrows move · Enter select · Esc cancel") + a.pushOverlay(box, p, "↑↓ move · ←→ change · Enter select · Esc cancel") } // openFull shows p above the whole view area (the tab bar and status bar stay visible). diff --git a/internal/ui/form.go b/internal/ui/form.go new file mode 100644 index 0000000..07811fa --- /dev/null +++ b/internal/ui/form.go @@ -0,0 +1,62 @@ +package ui + +import ( + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" +) + +// arrowNavigation lets a form be driven with the arrow keys: +// - Up/Down move between the fields and buttons, +// - Left/Right change the value of a closed dropdown without opening it +// (and move between the buttons), +// +// Enter or Space still opens a dropdown list. While a list is open it gets all keys. +func arrowNavigation(f *tview.Form) { + f.SetInputCapture(func(ev *tcell.EventKey) *tcell.EventKey { + item, button := f.GetFocusedItemIndex() + if item < 0 && button < 0 { + return ev + } + dd, _ := focusedDropDown(f, item) + if dd != nil && dd.IsOpen() { + return ev + } + total := f.GetFormItemCount() + f.GetButtonCount() + pos := item + if button >= 0 { + pos = f.GetFormItemCount() + button + } + switch ev.Key() { + case tcell.KeyDown: + f.SetFocus(min(pos+1, total-1)) + case tcell.KeyUp: + f.SetFocus(max(pos-1, 0)) + case tcell.KeyLeft, tcell.KeyRight: + step := 1 + if ev.Key() == tcell.KeyLeft { + step = -1 + } + switch { + case dd != nil: + n := dd.GetOptionCount() + cur, _ := dd.GetCurrentOption() + dd.SetCurrentOption((cur + step + n) % n) + case button >= 0: + f.SetFocus(min(max(pos+step, f.GetFormItemCount()), total-1)) + default: + return ev + } + default: + return ev + } + return nil + }) +} + +func focusedDropDown(f *tview.Form, item int) (*tview.DropDown, bool) { + if item < 0 { + return nil, false + } + dd, ok := f.GetFormItem(item).(*tview.DropDown) + return dd, ok +} diff --git a/internal/ui/form_test.go b/internal/ui/form_test.go new file mode 100644 index 0000000..b63008b --- /dev/null +++ b/internal/ui/form_test.go @@ -0,0 +1,50 @@ +package ui + +import ( + "testing" + + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" +) + +func TestArrowNavigation(t *testing.T) { + f := tview.NewForm(). + AddDropDown("A", []string{"x", "y", "z"}, 0, nil). + AddCheckbox("B", true, nil). + AddButton("Add", nil).AddButton("Cancel", nil) + arrowNavigation(f) + var focus func(p tview.Primitive) + focus = func(p tview.Primitive) { p.Focus(focus) } + f.Focus(focus) + cap := f.GetInputCapture() + send := func(key tcell.Key) { cap(tcell.NewEventKey(key, 0, 0)) } + + dd := f.GetFormItem(0).(*tview.DropDown) + send(tcell.KeyRight) + if i, _ := dd.GetCurrentOption(); i != 1 { + t.Fatalf("Right: option %d, want 1", i) + } + send(tcell.KeyLeft) + send(tcell.KeyLeft) + if i, _ := dd.GetCurrentOption(); i != 2 { + t.Fatalf("Left wraps: option %d, want 2", i) + } + send(tcell.KeyDown) + if item, _ := f.GetFocusedItemIndex(); item != 1 { + t.Fatalf("Down: item %d, want 1", item) + } + send(tcell.KeyDown) + send(tcell.KeyDown) + send(tcell.KeyDown) // clamps at the last button + if _, b := f.GetFocusedItemIndex(); b != 1 { + t.Fatalf("button %d, want 1", b) + } + send(tcell.KeyLeft) + if _, b := f.GetFocusedItemIndex(); b != 0 { + t.Fatalf("Left on buttons: %d, want 0", b) + } + send(tcell.KeyUp) + if item, _ := f.GetFocusedItemIndex(); item != 1 { + t.Fatalf("Up from first button: item %d, want 1", item) + } +} diff --git a/internal/ui/help.go b/internal/ui/help.go index 151f90c..59a3e76 100644 --- a/internal/ui/help.go +++ b/internal/ui/help.go @@ -14,7 +14,8 @@ const helpText = `[yellow]Global[-] [yellow]Movies and Series[-] / focus the search box - Enter, a add the selected title + Enter, a add the selected title (dialog: ↑↓ move, ←→ change a + dropdown, Space toggles, Enter/Space opens a list) i interactive release search (library titles) [yellow]Library[-] diff --git a/internal/ui/search.go b/internal/ui/search.go index 96d28cd..7225d8f 100644 --- a/internal/ui/search.go +++ b/internal/ui/search.go @@ -325,6 +325,7 @@ func (v *searchView) add(i int) { }) form.AddButton("Cancel", v.app.closeModal) form.SetCancelFunc(v.app.closeModal) + arrowNavigation(form) form.SetBorder(true).SetTitle(" Add " + tview.Escape(r.title()) + " ") v.app.openModal(form, 60, height)