cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

cat/module-path
012 / internal / ui / theme / theme.go
theme.go 18.8K
1// Package theme is the look of the UI: the style roles every view draws2// with, and the small widgets built from them (framed boxes, key chips,3// key hints). It knows nothing of the model, so any part of the UI can4// use it.5package theme67import (8 "charm.land/lipgloss/v2"9 "github.com/charmbracelet/x/ansi"1011 "src.rest/cat/012/internal/chart"12 "src.rest/cat/012/internal/sheet"13)1415// Theme holds every style the UI draws with. Views must use these roles16// instead of creating styles inline, so the look stays consistent and a17// new theme only has to be defined here.18//19// Roles are defined on the 16 ANSI colors (New). The terminal theme draws20// them as ANSI indexes, so the sheet follows the user's terminal palette,21// with dark and light variants that only differ where ANSI colors would22// lose contrast. A color scheme (FromPalette) draws the same roles in the23// scheme's colors, corrected for contrast, on its own background.24type Theme struct {25 Indicator lipgloss.Style // mode indicator, top right26 Recording lipgloss.Style // the REC chip beside the mode indicator while a macro is recorded27 Header lipgloss.Style // column letters, row numbers and the name box28 HeaderActive lipgloss.Style // header of the focused row and column29 HeaderSel lipgloss.Style // headers of selected rows and columns30 HeaderHover lipgloss.Style // a header under the mouse31 Handle lipgloss.Style // a column resize handle being hovered or dragged32 Pointer lipgloss.Style // the cell pointer33 Selection lipgloss.Style // a range being pointed at34 Hint lipgloss.Style // guidance on the context line35 Warning lipgloss.Style // recoverable problems, e.g. a formula error36 Error lipgloss.Style // ERROR mode message37 Muted lipgloss.Style // secondary text: key hints, file lists38 Key lipgloss.Style // emphasized text in the status line, e.g. a range39 KeyChip lipgloss.Style // a key cap in hints, menus and the palette, e.g. " Enter "40 ErrorCell lipgloss.Style // cells whose value is ERR or NA41 // ErrorMark is layered on an error's text, so errors show beyond42 // color: a curly underline, in the error color where the terminal43 // supports colored underlines.44 ErrorMark lipgloss.Style45 Link lipgloss.Style // a cell's URL or HYPERLINK label, layered on the cell's role46 Spilled lipgloss.Style // values an array formula spilled into the cells below and right of it47 Found lipgloss.Style // cells matching an open search48 // Precedent and Dependent mark what a formula reads and the formulas49 // reading a cell, while traced: in reverse video, dependents bold too,50 // so the two read apart without color.51 Precedent lipgloss.Style52 Dependent lipgloss.Style53 Argument lipgloss.Style // the argument at the caret in a function's signature54 // EvalNext is the part of a formula Evaluate formula computes next,55 // underlined; Evaluated the values it has put in place of the parts56 // it computed, italic.57 EvalNext lipgloss.Style58 Evaluated lipgloss.Style59 Progress lipgloss.Style // the done part of an import's progress bar60 ProgressTodo lipgloss.Style // the rest of the progress bar61 // Copied marks the range on the clipboard, like Sheets' dashed border:62 // a dashed underline across every cell, layered on the cell's own63 // style, with its own text color where the cell has none.64 Copied lipgloss.Style65 // FrozenLine divides frozen rows and columns from the scrolling ones,66 // like a tmux pane border.67 FrozenLine lipgloss.Style68 // Sheet tabs on the status line, like lazygit's panel tabs: plain69 // names (so they don't read as key chips), the sheet shown in the70 // accent and bold, and a tab under the mouse, or where a dragged tab71 // would go, bold and underlined.72 Tab lipgloss.Style73 TabActive lipgloss.Style74 TabHover lipgloss.Style75 // FilterOn is the filter mark in the header of a column whose filter76 // hides something (the mark itself also changes, from ▾ to ▼).77 FilterOn lipgloss.Style78 // TableHeader is a table's header row when the table styles it,79 // bold and underlined so it reads without color. TableBand is every80 // other data row of a banded table: a band of the background moved81 // toward the text, with the text on it readable.82 TableHeader lipgloss.Style83 TableBand lipgloss.Style84 // NoteMark is the mark in the top-right corner of a cell with a note,85 // like Sheets' small triangle.86 NoteMark lipgloss.Style87 // CellBorder draws the lines of Format > Borders, in the ink of the88 // text beside them rather than a color of their own, as Sheets draws89 // borders black.90 CellBorder lipgloss.Style9192 // Conditional formats (rules.go), indexed by sheet.Color: a rule's93 // text color on the cell, its fill with text readable on it, and94 // RuleOn[fill*sheet.NumColors+text] a text color on a fill, in the95 // fill's own ink where the text color wouldn't read on it.96 RuleText [sheet.NumColors]lipgloss.Style97 RuleFill [sheet.NumColors]lipgloss.Style98 RuleOn [sheet.NumColors * sheet.NumColors]lipgloss.Style99 // Invalid is layered on the text of a cell that fails its data100 // validation, as Sheets' red corner: a dotted underline, in the101 // warning color where the terminal colors underlines.102 Invalid lipgloss.Style103 // Dropdown is the ▾ at the right of a cell with a dropdown list.104 Dropdown lipgloss.Style105 // DropdownChip is a dropdown's value drawn as a chip, as Sheets106 // draws them: in reverse video, so it reads without color, with its107 // rounded ends (▐ ▌) in DropdownCap, the chip's color.108 DropdownChip lipgloss.Style109 DropdownCap lipgloss.Style110 // BarOn is text a data bar of a rule color runs under: reverse111 // video in the bar's color (see bars.go).112 BarOn [sheet.NumColors]lipgloss.Style113 // scales and shades keep the shades of color scales and of rule114 // colors drawn so far.115 scales map[scaleKey]Shade116 shades map[sheet.RuleStyle]Shade117118 // Chrome: the menu bar, dropdowns, the palette and dialogs.119 MenuBar lipgloss.Style // menu bar titles120 MenuAccel lipgloss.Style // a title's accelerator letter121 MenuSelected lipgloss.Style // open title, highlighted item122 MenuAccelSelected lipgloss.Style // accelerator letter of the open title123 Border lipgloss.Style // box borders and separators124 Title lipgloss.Style // box titles and group headings125 Disabled lipgloss.Style // items that can't run right now126 Match lipgloss.Style // characters matched by a search127 MatchSelected lipgloss.Style // matched characters in the highlighted row128 Cell lipgloss.Style // an ordinary cell: the base for bold, italic and underline129130 // Bars: full-width bands behind the menu bar, formula bar, context131 // line, column headers and status line. Each line is drawn on its132 // role's background, with its foreground for text that has none, out133 // to the terminal's edge; an empty role leaves the line as it is.134 MenuBarRow lipgloss.Style135 FormulaBarRow lipgloss.Style136 ContextRow lipgloss.Style137 ColumnHeaderRow lipgloss.Style138 StatusBarRow lipgloss.Style139 RowHeader lipgloss.Style // row numbers not focused, selected or hovered140 // Screen is the background and default text color of the whole141 // screen. Empty in the terminal theme, so the terminal's own show.142 Screen lipgloss.Style143144 // Name is the theme's name, and Palette its colors: nil for the145 // terminal theme, whose colors only the terminal knows.146 Name string147 Palette *Palette148149 // Notebooks: see package nbview. Code is a code cell's syntax, by150 // its kind (SyntaxCommand and the rest). CellHead is what the cells151 // say around their code: the [1]: and Out[1]: prompts, a cell's name152 // and state on its box. CellBar is the bar left of the active cell in153 // command mode, blue as Jupyter's, and CellBarEdit in edit mode,154 // green, which also draws the edited cell's box; both are glyphs, so155 // they read without color. OutputHead is an output table's header156 // row, bold and underlined so it reads without color, and Stale the157 // mark of an output that may be out of date.158 Code [NumSyntax]lipgloss.Style159 CellHead lipgloss.Style160 CellBar lipgloss.Style161 CellBarEdit lipgloss.Style162 OutputHead lipgloss.Style163 Stale lipgloss.Style164165 // Charts: see charts.go in package ui.166 ChartFrame lipgloss.Style // a chart's border167 ChartSelected lipgloss.Style // the border of the selected chart and its resize handle168 ChartAxis lipgloss.Style // axis lines and tick marks169 ChartLabel lipgloss.Style // tick values, category labels and legend text170 // Series colors bars, lines, slices and legend swatches, in order;171 // SeriesBg is the same colors as backgrounds, for the lower half of a172 // pie's half blocks. SeriesANSI is the ANSI index of each, so images173 // can use the terminal's own colors.174 Series [chart.Colors]lipgloss.Style175 SeriesBg [chart.Colors]lipgloss.Style176 SeriesANSI [chart.Colors]int177178 // Peers are the others sharing a workbook in 012 serve, each in a179 // color of their own: their pointer's cell in it, with a double180 // underline so it reads without color, and their initial as a chip181 // in it on the row header and their name on the status line (Peer,182 // text readable on the color); a cell they changed lately has a ▘ in183 // its top-left corner in their color (PeerMark).184 Peer [Peers]lipgloss.Style185 PeerMark [Peers]lipgloss.Style186 // Suggested marks a cell the agent's suggestion would set, waiting187 // for the person to accept or reject it (live mode): a ◇ in the188 // cell's top-left corner, so it reads without color.189 Suggested lipgloss.Style190191 // levels are the contrast minimums the roles meet: WCAG AA, or AAA192 // for a high-contrast scheme.193 levels levels194}195196// Peers is how many colors tell the others sharing a workbook apart.197const Peers = 6198199// peerRoles are the others' colors: on a dark terminal with black200// text, on a light one with text in black or white, whichever reads on201// the color. Blue and cyan stay the user's own (the selection, the202// pointer) where they can.203func peerRoles(t *Theme, dark bool) {204 type pair struct{ bg, fg ansi.BasicColor }205 peers := [Peers]pair{{lipgloss.Magenta, lipgloss.Black}, {lipgloss.Green, lipgloss.Black}, {lipgloss.Yellow, lipgloss.Black},206 {lipgloss.BrightBlue, lipgloss.Black}, {lipgloss.Red, lipgloss.Black}, {lipgloss.White, lipgloss.Black}}207 if !dark {208 peers = [Peers]pair{{lipgloss.Magenta, lipgloss.BrightWhite}, {lipgloss.Green, lipgloss.Black}, {lipgloss.Yellow, lipgloss.Black},209 {lipgloss.Blue, lipgloss.BrightWhite}, {lipgloss.Red, lipgloss.BrightWhite}, {lipgloss.White, lipgloss.Black}}210 }211 for i, p := range peers {212 t.Peer[i] = lipgloss.NewStyle().Background(p.bg).Foreground(p.fg)213 mark := p.bg214 if mark == lipgloss.White && !dark {215 mark = lipgloss.BrightBlack // white on a light screen doesn't show216 }217 t.PeerMark[i] = lipgloss.NewStyle().Foreground(mark)218 }219}220221// PeerCell is the role of a cell under another's pointer: their color,222// double-underlined.223func (t *Theme) PeerCell(i int) lipgloss.Style {224 return t.Peer[i%Peers].UnderlineStyle(lipgloss.UnderlineDouble)225}226227// ImageID is the style of an image's Unicode placeholders: the228// foreground color is not a color but the image's id, in the 256-color229// palette, which is how the terminal knows which image to draw there.230func (t *Theme) ImageID(id int) lipgloss.Style {231 return lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(id))232}233234// Text adds a cell's bold, italic, underline and strikethrough to base,235// one of the cell roles (cell, pointer, selection, errorCell), so text236// styles show through the pointer and selection colors.237func (t *Theme) Text(base lipgloss.Style, st sheet.Style) lipgloss.Style {238 if st.Bold {239 base = base.Bold(true)240 }241 if st.Italic {242 base = base.Italic(true)243 }244 if st.Underline {245 base = base.Underline(true)246 }247 if st.Strikethrough {248 base = base.Strikethrough(true)249 }250 return base251}252253// New returns the terminal theme's dark or light variant.254func New(dark bool) Theme {255 t := roles(dark)256 t.standouts()257 return t258}259260// standouts draws the roles that mark where the user is (the pointer,261// the selection, their headers, search matches, traced cells and the262// highlighted menu item) in reverse video: the colors swapped and SGR 7263// on, so they look the same in color and still stand out on a terminal264// without it. Call it last: the roles' colors read reversed afterwards.265func (t *Theme) standouts() {266 for _, s := range []*lipgloss.Style{&t.Pointer, &t.Selection, &t.HeaderActive, &t.HeaderSel,267 &t.Found, &t.Precedent, &t.Dependent, &t.MenuSelected, &t.MenuAccelSelected, &t.DropdownChip} {268 fg, bg := s.GetForeground(), s.GetBackground()269 *s = s.Foreground(bg).Background(fg).Reverse(true)270 }271}272273// Drawable returns s as it can be drawn: a standout role with an274// underline or strikethrough layered on it is drawn in its own colors275// without reverse video, because lipgloss draws the spaces of underlined276// or struck-out text without it, which would show their colors swapped.277// The underline or strikethrough is then that text's cue without color.278func Drawable(s lipgloss.Style) lipgloss.Style {279 if !s.GetReverse() || !s.GetUnderline() && !s.GetStrikethrough() {280 return s281 }282 fg, bg := s.GetForeground(), s.GetBackground()283 return s.Foreground(bg).Background(fg).Reverse(false)284}285286// roles are the terminal theme's roles, before standouts.287func roles(dark bool) Theme {288 // Contrast was checked against the reference palettes in e2e: text on289 // colored backgrounds stays at or above roughly 4.5:1.290 headerBg, headerFg := lipgloss.BrightBlack, lipgloss.BrightWhite291 selFg, muted, match := lipgloss.Black, lipgloss.BrightBlack, lipgloss.Yellow292 bar := lipgloss.Cyan293 filterFg := lipgloss.Yellow294 noteFg := lipgloss.Yellow295 suggested := lipgloss.BrightMagenta296 borderFg := lipgloss.White297 // Bands are the header's gray, lightest on a light terminal.298 bandBg, bandFg := lipgloss.BrightBlack, lipgloss.BrightWhite299 if !dark {300 bandBg, bandFg = lipgloss.White, lipgloss.Black301 borderFg = lipgloss.Black302 noteFg = lipgloss.Magenta303 suggested = lipgloss.Magenta304 headerBg, headerFg = lipgloss.White, lipgloss.Black305 selFg, muted, match = lipgloss.BrightWhite, lipgloss.Black, lipgloss.Blue306 filterFg = lipgloss.Blue307 bar = lipgloss.Blue308 }309 // Links are blue, as in Sheets; plain blue is too dark on a dark310 // background.311 link := lipgloss.BrightBlue312 // Series colors, most distinct first. On light backgrounds yellow and313 // cyan fade, so they come last or not at all.314 series := [chart.Colors]ansi.BasicColor{lipgloss.Cyan, lipgloss.Magenta, lipgloss.Yellow, lipgloss.Green, lipgloss.BrightBlue, lipgloss.BrightRed}315 if !dark {316 link = lipgloss.Blue317 series = [chart.Colors]ansi.BasicColor{lipgloss.Blue, lipgloss.Magenta, lipgloss.Green, lipgloss.Red, lipgloss.Cyan, lipgloss.BrightBlack}318 }319 accent := lipgloss.NewStyle().Background(lipgloss.Cyan).Foreground(lipgloss.Black)320 t := Theme{321 Indicator: accent.Bold(true),322 Recording: lipgloss.NewStyle().Background(lipgloss.Red).Foreground(selFg).Bold(true),323 Header: lipgloss.NewStyle().Background(headerBg).Foreground(headerFg),324 HeaderActive: accent.Bold(true),325 HeaderSel: lipgloss.NewStyle().Background(lipgloss.Blue).Foreground(selFg),326 HeaderHover: lipgloss.NewStyle().Background(headerBg).Foreground(lipgloss.Cyan).Bold(true),327 Handle: lipgloss.NewStyle().Background(headerBg).Foreground(lipgloss.Cyan).Bold(true),328 Pointer: accent,329 Selection: lipgloss.NewStyle().Background(lipgloss.Blue).Foreground(selFg),330 Hint: lipgloss.NewStyle().Foreground(muted),331 Warning: lipgloss.NewStyle().Foreground(lipgloss.Yellow),332 Error: lipgloss.NewStyle().Foreground(lipgloss.BrightRed).Bold(true),333 Muted: lipgloss.NewStyle().Foreground(muted),334 Key: lipgloss.NewStyle().Bold(true),335 KeyChip: lipgloss.NewStyle().Background(headerBg).Foreground(headerFg),336 ErrorCell: lipgloss.NewStyle().Foreground(lipgloss.Red),337 ErrorMark: lipgloss.NewStyle().UnderlineStyle(lipgloss.UnderlineCurly).UnderlineColor(lipgloss.Red),338 Link: lipgloss.NewStyle().Foreground(link).Underline(true),339 Spilled: lipgloss.NewStyle().Foreground(bar).Italic(true),340 Found: lipgloss.NewStyle().Background(lipgloss.Yellow).Foreground(lipgloss.Black),341 Precedent: lipgloss.NewStyle().Background(lipgloss.Green).Foreground(lipgloss.Black),342 Dependent: lipgloss.NewStyle().Background(lipgloss.Magenta).Foreground(lipgloss.Black).Bold(true),343 Argument: lipgloss.NewStyle().Bold(true).Underline(true),344 EvalNext: lipgloss.NewStyle().Bold(true).Underline(true),345 Evaluated: lipgloss.NewStyle().Foreground(bar).Italic(true),346 Progress: lipgloss.NewStyle().Foreground(bar),347 ProgressTodo: lipgloss.NewStyle().Foreground(lipgloss.BrightBlack),348 Copied: lipgloss.NewStyle().Foreground(lipgloss.Magenta).349 UnderlineStyle(lipgloss.UnderlineDashed).UnderlineSpaces(true),350 FrozenLine: lipgloss.NewStyle().Foreground(lipgloss.BrightBlack),351 Tab: lipgloss.NewStyle(),352 TabActive: accent.Bold(true),353 TabHover: lipgloss.NewStyle().Foreground(bar).Bold(true).Underline(true),354 FilterOn: lipgloss.NewStyle().Background(headerBg).Foreground(filterFg).Bold(true),355 NoteMark: lipgloss.NewStyle().Foreground(noteFg),356 TableHeader: lipgloss.NewStyle().Foreground(bar).Bold(true).Underline(true),357 TableBand: lipgloss.NewStyle().Background(bandBg).Foreground(bandFg),358 CellBorder: lipgloss.NewStyle().Foreground(borderFg),359360 MenuBar: lipgloss.NewStyle(),361 MenuAccel: lipgloss.NewStyle().Underline(true),362 MenuSelected: accent,363 MenuAccelSelected: accent.Underline(true),364 Border: lipgloss.NewStyle().Foreground(lipgloss.BrightBlack),365 Title: lipgloss.NewStyle().Bold(true),366 Disabled: lipgloss.NewStyle().Foreground(lipgloss.BrightBlack), // exempt from contrast, like Sheets367 Match: lipgloss.NewStyle().Foreground(match).Bold(true),368 MatchSelected: accent.Bold(true).Underline(true),369 Cell: lipgloss.NewStyle(),370371 RowHeader: lipgloss.NewStyle().Background(headerBg).Foreground(headerFg),372 Name: Terminal,373374 ChartFrame: lipgloss.NewStyle().Foreground(lipgloss.BrightBlack),375 ChartSelected: lipgloss.NewStyle().Foreground(lipgloss.Cyan).Bold(true),376 ChartAxis: lipgloss.NewStyle().Foreground(lipgloss.BrightBlack),377 ChartLabel: lipgloss.NewStyle().Foreground(muted),378379 Invalid: lipgloss.NewStyle().UnderlineStyle(lipgloss.UnderlineDotted).UnderlineColor(lipgloss.Yellow),380 Dropdown: lipgloss.NewStyle().Foreground(muted),381 DropdownChip: lipgloss.NewStyle().Background(headerBg).Foreground(headerFg),382 DropdownCap: lipgloss.NewStyle().Foreground(headerBg),383 scales: map[scaleKey]Shade{},384 shades: map[sheet.RuleStyle]Shade{},385 levels: aa,386 }387 ruleRoles(&t, dark)388 codeRoles(&t, dark)389 peerRoles(&t, dark)390 t.Suggested = lipgloss.NewStyle().Foreground(suggested).Bold(true)391 for i, c := range series {392 t.Series[i] = lipgloss.NewStyle().Foreground(c)393 t.SeriesBg[i] = lipgloss.NewStyle().Background(c)394 t.SeriesANSI[i] = int(c)395 }396 return t397}