| Layer | Choice |
|---|---|
| Language | Go 1.25+ |
| TUI framework | bubbletea |
| Components | bubbles — spinner, progress, stopwatch |
| Forms | huh — select, multi-select, confirm |
| Styles | lipgloss |
| Config format | TOML |
| CLI lib | cobra |
| Encryption | age — backup payloads |
| Self-update | minio/selfupdate |
| Packaging | goreleaser → tarballs + install.sh |
Repo layout
lfg/
├── cmd/
│ ├── lfg/main.go thin entry — calls cli.Execute()
│ └── snap/main.go screen → ANSI text helper for screenshots
├── internal/
│ ├── cli/ cobra subcommands (root, apply, backup, doctor, ...)
│ ├── preset/ bundle + tool data (built-in default + parser)
│ ├── detect/ binary + version probe (concurrent, timeout-bounded)
│ ├── installer/ brew/apt/mise/npm/custom + streaming exec
│ ├── backup/ tar + age snapshot pipeline
│ ├── doctor/ environment readiness checks
│ ├── state/ ~/.config/lfg/state.json
│ ├── version/ ldflags-injected build metadata
│ └── tui/
│ ├── app.go screen state machine + global hotkeys
│ ├── theme.go 4 palettes + huh theme builder
│ ├── layout.go Frame(): outer chrome, centering
│ ├── title.go figlet logo + gradient sweep
│ ├── welcome.go animated hero + numbered actions
│ ├── tree_picker.go collapsible bundle/tool tree
│ ├── confirm.go stats row + huh.Confirm
│ ├── progress.go channel-driven log tail wired to installer
│ ├── done.go next-steps card
│ ├── backup.go huh.Confirm (encrypt) → spinner → result
│ └── quit_confirm.go huh.Confirm dialog (`q` from anywhere)
├── .goreleaser.yaml
├── .github/workflows/
└── install.shScreen state machine
internal/tui/app.go::Model holds a screen enum + a child model per screen. The root Update dispatches messages to the active child; child models request transitions by returning a transitionMsg cmd. The transition method re-instantiates the destination child model with current selection state so each screen always sees fresh, correct data.
Selection state flows screen → screen via two maps on the root model:
selectedBundleIDs map[string]bool(set in bundle picker)selectedTools map[string]boolkeyed<bundleID>/<toolName>
Adding a screen: extend the screen enum, add a child field on Model, register dispatch in Update/forwardSize/View/transition, and emit goTo(screenX) from wherever you trigger the transition.
Layout & chrome
Frame(palette, w, h, subtitle, inner, footer, compactTitle) is the single source of chrome. Every screen ends in a Frame call. Frame builds a fully closed bulletin box: top corners + side rules + bottom corners.
CanvasW(width) is the single source of truth for inner canvas width — every screen calls it instead of recomputing the formula inline. Bounds: 56 min, 100 max.
compactTitle is height < 22 — falls back from figlet banner to a single-line gradient lfg string. xs/sm test widths depend on this contract.