Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Bar

Bar creates a determinate progress bar that shows filled/empty cells and a live percentage. Use SetProgress on the Update to advance the bar.

Bar demo

err := clog.Bar("Downloading", 100).
  Str("file", "release.tar.gz").
  Elapsed("elapsed").
  Progress(ctx, func(ctx context.Context, p *clog.Update) error {
    for i := range 101 {
      p.SetProgress(i).Msg("Downloading").Send()
      time.Sleep(20 * time.Millisecond)
    }
    return nil
  }).
  Symbol("✅").
  Msg("Download complete")
// INF ⏳ Downloading ━━━━━━━━━╸╺━━━━━━━━━━━ 42% elapsed=1.2s
// INF ✅ Download complete file=release.tar.gz elapsed=3.4s

SetTotal can be called mid-task to update the denominator if the total becomes known after the task starts:

p.SetProgress(50).SetTotal(200).Msg("Processing").Send()
p.Msgf("Processing %d/%d", current, total).Send()

AddTotal atomically adds to the total - useful for “discovered more work” patterns where the full scope isn’t known upfront:

p.AddTotal(50)  // discovered 50 more items
p.AddTotal(-10) // 10 items turned out to be duplicates

SetSymbol changes the icon displayed beside the message during animation. This is useful for showing different phases of a multi-stage task:

p.SetSymbol("📡").SetProgress(i).Str("stage", "receiving").Send()
p.SetSymbol("🔍").SetProgress(i).Str("stage", "resolving").Send()

SetLevel overrides the log level shown on the final done line. This is useful when a task fails and you want the completed line to render at error level:

p.SetSymbol("❌").SetLevel(clog.LevelError).Msg("Failed").Send()

Animated Symbol

By default, bars show a static symbol (⏳). Use .Spinner() to replace it with a spinning animation that cycles independently of the bar progress:

clog.Bar("Cloning", 100, opts...).
  Spinner().
  Progress(ctx, task).
  Msg("Cloned")

// With a custom spinner style
clog.Bar("Downloading", total).
  Spinner(spinner.WithStyle(spinner.Dots)).
  Progress(ctx, task).
  Msg("Downloaded")

With no arguments, .Spinner() uses the default spinner style (moon phases). Pass spinner.Option values to customise the frames, interval, or playback direction. See Spinner for the full list of options and presets.

The spinner animation is driven by wall-clock time, so it keeps animating even when the bar progress is stalled.

Pending Mode

If you don’t want zero-progress tasks to show an empty bar, use PendingHide:

clog.Bar("Cloning", 1, bar.WithPendingMode(bar.PendingHide))

This suppresses the entire bar block, including widgets, until progress becomes positive.

By default, the bar fill uses exponential smoothing (SmoothEase) so that large jumps in progress animate smoothly instead of snapping. Disable it with SmoothNone, or tune the time constant with WithSmoothingTau (default 120ms - smaller is snappier, larger is smoother):

clog.Bar("Uploading", total, bar.WithSmoothingMode(bar.SmoothNone))
clog.Bar("Uploading", total, bar.WithSmoothingTau(100*time.Millisecond))

If frequent updates make ETA or rate text too jumpy, coalesce timing-derived widget and dynamic-field updates:

clog.Bar("Cloning", 1, bar.WithUpdateInterval(time.Second))

This rate-limits timing-derived text such as ETA, rate, and elapsed while leaving the bar fill, percent/current-total text, message, symbol, and other live updates responsive.

Styles

Seven pre-built styles are available in fx/bar/presets.go. Pass any of them via bar.WithStyle():

PresetCharactersDescription
bar.Basic[=====> ]ASCII-only for maximum compatibility
bar.Braille[⣿⣿⣿⣿⣿⣦⠀⠀⠀⠀]Braille dot-fill with 8x sub-cell resolution
bar.Dash[----- ]Simple dash fill
bar.Thin━━━━━╸╺━━━━━Box-drawing with color-differentiated fill (default)
bar.Block│█████░░░░░│Solid block characters
bar.Gradient│██████▍ │Block elements with 8x sub-cell resolution
bar.Smooth█████████████Solid blocks with color-differentiated fill

Bar styles

clog.Bar("Uploading", total, bar.WithStyle(bar.Smooth)).
  Progress(ctx, task).
  Msg("Done")

bar.Thin and bar.Smooth use half-cell resolution via HalfFilled (and HalfEmpty for bar.Thin), giving twice the visual granularity of full-cell styles. bar.Gradient and bar.Braille use GradientFill for 8x sub-cell resolution - the smoothest built-in options. bar.Braille is inspired by Docker Compose’s progress display, using braille dot characters that progressively fill from bottom to top.

Custom Style

Build a fully custom style by passing a bar.WithStyle() option:

import "github.com/gechr/clog/fx/bar/widget"

clog.Bar("Uploading", total, bar.WithStyle(bar.Style{
    Placement:   bar.PlaceInline, // inline with message (default: bar.PlaceRightPad)

    CapStyle:    new(lipgloss.NewStyle().Bold(true)), // style for [ ] caps (default: bold white)
    CapLeft:     "|",
    CapRight:    "|",

    CharEmpty:   '-',
    CharFill:    '=',
    CharHead:    '>', // decorative head at leading edge (0 = disabled)

    HalfEmpty:   0, // half-cell trailing edge for 2x resolution (0 = disabled)
    HalfFilled:  0, // half-cell leading edge for 2x resolution (0 = disabled)

    Separator:   " ", // separator between message, bar, and widget text

    StyleEmpty:  new(lipgloss.NewStyle().Foreground(lipgloss.Color("8"))),  // grey
    StyleFill:   new(lipgloss.NewStyle().Foreground(lipgloss.Color("2"))),  // green

    WidgetLeft:  widget.Percent(widget.WithDigits(1)), // "50.0%" to the left of the bar
    WidgetRight: widget.None(), // suppress the default right-side percent

    Width:       30, // fixed inner width (0 = auto-size from terminal)
    WidthMin:    10, // auto-size minimum (default 10)
    WidthMax:    40, // auto-size maximum (default 40)
  })).
  Progress(ctx, task).
  Msg("Done")

When Width is 0, the bar auto-sizes to one quarter of the terminal width, clamped to [WidthMin, WidthMax].

Any field can be overridden via option functions without building a full custom style:

clog.Bar("Downloading", 100, bar.WithStyle(bar.Braille), bar.WithWidth(30))

Structure

OptionDescription
bar.WithCapLeft(s)Left cap string (e.g. "[", "│")
bar.WithCapRight(s)Right cap string (e.g. "]", "│")
bar.WithCapStyle(ls)Lipgloss style for caps (nil = unstyled)
bar.WithSeparator(s)String between message, bar, and widgets

Characters

OptionDescription
bar.WithCharEmpty(r)Rune for fully empty cells
bar.WithCharFill(r)Rune for fully filled cells
bar.WithCharHead(r)Decorative rune at leading edge (1x only; 0 = off)
bar.WithHalfFilled(r)Leading-edge rune for 2x resolution (0 = off)
bar.WithHalfEmpty(r)Trailing-edge rune for 2x resolution (0 = off)
bar.WithGradientFill(rs)Sub-cell runes (least→most filled) for Nx resolution

Width

OptionDescription
bar.WithWidth(n)Fixed inner width (0 = auto-size)
bar.WithMinWidth(n)Minimum auto-sized width (default 10)
bar.WithMaxWidth(n)Maximum auto-sized width (default 40)

Colors

OptionDescription
bar.WithStyleFill(ls)Lipgloss style for filled cells
bar.WithStyleEmpty(ls)Lipgloss style for empty cells
bar.WithProgressGradient(...)Color fill by progress (overrides StyleFill)

Widgets

OptionDescription
bar.WithWidgetLeft(w)Widget displayed to the left of the bar
bar.WithWidgetRight(w)Widget displayed to the right of the bar
bar.WithPlacement(p)Horizontal bar placement mode
bar.WithPendingMode(m)Whether to render the bar before progress starts
bar.WithSmoothingMode(m)Bar fill smoothing (SmoothEase default, SmoothNone to disable)
bar.WithSmoothingTau(d)Exponential decay time constant (default 120ms; smaller = snappier)
bar.WithUpdateInterval(d)Coalesce ETA/rate/elapsed-style text updates to at most once per duration

All presets use bold white CapStyle. Pass bar.WithCapStyle(nil) for unstyled caps.

Progress Gradient

Color the bar fill based on progress using ProgressGradient. The filled portion shifts through the gradient as progress advances (e.g. red at 0%, yellow at 50%, green at 100%):

style := bar.Block
style.ProgressGradient = bar.DefaultGradient() // red → yellow → green

clog.Bar("Building", 100, bar.WithStyle(style)).
  Progress(ctx, task).
  Msg("Built")

Custom gradients work the same as other gradient fields:

style.ProgressGradient = []style.ColorStop{
  {Position: 0, Color: colorful.Color{R: 0.3, G: 0.3, B: 1}},   // blue
  {Position: 0.5, Color: colorful.Color{R: 1, G: 1, B: 1}},     // white
  {Position: 1, Color: colorful.Color{R: 0.3, G: 1, B: 0.3}},   // green
}

When set, ProgressGradient overrides the StyleFill foreground color. Use bar.DefaultGradient() to get the default red → yellow → green stops.

Alignment

The Placement field on bar.Style controls where the bar appears on the line:

ConstantLayout
bar.PlaceRightPadINF ⏳ Downloading ━━━━━━━╸╺━━━━━━━━━━ 45% (default)
bar.PlaceLeftPadINF ⏳ ━━━━━━━╸╺━━━━━━━━━━ 45% Downloading
bar.PlaceInlineINF ⏳ Downloading ━━━━━━━╸╺━━━━━━━━━━ 45%
bar.PlaceRightINF ⏳ Downloading ━━━━━━━╸╺━━━━━━━━━━ 45%
bar.PlaceLeftINF ⏳ ━━━━━━━╸╺━━━━━━━━━━ 45% Downloading
bar.PlaceAlignedPads messages in a group so all bars start at the same column

The padded variants (bar.PlaceRightPad, bar.PlaceLeftPad) fill the gap between message and bar with spaces to span the terminal width. When the terminal is too narrow, they fall back to the Separator between parts.

bar.PlaceAligned dynamically calculates the longest message in a group and aligns all bars to the same starting column. For standalone (non-group) bars, it falls back to bar.PlaceRight.

Widgets

WidgetLeft and WidgetRight on bar.Style control text annotations beside the bar. Each is a bar.Widget - a callback that receives progress state and returns a string:

type bar.State struct {
  Current int
  Total   int
  Elapsed time.Duration
  Rate    float64 // items per second
}
type bar.Widget func(bar.State) string

All presets set WidgetRight: widget.Percent() - padded percentage on the right (e.g. " 42%"). When both widgets are nil, the same default applies.

Built-in widgets:

WidgetDescription
widget.Percent(...)Padded percentage (use widget.WithDigits(n) for decimal places)
widget.Bytes()SI byte progress (e.g. " 50 MB / 100 MB", base-1000)
widget.IBytes()IEC byte progress (e.g. "50 MiB / 100 MiB", base-1024)
widget.ETA()Estimated time remaining (e.g. "ETA 2m30s", "ETA ∞")
widget.Rate()Items per second (e.g. "150/s", "1.5k/s")
widget.BytesRate()SI byte throughput (e.g. "82.9 MB/s")
widget.IBytesRate()IEC byte throughput (e.g. "82.9 MiB/s")
widget.None()Always returns “” - suppresses default percent
widget.Separator(s)Always renders s - use as a divider inside widget.Widgets

All widget constructors accept widget.Option values:

OptionApplies toDescription
widget.WithDigits(n)widget.Percent, widget.Bytes, widget.IBytes, widget.BytesRate, widget.IBytesRatePrecision: significant digits or decimal places
widget.WithPrefix(s)widget.ETAOverride prefix (e.g. """2m30s", "~""~2m30s")
widget.WithUnit(label)widget.RateUnit label (e.g. "ops""150 ops/s")
widget.WithStyle(s)all widgetsLipgloss style applied to the widget’s output
widget.WithMinimumPercent(pct)widget.PercentHides widget when progress is below pct (e.g. 1 hides 0%)
widget.WithProgressGradient(...)widget.PercentColors widget text based on progress (overrides WithStyle)

Composing multiple widgets:

Use widget.Widgets to combine several widgets on a single side. Empty outputs are filtered and the rest are joined with a space:

style := bar.Thin
style.WidgetRight = widget.Widgets(widget.ETA(), widget.Rate())
// INF ⏳ Processing ━━━━━━━╸╺━━━━━━━━━━ ETA 2m30s 150/s

Add a visual divider with widget.Separator:

style.WidgetRight = widget.Widgets(
  widget.ETA(),
  widget.Separator("│"),
  widget.Rate(),
)
// INF ⏳ Processing ━━━━━━━╸╺━━━━━━━━━━ ETA 2m30s │ 150/s

Pass widget.WithStyle to any widget to apply a Lipgloss style to its output:

faint := new(lipgloss.NewStyle().Faint(true))
style.WidgetRight = widget.Widgets(
  widget.ETA(widget.WithStyle(faint)),
  widget.Separator("│", widget.WithStyle(faint)),
  widget.Rate(widget.WithStyle(faint)),
)
// INF ⏳ Processing ━━━━━━━╸╺━━━━━━━━━━ ETA 2m30s │ 150/s  (all rendered faint)

Pass widget.WithProgressGradient to color the percent text based on progress, matching the bar’s gradient:

style := bar.Smooth
style.ProgressGradient = bar.DefaultGradient()
style.WidgetLeft = widget.Percent(widget.WithProgressGradient(bar.DefaultGradient()...))
style.WidgetRight = widget.None()
// INF ⏳ Uploading │████▌     │  50%  (percent colored red→yellow→green by progress)

Move percent to the left:

style := bar.Thin
style.WidgetLeft = widget.Percent()
style.WidgetRight = widget.None()

Download progress with byte sizes:

fileSize := 150 * 1000 * 1000 // 150 MB
style := bar.Smooth
style.WidgetRight = widget.Bytes()

clog.Bar("Downloading", fileSize, bar.WithStyle(style)).
  Str("file", "model.bin").
  Progress(ctx, func(ctx context.Context, p *clog.Update) error {
    // p.SetProgress(bytesReceived).Send()
  }).
  Msg("Downloaded")
// INF ⏳ Downloading │████▌     │  75 MB / 150 MB file=model.bin

The current value is right-aligned to the total’s width to prevent the bar from jumping as digits change. Use widget.IBytes() for base-1024 units (KiB, MiB, GiB).

ETA and rate:

style := bar.Block
style.WidgetLeft = widget.ETA()
style.WidgetRight = widget.Rate(widget.WithUnit("items"))

clog.Bar("Processing", 500, bar.WithStyle(style)).
  Progress(ctx, task).
  Msg("Done")
// INF ⏳ Processing ETA 2m30s │█████░░░░░│ 150 items/s

widget.ETA shows "ETA ∞" before any progress, a countdown during the task, and "" when complete. widget.Rate accepts an optional widget.WithUnit for a label; without it, output is "150/s".

Byte throughput:

style := bar.Gradient
style.WidgetRight = widget.BytesRate()

clog.Bar("Uploading", totalBytes, bar.WithStyle(style)).
  Progress(ctx, task).
  Msg("Done")
// INF ⏳ Uploading │██████▍   │ 82.9 MB/s

Use widget.IBytesRate() for IEC units (MiB/s). Both accept widget.WithDigits(n) to control significant digits (default 3).

Custom widget:

style := bar.Thin
style.WidgetRight = func(s bar.State) string {
  remaining := s.Total - s.Current
  return fmt.Sprintf("%d remaining", remaining)
}

Suppress percent entirely:

style := bar.Thin
style.WidgetRight = widget.None()

Percentage as a Field

Use .BarPercent(key) on the builder to move the percentage into structured fields. This suppresses the default right-side widget:

clog.Bar("Installing", 100).
  BarPercent("progress").
  Elapsed("elapsed").
  Progress(ctx, task).
  Msg("Installed")
// INF ⏳ Installing          ━━━━━━━╸╺━━━━━━━━━━ progress=45% elapsed=1.2s

All animations gracefully degrade: when colors are disabled (CI, piped output), a static status line with an ⏳ symbol is printed instead.

The icon displayed during Pulse, Shimmer, and Bar animations defaults to ⏳ and can be changed with .Symbol() on the builder (before the task starts) or SetSymbol() on the Update (during the task):

clog.Pulse("Warming up").
  Symbol("🔄").
  Wait(ctx, action).
  Msg("Ready")