package ui import ( "strings" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) // helpItem is one line of the key reference: the keys and what they do. type helpItem struct{ keys, desc string } // helpSection is a titled group of items, with an optional note above them. type helpSection struct { title string note string items []helpItem } var helpSections = []helpSection{ {title: "Global", items: []helpItem{ {"F1-F7", "switch view (also from the release and episode lists)"}, {"Tab, Shift+Tab", "switch panel within a view"}, {"F9", "this help (works everywhere, also while typing)"}, {"Ctrl+C", "quit"}, {"Esc", "close a dialog / go back"}, }}, {title: "Movies and Series", items: []helpItem{ {"/", "focus the search box"}, {"Enter, a", "add the selected title (dialog: ↑↓ move, ←→ change a dropdown, Space toggles, Enter/Space opens a list)"}, {"i", "interactive release search (library titles)"}, }}, {title: "Library", items: []helpItem{ {"Tab", "switch between the movies and series lists"}, {"f", "cycle the status filter (all, missing, downloaded, monitored, unmonitored); every list has its own filters"}, {"F", "filter by file properties: quality, codec (x265, x264, AV1), HDR / DV / SDR, audio (Atmos, TrueHD, DTS, DD+ ...), language. Changes apply live behind the dialog, also while you move through an open list; Esc undoes them. Options that would leave no results are not offered. Series files are read on demand."}, {"/", "text filter for the focused list only; each list keeps its own text (Enter or Esc returns to the list)"}, {"t", "toggle monitored"}, {"d", "remove from the library (files are kept unless you tick \"Delete files\")"}, {"s", "automatic search (series: all missing, asks first)"}, {"i, Enter", "movie: release search; series: seasons and episodes"}, {"r", "reload"}, }}, {title: "Calendar (F5)", items: []helpItem{ {"[ ]", "move the two-week window back / forward by a week"}, {"t", "back to today"}, {"u", "include unmonitored items"}, {"Enter, i", "release search for the selected movie / episode"}, {"s", "automatic search"}, {"r", "reload"}, }}, {title: "Seasons and episodes", items: []helpItem{ {"Enter", "season: expand / collapse; episode: release search"}, {"i, s, t", "release search, automatic search, toggle monitored (on a season they apply to the whole season)"}, }}, {title: "Release list", items: []helpItem{ {"Enter, g", "grab the selected release"}, {"s", "cycle the sort (best, size, age)"}, {"f", "hide rejected releases"}, {"F", "filter by quality, codec, HDR / DV, audio, language (read from the release title, quality and custom formats)"}, }}, {title: "Downloads", items: []helpItem{ {"p", "pause / resume the SABnzbd queue"}, {"d", "delete the selected job or entry"}, {"b", "remove from the Radarr/Sonarr queue and blocklist"}, {"t", "retry a failed download (history)"}, {"r", "refresh now"}, }}, {title: "Config (F6)", note: "Edit the settings of the config file: service addresses and API keys, add-form defaults, refresh interval, posters, theme, notifications.", items: []helpItem{ {"↑↓", "move through all fields, one below the other, in the sections Radarr, Sonarr, SABnzbd and Interface; ↓ after the last field reaches Save and Revert"}, {"Tab", "jump between the fields and Save / Revert"}, {"Space", "toggle a checkbox; press a button"}, {"Test", "(button below the SABnzbd settings) checks the three services with the values typed in"}, {"Save, Revert", "(at the bottom) Save writes the file and applies everything at once (the theme at the next start); Revert discards edits"}, }}, {title: "Log (F7)", note: "Popups fade away by themselves; everything they said is kept here.", items: []helpItem{ {"↑↓, PgUp, PgDn", "scroll"}, {"G", "follow new entries"}, {"c", "clear the log"}, }}, } const helpKeyWidth = 16 // columns for the keys, before the descriptions // renderHelp lays the key reference out for a text area of the given width: // descriptions that do not fit wrap under themselves, not under the keys. func renderHelp(width int) string { var b strings.Builder descWidth := max(width-2-helpKeyWidth, 20) for i, sec := range helpSections { if i > 0 { b.WriteByte('\n') } b.WriteString("[yellow]" + sec.title + "[-]\n") if sec.note != "" { for _, l := range flowWrap(tview.Escape(sec.note), width-2, width-2, 0) { b.WriteString(" " + l + "\n") } } for _, it := range sec.items { keys := tview.Escape(it.keys) pad := strings.Repeat(" ", max(helpKeyWidth-tview.TaggedStringWidth(keys), 1)) lines := flowWrap(tview.Escape(it.desc), descWidth, descWidth, 0) for j, l := range lines { if j == 0 { b.WriteString(" " + keys + pad + l + "\n") } else { b.WriteString(strings.Repeat(" ", 2+helpKeyWidth) + l + "\n") } } } } return b.String() } // showHelp opens the key reference together with the connection state. func (a *App) showHelp() { screenW, screenH := 100, 40 if a.screen != nil { screenW, screenH = a.screen.Size() } width := min(100, screenW-4) textWidth := width - 3 // border and a column of air text := renderHelp(textWidth) + "\n[yellow]Connections[-]\n" for _, l := range strings.Split(strings.TrimRight(a.serviceSummary(), "\n"), "\n") { for j, w := range flowWrap(l, textWidth-2, textWidth-2, 0) { if j == 0 { text += " " + w + "\n" } else { text += " " + w + "\n" } } } tv := tview.NewTextView().SetDynamicColors(true).SetScrollable(true).SetWrap(false) tv.SetBorder(true).SetTitle(" Help — Esc to close ") tv.SetBorderPadding(0, 0, 1, 0) tv.SetText(text) tv.SetInputCapture(func(ev *tcell.EventKey) *tcell.EventKey { if ev.Key() == tcell.KeyEsc { a.closeModal() return nil } return ev }) height := min(screenH-2, strings.Count(text, "\n")+2) a.openModalHints(tv, width, height, "↑↓ PgUp PgDn scroll · Esc close") }