Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion internal/tui/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ func preflightReport(checks []core.HostCheck) string {
return strings.Join(lines, "\n")
}

func (m model) Init() tea.Cmd { return loadVMs }
func (m model) Init() tea.Cmd { return tea.Batch(loadVMs, tea.RequestBackgroundColor) }

// Update feeds the open byo screen without consuming the message, then runs
// the normal update.
Expand Down Expand Up @@ -253,6 +253,13 @@ func (m model) updateApp(msg tea.Msg) (tea.Model, tea.Cmd) {
}

switch msg := msg.(type) {
case tea.BackgroundColorMsg:
// The fixed palette reads as low contrast on a light terminal: every
// colour measured below 4.5:1 against #F7F7F7 before internal/theme
// grew a light set.
applyPalette(msg.IsDark())
return m, nil

case tea.WindowSizeMsg:
m.width, m.height = msg.Width, msg.Height
// A taller terminal shows more VMs before paginating; the width stays
Expand Down
65 changes: 45 additions & 20 deletions internal/tui/theme.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,35 +15,60 @@ type themeColors struct {
accent, up, down, warn, err, dim color.Color
}

var th = themeColors{
accent: theme.Accent,
up: theme.Up,
down: theme.Down,
warn: theme.Warn,
err: theme.Err,
dim: theme.Dim,
}
var th themeColors

var (
accentStyle = lipgloss.NewStyle().Foreground(th.accent)
dimStyle = lipgloss.NewStyle().Foreground(th.dim)
errStyle = lipgloss.NewStyle().Foreground(th.err)
warnStyle = lipgloss.NewStyle().Foreground(th.warn)
upStyle = lipgloss.NewStyle().Foreground(th.up)
downStyle = lipgloss.NewStyle().Foreground(th.down)
selStyle = lipgloss.NewStyle().Foreground(th.accent).Bold(true)
accentStyle lipgloss.Style
dimStyle lipgloss.Style
errStyle lipgloss.Style
warnStyle lipgloss.Style
upStyle lipgloss.Style
downStyle lipgloss.Style
selStyle lipgloss.Style

// paneStyle is the one border every screen draws with: a rounded box
// in the theme accent, with breathing room inside. No screen builds its
// own lipgloss.NewStyle().Border(...); they all go through pane().
paneStyle = lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(th.accent).
Padding(1, 2)
paneStyle lipgloss.Style

paneTitleStyle = accentStyle.Bold(true)
paneTitleStyle lipgloss.Style
)

// applyPalette points every style in this package at the palette for the
// terminal background the caller reports. Update calls it once, when Bubble
// Tea answers the background query started in Init.
//
// The styles stay package-level values because 150-odd call sites read them
// by name. One program draws with them at a time, and the switch happens
// before the first render that follows the query.
func applyPalette(isDark bool) {
p := theme.For(isDark)
th = themeColors{
accent: p.Accent,
up: p.Up,
down: p.Down,
warn: p.Warn,
err: p.Err,
dim: p.Dim,
}
accentStyle = lipgloss.NewStyle().Foreground(th.accent)
dimStyle = lipgloss.NewStyle().Foreground(th.dim)
errStyle = lipgloss.NewStyle().Foreground(th.err)
warnStyle = lipgloss.NewStyle().Foreground(th.warn)
upStyle = lipgloss.NewStyle().Foreground(th.up)
downStyle = lipgloss.NewStyle().Foreground(th.down)
selStyle = lipgloss.NewStyle().Foreground(th.accent).Bold(true)
paneStyle = lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(th.accent).
Padding(1, 2)
paneTitleStyle = accentStyle.Bold(true)
}

// A terminal that never answers the background query keeps the dark palette,
// which is what the unsuffixed constants in internal/theme are.
func init() { applyPalette(true) }

// paneFrame is the total width a pane() call adds on top of its content:
// border on both sides plus the horizontal padding baked into paneStyle. A
// caller that needs to bound a pane to the terminal width subtracts this
Expand Down
52 changes: 52 additions & 0 deletions internal/tui/theme_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package tui

import (
"image/color"
"testing"

tea "charm.land/bubbletea/v2"

"github.com/novusedge/stoat/internal/theme"
)

// applyPalette must point every style at the palette for the reported
// background. The fixed dark set measured below 4.5:1 against a light
// terminal for every colour, which is what internal/theme's light set fixes.
func TestApplyPaletteFollowsTheBackground(t *testing.T) {
t.Cleanup(func() { applyPalette(true) })

applyPalette(false)
light := theme.For(false)
if th.accent != light.Accent {
t.Errorf("accent = %v, want the light palette's %v", th.accent, light.Accent)
}
if got := accentStyle.GetForeground(); got != light.Accent {
t.Errorf("accentStyle foreground = %v, want %v", got, light.Accent)
}
if got := paneStyle.GetBorderTopForeground(); got != light.Accent {
t.Errorf("pane border = %v, want %v", got, light.Accent)
}
if got := selStyle.GetForeground(); got != light.Accent {
t.Errorf("selStyle foreground = %v, want %v", got, light.Accent)
}

applyPalette(true)
if dark := theme.For(true); th.accent != dark.Accent {
t.Errorf("accent = %v, want the dark palette's %v", th.accent, dark.Accent)
}
}

// Bubble Tea answers the query started in Init with this message, so Update
// has to act on it. Without the case, a light terminal keeps the dark palette
// for the life of the program.
func TestBackgroundColorMsgSwitchesThePalette(t *testing.T) {
t.Cleanup(func() { applyPalette(true) })

var m model
if _, cmd := m.Update(tea.BackgroundColorMsg{Color: color.White}); cmd != nil {
t.Errorf("the background report should not schedule work, got %T", cmd)
}
if light := theme.For(false); th.accent != light.Accent {
t.Errorf("a white background left accent at %v, want %v", th.accent, light.Accent)
}
}