Files
arindOS/sithego/themer/theme.go
T

181 lines
3.7 KiB
Go

package themer
import (
"os"
"strconv"
"strings"
"github.com/pelletier/go-toml/v2"
)
type Theme struct {
Meta struct {
Name string
Type string
Version string
}
Primitive map[string]string
Colors struct {
Semantic struct {
Background Color
Surface Color
SurfaceAlt Color
Disabled Color
Border Color
Text Color
Accent Color
Warn Color
}
} `toml:"Colors"`
Spacing struct {
Padding string
PaddingSmall string
PaddingLarge string
Margin string
MarginSmall string
Radius string
RadiusSmall string
RadiusLarge string
} `toml:"Spacing"`
}
type Color struct {
raw string
Value string
}
func (c Color) Alpha(bg Color, amount float64) string {
r1, g1, b1, ok1 := parseHexRGB(c.Value)
r2, g2, b2, ok2 := parseHexRGB(bg.Value)
if !ok1 || !ok2 {
return c.Value
}
r := blend8(r2, r1, amount)
g := blend8(g2, g1, amount)
b := blend8(b2, b1, amount)
return formatHexRGB(r, g, b)
}
func (c *Color) UnmarshalText(text []byte) error {
c.raw = strings.TrimSpace(string(text))
return nil
}
func LoadTheme(path string) (Theme, error) {
theme := Theme{}
if themeFile, err := os.ReadFile(path); err != nil {
return theme, err
} else {
if err := toml.Unmarshal(themeFile, &theme); err != nil {
return theme, err
}
}
theme.resolveColor(&theme.Colors.Semantic.Background)
theme.resolveColor(&theme.Colors.Semantic.Surface)
theme.resolveColor(&theme.Colors.Semantic.SurfaceAlt)
theme.resolveColor(&theme.Colors.Semantic.Disabled)
theme.resolveColor(&theme.Colors.Semantic.Border)
theme.resolveColor(&theme.Colors.Semantic.Text)
theme.resolveColor(&theme.Colors.Semantic.Accent)
theme.resolveColor(&theme.Colors.Semantic.Warn)
return theme, nil
}
func (t Theme) resolveColor(color *Color) {
raw, shadeRaw, hasShade := strings.Cut(color.raw, "|")
base := strings.TrimSpace(raw)
shade, err := strconv.Atoi(strings.TrimSpace(shadeRaw))
if primitive, ok := t.Primitive[base]; ok {
base = primitive
} else {
base = raw
}
base = normalizeHex(base)
if !hasShade || err != nil {
color.Value = base
return
}
if shade < 0 {
shade = 0
}
if shade > 1000 {
shade = 1000
}
r, g, b, ok := parseHexRGB(base)
if !ok {
color.Value = base
return
}
switch {
case shade == 500:
// unchanged
case shade < 500:
amount := float64(500-shade) / 500.0
r = blend8(r, 255, amount)
g = blend8(g, 255, amount)
b = blend8(b, 255, amount)
default:
amount := float64(shade-500) / 500.0
r = blend8(r, 0, amount)
g = blend8(g, 0, amount)
b = blend8(b, 0, amount)
}
color.Value = formatHexRGB(r, g, b)
}
func normalizeHex(s string) string {
s = strings.TrimSpace(s)
if s == "" {
return s
}
if strings.HasPrefix(s, "#") {
return s
}
return "#" + s
}
func parseHexRGB(s string) (uint8, uint8, uint8, bool) {
s = strings.TrimSpace(s)
if strings.HasPrefix(s, "#") {
s = s[1:]
}
if len(s) != 6 {
return 0, 0, 0, false
}
// Manual hex parsing to avoid extra deps; strconv handles base 16 fine.
rv, err1 := strconv.ParseUint(s[0:2], 16, 8)
gv, err2 := strconv.ParseUint(s[2:4], 16, 8)
bv, err3 := strconv.ParseUint(s[4:6], 16, 8)
if err1 != nil || err2 != nil || err3 != nil {
return 0, 0, 0, false
}
return uint8(rv), uint8(gv), uint8(bv), true
}
func formatHexRGB(r, g, b uint8) string {
const hex = "0123456789abcdef"
out := make([]byte, 7)
out[0] = '#'
out[1] = hex[r>>4]
out[2] = hex[r&0x0f]
out[3] = hex[g>>4]
out[4] = hex[g&0x0f]
out[5] = hex[b>>4]
out[6] = hex[b&0x0f]
return string(out)
}
func blend8(a, b uint8, t float64) uint8 {
// result = a*(1-t) + b*t, rounded
v := float64(a)*(1.0-t) + float64(b)*t
if v < 0 {
v = 0
}
if v > 255 {
v = 255
}
return uint8(v + 0.5)
}