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/internal/tui/detail/detail.go
T
Simon FrankenandClaude Opus 5.5 6e1236d495 Show a muted "in sync" for submodules without issues
An empty status cell could be read as missing data. The muted word
confirms the check ran without drawing attention. The word is defined
once in the theme and shared with the detail pane.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 16:18:17 +02:00

167 lines
4.5 KiB
Go

// Package detail renders the information pane for the selected submodule.
package detail
import (
"fmt"
"path"
"strings"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/ansi"
"submarine/internal/submodule"
"submarine/internal/tui/theme"
)
const labelWidth = 10
type Model struct {
theme theme.Theme
width int
height int
spinner string
}
func New(t theme.Theme) Model { return Model{theme: t} }
func (m *Model) SetSize(w, h int) { m.width, m.height = w, h }
func (m *Model) SetSpinner(frame string) { m.spinner = frame }
func (m Model) View(s *submodule.Submodule) string {
if m.width <= 0 || m.height <= 0 {
return ""
}
var lines []string
if s != nil {
lines = m.lines(s)
}
for i, l := range lines {
lines[i] = ansi.Truncate(l, m.width, "…")
}
if len(lines) > m.height {
lines = lines[:m.height]
}
return lipgloss.NewStyle().Width(m.width).Height(m.height).Render(strings.Join(lines, "\n"))
}
func (m Model) lines(s *submodule.Submodule) []string {
t := m.theme
muted, text := theme.Fg(t.Muted), theme.Fg(t.Text)
label := func(l string) string { return muted.Width(labelWidth).Render(l) }
dir := path.Dir(s.Path)
if dir == "." {
dir = ""
} else {
dir += "/"
}
out := []string{
muted.Render(dir),
text.Bold(true).Render(s.Name),
"",
}
switch {
case s.Err != nil:
return append(out, t.LevelStyle(submodule.LevelDanger, lipgloss.NewStyle()).Render("error")+" "+muted.Render(s.Err.Error()))
case s.Status == nil:
return append(out, theme.Fg(t.Primary).Render(m.spinner)+muted.Render(" loading…"))
}
st := s.Status
issues := st.Issues()
for _, is := range issues {
out = append(out, t.LevelStyle(is.Level, lipgloss.NewStyle()).Width(labelWidth).Render(theme.IssueWord(is.Kind))+text.Render(is.Detail))
}
if len(issues) == 0 {
out = append(out, label(theme.InSync)+muted.Render("nothing to do"))
}
if st.PinStaged {
out = append(out, theme.Fg(t.Info).Width(labelWidth).Render("staged")+text.Render("pin change staged, not committed"))
}
if st.Checkout == submodule.CheckoutDetached {
out = append(out, label("")+muted.Render("detached HEAD on pin"))
}
if st.Changes != nil {
out = append(out, "")
out = append(out, m.changeLines(st.Changes, label)...)
}
out = append(out, "")
// ref
if st.Pin.Ref != "" {
delta := t.Delta(st.Pin, lipgloss.NewStyle())
if delta != "" {
delta = " " + delta
}
out = append(out,
label("Ref")+text.Render(st.Pin.Ref)+delta,
label("")+muted.Render("via "+st.Pin.Source.String()),
)
}
if st.Presence == submodule.PresenceOK {
out = append(out, label("Fetched")+muted.Render(theme.Ago(st.FetchedAt)))
}
if s.FetchErr != nil {
out = append(out, theme.Fg(t.Warn).Width(labelWidth).Render("fetch")+text.Render("failed: "+s.FetchErr.Error()))
}
// commits
commitLine := func(name string, c *submodule.Commit, highlight bool) string {
if c == nil {
return label(name) + muted.Render("—")
}
shaStyle := theme.Fg(t.Primary)
if highlight {
shaStyle = theme.Fg(t.Warn)
}
return label(name) + shaStyle.Render(c.SHA) + " " + text.Render(c.Subject)
}
out = append(out,
commitLine("Pinned", st.Pinned, false),
commitLine("Checkout", st.CheckedOut, st.Checkout == submodule.CheckoutDiffers),
commitLine("Tip", st.Tip, false),
)
// difference between pin and tip
if len(st.Between) > 0 {
out = append(out, "", text.Render(m.betweenTitle(st.Pin)))
for _, c := range st.Between {
out = append(out,
" "+theme.Fg(t.Primary).Render(c.SHA)+" "+text.Render(c.Subject),
" "+muted.Render(fmt.Sprintf("%7s %s · %s", "", c.Author, theme.Ago(c.When))),
)
}
}
// children & meta
out = append(out, "")
if n := len(s.Children); n > 0 {
children := fmt.Sprintf("%d direct", n)
if k, worst := s.HiddenIssues(); k > 0 {
children += t.LevelStyle(worst, lipgloss.NewStyle()).Render(fmt.Sprintf(" · %d issue(s) below", k))
}
out = append(out, label("Children")+children)
}
out = append(out, label("URL")+muted.Render(s.URL))
return out
}
func (m Model) betweenTitle(p submodule.PinInfo) string {
plural := func(n int) string {
if n == 1 {
return "commit"
}
return "commits"
}
switch {
case p.Ahead > 0 && p.Behind > 0:
return fmt.Sprintf("Pin and %s have diverged (%s%d %s%d):",
p.Ref, m.theme.Glyphs.Ahead, p.Ahead, m.theme.Glyphs.Behind, p.Behind)
case p.Ahead > 0:
return fmt.Sprintf("Pin is %d %s ahead of %s:", p.Ahead, plural(p.Ahead), p.Ref)
default:
return fmt.Sprintf("Pin is %d %s behind %s:", p.Behind, plural(p.Behind), p.Ref)
}
}