package ui import ( "image/color" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) // theme is a complete set of colors, so the program looks the same whatever // palette the terminal's color theme has (themes often make "black" a grey // that is not their background, and light themes put dark grey slabs under // white text). Every color is one of the fixed entries 16-255 of the // 256-color table, so terminals without 24-bit color show exactly these too // (the nearest match of an exact color is itself, and the first 16 entries, // which themes change, never equal them). type theme struct { name string bg, text, border tcell.Color field, fieldMuted tcell.Color // input fields and buttons; placeholders on them list tcell.Color // open dropdown lists (drawn with bg-colored text) pressed tcell.Color // text of a pressed button (on the text color) toast tcell.Color // background of the popups // The accent colors, used by name in the texts ("[yellow]..."). gray, yellow, green, red, blue, purple tcell.Color } func c256(i int) tcell.Color { return tcell.PaletteColor(i).TrueColor() } var ( darkTheme = theme{ name: "dark", bg: c256(232), text: c256(253), border: c256(246), field: c256(236), fieldMuted: c256(245), list: c256(110), pressed: c256(25), toast: c256(236), gray: c256(245), yellow: c256(221), green: c256(113), red: c256(203), blue: c256(75), purple: c256(141), } lightTheme = theme{ name: "light", bg: c256(255), text: c256(234), border: c256(243), field: c256(252), fieldMuted: c256(245), list: c256(67), pressed: c256(255), toast: c256(252), gray: c256(243), yellow: c256(130), green: c256(28), red: c256(160), blue: c256(26), purple: c256(91), } ) // current is the theme in use. var current = darkTheme // chooseTheme resolves the ui.theme setting: "dark", "light", or "auto" (the // default), which follows the background color the terminal reported and is // dark when it did not say. The second value explains the choice for the log. func chooseTheme(setting string, info terminalInfo) (theme, string) { switch setting { case "dark": return darkTheme, "Theme: dark" case "light": return lightTheme, "Theme: light" } if !info.hasBG { return darkTheme, "Theme: dark (auto: the terminal did not report its background color)" } hex := hexColor(info.bg) if luminance(info.bg) > 0.5 { return lightTheme, "Theme: light (auto: the terminal's background " + hex + " is light)" } return darkTheme, "Theme: dark (auto: the terminal's background " + hex + " is dark)" } // apply makes t the colors of everything created afterwards: tview's default // styles (widgets copy them when they are created, so this must run before any // is) and the color names used in the texts, which tview looks up in tcell's // table: "[yellow]" means this theme's yellow, "white" its text color and // "black" its background (the active tab is "[black:white]"). func (t theme) apply() { current = t st := &tview.Styles st.PrimitiveBackgroundColor = t.bg st.ContrastBackgroundColor = t.field st.MoreContrastBackgroundColor = t.list st.BorderColor, st.TitleColor, st.GraphicsColor = t.border, t.text, t.border st.PrimaryTextColor = t.text st.SecondaryTextColor = t.yellow st.TertiaryTextColor = t.green st.InverseTextColor = t.pressed st.ContrastSecondaryTextColor = t.fieldMuted for name, c := range map[string]tcell.Color{ "white": t.text, "black": t.bg, "gray": t.gray, "grey": t.gray, "yellow": t.yellow, "green": t.green, "red": t.red, "blue": t.blue, "purple": t.purple, } { tcell.ColorNames[name] = c } } // named returns the theme's color for a name used in the texts. func named(name string) tcell.Color { return tcell.GetColor(name) } func hexColor(c color.RGBA) string { const digits = "0123456789abcdef" b := []byte{'#', 0, 0, 0, 0, 0, 0} for i, v := range []uint8{c.R, c.G, c.B} { b[1+2*i], b[2+2*i] = digits[v>>4], digits[v&15] } return string(b) }