Archived
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>
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
# submarine — documentation
|
||||
|
||||
submarine is a terminal UI for git repositories with many (recursively nested)
|
||||
submodules. It shows **only the submodules**, as a collapsible tree, and
|
||||
describes each pin as a **branch plus distance** (`origin/main ↓1`) instead of a
|
||||
bare commit hash.
|
||||
|
||||
## Documents
|
||||
|
||||
| Document | Contents |
|
||||
|---|---|
|
||||
| [requirements.md](requirements.md) | What the tool must do: functional and non-functional requirements |
|
||||
| [state-model.md](state-model.md) | How a submodule's state is determined, summarized into issues, and how pins are resolved to refs |
|
||||
| [ux-guide.md](ux-guide.md) | Layout, attention hierarchy, colors, wording, navigation and key bindings |
|
||||
| [architecture.md](architecture.md) | Package layout, the provider boundary, async loading, testing |
|
||||
| [decisions.md](decisions.md) | Decision log with rationale (ADR style) |
|
||||
|
||||
## Status
|
||||
|
||||
**Prototype.** The UI is complete for browsing and runs on mock data
|
||||
(`internal/submodule/mock`). There is no git backend yet, and actions
|
||||
(`u`, `b`, `f`, `/`, `!`) only show a "not implemented" message.
|
||||
|
||||
Next steps, roughly in order:
|
||||
|
||||
1. Git provider implementing `submodule.Provider` (read-only).
|
||||
2. Actions: fetch, checkout pin (`submodule update`), bump pin to tip, init.
|
||||
3. Filter (`/`) and "problems only" (`!`) views.
|
||||
4. Refresh after actions and on demand.
|
||||
@@ -0,0 +1,105 @@
|
||||
# Architecture
|
||||
|
||||
Go, [Bubble Tea](https://github.com/charmbracelet/bubbletea) (v1),
|
||||
[Lip Gloss](https://github.com/charmbracelet/lipgloss),
|
||||
[Bubbles](https://github.com/charmbracelet/bubbles) (key, help, spinner),
|
||||
[bubblezone](https://github.com/lrstanley/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
|
||||
|
||||
```go
|
||||
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
|
||||
|
||||
```sh
|
||||
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:
|
||||
|
||||
```sh
|
||||
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
|
||||
```
|
||||
@@ -0,0 +1,101 @@
|
||||
# Decision log
|
||||
|
||||
Each entry: context, decision, consequences. Newest last.
|
||||
|
||||
## D1 — Show pins as ref + distance, not hashes
|
||||
|
||||
**Context.** Hashes don't tell users whether a pin is current.
|
||||
**Decision.** Resolve each pin to a ref and show the distance (`origin/main ↓1`).
|
||||
The resolution order is fixed and the applied rule is shown in the detail pane
|
||||
(see [state-model.md](state-model.md#resolving-a-pin-to-a-ref)).
|
||||
**Consequences.** Resolution is a heuristic; showing the rule keeps it
|
||||
transparent.
|
||||
|
||||
## D2 — Compare three commits: pinned, checked out, tip
|
||||
|
||||
**Context.** The first mockup showed one branch for both the checkout and the
|
||||
pin, so a mismatch was invisible.
|
||||
**Decision.** Model all three commits. The tree's ref column always describes
|
||||
the **pin**; checkout drift is an issue (`off pin`).
|
||||
**Consequences.** The detail pane shows Pinned / Checkout / Tip rows.
|
||||
|
||||
## D3 — Raw dimensions in the domain, summarized issues in the UI
|
||||
|
||||
**Context.** States are not mutually exclusive (a submodule can be off pin and
|
||||
dirty), but showing every dimension made rows noisy (14 symbols).
|
||||
**Decision.** `Status` stores independent dimensions. `Status.Issues()`
|
||||
summarizes them into five kinds with three levels. The tree shows only the
|
||||
most severe issue (+n); the detail pane shows all.
|
||||
**Consequences.** One place defines what a problem is; a git backend gets
|
||||
the same behavior for free.
|
||||
|
||||
## D4 — Attention follows severity
|
||||
|
||||
**Context.** Healthy rows competed for attention with dangerous ones.
|
||||
**Decision.** Three levels: quiet (muted row, no word), warn (yellow),
|
||||
danger (red bold). Behind/ahead, staged pin and detached HEAD are quiet.
|
||||
**Consequences.** A long list reads as grey, with problems standing out.
|
||||
|
||||
## D5 — Words instead of glyphs
|
||||
|
||||
**Context.** Symbols (`≠ ✚ ○ ⚠ ●`) were found unintuitive.
|
||||
**Decision.** Short words: `unpushed`, `conflict`, `missing`, `off pin`,
|
||||
`changed` (plus `error`). Symbols remain only for arrows and tree markers.
|
||||
**Consequences.** Status column is 14 columns wide; no legend needed.
|
||||
|
||||
## D6 — "Unpushed pin" is the most severe state
|
||||
|
||||
**Context.** Committing a superproject whose pin isn't on any remote breaks
|
||||
everyone else's checkout.
|
||||
**Decision.** `unpushed` is Danger, listed first, and its `↑n` is red.
|
||||
|
||||
## D7 — Hidden issues bubble up to collapsed parents
|
||||
|
||||
**Context.** Only the first level is shown by default, so nested problems
|
||||
would be invisible.
|
||||
**Decision.** Collapsed parents show `·k` (issues in all descendants), colored
|
||||
by the worst one. Problems in a child do **not** mark the parent itself as
|
||||
`changed`.
|
||||
|
||||
## D8 — Collapsed by default, branch-first navigation
|
||||
|
||||
**Decision.** Only the first level is visible. `→` expands and enters the
|
||||
first child, `←` collapses and goes to the parent, `space` toggles,
|
||||
`ctrl+→/←` expand/collapse all (see [ux-guide.md](ux-guide.md#navigation)).
|
||||
`←` on an expanded node collapses it in place, so a node can be closed
|
||||
without leaving it. Remembered expansion is kept on collapse, but reset by
|
||||
"collapse all".
|
||||
|
||||
## D9 — Directory headers are path groups, not nesting
|
||||
|
||||
**Decision.** Submodules are grouped by the directory of their path relative
|
||||
to the parent repo. Headers (`src/`, `modules/`) are muted, non-selectable,
|
||||
and skipped by navigation. Nesting is expressed by indentation under the
|
||||
parent submodule.
|
||||
|
||||
## D10 — ANSI 16 colors
|
||||
|
||||
**Context.** Fixed 256-colors (e.g. violet 63) ignored the user's terminal
|
||||
theme and were unreadable on light backgrounds.
|
||||
**Decision.** Use ANSI 0–15 for all meaning (red, yellow, blue, cyan, grey,
|
||||
default foreground). Only background shades use `AdaptiveColor`. Detect the
|
||||
background before Bubble Tea starts.
|
||||
|
||||
## D11 — Provider boundary and async loading
|
||||
|
||||
**Decision.** The UI accesses data only through `submodule.Provider`:
|
||||
`Tree()` for the fast structure, `Status(path)` per submodule, loaded
|
||||
concurrently with a spinner per row. The prototype uses a mock provider with
|
||||
artificial latency.
|
||||
**Consequences.** A git backend can be added without UI changes; the UI
|
||||
stays responsive with many submodules.
|
||||
|
||||
## D12 — No network on startup
|
||||
|
||||
**Decision.** Work from local refs and show their age; fetching is an
|
||||
explicit action (`f`).
|
||||
|
||||
## D13 — Mouse support via bubblezone
|
||||
|
||||
**Decision.** Keep mouse support (click to select/toggle, wheel to move)
|
||||
using bubblezone markers per row.
|
||||
@@ -0,0 +1,104 @@
|
||||
# Requirements
|
||||
|
||||
## Problem
|
||||
|
||||
In repositories with many submodules, `git submodule status` shows pinned
|
||||
commit hashes. It doesn't answer the questions users actually have:
|
||||
|
||||
- Which branch is this submodule on, and is the pin current?
|
||||
- Which submodules need my attention, and which are fine?
|
||||
- Is anything dangerous, e.g. a pin that other people can't fetch?
|
||||
|
||||
## Goals
|
||||
|
||||
- Show **only submodules** (not files), recursively nested, as a tree.
|
||||
- Describe pins as **ref + distance** instead of a commit hash.
|
||||
- Let **problems stand out** and keep healthy submodules quiet.
|
||||
- Work comfortably with **dozens to hundreds** of submodules.
|
||||
|
||||
## Functional requirements
|
||||
|
||||
### Tree
|
||||
|
||||
- **F1** Show all submodules of the superproject, including nested submodules
|
||||
of submodules, at any depth.
|
||||
- **F2** Group submodules by the directory of their path relative to the
|
||||
parent repository. A directory with submodules is shown as a non-selectable
|
||||
header line (e.g. `src/`, `modules/`). Submodules at the repository root
|
||||
have no header.
|
||||
- **F3** Nested submodules are shown under their parent submodule. A
|
||||
directory header inside a submodule (e.g. `modules/`) means the nested
|
||||
submodule lives in that subdirectory of the parent.
|
||||
- **F4** On startup only the first level is shown; every submodule with
|
||||
nested submodules is **collapsed**.
|
||||
- **F5** A collapsed submodule shows the number of its **direct** children,
|
||||
e.g. `[2]`.
|
||||
- **F6** A collapsed submodule shows the number of issues **hidden** in all
|
||||
its descendants (`·2`), colored by the most severe hidden issue.
|
||||
- **F7** Expansion state of descendants is remembered when a parent is
|
||||
collapsed and restored when it is expanded again (except after
|
||||
"collapse all", which resets it).
|
||||
|
||||
### State per submodule
|
||||
|
||||
- **F10** Each submodule shows at most **one** state word: the most severe
|
||||
issue, plus `+n` if there are more. Submodules without issues show no word.
|
||||
See [state-model.md](state-model.md) for the full rules.
|
||||
- **F11** Each submodule shows its pin as a ref (`origin/main`, a tag, or a
|
||||
short SHA as a fallback) and the distance to that ref (`↑n`, `↓n`, `↑n↓m`).
|
||||
- **F12** A pin that is not reachable from any remote ref ("unpushed") is
|
||||
the most prominent state.
|
||||
- **F13** States load asynchronously per submodule; a submodule shows a
|
||||
spinner until its state is known. Loading errors are shown as `error`.
|
||||
|
||||
### Detail pane
|
||||
|
||||
- **F20** Shows the selected submodule: full path, name, all issues in
|
||||
words, staged-pin and detached-HEAD notes.
|
||||
- **F21** Shows the resolved ref, how it was resolved, and three commits:
|
||||
**Pinned**, **Checkout** and **Tip** (SHA + subject). A checkout that
|
||||
differs from the pin is highlighted.
|
||||
- **F22** Lists the commits between pin and tip (SHA, subject, author, age)
|
||||
with a title stating ahead/behind/diverged.
|
||||
- **F23** Shows child count, issues below, and the remote URL.
|
||||
|
||||
### Header and footer
|
||||
|
||||
- **F30** The header shows the superproject path, its branch and
|
||||
ahead/behind, the number of submodules, a count per issue kind (most
|
||||
severe first), staged pins, how many are still loading, and the time since
|
||||
the last fetch.
|
||||
- **F31** The footer shows key hints; `?` toggles the full help. It
|
||||
temporarily shows short messages (e.g. "not implemented").
|
||||
|
||||
### Navigation
|
||||
|
||||
- **F40** Keyboard navigation as specified in
|
||||
[ux-guide.md](ux-guide.md#navigation).
|
||||
- **F41** Mouse: click a row to select it, click `▸`/`▾` to toggle, scroll
|
||||
wheel moves the selection.
|
||||
|
||||
### Actions (planned, not implemented)
|
||||
|
||||
| Key | Action |
|
||||
|---|---|
|
||||
| `u` | Check out the pinned commit (`git submodule update`) |
|
||||
| `b` | Bump the pin to the tip of its ref |
|
||||
| `f` | Fetch remotes |
|
||||
| `/` | Filter by name/path |
|
||||
| `!` | Show only submodules with issues |
|
||||
|
||||
## Non-functional requirements
|
||||
|
||||
- **N1** Startup must not block on the network. The tool works from local
|
||||
refs and shows how old they are ("fetched 2m ago"); fetching is explicit.
|
||||
- **N2** Loading many submodules must not freeze the UI (async, per
|
||||
submodule).
|
||||
- **N3** Colors follow the user's terminal theme (ANSI 16 colors); the UI
|
||||
works on dark and light backgrounds.
|
||||
- **N4** Meaning never depends on color alone: every state has a word.
|
||||
- **N5** Works without a Nerd Font. `SUBMARINE_ASCII=1` replaces the few
|
||||
remaining Unicode symbols with ASCII.
|
||||
- **N6** Works in narrow terminals (detail pane hides below 90 columns).
|
||||
- **N7** The UI is independent of the data source: replacing the mock
|
||||
provider with a git provider requires no UI changes.
|
||||
@@ -0,0 +1,108 @@
|
||||
# State model
|
||||
|
||||
Code: `internal/submodule/model.go`, `internal/submodule/status.go`.
|
||||
|
||||
## Three commits per submodule
|
||||
|
||||
Every submodule involves three commits. All state is derived from comparing
|
||||
them.
|
||||
|
||||
| Commit | Source |
|
||||
|---|---|
|
||||
| **Pinned** | The gitlink recorded in the superproject (HEAD or index) |
|
||||
| **Checked out** | The submodule's `HEAD` |
|
||||
| **Tip** | The tip of the ref the pin was resolved to, e.g. `origin/main` |
|
||||
|
||||
This gives two independent questions:
|
||||
|
||||
- **Is the checkout on the pin?** → `off pin` if not.
|
||||
- **Where is the pin relative to its ref?** → `↑n` / `↓n` / `↑n↓m`.
|
||||
|
||||
## Raw status dimensions
|
||||
|
||||
`submodule.Status` stores the raw, independent dimensions. A submodule can be
|
||||
in several at once.
|
||||
|
||||
| Field | Values |
|
||||
|---|---|
|
||||
| `Presence` | `PresenceOK`, `NotInitialized`, `DirMissing` |
|
||||
| `Checkout` | `CheckoutMatch`, `CheckoutDiffers`, `CheckoutDetached` (on the pin, detached HEAD; informational) |
|
||||
| `Worktree` | `WorktreeClean`, `WorktreeModified`, `WorktreeUntracked`, `WorktreeConflict` |
|
||||
| `PinStaged` | Pin changed in the superproject index but not committed |
|
||||
| `Pin` | `Ref`, `Source`, `Ahead`, `Behind`, `Pushed` |
|
||||
| `Pinned`, `CheckedOut`, `Tip` | Commits (SHA, subject, author, time) |
|
||||
| `Between` | Commits that make up the pin ↔ tip difference |
|
||||
|
||||
`Status == nil` means "still loading". `Submodule.Err` is set if loading
|
||||
failed.
|
||||
|
||||
## Issues: summarizing the dimensions
|
||||
|
||||
The UI never shows raw dimensions in the tree. `Status.Issues()` summarizes
|
||||
them into **issue kinds**, each with a **level**. This is the single source of
|
||||
truth for what counts as a problem.
|
||||
|
||||
| Issue (word) | Combines | Level |
|
||||
|---|---|---|
|
||||
| `unpushed` | Pin not reachable from any remote ref | Danger |
|
||||
| `conflict` | Merge conflict in the worktree | Danger |
|
||||
| `missing` | Not initialized; directory missing | Warn |
|
||||
| `off pin` | Checkout differs from pin | Warn |
|
||||
| `changed` | Uncommitted changes; untracked files | Warn |
|
||||
|
||||
Rules:
|
||||
|
||||
- If the submodule is **missing**, that is its only issue (nothing else can
|
||||
be determined).
|
||||
- Issues are returned **most severe first**: unpushed, conflict, off pin,
|
||||
changed.
|
||||
- **Not issues** (quiet): in sync, pin behind/ahead/diverged but pushed,
|
||||
staged pin, detached HEAD on the pin. They are shown as information only.
|
||||
- A loading error is shown as `error` (Danger) in the UI; it is not an issue
|
||||
kind.
|
||||
|
||||
### Levels
|
||||
|
||||
| Level | Meaning | Presentation |
|
||||
|---|---|---|
|
||||
| `LevelQuiet` | Nothing to do | Whole row muted, no word |
|
||||
| `LevelWarn` | Look at it | Yellow word |
|
||||
| `LevelDanger` | Act now | Red bold word |
|
||||
|
||||
### Hidden issues
|
||||
|
||||
`Submodule.HiddenIssues()` counts the issues of **all** loaded descendants
|
||||
and returns the most severe level. A collapsed submodule shows this as `·k`
|
||||
in that level's color, so a danger can never hide inside a collapsed subtree.
|
||||
|
||||
## Resolving a pin to a ref
|
||||
|
||||
The pin is displayed as a ref, not a hash. A commit can be on several
|
||||
branches, on none, or deep in the history, so the git provider resolves it by
|
||||
this order and records which rule applied in `PinInfo.Source`:
|
||||
|
||||
| # | Rule | `RefSource` |
|
||||
|---|---|---|
|
||||
| 1 | `branch = …` in `.gitmodules` | `RefFromGitmodules` |
|
||||
| 2 | Branch currently checked out in the submodule | `RefFromCurrentBranch` |
|
||||
| 3 | Remote default branch, if it contains the pin | `RefFromDefaultBranch` |
|
||||
| 4 | Any remote branch containing the pin, preferring the closest tip (`git branch -r --contains`) | `RefFromContains` |
|
||||
| 5 | A tag (`git describe`) | `RefFromTag` |
|
||||
| 6 | Short SHA | `RefFromSHA` |
|
||||
|
||||
Distances: `git rev-list --left-right --count <pin>...<ref>` gives
|
||||
`Ahead` (left) and `Behind` (right).
|
||||
|
||||
`Pushed` is true if the pin is reachable from any remote ref. A pin that is
|
||||
ahead of its ref can still be pushed (e.g. on another branch); only
|
||||
`Pushed == false` is dangerous.
|
||||
|
||||
## Distance notation
|
||||
|
||||
| Display | Meaning |
|
||||
|---|---|
|
||||
| `↓n` | The ref has n commits the pin doesn't have; the pin is behind |
|
||||
| `↑n` | The pin has n commits the ref doesn't have |
|
||||
| `↑n↓m` | Diverged |
|
||||
|
||||
The arrows follow the convention of `git status`, lazygit and shell prompts.
|
||||
@@ -0,0 +1,145 @@
|
||||
# UX guide
|
||||
|
||||
## Principles
|
||||
|
||||
1. **Attention follows severity.** Healthy submodules are quiet, problems are
|
||||
loud, dangers are loudest. Users should be able to scan a long list and
|
||||
only see what needs action.
|
||||
2. **One word per row.** At most one state word in the tree (`+n` for more);
|
||||
the detail pane lists everything.
|
||||
3. **Words over symbols.** States are short words that need no legend.
|
||||
Symbols are only used where they are conventional (`↑↓`, `▸▾`).
|
||||
4. **Meaning never depends on color alone** (every state has a word).
|
||||
5. **Follow the user's terminal theme.** Use ANSI 16 colors for meaning.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
~/coding/my_project main 13 submodules · 1 unpushed · 1 missing · 2 off pin · … fetched 2m ago
|
||||
docs origin/main │ src/
|
||||
misc/tools/ │ my_submodule_3
|
||||
my_submodule_1 origin/main │
|
||||
my_submodule_2 missing a1b2c3d │ unpushed pin not pushed to any remote
|
||||
src/ │
|
||||
my_submodule_1 off pin origin/main │ Ref origin/main ↑1
|
||||
▸ my_submodule_2 [2] changed ·2 origin/main │ via checked out branch
|
||||
my_submodule_3 unpushed origin/main ↑1 │ Pinned b7c8d9e Hotfix null pointer
|
||||
libs/ │ Checkout b7c8d9e Hotfix null pointer
|
||||
logging origin/main ↓3 │ Tip 6f5e4d3 Release 2.1.0
|
||||
↑/↓ move • ←/→ collapse/expand • space toggle • u checkout pin • … • ? help • q quit
|
||||
```
|
||||
|
||||
| Area | Height / width |
|
||||
|---|---|
|
||||
| Header | 1 line |
|
||||
| Tree | Remaining width |
|
||||
| Detail pane | 40% of the width, at least 36 columns; hidden below 90 columns |
|
||||
| Footer | 1 line (more with full help) |
|
||||
|
||||
### Tree row
|
||||
|
||||
```
|
||||
indent ▸ name [n] status ref delta
|
||||
```
|
||||
|
||||
| Column | Width | Content |
|
||||
|---|---|---|
|
||||
| indent | 2 per depth | |
|
||||
| toggle | 3 | `▸` collapsed, `▾` expanded, blank for leaves |
|
||||
| name | fits the widest visible name | name, `[n]` = direct children (collapsed only) |
|
||||
| status | 14 | issue word, `+n`, `·k` hidden issues; spinner while loading |
|
||||
| ref | widest ref, max 22 | resolved ref; muted short SHA if unresolved |
|
||||
| delta | 6 | `↑n`, `↓n`, `↑n↓m` |
|
||||
|
||||
- The name column is only as wide as needed, so states stay close to names.
|
||||
- Directory headers (`src/`, `modules/`) are muted and not selectable.
|
||||
- Rows without issues: name and ref muted (except the selected row).
|
||||
|
||||
### Detail pane
|
||||
|
||||
1. Directory (muted) and name (bold)
|
||||
2. One line per issue: word (colored, 10 columns) + explanation;
|
||||
`in sync nothing to do` if there are none; `staged` and
|
||||
`detached HEAD on pin` as information
|
||||
3. `Ref` with distance and `via <rule>`
|
||||
4. `Pinned`, `Checkout`, `Tip`: SHA + subject; the checkout SHA is yellow if it
|
||||
differs from the pin
|
||||
5. "Pin is n commits behind/ahead of …" / "Pin and … have diverged": commit list
|
||||
6. `Children` (count and issues below), `URL`
|
||||
|
||||
### Header
|
||||
|
||||
`path branch ↑↓ · n submodules · <count> <issue>… · n pin staged · n loading · fetched <age>`
|
||||
|
||||
Issue counts are ordered by severity (unpushed, conflict, missing, off pin,
|
||||
changed), each in its level color. Zero counts are omitted.
|
||||
|
||||
## Colors
|
||||
|
||||
All colors are defined in `internal/tui/theme/theme.go`. Never hard-code
|
||||
colors in components.
|
||||
|
||||
| Role | Color | Used for |
|
||||
|---|---|---|
|
||||
| Danger | ANSI `1` red, bold | `unpushed`, `conflict`, `error`, `↑n` of an unpushed pin |
|
||||
| Warn | ANSI `3` yellow | `missing`, `off pin`, `changed`, differing checkout SHA |
|
||||
| Info | ANSI `4` blue | `staged` |
|
||||
| Primary | ANSI `6` cyan | branch names, commit hashes, spinner |
|
||||
| Muted | ANSI `8` grey | quiet rows, directory headers, arrows, labels |
|
||||
| Text | terminal default | normal text |
|
||||
| Border | ANSI `8` grey | pane and help separators |
|
||||
| Bar | adaptive 254 (light) / 235 (dark) | header background |
|
||||
| Selected | adaptive 253 (light) / 237 (dark) | selected row background |
|
||||
|
||||
- Background shades have no ANSI-16 equivalent, so they use `AdaptiveColor`.
|
||||
The background is detected once in `main` before Bubble Tea starts.
|
||||
- Styles that render on a background (header, selected row) take a `base`
|
||||
style (`LevelStyle(level, base)`, `Delta(pin, base)`), so inner segments
|
||||
keep the background.
|
||||
|
||||
## Wording
|
||||
|
||||
| Word | Detail pane text |
|
||||
|---|---|
|
||||
| `unpushed` | pin not pushed to any remote |
|
||||
| `conflict` | merge conflict |
|
||||
| `missing` | not initialized / directory missing |
|
||||
| `off pin` | checkout differs from pin |
|
||||
| `changed` | uncommitted changes / untracked files |
|
||||
| `staged` | pin change staged, not committed |
|
||||
| `in sync` | nothing to do |
|
||||
|
||||
## Navigation
|
||||
|
||||
| Key | Behavior |
|
||||
|---|---|
|
||||
| `↑` / `k`, `↓` / `j` | Previous / next submodule; directory headers are skipped |
|
||||
| `space` | Toggle expand/collapse of the selected submodule |
|
||||
| `→` / `l` | Collapsed with children: expand and select the first child. Expanded: select the first child. Leaf: nothing |
|
||||
| `←` / `h` | Expanded: collapse it (stays selected). Otherwise: collapse the parent and select it. Top level: nothing |
|
||||
| `ctrl+→` | Expand all; then follow the first child from the selection down to a submodule without children |
|
||||
| `ctrl+←` | Collapse all (resets remembered expansion); select the top-level ancestor |
|
||||
| `home` / `g`, `end` / `G` | First / last submodule |
|
||||
| `pgup`, `pgdn` | Move by one screen |
|
||||
| `?` | Toggle full help |
|
||||
| `q`, `ctrl+c` | Quit |
|
||||
| `u`, `b`, `f`, `/`, `!` | Planned actions (see requirements) |
|
||||
|
||||
"First child" always means the first child **as displayed** (children are
|
||||
grouped by directory).
|
||||
|
||||
Collapsing an ancestor of the selection moves the selection to that ancestor.
|
||||
The view scrolls so the selection is always visible; when the first submodule
|
||||
is selected, headers above it are shown too.
|
||||
|
||||
### Mouse
|
||||
|
||||
- Click a row: select it.
|
||||
- Click `▸` / `▾`: select and toggle.
|
||||
- Scroll wheel: move the selection.
|
||||
|
||||
## Accessibility and terminals
|
||||
|
||||
- `SUBMARINE_ASCII=1`: ASCII symbols (`^ v > v . *`) instead of Unicode.
|
||||
- Some terminals or multiplexers capture `ctrl+←/→`; letter aliases can be
|
||||
added if needed.
|
||||
Reference in New Issue
Block a user