Skip to content

Commit 9dc38ad

Browse files
committed
bugfix: fixed font cache refresh issues on macOS 14+
1 parent 80e6252 commit 9dc38ad

File tree

4 files changed

+129
-35
lines changed

4 files changed

+129
-35
lines changed

cmd/add.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -959,6 +959,7 @@ func installDownloadedFonts(fontPaths []string, fontManager platform.FontManager
959959
if !force {
960960
expectedPath := filepath.Join(fontDir, fontDisplayName)
961961
if _, err := os.Stat(expectedPath); err == nil {
962+
output.GetDebug().State("Font already installed, skipping: %s", fontDisplayName)
962963
skipped++
963964
os.Remove(fontPath) // Clean up temp file
964965
skippedFiles = append(skippedFiles, fontDisplayName)
@@ -967,18 +968,37 @@ func installDownloadedFonts(fontPaths []string, fontManager platform.FontManager
967968
}
968969

969970
// Install the font
971+
output.GetDebug().State("Installing font file: %s to %s (scope: %s)", fontDisplayName, fontDir, installScope)
970972
installErr := fontManager.InstallFont(fontPath, installScope, force)
971973

972974
if installErr != nil {
975+
// Check if error is related to font cache refresh (non-critical on macOS 14+)
976+
errStr := installErr.Error()
977+
isCacheError := strings.Contains(strings.ToLower(errStr), "cache refresh failed (non-critical)")
978+
979+
if isCacheError {
980+
// Font was installed successfully, only cache refresh failed
981+
// This is non-critical - treat as success
982+
output.GetDebug().Warning("Font cache refresh failed (non-critical on macOS 14+): %s", fontDisplayName)
983+
output.GetDebug().State("Font installed successfully, cache refresh is optional. Font will be available after app restart.")
984+
installed++
985+
installedFiles = append(installedFiles, fontDisplayName)
986+
os.Remove(fontPath) // Clean up temp file
987+
continue
988+
}
989+
990+
// Actual installation failure
973991
os.Remove(fontPath) // Clean up temp file
974992
failed++
975993
errorMsg := makeUserFriendlyError(fontDisplayName, installErr)
976994
errors = append(errors, errorMsg)
977995
failedFiles = append(failedFiles, fontDisplayName)
978-
output.GetDebug().State("fontManager.InstallFont() failed for %s: %v", fontDisplayName, installErr)
996+
output.GetDebug().Error("fontManager.InstallFont() failed for %s: %v", fontDisplayName, installErr)
979997
continue
980998
}
981999

1000+
output.GetDebug().State("Successfully installed font: %s", fontDisplayName)
1001+
9821002
// Clean up temp file
9831003
os.Remove(fontPath)
9841004
installed++

cmd/list.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,9 +317,36 @@ func collectFonts(scopes []platform.InstallationScope, fm platform.FontManager,
317317
if !shouldSuppressVerbose {
318318
output.GetVerbose().Info("Scanning %s scope: %s", scope, fontDir)
319319
}
320+
output.GetDebug().State("Checking font directory: %s (scope: %s)", fontDir, scope)
321+
322+
// Check if directory exists and is accessible
323+
if _, err := os.Stat(fontDir); os.IsNotExist(err) {
324+
output.GetVerbose().Warning("Font directory does not exist: %s", fontDir)
325+
output.GetDebug().Warning("Directory %s does not exist, skipping", fontDir)
326+
continue // Skip this scope, don't fail
327+
}
328+
329+
// Check read permissions
330+
if _, err := os.Open(fontDir); err != nil {
331+
if os.IsPermission(err) {
332+
output.GetVerbose().Warning("No read permission for font directory: %s", fontDir)
333+
output.GetDebug().Error("Permission denied accessing %s: %v", fontDir, err)
334+
// For machine scope, suggest using sudo
335+
if scope == platform.MachineScope {
336+
return nil, fmt.Errorf("insufficient permissions to read %s. Try running with sudo or use --scope user", fontDir)
337+
}
338+
// For user scope, this is unusual but not fatal
339+
output.GetDebug().Warning("Permission denied for user scope directory (unusual), continuing...")
340+
continue
341+
}
342+
output.GetDebug().Error("Unable to access font directory %s: %v", fontDir, err)
343+
return nil, fmt.Errorf("unable to access font directory %s: %w", fontDir, err)
344+
}
345+
320346
names, err := platform.ListInstalledFonts(fontDir)
321347
if err != nil {
322-
return nil, err
348+
output.GetDebug().Error("platform.ListInstalledFonts() failed for %s: %v", fontDir, err)
349+
return nil, fmt.Errorf("failed to list fonts in %s: %w", fontDir, err)
323350
}
324351
if !shouldSuppressVerbose {
325352
output.GetVerbose().Info("Found %d files in %s", len(names), fontDir)

cmd/remove.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,20 +269,35 @@ func removeFontFiles(params RemoveFontFilesParams) (removed, skipped, failed int
269269
err := params.FontManager.RemoveFont(matchingFont, params.Scope)
270270

271271
if err != nil {
272+
// Check if error is related to font cache refresh (non-critical on macOS 14+)
273+
errStr := err.Error()
274+
isCacheError := strings.Contains(strings.ToLower(errStr), "cache refresh failed (non-critical)")
275+
276+
if isCacheError {
277+
// Font was removed successfully, only cache refresh failed
278+
// This is non-critical - treat as success
279+
output.GetDebug().Warning("Font cache refresh failed (non-critical on macOS 14+): %s", matchingFont)
280+
output.GetDebug().State("Font removed successfully, cache refresh is optional. Font removal is effective immediately.")
281+
removed++
282+
details = append(details, fontDisplayName)
283+
continue
284+
}
285+
286+
// Actual removal failure
272287
failed++
273288
var errorMsg string
274-
errStr := err.Error()
275289
if containsAny(errStr, []string{"in use", "access denied", "permission"}) {
276290
errorMsg = "Font is in use or access denied"
277291
} else {
278292
errorMsg = "Failed to remove existing font"
279293
}
280294
errors = append(errors, errorMsg)
281295
details = append(details, fontDisplayName+" (Failed)")
282-
output.GetDebug().State("fontManager.RemoveFont() failed for %s: %v", matchingFont, err)
296+
output.GetDebug().Error("fontManager.RemoveFont() failed for %s: %v", matchingFont, err)
283297
continue
284298
}
285299

300+
output.GetDebug().State("Successfully removed font: %s", fontDisplayName)
286301
removed++
287302
details = append(details, fontDisplayName)
288303
}

internal/platform/darwin.go

Lines changed: 63 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"os"
99
"os/exec"
1010
"path/filepath"
11+
"time"
1112
)
1213

1314
type darwinFontManager struct {
@@ -90,11 +91,14 @@ func (m *darwinFontManager) InstallFont(fontPath string, scope InstallationScope
9091
return fmt.Errorf("failed to copy font file: %w", err)
9192
}
9293

93-
// Update the font cache
94+
// Update the font cache (non-critical on macOS 14+)
95+
// Fonts in ~/Library/Fonts and /Library/Fonts are auto-detected by macOS
9496
if err := m.updateFontCache(scope); err != nil {
95-
// Clean up the file if cache update fails
96-
os.Remove(targetPath)
97-
return fmt.Errorf("failed to update font cache: %w", err)
97+
// Cache refresh failure is non-critical - font is already installed
98+
// On macOS 14+, fonts are auto-detected without manual cache refresh
99+
// Don't remove the file - installation succeeded, cache refresh is optional
100+
// Return a warning-style error that can be handled gracefully
101+
return fmt.Errorf("font installed successfully, but cache refresh failed (non-critical): %w", err)
98102
}
99103

100104
return nil
@@ -120,9 +124,13 @@ func (m *darwinFontManager) RemoveFont(fontName string, scope InstallationScope)
120124
return fmt.Errorf("failed to remove font file: %w", err)
121125
}
122126

123-
// Update the font cache
127+
// Update the font cache (non-critical on macOS 14+)
128+
// Font removal is effective immediately, cache refresh is optional
124129
if err := m.updateFontCache(scope); err != nil {
125-
return fmt.Errorf("failed to update font cache: %w", err)
130+
// Cache refresh failure is non-critical - font is already removed
131+
// On macOS 14+, font removal is effective without manual cache refresh
132+
// Return a warning-style error that can be handled gracefully
133+
return fmt.Errorf("font removed successfully, but cache refresh failed (non-critical): %w", err)
126134
}
127135

128136
return nil
@@ -145,38 +153,62 @@ func (m *darwinFontManager) RequiresElevation(scope InstallationScope) bool {
145153
return scope == MachineScope
146154
}
147155

148-
// updateFontCache runs atsutil to update the font cache
156+
// updateFontCache refreshes the font cache on macOS
157+
// Uses modern method compatible with macOS 14+ (Sonoma)
158+
// On macOS 14+, atsutil was removed, so we use pkill fontd instead
149159
func (m *darwinFontManager) updateFontCache(scope InstallationScope) error {
150-
var cmds []*exec.Cmd
151-
152-
switch scope {
153-
case UserScope:
154-
// Reset user font cache
155-
cmds = []*exec.Cmd{
156-
exec.Command("atsutil", "databases", "-removeUser"),
157-
exec.Command("atsutil", "server", "-shutdown"),
158-
exec.Command("atsutil", "server", "-ping"),
159-
}
160-
case MachineScope:
161-
// Reset system font cache
162-
cmds = []*exec.Cmd{
163-
exec.Command("atsutil", "databases", "-remove"),
164-
exec.Command("atsutil", "databases", "-removeUser"),
165-
exec.Command("atsutil", "server", "-shutdown"),
166-
exec.Command("atsutil", "server", "-ping"),
160+
// Modern approach: restart fontd service to refresh cache
161+
// fontd automatically restarts and picks up new fonts
162+
// This works on both older macOS versions and macOS 14+
163+
164+
// Try pkill first (more reliable and available on all macOS versions)
165+
cmd := exec.Command("pkill", "fontd")
166+
if output, err := cmd.CombinedOutput(); err != nil {
167+
// pkill returns non-zero exit code if no process was found
168+
// This is not necessarily an error - fontd may not be running
169+
// Check if the error is "no process found" vs actual failure
170+
errStr := string(output)
171+
if !contains(errStr, "No matching processes") && !contains(errStr, "no process found") {
172+
// If pkill fails for other reasons, try killall as fallback
173+
cmd = exec.Command("killall", "fontd")
174+
if output, err := cmd.CombinedOutput(); err != nil {
175+
// Both methods failed, but this is non-critical
176+
// macOS will auto-detect fonts in ~/Library/Fonts and /Library/Fonts
177+
// Fonts will be available after app restart or system refresh
178+
return fmt.Errorf("failed to refresh font cache (non-critical, fonts will still work): %v\nOutput: %s", err, string(output))
179+
}
167180
}
168-
default:
169-
return fmt.Errorf("invalid installation scope: %s", scope)
170181
}
171182

172-
// Execute each command in sequence
173-
for _, cmd := range cmds {
174-
if output, err := cmd.CombinedOutput(); err != nil {
175-
return fmt.Errorf("atsutil command failed: %v\nOutput: %s", err, string(output))
183+
// Small delay to allow fontd to restart
184+
time.Sleep(500 * time.Millisecond)
185+
186+
return nil
187+
}
188+
189+
// contains checks if a string contains a substring (case-insensitive)
190+
func contains(s, substr string) bool {
191+
s = toLower(s)
192+
substr = toLower(substr)
193+
for i := 0; i <= len(s)-len(substr); i++ {
194+
if s[i:i+len(substr)] == substr {
195+
return true
176196
}
177197
}
198+
return false
199+
}
178200

179-
return nil
201+
// toLower converts a string to lowercase (simple implementation)
202+
func toLower(s string) string {
203+
result := make([]byte, len(s))
204+
for i := 0; i < len(s); i++ {
205+
c := s[i]
206+
if c >= 'A' && c <= 'Z' {
207+
c += 'a' - 'A'
208+
}
209+
result[i] = c
210+
}
211+
return string(result)
180212
}
181213

182214
// CreateHiddenDirectory creates a directory and sets it as hidden on macOS

0 commit comments

Comments
 (0)