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,81 @@
|
||||
package submodule
|
||||
|
||||
// Level is how much attention an issue deserves.
|
||||
type Level int
|
||||
|
||||
const (
|
||||
LevelQuiet Level = iota // nothing to do
|
||||
LevelWarn // look at it
|
||||
LevelDanger // act now
|
||||
)
|
||||
|
||||
// IssueKind is the summarized category of a problem.
|
||||
type IssueKind int
|
||||
|
||||
const (
|
||||
IssueMissing IssueKind = iota // not initialized or directory missing
|
||||
IssueUnpushed // pin not reachable from any remote ref
|
||||
IssueConflict // merge conflict in the worktree
|
||||
IssueOffPin // checkout differs from pin
|
||||
IssueChanged // uncommitted changes or untracked files
|
||||
)
|
||||
|
||||
// Issue is one problem of a submodule. Detail distinguishes the states that
|
||||
// are summarized into one kind.
|
||||
type Issue struct {
|
||||
Kind IssueKind
|
||||
Level Level
|
||||
Detail string
|
||||
}
|
||||
|
||||
// Issues summarizes the status, most severe first. Being behind or ahead of
|
||||
// the remote, a staged pin and a detached HEAD are not issues.
|
||||
func (s *Status) Issues() []Issue {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
switch s.Presence {
|
||||
case NotInitialized:
|
||||
return []Issue{{IssueMissing, LevelWarn, "not initialized"}}
|
||||
case DirMissing:
|
||||
return []Issue{{IssueMissing, LevelWarn, "directory missing"}}
|
||||
}
|
||||
|
||||
var out []Issue
|
||||
if !s.Pin.Pushed {
|
||||
out = append(out, Issue{IssueUnpushed, LevelDanger, "pin not pushed to any remote"})
|
||||
}
|
||||
if s.Worktree == WorktreeConflict {
|
||||
out = append(out, Issue{IssueConflict, LevelDanger, "merge conflict"})
|
||||
}
|
||||
if s.Checkout == CheckoutDiffers {
|
||||
out = append(out, Issue{IssueOffPin, LevelWarn, "checkout differs from pin"})
|
||||
}
|
||||
switch s.Worktree {
|
||||
case WorktreeModified:
|
||||
out = append(out, Issue{IssueChanged, LevelWarn, "uncommitted changes"})
|
||||
case WorktreeUntracked:
|
||||
out = append(out, Issue{IssueChanged, LevelWarn, "untracked files"})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// HiddenIssues counts the issues of all loaded descendants and returns the
|
||||
// most severe level among them.
|
||||
func (m *Submodule) HiddenIssues() (count int, worst Level) {
|
||||
Walk(m.Children, func(c *Submodule) {
|
||||
for _, is := range c.Status.Issues() {
|
||||
count++
|
||||
worst = max(worst, is.Level)
|
||||
}
|
||||
})
|
||||
return count, worst
|
||||
}
|
||||
|
||||
// Walk calls fn for every submodule in depth-first order.
|
||||
func Walk(mods []*Submodule, fn func(*Submodule)) {
|
||||
for _, m := range mods {
|
||||
fn(m)
|
||||
Walk(m.Children, fn)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user