From 5dc7c0d7974ee12a4d937426685346497a2a44cf Mon Sep 17 00:00:00 2001 From: bbedward Date: Wed, 7 Jan 2026 16:45:31 -0500 Subject: [PATCH] core: add resolve-include recursive fixes #1294 --- core/cmd/dms/commands_common.go | 1 + core/cmd/dms/commands_config.go | 318 ++++++++++++++++++ .../DisplayConfig/DisplayConfigState.qml | 29 +- .../Modules/Settings/ThemeColorsTab.qml | 30 +- 4 files changed, 362 insertions(+), 16 deletions(-) create mode 100644 core/cmd/dms/commands_config.go diff --git a/core/cmd/dms/commands_common.go b/core/cmd/dms/commands_common.go index a3462ee1..0c50df5e 100644 --- a/core/cmd/dms/commands_common.go +++ b/core/cmd/dms/commands_common.go @@ -514,5 +514,6 @@ func getCommonCommands() []*cobra.Command { matugenCmd, clipboardCmd, doctorCmd, + configCmd, } } diff --git a/core/cmd/dms/commands_config.go b/core/cmd/dms/commands_config.go new file mode 100644 index 00000000..d604256c --- /dev/null +++ b/core/cmd/dms/commands_config.go @@ -0,0 +1,318 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/AvengeMedia/DankMaterialShell/core/internal/log" + "github.com/AvengeMedia/DankMaterialShell/core/internal/utils" + "github.com/spf13/cobra" +) + +var configCmd = &cobra.Command{ + Use: "config", + Short: "Configuration utilities", +} + +var resolveIncludeCmd = &cobra.Command{ + Use: "resolve-include ", + Short: "Check if a file is included in compositor config", + Long: "Recursively check if a file is included/sourced in compositor configuration. Returns JSON with exists and included status.", + Args: cobra.ExactArgs(2), + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + switch len(args) { + case 0: + return []string{"hyprland", "niri", "mangowc"}, cobra.ShellCompDirectiveNoFileComp + case 1: + return []string{"cursor.kdl", "cursor.conf", "outputs.kdl", "outputs.conf", "binds.kdl", "binds.conf"}, cobra.ShellCompDirectiveNoFileComp + } + return nil, cobra.ShellCompDirectiveNoFileComp + }, + Run: runResolveInclude, +} + +func init() { + configCmd.AddCommand(resolveIncludeCmd) +} + +type IncludeResult struct { + Exists bool `json:"exists"` + Included bool `json:"included"` +} + +func runResolveInclude(cmd *cobra.Command, args []string) { + compositor := strings.ToLower(args[0]) + filename := args[1] + + var result IncludeResult + var err error + + switch compositor { + case "hyprland": + result, err = checkHyprlandInclude(filename) + case "niri": + result, err = checkNiriInclude(filename) + case "mangowc", "dwl", "mango": + result, err = checkMangoWCInclude(filename) + default: + log.Fatalf("Unknown compositor: %s", compositor) + } + + if err != nil { + log.Fatalf("Error checking include: %v", err) + } + + output, _ := json.Marshal(result) + fmt.Fprintln(os.Stdout, string(output)) +} + +func checkHyprlandInclude(filename string) (IncludeResult, error) { + configDir, err := utils.ExpandPath("$HOME/.config/hypr") + if err != nil { + return IncludeResult{}, err + } + + targetPath := filepath.Join(configDir, "dms", filename) + result := IncludeResult{} + + if _, err := os.Stat(targetPath); err == nil { + result.Exists = true + } + + mainConfig := filepath.Join(configDir, "hyprland.conf") + if _, err := os.Stat(mainConfig); os.IsNotExist(err) { + return result, nil + } + + processed := make(map[string]bool) + result.Included = hyprlandFindInclude(mainConfig, "dms/"+filename, processed) + return result, nil +} + +func hyprlandFindInclude(filePath, target string, processed map[string]bool) bool { + absPath, err := filepath.Abs(filePath) + if err != nil { + return false + } + + if processed[absPath] { + return false + } + processed[absPath] = true + + data, err := os.ReadFile(absPath) + if err != nil { + return false + } + + baseDir := filepath.Dir(absPath) + lines := strings.Split(string(data), "\n") + + for _, line := range lines { + trimmed := strings.TrimSpace(line) + if strings.HasPrefix(trimmed, "#") || trimmed == "" { + continue + } + + if !strings.HasPrefix(trimmed, "source") { + continue + } + + parts := strings.SplitN(trimmed, "=", 2) + if len(parts) < 2 { + continue + } + + sourcePath := strings.TrimSpace(parts[1]) + if matchesTarget(sourcePath, target) { + return true + } + + fullPath := sourcePath + if !filepath.IsAbs(sourcePath) { + fullPath = filepath.Join(baseDir, sourcePath) + } + + expanded, err := utils.ExpandPath(fullPath) + if err != nil { + continue + } + + if hyprlandFindInclude(expanded, target, processed) { + return true + } + } + + return false +} + +func checkNiriInclude(filename string) (IncludeResult, error) { + configDir, err := utils.ExpandPath("$HOME/.config/niri") + if err != nil { + return IncludeResult{}, err + } + + targetPath := filepath.Join(configDir, "dms", filename) + result := IncludeResult{} + + if _, err := os.Stat(targetPath); err == nil { + result.Exists = true + } + + mainConfig := filepath.Join(configDir, "config.kdl") + if _, err := os.Stat(mainConfig); os.IsNotExist(err) { + return result, nil + } + + processed := make(map[string]bool) + result.Included = niriFindInclude(mainConfig, "dms/"+filename, processed) + return result, nil +} + +func niriFindInclude(filePath, target string, processed map[string]bool) bool { + absPath, err := filepath.Abs(filePath) + if err != nil { + return false + } + + if processed[absPath] { + return false + } + processed[absPath] = true + + data, err := os.ReadFile(absPath) + if err != nil { + return false + } + + baseDir := filepath.Dir(absPath) + content := string(data) + + for _, line := range strings.Split(content, "\n") { + trimmed := strings.TrimSpace(line) + if strings.HasPrefix(trimmed, "//") || trimmed == "" { + continue + } + + if !strings.HasPrefix(trimmed, "include") { + continue + } + + startQuote := strings.Index(trimmed, "\"") + if startQuote == -1 { + continue + } + endQuote := strings.LastIndex(trimmed, "\"") + if endQuote <= startQuote { + continue + } + + includePath := trimmed[startQuote+1 : endQuote] + if matchesTarget(includePath, target) { + return true + } + + fullPath := includePath + if !filepath.IsAbs(includePath) { + fullPath = filepath.Join(baseDir, includePath) + } + + if niriFindInclude(fullPath, target, processed) { + return true + } + } + + return false +} + +func checkMangoWCInclude(filename string) (IncludeResult, error) { + configDir, err := utils.ExpandPath("$HOME/.config/mango") + if err != nil { + return IncludeResult{}, err + } + + targetPath := filepath.Join(configDir, "dms", filename) + result := IncludeResult{} + + if _, err := os.Stat(targetPath); err == nil { + result.Exists = true + } + + mainConfig := filepath.Join(configDir, "config.conf") + if _, err := os.Stat(mainConfig); os.IsNotExist(err) { + mainConfig = filepath.Join(configDir, "mango.conf") + } + if _, err := os.Stat(mainConfig); os.IsNotExist(err) { + return result, nil + } + + processed := make(map[string]bool) + result.Included = mangowcFindInclude(mainConfig, "dms/"+filename, processed) + return result, nil +} + +func mangowcFindInclude(filePath, target string, processed map[string]bool) bool { + absPath, err := filepath.Abs(filePath) + if err != nil { + return false + } + + if processed[absPath] { + return false + } + processed[absPath] = true + + data, err := os.ReadFile(absPath) + if err != nil { + return false + } + + baseDir := filepath.Dir(absPath) + lines := strings.Split(string(data), "\n") + + for _, line := range lines { + trimmed := strings.TrimSpace(line) + if strings.HasPrefix(trimmed, "#") || trimmed == "" { + continue + } + + if !strings.HasPrefix(trimmed, "source") { + continue + } + + parts := strings.SplitN(trimmed, "=", 2) + if len(parts) < 2 { + continue + } + + sourcePath := strings.TrimSpace(parts[1]) + if matchesTarget(sourcePath, target) { + return true + } + + fullPath := sourcePath + if !filepath.IsAbs(sourcePath) { + fullPath = filepath.Join(baseDir, sourcePath) + } + + expanded, err := utils.ExpandPath(fullPath) + if err != nil { + continue + } + + if mangowcFindInclude(expanded, target, processed) { + return true + } + } + + return false +} + +func matchesTarget(path, target string) bool { + path = strings.TrimPrefix(path, "./") + target = strings.TrimPrefix(target, "./") + return path == target || strings.HasSuffix(path, "/"+target) +} diff --git a/quickshell/Modules/Settings/DisplayConfig/DisplayConfigState.qml b/quickshell/Modules/Settings/DisplayConfig/DisplayConfigState.qml index 5e7fa4fb..794da017 100644 --- a/quickshell/Modules/Settings/DisplayConfig/DisplayConfigState.qml +++ b/quickshell/Modules/Settings/DisplayConfig/DisplayConfigState.qml @@ -401,8 +401,8 @@ Singleton { } function checkIncludeStatus() { - const paths = getConfigPaths(); - if (!paths) { + const compositor = CompositorService.compositor; + if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "dwl") { includeStatus = { "exists": false, "included": false @@ -410,14 +410,27 @@ Singleton { return; } + const filename = (compositor === "niri") ? "outputs.kdl" : "outputs.conf"; + const compositorArg = (compositor === "dwl") ? "mangowc" : compositor; + checkingInclude = true; - Proc.runCommand("check-outputs-include", ["sh", "-c", `exists=false; included=false; ` + `[ -f "${paths.outputsFile}" ] && exists=true; ` + `[ -f "${paths.configFile}" ] && grep -v '^[[:space:]]*\\(//\\|#\\)' "${paths.configFile}" | grep -q '${paths.grepPattern}' && included=true; ` + `echo "$exists $included"`], (output, exitCode) => { + Proc.runCommand("check-outputs-include", ["dms", "config", "resolve-include", compositorArg, filename], (output, exitCode) => { checkingInclude = false; - const parts = output.trim().split(" "); - includeStatus = { - "exists": parts[0] === "true", - "included": parts[1] === "true" - }; + if (exitCode !== 0) { + includeStatus = { + "exists": false, + "included": false + }; + return; + } + try { + includeStatus = JSON.parse(output.trim()); + } catch (e) { + includeStatus = { + "exists": false, + "included": false + }; + } }); } diff --git a/quickshell/Modules/Settings/ThemeColorsTab.qml b/quickshell/Modules/Settings/ThemeColorsTab.qml index 763b8e82..df839f3e 100644 --- a/quickshell/Modules/Settings/ThemeColorsTab.qml +++ b/quickshell/Modules/Settings/ThemeColorsTab.qml @@ -55,22 +55,36 @@ Item { } function checkCursorIncludeStatus() { - const paths = getCursorConfigPaths(); - if (!paths) { + const compositor = CompositorService.compositor; + if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "dwl") { cursorIncludeStatus = { "exists": false, "included": false }; return; } + + const filename = (compositor === "niri") ? "cursor.kdl" : "cursor.conf"; + const compositorArg = (compositor === "dwl") ? "mangowc" : compositor; + checkingCursorInclude = true; - Proc.runCommand("check-cursor-include", ["sh", "-c", `exists=false; included=false; ` + `[ -f "${paths.cursorFile}" ] && exists=true; ` + `[ -f "${paths.configFile}" ] && grep -v '^[[:space:]]*\\(//\\|#\\)' "${paths.configFile}" | grep -q '${paths.grepPattern}' && included=true; ` + `echo "$exists $included"`], (output, exitCode) => { + Proc.runCommand("check-cursor-include", ["dms", "config", "resolve-include", compositorArg, filename], (output, exitCode) => { checkingCursorInclude = false; - const parts = output.trim().split(" "); - cursorIncludeStatus = { - "exists": parts[0] === "true", - "included": parts[1] === "true" - }; + if (exitCode !== 0) { + cursorIncludeStatus = { + "exists": false, + "included": false + }; + return; + } + try { + cursorIncludeStatus = JSON.parse(output.trim()); + } catch (e) { + cursorIncludeStatus = { + "exists": false, + "included": false + }; + } }); }