289 lines
11 KiB
Go
289 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
type tintyColor struct {
|
|
Hex string `json:"hex_str"`
|
|
}
|
|
|
|
type tintyScheme struct {
|
|
ID string `json:"id"`
|
|
System string `json:"system"`
|
|
Variant string `json:"variant"`
|
|
Palette map[string]tintyColor `json:"palette"`
|
|
UI map[string]tintyColor `json:"ui"`
|
|
Syntax map[string]tintyColor `json:"syntax"`
|
|
}
|
|
|
|
func applyApps(zedSource string) error {
|
|
scheme, err := currentScheme()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
colors, err := semanticColors(scheme)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ui := colorValues(scheme.UI)
|
|
syntax := colorValues(scheme.Syntax)
|
|
if err := writeOMPTheme(colors, ui, syntax); err != nil {
|
|
return err
|
|
}
|
|
if err := writeHerdrTheme(colors, ui, syntax); err != nil {
|
|
return err
|
|
}
|
|
if err := writeConjureTheme(scheme.Variant, colors, ui); err != nil {
|
|
return err
|
|
}
|
|
return writeZedTheme(zedSource)
|
|
}
|
|
|
|
func currentScheme() (tintyScheme, error) {
|
|
current, err := exec.Command("tinty", "current").Output()
|
|
if err != nil {
|
|
return tintyScheme{}, err
|
|
}
|
|
output, err := exec.Command("tinty", "list", "--json").Output()
|
|
if err != nil {
|
|
return tintyScheme{}, err
|
|
}
|
|
var schemes []tintyScheme
|
|
if err := json.Unmarshal(output, &schemes); err != nil {
|
|
return tintyScheme{}, err
|
|
}
|
|
id := strings.TrimSpace(string(current))
|
|
for _, scheme := range schemes {
|
|
if scheme.ID == id {
|
|
return scheme, nil
|
|
}
|
|
}
|
|
return tintyScheme{}, fmt.Errorf("scheme not found: %s", id)
|
|
}
|
|
|
|
func colorValues(values map[string]tintyColor) map[string]string {
|
|
result := make(map[string]string, len(values))
|
|
for name, value := range values {
|
|
result[name] = value.Hex
|
|
}
|
|
return result
|
|
}
|
|
|
|
func semanticColors(scheme tintyScheme) (map[string]string, error) {
|
|
colors := colorValues(scheme.Palette)
|
|
if colors["base00"] != "" {
|
|
return colors, nil
|
|
}
|
|
if scheme.System != "tinted8" {
|
|
return nil, fmt.Errorf("unsupported scheme system: %s", scheme.System)
|
|
}
|
|
neutrals := map[string]string{
|
|
"base00": "black-normal", "base01": "black-bright",
|
|
"base02": "gray-dim", "base03": "gray-normal",
|
|
"base04": "gray-bright", "base05": "white-normal",
|
|
"base06": "white-bright", "base07": "white-bright",
|
|
}
|
|
if scheme.Variant == "light" {
|
|
neutrals = map[string]string{
|
|
"base00": "white-normal", "base01": "white-dim",
|
|
"base02": "gray-bright", "base03": "gray-normal",
|
|
"base04": "gray-dim", "base05": "black-normal",
|
|
"base06": "black-dim", "base07": "black-dim",
|
|
}
|
|
}
|
|
aliases := map[string]string{
|
|
"base08": "red-normal", "base09": "orange-normal",
|
|
"base0A": "yellow-normal", "base0B": "green-normal",
|
|
"base0C": "cyan-normal", "base0D": "blue-normal",
|
|
"base0E": "magenta-normal", "base0F": "brown-normal",
|
|
}
|
|
for base, tinted8 := range neutrals {
|
|
aliases[base] = tinted8
|
|
}
|
|
for base, tinted8 := range aliases {
|
|
value := colors[tinted8]
|
|
if value == "" {
|
|
return nil, fmt.Errorf("scheme %s lacks %s", scheme.ID, tinted8)
|
|
}
|
|
colors[base] = value
|
|
}
|
|
return colors, nil
|
|
}
|
|
|
|
func pick(values map[string]string, key string, colors map[string]string, fallback string) string {
|
|
if value := values[key]; value != "" {
|
|
return value
|
|
}
|
|
return colors[fallback]
|
|
}
|
|
|
|
func semanticTheme(colors, ui, syntax map[string]string) (map[string]string, map[string]string) {
|
|
u := func(key, fallback string) string { return pick(ui, key, colors, fallback) }
|
|
s := func(key, fallback string) string { return pick(syntax, key, colors, fallback) }
|
|
accent := u("accent.normal", "base0D")
|
|
foreground := u("global.foreground.normal", "base05")
|
|
muted := u("global.foreground.dark", "base04")
|
|
panel := u("chrome.background.normal", "base01")
|
|
info := u("status.info", "base0C")
|
|
success := u("status.success", "base0B")
|
|
warning := u("status.warning", "base0A")
|
|
failure := u("status.error", "base08")
|
|
omp := map[string]string{
|
|
"accent": accent, "border": u("border.normal", "base03"),
|
|
"borderAccent": accent, "borderMuted": u("indent-guide.background", "base02"),
|
|
"success": success, "error": failure, "warning": warning,
|
|
"muted": muted, "dim": u("whitespace.foreground", "base03"),
|
|
"text": foreground, "thinkingText": muted,
|
|
"selectedBg": u("selection.background", "base02"),
|
|
"userMessageBg": panel, "customMessageBg": panel,
|
|
"toolPendingBg": u("highlight.line.background", "base01"),
|
|
"toolSuccessBg": u("highlight.line.background", "base01"),
|
|
"toolErrorBg": u("highlight.line.background", "base01"),
|
|
"statusLineBg": u("chrome.background.dark", "base01"),
|
|
"userMessageText": foreground, "customMessageText": foreground,
|
|
"customMessageLabel": accent, "toolTitle": u("chrome.foreground.normal", "base05"),
|
|
"toolOutput": muted, "mdHeading": s("markup.heading", "base0D"),
|
|
"mdLink": s("markup.link", "base0C"), "mdLinkUrl": u("link.normal.foreground", "base04"),
|
|
"mdCode": s("markup.raw", "base0B"), "mdCodeBlock": s("text", "base05"),
|
|
"mdCodeBlockBorder": u("border.normal", "base02"),
|
|
"mdQuote": s("markup.quote", "base04"), "mdQuoteBorder": u("border.normal", "base03"),
|
|
"mdHr": u("indent-guide.background", "base02"), "mdListBullet": s("markup.list", "base0D"),
|
|
"toolDiffAdded": s("markup.inserted", "base0B"),
|
|
"toolDiffRemoved": s("markup.deleted", "base08"),
|
|
"toolDiffContext": s("markup.changed", "base04"),
|
|
"syntaxComment": s("comment", "base03"), "syntaxKeyword": s("keyword", "base0E"),
|
|
"syntaxFunction": s("entity.name.function", "base0D"),
|
|
"syntaxVariable": s("variable", "base05"), "syntaxString": s("string", "base0B"),
|
|
"syntaxNumber": s("constant.numeric", "base09"), "syntaxType": s("entity.name.type", "base0C"),
|
|
"syntaxOperator": s("keyword.operator", "base05"), "syntaxPunctuation": s("punctuation", "base05"),
|
|
"thinkingOff": u("whitespace.foreground", "base03"), "thinkingMinimal": muted,
|
|
"thinkingLow": info, "thinkingMedium": accent, "thinkingHigh": s("keyword", "base0E"),
|
|
"thinkingXhigh": warning, "thinkingMax": failure,
|
|
"bashMode": s("string", "base0C"), "pythonMode": s("keyword", "base0A"),
|
|
"statusLineSep": u("border.normal", "base03"), "statusLineModel": accent,
|
|
"statusLinePath": u("link.normal.foreground", "base0D"),
|
|
"statusLineGitClean": success, "statusLineGitDirty": warning,
|
|
"statusLineContext": info, "statusLineSpend": accent, "statusLineStaged": success,
|
|
"statusLineDirty": warning, "statusLineUntracked": info,
|
|
"statusLineOutput": foreground, "statusLineCost": warning, "statusLineSubagents": accent,
|
|
}
|
|
herdr := map[string]string{
|
|
"accent": accent, "panel_bg": u("global.background.normal", "base00"),
|
|
"sidebar_bg": u("chrome.background.dark", "base00"),
|
|
"active_row_bg": u("highlight.line.background", "base01"),
|
|
"selection_bg": u("selection.background", "base02"),
|
|
"surface0": panel, "surface1": u("highlight.line.background", "base02"),
|
|
"surface_dim": u("global.background.dark", "base00"),
|
|
"overlay0": u("border.normal", "base03"), "overlay1": muted,
|
|
"text": foreground, "subtext0": muted, "mauve": s("keyword", "base0E"),
|
|
"green": success, "yellow": warning, "red": failure, "blue": info,
|
|
"teal": accent, "peach": s("constant", "base09"),
|
|
}
|
|
return omp, herdr
|
|
}
|
|
|
|
func writeConjureTheme(variant string, colors, ui map[string]string) error {
|
|
if variant != "light" && variant != "dark" {
|
|
return fmt.Errorf("invalid Conjure variant: %q", variant)
|
|
}
|
|
mapping := map[string]string{
|
|
"background": pick(ui, "global.background.normal", colors, "base00"),
|
|
"text": pick(ui, "global.foreground.normal", colors, "base05"),
|
|
"secondaryText": pick(ui, "global.foreground.dark", colors, "base04"),
|
|
"accent": pick(ui, "accent.normal", colors, "base0D"),
|
|
"glow": pick(ui, "status.error", colors, "base08"),
|
|
"keyBackground": pick(ui, "chrome.background.normal", colors, "base01"),
|
|
"error": pick(ui, "status.error", colors, "base08"),
|
|
}
|
|
validColor := regexp.MustCompile(`^#[0-9a-fA-F]{6}$`)
|
|
for key, value := range mapping {
|
|
if !validColor.MatchString(value) {
|
|
return fmt.Errorf("invalid Conjure color %s: %q", key, value)
|
|
}
|
|
}
|
|
configHome := os.Getenv("XDG_CONFIG_HOME")
|
|
if !filepath.IsAbs(configHome) {
|
|
configHome = filepath.Join(homeDir(), ".config")
|
|
}
|
|
return writeJSON(filepath.Join(configHome, "conjure", "theme.json"), map[string]any{"variant": variant, "colors": mapping})
|
|
}
|
|
|
|
func writeOMPTheme(colors, ui, syntax map[string]string) error {
|
|
semantic, _ := semanticTheme(colors, ui, syntax)
|
|
theme := map[string]any{
|
|
"name": "tinty-current",
|
|
"vars": colors,
|
|
"colors": semantic,
|
|
"export": map[string]string{
|
|
"pageBg": pick(ui, "global.background.normal", colors, "base00"),
|
|
"cardBg": pick(ui, "chrome.background.normal", colors, "base01"),
|
|
"infoBg": pick(ui, "highlight.line.background", colors, "base02"),
|
|
},
|
|
}
|
|
return writeJSON(filepath.Join(homeDir(), ".omp", "agent", "themes", "tinty-current.json"), theme)
|
|
}
|
|
|
|
func writeHerdrTheme(colors, ui, syntax map[string]string) error {
|
|
_, mapping := semanticTheme(colors, ui, syntax)
|
|
path := filepath.Join(homeDir(), ".config", "herdr", "config.toml")
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
keys := []string{"accent", "panel_bg", "sidebar_bg", "active_row_bg", "selection_bg", "surface0", "surface1", "surface_dim", "overlay0", "overlay1", "text", "subtext0", "mauve", "green", "yellow", "red", "blue", "teal", "peach"}
|
|
var block strings.Builder
|
|
block.WriteString("# tint generated theme start\n[theme.custom]\n")
|
|
for _, key := range keys {
|
|
fmt.Fprintf(&block, "%s = %q\n", key, mapping[key])
|
|
}
|
|
block.WriteString("# tint generated theme end\n")
|
|
pattern := regexp.MustCompile(`(?s)\n?` + regexp.QuoteMeta("# tint generated theme start") + `.*?` + regexp.QuoteMeta("# tint generated theme end") + `\n?`)
|
|
clean := strings.TrimSpace(pattern.ReplaceAllString(string(data), "\n"))
|
|
if err := writeAtomic(path, []byte(clean+"\n\n"+block.String()), 0o644); err != nil {
|
|
return err
|
|
}
|
|
if _, err := exec.LookPath("herdr"); err == nil {
|
|
_ = exec.Command("herdr", "server", "reload-config").Run()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writeZedTheme(source string) error {
|
|
data, err := os.ReadFile(source)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var theme map[string]any
|
|
if err := json.Unmarshal(data, &theme); err != nil {
|
|
return err
|
|
}
|
|
theme["name"] = "Tinty Current"
|
|
variants, ok := theme["themes"].([]any)
|
|
if !ok {
|
|
return fmt.Errorf("invalid Zed theme: %s", source)
|
|
}
|
|
for _, value := range variants {
|
|
variant, ok := value.(map[string]any)
|
|
if !ok {
|
|
return fmt.Errorf("invalid Zed theme: %s", source)
|
|
}
|
|
variant["name"] = "Tinty Current"
|
|
}
|
|
return writeJSON(filepath.Join(homeDir(), ".config", "zed", "themes", "tinty-current.json"), theme)
|
|
}
|
|
|
|
func writeJSON(path string, value any) error {
|
|
data, err := json.MarshalIndent(value, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return writeAtomic(path, append(data, '\n'), 0o644)
|
|
}
|