// 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 }