package main import ( "os" "strings" "github.com/charmbracelet/lipgloss" "github.com/mattn/go-runewidth" ) // A restrained, high-contrast palette. Text always includes its meaning, so // colors supplement the UI rather than carrying state by themselves. type uiTheme struct { title, accent, muted, warning, success, active, selected lipgloss.Style border lipgloss.Style plain bool } func newTheme() uiTheme { t := uiTheme{plain: os.Getenv("NO_COLOR") != "" || strings.EqualFold(os.Getenv("TERM"), "dumb")} if t.plain { t.title = lipgloss.NewStyle() t.accent = lipgloss.NewStyle() t.muted = lipgloss.NewStyle() t.warning = lipgloss.NewStyle() t.success = lipgloss.NewStyle() t.active = lipgloss.NewStyle() t.selected = lipgloss.NewStyle() t.border = lipgloss.NewStyle() return t } t.title = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#D7E9EF")) t.accent = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#70D6D1")) t.muted = lipgloss.NewStyle().Foreground(lipgloss.Color("#8FA6B2")) t.warning = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#FFC078")) t.success = lipgloss.NewStyle().Foreground(lipgloss.Color("#A8DDA2")) t.active = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFC078")) t.selected = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#F4FAFC")).Background(lipgloss.Color("#244553")) t.border = lipgloss.NewStyle().Foreground(lipgloss.Color("#426573")) return t } func (t uiTheme) status(status string) string { label := "[" + safe(status) + "]" switch strings.ToLower(status) { case "closed", "done": return t.muted.Render(label) case "open": return t.success.Render(label) default: return t.active.Render(label) } } func (t uiTheme) heading(section string, count string, width int) string { prefix := "▌ TODOAPP / " section = runewidth.Truncate(safe(section), max(1, width-runewidth.StringWidth(prefix)), "…") return t.accent.Render("▌ ") + t.title.Render("TODOAPP") + t.muted.Render(" / "+section) + "\n" + t.muted.Render(runewidth.Truncate(safe(count), max(1, width), "…")) } // box frames content only when the terminal has room for a border. The width // argument is the terminal width, not the inner content width. func (t uiTheme) box(content string, width, height int) string { if width < 50 || height < 14 || t.plain { return content } return t.border.Border(lipgloss.RoundedBorder()).Padding(0, 1).Width(min(width, 100) - 2).Render(content) } func (t uiTheme) row(label string, selected bool, width int) string { prefix := " " if selected { prefix = "› " } text := runewidth.Truncate(prefix+label, max(1, width), "…") if selected { return t.selected.Width(min(width, 100)).Render(text) } return text } func (t uiTheme) hint(text string, width int) string { return t.muted.Render(runewidth.Truncate(text, max(1, width), "…")) } func (t uiTheme) alert(text string, width int) string { return t.warning.Render(runewidth.Truncate("! "+safe(text), max(1, width), "…")) }