Files
arr-tui/internal/ui/help.go
T
nilsandClaude Opus 5.5 67f5cf6ce7 Switch the theme live
Widgets copy their colors when they are created, so they now get role
tokens (one unique RGB value per role) instead of colors, and a wrapper
around the tcell screen replaces the tokens with the current theme's
colors in SetContent. Changing the theme in the Config view swaps that
table; the next draw recolors everything. Poster colors that equal a
token are nudged off it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019reK4DUGYqDUjniWYJvwUW
2026-10-21 12:11:19 +02:00

149 lines
5.8 KiB
Go

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; 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")
}