package tui import ( "strings" "git.ryuvia.com/niklas/terdut-tui/internal/api" "git.ryuvia.com/niklas/terdut-tui/internal/theme" "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/table" "github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/lipgloss" ) // Styles is every style the views draw with, built once from a theme and held // on the Model. Nothing here reads a colour literal: the theme is the only // place a colour is named. type Styles struct { Header lipgloss.Style TabActive lipgloss.Style TabInactive lipgloss.Style Footer lipgloss.Style Status lipgloss.Style Error lipgloss.Style Firing lipgloss.Style Resolved lipgloss.Style Muted lipgloss.Style Accent lipgloss.Style AlertName lipgloss.Style Bold lipgloss.Style Selected lipgloss.Style // Incident status. Triggered is unclaimed work and reads as loudly as a // firing alert; acknowledged means somebody has it. Triggered lipgloss.Style Acknowledged lipgloss.Style Snoozed lipgloss.Style // Severity, over the conventional Alertmanager label values. SevCritical lipgloss.Style SevError lipgloss.Style SevWarning lipgloss.Style SevInfo lipgloss.Style theme theme.Theme } func newStyles(t theme.Theme) Styles { return Styles{ Header: lipgloss.NewStyle(). Bold(true). Foreground(t.Primary). Padding(0, 1), TabActive: lipgloss.NewStyle(). Bold(true). Foreground(t.OnPrimary). Background(t.Primary). Padding(0, 2), TabInactive: lipgloss.NewStyle(). Foreground(t.Muted). Padding(0, 2), Footer: lipgloss.NewStyle().Foreground(t.Muted), Status: lipgloss.NewStyle(). Foreground(t.Accent). Bold(true), Error: lipgloss.NewStyle(). Foreground(t.Error). Bold(true), Firing: lipgloss.NewStyle().Foreground(t.Firing).Bold(true), Resolved: lipgloss.NewStyle().Foreground(t.Resolved), Muted: lipgloss.NewStyle().Foreground(t.Muted), Accent: lipgloss.NewStyle().Foreground(t.Accent), AlertName: lipgloss.NewStyle().Foreground(t.Text).Bold(true), Bold: lipgloss.NewStyle().Foreground(t.Text).Bold(true), Selected: lipgloss.NewStyle().Foreground(t.Primary).Bold(true), Triggered: lipgloss.NewStyle().Foreground(t.Firing).Bold(true), Acknowledged: lipgloss.NewStyle().Foreground(t.Accent).Bold(true), Snoozed: lipgloss.NewStyle().Foreground(t.Muted).Italic(true), SevCritical: lipgloss.NewStyle().Foreground(t.SevCritical).Bold(true), SevError: lipgloss.NewStyle().Foreground(t.SevError).Bold(true), SevWarning: lipgloss.NewStyle().Foreground(t.SevWarning), SevInfo: lipgloss.NewStyle().Foreground(t.SevInfo), theme: t, } } // Severity picks the style for a severity label, falling back to muted for // values this client does not recognise rather than dropping them. func (s Styles) Severity(severity string) lipgloss.Style { switch strings.ToLower(severity) { case "critical": return s.SevCritical case "error": return s.SevError case "warning": return s.SevWarning case "info": return s.SevInfo default: return s.Muted } } // IncidentStatus picks the style for an incident status, falling back to muted // for statuses added after this client was built. func (s Styles) IncidentStatus(status string) lipgloss.Style { switch status { case api.StatusTriggered: return s.Triggered case api.StatusAcknowledged: return s.Acknowledged case api.StatusResolved: return s.Resolved default: return s.Muted } } // TeamColor picks a stable identity colour for a team id, so the same team // always reads the same colour without the server needing to store one. func (s Styles) TeamColor(teamID int64) lipgloss.Style { n := int64(len(s.theme.Identity)) i := teamID % n if i < 0 { // ids are never negative in practice, but % can still return one i += n } return lipgloss.NewStyle().Foreground(s.theme.Identity[i]) } // ── Embedded bubbles components ──────────────────────────────────────────── // // Each ships its own hardcoded palette, so a theme that stopped at this // package's own styles would leave a pink selected row and grey help text // behind. These three restyle them from the same tokens. // Table styles the six tables. Padding comes from the bubbles defaults; only // the colours are ours. func (s Styles) Table() table.Styles { ts := table.DefaultStyles() ts.Header = ts.Header.Foreground(s.theme.Muted).Bold(true) // Cell deliberately keeps no foreground: bubbles renders each cell before // wrapping the whole row in Selected, so a colour here would emit a reset // mid-row and cut the selection highlight short. ts.Selected = ts.Selected. Foreground(s.theme.OnPrimary). Background(s.theme.Primary). Bold(true) return ts } // Help styles the key hints in the footer. func (s Styles) Help() help.Styles { key := lipgloss.NewStyle().Foreground(s.theme.Text) desc := lipgloss.NewStyle().Foreground(s.theme.Muted) sep := lipgloss.NewStyle().Foreground(s.theme.Muted) return help.Styles{ Ellipsis: sep, ShortKey: key, ShortDesc: desc, ShortSeparator: sep, FullKey: key, FullDesc: desc, FullSeparator: sep, } } // Input styles a text input and returns it, so NewModel can wrap each one as // it is built. func (s Styles) Input(ti textinput.Model) textinput.Model { ti.PromptStyle = lipgloss.NewStyle().Foreground(s.theme.Primary) ti.TextStyle = lipgloss.NewStyle().Foreground(s.theme.Text) ti.PlaceholderStyle = lipgloss.NewStyle().Foreground(s.theme.Muted) ti.CompletionStyle = lipgloss.NewStyle().Foreground(s.theme.Muted) ti.Cursor.Style = lipgloss.NewStyle().Foreground(s.theme.Primary) return ti }