Archived
Bubble Tea prototype for browsing repositories with many nested git submodules: collapsible tree, pins shown as ref + distance, summarized issue words with severity levels, detail pane, async loading via a Provider interface backed by mock data. Includes docs/ with requirements, state model, UX guide, architecture and decision log, plus AGENTS.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4.1 KiB
4.1 KiB
Architecture
Go, Bubble Tea (v1), Lip Gloss, Bubbles (key, help, spinner), bubblezone (mouse hit areas).
Package layout
cmd/submarine/main.go wiring: choose provider (--mock), start the program
internal/submodule/ domain model; NO UI imports
model.go Repo, Submodule, Status, PinInfo, Commit, enums
status.go Issues(), HiddenIssues(), Walk()
provider.go Provider interface
internal/submodule/mock/ fixture provider with artificial latency
internal/tui/ Bubble Tea application
app.go root model: layout, message routing, loading
keys.go key bindings (also feed the help view)
msgs.go treeLoadedMsg, statusLoadedMsg
theme/ colors, glyphs, issue words, level styles
tree/ tree component: rows, navigation, mouse, view
detail/ detail pane
header/ header line
footer/ help and flash messages
Layering rules
internal/submoduleknows nothing about the UI. Everything that decides what a state means (issue kinds, levels, hidden issues) lives here.internal/tui/themedecides how it looks: words, colors, symbols.- Components (
tree,detail,header,footer) use the theme; they never define colors or classify states themselves. - The UI only accesses data through
submodule.Provider.
Provider boundary
type Provider interface {
Tree(ctx context.Context) (*Repo, error) // structure only, fast
Status(ctx context.Context, path string) (*Status, error) // per submodule, may be slow
}
Treereturns the superproject and all submodules withParentandChildrenlinked.Statusfields are nil.Statusis called once per submodule, concurrently. It must be safe to call concurrently and must respectctx.Submodule.Path(relative to the superproject root) is the unique ID.
The mock provider (internal/submodule/mock) covers every state and sleeps
100–800 ms per Status call to exercise the loading path.
Loading flow
InitrunsTree()→treeLoadedMsg.- The app indexes submodules by path and starts one
tea.Cmdper submodule callingStatus()→statusLoadedMsg. - Each result is stored on the submodule (
StatusorErr); the view updates incrementally, including parents' hidden-issue counts. - The spinner stops ticking when nothing is loading.
Tree component
- State: roots,
expanded map[path]bool, cursor (a*Submodule), scroll offset, size. rows()flattens the visible tree into directory-header rows and submodule rows. Directory grouping usespath.Dir(RelPath)and keeps the order of first appearance.- Navigation methods (
Up,Down,Left,Right,Toggle,CollapseAll,ExpandAll,Home,End,PageUp,PageDown) are plain methods without Bubble Tea dependencies, so they are unit-tested directly. - Key mapping happens in
app.go; the tree only handles mouse messages. - Mouse zones:
toggle:<path>androw:<path>, prefixed per instance.
Rendering notes
- Each segment of a row is rendered with the row's
basestyle (background), because an inner style's reset would otherwise clear the outer background. - Widths are measured with
lipgloss.Width; truncation usesansi.Truncate, which keeps escape sequences and bubblezone markers intact. zone.Scanruns once, on the root view.
Testing
go vet ./...
go test ./...
internal/submodule: issue classification and hidden-issue aggregation.internal/tui/tree: navigation rules. Tests needzone.NewGlobal()(seeTestMain).
Manual check in a headless terminal:
go build -o /tmp/submarine ./cmd/submarine
tmux new-session -d -s sub -x 130 -y 24 /tmp/submarine
tmux send-keys -t sub Down Right
tmux capture-pane -pt sub # add -e to see colors
tmux kill-session -t sub