// Command updater-tui is a Bubble Tea front end over the updater Python backend.
//
// The split is deliberate: Go owns presentation, Python owns every fact. Nothing
// here reads a database, compares a version, or decides whether a package is
// safe to build. It renders what the backend says and reports what the user
// chooses, which keeps exactly one implementation of each piece of logic -- the
// one that is already verified against pacman.
package main
import (
"errors"
"flag"
"fmt"
"os"
"strconv"
"strings"
tea "github.com/charmbracelet/bubbletea"
"updater-tui/internal/config"
"updater-tui/internal/plan"
"updater-tui/internal/ui"
)
func main() {
var (
python = flag.String("python", "", "interpreter to run the backend (default $UPDATER_PYTHON or python3)")
home = flag.String("repo", "", "path to the updater tree, for an uninstalled checkout")
offline = flag.Bool("offline", false, "skip everything that needs the network")
noScan = flag.Bool("fast", false, "skip the slow optional scans (arch-audit)")
version = flag.Bool("version", false, "print the version and exit")
print = flag.String("print", "", "render one frame at WxH and exit, e.g. 120x36")
show = flag.String("show", "", "with --print, open an overlay: help, confirm, detail, advisory, security, plan, warnings or loading")
pkg = flag.String("pkg", "", "with --show detail, which package to open")
)
flag.Parse()
if *version {
fmt.Println("updater-tui 0.1.0")
return
}
backend := plan.DefaultBackend()
if *python != "" {
backend.Python = *python
}
backend = backend.Discover(*home)
if *offline {
backend.Args = append(backend.Args, "--offline")
}
if *noScan {
backend.Args = append(backend.Args, "--no-security")
}
model := ui.New(backend, config.Load())
model.SetHint(backend.Hint())
// A TUI cannot be inspected without a terminal, which makes it awkward to
// test and impossible to screenshot. Rendering one frame to stdout is the
// escape hatch: same code path, no alt screen.
if *print != "" {
w, h, err := parseSize(*print)
if err != nil {
fmt.Fprintf(os.Stderr, "updater-tui: %v\n", err)
os.Exit(2)
}
frame, err := model.RenderOnce(w, h, *show, *pkg)
if errors.Is(err, ui.ErrNothingToShow) {
// said, not failed: there is no overlay to draw, which is an
// answer rather than a fault
fmt.Fprintf(os.Stderr, "updater-tui: %v\n", err)
return
}
if err != nil {
fmt.Fprintf(os.Stderr, "updater-tui: %v\n%s\n", err, backend.Hint())
os.Exit(1)
}
fmt.Println(frame)
return
}
opts := []tea.ProgramOption{tea.WithAltScreen()}
if pager := os.Getenv("UPDATER_PAGER"); pager != "" {
opts = append(opts, tea.WithoutSignalHandler())
}
if _, err := tea.NewProgram(model, opts...).Run(); err != nil {
fmt.Fprintf(os.Stderr, "updater-tui: %v\n", err)
os.Exit(1)
}
}
func parseSize(spec string) (int, int, error) {
parts := strings.SplitN(strings.ToLower(spec), "x", 2)
if len(parts) != 2 {
return 0, 0, fmt.Errorf("expected WxH, got %q", spec)
}
w, err := strconv.Atoi(strings.TrimSpace(parts[0]))
if err != nil {
return 0, 0, fmt.Errorf("bad width in %q", spec)
}
h, err := strconv.Atoi(strings.TrimSpace(parts[1]))
if err != nil {
return 0, 0, fmt.Errorf("bad height in %q", spec)
}
return w, h, nil
}