Archived
For a dirty submodule, list the changed files so the user can remember
what they were doing there:
Changes 5 files · 2M 1A 1D 1R · +37 −12
──────────────────────────────────
M render.go src/render/ +20 −4 3d ago
? scratch.txt 5m ago
- Status.Changes: files changed against HEAD (staged and unstaged
combined) with kind, line counts and modification time; sorted by kind,
then by size; capped at 200 with totals over all files.
- Git provider: parse `status --porcelain=v2 -z` and one
`diff HEAD --numstat -z -M`, only for dirty submodules.
- Detail pane: flat list with a colored status letter per file, like IDE
source-control views; counts and age in compact columns; up to 10 files,
every kind keeps a line, the rest is summarized as "n more".
- Fixture gains libs/work with every kind of change; mock data extended.
- Docs: F24, change preview in the UX guide, D17.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5.5 KiB
5.5 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 (git or --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/git/ read-only provider using the git CLI
exec.go git runner (environment, errors)
parse.go pure parsers for git output (status, numstat, …)
provider.go Tree, Status, pin → ref resolution
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, change letters, level styles
tree/ tree component: rows, navigation, mouse, view
detail/ detail pane; changes.go: change preview
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.
Git provider
internal/submodule/git shells out to git -C <dir> … (see D14).
- Environment:
GIT_TERMINAL_PROMPT=0,GIT_OPTIONAL_LOCKS=0(reads never takeindex.lock),LC_ALL=C. No command touches the network. Treereads each repository's.gitmodules(git config -f .gitmodules) and recurses into initialized submodules. It records, per path, the parent repository and the.gitmodulesbranch forStatus.Nameis the last element ofRelPath.Statusruns about ten small git commands per submodule (see state-model.md). At mostruntime.NumCPU()Statuscalls run at once; the others wait on a semaphore and still respectctx(D16).- Superproject: branch from
symbolic-ref, ahead/behind against@{upstream}, "fetched" from the mtime ofFETCH_HEAD.
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/submodule/git: parser unit tests, and integration tests against a fixture repository built byscripts/make-fixture.sh(skipped ifgitis not installed).internal/tui/tree: navigation rules. Tests needzone.NewGlobal()(seeTestMain).
Manual check in a headless terminal:
scripts/make-fixture.sh /tmp/subfix # superproject with every state
go build -o /tmp/submarine ./cmd/submarine
tmux new-session -d -s sub -x 130 -y 24 "/tmp/submarine /tmp/subfix/super"
tmux send-keys -t sub Down Right
tmux capture-pane -pt sub # add -e to see colors
tmux kill-session -t sub