Archived
Replace the Pinned/Checkout/Tip rows and the list of commits between pin and tip with one line of history, newest on top: a node per commit with its roles (pin, tip, you, staged, HEAD) and the number of commits between two nodes. A staged pin shows the committed pin as its own node and notes when it moves back. All three on one commit read "in sync". Add Status.Drift, Committed and CommittedDrift and Status.Spine(); drop Status.Between and Repo.FetchedAt. The fetch age moves from the header to the detail pane (right-aligned in the name's line). The header summary is right-aligned, ordered by importance and cut with an ellipsis if too narrow. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
159 lines
4.1 KiB
Go
159 lines
4.1 KiB
Go
package theme
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/charmbracelet/lipgloss"
|
|
|
|
"submarine/internal/submodule"
|
|
)
|
|
|
|
// InSync is the word for a submodule without issues.
|
|
const InSync = "in sync"
|
|
|
|
// Staged is the word for a pin change staged in the superproject. It is not
|
|
// an issue: it replaces InSync, or follows an issue word as a marker.
|
|
const Staged = "staged"
|
|
|
|
// 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"
|
|
}
|
|
}
|
|
|
|
// RoleWord is the label of a commit's role on the detail pane's spine.
|
|
func RoleWord(r submodule.Role) string {
|
|
switch r {
|
|
case submodule.RolePin:
|
|
return "pin"
|
|
case submodule.RoleHead:
|
|
return "HEAD"
|
|
case submodule.RoleStaged:
|
|
return Staged
|
|
case submodule.RoleTip:
|
|
return "tip"
|
|
default:
|
|
return "you"
|
|
}
|
|
}
|
|
|
|
// StatusWord renders the state of s as one word: a spinner while loading,
|
|
// `error`, the most severe issue with "+n" for further ones, `staged`, or a
|
|
// muted "in sync". base is applied to every segment, e.g. to keep a row
|
|
// background.
|
|
func (t Theme) StatusWord(s *submodule.Submodule, base lipgloss.Style, spinner string) string {
|
|
seg := func(c lipgloss.TerminalColor, text string) string { return base.Foreground(c).Render(text) }
|
|
switch {
|
|
case s.Err != nil:
|
|
return t.LevelStyle(submodule.LevelDanger, base).Render("error")
|
|
case s.Status == nil:
|
|
return seg(t.Primary, spinner)
|
|
}
|
|
if issues := s.Status.Issues(); len(issues) > 0 {
|
|
out := t.LevelStyle(issues[0].Level, base).Render(IssueWord(issues[0].Kind))
|
|
if len(issues) > 1 {
|
|
out += seg(t.Muted, fmt.Sprintf(" +%d", len(issues)-1))
|
|
}
|
|
return out
|
|
}
|
|
if s.Status.PinStaged {
|
|
return seg(t.Info, Staged)
|
|
}
|
|
return seg(t.Muted, InSync)
|
|
}
|
|
|
|
// ChangeLetter is the one-letter label of a changed file.
|
|
func ChangeLetter(k submodule.ChangeKind) string {
|
|
switch k {
|
|
case submodule.ChangeConflicted:
|
|
return "!"
|
|
case submodule.ChangeModified:
|
|
return "M"
|
|
case submodule.ChangeAdded:
|
|
return "A"
|
|
case submodule.ChangeRemoved:
|
|
return "D"
|
|
case submodule.ChangeRenamed:
|
|
return "R"
|
|
default:
|
|
return "?"
|
|
}
|
|
}
|
|
|
|
// ChangeStyle returns base with the color of a change kind.
|
|
func (t Theme) ChangeStyle(k submodule.ChangeKind, base lipgloss.Style) lipgloss.Style {
|
|
switch k {
|
|
case submodule.ChangeConflicted:
|
|
return base.Foreground(t.Danger).Bold(true)
|
|
case submodule.ChangeModified:
|
|
return base.Foreground(t.Warn)
|
|
case submodule.ChangeAdded:
|
|
return base.Foreground(t.Added)
|
|
case submodule.ChangeRemoved:
|
|
return base.Foreground(t.Danger)
|
|
case submodule.ChangeRenamed:
|
|
return base.Foreground(t.Primary)
|
|
default:
|
|
return base.Foreground(t.Muted)
|
|
}
|
|
}
|
|
|
|
// 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"; the zero time is "never".
|
|
func Ago(t time.Time) string {
|
|
if t.IsZero() {
|
|
return "never"
|
|
}
|
|
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))
|
|
}
|
|
}
|