progress: render a single download with the same table as several
block() no longer special-cases one download with a [bracketed] one-liner; it always draws the summary line, the column header, and one row per download. The non-TTY path emits the same summary for one or many. This drops lineOne and the dual rendering path it created, so a column or state change is made in one place, not two that could drift.
This commit is contained in:
@@ -114,13 +114,10 @@ func (r *Reporter) render(final bool) {
|
|||||||
r.redraw(r.block(stats), final)
|
r.redraw(r.block(stats), final)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Not a TTY: no cursor control, so emit a single line sparingly (and always
|
// Not a TTY: no cursor control, so emit the summary line sparingly (and always
|
||||||
// the final one) to keep redirected output and logs readable.
|
// the final one) to keep redirected output and logs readable.
|
||||||
var line string
|
var line string
|
||||||
switch {
|
if len(stats) > 0 {
|
||||||
case len(stats) == 1:
|
|
||||||
line = r.lineOne(stats[0])
|
|
||||||
case len(stats) > 1:
|
|
||||||
line = r.summary(stats)
|
line = r.summary(stats)
|
||||||
}
|
}
|
||||||
if line != "" && (final || now.Sub(r.lastLog) >= logEvery) {
|
if line != "" && (final || now.Sub(r.lastLog) >= logEvery) {
|
||||||
@@ -129,52 +126,18 @@ func (r *Reporter) render(final bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Reporter) lineOne(s download.Stat) string {
|
|
||||||
dl, ul := r.rates(s)
|
|
||||||
seeding := s.Status == download.Seeding
|
|
||||||
var b strings.Builder
|
|
||||||
fmt.Fprintf(&b, "[%s ", elide(s.Name, nameW))
|
|
||||||
// While seeding, report the share ratio in place of the size block and drop
|
|
||||||
// the DL: field; otherwise show completed/total (or bare completed).
|
|
||||||
if seeding {
|
|
||||||
fmt.Fprintf(&b, "SEED(%.1f)", ratio(s.Uploaded, s.Completed))
|
|
||||||
} else if s.Total > 0 {
|
|
||||||
fmt.Fprintf(&b, "%s/%s(%d%%)", humanSize(s.Completed, r.human), humanSize(s.Total, r.human), percent(s.Completed, s.Total))
|
|
||||||
} else {
|
|
||||||
b.WriteString(humanSize(s.Completed, r.human))
|
|
||||||
}
|
|
||||||
// CN is always shown (including HTTP); SD is shown for any torrent.
|
|
||||||
fmt.Fprintf(&b, " CN:%d", s.Conns)
|
|
||||||
if s.IsBT {
|
|
||||||
fmt.Fprintf(&b, " SD:%d", s.Seeders)
|
|
||||||
}
|
|
||||||
if !seeding {
|
|
||||||
fmt.Fprintf(&b, " DL:%s", humanSize(dl, r.human))
|
|
||||||
}
|
|
||||||
if seeding || ul > 0 {
|
|
||||||
fmt.Fprintf(&b, " UL:%s", humanSize(ul, r.human))
|
|
||||||
}
|
|
||||||
if !seeding && s.Total > 0 && dl > 0 {
|
|
||||||
fmt.Fprintf(&b, " ETA:%s", secfmt(etaDuration(s.Total-s.Completed, dl)))
|
|
||||||
}
|
|
||||||
b.WriteByte(']')
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// nameW is the fixed rune width of the name column in the table, matching
|
// nameW is the fixed rune width of the name column in the table, matching
|
||||||
// elide's cap so a padded name lines the columns up.
|
// elide's cap so a padded name lines the columns up.
|
||||||
const nameW = 20
|
const nameW = 20
|
||||||
|
|
||||||
// block builds the lines of the live display: the single-download dashboard for
|
// block builds the lines of the live display: a summary line, the column header,
|
||||||
// one download, a summary + column-header + one row each for several, or a
|
// and one row per download — or just the one-line summary when the table would
|
||||||
// one-line summary when the table would not fit the window.
|
// not fit the window. A single download renders the same way as several.
|
||||||
func (r *Reporter) block(stats []download.Stat) []string {
|
func (r *Reporter) block(stats []download.Stat) []string {
|
||||||
switch {
|
if len(stats) == 0 {
|
||||||
case len(stats) == 0:
|
|
||||||
return nil
|
return nil
|
||||||
case len(stats) == 1:
|
}
|
||||||
return []string{r.lineOne(stats[0])}
|
if len(stats)+3 > r.height {
|
||||||
case len(stats)+3 > r.height:
|
|
||||||
// The block is repainted in place by moving the cursor up over it, which
|
// The block is repainted in place by moving the cursor up over it, which
|
||||||
// breaks if printing the last row reaches the screen's bottom row and
|
// breaks if printing the last row reaches the screen's bottom row and
|
||||||
// scrolls the frame out from under the cursor. term height counts the row
|
// scrolls the frame out from under the cursor. term height counts the row
|
||||||
@@ -182,14 +145,13 @@ func (r *Reporter) block(stats []download.Stat) []string {
|
|||||||
// (len+2 lines) must still leave the bottom row free: fall back to the
|
// (len+2 lines) must still leave the bottom row free: fall back to the
|
||||||
// single summary line once len+2 would fill to the bottom.
|
// single summary line once len+2 would fill to the bottom.
|
||||||
return []string{r.summary(stats)}
|
return []string{r.summary(stats)}
|
||||||
default:
|
|
||||||
out := make([]string, 0, len(stats)+2)
|
|
||||||
out = append(out, r.summary(stats), tableHeader())
|
|
||||||
for _, s := range stats {
|
|
||||||
out = append(out, r.tableRow(s))
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
}
|
||||||
|
out := make([]string, 0, len(stats)+2)
|
||||||
|
out = append(out, r.summary(stats), tableHeader())
|
||||||
|
for _, s := range stats {
|
||||||
|
out = append(out, r.tableRow(s))
|
||||||
|
}
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// redraw repaints the block in place: move to the top of the previous frame,
|
// redraw repaints the block in place: move to the top of the previous frame,
|
||||||
|
|||||||
@@ -78,42 +78,6 @@ func TestRatesKeyedByID(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestLineOneSeeding: while seeding, render SEED(ratio), drop DL:, keep UL:.
|
|
||||||
func TestLineOneSeeding(t *testing.T) {
|
|
||||||
r := newReporter(true)
|
|
||||||
s := download.Stat{
|
|
||||||
ID: "h", Name: "f", IsBT: true, Status: download.Seeding,
|
|
||||||
Total: 1000, Completed: 1000, Uploaded: 1500, Conns: 3, Seeders: 2,
|
|
||||||
}
|
|
||||||
line := r.lineOne(s)
|
|
||||||
if !strings.Contains(line, "SEED(1.5)") {
|
|
||||||
t.Errorf("missing SEED(1.5): %q", line)
|
|
||||||
}
|
|
||||||
if strings.Contains(line, "DL:") {
|
|
||||||
t.Errorf("DL: should be dropped while seeding: %q", line)
|
|
||||||
}
|
|
||||||
if !strings.Contains(line, "UL:") {
|
|
||||||
t.Errorf("UL: should be kept while seeding: %q", line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestLineOneCNSD: CN always shown; SD shown for torrents (even with 0 seeders),
|
|
||||||
// absent for HTTP.
|
|
||||||
func TestLineOneCNSD(t *testing.T) {
|
|
||||||
r := newReporter(true)
|
|
||||||
bt := r.lineOne(download.Stat{ID: "h", Name: "f", IsBT: true, Total: 10, Completed: 1, Conns: 4, Seeders: 0})
|
|
||||||
if !strings.Contains(bt, "CN:4") || !strings.Contains(bt, "SD:0") {
|
|
||||||
t.Errorf("torrent line missing CN/SD: %q", bt)
|
|
||||||
}
|
|
||||||
http := r.lineOne(download.Stat{ID: "h2", Name: "f", IsBT: false, Total: 10, Completed: 1, Conns: 1})
|
|
||||||
if !strings.Contains(http, "CN:1") {
|
|
||||||
t.Errorf("http line missing CN: %q", http)
|
|
||||||
}
|
|
||||||
if strings.Contains(http, "SD:") {
|
|
||||||
t.Errorf("http line should not show SD: %q", http)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPctField(t *testing.T) {
|
func TestPctField(t *testing.T) {
|
||||||
if got := pctField(download.Stat{Status: download.Active, Total: 100, Completed: 50}); got != " 50%" {
|
if got := pctField(download.Stat{Status: download.Active, Total: 100, Completed: 50}); got != " 50%" {
|
||||||
t.Errorf("active 50%% = %q, want ' 50%%'", got)
|
t.Errorf("active 50%% = %q, want ' 50%%'", got)
|
||||||
@@ -183,6 +147,10 @@ func TestBlockTableAndFallback(t *testing.T) {
|
|||||||
if got := r.block(stats); len(got) != 4 { // summary + header + 2 rows
|
if got := r.block(stats); len(got) != 4 { // summary + header + 2 rows
|
||||||
t.Errorf("table block = %d lines, want 4:\n%v", len(got), got)
|
t.Errorf("table block = %d lines, want 4:\n%v", len(got), got)
|
||||||
}
|
}
|
||||||
|
// A single download renders the same way as several: summary + header + 1 row.
|
||||||
|
if got := r.block(stats[:1]); len(got) != 3 {
|
||||||
|
t.Errorf("single-download block = %d lines, want 3 (summary+header+row):\n%v", len(got), got)
|
||||||
|
}
|
||||||
r.height = 5 // 4 lines fit with the bottom row left free
|
r.height = 5 // 4 lines fit with the bottom row left free
|
||||||
if got := r.block(stats); len(got) != 4 {
|
if got := r.block(stats); len(got) != 4 {
|
||||||
t.Errorf("just-fits block = %d lines, want 4:\n%v", len(got), got)
|
t.Errorf("just-fits block = %d lines, want 4:\n%v", len(got), got)
|
||||||
|
|||||||
Reference in New Issue
Block a user