Config view: one list with section headings, navigated with the arrow keys

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 23:53:00 +02:00
co-authored by Claude Sonnet 5.5
parent ff9e59a310
commit 56bd80f7bb
6 changed files with 206 additions and 85 deletions
+42 -45
View File
@@ -102,9 +102,10 @@ type settingsView struct {
app *App
root *tview.Flex
radarr, sonarr, sab, ui, actions *tview.Form
groups []*tview.Form // keyboard order
status *tview.TextView
form *tview.Form // every field, one below the other, with section headings
actions *tview.Form // the buttons below it
groups []*tview.Form // keyboard order: the fields, then the buttons
status *tview.TextView
inputs map[string]*tview.InputField // by setting key, e.g. "radarr.url"
refresh *tview.InputField
@@ -119,21 +120,21 @@ type settingsView struct {
}
func settingsHints() string {
return "↑↓ move · Tab next section · Space toggles · Enter presses a button"
return "↑↓ move through the fields · Space toggles · Enter presses a button · Tab jumps between fields and buttons"
}
func newSettingsView(app *App) *settingsView {
v := &settingsView{app: app, inputs: map[string]*tview.InputField{}, overridden: config.Overrides(os.Getenv)}
section := func(title string) *tview.Form {
f := tview.NewForm().SetItemPadding(0)
f.SetBorder(true).SetTitle(" " + title + " ")
f.SetBorderPadding(0, 0, 1, 1) // one row per field
return f
}
input := func(f *tview.Form, label, key string, width int) {
in := tview.NewInputField().SetLabel(label).SetFieldWidth(width)
in.SetLabelWidth(24) // the same column in every section
// One form with every field below the other: ↑↓ moves through all of them
// and the form scrolls to keep the focused field in view.
f := tview.NewForm().SetItemPadding(0)
f.SetBorder(true).SetTitle(" Settings ")
f.SetBorderPadding(0, 0, 1, 1) // one row per field
v.form = f
input := func(label, key string) {
in := tview.NewInputField().SetLabel(label)
if strings.HasSuffix(key, ".apikey") {
in.SetMaskCharacter('*')
}
@@ -145,33 +146,33 @@ func newSettingsView(app *App) *settingsView {
f.AddFormItem(in)
v.inputs[key] = in
}
service := func(f *tview.Form, name string, withDefaults bool) {
input(f, "Address", name+".url", 0)
input(f, "API key", name+".apikey", 0)
service := func(title, name string, withDefaults bool) {
f.AddFormItem(newFormHeading(title))
input("Address", name+".url")
input("API key", name+".apikey")
if withDefaults {
input(f, "Default quality profile", name+".profile", 0)
input(f, "Default root folder", name+".folder", 0)
input("Default quality profile", name+".profile")
input("Default root folder", name+".folder")
}
}
v.radarr, v.sonarr, v.sab = section("Radarr"), section("Sonarr"), section("SABnzbd")
service(v.radarr, "radarr", true)
service(v.sonarr, "sonarr", true)
service(v.sab, "sabnzbd", false)
service("Radarr", "radarr", true)
service("Sonarr", "sonarr", true)
service("SABnzbd", "sabnzbd", false)
v.ui = section("Interface")
f.AddFormItem(newFormHeading("Interface"))
v.refresh = tview.NewInputField().SetLabel("Downloads refresh (seconds)").SetFieldWidth(6)
v.refresh.SetChangedFunc(func(string) { v.edited() })
v.ui.AddFormItem(v.refresh)
v.ui.AddCheckbox("Posters", true, func(bool) { v.edited() })
v.ui.AddDropDown("Poster style", coverStyles, 0, func(string, int) { v.edited() })
v.ui.AddCheckbox("Notify about finished downloads", true, func(bool) { v.edited() })
v.ui.AddCheckbox("Ring the terminal bell too", false, func(bool) { v.edited() })
v.ui.AddCheckbox("Show API keys", false, func(shown bool) { v.maskKeys(!shown) })
v.covers = v.ui.GetFormItem(1).(*tview.Checkbox)
v.style = v.ui.GetFormItem(2).(*tview.DropDown)
v.notify = v.ui.GetFormItem(3).(*tview.Checkbox)
v.bell = v.ui.GetFormItem(4).(*tview.Checkbox)
v.showKeys = v.ui.GetFormItem(5).(*tview.Checkbox)
f.AddFormItem(v.refresh)
v.covers = tview.NewCheckbox().SetLabel("Posters").SetChangedFunc(func(bool) { v.edited() })
f.AddFormItem(v.covers)
v.style = tview.NewDropDown().SetLabel("Poster style").SetOptions(coverStyles, func(string, int) { v.edited() })
f.AddFormItem(v.style)
v.notify = tview.NewCheckbox().SetLabel("Notify about finished downloads").SetChangedFunc(func(bool) { v.edited() })
f.AddFormItem(v.notify)
v.bell = tview.NewCheckbox().SetLabel("Ring the terminal bell too").SetChangedFunc(func(bool) { v.edited() })
f.AddFormItem(v.bell)
v.showKeys = tview.NewCheckbox().SetLabel("Show API keys").SetChangedFunc(func(shown bool) { v.maskKeys(!shown) })
f.AddFormItem(v.showKeys)
v.actions = tview.NewForm().SetItemPadding(0)
v.actions.SetBorder(true).SetTitle(" Actions ")
@@ -184,19 +185,15 @@ func newSettingsView(app *App) *settingsView {
v.status.SetBorder(true).SetTitle(" Result ")
left := tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(v.radarr, 6, 0, true).
AddItem(v.sonarr, 6, 0, false).
AddItem(v.sab, 4, 0, false)
right := tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(v.ui, 8, 0, false).
AddItem(v.actions, 4, 0, false).
AddItem(v.status, 0, 1, false)
v.root = tview.NewFlex().AddItem(left, 0, 1, true).AddItem(right, 0, 1, false)
AddItem(v.form, 0, 1, true).
AddItem(v.actions, 4, 0, false)
v.root = tview.NewFlex().AddItem(left, 0, 3, true).AddItem(v.status, 0, 2, false)
v.groups = []*tview.Form{v.radarr, v.sonarr, v.sab, v.ui, v.actions}
v.groups = []*tview.Form{v.form, v.actions}
for i, g := range v.groups {
i := i
arrowNavigation(g, func() {}, &browseHooks{
// ↓ past the last field reaches the buttons, ↑ from them goes back.
edge: func(down bool) bool {
if down && i+1 < len(v.groups) {
v.focusGroup(i+1, false)
@@ -220,7 +217,7 @@ func newSettingsView(app *App) *settingsView {
return v
}
// focusGroup moves the focus to a section, to its first or last field.
// focusGroup moves the focus to a group, to its first or last field.
func (v *settingsView) focusGroup(i int, last bool) {
// tview only blurs the leaf that had the focus, not the other items of a
// form or the form itself; a form that keeps thinking it has focus would
@@ -231,7 +228,7 @@ func (v *settingsView) focusGroup(i int, last bool) {
}
}
g := v.groups[i]
idx := 0
idx := firstFocusable(g)
if last {
idx = g.GetFormItemCount() + g.GetButtonCount() - 1
}