024dc095a5
T used to silently cycle Model.activeTeamID through the caller's teams with no visible list of choices. It now opens a full picker (modelled on the existing user picker) listing every team plus "All teams", each with a stable identity colour from a new six-colour theme palette. The same colour now also shows as a bullet next to "team: <name>" in the header, so the active team stays visible without opening the picker. Adds an Identity palette to the theme package (six hues, skipping the ones that already mean firing/critical), Styles.TeamColor to pick one by team id, and identity_1..6 as theme-file tokens alongside the existing twelve so a fully custom theme can still set every token.
187 lines
5.7 KiB
Go
187 lines
5.7 KiB
Go
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
|
|
}
|