1package theme23import (4 "image/color"5 "math"6 "strings"78 "charm.land/lipgloss/v2"9 "github.com/charmbracelet/x/ansi"1011 "src.rest/cat/012/internal/sheet"12)1314// Conditional formats draw in the named colors of sheet.Color, each an15// ANSI slot, so a rule looks right in the terminal's palette and in every16// color scheme: a text color is the slot as a foreground, a fill the slot17// as a background with text readable on it, and a color scale blends18// the slots of its points.1920// ruleSlots are the ANSI colors of sheet.Color, by index.21var ruleSlots = [sheet.NumColors]ansi.BasicColor{0, lipgloss.Red, lipgloss.Yellow, lipgloss.Green,22 lipgloss.Cyan, lipgloss.Blue, lipgloss.Magenta}2324// RuleSlot is the ANSI color index of a rule color, or -1 for none.25func RuleSlot(c sheet.Color) int {26 if c == sheet.ColorNone || int(c) >= sheet.NumColors {27 return -128 }29 return int(ruleSlots[c])30}3132// referenceANSI are the palettes the terminal theme's rule colors are33// checked against, the reference palettes of the e2e screens (common34// Ghostty defaults), dark and light.35var referenceANSI = [2][16]uint32{36 {0x1d1f21, 0xcc6666, 0xb5bd68, 0xf0c674, 0x81a2be, 0xb294bb, 0x8abeb7, 0xc5c8c6,37 0x666666, 0xd54e53, 0xb9ca4a, 0xe7c547, 0x7aa6da, 0xc397d8, 0x70c0b1, 0xeaeaea},38 {0x1d1f21, 0xc82829, 0x718c00, 0xb58900, 0x4271ae, 0x8959a8, 0x3e999f, 0xd6d6d6,39 0x8e908c, 0xc82829, 0x718c00, 0xb58900, 0x4271ae, 0x8959a8, 0x3e999f, 0xffffff},40}4142func reference(dark bool, slot ansi.BasicColor) color.Color {43 p := referenceANSI[1]44 if dark {45 p = referenceANSI[0]46 }47 v := p[slot%16]48 return color.RGBA{R: uint8(v >> 16), G: uint8(v >> 8), B: uint8(v), A: 0xff}49}5051// ruleInk is the text color on a fill: black or bright white, whichever52// reads better on the reference palette.53func ruleInk(dark bool, fill ansi.BasicColor) ansi.BasicColor {54 bg := reference(dark, fill)55 if contrast(reference(dark, lipgloss.Black), bg) >= contrast(reference(dark, lipgloss.BrightWhite), bg) {56 return lipgloss.Black57 }58 return lipgloss.BrightWhite59}6061// ruleRoles sets the roles of rule colors for the terminal theme.62func ruleRoles(t *Theme, dark bool) {63 n := sheet.NumColors64 for c := 1; c < n; c++ {65 slot := ruleSlots[c]66 t.RuleText[c] = lipgloss.NewStyle().Foreground(slot)67 t.RuleFill[c] = lipgloss.NewStyle().Background(slot).Foreground(ruleInk(dark, slot))68 t.BarOn[c] = lipgloss.NewStyle().Foreground(slot).Reverse(true)69 }70 for f := 1; f < n; f++ {71 for c := 1; c < n; c++ {72 // A text color on a fill where it reads; the fill's own ink73 // where it wouldn't (red on red, yellow on green).74 fg := ruleSlots[c]75 if c == f || contrast(reference(dark, fg), reference(dark, ruleSlots[f])) < minText {76 fg = ruleInk(dark, ruleSlots[f])77 }78 t.RuleOn[f*n+c] = lipgloss.NewStyle().Background(ruleSlots[f]).Foreground(fg)79 }80 }81}8283// Rule is the base style of a cell a single-color rule formats: its84// fill and text color, with text readable on the fill. Text styles are85// added by Text.86func (t *Theme) Rule(st sheet.RuleStyle) lipgloss.Style {87 switch {88 case st.Fill == sheet.ColorNone && st.Text == sheet.ColorNone:89 return t.Cell90 case st.Fill == sheet.ColorNone:91 return t.RuleText[st.Text]92 case st.Text == sheet.ColorNone:93 return t.RuleFill[st.Fill]94 }95 return t.RuleOn[int(st.Fill)*sheet.NumColors+int(st.Text)]96}9798// scaleSteps is how many shades a color scale has between two points.99const scaleSteps = 32100101type scaleKey struct {102 from, to color.RGBA103 step int104}105106// Shade is a style drawn often, with the escape codes that open and107// close it, so a cell of plain text in it is written without rendering108// the style again.109type Shade struct {110 Style lipgloss.Style111 Open, Close string112}113114// ShadeOf is s with the escape codes that open and close it, for text115// drawn in it often.116func ShadeOf(s lipgloss.Style) Shade { return shadeOf(s) }117118func shadeOf(s lipgloss.Style) Shade {119 r := s.Render(shadeMark)120 i := strings.Index(r, shadeMark)121 if i < 0 {122 return Shade{Style: s}123 }124 return Shade{Style: s, Open: r[:i], Close: r[i+len(shadeMark):]}125}126127// shadeMark stands for the text while a shade's codes are worked out: a128// private use character, which no style changes.129const shadeMark = ""130131// Wrap draws text in the shade.132func (s Shade) Wrap(text string) string {133 if s.Open == "" && s.Close == "" {134 return s.Style.Render(text)135 }136 return s.Open + text + s.Close137}138139// RuleShade is Rule's style with its codes, kept per theme.140func (t *Theme) RuleShade(st sheet.RuleStyle) Shade {141 k := sheet.RuleStyle{Text: st.Text, Fill: st.Fill}142 if s, ok := t.shades[k]; ok {143 return s144 }145 s := shadeOf(t.Rule(k))146 if t.shades != nil {147 t.shades[k] = s148 }149 return s150}151152// ScaleFill is the fill of a color scale's cell, pos (0 to 1) of the way153// from one point's color to the next's, with text readable on it. The154// colors are the scheme's, or for the terminal theme rgb's (the155// terminal's palette as it reports it). Shades are kept per theme.156func (t *Theme) ScaleFill(from, to sheet.Color, pos float64, rgb func(slot int) color.Color) Shade {157 slotRGB := func(c sheet.Color) color.RGBA {158 var col color.Color = color.Black159 if s := RuleSlot(c); s >= 0 {160 if t.Palette != nil {161 col = t.Palette.ANSI[s]162 } else {163 col = rgb(s)164 }165 }166 r, g, b, _ := col.RGBA()167 return color.RGBA{R: uint8(r >> 8), G: uint8(g >> 8), B: uint8(b >> 8), A: 0xff}168 }169 step := int(math.Round(min(max(pos, 0), 1) * scaleSteps))170 k := scaleKey{slotRGB(from), slotRGB(to), step}171 if s, ok := t.scales[k]; ok {172 return s173 }174 bg := blend(k.from, k.to, float64(step)/scaleSteps)175 s := shadeOf(lipgloss.NewStyle().Background(bg).Foreground(t.scaleInk(bg, rgb)))176 if t.scales != nil && len(t.scales) < 4096 {177 t.scales[k] = s178 }179 return s180}181182// scaleInk is the text color on a scale's shade: the screen's text or183// background color, or failing both black or white.184func (t *Theme) scaleInk(bg color.Color, rgb func(int) color.Color) color.Color {185 fg, back := rgb(15), rgb(0)186 if t.Palette != nil {187 fg, back = t.Palette.Foreground, t.Palette.Background188 }189 best := fg190 if contrast(back, bg) > contrast(fg, bg) {191 best = back192 }193 if contrast(best, bg) >= minText {194 return best195 }196 if contrast(color.Black, bg) > contrast(color.White, bg) {197 return color.Black198 }199 return color.White200}