From 5b52ccad7cb7a19252132411e1eac9fdef5be51f Mon Sep 17 00:00:00 2001 From: Leonard <101025489+cmleon51@users.noreply.github.com> Date: Thu, 29 Oct 2026 17:58:28 +0100 Subject: [PATCH] doctor: match font names the way qt lists them (#3972) port 2.0 (cherry picked from commit b18fc386930d30834f83a14cd7c944e10853cb97) --- core/cmd/dms/commands_doctor.go | 66 +++++++++++++++------- core/cmd/dms/commands_doctor_fonts_test.go | 61 +++++++++++++++----- 2 files changed, 93 insertions(+), 34 deletions(-) diff --git a/core/cmd/dms/commands_doctor.go b/core/cmd/dms/commands_doctor.go index 1e6cf288e..2d6b3fed5 100644 --- a/core/cmd/dms/commands_doctor.go +++ b/core/cmd/dms/commands_doctor.go @@ -1761,6 +1761,9 @@ var bundledFontRelPaths = map[string][]string{ "notable": { "DCommon/assets/fonts/notable/Notable-Regular.ttf", }, + "gochi hand": { + "DCommon/assets/fonts/gochi-hand/GochiHand-Regular.ttf", + }, } func resolveDoctorShellPath() string { @@ -1795,22 +1798,47 @@ func isBundledDefaultFont(family string) bool { return ok } -func fontInFcList(name, cacheLower string) bool { - target := strings.ToLower(strings.TrimSpace(name)) - if target == "" { - return false +const fcListQtFormat = "%{family}\t%{style}\t%{foundry}\n" + +var qtGenericFamilies = map[string]bool{"sans serif": true, "serif": true, "monospace": true} + +func qtFontFamilies() (map[string]bool, error) { + output, err := exec.Command("fc-list", "--format", fcListQtFormat).Output() + if err != nil { + return nil, err } - for line := range strings.SplitSeq(cacheLower, "\n") { - for fam := range strings.SplitSeq(strings.TrimSpace(line), ",") { - if strings.TrimSpace(fam) == target { - return true + return parseQtFontFamilies(string(output)), nil +} + +// parseQtFontFamilies retrieves, from the provided fcOutput list, a new map in the style of Qt.fontFamilies() +func parseQtFontFamilies(fcOutput string) map[string]bool { + families := map[string]bool{} + for line := range strings.SplitSeq(fcOutput, "\n") { + fields := strings.Split(line, "\t") + if len(fields) != 3 || fields[0] == "" { + continue + } + styleCount := len(strings.Split(fields[1], ",")) + for i, fam := range strings.Split(fields[0], ",") { + if i > 0 && i >= styleCount { + break + } + families[strings.ToLower(fam)] = true + // Qt only lists "Family [Foundry]" for multi-foundry families but resolves it either way + if fields[2] != "" { + families[strings.ToLower(fam+" ["+fields[2]+"]")] = true } } } - return false + return families } -func checkConfiguredFont(label, family, shellPath, fcCache string, fcListAvailable bool, url string) checkResult { +func fontInFcList(name string, families map[string]bool) bool { + target := strings.ToLower(strings.TrimSpace(name)) + return qtGenericFamilies[target] || families[target] +} + +func checkConfiguredFont(label, family, shellPath string, fcFamilies map[string]bool, fcListAvailable bool, url string) checkResult { if bundled := findBundledFontFile(shellPath, family); bundled != "" { details := "Bundled (Qt FontLoader)" if doctorVerbose { @@ -1845,7 +1873,7 @@ func checkConfiguredFont(label, family, shellPath, fcCache string, fcListAvailab } } - if fcCache == "" { + if len(fcFamilies) == 0 { return checkResult{ catFonts, label, statusWarn, fmt.Sprintf("'%s' not found", family), @@ -1854,7 +1882,7 @@ func checkConfiguredFont(label, family, shellPath, fcCache string, fcListAvailab } } - if fontInFcList(family, fcCache) { + if fontInFcList(family, fcFamilies) { return checkResult{catFonts, label, statusOK, family, "Available via fontconfig", url} } @@ -1900,18 +1928,18 @@ func checkFonts() []checkResult { needFontconfig := !isBundledDefaultFont(fontFamily) || !isBundledDefaultFont(monoFontFamily) || !isBundledDefaultFont(displayFontFamily) fcListAvailable := utils.CommandExists("fc-list") - fcCache := "" + var fcFamilies map[string]bool if needFontconfig { if !fcListAvailable { results = append(results, checkResult{catFonts, "Fontconfig Tools", statusWarn, "fc-list not installed", "Cannot verify custom fonts in fontconfig cache.", url}) } else { - output, err := exec.Command("fc-list", ":", "family").Output() + families, err := qtFontFamilies() 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}) } else { - fcCache = strings.ToLower(string(output)) - if len(strings.TrimSpace(fcCache)) == 0 { + fcFamilies = families + if len(fcFamilies) == 0 { results = append(results, checkResult{catFonts, "Fontconfig Cache", statusError, "Cache is empty", "No fonts found in fontconfig cache. Try running 'fc-cache -fv'.", url}) } } @@ -1919,9 +1947,9 @@ func checkFonts() []checkResult { } results = append(results, - checkConfiguredFont("Normal Font", fontFamily, shellPath, fcCache, fcListAvailable, url), - checkConfiguredFont("Monospace Font", monoFontFamily, shellPath, fcCache, fcListAvailable, url), - checkConfiguredFont("Display Font", displayFontFamily, shellPath, fcCache, fcListAvailable, url), + checkConfiguredFont("Normal Font", fontFamily, shellPath, fcFamilies, fcListAvailable, url), + checkConfiguredFont("Monospace Font", monoFontFamily, shellPath, fcFamilies, fcListAvailable, url), + checkConfiguredFont("Display Font", displayFontFamily, shellPath, fcFamilies, fcListAvailable, url), ) return results diff --git a/core/cmd/dms/commands_doctor_fonts_test.go b/core/cmd/dms/commands_doctor_fonts_test.go index 5b5528ac8..954f1164d 100644 --- a/core/cmd/dms/commands_doctor_fonts_test.go +++ b/core/cmd/dms/commands_doctor_fonts_test.go @@ -3,26 +3,57 @@ package main import ( "os" "path/filepath" + "strings" "testing" ) -func TestCheckBundledGoogleSans(t *testing.T) { - shellPath := t.TempDir() - fontPath := filepath.Join(shellPath, "DCommon", "assets", "fonts", "google-sans-flex", "GoogleSansFlex.ttf") - missing := checkConfiguredFont("UI font", "Google Sans Flex", shellPath, "", false, "") - if missing.status != statusWarn { - t.Fatalf("missing bundled font status = %v, want %v", missing.status, statusWarn) +func TestCheckBundledFonts(t *testing.T) { + for family, relPaths := range bundledFontRelPaths { + t.Run(family, func(t *testing.T) { + shellPath := t.TempDir() + if missing := checkConfiguredFont("Font", family, shellPath, nil, false, ""); missing.status != statusWarn { + t.Fatalf("missing bundled font status = %v, want %v", missing.status, statusWarn) + } + fontPath := filepath.Join(shellPath, relPaths[0]) + if err := os.MkdirAll(filepath.Dir(fontPath), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(fontPath, []byte("font"), 0o644); err != nil { + t.Fatal(err) + } + for _, name := range []string{family, " " + strings.ToUpper(family) + " "} { + if result := checkConfiguredFont("Font", name, shellPath, nil, false, ""); result.status != statusOK { + t.Fatalf("bundled font %q status = %v, want %v", name, result.status, statusOK) + } + } + }) } - if err := os.MkdirAll(filepath.Dir(fontPath), 0o755); err != nil { - t.Fatal(err) +} + +func TestQtFontFamilies(t *testing.T) { + fcOutput := "RobotoMono Nerd Font\tBold\tGOOG\n" + + "RobotoMono Nerd Font\tBold Italic\tpyrs\n" + + "Nimbus Sans\tRegular\tURW \n" + + "Nimbus Sans\tBold\tUKWN\n" + + "Plex,Plex Lt\tLight,Regular\tIBM\n" + + "Mono,Mono NF,Mono NF Light\tLight,Regular\tNONE\n" + families := parseQtFontFamilies(fcOutput) + + cases := map[string]bool{ + "RobotoMono Nerd Font [GOOG]": true, + "robotomono nerd font [pyrs]": true, + "RobotoMono Nerd Font": true, + "Nimbus Sans [URW ]": true, + "Plex Lt": true, + "Plex [IBM]": true, + "Mono NF": true, + "Mono NF Light": false, + "Monospace": true, + "Missing Font [GOOG]": false, } - if err := os.WriteFile(fontPath, []byte("font"), 0o644); err != nil { - t.Fatal(err) - } - for _, family := range []string{"Google Sans Flex", " google sans flex "} { - result := checkConfiguredFont("UI font", family, shellPath, "", false, "") - if result.status != statusOK { - t.Fatalf("bundled font %q status = %v, want %v", family, result.status, statusOK) + for name, want := range cases { + if got := fontInFcList(name, families); got != want { + t.Errorf("fontInFcList(%q) = %v, want %v", name, got, want) } } }