From: Bjørn Erik Pedersen Date: Tue, 30 Sep 2025 09:43:26 +0000 (+0200) Subject: Report OSC 9;4 progress when building X-Git-Url: http://git.maquefel.me/?a=commitdiff_plain;h=ec463c09772e09f01b70fa16f517c5b72f6eda67;p=brevno-suite%2Fhugo Report OSC 9;4 progress when building As supported by the Ghostty terminal and others. --- diff --git a/commands/hugobuilder.go b/commands/hugobuilder.go index d3044bdd6..3ca088985 100644 --- a/commands/hugobuilder.go +++ b/commands/hugobuilder.go @@ -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) diff --git a/common/terminal/colors.go b/common/terminal/colors.go index fef6efce8..853c76ed4 100644 --- a/common/terminal/colors.go +++ b/common/terminal/colors.go @@ -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) +} diff --git a/hugolib/content_map_page.go b/hugolib/content_map_page.go index 47637d3e8..e716a8df9 100644 --- a/hugolib/content_map_page.go +++ b/hugolib/content_map_page.go @@ -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( diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go index a79a77d36..7caa76481 100644 --- a/hugolib/hugo_sites.go +++ b/hugolib/hugo_sites.go @@ -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 diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go index 70c806b78..320a82664 100644 --- a/hugolib/hugo_sites_build.go +++ b/hugolib/hugo_sites_build.go @@ -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 } diff --git a/hugolib/page.go b/hugolib/page.go index 7ffb7cdfb..9e7ec22c3 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -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) } diff --git a/hugolib/pages_capture.go b/hugolib/pages_capture.go index 91f3afae4..bd9cd0f8e 100644 --- a/hugolib/pages_capture.go +++ b/hugolib/pages_capture.go @@ -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) diff --git a/hugolib/site.go b/hugolib/site.go index e37dbd5ef..73067bb6c 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -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)