Skip to content

Commit ec463c0

Browse files
committed
Report OSC 9;4 progress when building
As supported by the Ghostty terminal and others.
1 parent 4d2743e commit ec463c0

File tree

8 files changed

+155
-3
lines changed

8 files changed

+155
-3
lines changed

‎commands/hugobuilder.go‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"sync/atomic"
2828
"time"
2929

30+
"github.com/bep/debounce"
3031
"github.com/bep/simplecobra"
3132
"github.com/fsnotify/fsnotify"
3233
"github.com/gohugoio/hugo/common/herrors"
@@ -515,6 +516,14 @@ func (c *hugoBuilder) doWithPublishDirs(f func(sourceFs *filesystems.SourceFiles
515516
return langCount, nil
516517
}
517518

519+
func (c *hugoBuilder) progressIntermediate() {
520+
terminal.ReportProgress(c.r.StdOut, terminal.ProgressIntermediate, 0)
521+
}
522+
523+
func (c *hugoBuilder) progressHidden() {
524+
terminal.ReportProgress(c.r.StdOut, terminal.ProgressHidden, 0)
525+
}
526+
518527
func (c *hugoBuilder) fullBuild(noBuildLock bool) error {
519528
var (
520529
g errgroup.Group
@@ -1027,6 +1036,17 @@ func (c *hugoBuilder) hugoTry() *hugolib.HugoSites {
10271036
}
10281037

10291038
func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error {
1039+
if terminal.PrintANSIColors(os.Stdout) {
1040+
defer c.progressHidden()
1041+
// If the configuration takes a while to load, we want to show some progress.
1042+
// This is typically loading of external modules.
1043+
d := debounce.New(500 * time.Millisecond)
1044+
d(func() {
1045+
c.progressIntermediate()
1046+
})
1047+
defer d(func() {})
1048+
}
1049+
10301050
cfg := config.New()
10311051
cfg.Set("renderToMemory", c.r.renderToMemory)
10321052
watch := c.r.buildWatch || (c.s != nil && c.s.serverWatch)

‎common/terminal/colors.go‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package terminal
1616

1717
import (
1818
"fmt"
19+
"io"
1920
"os"
2021
"strings"
2122

@@ -72,3 +73,27 @@ func doublePercent(str string) string {
7273
func singlePercent(str string) string {
7374
return strings.Replace(str, "%%", "%", -1)
7475
}
76+
77+
type ProgressState int
78+
79+
const (
80+
ProgressHidden ProgressState = iota
81+
ProgressNormal
82+
ProgressError
83+
ProgressIntermediate
84+
ProgressWarning
85+
)
86+
87+
// ReportProgress writes OSC 9;4 sequence to w.
88+
func ReportProgress(w io.Writer, state ProgressState, progress float64) {
89+
if progress < 0 {
90+
progress = 0.0
91+
}
92+
if progress > 1 {
93+
progress = 1.0
94+
}
95+
96+
pi := int(progress * 100)
97+
98+
fmt.Fprintf(w, "\033]9;4;%d;%d\007", state, pi)
99+
}

‎hugolib/content_map_page.go‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1760,6 +1760,13 @@ func (sa *sitePagesAssembler) assembleResources() error {
17601760
duplicateResourceFiles = ps.s.ContentSpec.Converters.GetMarkupConfig().Goldmark.DuplicateResourceFiles
17611761
}
17621762

1763+
if !sa.h.isRebuild() {
1764+
if ps.hasRenderableOutput() {
1765+
// For multi output pages this will not be complete, but will have to do for now.
1766+
sa.h.buildProgress.numPagesToRender.Add(1)
1767+
}
1768+
}
1769+
17631770
duplicateResourceFiles = duplicateResourceFiles || ps.s.Conf.IsMultihost()
17641771

17651772
err := sa.pageMap.forEachResourceInPage(

‎hugolib/hugo_sites.go‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"strings"
2121
"sync"
2222
"sync/atomic"
23+
"time"
2324

2425
"github.com/bep/logg"
2526
"github.com/gohugoio/hugo/cache/dynacache"
@@ -33,9 +34,11 @@ import (
3334
"github.com/gohugoio/hugo/output"
3435
"github.com/gohugoio/hugo/parser/metadecoders"
3536

37+
"github.com/gohugoio/hugo/common/htime"
3638
"github.com/gohugoio/hugo/common/hugo"
3739
"github.com/gohugoio/hugo/common/maps"
3840
"github.com/gohugoio/hugo/common/para"
41+
"github.com/gohugoio/hugo/common/terminal"
3942
"github.com/gohugoio/hugo/common/types"
4043
"github.com/gohugoio/hugo/hugofs"
4144

@@ -98,12 +101,29 @@ type HugoSites struct {
98101
numWorkersSites int
99102
numWorkers int
100103

104+
buildProgress progressReporter
101105
*fatalErrorHandler
102106
*buildCounters
103107
// Tracks invocations of the Build method.
104108
buildCounter atomic.Uint64
105109
}
106110

111+
type progressReporter struct {
112+
mu sync.Mutex
113+
t time.Time
114+
progress float64
115+
queue []func(*progressReporter) (state terminal.ProgressState, progress float64)
116+
state terminal.ProgressState
117+
renderProgressStart float64
118+
numPagesToRender atomic.Uint64
119+
}
120+
121+
func (p *progressReporter) Start() {
122+
p.mu.Lock()
123+
defer p.mu.Unlock()
124+
p.t = htime.Now()
125+
}
126+
107127
// ShouldSkipFileChangeEvent allows skipping filesystem event early before
108128
// the build is started.
109129
func (h *HugoSites) ShouldSkipFileChangeEvent(ev fsnotify.Event) bool {
@@ -254,6 +274,52 @@ func (h *HugoSites) codeownersForPage(p page.Page) ([]string, error) {
254274
return h.codeownerInfo.forPage(p), nil
255275
}
256276

277+
func (h *HugoSites) reportProgress(f func(*progressReporter) (state terminal.ProgressState, progress float64)) {
278+
h.buildProgress.mu.Lock()
279+
defer h.buildProgress.mu.Unlock()
280+
281+
if h.buildProgress.t.IsZero() {
282+
// Not started yet, queue it up and return.
283+
h.buildProgress.queue = append(h.buildProgress.queue, f)
284+
return
285+
}
286+
287+
handleOne := func(ff func(*progressReporter) (state terminal.ProgressState, progress float64)) {
288+
state, progress := ff(&h.buildProgress)
289+
290+
if h.buildProgress.progress > 0 && h.buildProgress.state == state && progress <= h.buildProgress.progress {
291+
// Only report progress forward.
292+
return
293+
}
294+
295+
h.buildProgress.state = state
296+
h.buildProgress.progress = progress
297+
terminal.ReportProgress(h.Log.StdOut(), state, h.buildProgress.progress)
298+
}
299+
300+
// Drain queue first.
301+
for _, ff := range h.buildProgress.queue {
302+
handleOne(ff)
303+
}
304+
h.buildProgress.queue = nil
305+
306+
handleOne(f)
307+
}
308+
309+
func (h *HugoSites) onPageRender() {
310+
pagesRendered := h.buildCounters.pageRenderCounter.Add(1)
311+
if pagesRendered <= 100 || pagesRendered%10 == 0 {
312+
h.reportProgress(func(pr *progressReporter) (terminal.ProgressState, float64) {
313+
if pr.renderProgressStart == 0.0 && pr.state == terminal.ProgressNormal {
314+
pr.renderProgressStart = h.buildProgress.progress
315+
}
316+
numPagesToRender := pr.numPagesToRender.Load()
317+
pagesProgress := pr.renderProgressStart + float64(pagesRendered)/float64(numPagesToRender)*(1.0-pr.renderProgressStart)
318+
return terminal.ProgressNormal, pagesProgress
319+
})
320+
}
321+
}
322+
257323
func (h *HugoSites) pickOneAndLogTheRest(errors []error) error {
258324
if len(errors) == 0 {
259325
return nil

‎hugolib/hugo_sites_build.go‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"strings"
2626
"time"
2727

28+
"github.com/bep/debounce"
2829
"github.com/bep/logg"
2930
"github.com/gohugoio/hugo/bufferpool"
3031
"github.com/gohugoio/hugo/deps"
@@ -45,6 +46,7 @@ import (
4546
"github.com/gohugoio/hugo/common/para"
4647
"github.com/gohugoio/hugo/common/paths"
4748
"github.com/gohugoio/hugo/common/rungroup"
49+
"github.com/gohugoio/hugo/common/terminal"
4850
"github.com/gohugoio/hugo/config"
4951
"github.com/gohugoio/hugo/resources/page"
5052
"github.com/gohugoio/hugo/resources/page/siteidentities"
@@ -58,9 +60,25 @@ import (
5860
// Build builds all sites. If filesystem events are provided,
5961
// this is considered to be a potential partial rebuild.
6062
func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error {
63+
if !h.isRebuild() && terminal.PrintANSIColors(os.Stdout) {
64+
// Don't show progress for fast builds.
65+
d := debounce.New(250 * time.Millisecond)
66+
d(func() {
67+
h.buildProgress.Start()
68+
h.reportProgress(func(*progressReporter) (state terminal.ProgressState, progress float64) {
69+
// We don't know how many files to process below, so use the intermediate state as the first progress.
70+
return terminal.ProgressIntermediate, 1.0
71+
})
72+
})
73+
defer d(func() {})
74+
}
75+
6176
infol := h.Log.InfoCommand("build")
6277
defer loggers.TimeTrackf(infol, time.Now(), nil, "")
6378
defer func() {
79+
h.reportProgress(func(*progressReporter) (state terminal.ProgressState, progress float64) {
80+
return terminal.ProgressHidden, 1.0
81+
})
6482
h.buildCounter.Add(1)
6583
}()
6684

@@ -148,10 +166,15 @@ func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error {
148166
if err := h.process(ctx, infol, conf, init, events...); err != nil {
149167
return fmt.Errorf("process: %w", err)
150168
}
151-
169+
h.reportProgress(func(*progressReporter) (state terminal.ProgressState, progress float64) {
170+
return terminal.ProgressNormal, 0.2
171+
})
152172
if err := h.assemble(ctx, infol, conf); err != nil {
153173
return fmt.Errorf("assemble: %w", err)
154174
}
175+
h.reportProgress(func(*progressReporter) (state terminal.ProgressState, progress float64) {
176+
return terminal.ProgressNormal, 0.25
177+
})
155178

156179
return nil
157180
}

‎hugolib/page.go‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,17 @@ type pageState struct {
113113
dependencyManager identity.Manager
114114
}
115115

116+
// This is not accurate and only used for progress reporting.
117+
// We can do better, but this will do for now.
118+
func (p *pageState) hasRenderableOutput() bool {
119+
for _, po := range p.pageOutputs {
120+
if po.render {
121+
return true
122+
}
123+
}
124+
return false
125+
}
126+
116127
func (p *pageState) incrPageOutputTemplateVariation() {
117128
p.pageOutputTemplateVariationsState.Add(1)
118129
}

‎hugolib/pages_capture.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ func (c *pagesCollector) Collect() (collectErr error) {
118118
logFilesProcessed(true)
119119
}()
120120

121-
c.g = rungroup.Run[hugofs.FileMetaInfo](c.ctx, rungroup.Config[hugofs.FileMetaInfo]{
121+
c.g = rungroup.Run(c.ctx, rungroup.Config[hugofs.FileMetaInfo]{
122122
NumWorkers: numWorkers,
123123
Handle: func(ctx context.Context, fi hugofs.FileMetaInfo) error {
124124
numPages, numResources, err := c.m.AddFi(fi, c.buildConfig)

‎hugolib/site.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1440,7 +1440,7 @@ const (
14401440
)
14411441

14421442
func (s *Site) renderAndWritePage(statCounter *uint64, name string, targetPath string, p *pageState, d any, templ *tplimpl.TemplInfo) error {
1443-
s.h.buildCounters.pageRenderCounter.Add(1)
1443+
s.h.onPageRender()
14441444
renderBuffer := bp.GetBuffer()
14451445
defer bp.PutBuffer(renderBuffer)
14461446

0 commit comments

Comments
 (0)