Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 47 additions & 19 deletions core/cmd/dms/commands_doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 fontInFcList(name string, families map[string]bool) bool {
target := strings.ToLower(strings.TrimSpace(name))
return qtGenericFamilies[target] || families[target]
}

func checkConfiguredFont(label, family, shellPath, fcCache string, fcListAvailable bool, url string) checkResult {
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 {
Expand Down Expand Up @@ -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),
Expand All @@ -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}
}

Expand Down Expand Up @@ -1900,28 +1928,28 @@ 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})
}
}
}
}

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
Expand Down
61 changes: 46 additions & 15 deletions core/cmd/dms/commands_doctor_fonts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
if err := os.WriteFile(fontPath, []byte("font"), 0o644); 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,
}
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)
}
}
}
Loading