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/theme/issues.go
T
Simon FrankenandClaude Opus 5.5 19c6f17eb2 Preview uncommitted changes in the detail pane
For a dirty submodule, list the changed files so the user can remember
what they were doing there:

  Changes   5 files · 2M 1A 1D 1R · +37 −12
            ──────────────────────────────────
            M  render.go  src/render/  +20 −4  3d ago
            ?  scratch.txt                     5m ago

- Status.Changes: files changed against HEAD (staged and unstaged
  combined) with kind, line counts and modification time; sorted by kind,
  then by size; capped at 200 with totals over all files.
- Git provider: parse `status --porcelain=v2 -z` and one
  `diff HEAD --numstat -z -M`, only for dirty submodules.
- Detail pane: flat list with a colored status letter per file, like IDE
  source-control views; counts and age in compact columns; up to 10 files,
  every kind keeps a line, the rest is summarized as "n more".
- Fixture gains libs/work with every kind of change; mock data extended.
- Docs: F24, change preview in the UX guide, D17.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 15:55:46 +02:00

111 lines
2.7 KiB
Go

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"
}
}
// 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))
}
}