]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
Report OSC 9;4 progress when building
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Tue, 30 Sep 2025 09:43:26 +0000 (11:43 +0200)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Wed, 1 Oct 2025 12:27:23 +0000 (14:27 +0200)
As supported by the Ghostty terminal and others.

commands/hugobuilder.go
common/terminal/colors.go
hugolib/content_map_page.go
hugolib/hugo_sites.go
hugolib/hugo_sites_build.go
hugolib/page.go
hugolib/pages_capture.go
hugolib/site.go

index d3044bdd64bfae2c5057e8d65b466e7e004ef201..3ca088985a81d4cb5452cea5bbc4c896eb3b4d58 100644 (file)
@@ -27,6 +27,7 @@ import (
        "sync/atomic"
        "time"
 
+       "github.com/bep/debounce"
        "github.com/bep/simplecobra"
        "github.com/fsnotify/fsnotify"
        "github.com/gohugoio/hugo/common/herrors"
@@ -515,6 +516,14 @@ func (c *hugoBuilder) doWithPublishDirs(f func(sourceFs *filesystems.SourceFiles
        return langCount, nil
 }
 
+func (c *hugoBuilder) progressIntermediate() {
+       terminal.ReportProgress(c.r.StdOut, terminal.ProgressIntermediate, 0)
+}
+
+func (c *hugoBuilder) progressHidden() {
+       terminal.ReportProgress(c.r.StdOut, terminal.ProgressHidden, 0)
+}
+
 func (c *hugoBuilder) fullBuild(noBuildLock bool) error {
        var (
                g         errgroup.Group
@@ -1027,6 +1036,17 @@ func (c *hugoBuilder) hugoTry() *hugolib.HugoSites {
 }
 
 func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error {
+       if terminal.PrintANSIColors(os.Stdout) {
+               defer c.progressHidden()
+               // If the configuration takes a while to load, we want to show some progress.
+               // This is typically loading of external modules.
+               d := debounce.New(500 * time.Millisecond)
+               d(func() {
+                       c.progressIntermediate()
+               })
+               defer d(func() {})
+       }
+
        cfg := config.New()
        cfg.Set("renderToMemory", c.r.renderToMemory)
        watch := c.r.buildWatch || (c.s != nil && c.s.serverWatch)
index fef6efce882304fc833370ef25c3467044dbc83e..853c76ed4ccc81614b98cca3b7b200e66fd323d9 100644 (file)
@@ -16,6 +16,7 @@ package terminal
 
 import (
        "fmt"
+       "io"
        "os"
        "strings"
 
@@ -72,3 +73,27 @@ func doublePercent(str string) string {
 func singlePercent(str string) string {
        return strings.Replace(str, "%%", "%", -1)
 }
+
+type ProgressState int
+
+const (
+       ProgressHidden ProgressState = iota
+       ProgressNormal
+       ProgressError
+       ProgressIntermediate
+       ProgressWarning
+)
+
+// ReportProgress writes OSC 9;4 sequence to w.
+func ReportProgress(w io.Writer, state ProgressState, progress float64) {
+       if progress < 0 {
+               progress = 0.0
+       }
+       if progress > 1 {
+               progress = 1.0
+       }
+
+       pi := int(progress * 100)
+
+       fmt.Fprintf(w, "\033]9;4;%d;%d\007", state, pi)
+}
index 47637d3e8dbb748c69f9ce8289e37aa37b93aea9..e716a8df991bc1f6617083017214dcf5ccf18262 100644 (file)
@@ -1760,6 +1760,13 @@ func (sa *sitePagesAssembler) assembleResources() error {
                                duplicateResourceFiles = ps.s.ContentSpec.Converters.GetMarkupConfig().Goldmark.DuplicateResourceFiles
                        }
 
+                       if !sa.h.isRebuild() {
+                               if ps.hasRenderableOutput() {
+                                       // For multi output pages this will not be complete, but will have to do for now.
+                                       sa.h.buildProgress.numPagesToRender.Add(1)
+                               }
+                       }
+
                        duplicateResourceFiles = duplicateResourceFiles || ps.s.Conf.IsMultihost()
 
                        err := sa.pageMap.forEachResourceInPage(
index a79a77d36a14e9ea556237758db27b16b1755d7e..7caa76481e60b8139034a34cff500a5cd36ee2b5 100644 (file)
@@ -20,6 +20,7 @@ import (
        "strings"
        "sync"
        "sync/atomic"
+       "time"
 
        "github.com/bep/logg"
        "github.com/gohugoio/hugo/cache/dynacache"
@@ -33,9 +34,11 @@ import (
        "github.com/gohugoio/hugo/output"
        "github.com/gohugoio/hugo/parser/metadecoders"
 
+       "github.com/gohugoio/hugo/common/htime"
        "github.com/gohugoio/hugo/common/hugo"
        "github.com/gohugoio/hugo/common/maps"
        "github.com/gohugoio/hugo/common/para"
+       "github.com/gohugoio/hugo/common/terminal"
        "github.com/gohugoio/hugo/common/types"
        "github.com/gohugoio/hugo/hugofs"
 
@@ -98,12 +101,29 @@ type HugoSites struct {
        numWorkersSites int
        numWorkers      int
 
+       buildProgress progressReporter
        *fatalErrorHandler
        *buildCounters
        // Tracks invocations of the Build method.
        buildCounter atomic.Uint64
 }
 
+type progressReporter struct {
+       mu                  sync.Mutex
+       t                   time.Time
+       progress            float64
+       queue               []func(*progressReporter) (state terminal.ProgressState, progress float64)
+       state               terminal.ProgressState
+       renderProgressStart float64
+       numPagesToRender    atomic.Uint64
+}
+
+func (p *progressReporter) Start() {
+       p.mu.Lock()
+       defer p.mu.Unlock()
+       p.t = htime.Now()
+}
+
 // ShouldSkipFileChangeEvent allows skipping filesystem event early before
 // the build is started.
 func (h *HugoSites) ShouldSkipFileChangeEvent(ev fsnotify.Event) bool {
@@ -254,6 +274,52 @@ func (h *HugoSites) codeownersForPage(p page.Page) ([]string, error) {
        return h.codeownerInfo.forPage(p), nil
 }
 
+func (h *HugoSites) reportProgress(f func(*progressReporter) (state terminal.ProgressState, progress float64)) {
+       h.buildProgress.mu.Lock()
+       defer h.buildProgress.mu.Unlock()
+
+       if h.buildProgress.t.IsZero() {
+               // Not started yet, queue it up and return.
+               h.buildProgress.queue = append(h.buildProgress.queue, f)
+               return
+       }
+
+       handleOne := func(ff func(*progressReporter) (state terminal.ProgressState, progress float64)) {
+               state, progress := ff(&h.buildProgress)
+
+               if h.buildProgress.progress > 0 && h.buildProgress.state == state && progress <= h.buildProgress.progress {
+                       // Only report progress forward.
+                       return
+               }
+
+               h.buildProgress.state = state
+               h.buildProgress.progress = progress
+               terminal.ReportProgress(h.Log.StdOut(), state, h.buildProgress.progress)
+       }
+
+       // Drain queue first.
+       for _, ff := range h.buildProgress.queue {
+               handleOne(ff)
+       }
+       h.buildProgress.queue = nil
+
+       handleOne(f)
+}
+
+func (h *HugoSites) onPageRender() {
+       pagesRendered := h.buildCounters.pageRenderCounter.Add(1)
+       if pagesRendered <= 100 || pagesRendered%10 == 0 {
+               h.reportProgress(func(pr *progressReporter) (terminal.ProgressState, float64) {
+                       if pr.renderProgressStart == 0.0 && pr.state == terminal.ProgressNormal {
+                               pr.renderProgressStart = h.buildProgress.progress
+                       }
+                       numPagesToRender := pr.numPagesToRender.Load()
+                       pagesProgress := pr.renderProgressStart + float64(pagesRendered)/float64(numPagesToRender)*(1.0-pr.renderProgressStart)
+                       return terminal.ProgressNormal, pagesProgress
+               })
+       }
+}
+
 func (h *HugoSites) pickOneAndLogTheRest(errors []error) error {
        if len(errors) == 0 {
                return nil
index 70c806b78a330edfb72e78112c8b5aa8185d960e..320a8266474efec28bd68005314189ac64fcae47 100644 (file)
@@ -25,6 +25,7 @@ import (
        "strings"
        "time"
 
+       "github.com/bep/debounce"
        "github.com/bep/logg"
        "github.com/gohugoio/hugo/bufferpool"
        "github.com/gohugoio/hugo/deps"
@@ -45,6 +46,7 @@ import (
        "github.com/gohugoio/hugo/common/para"
        "github.com/gohugoio/hugo/common/paths"
        "github.com/gohugoio/hugo/common/rungroup"
+       "github.com/gohugoio/hugo/common/terminal"
        "github.com/gohugoio/hugo/config"
        "github.com/gohugoio/hugo/resources/page"
        "github.com/gohugoio/hugo/resources/page/siteidentities"
@@ -58,9 +60,25 @@ import (
 // Build builds all sites. If filesystem events are provided,
 // this is considered to be a potential partial rebuild.
 func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error {
+       if !h.isRebuild() && terminal.PrintANSIColors(os.Stdout) {
+               // Don't show progress for fast builds.
+               d := debounce.New(250 * time.Millisecond)
+               d(func() {
+                       h.buildProgress.Start()
+                       h.reportProgress(func(*progressReporter) (state terminal.ProgressState, progress float64) {
+                               // We don't know how many files to process below, so use the intermediate state as the first progress.
+                               return terminal.ProgressIntermediate, 1.0
+                       })
+               })
+               defer d(func() {})
+       }
+
        infol := h.Log.InfoCommand("build")
        defer loggers.TimeTrackf(infol, time.Now(), nil, "")
        defer func() {
+               h.reportProgress(func(*progressReporter) (state terminal.ProgressState, progress float64) {
+                       return terminal.ProgressHidden, 1.0
+               })
                h.buildCounter.Add(1)
        }()
 
@@ -148,10 +166,15 @@ func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error {
                        if err := h.process(ctx, infol, conf, init, events...); err != nil {
                                return fmt.Errorf("process: %w", err)
                        }
-
+                       h.reportProgress(func(*progressReporter) (state terminal.ProgressState, progress float64) {
+                               return terminal.ProgressNormal, 0.2
+                       })
                        if err := h.assemble(ctx, infol, conf); err != nil {
                                return fmt.Errorf("assemble: %w", err)
                        }
+                       h.reportProgress(func(*progressReporter) (state terminal.ProgressState, progress float64) {
+                               return terminal.ProgressNormal, 0.25
+                       })
 
                        return nil
                }
index 7ffb7cdfbb8f17f339295852d2d724b4c17e954e..9e7ec22c3356932ee84b614c649703a0336e19d3 100644 (file)
@@ -113,6 +113,17 @@ type pageState struct {
        dependencyManager identity.Manager
 }
 
+// This is not accurate and only used for progress reporting.
+// We can do better, but this will do for now.
+func (p *pageState) hasRenderableOutput() bool {
+       for _, po := range p.pageOutputs {
+               if po.render {
+                       return true
+               }
+       }
+       return false
+}
+
 func (p *pageState) incrPageOutputTemplateVariation() {
        p.pageOutputTemplateVariationsState.Add(1)
 }
index 91f3afae4fbd002360d74678a0b1f2dd33d1af19..bd9cd0f8ec9d0a0375013edf4f175830d5edce69 100644 (file)
@@ -118,7 +118,7 @@ func (c *pagesCollector) Collect() (collectErr error) {
                logFilesProcessed(true)
        }()
 
-       c.g = rungroup.Run[hugofs.FileMetaInfo](c.ctx, rungroup.Config[hugofs.FileMetaInfo]{
+       c.g = rungroup.Run(c.ctx, rungroup.Config[hugofs.FileMetaInfo]{
                NumWorkers: numWorkers,
                Handle: func(ctx context.Context, fi hugofs.FileMetaInfo) error {
                        numPages, numResources, err := c.m.AddFi(fi, c.buildConfig)
index e37dbd5ef7a970d2abfa1348336eb1b9efce58d4..73067bb6ccd6f761d5c5246765e4cc60caefc6dd 100644 (file)
@@ -1440,7 +1440,7 @@ const (
 )
 
 func (s *Site) renderAndWritePage(statCounter *uint64, name string, targetPath string, p *pageState, d any, templ *tplimpl.TemplInfo) error {
-       s.h.buildCounters.pageRenderCounter.Add(1)
+       s.h.onPageRender()
        renderBuffer := bp.GetBuffer()
        defer bp.PutBuffer(renderBuffer)