feat(theme): add catppuccin themes with transparency and 256-color support
Add a theme package providing all four catppuccin flavors (mocha, frappe, macchiato, latte) mapped to the xterm-256 palette. Themes are selected with -theme or the CTOP_THEME environment variable; -i inverts the flavor. Background slots use the terminal's native color for transparency (opt out with -solid / CTOP_SOLID), and termbox is switched to Output256 so colors render correctly. Replace hardcoded colors in the compact and single views with ctheme portrait slots.
This commit is contained in:
+217
@@ -0,0 +1,217 @@
|
||||
package theme
|
||||
|
||||
import ui "github.com/gizak/termui"
|
||||
|
||||
type Flavor string
|
||||
const (
|
||||
Mocha Flavor = "mocha"
|
||||
Frappe Flavor = "frappe"
|
||||
Macchiato Flavor = "macchiato"
|
||||
Latte Flavor = "latte"
|
||||
)
|
||||
|
||||
func Flavors() []string { return []string{"mocha", "frappe", "macchiato", "latte"} }
|
||||
|
||||
func ParseFlavor(s string) (Flavor, bool) {
|
||||
if s == "mocha" { return Mocha, true }
|
||||
if s == "frappe" { return Frappe, true }
|
||||
if s == "macchiato" { return Macchiato, true }
|
||||
if s == "latte" { return Latte, true }
|
||||
return "", false
|
||||
}
|
||||
|
||||
var paletteMocha = map[string]int{
|
||||
"rosewater": 224,
|
||||
"flamingo": 224,
|
||||
"pink": 218,
|
||||
"mauve": 183,
|
||||
"red": 211,
|
||||
"maroon": 181,
|
||||
"peach": 216,
|
||||
"yellow": 223,
|
||||
"green": 151,
|
||||
"teal": 116,
|
||||
"sky": 116,
|
||||
"sapphire": 117,
|
||||
"blue": 111,
|
||||
"lavender": 147,
|
||||
"text": 189,
|
||||
"subtext1": 146,
|
||||
"subtext0": 146,
|
||||
"overlay2": 103,
|
||||
"overlay1": 103,
|
||||
"overlay0": 243,
|
||||
"surface2": 241,
|
||||
"surface1": 239,
|
||||
"surface0": 237,
|
||||
"base": 235,
|
||||
"mantle": 234,
|
||||
"crust": 233,
|
||||
}
|
||||
|
||||
var paletteFrappe = map[string]int{
|
||||
"rosewater": 224,
|
||||
"flamingo": 217,
|
||||
"pink": 218,
|
||||
"mauve": 182,
|
||||
"red": 174,
|
||||
"maroon": 175,
|
||||
"peach": 216,
|
||||
"yellow": 186,
|
||||
"green": 150,
|
||||
"teal": 115,
|
||||
"sky": 116,
|
||||
"sapphire": 110,
|
||||
"blue": 111,
|
||||
"lavender": 147,
|
||||
"text": 189,
|
||||
"subtext1": 146,
|
||||
"subtext0": 146,
|
||||
"overlay2": 109,
|
||||
"overlay1": 103,
|
||||
"overlay0": 243,
|
||||
"surface2": 60,
|
||||
"surface1": 59,
|
||||
"surface0": 239,
|
||||
"base": 237,
|
||||
"mantle": 236,
|
||||
"crust": 235,
|
||||
}
|
||||
|
||||
var paletteMacchiato = map[string]int{
|
||||
"rosewater": 224,
|
||||
"flamingo": 224,
|
||||
"pink": 218,
|
||||
"mauve": 183,
|
||||
"red": 210,
|
||||
"maroon": 211,
|
||||
"peach": 216,
|
||||
"yellow": 223,
|
||||
"green": 150,
|
||||
"teal": 116,
|
||||
"sky": 116,
|
||||
"sapphire": 116,
|
||||
"blue": 111,
|
||||
"lavender": 147,
|
||||
"text": 189,
|
||||
"subtext1": 146,
|
||||
"subtext0": 146,
|
||||
"overlay2": 103,
|
||||
"overlay1": 103,
|
||||
"overlay0": 243,
|
||||
"surface2": 60,
|
||||
"surface1": 240,
|
||||
"surface0": 238,
|
||||
"base": 236,
|
||||
"mantle": 235,
|
||||
"crust": 234,
|
||||
}
|
||||
|
||||
var paletteLatte = map[string]int{
|
||||
"rosewater": 174,
|
||||
"flamingo": 174,
|
||||
"pink": 176,
|
||||
"mauve": 99,
|
||||
"red": 161,
|
||||
"maroon": 167,
|
||||
"peach": 202,
|
||||
"yellow": 172,
|
||||
"green": 70,
|
||||
"teal": 30,
|
||||
"sky": 38,
|
||||
"sapphire": 37,
|
||||
"blue": 27,
|
||||
"lavender": 69,
|
||||
"text": 240,
|
||||
"subtext1": 60,
|
||||
"subtext0": 243,
|
||||
"overlay2": 102,
|
||||
"overlay1": 246,
|
||||
"overlay0": 248,
|
||||
"surface2": 249,
|
||||
"surface1": 7,
|
||||
"surface0": 252,
|
||||
"base": 255,
|
||||
"mantle": 255,
|
||||
"crust": 254,
|
||||
}
|
||||
|
||||
var palettes = map[Flavor]map[string]int{
|
||||
Mocha: paletteMocha,
|
||||
Frappe: paletteFrappe,
|
||||
Macchiato: paletteMacchiato,
|
||||
Latte: paletteLatte,
|
||||
}
|
||||
|
||||
var slots = map[string]string{
|
||||
"fg": "text",
|
||||
"bg": "",
|
||||
"block.bg": "",
|
||||
"border.bg": "",
|
||||
"border.fg": "overlay1",
|
||||
"label.fg": "mauve",
|
||||
"menu.text.fg": "text",
|
||||
"menu.text.bg": "",
|
||||
"menu.border.fg": "mauve",
|
||||
"menu.label.fg": "mauve",
|
||||
"header.fg": "crust",
|
||||
"header.bg": "mauve",
|
||||
"gauge.bar.bg": "teal",
|
||||
"gauge.percent.fg": "text",
|
||||
"linechart.axes.fg": "overlay1",
|
||||
"linechart.line.fg": "blue",
|
||||
"mbarchart.bar.bg": "teal",
|
||||
"mbarchart.num.fg": "text",
|
||||
"mbarchart.text.fg": "text",
|
||||
"par.text.fg": "text",
|
||||
"par.text.bg": "",
|
||||
"par.text.hi": "crust",
|
||||
"sparkline.line.fg": "blue",
|
||||
"sparkline.title.fg": "subtext1",
|
||||
"status.ok": "green",
|
||||
"status.warn": "yellow",
|
||||
"status.danger": "red",
|
||||
"io.read": "teal",
|
||||
"io.write": "peach",
|
||||
"net.rx": "teal",
|
||||
"net.tx": "peach",
|
||||
}
|
||||
|
||||
var current = Mocha
|
||||
var opaque = false
|
||||
|
||||
func Set(f Flavor) { current = f }
|
||||
func Current() Flavor { return current }
|
||||
|
||||
// SetOpaque toggles whether background slots paint the flavor's base color
|
||||
// (opaque) or leave the terminal background untouched (transparent).
|
||||
func SetOpaque(v bool) { opaque = v }
|
||||
|
||||
func Color(name string) ui.Attribute {
|
||||
if p, ok := palettes[current]; ok {
|
||||
if idx, ok := p[name]; ok {
|
||||
return ui.Attribute(idx + 1)
|
||||
}
|
||||
}
|
||||
return ui.ColorDefault
|
||||
}
|
||||
|
||||
func Attr(slot string) ui.Attribute {
|
||||
if name, ok := slots[slot]; ok {
|
||||
if name == "" && opaque {
|
||||
return Color("base")
|
||||
}
|
||||
return Color(name)
|
||||
}
|
||||
return ui.ColorDefault
|
||||
}
|
||||
|
||||
func BuildColorMap() map[string]ui.Attribute {
|
||||
m := make(map[string]ui.Attribute, len(slots)+4)
|
||||
for slot := range slots {
|
||||
m[slot] = Attr(slot)
|
||||
}
|
||||
m["par.fg"] = Color("text")
|
||||
m["par.label.bg"] = Color("surface0")
|
||||
return m
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
func TestPaletteIndices(t *testing.T) {}
|
||||
var want = map[Flavor]map[string]int{
|
||||
Mocha: {
|
||||
"rosewater": 224,
|
||||
"flamingo": 224,
|
||||
"pink": 218,
|
||||
"mauve": 183,
|
||||
"red": 211,
|
||||
"maroon": 181,
|
||||
"peach": 216,
|
||||
"yellow": 223,
|
||||
"green": 151,
|
||||
"teal": 116,
|
||||
"sky": 116,
|
||||
"sapphire": 117,
|
||||
"blue": 111,
|
||||
"lavender": 147,
|
||||
"text": 189,
|
||||
"subtext1": 146,
|
||||
"subtext0": 146,
|
||||
"overlay2": 103,
|
||||
"overlay1": 103,
|
||||
"overlay0": 243,
|
||||
"surface2": 241,
|
||||
"surface1": 239,
|
||||
"surface0": 237,
|
||||
"base": 235,
|
||||
"mantle": 234,
|
||||
"crust": 233,
|
||||
},
|
||||
Frappe: {
|
||||
"rosewater": 224,
|
||||
"flamingo": 217,
|
||||
"pink": 218,
|
||||
"mauve": 182,
|
||||
"red": 174,
|
||||
"maroon": 175,
|
||||
"peach": 216,
|
||||
"yellow": 186,
|
||||
"green": 150,
|
||||
"teal": 115,
|
||||
"sky": 116,
|
||||
"sapphire": 110,
|
||||
"blue": 111,
|
||||
"lavender": 147,
|
||||
"text": 189,
|
||||
"subtext1": 146,
|
||||
"subtext0": 146,
|
||||
"overlay2": 109,
|
||||
"overlay1": 103,
|
||||
"overlay0": 243,
|
||||
"surface2": 60,
|
||||
"surface1": 59,
|
||||
"surface0": 239,
|
||||
"base": 237,
|
||||
"mantle": 236,
|
||||
"crust": 235,
|
||||
},
|
||||
Macchiato: {
|
||||
"rosewater": 224,
|
||||
"flamingo": 224,
|
||||
"pink": 218,
|
||||
"mauve": 183,
|
||||
"red": 210,
|
||||
"maroon": 211,
|
||||
"peach": 216,
|
||||
"yellow": 223,
|
||||
"green": 150,
|
||||
"teal": 116,
|
||||
"sky": 116,
|
||||
"sapphire": 116,
|
||||
"blue": 111,
|
||||
"lavender": 147,
|
||||
"text": 189,
|
||||
"subtext1": 146,
|
||||
"subtext0": 146,
|
||||
"overlay2": 103,
|
||||
"overlay1": 103,
|
||||
"overlay0": 243,
|
||||
"surface2": 60,
|
||||
"surface1": 240,
|
||||
"surface0": 238,
|
||||
"base": 236,
|
||||
"mantle": 235,
|
||||
"crust": 234,
|
||||
},
|
||||
Latte: {
|
||||
"rosewater": 174,
|
||||
"flamingo": 174,
|
||||
"pink": 176,
|
||||
"mauve": 99,
|
||||
"red": 161,
|
||||
"maroon": 167,
|
||||
"peach": 202,
|
||||
"yellow": 172,
|
||||
"green": 70,
|
||||
"teal": 30,
|
||||
"sky": 38,
|
||||
"sapphire": 37,
|
||||
"blue": 27,
|
||||
"lavender": 69,
|
||||
"text": 240,
|
||||
"subtext1": 60,
|
||||
"subtext0": 243,
|
||||
"overlay2": 102,
|
||||
"overlay1": 246,
|
||||
"overlay0": 248,
|
||||
"surface2": 249,
|
||||
"surface1": 7,
|
||||
"surface0": 252,
|
||||
"base": 255,
|
||||
"mantle": 255,
|
||||
"crust": 254,
|
||||
},
|
||||
}
|
||||
|
||||
func TestPaletteColors(t *testing.T) {
|
||||
for flavor, m := range want {
|
||||
Set(flavor)
|
||||
for name, idx := range m {
|
||||
got := int(Color(name))
|
||||
if got != idx+1 {
|
||||
t.Errorf("%s/%s: Color=%d want %d", flavor, name, got, idx+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlotsResolve(t *testing.T) {
|
||||
for _, f := range []Flavor{Mocha, Frappe, Macchiato, Latte} {
|
||||
Set(f)
|
||||
m := BuildColorMap()
|
||||
for slot := range slots {
|
||||
if _, ok := m[slot]; !ok { t.Fatalf("%s: missing slot %s", f, slot) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFlavor(t *testing.T) {
|
||||
for _, s := range []string{"mocha","frappe","macchiato","latte"} {
|
||||
if _, ok := ParseFlavor(s); !ok { t.Errorf("ParseFlavor(%s) failed", s) }
|
||||
}
|
||||
if _, ok := ParseFlavor("bogus"); ok { t.Error("bogus flavor accepted") }
|
||||
}
|
||||
|
||||
func TestTransparentSlots(t *testing.T) {
|
||||
Set(Mocha)
|
||||
SetOpaque(false)
|
||||
for _, slot := range []string{"bg", "block.bg", "border.bg", "par.text.bg", "menu.text.bg"} {
|
||||
if got := Attr(slot); got != ui.ColorDefault {
|
||||
t.Errorf("transparent %s: got %d want ColorDefault(0)", slot, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpaqueSlots(t *testing.T) {
|
||||
Set(Mocha)
|
||||
SetOpaque(true)
|
||||
base := Color("base")
|
||||
if base == 0 { t.Fatal("Color(base) should not be ColorDefault") }
|
||||
for _, slot := range []string{"bg", "block.bg", "border.bg", "par.text.bg", "menu.text.bg"} {
|
||||
if got := Attr(slot); got != base {
|
||||
t.Errorf("opaque %s: got %d want base(%d)", slot, got, base)
|
||||
}
|
||||
}
|
||||
SetOpaque(false) // restore
|
||||
}
|
||||
|
||||
var _ = ui.ColorDefault // keep import
|
||||
Reference in New Issue
Block a user