88 "os"
99 "os/exec"
1010 "path/filepath"
11+ "time"
1112)
1213
1314type 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
149159func (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\n Output: %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\n Output: %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