4a579bdbc6
CI / test (pull_request) Successful in 4s
Every colour was a 256-colour ANSI index hardcoded in styles.go, so changing the palette meant editing the styles themselves. This puts a semantic token set between the two: styles name roles, a theme supplies the colours. internal/theme holds the twelve tokens, the two built-ins (gruvbox-dark, the new default, and gruvbox-light) and the loader for user themes in ~/.config/terdut-tui/themes/. A user file may 'extends:' a built-in and override only what it cares about, and may shadow a built-in name to tweak it in place. Unknown keys, malformed colours and incomplete themes are refused with a message naming what went wrong. Colours are truecolor hex now: lipgloss downsamples for 256- and 16-colour terminals and honours NO_COLOR, so themes carry no fallbacks of their own. An ANSI index is still accepted for anyone who would rather follow their terminal's own palette. The 21 package-level style vars become a Styles struct on the Model, which is what rule 3 asked for all along; the four free functions in view.go take one as their first argument. The embedded bubbles components are restyled from the same tokens — otherwise a theme would leave a pink selected row and grey help text behind. Note that the table's Cell style deliberately keeps no foreground: bubbles renders cells before wrapping the row in Selected, so a colour there cuts the selection highlight short.
132 lines
4.7 KiB
Go
132 lines
4.7 KiB
Go
package tui
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"git.ryuvia.com/niklas/terdut-tui/internal/api"
|
|
"git.ryuvia.com/niklas/terdut-tui/internal/theme"
|
|
"github.com/charmbracelet/bubbles/textinput"
|
|
"github.com/charmbracelet/lipgloss"
|
|
)
|
|
|
|
// Assertions here read the colours off the styles rather than off rendered
|
|
// output: under `go test` stdout is not a TTY, so lipgloss strips every escape
|
|
// sequence and rendered strings would all compare equal.
|
|
|
|
func wantFg(t *testing.T, s lipgloss.Style, want lipgloss.Color, what string) {
|
|
t.Helper()
|
|
if got := s.GetForeground(); got != want {
|
|
t.Errorf("%s foreground = %v, want %v", what, got, want)
|
|
}
|
|
}
|
|
|
|
func TestStyles_TokensReachTheStyles(t *testing.T) {
|
|
th := theme.GruvboxDark
|
|
s := newStyles(th)
|
|
|
|
wantFg(t, s.Header, th.Primary, "header")
|
|
wantFg(t, s.Firing, th.Firing, "firing")
|
|
wantFg(t, s.Resolved, th.Resolved, "resolved")
|
|
wantFg(t, s.Muted, th.Muted, "muted")
|
|
wantFg(t, s.Status, th.Accent, "status")
|
|
wantFg(t, s.Error, th.Error, "error")
|
|
wantFg(t, s.SevWarning, th.SevWarning, "sev warning")
|
|
|
|
// The two inverted spots need the pair, not just a foreground.
|
|
wantFg(t, s.TabActive, th.OnPrimary, "active tab")
|
|
if got := s.TabActive.GetBackground(); got != th.Primary {
|
|
t.Errorf("active tab background = %v, want %v", got, th.Primary)
|
|
}
|
|
|
|
// Attributes the old package-level styles carried must survive.
|
|
if !s.Firing.GetBold() {
|
|
t.Error("firing lost its bold")
|
|
}
|
|
if !s.Snoozed.GetItalic() {
|
|
t.Error("snoozed lost its italic")
|
|
}
|
|
if top, right, bottom, left := s.TabActive.GetPadding(); top != 0 || right != 2 || bottom != 0 || left != 2 {
|
|
t.Errorf("active tab padding = %d %d %d %d, want 0 2 0 2", top, right, bottom, left)
|
|
}
|
|
}
|
|
|
|
func TestStyles_EachBuiltinIsFullyPopulated(t *testing.T) {
|
|
for _, th := range []theme.Theme{theme.GruvboxDark, theme.GruvboxLight} {
|
|
s := newStyles(th)
|
|
for what, style := range map[string]lipgloss.Style{
|
|
"header": s.Header, "tab inactive": s.TabInactive, "footer": s.Footer,
|
|
"status": s.Status, "error": s.Error, "firing": s.Firing,
|
|
"resolved": s.Resolved, "muted": s.Muted, "accent": s.Accent,
|
|
"alert name": s.AlertName, "bold": s.Bold, "selected": s.Selected,
|
|
"triggered": s.Triggered, "acknowledged": s.Acknowledged, "snoozed": s.Snoozed,
|
|
"sev critical": s.SevCritical, "sev error": s.SevError,
|
|
"sev warning": s.SevWarning, "sev info": s.SevInfo,
|
|
} {
|
|
if style.GetForeground() == (lipgloss.NoColor{}) {
|
|
t.Errorf("%s: %s has no foreground", th.Name, what)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStyles_SeverityFallsBackToMuted(t *testing.T) {
|
|
s := newStyles(theme.GruvboxDark)
|
|
|
|
for _, sev := range []string{"critical", "CRITICAL", "error", "warning", "info"} {
|
|
if s.Severity(sev).GetForeground() == s.Muted.GetForeground() {
|
|
t.Errorf("severity %q should have its own colour", sev)
|
|
}
|
|
}
|
|
for _, sev := range []string{"", "page", "unknown"} {
|
|
if s.Severity(sev).GetForeground() != s.Muted.GetForeground() {
|
|
t.Errorf("severity %q should fall back to muted", sev)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStyles_IncidentStatusFallsBackToMuted(t *testing.T) {
|
|
s := newStyles(theme.GruvboxDark)
|
|
|
|
for _, status := range []string{api.StatusTriggered, api.StatusAcknowledged, api.StatusResolved} {
|
|
if s.IncidentStatus(status).GetForeground() == s.Muted.GetForeground() {
|
|
t.Errorf("status %q should have its own colour", status)
|
|
}
|
|
}
|
|
if s.IncidentStatus("invented-later").GetForeground() != s.Muted.GetForeground() {
|
|
t.Error("an unknown status should fall back to muted")
|
|
}
|
|
}
|
|
|
|
// The bubbles components ship their own palettes — a pink selected row, grey
|
|
// help text, a 240 placeholder. These check we replaced them.
|
|
func TestStyles_BubblesComponentsFollowTheTheme(t *testing.T) {
|
|
th := theme.GruvboxDark
|
|
s := newStyles(th)
|
|
|
|
ts := s.Table()
|
|
wantFg(t, ts.Selected, th.OnPrimary, "table selection")
|
|
if got := ts.Selected.GetBackground(); got != th.Primary {
|
|
t.Errorf("table selection background = %v, want %v", got, th.Primary)
|
|
}
|
|
wantFg(t, ts.Header, th.Muted, "table header")
|
|
if _, right, _, left := ts.Cell.GetPadding(); right != 1 || left != 1 {
|
|
t.Error("table cell padding was lost")
|
|
}
|
|
// A foreground on Cell would emit a reset mid-row and truncate the
|
|
// selection highlight, so it must stay unset.
|
|
if ts.Cell.GetForeground() != (lipgloss.NoColor{}) {
|
|
t.Error("table cells must not carry a foreground")
|
|
}
|
|
|
|
h := s.Help()
|
|
wantFg(t, h.ShortKey, th.Text, "help key")
|
|
wantFg(t, h.ShortDesc, th.Muted, "help description")
|
|
wantFg(t, h.FullSeparator, th.Muted, "help separator")
|
|
|
|
in := s.Input(textinput.New())
|
|
wantFg(t, in.PlaceholderStyle, th.Muted, "input placeholder")
|
|
wantFg(t, in.TextStyle, th.Text, "input text")
|
|
wantFg(t, in.PromptStyle, th.Primary, "input prompt")
|
|
wantFg(t, in.Cursor.Style, th.Primary, "input cursor")
|
|
}
|