package main import ( "context" "encoding/json" "fmt" "os" "os/exec" "path/filepath" "slices" "strconv" "strings" "sync" "time" "github.com/AvengeMedia/DankMaterialShell/core/internal/log" ) type ipcTargets map[string]map[string][]string var qsHasAnyDisplay = sync.OnceValue(func() bool { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() cmd := exec.CommandContext(ctx, "qs", "ipc", "--help") cmd.WaitDelay = 500 * time.Millisecond out, err := cmd.Output() if err != nil { return false } return strings.Contains(string(out), "--any-display") }) func parseTargetsFromIPCShowOutput(output string) ipcTargets { targets := make(ipcTargets) var currentTarget string for line := range strings.SplitSeq(output, "\n") { if after, ok := strings.CutPrefix(line, "target "); ok { currentTarget = strings.TrimSpace(after) targets[currentTarget] = make(map[string][]string) } if strings.HasPrefix(line, " function") && currentTarget != "" { argsList := []string{} currentFunc := strings.TrimPrefix(line, " function ") funcDef := strings.SplitN(currentFunc, "(", 2) argList := strings.SplitN(funcDef[1], ")", 2)[0] args := strings.Split(argList, ",") if len(args) > 0 && strings.TrimSpace(args[0]) != "" { argsList = append(argsList, funcDef[0]) for _, arg := range args { argName := strings.SplitN(strings.TrimSpace(arg), ":", 2)[0] argsList = append(argsList, argName) } targets[currentTarget][funcDef[0]] = argsList } else { targets[currentTarget][funcDef[0]] = make([]string, 0) } } } return targets } func buildQsIPCBaseArgs() ([]string, error) { cmdArgs := []string{"ipc"} switch pid, ok := shellApp.SessionPID(); { case ok: cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid)) default: if err := shellApp.ResolveConfig(nil, nil); err != nil { return nil, err } if qsHasAnyDisplay() { cmdArgs = append(cmdArgs, "--any-display") } cmdArgs = append(cmdArgs, "-p", shellApp.ConfigPath()) } return cmdArgs, nil } func getShellIPCCompletions(args []string, _ string) []string { baseArgs, err := buildQsIPCBaseArgs() if err != nil { log.Debugf("Error building IPC args for completions: %v", err) return nil } cmdArgs := append(baseArgs, "show") ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() cmd := exec.CommandContext(ctx, "qs", cmdArgs...) cmd.WaitDelay = 500 * time.Millisecond var targets ipcTargets if output, err := cmd.Output(); err == nil { targets = parseTargetsFromIPCShowOutput(string(output)) } else { log.Debugf("Error getting IPC show output for completions: %v", err) return nil } if len(args) > 0 && args[0] == "call" { args = args[1:] } if len(args) == 0 { targetNames := make([]string, 0) targetNames = append(targetNames, "call", "list") for k := range targets { targetNames = append(targetNames, k) } return targetNames } if len(args) == 1 { if targetFuncs, ok := targets[args[0]]; ok { funcNames := make([]string, 0) for k := range targetFuncs { funcNames = append(funcNames, k) } return funcNames } return nil } if len(args) <= len(targets[args[0]]) { funcArgs := targets[args[0]][args[1]] if len(funcArgs) >= len(args) { return []string{fmt.Sprintf("[%s]", funcArgs[len(args)-1])} } } return nil } func runShellIPCCommand(args []string) { if len(args) == 0 { printIPCHelp() return } if args[0] != "call" { args = append([]string{"call"}, args...) } baseArgs, err := buildQsIPCBaseArgs() if err != nil { log.Fatalf("Error finding config: %v", err) } cmdArgs := append(baseArgs, args...) cmd := exec.Command("qs", cmdArgs...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { log.Fatalf("Error running IPC command: %v", err) } } func printIPCHelp() { fmt.Println("Usage: dms ipc call [args...]") fmt.Println() baseArgs, err := buildQsIPCBaseArgs() if err != nil { printIPCHelpFailure(err) return } cmdArgs := append(baseArgs, "show") cmd := exec.Command("qs", cmdArgs...) output, err := cmd.Output() if err != nil { printIPCHelpFailure(err) return } targets := parseTargetsFromIPCShowOutput(string(output)) if len(targets) == 0 { fmt.Println("No IPC targets available") return } fmt.Println("Targets:") targetNames := make([]string, 0, len(targets)) for name := range targets { targetNames = append(targetNames, name) } slices.Sort(targetNames) for _, targetName := range targetNames { funcs := targets[targetName] funcNames := make([]string, 0, len(funcs)) for fn := range funcs { funcNames = append(funcNames, fn) } slices.Sort(funcNames) fmt.Printf(" %-16s %s\n", targetName, strings.Join(funcNames, ", ")) } } func printIPCHelpFailure(err error) { fmt.Println("Could not retrieve IPC targets.") if err != nil { fmt.Printf(" %v\n", err) } fmt.Println() fmt.Println(" Full docs: https://danklinux.com/docs/dankmaterialshell/keybinds-ipc") fmt.Println(" Try: dms ipc call ") } // ensureFontCache rebuilds the fontconfig cache if user-configured fonts are missing while skipping defaults func ensureFontCache() { if _, err := exec.LookPath("fc-list"); err != nil { return } if _, err := exec.LookPath("fc-cache"); err != nil { return } var fontsToCheck []string if configDir, err := os.UserConfigDir(); err == nil { settingsPath := filepath.Join(configDir, "DankMaterialShell", "settings.json") if data, err := os.ReadFile(settingsPath); err == nil { var settings struct { FontFamily string `json:"fontFamily"` MonoFontFamily string `json:"monoFontFamily"` } if err := json.Unmarshal(data, &settings); err == nil { if settings.FontFamily != "" && settings.FontFamily != "Inter Variable" { fontsToCheck = append(fontsToCheck, settings.FontFamily) } if settings.MonoFontFamily != "" && settings.MonoFontFamily != "Fira Code" { fontsToCheck = append(fontsToCheck, settings.MonoFontFamily) } } } } if len(fontsToCheck) == 0 { return } output, err := exec.Command("fc-list", ":", "family").Output() if err != nil || len(strings.TrimSpace(string(output))) == 0 { log.Warnf("Font cache appears empty or corrupt, rebuilding...") rebuildFontCache() return } cacheFonts := strings.ToLower(string(output)) var missing []string for _, font := range fontsToCheck { if !fontInCache(strings.ToLower(font), cacheFonts) { missing = append(missing, font) } } if len(missing) > 0 { log.Warnf("Font(s) not found in cache: %s — rebuilding...", strings.Join(missing, ", ")) rebuildFontCache() } } func fontInCache(target, cache string) bool { for _, line := range strings.Split(cache, "\n") { for _, fam := range strings.Split(strings.TrimSpace(line), ",") { if strings.TrimSpace(fam) == target { return true } } } return false } func rebuildFontCache() { cmd := exec.Command("fc-cache", "-f") if output, err := cmd.CombinedOutput(); err != nil { log.Warnf("Failed to rebuild font cache: %v\n%s", err, string(output)) } else { log.Infof("Font cache rebuilt successfully") } } // logStartupFailure logs diagnostic advice if qs crashes within 5s of launch. func logStartupFailure(exitCode int, uptime time.Duration, stderrTail string) { if uptime >= 5*time.Second || exitCode == 0 || exitCode > 128 { return } if containsFontCrashSignature(stderrTail) { log.Errorf("DMS startup failed due to a potential font/rendering crash. Try running 'fc-cache -fv' and restarting DMS.") } else { log.Errorf("DMS startup failed (exit code %d). Run 'dms doctor' for more diagnostics.", exitCode) } } func containsFontCrashSignature(logStr string) bool { logStr = strings.ToLower(logStr) signatures := []string{ "fontconfig", "freetype", "ft_load_glyph", "ft_face", "fc-list", "fc-cache", "glyph", "typeface", } for _, sig := range signatures { if strings.Contains(logStr, sig) { return true } } return false }