@@ -704,6 +704,31 @@ Use --scope to set installation location:
704704 send (components.ProgressUpdateMsg {Percent : percent })
705705
706706 // Install the font using the installFont helper
707+ lastStep := ""
708+ lastPctBucket := - 1
709+ onProgress := func (step string , stepPct float64 ) {
710+ // Avoid spamming the UI: only update on step change or ~5% within-step progress.
711+ bucket := int (shared .Clamp01 (stepPct ) * 20.0 ) // 0..20
712+ if step == lastStep && bucket == lastPctBucket {
713+ return
714+ }
715+ lastStep = step
716+ lastPctBucket = bucket
717+
718+ msg := step + "..."
719+ if step == installStepDownload {
720+ msg = "Downloading from " + fontGroup .SourceName
721+ }
722+
723+ send (components.ItemUpdateMsg {
724+ Index : itemIndex ,
725+ Status : "in_progress" ,
726+ Message : msg ,
727+ })
728+ send (components.ProgressUpdateMsg {
729+ Percent : OverallInstallPercent (itemIndex , len (fontsToInstall ), step , stepPct ),
730+ })
731+ }
707732 result , err := installFont (
708733 fontGroup .Fonts ,
709734 fontGroup .FontID ,
@@ -712,6 +737,7 @@ Use --scope to set installation location:
712737 force ,
713738 fontDir ,
714739 true , // suppress per-file verbose download lines while Bubble Tea owns stdout
740+ onProgress ,
715741 )
716742
717743 if err != nil {
@@ -800,8 +826,7 @@ Use --scope to set installation location:
800826 })
801827
802828 // Update progress percentage - now based on actual completion
803- percent = float64 (itemIndex + 1 ) / float64 (len (fontsToInstall )) * 100
804- send (components.ProgressUpdateMsg {Percent : percent })
829+ send (components.ProgressUpdateMsg {Percent : OverallInstallPercent (itemIndex , len (fontsToInstall ), installStepCompleted , 1 )})
805830 }
806831
807832 return nil
@@ -942,6 +967,7 @@ func installFontsInDebugMode(fontManager platform.FontManager, fontsToInstall []
942967 force ,
943968 fontDir ,
944969 false , // debug path: allow per-file verbose download lines
970+ nil ,
945971 )
946972
947973 if err != nil {
@@ -1034,13 +1060,37 @@ func humanizeFontStyleLabel(s string) string {
10341060}
10351061
10361062// downloadFontVariants downloads all variants of a font family
1037- func downloadFontVariants (fontFiles []repo.FontFile , tempDir string , downloadOpts * repo.DownloadFontOptions ) ([]string , error ) {
1063+ func downloadFontVariants (fontFiles []repo.FontFile , tempDir string , downloadOpts * repo.DownloadFontOptions , onProgress StepProgressFunc ) ([]string , error ) {
10381064 var allFontPaths []string
10391065
10401066 // Download each variant - only log errors and unusual cases
1041- for _ , fontFile := range fontFiles {
1067+ total := len (fontFiles )
1068+ for i , fontFile := range fontFiles {
1069+ if onProgress != nil && total > 0 {
1070+ onProgress (installStepDownload , float64 (i )/ float64 (total ))
1071+ }
1072+ opts := downloadOpts
1073+ if onProgress != nil {
1074+ // Ensure we always pass a progress-enabled options struct, while preserving suppression.
1075+ suppress := opts != nil && opts .SuppressVerboseProgressLine
1076+ opts = & repo.DownloadFontOptions {SuppressVerboseProgressLine : suppress }
1077+ opts .OnBytesDownloaded = func (doneBytes int64 , totalBytes int64 ) {
1078+ if totalBytes > 0 {
1079+ onProgress (installStepDownload , float64 (doneBytes )/ float64 (totalBytes ))
1080+ }
1081+ }
1082+ opts .OnExtractProgress = func (done int , total int ) {
1083+ if total > 0 {
1084+ onProgress (installStepExtract , float64 (done )/ float64 (total ))
1085+ return
1086+ }
1087+ // Unknown totals (e.g., tar streams): use a soft-saturating curve so the UI moves.
1088+ onProgress (installStepExtract , float64 (done )/ float64 (done + 12 ))
1089+ }
1090+ }
1091+
10421092 output .GetDebug ().State ("Calling repo.DownloadAndExtractFont() for variant: %s from %s" , fontFile .Variant , fontFile .DownloadURL )
1043- fontPaths , err := repo .DownloadAndExtractFont (& fontFile , tempDir , downloadOpts )
1093+ fontPaths , err := repo .DownloadAndExtractFont (& fontFile , tempDir , opts )
10441094 if err != nil {
10451095 output .GetDebug ().State ("repo.DownloadAndExtractFont() failed for variant %s: %v" , fontFile .Variant , err )
10461096 return nil , err
@@ -1052,18 +1102,26 @@ func downloadFontVariants(fontFiles []repo.FontFile, tempDir string, downloadOpt
10521102 }
10531103 }
10541104
1105+ if onProgress != nil {
1106+ onProgress (installStepDownload , 1 )
1107+ onProgress (installStepExtract , 1 )
1108+ }
10551109 return allFontPaths , nil
10561110}
10571111
10581112// installDownloadedFonts installs downloaded font files to system
1059- func installDownloadedFonts (fontPaths []string , fontManager platform.FontManager , installScope platform.InstallationScope , fontDir string , force bool ) (installed , skipped , failed int , details []string , errors []string , downloadSize int64 ) {
1113+ func installDownloadedFonts (fontPaths []string , fontManager platform.FontManager , installScope platform.InstallationScope , fontDir string , force bool , onProgress StepProgressFunc ) (installed , skipped , failed int , details []string , errors []string , downloadSize int64 ) {
10601114 var installedFiles []string
10611115 var skippedFiles []string
10621116 var failedFiles []string
10631117
10641118 batchOpts := & platform.InstallFontOptions {SkipPostInstallCacheRefresh : true }
10651119
1066- for _ , fontPath := range fontPaths {
1120+ total := len (fontPaths )
1121+ for i , fontPath := range fontPaths {
1122+ if onProgress != nil && total > 0 {
1123+ onProgress (installStepInstall , float64 (i )/ float64 (total ))
1124+ }
10671125 fontDisplayName := filepath .Base (fontPath )
10681126
10691127 // Get file size before we potentially remove it
@@ -1106,8 +1164,15 @@ func installDownloadedFonts(fontPaths []string, fontManager platform.FontManager
11061164 installedFiles = append (installedFiles , fontDisplayName )
11071165 }
11081166
1167+ if onProgress != nil {
1168+ onProgress (installStepInstall , 1 )
1169+ }
1170+
11091171 // Single cache refresh / font-change notification after all copies (avoids pkill fontd / fc-cache / WM_FONTCHANGE per file)
11101172 if installed > 0 {
1173+ if onProgress != nil {
1174+ onProgress (installStepFinalize , 0 )
1175+ }
11111176 if flushErr := fontManager .FlushFontCache (installScope ); flushErr != nil {
11121177 errStr := flushErr .Error ()
11131178 isDarwinNonCritical := strings .Contains (strings .ToLower (errStr ), "failed to refresh font cache (non-critical" )
@@ -1119,6 +1184,9 @@ func installDownloadedFonts(fontPaths []string, fontManager platform.FontManager
11191184 output .GetDebug ().Error ("Post-install font cache flush failed: %v" , flushErr )
11201185 }
11211186 }
1187+ if onProgress != nil {
1188+ onProgress (installStepFinalize , 1 )
1189+ }
11221190 }
11231191
11241192 // Store categorized details: installed, then skipped, then failed
@@ -1168,7 +1236,11 @@ func installFont(
11681236 force bool ,
11691237 fontDir string ,
11701238 suppressVerboseDownloads bool ,
1239+ onProgress StepProgressFunc ,
11711240) (* InstallResult , error ) {
1241+ if onProgress != nil {
1242+ onProgress (installStepPrecheck , 0 )
1243+ }
11721244 // Check if font is already installed BEFORE downloading (unless force flag is set)
11731245 // This saves bandwidth by skipping downloads for already-installed fonts
11741246 if ! force && fontID != "" && len (fontFiles ) > 0 {
@@ -1182,6 +1254,10 @@ func installFont(
11821254 // Log warning but continue with installation (fail-safe behavior)
11831255 GetLogger ().Warn ("Failed to check if font is already installed (ID: %s): %v. Proceeding with installation." , fontID , checkErr )
11841256 } else if alreadyInstalled {
1257+ if onProgress != nil {
1258+ onProgress (installStepPrecheck , 1 )
1259+ onProgress (installStepCompleted , 1 )
1260+ }
11851261 // Font is already installed - skip download and mark all variants as skipped
11861262 output .GetDebug ().State ("Font %s (ID: %s) is already installed, skipping download" , fontName , fontID )
11871263 var details []string
@@ -1191,6 +1267,9 @@ func installFont(
11911267 return buildInstallResult (InstallStatusSkipped , "Already installed" , 0 , len (fontFiles ), 0 , details , nil , 0 ), nil
11921268 }
11931269 }
1270+ if onProgress != nil {
1271+ onProgress (installStepPrecheck , 1 )
1272+ }
11941273
11951274 // Download all variants of this font family
11961275 tempDir , err := platform .GetTempFontsDir ()
@@ -1211,14 +1290,17 @@ func installFont(
12111290 if suppressVerboseDownloads {
12121291 downloadOpts = & repo.DownloadFontOptions {SuppressVerboseProgressLine : true }
12131292 }
1214- allFontPaths , downloadErr := downloadFontVariants (fontFiles , tempDir , downloadOpts )
1293+ if onProgress != nil {
1294+ onProgress (installStepDownload , 0 )
1295+ }
1296+ allFontPaths , downloadErr := downloadFontVariants (fontFiles , tempDir , downloadOpts , onProgress )
12151297 if downloadErr != nil {
12161298 return buildInstallResult (InstallStatusFailed , "Download failed" , 0 , 0 , len (fontFiles ), nil , nil , 0 ), downloadErr
12171299 }
12181300
12191301 // Install downloaded fonts
12201302 installed , skipped , failed , details , errors , downloadSize := installDownloadedFonts (
1221- allFontPaths , fontManager , installScope , fontDir , force )
1303+ allFontPaths , fontManager , installScope , fontDir , force , onProgress )
12221304
12231305 // Determine final status
12241306 status := InstallStatusCompleted
0 commit comments