Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92d07525c6 |
@@ -770,7 +770,9 @@ Additional requirements:
|
||||
gives way to the spinner. Only when those four alone are too wide, as on a
|
||||
terminal narrower than about 70 columns hours into a scan of millions of
|
||||
files, is the line cut short at the terminal's edge, so the end of the ETA is
|
||||
lost first.
|
||||
lost first. A terminal made narrower while a phase runs gets the same: the
|
||||
next draw erases the line within the new width and fits the line to it, and
|
||||
the display goes on redrawing in place.
|
||||
- When stderr is not a terminal (a pipe, a file, `/dev/null`), do not emit ANSI
|
||||
redraws: print the line the moment each phase starts, every 5 seconds while it
|
||||
runs (whether or not an item has completed, and never more often), and once
|
||||
|
||||
@@ -37,12 +37,12 @@
|
||||
until the first one is returned. On a terminal a phase with an exact total
|
||||
draws its line followed by the library's bar, and any other a spinner followed
|
||||
by the line; every display is drawn again several times a second. No line
|
||||
reaches the terminal's last column, however narrow: a line too wide leaves out
|
||||
its rate, then its bar, keeping the phase, progress, elapsed time and ETA
|
||||
whole, and is cut short only when those alone do not fit, as below about 70
|
||||
columns late in a large scan. In plain mode the line is printed every 5
|
||||
seconds whether or not an item completed (2026-10-16,
|
||||
https://git.eeqj.de/sneak/sfdupes/issues/112)
|
||||
reaches the terminal's last column, however narrow, also after the terminal is
|
||||
made narrower during a phase: a line too wide leaves out its rate, then its
|
||||
bar, keeping the phase, progress, elapsed time and ETA whole, and is cut short
|
||||
only when those alone do not fit, as below about 70 columns late in a large
|
||||
scan. In plain mode the line is printed every 5 seconds whether or not an item
|
||||
completed (2026-10-16, https://git.eeqj.de/sneak/sfdupes/issues/112)
|
||||
|
||||
- `scan`, `report` and `trees` first print a line on stderr naming the database
|
||||
they use, as an absolute path, and whether that path is the default or comes
|
||||
|
||||
+24
-10
@@ -5,6 +5,7 @@ import (
|
||||
"math"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -98,6 +99,7 @@ type progress struct {
|
||||
mu sync.Mutex
|
||||
bar *progressbar.ProgressBar // what the library draws; nil until then
|
||||
withBar bool // bar is the library's bar, not its spinner
|
||||
widest int // the widest line bar has drawn, in columns
|
||||
label string
|
||||
total int64
|
||||
estimate bool
|
||||
@@ -176,15 +178,20 @@ func (p *progress) draw() {
|
||||
text, withBar = p.line(width-barMargin, false)
|
||||
}
|
||||
|
||||
margin := barMargin
|
||||
if !withBar {
|
||||
text, _ = p.line(width-spinnerMargin, true)
|
||||
margin = spinnerMargin
|
||||
text, _ = p.line(width-margin, true)
|
||||
}
|
||||
|
||||
// The library makes a bar or a spinner. A display that turns from
|
||||
// one to the other, as its line grows or the terminal is resized,
|
||||
// gets a new one.
|
||||
if p.bar == nil || p.withBar != withBar {
|
||||
p.newBar(withBar)
|
||||
// The library makes a bar or a spinner, and before each draw erases
|
||||
// its line with as many spaces as the widest line it has drawn. A
|
||||
// display gets a new one when it turns from one to the other, as its
|
||||
// line grows or the terminal is resized, and when the terminal has
|
||||
// been made too narrow for that erase, which would wrap and leave a
|
||||
// line behind at every draw.
|
||||
if p.bar == nil || p.withBar != withBar || p.widest >= width {
|
||||
p.newBar(withBar, width)
|
||||
}
|
||||
|
||||
if withBar {
|
||||
@@ -192,16 +199,23 @@ func (p *progress) draw() {
|
||||
}
|
||||
|
||||
p.bar.Describe(text)
|
||||
|
||||
// The library drew the text and its margin, less the terminal's last
|
||||
// column, which the margin keeps clear.
|
||||
p.widest = max(p.widest, len(text)+margin-1)
|
||||
}
|
||||
|
||||
// newBar has the library make what draw draws: a bar for the phase's
|
||||
// total when withBar is set, and a spinner when not. Either shows the
|
||||
// text draw gives it, and nothing of its own besides the spinner, or
|
||||
// the bar and its percent. p.mu must be held.
|
||||
func (p *progress) newBar(withBar bool) {
|
||||
func (p *progress) newBar(withBar bool, width int) {
|
||||
if p.bar != nil {
|
||||
// Erase what was drawn before.
|
||||
_ = p.bar.Clear()
|
||||
// Erase what was drawn before, short of the last column of a
|
||||
// terminal that may have been made narrower since: the library's
|
||||
// own erase is as wide as the widest line it drew.
|
||||
blank := strings.Repeat(" ", min(p.widest, width-1))
|
||||
fmt.Fprint(os.Stderr, "\r"+blank+"\r")
|
||||
}
|
||||
|
||||
total := int64(-1) // the library's spinner
|
||||
@@ -220,7 +234,7 @@ func (p *progress) newBar(withBar bool) {
|
||||
progressbar.OptionSetElapsedTime(false),
|
||||
progressbar.OptionSetPredictTime(false),
|
||||
)
|
||||
p.withBar = withBar
|
||||
p.withBar, p.widest = withBar, 0
|
||||
}
|
||||
|
||||
// setTotal gives a phase that started without a total its total, once
|
||||
|
||||
@@ -410,6 +410,60 @@ func TestTerminalLinesFitWidth(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestTerminalNarrowedRedrawsInPlace draws a large scan's display, two
|
||||
// and a half hours into the phase, on a terminal 120 columns wide, then
|
||||
// makes the terminal narrower than that line and draws it again a few
|
||||
// times. Nothing written after that, the erase before each redraw
|
||||
// included, may reach the narrower terminal's last column: it would
|
||||
// wrap, and every redraw would leave a line behind. At these widths the
|
||||
// spinner stays a spinner and the bar stays a bar.
|
||||
//
|
||||
//nolint:paralleltest // captureStderr replaces the process-wide os.Stderr
|
||||
func TestTerminalNarrowedRedrawsInPlace(t *testing.T) {
|
||||
stderr := captureStderr(t)
|
||||
|
||||
cases := []struct {
|
||||
params progressParams
|
||||
narrow int
|
||||
}{
|
||||
{progressParams{label: scanPhase("walk"), total: -1}, 60},
|
||||
{progressParams{label: scanPhase("hash"), total: 9876543}, 90},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
width := 120
|
||||
|
||||
// No timed draws: the test draws.
|
||||
p := startProgress(tc.params, func() int { return width }, time.Hour)
|
||||
|
||||
p.mu.Lock()
|
||||
p.count = 1234567
|
||||
p.start = p.start.Add(-150 * time.Minute)
|
||||
p.rateStart = p.start
|
||||
p.mu.Unlock()
|
||||
|
||||
p.draw()
|
||||
|
||||
width = tc.narrow
|
||||
before := len(stderr())
|
||||
|
||||
p.draw()
|
||||
p.draw()
|
||||
p.finish()
|
||||
|
||||
lines := strings.FieldsFuncSeq(stderr()[before:], func(r rune) bool {
|
||||
return r == '\r' || r == '\n'
|
||||
})
|
||||
|
||||
for line := range lines {
|
||||
if n := utf8.RuneCountInString(line); n >= width {
|
||||
t.Errorf("%d columns written on a terminal made %d wide: %q",
|
||||
n, width, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestProgressWarningsOnOwnLines drives the terminal displays, spinner
|
||||
// and bar, through a run of warnings with no items between them, as when
|
||||
// the walk meets many unreadable paths, for many redraws: every warning
|
||||
|
||||
Reference in New Issue
Block a user