Add the Config view: edit and save the settings, applied without a restart
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
+70
-22
@@ -25,6 +25,9 @@ type view struct {
|
||||
onShow, onHide func()
|
||||
// hints lists the keys that work in this view (shown in the status bar).
|
||||
hints func() string
|
||||
// captureKeys makes the global single-key shortcuts (q, ?) stand back
|
||||
// while this view is shown, because its fields take typed text.
|
||||
captureKeys bool
|
||||
}
|
||||
|
||||
// App is the top-level arr-tui application.
|
||||
@@ -46,8 +49,18 @@ type App struct {
|
||||
state map[string]serviceState // keyed by service name
|
||||
probes []probe
|
||||
|
||||
screen tcell.Screen // remembered at the first draw, for the bell
|
||||
screen tcell.Screen // remembered at the first draw, for the bell
|
||||
cfg *config.Config // the settings in use (file, environment and defaults combined)
|
||||
cfgPath string // the config file, empty if unknown
|
||||
cfgFile *config.Config // the file's contents as written, for saving
|
||||
radarr *radarr.Client
|
||||
sonarr *sonarr.Client
|
||||
sab *sabnzbd.Client
|
||||
downloads *downloadsView
|
||||
settings *settingsView
|
||||
panes []*detailPane // every detail pane, to restyle posters when settings change
|
||||
reconnect []func() // run after the service addresses or keys changed
|
||||
watchStop context.CancelFunc
|
||||
notifyDone bool // announce finished downloads
|
||||
bell bool
|
||||
covers bool // poster thumbnails enabled
|
||||
@@ -111,8 +124,10 @@ func New(cfg *config.Config) *App {
|
||||
a.gfx = newKittyGraphics(a.writeTerminal)
|
||||
}
|
||||
|
||||
a.cfg = cfg
|
||||
radarrClient := radarr.NewClient(cfg.Radarr.URL, cfg.Radarr.APIKey)
|
||||
sonarrClient := sonarr.NewClient(cfg.Sonarr.URL, cfg.Sonarr.APIKey)
|
||||
a.radarr, a.sonarr = radarrClient, sonarrClient
|
||||
a.logs = newLogView(a)
|
||||
sabClient := sabnzbd.NewClient(cfg.SABnzbd.URL, cfg.SABnzbd.APIKey)
|
||||
a.sab = sabClient
|
||||
@@ -127,6 +142,8 @@ func New(cfg *config.Config) *App {
|
||||
}},
|
||||
{"SABnzbd", sabClient.Version},
|
||||
}
|
||||
settings := newSettingsView(a)
|
||||
a.settings = settings
|
||||
calendar := newCalendarView(a, radarrClient, sonarrClient)
|
||||
library := newLibraryView(a, radarrClient, sonarrClient)
|
||||
downloads := newDownloadsView(a, sabClient,
|
||||
@@ -135,13 +152,15 @@ func New(cfg *config.Config) *App {
|
||||
{name: "Sonarr", queue: sonarrClient.Queue, remove: sonarrClient.RemoveQueueItem},
|
||||
},
|
||||
time.Duration(cfg.UI.RefreshSeconds)*time.Second)
|
||||
a.downloads = downloads
|
||||
a.views = []view{
|
||||
{key: '1', name: "Movies", body: newSearchView(a, radarrBackend{radarrClient}, cfg.Radarr).root, hints: searchHints},
|
||||
{key: '2', name: "Series", body: newSearchView(a, sonarrBackend{sonarrClient}, cfg.Sonarr).root, hints: searchHints},
|
||||
{key: '1', name: "Movies", body: newSearchView(a, radarrBackend{radarrClient}).root, hints: searchHints},
|
||||
{key: '2', name: "Series", body: newSearchView(a, sonarrBackend{sonarrClient}).root, hints: searchHints},
|
||||
{key: '3', name: "Library", body: library.root, onShow: library.activate, hints: libraryHints},
|
||||
{key: '4', name: "Downloads", body: downloads.root, onShow: downloads.activate, onHide: downloads.deactivate, hints: downloads.hints},
|
||||
{key: '5', name: "Calendar", body: calendar.root, onShow: calendar.activate, hints: calendarHints},
|
||||
{key: '6', name: "Log", body: a.logs.root, onShow: a.logs.onShow, hints: logHints},
|
||||
{key: '7', name: "Config", body: settings.root, onShow: settings.activate, hints: settingsHints, captureKeys: true},
|
||||
}
|
||||
for i, v := range a.views {
|
||||
a.pages.AddPage(v.name, v.body, true, i == 0)
|
||||
@@ -236,6 +255,7 @@ func (a *App) setNarrow(narrow bool) {
|
||||
a.narrow = narrow
|
||||
a.topBar.ResizeItem(a.services, servicesWidth(narrow), 0)
|
||||
a.renderServices()
|
||||
a.renderTabs()
|
||||
}
|
||||
|
||||
func (a *App) renderServices() {
|
||||
@@ -295,27 +315,31 @@ func (a *App) Run() error {
|
||||
}
|
||||
}()
|
||||
go a.watchServices(stop)
|
||||
if a.notifyDone {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
go a.watchDownloads(ctx, a.sab, a.bell)
|
||||
}
|
||||
a.setNotifyDownloads(a.notifyDone)
|
||||
defer a.setNotifyDownloads(false)
|
||||
return a.app.Run()
|
||||
}
|
||||
|
||||
// setNotifyDownloads starts or stops the background watch for finished downloads.
|
||||
func (a *App) setNotifyDownloads(on bool) {
|
||||
a.notifyDone = on
|
||||
if a.watchStop != nil {
|
||||
a.watchStop()
|
||||
a.watchStop = nil
|
||||
}
|
||||
if on {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
a.watchStop = cancel
|
||||
go a.watchDownloads(ctx, a.sab)
|
||||
}
|
||||
}
|
||||
|
||||
// watchServices checks all services now and then every heartbeat until stop is closed.
|
||||
func (a *App) watchServices(stop <-chan struct{}) {
|
||||
tick := time.NewTicker(heartbeat)
|
||||
defer tick.Stop()
|
||||
for {
|
||||
for _, p := range a.probes {
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
version, err := p.check(ctx)
|
||||
a.post(func() { a.SetService(p.name, version, err) })
|
||||
}()
|
||||
}
|
||||
a.probeAll()
|
||||
select {
|
||||
case <-tick.C:
|
||||
case <-stop:
|
||||
@@ -324,15 +348,34 @@ func (a *App) watchServices(stop <-chan struct{}) {
|
||||
}
|
||||
}
|
||||
|
||||
// probeAll checks every service in the background and updates the indicators.
|
||||
func (a *App) probeAll() {
|
||||
for _, p := range a.probes {
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
version, err := p.check(ctx)
|
||||
a.post(func() { a.SetService(p.name, version, err) })
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) renderStatus() {
|
||||
var hints string
|
||||
if n := len(a.overlays); n > 0 {
|
||||
hints = a.overlays[n-1].hints + " · Ctrl+C quit"
|
||||
} else {
|
||||
if h := a.views[a.active].hints; h != nil {
|
||||
hints = h() + " · "
|
||||
v := a.views[a.active]
|
||||
var parts []string
|
||||
if v.hints != nil {
|
||||
parts = append(parts, v.hints())
|
||||
}
|
||||
hints += "? help · F1-F6 · q quit"
|
||||
if v.captureKeys { // q and ? are text there
|
||||
parts = append(parts, "F9 help · F1-F7 views")
|
||||
} else {
|
||||
parts = append(parts, "? help · F1-F7 · q quit")
|
||||
}
|
||||
hints = strings.Join(parts, " · ")
|
||||
}
|
||||
a.status.SetText("[gray]" + hints)
|
||||
}
|
||||
@@ -419,7 +462,7 @@ func (a *App) onlyFullOverlays() bool {
|
||||
// typing reports whether keystrokes belong to a text field or dialog and must
|
||||
// not trigger global shortcuts.
|
||||
func (a *App) typing() bool {
|
||||
if len(a.overlays) > 0 {
|
||||
if len(a.overlays) > 0 || a.views[a.active].captureKeys {
|
||||
return true
|
||||
}
|
||||
_, isInput := a.app.GetFocus().(*tview.InputField)
|
||||
@@ -483,9 +526,14 @@ func (a *App) switchTo(i int) {
|
||||
func (a *App) renderTabs() {
|
||||
var s string
|
||||
for i, v := range a.views {
|
||||
if i == a.active {
|
||||
switch {
|
||||
case a.narrow && i == a.active:
|
||||
s += fmt.Sprintf("[black:white]%c %s[-:-] ", v.key, v.name)
|
||||
case a.narrow:
|
||||
s += fmt.Sprintf("[gray]%c %s[-] ", v.key, v.name)
|
||||
case i == a.active:
|
||||
s += fmt.Sprintf("[black:white] %c %s [-:-] ", v.key, v.name)
|
||||
} else {
|
||||
default:
|
||||
s += fmt.Sprintf("[gray] %c %s [-]", v.key, v.name)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user