192 lines
4.0 KiB
Go
192 lines
4.0 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) RGBA(alpha int) string {
|
|
r, g, b, ok := parseHexRGB(c.Value)
|
|
if !ok {
|
|
return c.Value
|
|
}
|
|
if alpha < 100 {
|
|
return "rgba ( " + strconv.Itoa(int(r)) + ", " + strconv.Itoa(int(g)) + ", " + strconv.Itoa(int(b)) + ", " + strconv.Itoa(alpha) + " % )"
|
|
}
|
|
return c.Value
|
|
}
|
|
|
|
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)
|
|
}
|