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,71 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
||||
"submarine/internal/submodule"
|
||||
)
|
||||
|
||||
// IssueWord is the short label for an issue kind shown in the tree.
|
||||
func IssueWord(k submodule.IssueKind) string {
|
||||
switch k {
|
||||
case submodule.IssueMissing:
|
||||
return "missing"
|
||||
case submodule.IssueUnpushed:
|
||||
return "unpushed"
|
||||
case submodule.IssueConflict:
|
||||
return "conflict"
|
||||
case submodule.IssueOffPin:
|
||||
return "off pin"
|
||||
default:
|
||||
return "changed"
|
||||
}
|
||||
}
|
||||
|
||||
// LevelStyle returns base with the text color (and weight) for a level.
|
||||
func (t Theme) LevelStyle(l submodule.Level, base lipgloss.Style) lipgloss.Style {
|
||||
switch l {
|
||||
case submodule.LevelDanger:
|
||||
return base.Foreground(t.Danger).Bold(true)
|
||||
case submodule.LevelWarn:
|
||||
return base.Foreground(t.Warn)
|
||||
default:
|
||||
return base.Foreground(t.Muted)
|
||||
}
|
||||
}
|
||||
|
||||
// Delta renders ahead/behind counts like "↑1↓2", muted unless the pin is
|
||||
// unpushed. Empty if both are zero. base is applied to every segment, e.g.
|
||||
// to keep a row background.
|
||||
func (t Theme) Delta(p submodule.PinInfo, base lipgloss.Style) string {
|
||||
var s string
|
||||
if p.Ahead > 0 {
|
||||
c := t.Muted
|
||||
if !p.Pushed {
|
||||
c = t.Danger
|
||||
}
|
||||
s += base.Foreground(c).Render(fmt.Sprintf("%s%d", t.Glyphs.Ahead, p.Ahead))
|
||||
}
|
||||
if p.Behind > 0 {
|
||||
s += base.Foreground(t.Muted).Render(fmt.Sprintf("%s%d", t.Glyphs.Behind, p.Behind))
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Ago formats a time relative to now, e.g. "5m ago".
|
||||
func Ago(t time.Time) string {
|
||||
d := time.Since(t)
|
||||
switch {
|
||||
case d < time.Minute:
|
||||
return "just now"
|
||||
case d < time.Hour:
|
||||
return fmt.Sprintf("%dm ago", int(d.Minutes()))
|
||||
case d < 24*time.Hour:
|
||||
return fmt.Sprintf("%dh ago", int(d.Hours()))
|
||||
default:
|
||||
return fmt.Sprintf("%dd ago", int(d.Hours()/24))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// Package theme holds the colors, glyphs and shared styles for the TUI.
|
||||
package theme
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// Theme uses the 16 ANSI colors for everything that carries meaning, so
|
||||
// the TUI follows the user's terminal color scheme. Only background shades,
|
||||
// which have no ANSI equivalent, use adaptive 256-color values.
|
||||
type Theme struct {
|
||||
Primary lipgloss.TerminalColor // accent: branch names, commit hashes
|
||||
Info lipgloss.TerminalColor
|
||||
Warn lipgloss.TerminalColor
|
||||
Danger lipgloss.TerminalColor
|
||||
Muted lipgloss.TerminalColor
|
||||
Text lipgloss.TerminalColor
|
||||
Border lipgloss.TerminalColor // pane separators, help separators
|
||||
Bar lipgloss.TerminalColor // background of the header bar
|
||||
Selected lipgloss.TerminalColor // background of the selected row
|
||||
Glyphs Glyphs
|
||||
}
|
||||
|
||||
func Default() Theme {
|
||||
g := Unicode()
|
||||
if os.Getenv("SUBMARINE_ASCII") != "" {
|
||||
g = ASCII()
|
||||
}
|
||||
return Theme{
|
||||
Primary: lipgloss.Color("6"), // cyan
|
||||
Info: lipgloss.Color("4"), // blue
|
||||
Warn: lipgloss.Color("3"), // yellow
|
||||
Danger: lipgloss.Color("1"), // red
|
||||
Muted: lipgloss.Color("8"), // bright black / grey
|
||||
Text: lipgloss.NoColor{}, // terminal default foreground
|
||||
Border: lipgloss.Color("8"),
|
||||
Bar: lipgloss.AdaptiveColor{Light: "254", Dark: "235"},
|
||||
Selected: lipgloss.AdaptiveColor{Light: "253", Dark: "237"},
|
||||
Glyphs: g,
|
||||
}
|
||||
}
|
||||
|
||||
// Fg returns a style with the given foreground color.
|
||||
func Fg(c lipgloss.TerminalColor) lipgloss.Style {
|
||||
return lipgloss.NewStyle().Foreground(c)
|
||||
}
|
||||
|
||||
// Glyphs are the few symbols used next to the state words.
|
||||
type Glyphs struct {
|
||||
Staged, Ahead, Behind, Collapsed, Expanded, Problems string
|
||||
}
|
||||
|
||||
func Unicode() Glyphs {
|
||||
return Glyphs{
|
||||
Staged: "●", Ahead: "↑", Behind: "↓", Collapsed: "▸", Expanded: "▾", Problems: "·",
|
||||
}
|
||||
}
|
||||
|
||||
func ASCII() Glyphs {
|
||||
return Glyphs{
|
||||
Staged: "*", Ahead: "^", Behind: "v", Collapsed: ">", Expanded: "v", Problems: ".",
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user