Skip to content

add ProgressIndicatorSpinner, for work of unknown duration - #10081

Merged
ThomasWaldmann merged 1 commit into
borgbackup:masterfrom
ThomasWaldmann:spinner
Aug 11, 2026
Merged

add ProgressIndicatorSpinner, for work of unknown duration#10081
ThomasWaldmann merged 1 commit into
borgbackup:masterfrom
ThomasWaldmann:spinner

Conversation

@ThomasWaldmann

@ThomasWaldmann ThomasWaldmann commented Aug 11, 2026

Copy link
Copy Markdown
Member

The other progress indicators either count (ProgressIndicatorPercent) or just emit a message (ProgressIndicatorMessage). For work whose duration or size is not known in advance, neither fits: what we want to show is "borg is still working, at this" — and that is a spinner.

with ProgressIndicatorSpinner("Deduplicating", msgid="create") as spinner:
    for chunk in chunks:
        process(chunk)
        spinner.show()

How it behaves

  • Pull-based on purpose: no thread, no timer, nothing runs while the caller is not calling. show() is rate limited by BORG_PROGRESS_FPS internally, so calling it a million times a second costs one time.monotonic() per call.
  • On a terminal: the frame and the message are repainted in place, in borg green (#22D045 on truecolor terminals, else the terminal's own bright green — the nearest xterm-256 entry drops the red channel and reads too acid). The message is truncated to the terminal width, because a wrapped line could not be repainted in place. The cursor is hidden at the first paint and switched back on in finish().
  • Without a terminal, or with --log-json: an animation would be pointless, so nothing is repainted — only message changes are logged, as progress_message, exactly like ProgressIndicatorMessage does it. So a logfile gets one line per phase rather than one per frame, and frontends need no changes.
  • --quiet silences it, like the other indicators.

Frames

All frames are East Asian Width "Neutral", so they occupy exactly one cell and the message after the spinner never shifts column:

name frames
square (default) ▫ ▪ ◻ ◼ pulsing: small to medium, hollow to solid
cube ◰ ◳ ◲ ◱ quadrant rotating clockwise
boxed ⊞ ⊠ ⊟ ⊡ squared plus/times/minus/dot
ascii | / - \ the classic

If the output encoding can not represent the fancy frames, the ASCII ones are used automatically.

New environment variable

BORG_SPINNER=off never animates, BORG_SPINNER=ascii forces the ASCII frames. Documented in the environment help topic (and thus in the docs). Colour follows the usual NO_COLOR / COLORTERM conventions.

Notes

  • No caller yet — this adds the indicator only. The existing indefinite-duration spots (e.g. Cache.close()) are single-call phases where nothing would animate; wiring it up where there is a real loop is a separate change.
  • Tests cover animation, frame sets and fallbacks, colour, rate limiting, truncation, --quiet, non-tty logging, and a stream that goes away mid-run.

The other progress indicators either count (percent) or just emit a message.
For work whose duration or size is not known in advance, neither fits: what we
want to show is "borg is still working, at this", and that is a spinner.

It is pull-based on purpose: no thread, no timer, nothing runs while the caller
is not calling. show() is rate limited by BORG_PROGRESS_FPS, so calling it very
often is cheap.

On a terminal, the frame and the message are repainted in place, in borg green.
Without a terminal (or with --log-json) there is nothing to animate, so only
message changes are logged, as "progress_message" - same as
ProgressIndicatorMessage does it, so frontends need no changes.

The frames are all East Asian Width "Neutral", so they take exactly one cell
and the message never shifts column. If the output encoding can not represent
them (or BORG_SPINNER=ascii), the classic ASCII frames are used instead.
BORG_SPINNER=off never animates.
@ThomasWaldmann

ThomasWaldmann commented Aug 11, 2026

Copy link
Copy Markdown
Member Author

@mr-raj12 use it where appropriate.

I think it can be used to give life-signs at a finer granularity than what we had yet (because it stays in one place, it is fps rate limited). No more "is borg hanging?" questions...

@codecov

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.05825% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.66%. Comparing base (4f861a3) to head (acf222a).
⚠️ Report is 9 commits behind head on master.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/borg/helpers/progress.py 98.03% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #10081      +/-   ##
==========================================
- Coverage   86.70%   86.66%   -0.04%     
==========================================
  Files          98       98              
  Lines       17174    17275     +101     
  Branches     2609     2621      +12     
==========================================
+ Hits        14890    14971      +81     
- Misses       1586     1604      +18     
- Partials      698      700       +2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@ThomasWaldmann
ThomasWaldmann merged commit 91129b2 into borgbackup:master Aug 11, 2026
18 of 20 checks passed
@ThomasWaldmann
ThomasWaldmann deleted the spinner branch August 11, 2026 23:20
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.

1 participant