diff options
| author | AlexanderCurl <alexc@alexc.hu> | 2026-04-18 17:46:06 +0100 |
|---|---|---|
| committer | AlexanderCurl <alexc@alexc.hu> | 2026-04-18 17:46:06 +0100 |
| commit | 70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69 (patch) | |
| tree | ab1123d4067c1b086dd6faa7ee4ea643236b565a /raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/matugen/matugen.go | |
| parent | 5d94c0a7d44a2255b81815a52a7056a94a39842d (diff) | |
| download | RaveOS-PKGBUILD-70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69.tar.gz RaveOS-PKGBUILD-70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69.zip | |
Replaced file structures for themes in the correct format
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/matugen/matugen.go')
| -rw-r--r-- | raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/matugen/matugen.go | 978 |
1 files changed, 978 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/matugen/matugen.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/matugen/matugen.go new file mode 100644 index 0000000..a29d324 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/matugen/matugen.go @@ -0,0 +1,978 @@ +package matugen + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "math" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "sync" + "syscall" + "time" + + "github.com/AvengeMedia/DankMaterialShell/core/internal/dank16" + "github.com/AvengeMedia/DankMaterialShell/core/internal/log" + "github.com/AvengeMedia/DankMaterialShell/core/internal/utils" + "github.com/lucasb-eyer/go-colorful" +) + +var ErrNoChanges = errors.New("no color changes") + +type ColorMode string + +const ( + ColorModeDark ColorMode = "dark" + ColorModeLight ColorMode = "light" +) + +type TemplateKind int + +const ( + TemplateKindNormal TemplateKind = iota + TemplateKindTerminal + TemplateKindGTK + TemplateKindVSCode + TemplateKindEmacs +) + +type TemplateDef struct { + ID string + Commands []string + Flatpaks []string + ConfigFile string + Kind TemplateKind + RunUnconditionally bool +} + +var templateRegistry = []TemplateDef{ + {ID: "gtk", Kind: TemplateKindGTK, RunUnconditionally: true}, + {ID: "niri", Commands: []string{"niri"}, ConfigFile: "niri.toml"}, + {ID: "hyprland", Commands: []string{"Hyprland"}, ConfigFile: "hyprland.toml"}, + {ID: "mangowc", Commands: []string{"mango"}, ConfigFile: "mangowc.toml"}, + {ID: "qt5ct", Commands: []string{"qt5ct"}, ConfigFile: "qt5ct.toml"}, + {ID: "qt6ct", Commands: []string{"qt6ct"}, ConfigFile: "qt6ct.toml"}, + {ID: "firefox", Commands: []string{"firefox"}, ConfigFile: "firefox.toml"}, + {ID: "pywalfox", Commands: []string{"pywalfox"}, ConfigFile: "pywalfox.toml"}, + {ID: "zenbrowser", Commands: []string{"zen", "zen-browser", "zen-beta", "zen-twilight"}, Flatpaks: []string{"app.zen_browser.zen"}, ConfigFile: "zenbrowser.toml"}, + {ID: "vesktop", Commands: []string{"vesktop"}, Flatpaks: []string{"dev.vencord.Vesktop"}, ConfigFile: "vesktop.toml"}, + {ID: "equibop", Commands: []string{"equibop"}, ConfigFile: "equibop.toml"}, + {ID: "ghostty", Commands: []string{"ghostty"}, ConfigFile: "ghostty.toml", Kind: TemplateKindTerminal}, + {ID: "kitty", Commands: []string{"kitty"}, ConfigFile: "kitty.toml", Kind: TemplateKindTerminal}, + {ID: "foot", Commands: []string{"foot"}, ConfigFile: "foot.toml", Kind: TemplateKindTerminal}, + {ID: "alacritty", Commands: []string{"alacritty"}, ConfigFile: "alacritty.toml", Kind: TemplateKindTerminal}, + {ID: "wezterm", Commands: []string{"wezterm"}, ConfigFile: "wezterm.toml", Kind: TemplateKindTerminal}, + {ID: "nvim", Commands: []string{"nvim"}, ConfigFile: "neovim.toml", Kind: TemplateKindTerminal}, + {ID: "dgop", Commands: []string{"dgop"}, ConfigFile: "dgop.toml"}, + {ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true}, + {ID: "vscode", Kind: TemplateKindVSCode}, + {ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml", Kind: TemplateKindEmacs}, + {ID: "zed", Commands: []string{"zed", "zeditor", "zedit"}, ConfigFile: "zed.toml"}, +} + +func (c *ColorMode) GTKTheme() string { + switch *c { + case ColorModeDark: + return "adw-gtk3-dark" + default: + return "adw-gtk3" + } +} + +var ( + matugenVersionMu sync.Mutex + matugenVersionOK bool + matugenSupportsCOE bool + matugenIsV4 bool +) + +type Options struct { + StateDir string + ShellDir string + ConfigDir string + Kind string + Value string + Mode ColorMode + IconTheme string + MatugenType string + Contrast float64 + RunUserTemplates bool + ColorsOnly bool + StockColors string + SyncModeWithPortal bool + TerminalsAlwaysDark bool + SkipTemplates string + AppChecker utils.AppChecker +} + +type ColorsOutput struct { + Colors struct { + Dark map[string]string `json:"dark"` + Light map[string]string `json:"light"` + } `json:"colors"` +} + +func (o *Options) ColorsOutput() string { + return filepath.Join(o.StateDir, "dms-colors.json") +} + +func (o *Options) ShouldSkipTemplate(name string) bool { + if o.SkipTemplates == "" { + return false + } + for _, skip := range strings.Split(o.SkipTemplates, ",") { + if strings.TrimSpace(skip) == name { + return true + } + } + return false +} + +func Run(opts Options) error { + if opts.StateDir == "" { + return fmt.Errorf("state-dir is required") + } + if opts.ShellDir == "" { + return fmt.Errorf("shell-dir is required") + } + if opts.ConfigDir == "" { + return fmt.Errorf("config-dir is required") + } + if opts.Kind == "" { + return fmt.Errorf("kind is required") + } + if opts.Value == "" { + return fmt.Errorf("value is required") + } + if opts.Mode == "" { + opts.Mode = ColorModeDark + } + if opts.MatugenType == "" { + opts.MatugenType = "scheme-tonal-spot" + } + if opts.IconTheme == "" { + opts.IconTheme = "System Default" + } + if opts.AppChecker == nil { + opts.AppChecker = utils.DefaultAppChecker{} + } + + if err := os.MkdirAll(opts.StateDir, 0o755); err != nil { + return fmt.Errorf("failed to create state dir: %w", err) + } + + log.Infof("Building theme: %s %s (%s)", opts.Kind, opts.Value, opts.Mode) + + changed, buildErr := buildOnce(&opts) + if buildErr != nil { + return buildErr + } + + if !changed { + log.Info("No color changes detected, skipping refresh") + return ErrNoChanges + } + + if opts.SyncModeWithPortal { + syncColorScheme(opts.Mode) + } + + log.Info("Done") + return nil +} + +func buildOnce(opts *Options) (bool, error) { + cfgFile, err := os.CreateTemp("", "matugen-config-*.toml") + if err != nil { + return false, fmt.Errorf("failed to create temp config: %w", err) + } + defer os.Remove(cfgFile.Name()) + defer cfgFile.Close() + + tmpDir, err := os.MkdirTemp("", "matugen-templates-*") + if err != nil { + return false, fmt.Errorf("failed to create temp dir: %w", err) + } + defer os.RemoveAll(tmpDir) + + if err := buildMergedConfig(opts, cfgFile, tmpDir); err != nil { + return false, fmt.Errorf("failed to build config: %w", err) + } + cfgFile.Close() + + oldColors, _ := os.ReadFile(opts.ColorsOutput()) + + var primaryDark, primaryLight, surface string + var dank16JSON string + var importArgs []string + + if opts.StockColors != "" { + log.Info("Using stock/custom theme colors with matugen base") + primaryDark = extractNestedColor(opts.StockColors, "primary", "dark") + primaryLight = extractNestedColor(opts.StockColors, "primary", "light") + surface = extractNestedColor(opts.StockColors, "surface", "dark") + + if primaryDark == "" { + return false, fmt.Errorf("failed to extract primary dark from stock colors") + } + if primaryLight == "" { + primaryLight = primaryDark + } + + dank16JSON = generateDank16Variants(primaryDark, primaryLight, surface, opts.Mode) + importData := fmt.Sprintf(`{"colors": %s, "dank16": %s}`, opts.StockColors, dank16JSON) + importArgs = []string{"--import-json-string", importData} + + log.Info("Running matugen color hex with stock color overrides") + args := []string{"color", "hex", primaryDark, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name()} + args = appendContrastArg(args, opts.Contrast) + args = append(args, importArgs...) + if err := runMatugen(args); err != nil { + return false, err + } + } else { + log.Infof("Using dynamic theme from %s: %s", opts.Kind, opts.Value) + + matJSON, err := runMatugenDryRun(opts) + if err != nil { + return false, fmt.Errorf("matugen dry-run failed: %w", err) + } + + primaryDark = extractMatugenColor(matJSON, "primary", "dark") + primaryLight = extractMatugenColor(matJSON, "primary", "light") + surface = extractMatugenColor(matJSON, "surface", "dark") + + if primaryDark == "" { + return false, fmt.Errorf("failed to extract primary color") + } + if primaryLight == "" { + primaryLight = primaryDark + } + + dank16JSON = generateDank16Variants(primaryDark, primaryLight, surface, opts.Mode) + importData := fmt.Sprintf(`{"dank16": %s}`, dank16JSON) + importArgs = []string{"--import-json-string", importData} + + log.Infof("Running matugen %s with dank16 injection", opts.Kind) + var args []string + switch opts.Kind { + case "hex": + args = []string{"color", "hex", opts.Value} + default: + args = []string{opts.Kind, opts.Value} + } + args = append(args, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name()) + args = appendContrastArg(args, opts.Contrast) + args = append(args, importArgs...) + if err := runMatugen(args); err != nil { + return false, err + } + } + + newColors, _ := os.ReadFile(opts.ColorsOutput()) + if bytes.Equal(oldColors, newColors) && len(oldColors) > 0 { + return false, nil + } + + if opts.ColorsOnly { + return true, nil + } + + if isDMSGTKActive(opts.ConfigDir) { + switch opts.Mode { + case ColorModeLight: + syncAccentColor(primaryLight) + default: + syncAccentColor(primaryDark) + } + refreshGTK(opts.Mode) + refreshGTK4() + } + + if !opts.ShouldSkipTemplate("qt6ct") && appExists(opts.AppChecker, []string{"qt6ct"}, nil) { + refreshQt6ct() + } + + signalTerminals(opts) + + return true, nil +} + +func appendContrastArg(args []string, contrast float64) []string { + if contrast == 0 { + return args + } + return append(args, "--contrast", strconv.FormatFloat(contrast, 'f', -1, 64)) +} + +func buildMergedConfig(opts *Options, cfgFile *os.File, tmpDir string) error { + userConfigPath := filepath.Join(opts.ConfigDir, "matugen", "config.toml") + + wroteConfig := false + if opts.RunUserTemplates { + if data, err := os.ReadFile(userConfigPath); err == nil { + configSection := extractTOMLSection(string(data), "[config]", "[templates]") + if configSection != "" { + cfgFile.WriteString(configSection) + cfgFile.WriteString("\n") + wroteConfig = true + } + } + } + if !wroteConfig { + cfgFile.WriteString("[config]\n\n") + } + + baseConfigPath := filepath.Join(opts.ShellDir, "matugen", "configs", "base.toml") + if data, err := os.ReadFile(baseConfigPath); err == nil { + content := string(data) + lines := strings.Split(content, "\n") + for _, line := range lines { + if strings.TrimSpace(line) == "[config]" { + continue + } + cfgFile.WriteString(substituteVars(line, opts.ShellDir) + "\n") + } + cfgFile.WriteString("\n") + } + + fmt.Fprintf(cfgFile, `[templates.dank] +input_path = '%s/matugen/templates/dank.json' +output_path = '%s' + +`, opts.ShellDir, opts.ColorsOutput()) + + if opts.ColorsOnly { + return nil + } + + homeDir, _ := os.UserHomeDir() + for _, tmpl := range templateRegistry { + if opts.ShouldSkipTemplate(tmpl.ID) { + continue + } + + switch tmpl.Kind { + case TemplateKindGTK: + switch opts.Mode { + case ColorModeLight: + appendConfig(opts, cfgFile, nil, nil, "gtk3-light.toml") + default: + appendConfig(opts, cfgFile, nil, nil, "gtk3-dark.toml") + } + case TemplateKindTerminal: + appendTerminalConfig(opts, cfgFile, tmpDir, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile) + case TemplateKindVSCode: + appendVSCodeConfig(cfgFile, "vscode", filepath.Join(homeDir, ".vscode/extensions"), opts.ShellDir) + appendVSCodeConfig(cfgFile, "codium", filepath.Join(homeDir, ".vscode-oss/extensions"), opts.ShellDir) + appendVSCodeConfig(cfgFile, "codeoss", filepath.Join(homeDir, ".config/Code - OSS/extensions"), opts.ShellDir) + appendVSCodeConfig(cfgFile, "cursor", filepath.Join(homeDir, ".cursor/extensions"), opts.ShellDir) + appendVSCodeConfig(cfgFile, "windsurf", filepath.Join(homeDir, ".windsurf/extensions"), opts.ShellDir) + appendVSCodeConfig(cfgFile, "vscode-insiders", filepath.Join(homeDir, ".vscode-insiders/extensions"), opts.ShellDir) + case TemplateKindEmacs: + if utils.EmacsConfigDir() != "" { + appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile) + } + default: + appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile) + } + } + + if opts.RunUserTemplates { + if data, err := os.ReadFile(userConfigPath); err == nil { + templatesSection := extractTOMLSection(string(data), "[templates]", "") + if templatesSection != "" { + cfgFile.WriteString(templatesSection) + cfgFile.WriteString("\n") + } + } + } + + userPluginConfigDir := filepath.Join(opts.ConfigDir, "matugen", "dms", "configs") + if entries, err := os.ReadDir(userPluginConfigDir); err == nil { + for _, entry := range entries { + if !strings.HasSuffix(entry.Name(), ".toml") { + continue + } + if data, err := os.ReadFile(filepath.Join(userPluginConfigDir, entry.Name())); err == nil { + cfgFile.WriteString(string(data)) + cfgFile.WriteString("\n") + } + } + } + + return nil +} + +func appendConfig( + opts *Options, + cfgFile *os.File, + checkCmd []string, + checkFlatpaks []string, + fileName string, +) { + configPath := filepath.Join(opts.ShellDir, "matugen", "configs", fileName) + if _, err := os.Stat(configPath); err != nil { + return + } + if !appExists(opts.AppChecker, checkCmd, checkFlatpaks) { + return + } + data, err := os.ReadFile(configPath) + if err != nil { + return + } + cfgFile.WriteString(substituteVars(string(data), opts.ShellDir)) + cfgFile.WriteString("\n") +} + +func appendTerminalConfig(opts *Options, cfgFile *os.File, tmpDir string, checkCmd []string, checkFlatpaks []string, fileName string) { + configPath := filepath.Join(opts.ShellDir, "matugen", "configs", fileName) + if _, err := os.Stat(configPath); err != nil { + return + } + if !appExists(opts.AppChecker, checkCmd, checkFlatpaks) { + return + } + data, err := os.ReadFile(configPath) + if err != nil { + return + } + + content := string(data) + + if !opts.TerminalsAlwaysDark { + cfgFile.WriteString(substituteVars(content, opts.ShellDir)) + cfgFile.WriteString("\n") + return + } + + lines := strings.Split(content, "\n") + for _, line := range lines { + if !strings.Contains(line, "input_path") || !strings.Contains(line, "SHELL_DIR/matugen/templates/") { + continue + } + + start := strings.Index(line, "'SHELL_DIR/matugen/templates/") + if start == -1 { + continue + } + end := strings.Index(line[start+1:], "'") + if end == -1 { + continue + } + templateName := line[start+len("'SHELL_DIR/matugen/templates/") : start+1+end] + origPath := filepath.Join(opts.ShellDir, "matugen", "templates", templateName) + + origData, err := os.ReadFile(origPath) + if err != nil { + continue + } + + modified := strings.ReplaceAll(string(origData), ".default.", ".dark.") + tmpPath := filepath.Join(tmpDir, templateName) + if err := os.WriteFile(tmpPath, []byte(modified), 0o644); err != nil { + continue + } + + content = strings.ReplaceAll(content, + fmt.Sprintf("'SHELL_DIR/matugen/templates/%s'", templateName), + fmt.Sprintf("'%s'", tmpPath)) + } + + cfgFile.WriteString(substituteVars(content, opts.ShellDir)) + cfgFile.WriteString("\n") +} + +func appExists(checker utils.AppChecker, checkCmd []string, checkFlatpaks []string) bool { + // Both nil is treated as "skip check" / unconditionally run + if checkCmd == nil && checkFlatpaks == nil { + return true + } + if checkCmd != nil && checker.AnyCommandExists(checkCmd...) { + return true + } + if checkFlatpaks != nil && checker.AnyFlatpakExists(checkFlatpaks...) { + return true + } + return false +} + +func appendVSCodeConfig(cfgFile *os.File, name, extBaseDir, shellDir string) { + pattern := filepath.Join(extBaseDir, "danklinux.dms-theme-*") + matches, err := filepath.Glob(pattern) + if err != nil || len(matches) == 0 { + return + } + + extDir := matches[0] + templateDir := filepath.Join(shellDir, "matugen", "templates") + fmt.Fprintf(cfgFile, `[templates.dms%sdefault] +input_path = '%s/vscode-color-theme-default.json' +output_path = '%s/themes/dankshell-default.json' + +[templates.dms%sdark] +input_path = '%s/vscode-color-theme-dark.json' +output_path = '%s/themes/dankshell-dark.json' + +[templates.dms%slight] +input_path = '%s/vscode-color-theme-light.json' +output_path = '%s/themes/dankshell-light.json' + +`, name, templateDir, extDir, + name, templateDir, extDir, + name, templateDir, extDir) + log.Infof("Added %s theme config (extension found at %s)", name, extDir) +} + +func substituteVars(content, shellDir string) string { + result := strings.ReplaceAll(content, "'SHELL_DIR/", "'"+shellDir+"/") + result = strings.ReplaceAll(result, "'CONFIG_DIR/", "'"+utils.XDGConfigHome()+"/") + result = strings.ReplaceAll(result, "'DATA_DIR/", "'"+utils.XDGDataHome()+"/") + result = strings.ReplaceAll(result, "'CACHE_DIR/", "'"+utils.XDGCacheHome()+"/") + if emacsDir := utils.EmacsConfigDir(); emacsDir != "" { + result = strings.ReplaceAll(result, "'EMACS_DIR/", "'"+emacsDir+"/") + } + return result +} + +func extractTOMLSection(content, startMarker, endMarker string) string { + startIdx := strings.Index(content, startMarker) + if startIdx == -1 { + return "" + } + + if endMarker == "" { + return content[startIdx:] + } + + endIdx := strings.Index(content[startIdx:], endMarker) + if endIdx == -1 { + return content[startIdx:] + } + return content[startIdx : startIdx+endIdx] +} + +type matugenFlags struct { + supportsCOE bool + isV4 bool +} + +func detectMatugenVersion() (matugenFlags, error) { + matugenVersionMu.Lock() + defer matugenVersionMu.Unlock() + + if matugenVersionOK { + return matugenFlags{matugenSupportsCOE, matugenIsV4}, nil + } + + return detectMatugenVersionLocked() +} + +func redetectMatugenVersion(old matugenFlags) (matugenFlags, bool) { + matugenVersionMu.Lock() + defer matugenVersionMu.Unlock() + + matugenVersionOK = false + flags, err := detectMatugenVersionLocked() + if err != nil { + return old, false + } + changed := flags.supportsCOE != old.supportsCOE || flags.isV4 != old.isV4 + return flags, changed +} + +func detectMatugenVersionLocked() (matugenFlags, error) { + cmd := exec.Command("matugen", "--version") + output, err := cmd.Output() + if err != nil { + return matugenFlags{}, fmt.Errorf("failed to get matugen version: %w", err) + } + + versionStr := strings.TrimSpace(string(output)) + versionStr = strings.TrimPrefix(versionStr, "matugen ") + + parts := strings.Split(versionStr, ".") + if len(parts) < 2 { + return matugenFlags{}, fmt.Errorf("unexpected matugen version format: %q", versionStr) + } + + major, err := strconv.Atoi(parts[0]) + if err != nil { + return matugenFlags{}, fmt.Errorf("failed to parse matugen major version %q: %w", parts[0], err) + } + + minor, err := strconv.Atoi(parts[1]) + if err != nil { + return matugenFlags{}, fmt.Errorf("failed to parse matugen minor version %q: %w", parts[1], err) + } + + matugenSupportsCOE = major > 3 || (major == 3 && minor >= 1) + matugenIsV4 = major >= 4 + matugenVersionOK = true + + if matugenSupportsCOE { + log.Debugf("Matugen %s detected: continue-on-error support enabled", versionStr) + } + if matugenIsV4 { + log.Debugf("Matugen %s detected: using v4 compatibility flags", versionStr) + } + return matugenFlags{matugenSupportsCOE, matugenIsV4}, nil +} + +func buildMatugenArgs(baseArgs []string, flags matugenFlags) []string { + args := make([]string, 0, len(baseArgs)+4) + if flags.supportsCOE { + args = append(args, "--continue-on-error") + } + args = append(args, baseArgs...) + if flags.isV4 { + args = append(args, "--source-color-index", "0") + } + return args +} + +func runMatugen(baseArgs []string) error { + flags, err := detectMatugenVersion() + if err != nil { + return err + } + + args := buildMatugenArgs(baseArgs, flags) + cmd := exec.Command("matugen", args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + runErr := cmd.Run() + if runErr == nil { + return nil + } + + log.Warnf("Matugen failed (v4=%v): %v", flags.isV4, runErr) + + newFlags, changed := redetectMatugenVersion(flags) + if !changed { + return runErr + } + + log.Warnf("Matugen version changed (v4: %v -> %v), retrying", flags.isV4, newFlags.isV4) + args = buildMatugenArgs(baseArgs, newFlags) + retryCmd := exec.Command("matugen", args...) + retryCmd.Stdout = os.Stdout + retryCmd.Stderr = os.Stderr + return retryCmd.Run() +} + +func runMatugenDryRun(opts *Options) (string, error) { + flags, err := detectMatugenVersion() + if err != nil { + return "", err + } + + output, dryErr := execDryRun(opts, flags) + if dryErr == nil { + return output, nil + } + + log.Warnf("Matugen dry-run failed (v4=%v): %v", flags.isV4, dryErr) + + newFlags, changed := redetectMatugenVersion(flags) + if !changed { + return "", dryErr + } + + log.Warnf("Matugen version changed (v4: %v -> %v), retrying dry-run", flags.isV4, newFlags.isV4) + return execDryRun(opts, newFlags) +} + +func execDryRun(opts *Options, flags matugenFlags) (string, error) { + var baseArgs []string + switch opts.Kind { + case "hex": + baseArgs = []string{"color", "hex", opts.Value} + default: + baseArgs = []string{opts.Kind, opts.Value} + } + baseArgs = append(baseArgs, "-m", "dark", "-t", opts.MatugenType, "--json", "hex", "--dry-run") + baseArgs = appendContrastArg(baseArgs, opts.Contrast) + if flags.isV4 { + baseArgs = append(baseArgs, "--source-color-index", "0", "--old-json-output") + } + + cmd := exec.Command("matugen", baseArgs...) + var stderr strings.Builder + cmd.Stderr = &stderr + output, err := cmd.Output() + if err != nil { + if stderr.Len() > 0 { + return "", fmt.Errorf("matugen %v failed (v4=%v): %s", baseArgs, flags.isV4, strings.TrimSpace(stderr.String())) + } + return "", fmt.Errorf("matugen %v failed (v4=%v): %w", baseArgs, flags.isV4, err) + } + return strings.ReplaceAll(string(output), "\n", ""), nil +} + +func extractMatugenColor(jsonStr, colorName, variant string) string { + var data map[string]any + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return "" + } + + colors, ok := data["colors"].(map[string]any) + if !ok { + return "" + } + + colorData, ok := colors[colorName].(map[string]any) + if !ok { + return "" + } + + variantData, ok := colorData[variant].(string) + if !ok { + return "" + } + + return variantData +} + +func extractNestedColor(jsonStr, colorName, variant string) string { + var data map[string]any + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return "" + } + + colorData, ok := data[colorName].(map[string]any) + if !ok { + return "" + } + + variantData, ok := colorData[variant].(map[string]any) + if !ok { + return "" + } + + color, ok := variantData["color"].(string) + if !ok { + return "" + } + + return color +} + +func generateDank16Variants(primaryDark, primaryLight, surface string, mode ColorMode) string { + variantOpts := dank16.VariantOptions{ + PrimaryDark: primaryDark, + PrimaryLight: primaryLight, + Background: surface, + UseDPS: true, + IsLightMode: mode == ColorModeLight, + } + variantColors := dank16.GenerateVariantPalette(variantOpts) + return dank16.GenerateVariantJSON(variantColors) +} + +func isDMSGTKActive(configDir string) bool { + gtkCSS := filepath.Join(configDir, "gtk-3.0", "gtk.css") + + info, err := os.Lstat(gtkCSS) + if err != nil { + return false + } + + if info.Mode()&os.ModeSymlink != 0 { + target, err := os.Readlink(gtkCSS) + return err == nil && strings.Contains(target, "dank-colors.css") + } + + data, err := os.ReadFile(gtkCSS) + return err == nil && strings.Contains(string(data), "dank-colors.css") +} + +func refreshGTK(mode ColorMode) { + if err := utils.GsettingsSet("org.gnome.desktop.interface", "gtk-theme", ""); err != nil { + log.Warnf("Failed to reset gtk-theme: %v", err) + } + if err := utils.GsettingsSet("org.gnome.desktop.interface", "gtk-theme", mode.GTKTheme()); err != nil { + log.Warnf("Failed to set gtk-theme: %v", err) + } +} + +func refreshGTK4() { + output, err := utils.GsettingsGet("org.gnome.desktop.interface", "color-scheme") + if err != nil { + return + } + current := strings.Trim(output, "'") + + var toggle string + if current == "prefer-dark" { + toggle = "default" + } else { + toggle = "prefer-dark" + } + + if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", toggle); err != nil { + log.Warnf("Failed to toggle color-scheme for GTK4 refresh: %v", err) + return + } + time.Sleep(50 * time.Millisecond) + if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", current); err != nil { + log.Warnf("Failed to restore color-scheme for GTK4 refresh: %v", err) + } +} + +func refreshQt6ct() { + confPath := filepath.Join(utils.XDGConfigHome(), "qt6ct", "qt6ct.conf") + now := time.Now() + if err := os.Chtimes(confPath, now, now); err != nil { + log.Warnf("Failed to touch qt6ct.conf: %v", err) + } +} + +func signalTerminals(opts *Options) { + if !opts.ShouldSkipTemplate("kitty") && appExists(opts.AppChecker, []string{"kitty"}, nil) { + signalByName("kitty", syscall.SIGUSR1) + signalByName(".kitty-wrapped", syscall.SIGUSR1) + } + if !opts.ShouldSkipTemplate("ghostty") && appExists(opts.AppChecker, []string{"ghostty"}, nil) { + signalByName("ghostty", syscall.SIGUSR2) + signalByName(".ghostty-wrappe", syscall.SIGUSR2) + } +} + +func signalByName(name string, sig syscall.Signal) { + entries, err := os.ReadDir("/proc") + if err != nil { + return + } + for _, entry := range entries { + pid, err := strconv.Atoi(entry.Name()) + if err != nil { + continue + } + comm, err := os.ReadFile(filepath.Join("/proc", entry.Name(), "comm")) + if err != nil { + continue + } + if strings.TrimSpace(string(comm)) == name { + syscall.Kill(pid, sig) + } + } +} + +func syncColorScheme(mode ColorMode) { + scheme := "prefer-dark" + if mode == ColorModeLight { + scheme = "default" + } + + if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", scheme); err != nil { + log.Warnf("Failed to sync color-scheme: %v", err) + } +} + +var adwaitaAccents = []struct { + name string + colors []colorful.Color +}{ + {"blue", hexColors("#3f8ae5", "#438de6", "#a4caee")}, + {"green", hexColors("#26a269", "#39ac76", "#81d5ad")}, + {"orange", hexColors("#f17738", "#ff7800", "#ffc994")}, + {"pink", hexColors("#e4358a", "#e64392", "#f9b3d5")}, + {"purple", hexColors("#954ab5", "#9c46b9", "#d099d6")}, + {"red", hexColors("#e84053", "#e01b24", "#f2a1a5")}, + {"slate", hexColors("#557b9f", "#6a8daf", "#b4c6d6")}, + {"teal", hexColors("#129eb0", "#2190a4", "#7bdff4")}, + {"yellow", hexColors("#cbac10", "#d4b411", "#f5c211")}, +} + +func hexColors(hexes ...string) []colorful.Color { + out := make([]colorful.Color, len(hexes)) + for i, h := range hexes { + out[i], _ = colorful.Hex(h) + } + return out +} + +func closestAdwaitaAccent(primaryHex string) string { + c, err := colorful.Hex(primaryHex) + if err != nil { + return "blue" + } + + best := "blue" + bestDist := math.MaxFloat64 + for _, a := range adwaitaAccents { + for _, ref := range a.colors { + d := c.DistanceCIEDE2000(ref) + if d < bestDist { + bestDist = d + best = a.name + } + } + } + return best +} + +func syncAccentColor(primaryHex string) { + accent := closestAdwaitaAccent(primaryHex) + log.Infof("Setting GNOME accent color: %s", accent) + if err := utils.GsettingsSet("org.gnome.desktop.interface", "accent-color", accent); err != nil { + log.Warnf("Failed to set accent-color: %v", err) + } +} + +type TemplateCheck struct { + ID string `json:"id"` + Detected bool `json:"detected"` +} + +func CheckTemplates(checker utils.AppChecker) []TemplateCheck { + if checker == nil { + checker = utils.DefaultAppChecker{} + } + + homeDir, _ := os.UserHomeDir() + checks := make([]TemplateCheck, 0, len(templateRegistry)) + + for _, tmpl := range templateRegistry { + detected := false + + switch { + case tmpl.RunUnconditionally: + detected = true + case tmpl.Kind == TemplateKindVSCode: + detected = checkVSCodeExtension(homeDir) + case tmpl.Kind == TemplateKindEmacs: + detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks) && utils.EmacsConfigDir() != "" + default: + detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks) + } + + checks = append(checks, TemplateCheck{ID: tmpl.ID, Detected: detected}) + } + + return checks +} + +func checkVSCodeExtension(homeDir string) bool { + extDirs := []string{ + filepath.Join(homeDir, ".vscode/extensions"), + filepath.Join(homeDir, ".vscode-oss/extensions"), + filepath.Join(homeDir, ".config/Code - OSS/extensions"), + filepath.Join(homeDir, ".cursor/extensions"), + filepath.Join(homeDir, ".windsurf/extensions"), + } + + for _, extDir := range extDirs { + pattern := filepath.Join(extDir, "danklinux.dms-theme-*") + if matches, err := filepath.Glob(pattern); err == nil && len(matches) > 0 { + return true + } + } + return false +} |