Release / Build and publish (push) Successful in 24s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
201 lines
6.3 KiB
Go
201 lines
6.3 KiB
Go
package styles
|
|
|
|
import (
|
|
"fmt"
|
|
"image/color"
|
|
"slices"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"charm.land/lipgloss/v2"
|
|
)
|
|
|
|
// A theme maps roles to a colour and attributes, like lazygit's gui.theme:
|
|
//
|
|
// activeBorderColor: ["#e0af68", bold]
|
|
// selectedLineBgColor: ["#2d3f76"]
|
|
//
|
|
// Role names follow lazygit where the meaning is the same, so an editor
|
|
// plugin can map them from highlight groups the way snacks.nvim does for
|
|
// lazygit. Colours are hex (#rrggbb), a palette number (0-255) or a name
|
|
// (black, red, green, yellow, blue, magenta, cyan, white, their bright
|
|
// variants like brightred, or default). Attributes: bold, italic,
|
|
// underline, reverse, faint.
|
|
|
|
// Roles lists the theme roles with what they colour.
|
|
var Roles = map[string]string{
|
|
"activeBorderColor": "border and title of the focused pane",
|
|
"inactiveBorderColor": "borders of other panes",
|
|
"selectedLineBgColor": "background of the cursor row in the focused pane",
|
|
"inactiveViewSelectedLineBgColor": "background of the cursor row in other panes",
|
|
"defaultFgColor": "default text colour",
|
|
"optionsTextColor": "keys in the help bar and menus, and labels in the detail pane",
|
|
"searchMatchColor": "matched characters when filtering",
|
|
"errorColor": "✗ errors",
|
|
"noticeColor": "notices, the clipboard countdown and current versions",
|
|
"successColor": "the ✓ before confirmations (copied, opened…)",
|
|
"warningColor": "⚠ messages: what can't be done here, and why",
|
|
"mutedColor": "secondary text",
|
|
"favouriteColor": "stars and questions",
|
|
"valueColor": "revealed values",
|
|
"azureColor": "Azure sources and vaults",
|
|
"databricksColor": "Databricks workspaces and scopes",
|
|
"repoColor": "repos and their secret files (SOPS)",
|
|
}
|
|
|
|
// FromTheme returns the default styles with the theme's roles applied, and
|
|
// the default text colour if the theme sets one.
|
|
func FromTheme(theme map[string][]string) (Styles, color.Color, error) {
|
|
st := Default()
|
|
var fg color.Color
|
|
roles := make([]string, 0, len(theme))
|
|
for r := range theme {
|
|
roles = append(roles, r)
|
|
}
|
|
sort.Strings(roles)
|
|
for _, role := range roles {
|
|
if _, ok := Roles[role]; !ok {
|
|
return st, nil, fmt.Errorf("theme: unknown role %q (known: %s)", role, strings.Join(sortedRoles(), ", "))
|
|
}
|
|
v, err := parse(theme[role])
|
|
if err != nil {
|
|
return st, nil, fmt.Errorf("theme: %s: %w", role, err)
|
|
}
|
|
switch role {
|
|
case "activeBorderColor":
|
|
st.BorderFocused = v.fg(lipgloss.NewStyle())
|
|
st.TitleFocused = v.fg(lipgloss.NewStyle()).Bold(true)
|
|
case "inactiveBorderColor":
|
|
st.BorderBlurred = v.fg(lipgloss.NewStyle())
|
|
case "selectedLineBgColor":
|
|
st.Selected = v.bg(lipgloss.NewStyle()).Bold(true)
|
|
st.themed = true
|
|
case "inactiveViewSelectedLineBgColor":
|
|
st.SelectedBlurred = v.bg(lipgloss.NewStyle()).Bold(true)
|
|
st.themedBlurred = true
|
|
case "defaultFgColor":
|
|
fg = v.color
|
|
case "optionsTextColor":
|
|
st.Label = v.fg(lipgloss.NewStyle())
|
|
case "searchMatchColor":
|
|
st.Match = v.fg(lipgloss.NewStyle())
|
|
case "errorColor":
|
|
st.Error = v.fg(lipgloss.NewStyle())
|
|
case "noticeColor":
|
|
st.Notice = v.fg(lipgloss.NewStyle())
|
|
case "warningColor":
|
|
st.Warning = v.fg(lipgloss.NewStyle())
|
|
case "successColor":
|
|
st.Success = v.fg(lipgloss.NewStyle()).Bold(true)
|
|
case "mutedColor":
|
|
st.Muted = v.fg(lipgloss.NewStyle())
|
|
st.Masked, st.Footer = st.Muted, st.Muted
|
|
case "favouriteColor":
|
|
st.Favourite = v.fg(lipgloss.NewStyle())
|
|
st.Prompt = v.fg(lipgloss.NewStyle()).Bold(true)
|
|
case "valueColor":
|
|
st.Value = v.fg(lipgloss.NewStyle())
|
|
case "azureColor":
|
|
st.KindAzure = v.fg(lipgloss.NewStyle())
|
|
case "databricksColor":
|
|
st.KindDatabricks = v.fg(lipgloss.NewStyle())
|
|
case "repoColor":
|
|
st.KindRepo = v.fg(lipgloss.NewStyle())
|
|
}
|
|
}
|
|
return st, fg, nil
|
|
}
|
|
|
|
func sortedRoles() []string {
|
|
var out []string
|
|
for r := range Roles {
|
|
out = append(out, r)
|
|
}
|
|
slices.Sort(out)
|
|
return out
|
|
}
|
|
|
|
// value is a parsed role: a colour (nil for default) and attributes.
|
|
type value struct {
|
|
color color.Color
|
|
bold, italic, underline, reverse, faint bool
|
|
}
|
|
|
|
// fg styles s with the value as foreground, bg as background.
|
|
func (v value) fg(s lipgloss.Style) lipgloss.Style {
|
|
if v.color != nil {
|
|
s = s.Foreground(v.color)
|
|
}
|
|
return v.attrs(s)
|
|
}
|
|
|
|
func (v value) bg(s lipgloss.Style) lipgloss.Style {
|
|
if v.color != nil {
|
|
s = s.Background(v.color)
|
|
}
|
|
return v.attrs(s)
|
|
}
|
|
|
|
func (v value) attrs(s lipgloss.Style) lipgloss.Style {
|
|
return s.Bold(v.bold).Italic(v.italic).Underline(v.underline).Reverse(v.reverse).Faint(v.faint)
|
|
}
|
|
|
|
var names = map[string]color.Color{
|
|
"black": lipgloss.Black, "red": lipgloss.Red, "green": lipgloss.Green, "yellow": lipgloss.Yellow,
|
|
"blue": lipgloss.Blue, "magenta": lipgloss.Magenta, "cyan": lipgloss.Cyan, "white": lipgloss.White,
|
|
"brightblack": lipgloss.BrightBlack, "brightred": lipgloss.BrightRed, "brightgreen": lipgloss.BrightGreen,
|
|
"brightyellow": lipgloss.BrightYellow, "brightblue": lipgloss.BrightBlue,
|
|
"brightmagenta": lipgloss.BrightMagenta, "brightcyan": lipgloss.BrightCyan, "brightwhite": lipgloss.BrightWhite,
|
|
}
|
|
|
|
func parse(items []string) (value, error) {
|
|
var v value
|
|
for _, it := range items {
|
|
s := strings.ToLower(strings.TrimSpace(it))
|
|
switch s {
|
|
case "bold":
|
|
v.bold = true
|
|
case "italic":
|
|
v.italic = true
|
|
case "underline":
|
|
v.underline = true
|
|
case "reverse":
|
|
v.reverse = true
|
|
case "faint", "dim":
|
|
v.faint = true
|
|
case "default", "":
|
|
// the terminal's own colour
|
|
default:
|
|
c, err := parseColor(s)
|
|
if err != nil {
|
|
return v, err
|
|
}
|
|
v.color = c
|
|
}
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
func parseColor(s string) (color.Color, error) {
|
|
if c, ok := names[s]; ok {
|
|
return c, nil
|
|
}
|
|
if strings.HasPrefix(s, "#") {
|
|
if len(s) != 7 {
|
|
return nil, fmt.Errorf("%q: want #rrggbb", s)
|
|
}
|
|
if _, err := strconv.ParseUint(s[1:], 16, 32); err != nil {
|
|
return nil, fmt.Errorf("%q: want #rrggbb", s)
|
|
}
|
|
return lipgloss.Color(s), nil
|
|
}
|
|
if n, err := strconv.Atoi(s); err == nil {
|
|
if n < 0 || n > 255 {
|
|
return nil, fmt.Errorf("%q: palette numbers are 0-255", s)
|
|
}
|
|
return lipgloss.Color(s), nil
|
|
}
|
|
return nil, fmt.Errorf("%q isn't a colour or attribute", s)
|
|
}
|