This repository has been archived on 2026-10-25. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
submarine/docs/architecture.md
T
Simon FrankenandClaude Opus 5.5 2212b24fe1 Add submarine TUI prototype with mock data
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>
2026-10-21 14:59:53 +02:00

4.1 KiB
Raw Blame History

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/submodule knows nothing about the UI. Everything that decides what a state means (issue kinds, levels, hidden issues) lives here.
  • internal/tui/theme decides 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
}
  • Tree returns the superproject and all submodules with Parent and Children linked. Status fields are nil.
  • Status is called once per submodule, concurrently. It must be safe to call concurrently and must respect ctx.
  • 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

  1. Init runs Tree() → treeLoadedMsg.
  2. The app indexes submodules by path and starts one tea.Cmd per submodule calling Status() → statusLoadedMsg.
  3. Each result is stored on the submodule (Status or Err); the view updates incrementally, including parents' hidden-issue counts.
  4. 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 uses path.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> and row:<path>, prefixed per instance.

Rendering notes

  • Each segment of a row is rendered with the row's base style (background), because an inner style's reset would otherwise clear the outer background.
  • Widths are measured with lipgloss.Width; truncation uses ansi.Truncate, which keeps escape sequences and bubblezone markers intact.
  • zone.Scan runs once, on the root view.

Testing

go vet ./...
go test ./...
  • internal/submodule: issue classification and hidden-issue aggregation.
  • internal/tui/tree: navigation rules. Tests need zone.NewGlobal() (see TestMain).

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