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 } id := strings.TrimSpace(string(current)) for _, args := range [][]string{{"list", "--json"}, {"list", "--json", "--custom-schemes"}} { output, err := exec.Command("tinty", args...).Output() if err != nil { return tintyScheme{}, err } var schemes []tintyScheme if err := json.Unmarshal(output, &schemes); err != nil { return tintyScheme{}, err } 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) }