From 8155970ba2861d1b5e3e25dc3d91bb34643ceedb Mon Sep 17 00:00:00 2001 From: purian23 Date: Sat, 6 Jun 2026 19:24:52 -0400 Subject: [PATCH] fix(fonts): auto-rebuild font cache when configured fonts are missing - Add Fonts category to dms doctor for manual diagnostics - Fix a default font setting warning --- core/cmd/dms/commands_doctor.go | 101 +++++++++++ core/cmd/dms/shell.go | 158 +++++++++++++++++- quickshell/Common/settings/SettingsSpec.js | 1 + .../Modules/Notepad/NotepadSettings.qml | 2 +- .../Modules/Settings/TypographyMotionTab.qml | 4 +- 5 files changed, 257 insertions(+), 9 deletions(-) diff --git a/core/cmd/dms/commands_doctor.go b/core/cmd/dms/commands_doctor.go index bb25cffd..206a8597 100644 --- a/core/cmd/dms/commands_doctor.go +++ b/core/cmd/dms/commands_doctor.go @@ -125,6 +125,7 @@ const ( catConfigFiles catServices catEnvironment + catFonts ) func (c category) String() string { @@ -147,6 +148,8 @@ func (c category) String() string { return "Services" case catEnvironment: return "Environment" + case catFonts: + return "Fonts" default: return "Unknown" } @@ -213,6 +216,7 @@ func runDoctor(cmd *cobra.Command, args []string) { checkConfigurationFiles(), checkSystemdServices(), checkEnvironmentVars(), + checkFonts(), ) switch { @@ -1135,3 +1139,100 @@ func formatResultsPlain(results []checkResult) string { return sb.String() } + +func checkFonts() []checkResult { + var results []checkResult + url := doctorDocsURL + "#fonts" + + configDir, err := os.UserConfigDir() + if err != nil { + return nil + } + settingsPath := filepath.Join(configDir, "DankMaterialShell", "settings.json") + + fontFamily := "Inter Variable" + monoFontFamily := "Fira Code" + + 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 != "" { + fontFamily = settings.FontFamily + } + if settings.MonoFontFamily != "" { + monoFontFamily = settings.MonoFontFamily + } + } + } + + if !utils.CommandExists("fc-list") { + results = append(results, checkResult{catFonts, "Fontconfig Tools", statusWarn, "fc-list not installed", "Cannot verify if fonts are cached.", url}) + return results + } + + // Retrieve font list + output, err := exec.Command("fc-list", ":", "family").Output() + if err != nil { + results = append(results, checkResult{catFonts, "Fontconfig Cache", statusError, "Failed to query font list", "Fontconfig cache query failed. Try running 'fc-cache -fv'.", url}) + return results + } + + outStr := string(output) + if len(strings.TrimSpace(outStr)) == 0 { + results = append(results, checkResult{catFonts, "Fontconfig Cache", statusError, "Cache is empty", "No fonts found in fontconfig cache. Try running 'fc-cache -fv'.", url}) + return results + } + + lowerFonts := strings.ToLower(outStr) + + // Helper to check if a font exists + hasFont := func(name string) bool { + target := strings.ToLower(strings.TrimSpace(name)) + if target == "" { + return false + } + for _, line := range strings.Split(lowerFonts, "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + // Each line can have comma-separated families + families := strings.Split(line, ",") + for _, fam := range families { + if strings.TrimSpace(fam) == target { + return true + } + } + } + return false + } + + // Normal Font Check + if hasFont(fontFamily) { + results = append(results, checkResult{catFonts, "Normal Font", statusOK, fontFamily, "Available", url}) + } else { + results = append(results, checkResult{ + catFonts, "Normal Font", statusWarn, + fmt.Sprintf("'%s' not found", fontFamily), + "Font is not registered. Try running 'fc-cache -fv' or install the font.", + url, + }) + } + + // Monospace Font Check + if hasFont(monoFontFamily) { + results = append(results, checkResult{catFonts, "Monospace Font", statusOK, monoFontFamily, "Available", url}) + } else { + results = append(results, checkResult{ + catFonts, "Monospace Font", statusWarn, + fmt.Sprintf("'%s' not found", monoFontFamily), + "Font is not registered. Try running 'fc-cache -fv' or install the font.", + url, + }) + } + + return results +} diff --git a/core/cmd/dms/shell.go b/core/cmd/dms/shell.go index b5bcc615..ced65dcf 100644 --- a/core/cmd/dms/shell.go +++ b/core/cmd/dms/shell.go @@ -2,7 +2,9 @@ package main import ( "context" + "encoding/json" "fmt" + "io" "os" "os/exec" "os/signal" @@ -192,6 +194,7 @@ func runShellInteractive(session bool) { } }() + ensureFontCache() log.Infof("Spawning quickshell with -p %s", configPath) cmd := exec.CommandContext(ctx, "qs", "-p", configPath) @@ -227,8 +230,10 @@ func runShellInteractive(session bool) { cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr + tracker := &stderrTracker{parent: os.Stderr} + cmd.Stderr = tracker + startTime := time.Now() if err := cmd.Start(); err != nil { log.Fatalf("Error starting quickshell: %v", err) } @@ -277,7 +282,9 @@ func runShellInteractive(session bool) { case <-errChan: cancel() os.Remove(socketPath) - os.Exit(getProcessExitCode(cmd.ProcessState)) + exitCode := getProcessExitCode(cmd.ProcessState) + logStartupFailure(startTime, exitCode, tracker) + os.Exit(exitCode) case <-time.After(500 * time.Millisecond): } @@ -294,7 +301,9 @@ func runShellInteractive(session bool) { cmd.Process.Signal(syscall.SIGTERM) } os.Remove(socketPath) - os.Exit(getProcessExitCode(cmd.ProcessState)) + exitCode := getProcessExitCode(cmd.ProcessState) + logStartupFailure(startTime, exitCode, tracker) + os.Exit(exitCode) } } } @@ -434,6 +443,7 @@ func runShellDaemon(session bool) { } }() + ensureFontCache() log.Infof("Spawning quickshell with -p %s", configPath) cmd := exec.CommandContext(ctx, "qs", "-p", configPath) @@ -478,8 +488,10 @@ func runShellDaemon(session bool) { cmd.Stdin = devNull cmd.Stdout = devNull - cmd.Stderr = devNull + tracker := &stderrTracker{parent: devNull} + cmd.Stderr = tracker + startTime := time.Now() if err := cmd.Start(); err != nil { log.Fatalf("Error starting daemon: %v", err) } @@ -528,7 +540,9 @@ func runShellDaemon(session bool) { case <-errChan: cancel() os.Remove(socketPath) - os.Exit(getProcessExitCode(cmd.ProcessState)) + exitCode := getProcessExitCode(cmd.ProcessState) + logStartupFailure(startTime, exitCode, tracker) + os.Exit(exitCode) case <-time.After(500 * time.Millisecond): } @@ -543,7 +557,9 @@ func runShellDaemon(session bool) { cmd.Process.Signal(syscall.SIGTERM) } os.Remove(socketPath) - os.Exit(getProcessExitCode(cmd.ProcessState)) + exitCode := getProcessExitCode(cmd.ProcessState) + logStartupFailure(startTime, exitCode, tracker) + os.Exit(exitCode) } } } @@ -748,3 +764,133 @@ func printIPCHelp() { fmt.Printf(" %-16s %s\n", targetName, strings.Join(funcNames, ", ")) } } + +// 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") + } +} + +type stderrTracker struct { + mu sync.Mutex + buf strings.Builder + parent io.Writer +} + +func (s *stderrTracker) Write(p []byte) (n int, err error) { + s.mu.Lock() + defer s.mu.Unlock() + if s.buf.Len() < 8192 { + s.buf.Write(p) + } + if s.parent != nil { + return s.parent.Write(p) + } + return len(p), nil +} + +func (s *stderrTracker) String() string { + s.mu.Lock() + defer s.mu.Unlock() + return s.buf.String() +} + +// logStartupFailure logs diagnostic advice if qs crashes within 5s of launch. +func logStartupFailure(startTime time.Time, exitCode int, tracker *stderrTracker) { + if time.Since(startTime) >= 5*time.Second || exitCode == 0 || exitCode > 128 { + return + } + if containsFontCrashSignature(tracker.String()) { + 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 +} diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index 7e69fd16..aa6a76d0 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -594,6 +594,7 @@ function getValidKeys() { function set(root, key, value, saveFn, hooks) { if (!(key in SPEC)) return; + if (value === undefined || value === null) value = SPEC[key].def; root[key] = value; var hookName = SPEC[key].onChange; if (hookName && hooks && hooks[hookName]) { diff --git a/quickshell/Modules/Notepad/NotepadSettings.qml b/quickshell/Modules/Notepad/NotepadSettings.qml index a701df62..7f76e81d 100644 --- a/quickshell/Modules/Notepad/NotepadSettings.qml +++ b/quickshell/Modules/Notepad/NotepadSettings.qml @@ -41,7 +41,7 @@ Item { var fontName2 = availableFonts[j]; if (fontName2.startsWith(".")) continue; - if (fontName2 === SettingsData.defaultMonoFontFamily) + if (fontName2 === Theme.defaultMonoFontFamily) continue; var lowerName = fontName2.toLowerCase(); if (lowerName.includes("mono") || lowerName.includes("code") || lowerName.includes("console") || lowerName.includes("terminal") || lowerName.includes("courier") || lowerName.includes("dejavu sans mono") || lowerName.includes("jetbrains") || lowerName.includes("fira") || lowerName.includes("hack") || lowerName.includes("source code") || lowerName.includes("ubuntu mono") || lowerName.includes("cascadia")) { diff --git a/quickshell/Modules/Settings/TypographyMotionTab.qml b/quickshell/Modules/Settings/TypographyMotionTab.qml index 283effec..73a6eb43 100644 --- a/quickshell/Modules/Settings/TypographyMotionTab.qml +++ b/quickshell/Modules/Settings/TypographyMotionTab.qml @@ -226,13 +226,13 @@ Item { text: I18n.tr("Monospace Font") description: I18n.tr("Select monospace font for process list and technical displays") options: root.fontsEnumerated ? root.cachedMonoFamilies : ["Default"] - currentValue: SettingsData.monoFontFamily === SettingsData.defaultMonoFontFamily ? "Default" : (SettingsData.monoFontFamily || "Default") + currentValue: SettingsData.monoFontFamily === Theme.defaultMonoFontFamily ? "Default" : (SettingsData.monoFontFamily || "Default") enableFuzzySearch: true popupWidthOffset: 100 maxPopupHeight: 400 onValueChanged: value => { if (value === "Default") - SettingsData.set("monoFontFamily", SettingsData.defaultMonoFontFamily); + SettingsData.set("monoFontFamily", Theme.defaultMonoFontFamily); else SettingsData.set("monoFontFamily", value); }