Skip to content

feat: add download progress bar and install spinner - #212

Merged
jm2 merged 10 commits into
google:masterfrom
jm2:progress-ui
Sep 28, 2026
Merged

jm2 merged 10 commits into
google:masterfrom
jm2:progress-ui

Conversation

@jm2

@jm2 jm2 commented Sep 24, 2026

Copy link
Copy Markdown
Member

Interactive googet install/update runs print "Installing X..." and then go silent for the whole download and install, which can take minutes for large MSI/EXE packages. Add a small progress package with a Cabbie-style download bar and an install spinner.

Behavior:

  • When stderr is not a terminal (services, startup scripts, serial console logging via an agent, CI, pipes), output is byte-identical to before: nothing is rendered and installer output goes straight to stdout/stderr.
  • On an interactive terminal, downloads show a two-line bar (Downloading X.goo (30 MiB) / |====---| 42% 12 MiB / 30 MiB [00:04]) and installs show Installing X... / [00:12], ending in done or failed.
  • Installer output is never hidden. While the spinner is active, child process output is passed through live, a complete line at a time, after clearing the spinner line. stdout stays stdout and stderr stays stderr, so redirected output (e.g. googet install foo > install.log) still holds everything the installer printed. An unterminated line is held (up to 4 KiB) so a spinner frame cannot split it, and is flushed when the spinner stops.
  • Output is ASCII-only, identical frames are not re-sent, and redraws are throttled (200 ms bar, 250 ms spinner) to keep serial console logs readable.
  • TERM=dumb and -verbose (INFO logs on stdout) disable rendering.

Controls:

  • New global -no_progress flag.
  • New noprogress: true option in googet.conf for a machine-wide opt-out; an explicit -no_progress=<bool> overrides it.

Also:

  • packageHTTP restarts from byte zero when a Range request is answered with 200 OK (a proxy or server that ignores ranges). Previously the full body was appended to the partial file and the checksum failed. The non-2xx error now includes the HTTP status instead of a nil error.
  • The file-conflict warning is routed through progress.Printf so it is not drawn over the spinner.

Adds golang.org/x/term (v0.45.0, compatible with the existing x/sys v0.47.0) for terminal detection.

Interactive `googet install`/`update` runs print "Installing X..." and then
go silent for the whole download and install, which can take minutes for
large MSI/EXE packages. Add a small `progress` package with a Cabbie-style
download bar and an install spinner.

Behavior:

- When stderr is not a terminal (services, startup scripts, serial console
  logging via an agent, CI, pipes), output is byte-identical to before:
  nothing is rendered and installer output goes straight to stdout/stderr.
- On an interactive terminal, downloads show a two-line bar
  (`Downloading X.goo (30 MiB)` / `|====---| 42%  12 MiB / 30 MiB [00:04]`)
  and installs show `Installing X... / [00:12]`, ending in `done` or
  `failed`.
- Installer output is never hidden. While the spinner is active, child
  process output is passed through live, a complete line at a time, after
  clearing the spinner line. stdout stays stdout and stderr stays stderr, so
  redirected output (e.g. `googet install foo > install.log`) still holds
  everything the installer printed. An unterminated line is held (up to
  4 KiB) so a spinner frame cannot split it, and is flushed when the spinner
  stops.
- Output is ASCII-only, identical frames are not re-sent, and redraws are
  throttled (200 ms bar, 250 ms spinner) to keep serial console logs
  readable.
- TERM=dumb and -verbose (INFO logs on stdout) disable rendering.

Controls:

- New global `-no_progress` flag.
- New `noprogress: true` option in googet.conf for a machine-wide opt-out; an
  explicit `-no_progress=<bool>` overrides it.

Also:

- `packageHTTP` restarts from byte zero when a Range request is answered
  with 200 OK (a proxy or server that ignores ranges). Previously the full
  body was appended to the partial file and the checksum failed. The non-2xx
  error now includes the HTTP status instead of a nil error.
- The file-conflict warning is routed through `progress.Printf` so it is not
  drawn over the spinner.

Adds golang.org/x/term (v0.45.0, compatible with the existing x/sys
v0.47.0) for terminal detection.
@jm2
jm2 requested a review from nguyen-phillip September 28, 2026 13:42
jm2 added 2 commits September 28, 2026 10:16
The merge of master into progress-ui resolved the go.mod conflict by taking
master's file, which dropped the golang.org/x/term requirement that the
progress package imports. Re-add it at v0.46.0, the version matching
master's go 1.26.0 and golang.org/x/sys v0.48.0.

@nguyen-phillip nguyen-phillip left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Comment thread googet.go Outdated
Comment thread progress/progress.go
jm2 added 7 commits September 28, 2026 14:40
Address review feedback that negative knobs are less ergonomic.

- The -no_progress flag is now -progress (default true); -progress=false
  disables the download bar and install spinner.
- The noprogress googet.conf option is now progress (default true). It is
  read into a *bool so an absent key keeps the default instead of turning
  progress off.
- An explicit -progress, either value, still overrides the config file.
A spinner line wider than the terminal wraps, and a carriage return then
only returns to the start of the wrapped row, so every frame stacks on a
new line. Read the stderr width with term.GetSize and cut the title so
the widest spinner line ("... failed [mm:ss]") leaves the last column
free. An unknown width (not a console, or GetSize fails) leaves the
title unchanged.
The bar line is up to 71 columns, so on narrower terminals it wraps and
each redraw leaves a stale row behind. Size the bar to the stderr width,
reserving the widest possible text, and drop the bar cells entirely
below 10 so only the percentage and byte counts are redrawn. An unknown
width keeps the full 35-cell bar.
The comment said child output is captured and only shown on failure.
It is actually passed through a line at a time as it is produced.
An unterminated child output line was held until its newline, 4 KiB, or
the spinner stopping, so an installer that printed a partial line and
then worked silently showed nothing. The spinner tick now writes out a
held line once it has waited partialLineDelay (500ms), bounding the delay
to under a second. The child's own stream stays byte-exact.
Move the googet.conf / -progress / -verbose resolution into wantProgress
and table-test every combination, including an explicit -progress=true
with -verbose.
A panic or future early return could leave the spinner goroutine
redrawing. Stop it in a deferred call; a panic renders "failed".
@jm2
jm2 merged commit 615e103 into google:master Sep 28, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants