Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92d07525c6 |
No files matched your search
@@ -770,7 +770,9 @@ Additional requirements:
|
|||||||
gives way to the spinner. Only when those four alone are too wide, as on a
|
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
|
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
|
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
|
- 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
|
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
|
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
|
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
|
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
|
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
|
reaches the terminal's last column, however narrow, also after the terminal is
|
||||||
its rate, then its bar, keeping the phase, progress, elapsed time and ETA
|
made narrower during a phase: a line too wide leaves out its rate, then its
|
||||||
whole, and is cut short only when those alone do not fit, as below about 70
|
bar, keeping the phase, progress, elapsed time and ETA whole, and is cut short
|
||||||
columns late in a large scan. In plain mode the line is printed every 5
|
only when those alone do not fit, as below about 70 columns late in a large
|
||||||
seconds whether or not an item completed (2026-10-16,
|
scan. In plain mode the line is printed every 5 seconds whether or not an item
|
||||||
https://git.eeqj.de/sneak/sfdupes/issues/112)
|
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
|
- `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
|
they use, as an absolute path, and whether that path is the default or comes
|
||||||
|
|||||||
+24
-10
@@ -5,6 +5,7 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -98,6 +99,7 @@ type progress struct {
|
|||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
bar *progressbar.ProgressBar // what the library draws; nil until then
|
bar *progressbar.ProgressBar // what the library draws; nil until then
|
||||||
withBar bool // bar is the library's bar, not its spinner
|
withBar bool // bar is the library's bar, not its spinner
|
||||||
|
widest int // the widest line bar has drawn, in columns
|
||||||
label string
|
label string
|
||||||
total int64
|
total int64
|
||||||
estimate bool
|
estimate bool
|
||||||
@@ -176,15 +178,20 @@ func (p *progress) draw() {
|
|||||||
text, withBar = p.line(width-barMargin, false)
|
text, withBar = p.line(width-barMargin, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
margin := barMargin
|
||||||
if !withBar {
|
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
|
// The library makes a bar or a spinner, and before each draw erases
|
||||||
// one to the other, as its line grows or the terminal is resized,
|
// its line with as many spaces as the widest line it has drawn. A
|
||||||
// gets a new one.
|
// display gets a new one when it turns from one to the other, as its
|
||||||
if p.bar == nil || p.withBar != withBar {
|
// line grows or the terminal is resized, and when the terminal has
|
||||||
p.newBar(withBar)
|
// 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 {
|
if withBar {
|
||||||
@@ -192,16 +199,23 @@ func (p *progress) draw() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
p.bar.Describe(text)
|
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
|
// 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
|
// 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
|
// text draw gives it, and nothing of its own besides the spinner, or
|
||||||
// the bar and its percent. p.mu must be held.
|
// 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 {
|
if p.bar != nil {
|
||||||
// Erase what was drawn before.
|
// Erase what was drawn before, short of the last column of a
|
||||||
_ = p.bar.Clear()
|
// 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
|
total := int64(-1) // the library's spinner
|
||||||
@@ -220,7 +234,7 @@ func (p *progress) newBar(withBar bool) {
|
|||||||
progressbar.OptionSetElapsedTime(false),
|
progressbar.OptionSetElapsedTime(false),
|
||||||
progressbar.OptionSetPredictTime(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
|
// 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
|
// TestProgressWarningsOnOwnLines drives the terminal displays, spinner
|
||||||
// and bar, through a run of warnings with no items between them, as when
|
// 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
|
// the walk meets many unreadable paths, for many redraws: every warning
|
||||||
|
|||||||
Reference in new issue
Block a user