diff options
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/matugen')
3 files changed, 0 insertions, 1564 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 deleted file mode 100644 index a29d324..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/matugen/matugen.go +++ /dev/null @@ -1,978 +0,0 @@ -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 -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/matugen/matugen_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/matugen/matugen_test.go deleted file mode 100644 index 48880df..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/matugen/matugen_test.go +++ /dev/null @@ -1,443 +0,0 @@ -package matugen - -import ( - "os" - "path/filepath" - "strings" - "testing" - - mocks_utils "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/utils" - "github.com/AvengeMedia/DankMaterialShell/core/internal/utils" - "github.com/stretchr/testify/assert" -) - -func TestAppendConfigBinaryExists(t *testing.T) { - tempDir := t.TempDir() - - shellDir := filepath.Join(tempDir, "shell") - configsDir := filepath.Join(shellDir, "matugen", "configs") - if err := os.MkdirAll(configsDir, 0o755); err != nil { - t.Fatalf("failed to create configs dir: %v", err) - } - - testConfig := "test config content" - configPath := filepath.Join(configsDir, "test.toml") - if err := os.WriteFile(configPath, []byte(testConfig), 0o644); err != nil { - t.Fatalf("failed to write config: %v", err) - } - - outFile := filepath.Join(tempDir, "output.toml") - cfgFile, err := os.Create(outFile) - if err != nil { - t.Fatalf("failed to create output file: %v", err) - } - defer cfgFile.Close() - - mockChecker := mocks_utils.NewMockAppChecker(t) - mockChecker.EXPECT().AnyCommandExists("sh").Return(true) - - opts := &Options{ShellDir: shellDir, AppChecker: mockChecker} - - appendConfig(opts, cfgFile, []string{"sh"}, nil, "test.toml") - - cfgFile.Close() - output, err := os.ReadFile(outFile) - if err != nil { - t.Fatalf("failed to read output: %v", err) - } - - if len(output) == 0 { - t.Errorf("expected config to be written when binary exists") - } - if string(output) != testConfig+"\n" { - t.Errorf("expected %q, got %q", testConfig+"\n", string(output)) - } -} - -func TestAppendConfigBinaryDoesNotExist(t *testing.T) { - tempDir := t.TempDir() - - shellDir := filepath.Join(tempDir, "shell") - configsDir := filepath.Join(shellDir, "matugen", "configs") - if err := os.MkdirAll(configsDir, 0o755); err != nil { - t.Fatalf("failed to create configs dir: %v", err) - } - - testConfig := "test config content" - configPath := filepath.Join(configsDir, "test.toml") - if err := os.WriteFile(configPath, []byte(testConfig), 0o644); err != nil { - t.Fatalf("failed to write config: %v", err) - } - - outFile := filepath.Join(tempDir, "output.toml") - cfgFile, err := os.Create(outFile) - if err != nil { - t.Fatalf("failed to create output file: %v", err) - } - defer cfgFile.Close() - - mockChecker := mocks_utils.NewMockAppChecker(t) - mockChecker.EXPECT().AnyCommandExists("nonexistent-binary-12345").Return(false) - mockChecker.EXPECT().AnyFlatpakExists().Return(false) - - opts := &Options{ShellDir: shellDir, AppChecker: mockChecker} - - appendConfig(opts, cfgFile, []string{"nonexistent-binary-12345"}, []string{}, "test.toml") - - cfgFile.Close() - output, err := os.ReadFile(outFile) - if err != nil { - t.Fatalf("failed to read output: %v", err) - } - - if len(output) != 0 { - t.Errorf("expected no config when binary doesn't exist, got: %q", string(output)) - } -} - -func TestAppendConfigFlatpakExists(t *testing.T) { - tempDir := t.TempDir() - - shellDir := filepath.Join(tempDir, "shell") - configsDir := filepath.Join(shellDir, "matugen", "configs") - if err := os.MkdirAll(configsDir, 0o755); err != nil { - t.Fatalf("failed to create configs dir: %v", err) - } - - testConfig := "zen config content" - configPath := filepath.Join(configsDir, "test.toml") - if err := os.WriteFile(configPath, []byte(testConfig), 0o644); err != nil { - t.Fatalf("failed to write config: %v", err) - } - - outFile := filepath.Join(tempDir, "output.toml") - cfgFile, err := os.Create(outFile) - if err != nil { - t.Fatalf("failed to create output file: %v", err) - } - defer cfgFile.Close() - - mockChecker := mocks_utils.NewMockAppChecker(t) - mockChecker.EXPECT().AnyFlatpakExists("app.zen_browser.zen").Return(true) - - opts := &Options{ShellDir: shellDir, AppChecker: mockChecker} - - appendConfig(opts, cfgFile, nil, []string{"app.zen_browser.zen"}, "test.toml") - - cfgFile.Close() - output, err := os.ReadFile(outFile) - if err != nil { - t.Fatalf("failed to read output: %v", err) - } - - if len(output) == 0 { - t.Errorf("expected config to be written when flatpak exists") - } -} - -func TestAppendConfigFlatpakDoesNotExist(t *testing.T) { - tempDir := t.TempDir() - - shellDir := filepath.Join(tempDir, "shell") - configsDir := filepath.Join(shellDir, "matugen", "configs") - if err := os.MkdirAll(configsDir, 0o755); err != nil { - t.Fatalf("failed to create configs dir: %v", err) - } - - testConfig := "test config content" - configPath := filepath.Join(configsDir, "test.toml") - if err := os.WriteFile(configPath, []byte(testConfig), 0o644); err != nil { - t.Fatalf("failed to write config: %v", err) - } - - outFile := filepath.Join(tempDir, "output.toml") - cfgFile, err := os.Create(outFile) - if err != nil { - t.Fatalf("failed to create output file: %v", err) - } - defer cfgFile.Close() - - mockChecker := mocks_utils.NewMockAppChecker(t) - mockChecker.EXPECT().AnyCommandExists().Return(false) - mockChecker.EXPECT().AnyFlatpakExists("com.nonexistent.flatpak").Return(false) - - opts := &Options{ShellDir: shellDir, AppChecker: mockChecker} - - appendConfig(opts, cfgFile, []string{}, []string{"com.nonexistent.flatpak"}, "test.toml") - - cfgFile.Close() - output, err := os.ReadFile(outFile) - if err != nil { - t.Fatalf("failed to read output: %v", err) - } - - if len(output) != 0 { - t.Errorf("expected no config when flatpak doesn't exist, got: %q", string(output)) - } -} - -func TestAppendConfigBothExist(t *testing.T) { - tempDir := t.TempDir() - - shellDir := filepath.Join(tempDir, "shell") - configsDir := filepath.Join(shellDir, "matugen", "configs") - if err := os.MkdirAll(configsDir, 0o755); err != nil { - t.Fatalf("failed to create configs dir: %v", err) - } - - testConfig := "zen config content" - configPath := filepath.Join(configsDir, "test.toml") - if err := os.WriteFile(configPath, []byte(testConfig), 0o644); err != nil { - t.Fatalf("failed to write config: %v", err) - } - - outFile := filepath.Join(tempDir, "output.toml") - cfgFile, err := os.Create(outFile) - if err != nil { - t.Fatalf("failed to create output file: %v", err) - } - defer cfgFile.Close() - - mockChecker := mocks_utils.NewMockAppChecker(t) - mockChecker.EXPECT().AnyCommandExists("sh").Return(true) - - opts := &Options{ShellDir: shellDir, AppChecker: mockChecker} - - appendConfig(opts, cfgFile, []string{"sh"}, []string{"app.zen_browser.zen"}, "test.toml") - - cfgFile.Close() - output, err := os.ReadFile(outFile) - if err != nil { - t.Fatalf("failed to read output: %v", err) - } - - if len(output) == 0 { - t.Errorf("expected config to be written when both binary and flatpak exist") - } -} - -func TestAppendConfigNeitherExists(t *testing.T) { - tempDir := t.TempDir() - - shellDir := filepath.Join(tempDir, "shell") - configsDir := filepath.Join(shellDir, "matugen", "configs") - if err := os.MkdirAll(configsDir, 0o755); err != nil { - t.Fatalf("failed to create configs dir: %v", err) - } - - testConfig := "test config content" - configPath := filepath.Join(configsDir, "test.toml") - if err := os.WriteFile(configPath, []byte(testConfig), 0o644); err != nil { - t.Fatalf("failed to write config: %v", err) - } - - outFile := filepath.Join(tempDir, "output.toml") - cfgFile, err := os.Create(outFile) - if err != nil { - t.Fatalf("failed to create output file: %v", err) - } - defer cfgFile.Close() - - mockChecker := mocks_utils.NewMockAppChecker(t) - mockChecker.EXPECT().AnyCommandExists("nonexistent-binary-12345").Return(false) - mockChecker.EXPECT().AnyFlatpakExists("com.nonexistent.flatpak").Return(false) - - opts := &Options{ShellDir: shellDir, AppChecker: mockChecker} - - appendConfig(opts, cfgFile, []string{"nonexistent-binary-12345"}, []string{"com.nonexistent.flatpak"}, "test.toml") - - cfgFile.Close() - output, err := os.ReadFile(outFile) - if err != nil { - t.Fatalf("failed to read output: %v", err) - } - - if len(output) != 0 { - t.Errorf("expected no config when neither exists, got: %q", string(output)) - } -} - -func TestAppendConfigNoChecks(t *testing.T) { - tempDir := t.TempDir() - - shellDir := filepath.Join(tempDir, "shell") - configsDir := filepath.Join(shellDir, "matugen", "configs") - if err := os.MkdirAll(configsDir, 0o755); err != nil { - t.Fatalf("failed to create configs dir: %v", err) - } - - testConfig := "always include" - configPath := filepath.Join(configsDir, "test.toml") - if err := os.WriteFile(configPath, []byte(testConfig), 0o644); err != nil { - t.Fatalf("failed to write config: %v", err) - } - - outFile := filepath.Join(tempDir, "output.toml") - cfgFile, err := os.Create(outFile) - if err != nil { - t.Fatalf("failed to create output file: %v", err) - } - defer cfgFile.Close() - - opts := &Options{ShellDir: shellDir} - - appendConfig(opts, cfgFile, nil, nil, "test.toml") - - cfgFile.Close() - output, err := os.ReadFile(outFile) - if err != nil { - t.Fatalf("failed to read output: %v", err) - } - - if len(output) == 0 { - t.Errorf("expected config to be written when no checks specified") - } -} - -func TestAppendConfigFileDoesNotExist(t *testing.T) { - tempDir := t.TempDir() - - shellDir := filepath.Join(tempDir, "shell") - configsDir := filepath.Join(shellDir, "matugen", "configs") - if err := os.MkdirAll(configsDir, 0o755); err != nil { - t.Fatalf("failed to create configs dir: %v", err) - } - - outFile := filepath.Join(tempDir, "output.toml") - cfgFile, err := os.Create(outFile) - if err != nil { - t.Fatalf("failed to create output file: %v", err) - } - defer cfgFile.Close() - - opts := &Options{ShellDir: shellDir} - - appendConfig(opts, cfgFile, nil, nil, "nonexistent.toml") - - cfgFile.Close() - output, err := os.ReadFile(outFile) - if err != nil { - t.Fatalf("failed to read output: %v", err) - } - - if len(output) != 0 { - t.Errorf("expected no config when file doesn't exist, got: %q", string(output)) - } -} - -func TestSubstituteVars(t *testing.T) { - configDir := utils.XDGConfigHome() - dataDir := utils.XDGDataHome() - cacheDir := utils.XDGCacheHome() - - tests := []struct { - name string - input string - shellDir string - expected string - }{ - { - name: "substitutes SHELL_DIR", - input: "input_path = 'SHELL_DIR/matugen/templates/foo.conf'", - shellDir: "/home/user/shell", - expected: "input_path = '/home/user/shell/matugen/templates/foo.conf'", - }, - { - name: "substitutes CONFIG_DIR", - input: "output_path = 'CONFIG_DIR/kitty/theme.conf'", - shellDir: "/home/user/shell", - expected: "output_path = '" + configDir + "/kitty/theme.conf'", - }, - { - name: "substitutes DATA_DIR", - input: "output_path = 'DATA_DIR/color-schemes/theme.colors'", - shellDir: "/home/user/shell", - expected: "output_path = '" + dataDir + "/color-schemes/theme.colors'", - }, - { - name: "substitutes CACHE_DIR", - input: "output_path = 'CACHE_DIR/wal/colors.json'", - shellDir: "/home/user/shell", - expected: "output_path = '" + cacheDir + "/wal/colors.json'", - }, - { - name: "substitutes all dir types", - input: "'SHELL_DIR/a' 'CONFIG_DIR/b' 'DATA_DIR/c' 'CACHE_DIR/d'", - shellDir: "/shell", - expected: "'/shell/a' '" + configDir + "/b' '" + dataDir + "/c' '" + cacheDir + "/d'", - }, - { - name: "no substitution when no placeholders", - input: "input_path = '/absolute/path/foo.conf'", - shellDir: "/home/user/shell", - expected: "input_path = '/absolute/path/foo.conf'", - }, - { - name: "multiple SHELL_DIR occurrences", - input: "'SHELL_DIR/a' and 'SHELL_DIR/b'", - shellDir: "/shell", - expected: "'/shell/a' and '/shell/b'", - }, - { - name: "only substitutes quoted paths", - input: "SHELL_DIR/unquoted and 'SHELL_DIR/quoted'", - shellDir: "/shell", - expected: "SHELL_DIR/unquoted and '/shell/quoted'", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - result := substituteVars(tc.input, tc.shellDir) - assert.Equal(t, tc.expected, result) - }) - } -} - -func TestBuildMergedConfigColorsOnly(t *testing.T) { - tempDir := t.TempDir() - - shellDir := filepath.Join(tempDir, "shell") - configsDir := filepath.Join(shellDir, "matugen", "configs") - if err := os.MkdirAll(configsDir, 0o755); err != nil { - t.Fatalf("failed to create configs dir: %v", err) - } - - baseConfig := "[config]\ncustom_keywords = []\n" - if err := os.WriteFile(filepath.Join(configsDir, "base.toml"), []byte(baseConfig), 0o644); err != nil { - t.Fatalf("failed to write base config: %v", err) - } - - cfgFile, err := os.CreateTemp(tempDir, "merged-*.toml") - if err != nil { - t.Fatalf("failed to create temp config: %v", err) - } - defer os.Remove(cfgFile.Name()) - defer cfgFile.Close() - - opts := &Options{ - ShellDir: shellDir, - ConfigDir: filepath.Join(tempDir, "config"), - StateDir: filepath.Join(tempDir, "state"), - ColorsOnly: true, - } - - if err := buildMergedConfig(opts, cfgFile, filepath.Join(tempDir, "templates")); err != nil { - t.Fatalf("buildMergedConfig failed: %v", err) - } - - if err := cfgFile.Close(); err != nil { - t.Fatalf("failed to close merged config: %v", err) - } - - output, err := os.ReadFile(cfgFile.Name()) - if err != nil { - t.Fatalf("failed to read merged config: %v", err) - } - - content := string(output) - assert.Contains(t, content, "[templates.dank]") - assert.Contains(t, content, "output_path = '"+filepath.Join(opts.StateDir, "dms-colors.json")+"'") - assert.NotContains(t, content, "[templates.gtk]") - assert.False(t, strings.Contains(content, "output_path = 'CONFIG_DIR/"), "colors-only config should not emit app template outputs") -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/matugen/queue.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/matugen/queue.go deleted file mode 100644 index 38cf72a..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/matugen/queue.go +++ /dev/null @@ -1,143 +0,0 @@ -package matugen - -import ( - "context" - "errors" - "sync" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" -) - -type Result struct { - Success bool - Error error -} - -type QueuedJob struct { - Options Options - Done chan Result - Ctx context.Context - Cancel context.CancelFunc -} - -type Queue struct { - mu sync.Mutex - current *QueuedJob - pending *QueuedJob - jobDone chan struct{} -} - -var globalQueue *Queue -var queueOnce sync.Once - -func GetQueue() *Queue { - queueOnce.Do(func() { - globalQueue = &Queue{ - jobDone: make(chan struct{}, 1), - } - }) - return globalQueue -} - -func (q *Queue) Submit(opts Options) <-chan Result { - result := make(chan Result, 1) - ctx, cancel := context.WithCancel(context.Background()) - - job := &QueuedJob{ - Options: opts, - Done: result, - Ctx: ctx, - Cancel: cancel, - } - - q.mu.Lock() - - if q.pending != nil { - log.Info("Cancelling pending theme request") - q.pending.Cancel() - q.pending.Done <- Result{Success: false, Error: context.Canceled} - close(q.pending.Done) - } - - if q.current != nil { - q.pending = job - q.mu.Unlock() - log.Info("Theme request queued (worker running)") - return result - } - - q.current = job - q.mu.Unlock() - - go q.runWorker() - return result -} - -func (q *Queue) runWorker() { - for { - q.mu.Lock() - job := q.current - if job == nil { - q.mu.Unlock() - return - } - q.mu.Unlock() - - select { - case <-job.Ctx.Done(): - q.finishJob(Result{Success: false, Error: context.Canceled}) - continue - default: - } - - log.Infof("Processing theme: %s %s (%s)", job.Options.Kind, job.Options.Value, job.Options.Mode) - err := Run(job.Options) - - var result Result - switch { - case err == nil: - result = Result{Success: true} - case errors.Is(err, ErrNoChanges): - result = Result{Success: true} - default: - result = Result{Success: false, Error: err} - } - - q.finishJob(result) - } -} - -func (q *Queue) finishJob(result Result) { - q.mu.Lock() - defer q.mu.Unlock() - - if q.current != nil { - select { - case q.current.Done <- result: - default: - } - close(q.current.Done) - } - - q.current = q.pending - q.pending = nil - - if q.current == nil { - select { - case q.jobDone <- struct{}{}: - default: - } - } -} - -func (q *Queue) IsRunning() bool { - q.mu.Lock() - defer q.mu.Unlock() - return q.current != nil -} - -func (q *Queue) HasPending() bool { - q.mu.Lock() - defer q.mu.Unlock() - return q.pending != nil -} |