As supported by the Ghostty terminal and others.
"sync/atomic"
"time"
+ "github.com/bep/debounce"
"github.com/bep/simplecobra"
"github.com/fsnotify/fsnotify"
"github.com/gohugoio/hugo/common/herrors"
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
}
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)
import (
"fmt"
+ "io"
"os"
"strings"
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)
+}
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(
"strings"
"sync"
"sync/atomic"
+ "time"
"github.com/bep/logg"
"github.com/gohugoio/hugo/cache/dynacache"
"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"
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 {
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
"strings"
"time"
+ "github.com/bep/debounce"
"github.com/bep/logg"
"github.com/gohugoio/hugo/bufferpool"
"github.com/gohugoio/hugo/deps"
"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"
// 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)
}()
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
}
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)
}
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)
)
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)