mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-24 21:42:51 -05:00
matugen: scrap shell script for proper backend implementation with queue
system
This commit is contained in:
@@ -21,6 +21,7 @@ linters:
|
||||
# Signal handling
|
||||
- (*os.Process).Signal
|
||||
- (*os.Process).Kill
|
||||
- syscall.Kill
|
||||
# DBus cleanup
|
||||
- (*github.com/godbus/dbus/v5.Conn).RemoveMatchSignal
|
||||
- (*github.com/godbus/dbus/v5.Conn).RemoveSignal
|
||||
|
||||
@@ -454,7 +454,6 @@ func uninstallPluginCLI(idOrName string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// getCommonCommands returns the commands available in all builds
|
||||
func getCommonCommands() []*cobra.Command {
|
||||
return []*cobra.Command{
|
||||
versionCmd,
|
||||
@@ -474,5 +473,6 @@ func getCommonCommands() []*cobra.Command {
|
||||
colorCmd,
|
||||
screenshotCmd,
|
||||
notifyActionCmd,
|
||||
matugenCmd,
|
||||
}
|
||||
}
|
||||
|
||||
182
core/cmd/dms/commands_matugen.go
Normal file
182
core/cmd/dms/commands_matugen.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/matugen"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var matugenCmd = &cobra.Command{
|
||||
Use: "matugen",
|
||||
Short: "Generate Material Design themes",
|
||||
Long: "Generate Material Design themes using matugen with dank16 color integration",
|
||||
}
|
||||
|
||||
var matugenGenerateCmd = &cobra.Command{
|
||||
Use: "generate",
|
||||
Short: "Generate theme synchronously",
|
||||
Run: runMatugenGenerate,
|
||||
}
|
||||
|
||||
var matugenQueueCmd = &cobra.Command{
|
||||
Use: "queue",
|
||||
Short: "Queue theme generation (uses socket if available)",
|
||||
Run: runMatugenQueue,
|
||||
}
|
||||
|
||||
func init() {
|
||||
matugenCmd.AddCommand(matugenGenerateCmd)
|
||||
matugenCmd.AddCommand(matugenQueueCmd)
|
||||
|
||||
for _, cmd := range []*cobra.Command{matugenGenerateCmd, matugenQueueCmd} {
|
||||
cmd.Flags().String("state-dir", "", "State directory for cache files")
|
||||
cmd.Flags().String("shell-dir", "", "DMS shell installation directory")
|
||||
cmd.Flags().String("config-dir", "", "User config directory")
|
||||
cmd.Flags().String("kind", "image", "Source type: image or hex")
|
||||
cmd.Flags().String("value", "", "Wallpaper path or hex color")
|
||||
cmd.Flags().String("mode", "dark", "Color mode: dark or light")
|
||||
cmd.Flags().String("icon-theme", "System Default", "Icon theme name")
|
||||
cmd.Flags().String("matugen-type", "scheme-tonal-spot", "Matugen scheme type")
|
||||
cmd.Flags().Bool("run-user-templates", true, "Run user matugen templates")
|
||||
cmd.Flags().String("stock-colors", "", "Stock theme colors JSON")
|
||||
cmd.Flags().Bool("sync-mode-with-portal", false, "Sync color scheme with GNOME portal")
|
||||
cmd.Flags().Bool("terminals-always-dark", false, "Force terminal themes to dark variant")
|
||||
}
|
||||
|
||||
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
|
||||
matugenQueueCmd.Flags().Duration("timeout", 30*time.Second, "Timeout for waiting")
|
||||
}
|
||||
|
||||
func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
||||
stateDir, _ := cmd.Flags().GetString("state-dir")
|
||||
shellDir, _ := cmd.Flags().GetString("shell-dir")
|
||||
configDir, _ := cmd.Flags().GetString("config-dir")
|
||||
kind, _ := cmd.Flags().GetString("kind")
|
||||
value, _ := cmd.Flags().GetString("value")
|
||||
mode, _ := cmd.Flags().GetString("mode")
|
||||
iconTheme, _ := cmd.Flags().GetString("icon-theme")
|
||||
matugenType, _ := cmd.Flags().GetString("matugen-type")
|
||||
runUserTemplates, _ := cmd.Flags().GetBool("run-user-templates")
|
||||
stockColors, _ := cmd.Flags().GetString("stock-colors")
|
||||
syncModeWithPortal, _ := cmd.Flags().GetBool("sync-mode-with-portal")
|
||||
terminalsAlwaysDark, _ := cmd.Flags().GetBool("terminals-always-dark")
|
||||
|
||||
return matugen.Options{
|
||||
StateDir: stateDir,
|
||||
ShellDir: shellDir,
|
||||
ConfigDir: configDir,
|
||||
Kind: kind,
|
||||
Value: value,
|
||||
Mode: mode,
|
||||
IconTheme: iconTheme,
|
||||
MatugenType: matugenType,
|
||||
RunUserTemplates: runUserTemplates,
|
||||
StockColors: stockColors,
|
||||
SyncModeWithPortal: syncModeWithPortal,
|
||||
TerminalsAlwaysDark: terminalsAlwaysDark,
|
||||
}
|
||||
}
|
||||
|
||||
func runMatugenGenerate(cmd *cobra.Command, args []string) {
|
||||
opts := buildMatugenOptions(cmd)
|
||||
if err := matugen.Run(opts); err != nil {
|
||||
log.Fatalf("Theme generation failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func runMatugenQueue(cmd *cobra.Command, args []string) {
|
||||
opts := buildMatugenOptions(cmd)
|
||||
wait, _ := cmd.Flags().GetBool("wait")
|
||||
timeout, _ := cmd.Flags().GetDuration("timeout")
|
||||
|
||||
socketPath := os.Getenv("DMS_SOCKET")
|
||||
if socketPath == "" {
|
||||
var err error
|
||||
socketPath, err = server.FindSocket()
|
||||
if err != nil {
|
||||
log.Info("No socket available, running synchronously")
|
||||
if err := matugen.Run(opts); err != nil {
|
||||
log.Fatalf("Theme generation failed: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
conn, err := net.Dial("unix", socketPath)
|
||||
if err != nil {
|
||||
log.Info("Socket connection failed, running synchronously")
|
||||
if err := matugen.Run(opts); err != nil {
|
||||
log.Fatalf("Theme generation failed: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
request := map[string]any{
|
||||
"id": 1,
|
||||
"method": "matugen.queue",
|
||||
"params": map[string]any{
|
||||
"stateDir": opts.StateDir,
|
||||
"shellDir": opts.ShellDir,
|
||||
"configDir": opts.ConfigDir,
|
||||
"kind": opts.Kind,
|
||||
"value": opts.Value,
|
||||
"mode": opts.Mode,
|
||||
"iconTheme": opts.IconTheme,
|
||||
"matugenType": opts.MatugenType,
|
||||
"runUserTemplates": opts.RunUserTemplates,
|
||||
"stockColors": opts.StockColors,
|
||||
"syncModeWithPortal": opts.SyncModeWithPortal,
|
||||
"terminalsAlwaysDark": opts.TerminalsAlwaysDark,
|
||||
"wait": wait,
|
||||
},
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(conn).Encode(request); err != nil {
|
||||
log.Fatalf("Failed to send request: %v", err)
|
||||
}
|
||||
|
||||
if !wait {
|
||||
fmt.Println("Theme generation queued")
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
resultCh := make(chan error, 1)
|
||||
go func() {
|
||||
var response struct {
|
||||
ID int `json:"id"`
|
||||
Result any `json:"result"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
if err := json.NewDecoder(conn).Decode(&response); err != nil {
|
||||
resultCh <- fmt.Errorf("failed to read response: %w", err)
|
||||
return
|
||||
}
|
||||
if response.Error != "" {
|
||||
resultCh <- fmt.Errorf("server error: %s", response.Error)
|
||||
return
|
||||
}
|
||||
resultCh <- nil
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-resultCh:
|
||||
if err != nil {
|
||||
log.Fatalf("Theme generation failed: %v", err)
|
||||
}
|
||||
fmt.Println("Theme generation completed")
|
||||
case <-ctx.Done():
|
||||
log.Fatalf("Timeout waiting for theme generation")
|
||||
}
|
||||
}
|
||||
@@ -504,7 +504,7 @@ func parseTargetsFromIPCShowOutput(output string) ipcTargets {
|
||||
return targets
|
||||
}
|
||||
|
||||
func getShellIPCCompletions(args []string, toComplete string) []string {
|
||||
func getShellIPCCompletions(args []string, _ string) []string {
|
||||
cmdArgs := []string{"-p", configPath, "ipc", "show"}
|
||||
cmd := exec.Command("qs", cmdArgs...)
|
||||
var targets ipcTargets
|
||||
|
||||
583
core/internal/matugen/matugen.go
Normal file
583
core/internal/matugen/matugen.go
Normal file
@@ -0,0 +1,583 @@
|
||||
package matugen
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/dank16"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
)
|
||||
|
||||
var (
|
||||
matugenVersionOnce sync.Once
|
||||
matugenSupportsCOE bool
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
StateDir string
|
||||
ShellDir string
|
||||
ConfigDir string
|
||||
Kind string
|
||||
Value string
|
||||
Mode string
|
||||
IconTheme string
|
||||
MatugenType string
|
||||
RunUserTemplates bool
|
||||
StockColors string
|
||||
SyncModeWithPortal bool
|
||||
TerminalsAlwaysDark bool
|
||||
}
|
||||
|
||||
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 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 = "dark"
|
||||
}
|
||||
if opts.MatugenType == "" {
|
||||
opts.MatugenType = "scheme-tonal-spot"
|
||||
}
|
||||
if opts.IconTheme == "" {
|
||||
opts.IconTheme = "System Default"
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(opts.StateDir, 0755); 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)
|
||||
|
||||
if err := buildOnce(&opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if opts.SyncModeWithPortal {
|
||||
syncColorScheme(opts.Mode)
|
||||
}
|
||||
|
||||
log.Info("Done")
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildOnce(opts *Options) error {
|
||||
cfgFile, err := os.CreateTemp("", "matugen-config-*.toml")
|
||||
if err != nil {
|
||||
return 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 fmt.Errorf("failed to create temp dir: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
if err := buildMergedConfig(opts, cfgFile, tmpDir); err != nil {
|
||||
return fmt.Errorf("failed to build config: %w", err)
|
||||
}
|
||||
cfgFile.Close()
|
||||
|
||||
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 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", opts.Mode, "-t", opts.MatugenType, "-c", cfgFile.Name()}
|
||||
args = append(args, importArgs...)
|
||||
if err := runMatugen(args); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Infof("Using dynamic theme from %s: %s", opts.Kind, opts.Value)
|
||||
|
||||
matJSON, err := runMatugenDryRun(opts)
|
||||
if err != nil {
|
||||
return 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 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", opts.Mode, "-t", opts.MatugenType, "-c", cfgFile.Name())
|
||||
args = append(args, importArgs...)
|
||||
if err := runMatugen(args); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
refreshGTK(opts.ConfigDir, opts.Mode)
|
||||
signalTerminals()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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(substituteShellDir(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())
|
||||
|
||||
switch opts.Mode {
|
||||
case "light":
|
||||
appendConfig(opts, cfgFile, "skip", "gtk3-light.toml")
|
||||
default:
|
||||
appendConfig(opts, cfgFile, "skip", "gtk3-dark.toml")
|
||||
}
|
||||
|
||||
appendConfig(opts, cfgFile, "niri", "niri.toml")
|
||||
appendConfig(opts, cfgFile, "qt5ct", "qt5ct.toml")
|
||||
appendConfig(opts, cfgFile, "qt6ct", "qt6ct.toml")
|
||||
appendConfig(opts, cfgFile, "firefox", "firefox.toml")
|
||||
appendConfig(opts, cfgFile, "pywalfox", "pywalfox.toml")
|
||||
appendConfig(opts, cfgFile, "vesktop", "vesktop.toml")
|
||||
|
||||
appendTerminalConfig(opts, cfgFile, tmpDir, "ghostty", "ghostty.toml")
|
||||
appendTerminalConfig(opts, cfgFile, tmpDir, "kitty", "kitty.toml")
|
||||
appendTerminalConfig(opts, cfgFile, tmpDir, "foot", "foot.toml")
|
||||
appendTerminalConfig(opts, cfgFile, tmpDir, "alacritty", "alacritty.toml")
|
||||
appendTerminalConfig(opts, cfgFile, tmpDir, "wezterm", "wezterm.toml")
|
||||
|
||||
appendConfig(opts, cfgFile, "dgop", "dgop.toml")
|
||||
|
||||
homeDir, _ := os.UserHomeDir()
|
||||
appendVSCodeConfig(cfgFile, "vscode", filepath.Join(homeDir, ".vscode/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
||||
appendVSCodeConfig(cfgFile, "codium", filepath.Join(homeDir, ".vscode-oss/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
||||
appendVSCodeConfig(cfgFile, "codeoss", filepath.Join(homeDir, ".config/Code - OSS/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
||||
appendVSCodeConfig(cfgFile, "cursor", filepath.Join(homeDir, ".cursor/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
||||
appendVSCodeConfig(cfgFile, "windsurf", filepath.Join(homeDir, ".windsurf/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
||||
|
||||
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, fileName string) {
|
||||
configPath := filepath.Join(opts.ShellDir, "matugen", "configs", fileName)
|
||||
if _, err := os.Stat(configPath); err != nil {
|
||||
return
|
||||
}
|
||||
if checkCmd != "skip" && !commandExists(checkCmd) {
|
||||
return
|
||||
}
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cfgFile.WriteString(substituteShellDir(string(data), opts.ShellDir))
|
||||
cfgFile.WriteString("\n")
|
||||
}
|
||||
|
||||
func appendTerminalConfig(opts *Options, cfgFile *os.File, tmpDir, checkCmd, fileName string) {
|
||||
configPath := filepath.Join(opts.ShellDir, "matugen", "configs", fileName)
|
||||
if _, err := os.Stat(configPath); err != nil {
|
||||
return
|
||||
}
|
||||
if checkCmd != "skip" && !commandExists(checkCmd) {
|
||||
return
|
||||
}
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
content := string(data)
|
||||
|
||||
if !opts.TerminalsAlwaysDark {
|
||||
cfgFile.WriteString(substituteShellDir(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), 0644); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
content = strings.ReplaceAll(content,
|
||||
fmt.Sprintf("'SHELL_DIR/matugen/templates/%s'", templateName),
|
||||
fmt.Sprintf("'%s'", tmpPath))
|
||||
}
|
||||
|
||||
cfgFile.WriteString(substituteShellDir(content, opts.ShellDir))
|
||||
cfgFile.WriteString("\n")
|
||||
}
|
||||
|
||||
func appendVSCodeConfig(cfgFile *os.File, name, extDir, shellDir string) {
|
||||
if _, err := os.Stat(extDir); err != nil {
|
||||
return
|
||||
}
|
||||
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 substituteShellDir(content, shellDir string) string {
|
||||
return strings.ReplaceAll(content, "'SHELL_DIR/", "'"+shellDir+"/")
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
func commandExists(name string) bool {
|
||||
_, err := exec.LookPath(name)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func checkMatugenVersion() {
|
||||
matugenVersionOnce.Do(func() {
|
||||
cmd := exec.Command("matugen", "--version")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
versionStr := strings.TrimSpace(string(output))
|
||||
versionStr = strings.TrimPrefix(versionStr, "matugen ")
|
||||
|
||||
parts := strings.Split(versionStr, ".")
|
||||
if len(parts) < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
major, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
minor, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
matugenSupportsCOE = major > 3 || (major == 3 && minor >= 1)
|
||||
if matugenSupportsCOE {
|
||||
log.Infof("Matugen %s supports --continue-on-error", versionStr)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func runMatugen(args []string) error {
|
||||
checkMatugenVersion()
|
||||
|
||||
if matugenSupportsCOE {
|
||||
args = append([]string{"--continue-on-error"}, args...)
|
||||
}
|
||||
|
||||
cmd := exec.Command("matugen", args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func runMatugenDryRun(opts *Options) (string, error) {
|
||||
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", "dark", "-t", opts.MatugenType, "--json", "hex", "--dry-run")
|
||||
|
||||
cmd := exec.Command("matugen", args...)
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", 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, mode string) string {
|
||||
variantOpts := dank16.VariantOptions{
|
||||
PrimaryDark: primaryDark,
|
||||
PrimaryLight: primaryLight,
|
||||
Background: surface,
|
||||
UseDPS: true,
|
||||
IsLightMode: mode == "light",
|
||||
}
|
||||
variantColors := dank16.GenerateVariantPalette(variantOpts)
|
||||
return dank16.GenerateVariantJSON(variantColors)
|
||||
}
|
||||
|
||||
func refreshGTK(configDir, mode string) {
|
||||
gtkCSS := filepath.Join(configDir, "gtk-3.0", "gtk.css")
|
||||
|
||||
info, err := os.Lstat(gtkCSS)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
shouldRun := false
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
target, err := os.Readlink(gtkCSS)
|
||||
if err == nil && strings.Contains(target, "dank-colors.css") {
|
||||
shouldRun = true
|
||||
}
|
||||
} else {
|
||||
data, err := os.ReadFile(gtkCSS)
|
||||
if err == nil && strings.Contains(string(data), "dank-colors.css") {
|
||||
shouldRun = true
|
||||
}
|
||||
}
|
||||
|
||||
if !shouldRun {
|
||||
return
|
||||
}
|
||||
|
||||
exec.Command("gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", "").Run()
|
||||
exec.Command("gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", "adw-gtk3-"+mode).Run()
|
||||
}
|
||||
|
||||
func signalTerminals() {
|
||||
signalProcess("kitty", syscall.SIGUSR1)
|
||||
signalProcess("ghostty", syscall.SIGUSR2)
|
||||
signalProcess(".kitty-wrapped", syscall.SIGUSR1)
|
||||
signalProcess(".ghostty-wrappe", syscall.SIGUSR2)
|
||||
}
|
||||
|
||||
func signalProcess(name string, sig syscall.Signal) {
|
||||
cmd := exec.Command("pgrep", "-x", name)
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
pids := strings.Fields(string(output))
|
||||
for _, pidStr := range pids {
|
||||
pid, err := strconv.Atoi(pidStr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
syscall.Kill(pid, sig)
|
||||
}
|
||||
}
|
||||
|
||||
func syncColorScheme(mode string) {
|
||||
scheme := "prefer-dark"
|
||||
if mode == "light" {
|
||||
scheme = "default"
|
||||
}
|
||||
|
||||
if err := exec.Command("gsettings", "set", "org.gnome.desktop.interface", "color-scheme", scheme).Run(); err != nil {
|
||||
exec.Command("dconf", "write", "/org/gnome/desktop/interface/color-scheme", "'"+scheme+"'").Run()
|
||||
}
|
||||
}
|
||||
139
core/internal/matugen/queue.go
Normal file
139
core/internal/matugen/queue.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package matugen
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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
|
||||
if err != nil {
|
||||
result = Result{Success: false, Error: err}
|
||||
} else {
|
||||
result = Result{Success: true}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
91
core/internal/server/matugen_handler.go
Normal file
91
core/internal/server/matugen_handler.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/matugen"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
)
|
||||
|
||||
type MatugenQueueResult struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
func handleMatugenQueue(conn net.Conn, req models.Request) {
|
||||
getString := func(key string) string {
|
||||
if v, ok := req.Params[key].(string); ok {
|
||||
return v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
getBool := func(key string, def bool) bool {
|
||||
if v, ok := req.Params[key].(bool); ok {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
opts := matugen.Options{
|
||||
StateDir: getString("stateDir"),
|
||||
ShellDir: getString("shellDir"),
|
||||
ConfigDir: getString("configDir"),
|
||||
Kind: getString("kind"),
|
||||
Value: getString("value"),
|
||||
Mode: getString("mode"),
|
||||
IconTheme: getString("iconTheme"),
|
||||
MatugenType: getString("matugenType"),
|
||||
RunUserTemplates: getBool("runUserTemplates", true),
|
||||
StockColors: getString("stockColors"),
|
||||
SyncModeWithPortal: getBool("syncModeWithPortal", false),
|
||||
TerminalsAlwaysDark: getBool("terminalsAlwaysDark", false),
|
||||
}
|
||||
|
||||
wait := getBool("wait", true)
|
||||
|
||||
queue := matugen.GetQueue()
|
||||
resultCh := queue.Submit(opts)
|
||||
|
||||
if !wait {
|
||||
models.Respond(conn, req.ID, MatugenQueueResult{
|
||||
Success: true,
|
||||
Message: "queued",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
select {
|
||||
case result := <-resultCh:
|
||||
if result.Error != nil {
|
||||
if result.Error == context.Canceled {
|
||||
models.Respond(conn, req.ID, MatugenQueueResult{
|
||||
Success: false,
|
||||
Message: "cancelled",
|
||||
})
|
||||
return
|
||||
}
|
||||
models.RespondError(conn, req.ID, result.Error.Error())
|
||||
return
|
||||
}
|
||||
models.Respond(conn, req.ID, MatugenQueueResult{
|
||||
Success: true,
|
||||
Message: "completed",
|
||||
})
|
||||
case <-ctx.Done():
|
||||
models.RespondError(conn, req.ID, "timeout waiting for theme generation")
|
||||
}
|
||||
}
|
||||
|
||||
func handleMatugenStatus(conn net.Conn, req models.Request) {
|
||||
queue := matugen.GetQueue()
|
||||
models.Respond(conn, req.ID, map[string]bool{
|
||||
"running": queue.IsRunning(),
|
||||
"pending": queue.HasPending(),
|
||||
})
|
||||
}
|
||||
@@ -215,6 +215,10 @@ func RouteRequest(conn net.Conn, req models.Request) {
|
||||
models.Respond(conn, req.ID, info)
|
||||
case "subscribe":
|
||||
handleSubscribe(conn, req)
|
||||
case "matugen.queue":
|
||||
handleMatugenQueue(conn, req)
|
||||
case "matugen.status":
|
||||
handleMatugenStatus(conn, req)
|
||||
default:
|
||||
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
|
||||
}
|
||||
|
||||
@@ -820,18 +820,35 @@ Singleton {
|
||||
"runUserTemplates": (typeof SettingsData !== "undefined") ? SettingsData.runUserMatugenTemplates : true
|
||||
};
|
||||
|
||||
if (stockColors) {
|
||||
desired.stockColors = JSON.stringify(stockColors);
|
||||
}
|
||||
|
||||
const json = JSON.stringify(desired);
|
||||
const desiredPath = stateDir + "/matugen.desired.json";
|
||||
const syncModeWithPortal = (typeof SettingsData !== "undefined" && SettingsData.syncModeWithPortal) ? "true" : "false";
|
||||
const terminalsAlwaysDark = (typeof SettingsData !== "undefined" && SettingsData.terminalsAlwaysDark) ? "true" : "false";
|
||||
|
||||
console.log("Theme: Starting matugen worker");
|
||||
workerRunning = true;
|
||||
systemThemeGenerator.command = ["sh", "-c", `mkdir -p '${stateDir}' && cat > '${desiredPath}' << 'EOF'\n${json}\nEOF\nexec '${shellDir}/scripts/matugen-worker.sh' '${stateDir}' '${shellDir}' '${configDir}' '${syncModeWithPortal}' '${terminalsAlwaysDark}' --run`];
|
||||
|
||||
const args = [
|
||||
"dms", "matugen", "queue",
|
||||
"--state-dir", stateDir,
|
||||
"--shell-dir", shellDir,
|
||||
"--config-dir", configDir,
|
||||
"--kind", desired.kind,
|
||||
"--value", desired.value,
|
||||
"--mode", desired.mode,
|
||||
"--icon-theme", desired.iconTheme,
|
||||
"--matugen-type", desired.matugenType,
|
||||
];
|
||||
|
||||
if (!desired.runUserTemplates) {
|
||||
args.push("--run-user-templates=false");
|
||||
}
|
||||
if (stockColors) {
|
||||
args.push("--stock-colors", JSON.stringify(stockColors));
|
||||
}
|
||||
if (typeof SettingsData !== "undefined" && SettingsData.syncModeWithPortal) {
|
||||
args.push("--sync-mode-with-portal");
|
||||
}
|
||||
if (typeof SettingsData !== "undefined" && SettingsData.terminalsAlwaysDark) {
|
||||
args.push("--terminals-always-dark");
|
||||
}
|
||||
|
||||
systemThemeGenerator.command = args;
|
||||
systemThemeGenerator.running = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,319 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -uo pipefail
|
||||
|
||||
log() { echo "[matugen-worker] $*" >&2; }
|
||||
err() { echo "[matugen-worker] ERROR: $*" >&2; }
|
||||
|
||||
[[ $# -lt 6 ]] && { echo "Usage: $0 STATE_DIR SHELL_DIR CONFIG_DIR SYNC_MODE_WITH_PORTAL TERMINALS_ALWAYS_DARK --run" >&2; exit 1; }
|
||||
|
||||
STATE_DIR="$1"
|
||||
SHELL_DIR="$2"
|
||||
CONFIG_DIR="$3"
|
||||
SYNC_MODE_WITH_PORTAL="$4"
|
||||
TERMINALS_ALWAYS_DARK="$5"
|
||||
shift 5
|
||||
[[ "${1:-}" != "--run" ]] && { echo "Usage: $0 ... --run" >&2; exit 1; }
|
||||
|
||||
[[ ! -d "$STATE_DIR" ]] && { err "STATE_DIR '$STATE_DIR' does not exist"; exit 1; }
|
||||
[[ ! -d "$SHELL_DIR" ]] && { err "SHELL_DIR '$SHELL_DIR' does not exist"; exit 1; }
|
||||
[[ ! -d "$CONFIG_DIR" ]] && { err "CONFIG_DIR '$CONFIG_DIR' does not exist"; exit 1; }
|
||||
|
||||
DESIRED_JSON="$STATE_DIR/matugen.desired.json"
|
||||
BUILT_KEY="$STATE_DIR/matugen.key"
|
||||
LOCK="$STATE_DIR/matugen-worker.lock"
|
||||
COLORS_OUTPUT="$STATE_DIR/dms-colors.json"
|
||||
|
||||
exec 9>"$LOCK"
|
||||
flock 9
|
||||
rm -f "$BUILT_KEY"
|
||||
|
||||
read_json_field() {
|
||||
local json="$1" field="$2"
|
||||
echo "$json" | sed -n "s/.*\"$field\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" | head -1
|
||||
}
|
||||
|
||||
read_json_escaped_field() {
|
||||
local json="$1" field="$2"
|
||||
local after="${json#*\"$field\":\"}"
|
||||
[[ "$after" == "$json" ]] && return
|
||||
local result=""
|
||||
while [[ -n "$after" ]]; do
|
||||
local char="${after:0:1}"
|
||||
after="${after:1}"
|
||||
[[ "$char" == '"' ]] && break
|
||||
[[ "$char" == '\' ]] && { result+="${after:0:1}"; after="${after:1}"; continue; }
|
||||
result+="$char"
|
||||
done
|
||||
echo "$result"
|
||||
}
|
||||
|
||||
read_json_bool() {
|
||||
local json="$1" field="$2"
|
||||
echo "$json" | sed -n "s/.*\"$field\"[[:space:]]*:[[:space:]]*\([^,}]*\).*/\1/p" | head -1 | tr -d ' '
|
||||
}
|
||||
|
||||
compute_key() {
|
||||
local json="$1"
|
||||
local kind=$(read_json_field "$json" "kind")
|
||||
local value=$(read_json_field "$json" "value")
|
||||
local mode=$(read_json_field "$json" "mode")
|
||||
local icon=$(read_json_field "$json" "iconTheme")
|
||||
local mtype=$(read_json_field "$json" "matugenType")
|
||||
local run_user=$(read_json_bool "$json" "runUserTemplates")
|
||||
local stock_colors=$(read_json_escaped_field "$json" "stockColors")
|
||||
echo "${kind}|${value}|${mode}|${icon:-default}|${mtype:-scheme-tonal-spot}|${run_user:-true}|${stock_colors:-}|${TERMINALS_ALWAYS_DARK:-false}" | sha256sum | cut -d' ' -f1
|
||||
}
|
||||
|
||||
append_config() {
|
||||
local check_cmd="$1" file_name="$2" cfg_file="$3"
|
||||
local target="$SHELL_DIR/matugen/configs/$file_name"
|
||||
[[ ! -f "$target" ]] && return
|
||||
[[ "$check_cmd" != "skip" ]] && ! command -v "$check_cmd" >/dev/null 2>&1 && return
|
||||
sed "s|'SHELL_DIR/|'$SHELL_DIR/|g" "$target" >> "$cfg_file"
|
||||
echo "" >> "$cfg_file"
|
||||
}
|
||||
|
||||
append_terminal_config() {
|
||||
local check_cmd="$1" file_name="$2" cfg_file="$3" tmp_dir="$4"
|
||||
local config_file="$SHELL_DIR/matugen/configs/$file_name"
|
||||
[[ ! -f "$config_file" ]] && return
|
||||
[[ "$check_cmd" != "skip" ]] && ! command -v "$check_cmd" >/dev/null 2>&1 && return
|
||||
|
||||
if [[ "$TERMINALS_ALWAYS_DARK" == "true" ]]; then
|
||||
local config_content
|
||||
config_content=$(cat "$config_file")
|
||||
local templates
|
||||
templates=$(echo "$config_content" | grep "input_path.*SHELL_DIR/matugen/templates/" | sed "s/.*'SHELL_DIR\/matugen\/templates\/\([^']*\)'.*/\1/")
|
||||
for tpl in $templates; do
|
||||
local orig="$SHELL_DIR/matugen/templates/$tpl"
|
||||
[[ ! -f "$orig" ]] && continue
|
||||
local tmp_template="$tmp_dir/$tpl"
|
||||
sed 's/\.default\./\.dark\./g' "$orig" > "$tmp_template"
|
||||
config_content=$(echo "$config_content" | sed "s|'SHELL_DIR/matugen/templates/$tpl'|'$tmp_template'|g")
|
||||
done
|
||||
echo "$config_content" | sed "s|'SHELL_DIR/|'$SHELL_DIR/|g" >> "$cfg_file"
|
||||
echo "" >> "$cfg_file"
|
||||
return
|
||||
fi
|
||||
|
||||
sed "s|'SHELL_DIR/|'$SHELL_DIR/|g" "$config_file" >> "$cfg_file"
|
||||
echo "" >> "$cfg_file"
|
||||
}
|
||||
|
||||
append_vscode_config() {
|
||||
local name="$1" ext_dir="$2" cfg_file="$3"
|
||||
[[ ! -d "$ext_dir" ]] && return
|
||||
local template_dir="$SHELL_DIR/matugen/templates"
|
||||
cat >> "$cfg_file" << EOF
|
||||
[templates.dms${name}default]
|
||||
input_path = '$template_dir/vscode-color-theme-default.json'
|
||||
output_path = '$ext_dir/themes/dankshell-default.json'
|
||||
|
||||
[templates.dms${name}dark]
|
||||
input_path = '$template_dir/vscode-color-theme-dark.json'
|
||||
output_path = '$ext_dir/themes/dankshell-dark.json'
|
||||
|
||||
[templates.dms${name}light]
|
||||
input_path = '$template_dir/vscode-color-theme-light.json'
|
||||
output_path = '$ext_dir/themes/dankshell-light.json'
|
||||
|
||||
EOF
|
||||
log "Added $name theme config (extension found at $ext_dir)"
|
||||
}
|
||||
|
||||
build_merged_config() {
|
||||
local mode="$1" run_user="$2" cfg_file="$3" tmp_dir="$4"
|
||||
|
||||
if [[ "$run_user" == "true" && -f "$CONFIG_DIR/matugen/config.toml" ]]; then
|
||||
awk '/^\[config\]/{p=1} /^\[templates\]/{p=0} p' "$CONFIG_DIR/matugen/config.toml" >> "$cfg_file"
|
||||
else
|
||||
echo "[config]" >> "$cfg_file"
|
||||
fi
|
||||
echo "" >> "$cfg_file"
|
||||
|
||||
grep -v '^\[config\]' "$SHELL_DIR/matugen/configs/base.toml" | sed "s|'SHELL_DIR/|'$SHELL_DIR/|g" >> "$cfg_file"
|
||||
echo "" >> "$cfg_file"
|
||||
|
||||
cat >> "$cfg_file" << EOF
|
||||
[templates.dank]
|
||||
input_path = '$SHELL_DIR/matugen/templates/dank.json'
|
||||
output_path = '$COLORS_OUTPUT'
|
||||
|
||||
EOF
|
||||
|
||||
[[ "$mode" == "light" ]] && append_config "skip" "gtk3-light.toml" "$cfg_file" || append_config "skip" "gtk3-dark.toml" "$cfg_file"
|
||||
|
||||
append_config "niri" "niri.toml" "$cfg_file"
|
||||
append_config "qt5ct" "qt5ct.toml" "$cfg_file"
|
||||
append_config "qt6ct" "qt6ct.toml" "$cfg_file"
|
||||
append_config "firefox" "firefox.toml" "$cfg_file"
|
||||
append_config "pywalfox" "pywalfox.toml" "$cfg_file"
|
||||
append_config "vesktop" "vesktop.toml" "$cfg_file"
|
||||
append_terminal_config "ghostty" "ghostty.toml" "$cfg_file" "$tmp_dir"
|
||||
append_terminal_config "kitty" "kitty.toml" "$cfg_file" "$tmp_dir"
|
||||
append_terminal_config "foot" "foot.toml" "$cfg_file" "$tmp_dir"
|
||||
append_terminal_config "alacritty" "alacritty.toml" "$cfg_file" "$tmp_dir"
|
||||
append_terminal_config "wezterm" "wezterm.toml" "$cfg_file" "$tmp_dir"
|
||||
append_config "dgop" "dgop.toml" "$cfg_file"
|
||||
|
||||
append_vscode_config "vscode" "$HOME/.vscode/extensions/local.dynamic-base16-dankshell-0.0.1" "$cfg_file"
|
||||
append_vscode_config "codium" "$HOME/.vscode-oss/extensions/local.dynamic-base16-dankshell-0.0.1" "$cfg_file"
|
||||
append_vscode_config "codeoss" "$HOME/.config/Code - OSS/extensions/local.dynamic-base16-dankshell-0.0.1" "$cfg_file"
|
||||
append_vscode_config "cursor" "$HOME/.cursor/extensions/local.dynamic-base16-dankshell-0.0.1" "$cfg_file"
|
||||
append_vscode_config "windsurf" "$HOME/.windsurf/extensions/local.dynamic-base16-dankshell-0.0.1" "$cfg_file"
|
||||
|
||||
if [[ "$run_user" == "true" && -f "$CONFIG_DIR/matugen/config.toml" ]]; then
|
||||
awk '/^\[templates\]/{p=1} p' "$CONFIG_DIR/matugen/config.toml" >> "$cfg_file"
|
||||
echo "" >> "$cfg_file"
|
||||
fi
|
||||
|
||||
if [[ -d "$CONFIG_DIR/matugen/dms/configs" ]]; then
|
||||
for config in "$CONFIG_DIR/matugen/dms/configs"/*.toml; do
|
||||
[[ -f "$config" ]] || continue
|
||||
cat "$config" >> "$cfg_file"
|
||||
echo "" >> "$cfg_file"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
generate_dank16_variants() {
|
||||
local primary_dark="$1" primary_light="$2" surface="$3" mode="$4"
|
||||
local args=(--variants --primary-dark "$primary_dark" --primary-light "$primary_light")
|
||||
[[ "$mode" == "light" ]] && args+=(--light)
|
||||
[[ -n "$surface" ]] && args+=(--background "$surface")
|
||||
dms dank16 "${args[@]}" 2>/dev/null || echo '{}'
|
||||
}
|
||||
|
||||
set_system_color_scheme() {
|
||||
[[ "$SYNC_MODE_WITH_PORTAL" != "true" ]] && return
|
||||
local mode="$1"
|
||||
local scheme="prefer-dark"
|
||||
[[ "$mode" == "light" ]] && scheme="default"
|
||||
gsettings set org.gnome.desktop.interface color-scheme "$scheme" 2>/dev/null || \
|
||||
dconf write /org/gnome/desktop/interface/color-scheme "'$scheme'" 2>/dev/null || true
|
||||
}
|
||||
|
||||
sync_color_scheme_on_exit() {
|
||||
[[ "$SYNC_MODE_WITH_PORTAL" != "true" ]] && return
|
||||
[[ ! -f "$DESIRED_JSON" ]] && return
|
||||
local json mode
|
||||
json=$(cat "$DESIRED_JSON" 2>/dev/null) || return
|
||||
mode=$(read_json_field "$json" "mode")
|
||||
[[ -n "$mode" ]] && set_system_color_scheme "$mode"
|
||||
}
|
||||
|
||||
trap sync_color_scheme_on_exit EXIT
|
||||
|
||||
refresh_gtk() {
|
||||
local mode="$1"
|
||||
local gtk_css="$CONFIG_DIR/gtk-3.0/gtk.css"
|
||||
[[ ! -e "$gtk_css" ]] && return
|
||||
local should_run=false
|
||||
if [[ -L "$gtk_css" ]]; then
|
||||
[[ "$(readlink "$gtk_css")" == *"dank-colors.css"* ]] && should_run=true
|
||||
elif grep -q "dank-colors.css" "$gtk_css" 2>/dev/null; then
|
||||
should_run=true
|
||||
fi
|
||||
[[ "$should_run" != "true" ]] && return
|
||||
gsettings set org.gnome.desktop.interface gtk-theme "" 2>/dev/null || true
|
||||
gsettings set org.gnome.desktop.interface gtk-theme "adw-gtk3-${mode}" 2>/dev/null || true
|
||||
}
|
||||
|
||||
signal_terminals() {
|
||||
pgrep -x kitty >/dev/null 2>&1 && pkill -USR1 kitty
|
||||
pgrep -x ghostty >/dev/null 2>&1 && pkill -USR2 ghostty
|
||||
pgrep -x .kitty-wrapped >/dev/null 2>&1 && pkill -USR2 .kitty-wrapped
|
||||
pgrep -x .ghostty-wrappe >/dev/null 2>&1 && pkill -USR2 .ghostty-wrappe
|
||||
}
|
||||
|
||||
build_once() {
|
||||
local json="$1"
|
||||
local kind=$(read_json_field "$json" "kind")
|
||||
local value=$(read_json_field "$json" "value")
|
||||
local mode=$(read_json_field "$json" "mode")
|
||||
local mtype=$(read_json_field "$json" "matugenType")
|
||||
local run_user=$(read_json_bool "$json" "runUserTemplates")
|
||||
local stock_colors=$(read_json_escaped_field "$json" "stockColors")
|
||||
|
||||
[[ -z "$mtype" ]] && mtype="scheme-tonal-spot"
|
||||
[[ -z "$run_user" ]] && run_user="true"
|
||||
|
||||
local TMP_CFG=$(mktemp)
|
||||
local TMP_DIR=$(mktemp -d)
|
||||
trap "rm -f '$TMP_CFG'; rm -rf '$TMP_DIR'" RETURN
|
||||
|
||||
build_merged_config "$mode" "$run_user" "$TMP_CFG" "$TMP_DIR"
|
||||
|
||||
local primary_dark primary_light surface dank16 import_args=()
|
||||
|
||||
if [[ -n "$stock_colors" ]]; then
|
||||
log "Using stock/custom theme colors with matugen base"
|
||||
primary_dark=$(echo "$stock_colors" | sed -n 's/.*"primary"[^{]*{[^}]*"dark"[^{]*{[^}]*"color"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
|
||||
primary_light=$(echo "$stock_colors" | sed -n 's/.*"primary"[^{]*{[^}]*"light"[^{]*{[^}]*"color"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
|
||||
surface=$(echo "$stock_colors" | sed -n 's/.*"surface"[^{]*{[^}]*"dark"[^{]*{[^}]*"color"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
|
||||
|
||||
[[ -z "$primary_dark" ]] && { err "Failed to extract primary dark from stock colors"; return 1; }
|
||||
[[ -z "$primary_light" ]] && primary_light="$primary_dark"
|
||||
|
||||
dank16=$(generate_dank16_variants "$primary_dark" "$primary_light" "$surface" "$mode")
|
||||
|
||||
import_args+=(--import-json-string "{\"colors\": $stock_colors, \"dank16\": $dank16}")
|
||||
|
||||
log "Running matugen color hex with stock color overrides"
|
||||
if ! matugen color hex "$primary_dark" -m "$mode" -t "${mtype:-scheme-tonal-spot}" -c "$TMP_CFG" "${import_args[@]}"; then
|
||||
err "matugen failed"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
log "Using dynamic theme from $kind: $value"
|
||||
|
||||
local matugen_cmd=("matugen")
|
||||
[[ "$kind" == "hex" ]] && matugen_cmd+=("color" "hex") || matugen_cmd+=("$kind")
|
||||
matugen_cmd+=("$value")
|
||||
|
||||
local mat_json
|
||||
mat_json=$("${matugen_cmd[@]}" -m dark -t "$mtype" --json hex --dry-run 2>/dev/null | tr -d '\n')
|
||||
[[ -z "$mat_json" ]] && { err "matugen dry-run failed"; return 1; }
|
||||
|
||||
primary_dark=$(echo "$mat_json" | sed -n 's/.*"primary"[[:space:]]*:[[:space:]]*{[^}]*"dark"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
|
||||
primary_light=$(echo "$mat_json" | sed -n 's/.*"primary"[[:space:]]*:[[:space:]]*{[^}]*"light"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
|
||||
surface=$(echo "$mat_json" | sed -n 's/.*"surface"[[:space:]]*:[[:space:]]*{[^}]*"dark"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
|
||||
|
||||
[[ -z "$primary_dark" ]] && { err "Failed to extract primary color"; return 1; }
|
||||
[[ -z "$primary_light" ]] && primary_light="$primary_dark"
|
||||
|
||||
dank16=$(generate_dank16_variants "$primary_dark" "$primary_light" "$surface" "$mode")
|
||||
|
||||
import_args+=(--import-json-string "{\"dank16\": $dank16}")
|
||||
|
||||
log "Running matugen $kind with dank16 injection"
|
||||
if ! "${matugen_cmd[@]}" -m "$mode" -t "$mtype" -c "$TMP_CFG" "${import_args[@]}"; then
|
||||
err "matugen failed"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
refresh_gtk "$mode"
|
||||
signal_terminals
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
[[ ! -f "$DESIRED_JSON" ]] && { log "No desired state file"; exit 0; }
|
||||
|
||||
DESIRED=$(cat "$DESIRED_JSON")
|
||||
WANT_KEY=$(compute_key "$DESIRED")
|
||||
HAVE_KEY=""
|
||||
[[ -f "$BUILT_KEY" ]] && HAVE_KEY=$(cat "$BUILT_KEY" 2>/dev/null || true)
|
||||
|
||||
[[ "$WANT_KEY" == "$HAVE_KEY" ]] && { log "Already up to date"; exit 0; }
|
||||
|
||||
log "Building theme (key: ${WANT_KEY:0:12}...)"
|
||||
if build_once "$DESIRED"; then
|
||||
echo "$WANT_KEY" > "$BUILT_KEY"
|
||||
log "Done"
|
||||
exit 0
|
||||
else
|
||||
err "Build failed"
|
||||
exit 2
|
||||
fi
|
||||
Reference in New Issue
Block a user