From 093fe532914d595b23521cb80554c5aaee5bc208 Mon Sep 17 00:00:00 2001 From: Nils Stinnesbeck Date: Tue, 20 Oct 2026 19:22:24 +0200 Subject: [PATCH] Tab cycles panels within a view; views switch only via F1-F4/Alt+digit Co-Authored-By: Claude Sonnet 5.5 --- README.md | 5 +++-- SPECS.md | 4 ++-- internal/ui/app.go | 8 +------- internal/ui/downloads.go | 16 ++++++++++++---- internal/ui/help.go | 4 ++-- internal/ui/search.go | 6 +++++- 6 files changed, 25 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index ca79e15..54cf85a 100644 --- a/README.md +++ b/README.md @@ -46,13 +46,14 @@ Keep the file private (`chmod 600`); it holds your API keys. | Key | Action | |---|---| -| `F1`–`F4`, `Alt+1`–`4`, `Tab` | switch view | +| `F1`–`F4`, `Alt+1`–`4` | switch view | +| `Tab`, `Shift+Tab` | switch panel within a view | | `?` / `F9` | help | | `/` | focus search box | | `Enter`, `a` | add selected title | | `i` | interactive release search | | `Enter`/`g`, `s`, `f` (release list) | grab, cycle sort, hide rejected | -| `p`, `d`, `t`, `b`, `h`, `r` (downloads) | pause, delete, retry, blocklist, next panel, refresh | +| `p`, `d`, `t`, `b`, `r` (downloads) | pause, delete, retry, blocklist, refresh | | `Esc` | back / close dialog | | `q`, `Ctrl+C` | quit | diff --git a/SPECS.md b/SPECS.md index 4df61f8..579d8f7 100644 --- a/SPECS.md +++ b/SPECS.md @@ -100,7 +100,7 @@ Keybindings (initial proposal): | Key | Action | |---|---| -| `Tab` / `1-4` | Switch view | +| `Tab` / `Shift+Tab` | Switch panel within the current view (never the view) | | `F1-F4` / `Alt+1-4` | Switch view, also while typing in a search box | | `/` | Focus search | | `Enter` | Search / open details / confirm | @@ -109,7 +109,7 @@ Keybindings (initial proposal): | `p` | Pause/resume SABnzbd (Downloads view) | | `d` | Delete selected download, history entry or Radarr/Sonarr queue item (with confirm) | | `t` | Retry the selected failed download (history) | -| `h` | Downloads view: cycle focus between SABnzbd queue, Radarr/Sonarr queue and history | +| `Tab` / `Shift+Tab` | Downloads view: cycle focus between SABnzbd queue, Radarr/Sonarr queue and history | | `b` | Downloads view (Radarr/Sonarr queue): blocklist the release and search for another | | `i` | Interactive release search for the selected library item (movies; series ask for a season, then an episode or the whole season) | | `g` / `Enter` | Release list: grab the selected release (rejected ones ask for confirmation) | diff --git a/internal/ui/app.go b/internal/ui/app.go index bd34441..8ac4f5e 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -164,7 +164,7 @@ func (a *App) Run() error { } func (a *App) renderStatus() { - hints := "[gray]?/F9 help · q quit · Tab/F1-F4 views · / search" + hints := "[gray]?/F9 help · q quit · F1-F4 views · Tab panels · / search" if len(a.overlays) > 0 { hints = "[gray]Esc back · Ctrl+C quit" } @@ -260,12 +260,6 @@ func (a *App) handleKey(ev *tcell.EventKey) *tcell.EventKey { return ev } switch ev.Key() { - case tcell.KeyTab: - a.switchTo((a.active + 1) % len(a.views)) - return nil - case tcell.KeyBacktab: - a.switchTo((a.active + len(a.views) - 1) % len(a.views)) - return nil case tcell.KeyRune: if ev.Rune() == 'q' { a.app.Stop() diff --git a/internal/ui/downloads.go b/internal/ui/downloads.go index efe3ff6..e327c98 100644 --- a/internal/ui/downloads.go +++ b/internal/ui/downloads.go @@ -84,13 +84,20 @@ func newDownloadsView(app *App, sab *sabnzbd.Client, sources []arrSource, interv common := func(extra map[rune]func()) func(*tcell.EventKey) *tcell.EventKey { keys := map[rune]func(){ 'r': v.refreshNow, - 'h': v.nextPanel, 'p': v.togglePause, } for r, f := range extra { keys[r] = f } return func(ev *tcell.EventKey) *tcell.EventKey { + switch ev.Key() { + case tcell.KeyTab: + v.nextPanel(1) + return nil + case tcell.KeyBacktab: + v.nextPanel(-1) + return nil + } if ev.Key() == tcell.KeyRune { if f, ok := keys[ev.Rune()]; ok { f() @@ -114,12 +121,13 @@ func newDownloadsView(app *App, sab *sabnzbd.Client, sources []arrSource, interv return v } -// nextPanel moves focus to the next panel (queue, Radarr/Sonarr queue, history). -func (v *downloadsView) nextPanel() { +// nextPanel moves focus to the next (dir 1) or previous (dir -1) panel: +// queue, Radarr/Sonarr queue, history. +func (v *downloadsView) nextPanel(dir int) { cur := v.app.app.GetFocus() for i, p := range v.panels { if p == cur { - v.app.app.SetFocus(v.panels[(i+1)%len(v.panels)]) + v.app.app.SetFocus(v.panels[(i+dir+len(v.panels))%len(v.panels)]) return } } diff --git a/internal/ui/help.go b/internal/ui/help.go index 362b3b2..e8cb4cd 100644 --- a/internal/ui/help.go +++ b/internal/ui/help.go @@ -6,7 +6,8 @@ import ( ) const helpText = `[yellow]Global[-] - F1-F4, Alt+1-4, Tab switch view + F1-F4, Alt+1-4 switch view + Tab, Shift+Tab switch panel within a view ?, F9 this help (F9 works while typing) q, Ctrl+C quit Esc close dialog / go back @@ -22,7 +23,6 @@ const helpText = `[yellow]Global[-] f hide rejected releases [yellow]Downloads[-] - h next panel p pause / resume the SABnzbd queue d delete the selected job or entry b remove from arr queue + blocklist diff --git a/internal/ui/search.go b/internal/ui/search.go index 0e6d7a3..96d28cd 100644 --- a/internal/ui/search.go +++ b/internal/ui/search.go @@ -70,7 +70,7 @@ func newSearchView(app *App, be backend, cfg config.Arr) *searchView { case tcell.KeyEnter: v.search(strings.TrimSpace(v.input.GetText())) v.app.app.SetFocus(v.list) - case tcell.KeyEscape, tcell.KeyTab: + case tcell.KeyEscape, tcell.KeyTab, tcell.KeyBacktab: v.app.app.SetFocus(v.list) } }) @@ -85,6 +85,10 @@ func newSearchView(app *App, be backend, cfg config.Arr) *searchView { v.list.SetChangedFunc(func(i int, _, _ string, _ rune) { v.showDetails(i) }) v.list.SetSelectedFunc(func(i int, _, _ string, _ rune) { v.add(i) }) v.list.SetInputCapture(func(ev *tcell.EventKey) *tcell.EventKey { + if ev.Key() == tcell.KeyTab || ev.Key() == tcell.KeyBacktab { + v.app.app.SetFocus(v.input) + return nil + } if ev.Key() != tcell.KeyRune { return ev }