package tui import ( "image/color" "os" "strings" "charm.land/lipgloss/v2" ) type Theme struct { Dark bool Surface color.Color Border color.Color BorderFocus color.Color TextBright color.Color Text color.Color TextMuted color.Color Accent color.Color Success color.Color Warning color.Color Danger color.Color OnAccent color.Color } func ResolveTheme() Theme { switch strings.ToLower(strings.TrimSpace(os.Getenv("INTAKE_THEME"))) { case "dark": return makeTheme(true) case "light": return makeTheme(false) } dark := true if v := os.Getenv("COLORFGBG"); v != "" { parts := strings.Split(v, ";") if len(parts) >= 2 { if bg := strings.TrimSpace(parts[len(parts)-1]); bg == "7" || bg == "15" { dark = false } } } return makeTheme(dark) } func makeTheme(dark bool) Theme { c := func(lightHex, darkHex string) color.Color { if dark { return lipgloss.Color(darkHex) } return lipgloss.Color(lightHex) } return Theme{ Dark: dark, Surface: c("#F6F7F9", "#16181D"), Border: c("#D6DAE0", "#2A2E36"), BorderFocus: c("#5B3BB5", "#B79CFF"), TextBright: c("#0B0D10", "#FFFFFF"), Text: c("#24292F", "#D6D9DE"), TextMuted: c("#5B6470", "#8B929C"), Accent: c("#5B3BB5", "#B79CFF"), Success: c("#1F7A4D", "#5FD79B"), Warning: c("#8A5A00", "#F0B860"), Danger: c("#B3261E", "#FF8A80"), OnAccent: c("#FFFFFF", "#14161A"), } } type Styles struct { Theme Theme AppName lipgloss.Style Crumb lipgloss.Style CrumbDim lipgloss.Style Hint lipgloss.Style HintKey lipgloss.Style HintDesc lipgloss.Style Status lipgloss.Style Error lipgloss.Style Title lipgloss.Style Heading lipgloss.Style Label lipgloss.Style Value lipgloss.Style Muted lipgloss.Style Dim lipgloss.Style Code lipgloss.Style Accent lipgloss.Style Success lipgloss.Style Warning lipgloss.Style Danger lipgloss.Style Badge lipgloss.Style Pill lipgloss.Style Panel lipgloss.Style PanelActive lipgloss.Style Inner lipgloss.Style Rule lipgloss.Style Selected lipgloss.Style SelectedDim lipgloss.Style Marker lipgloss.Style ListTitle lipgloss.Style ListItem lipgloss.Style ListSelected lipgloss.Style ListDesc lipgloss.Style ListDescSel lipgloss.Style ListFilter lipgloss.Style ListFilterCur lipgloss.Style ListStatusBar lipgloss.Style ListStatusKeys lipgloss.Style ListStatusNums lipgloss.Style ListSpinner lipgloss.Style } func DefaultStyles() Styles { return StylesFor(ResolveTheme()) } func StylesFor(t Theme) Styles { s := Styles{Theme: t} s.AppName = lipgloss.NewStyle().Bold(true).Foreground(t.OnAccent).Background(t.Accent).Padding(0, 1) s.Crumb = lipgloss.NewStyle().Foreground(t.Text) s.CrumbDim = lipgloss.NewStyle().Foreground(t.TextMuted) s.Hint = lipgloss.NewStyle().Foreground(t.TextMuted) s.HintKey = lipgloss.NewStyle().Foreground(t.Text).Bold(true) s.HintDesc = lipgloss.NewStyle().Foreground(t.TextMuted) s.Status = lipgloss.NewStyle().Foreground(t.TextMuted) s.Error = lipgloss.NewStyle().Foreground(t.Danger) s.Title = lipgloss.NewStyle().Bold(true).Foreground(t.TextBright) s.Heading = lipgloss.NewStyle().Bold(true).Foreground(t.Accent) s.Label = lipgloss.NewStyle().Foreground(t.TextMuted) s.Value = lipgloss.NewStyle().Foreground(t.Text) s.Muted = lipgloss.NewStyle().Foreground(t.TextMuted) s.Dim = lipgloss.NewStyle().Foreground(t.TextMuted).Faint(true) s.Code = lipgloss.NewStyle().Foreground(t.Text).Background(t.Surface).Padding(0, 1) s.Accent = lipgloss.NewStyle().Foreground(t.Accent) s.Success = lipgloss.NewStyle().Foreground(t.Success) s.Warning = lipgloss.NewStyle().Foreground(t.Warning) s.Danger = lipgloss.NewStyle().Foreground(t.Danger) s.Badge = lipgloss.NewStyle().Foreground(t.OnAccent).Background(t.Accent).Padding(0, 1) s.Pill = lipgloss.NewStyle().Foreground(t.TextMuted) s.Panel = lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). BorderForeground(t.Border). Padding(0, 1) s.PanelActive = s.Panel.BorderForeground(t.BorderFocus) s.Inner = lipgloss.NewStyle().Padding(0, 1) s.Rule = lipgloss.NewStyle().Foreground(t.Border) s.Selected = lipgloss.NewStyle().Foreground(t.OnAccent).Background(t.Accent).Bold(true) s.SelectedDim = lipgloss.NewStyle().Foreground(t.OnAccent).Background(t.Accent) s.Marker = lipgloss.NewStyle().Foreground(t.Accent).Bold(true) s.ListTitle = lipgloss.NewStyle().Bold(true).Foreground(t.TextBright).Padding(0, 1) s.ListItem = lipgloss.NewStyle().Foreground(t.Text).Padding(0, 1) s.ListSelected = lipgloss.NewStyle().Foreground(t.OnAccent).Background(t.Accent).Bold(true).Padding(0, 1) s.ListDesc = lipgloss.NewStyle().Foreground(t.TextMuted).Padding(0, 1) s.ListDescSel = lipgloss.NewStyle().Foreground(t.OnAccent).Padding(0, 1) s.ListFilter = lipgloss.NewStyle().Foreground(t.Accent) s.ListFilterCur = lipgloss.NewStyle().Foreground(t.TextBright) s.ListStatusBar = lipgloss.NewStyle().Foreground(t.TextMuted) s.ListStatusKeys = lipgloss.NewStyle().Foreground(t.Text).Bold(true) s.ListStatusNums = lipgloss.NewStyle().Foreground(t.Text) s.ListSpinner = lipgloss.NewStyle().Foreground(t.Accent) return s } type Box struct{ W, H int } const chrome = 2 func (m Module) body() Box { h := m.height - chrome if m.railShown() { h-- } if h < 3 { h = 3 } w := m.width if w < 20 { w = 20 } return Box{W: w, H: h} } type mode int const ( modeTiny mode = iota modeMini modeCompact modeFull ) func (m Module) mode() mode { switch { case m.height < 8: return modeTiny case m.width < 60 || m.height < 18: return modeMini case m.width < 100 || m.height < 28: return modeCompact default: return modeFull } } func truncateTo(s string, width int) string { if width <= 0 { return "" } if displayWidth(s) <= width { return s } return truncateANSI(s, width) } func clamp(block string, width, height int) string { lines := strings.Split(block, "\n") if len(lines) > height { lines = lines[:height] } for i, l := range lines { lines[i] = truncateTo(l, width) } return strings.Join(lines, "\n") } func fitHeight(block string, height int) string { lines := strings.Split(block, "\n") for len(lines) < height { lines = append(lines, "") } if len(lines) > height { lines = lines[:height] } return strings.Join(lines, "\n") } func panelWith(s Styles, title, body string, width int, active bool) string { style := s.Panel if active { style = s.PanelActive } inner := width - 4 if inner < 8 { inner = 8 } head := "" if title != "" { head = s.Heading.Render(title) + "\n" } return style.Render(clamp(head+body, inner, 1000)) }