Files
arr-tui/internal/ui/help.go
T

148 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. F9 works everywhere; ? only when no text field has the focus (press Esc first in a search box)"},
{"q, 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, notifications.", items: []helpItem{
{"↑↓, Tab", "move between fields / to the next section"},
{"Space", "toggle a checkbox"},
{"Enter", "press a button: Test connections checks the three services with the values typed in; Save writes the file and applies everything at once; Revert discards edits"},
{"q, ?", "are typed here as text; F9 opens this help"},
}},
{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 || (ev.Key() == tcell.KeyRune && (ev.Rune() == 'q' || ev.Rune() == '?')) {
a.closeModal()
return nil
}
return ev
})
height := min(screenH-2, strings.Count(text, "\n")+2)
a.openModalHints(tv, width, height, "↑↓ PgUp PgDn scroll · Esc, q or ? close")
}