Initial project skeleton (M0)
Add SPECS.md, JSON config loader with env overrides and validation, and an empty tview shell with tabs and status bar. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
// Package ui contains the tview user interface.
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
type view struct {
|
||||
key rune
|
||||
name string
|
||||
body tview.Primitive
|
||||
}
|
||||
|
||||
// App is the top-level arr-tui application.
|
||||
type App struct {
|
||||
app *tview.Application
|
||||
tabs *tview.TextView
|
||||
pages *tview.Pages
|
||||
status *tview.TextView
|
||||
views []view
|
||||
active int
|
||||
}
|
||||
|
||||
// New builds the application shell: a tab bar, one page per view and a status bar.
|
||||
func New() *App {
|
||||
a := &App{
|
||||
app: tview.NewApplication(),
|
||||
tabs: tview.NewTextView().SetDynamicColors(true),
|
||||
pages: tview.NewPages(),
|
||||
status: tview.NewTextView().SetDynamicColors(true),
|
||||
}
|
||||
|
||||
a.views = []view{
|
||||
{'1', "Movies", placeholder("Movie search (M2)")},
|
||||
{'2', "Series", placeholder("Series search (M3)")},
|
||||
{'3', "Downloads", placeholder("Downloads (M4/M5)")},
|
||||
{'4', "Library", placeholder("Library (v0.2)")},
|
||||
}
|
||||
for i, v := range a.views {
|
||||
a.pages.AddPage(v.name, v.body, true, i == 0)
|
||||
}
|
||||
|
||||
layout := tview.NewFlex().SetDirection(tview.FlexRow).
|
||||
AddItem(a.tabs, 1, 0, false).
|
||||
AddItem(a.pages, 0, 1, true).
|
||||
AddItem(a.status, 1, 0, false)
|
||||
|
||||
a.app.SetRoot(layout, true).EnableMouse(false)
|
||||
a.app.SetInputCapture(a.handleKey)
|
||||
a.SetStatus("[gray]q quit · Tab / 1-4 switch view")
|
||||
a.switchTo(0)
|
||||
return a
|
||||
}
|
||||
|
||||
// SetStatus replaces the status bar text. Safe to call only from the UI goroutine;
|
||||
// use QueueUpdateDraw from other goroutines.
|
||||
func (a *App) SetStatus(text string) {
|
||||
a.status.SetText(text)
|
||||
}
|
||||
|
||||
// Run starts the event loop and blocks until the application quits.
|
||||
func (a *App) Run() error {
|
||||
return a.app.Run()
|
||||
}
|
||||
|
||||
func (a *App) handleKey(ev *tcell.EventKey) *tcell.EventKey {
|
||||
switch ev.Key() {
|
||||
case tcell.KeyCtrlC:
|
||||
a.app.Stop()
|
||||
return nil
|
||||
case tcell.KeyTab:
|
||||
a.switchTo((a.active + 1) % len(a.views))
|
||||
return nil
|
||||
case tcell.KeyBacktab:
|
||||
a.switchTo((a.active + len(a.views) - 1) % len(a.views))
|
||||
return nil
|
||||
case tcell.KeyRune:
|
||||
if ev.Rune() == 'q' {
|
||||
a.app.Stop()
|
||||
return nil
|
||||
}
|
||||
for i, v := range a.views {
|
||||
if ev.Rune() == v.key {
|
||||
a.switchTo(i)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return ev
|
||||
}
|
||||
|
||||
func (a *App) switchTo(i int) {
|
||||
a.active = i
|
||||
a.pages.SwitchToPage(a.views[i].name)
|
||||
a.renderTabs()
|
||||
}
|
||||
|
||||
func (a *App) renderTabs() {
|
||||
var s string
|
||||
for i, v := range a.views {
|
||||
if i == a.active {
|
||||
s += fmt.Sprintf("[black:white] %c %s [-:-] ", v.key, v.name)
|
||||
} else {
|
||||
s += fmt.Sprintf("[gray] %c %s [-] ", v.key, v.name)
|
||||
}
|
||||
}
|
||||
a.tabs.SetText(s)
|
||||
}
|
||||
|
||||
func placeholder(text string) tview.Primitive {
|
||||
return tview.NewTextView().SetText(text).SetTextAlign(tview.AlignCenter)
|
||||
}
|
||||
Reference in New Issue
Block a user