Remove digit view shortcuts; check all services at startup and every 30s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
+49
-7
@@ -2,6 +2,7 @@
|
|||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -40,8 +41,19 @@ type App struct {
|
|||||||
|
|
||||||
services *tview.TextView
|
services *tview.TextView
|
||||||
state map[string]serviceState // keyed by service name
|
state map[string]serviceState // keyed by service name
|
||||||
|
probes []probe
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// probe checks that one service answers; it returns its version.
|
||||||
|
type probe struct {
|
||||||
|
name string
|
||||||
|
check func(ctx context.Context) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// heartbeat is how often the connection indicators are refreshed in the
|
||||||
|
// background, so a service shows up as connected without visiting its view.
|
||||||
|
const heartbeat = 30 * time.Second
|
||||||
|
|
||||||
// serviceState is the last known connection state of a backend service.
|
// serviceState is the last known connection state of a backend service.
|
||||||
type serviceState struct {
|
type serviceState struct {
|
||||||
known bool
|
known bool
|
||||||
@@ -77,8 +89,20 @@ func New(cfg *config.Config) *App {
|
|||||||
radarrClient := radarr.NewClient(cfg.Radarr.URL, cfg.Radarr.APIKey)
|
radarrClient := radarr.NewClient(cfg.Radarr.URL, cfg.Radarr.APIKey)
|
||||||
sonarrClient := sonarr.NewClient(cfg.Sonarr.URL, cfg.Sonarr.APIKey)
|
sonarrClient := sonarr.NewClient(cfg.Sonarr.URL, cfg.Sonarr.APIKey)
|
||||||
a.logs = newLogView(a)
|
a.logs = newLogView(a)
|
||||||
|
sabClient := sabnzbd.NewClient(cfg.SABnzbd.URL, cfg.SABnzbd.APIKey)
|
||||||
|
a.probes = []probe{
|
||||||
|
{"Radarr", func(ctx context.Context) (string, error) {
|
||||||
|
st, err := radarrClient.SystemStatus(ctx)
|
||||||
|
return st.Version, err
|
||||||
|
}},
|
||||||
|
{"Sonarr", func(ctx context.Context) (string, error) {
|
||||||
|
st, err := sonarrClient.SystemStatus(ctx)
|
||||||
|
return st.Version, err
|
||||||
|
}},
|
||||||
|
{"SABnzbd", sabClient.Version},
|
||||||
|
}
|
||||||
library := newLibraryView(a, radarrClient, sonarrClient)
|
library := newLibraryView(a, radarrClient, sonarrClient)
|
||||||
downloads := newDownloadsView(a, sabnzbd.NewClient(cfg.SABnzbd.URL, cfg.SABnzbd.APIKey),
|
downloads := newDownloadsView(a, sabClient,
|
||||||
[]arrSource{
|
[]arrSource{
|
||||||
{name: "Radarr", queue: radarrClient.Queue, remove: radarrClient.RemoveQueueItem},
|
{name: "Radarr", queue: radarrClient.Queue, remove: radarrClient.RemoveQueueItem},
|
||||||
{name: "Sonarr", queue: sonarrClient.Queue, remove: sonarrClient.RemoveQueueItem},
|
{name: "Sonarr", queue: sonarrClient.Queue, remove: sonarrClient.RemoveQueueItem},
|
||||||
@@ -170,9 +194,33 @@ func (a *App) post(f func()) {
|
|||||||
|
|
||||||
// Run starts the event loop and blocks until the application quits.
|
// Run starts the event loop and blocks until the application quits.
|
||||||
func (a *App) Run() error {
|
func (a *App) Run() error {
|
||||||
|
stop := make(chan struct{})
|
||||||
|
defer close(stop)
|
||||||
|
go a.watchServices(stop)
|
||||||
return a.app.Run()
|
return a.app.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) })
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-tick.C:
|
||||||
|
case <-stop:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) renderStatus() {
|
func (a *App) renderStatus() {
|
||||||
var hints string
|
var hints string
|
||||||
if n := len(a.overlays); n > 0 {
|
if n := len(a.overlays); n > 0 {
|
||||||
@@ -304,12 +352,6 @@ func (a *App) handleKey(ev *tcell.EventKey) *tcell.EventKey {
|
|||||||
a.app.Stop()
|
a.app.Stop()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
for i, v := range a.views {
|
|
||||||
if ev.Rune() == v.key {
|
|
||||||
a.switchTo(i)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return ev
|
return ev
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gdamore/tcell/v2"
|
||||||
|
|
||||||
|
"git.stinnesbeck.com/nils/arr-tui/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newTestApp(t *testing.T) *App {
|
||||||
|
t.Helper()
|
||||||
|
// Unreachable services: the views only start background requests that fail.
|
||||||
|
cfg := &config.Config{
|
||||||
|
Radarr: config.Arr{Service: config.Service{URL: "http://127.0.0.1:1", APIKey: "k"}},
|
||||||
|
Sonarr: config.Arr{Service: config.Service{URL: "http://127.0.0.1:1", APIKey: "k"}},
|
||||||
|
SABnzbd: config.Service{URL: "http://127.0.0.1:1", APIKey: "k"},
|
||||||
|
UI: config.UI{RefreshSeconds: 3},
|
||||||
|
}
|
||||||
|
return New(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOnlyFunctionKeysSwitchViews(t *testing.T) {
|
||||||
|
a := newTestApp(t)
|
||||||
|
for _, r := range "12345\t" {
|
||||||
|
key := tcell.NewEventKey(tcell.KeyRune, r, 0)
|
||||||
|
if r == '\t' {
|
||||||
|
key = tcell.NewEventKey(tcell.KeyTab, 0, 0)
|
||||||
|
}
|
||||||
|
a.handleKey(key)
|
||||||
|
if a.active != 0 {
|
||||||
|
t.Fatalf("key %q switched to view %d", r, a.active)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, k := range []tcell.Key{tcell.KeyF1, tcell.KeyF2, tcell.KeyF3, tcell.KeyF4, tcell.KeyF5} {
|
||||||
|
if a.handleKey(tcell.NewEventKey(k, 0, 0)) != nil || a.active != i {
|
||||||
|
t.Errorf("F%d: active view %d, want %d", i+1, a.active, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user