Use ANSI 2K to clear the progress line before notify/render · Entire

Use ANSI 2K to clear the progress line before notify/render

f240525→main· Soph·2mo ago·2 files·+46 added/-6 removed

The previous "overwrite with N spaces" approach for clearing the ticker frame relied on tracked byte length matching display width. That assumption breaks for our line: it contains four multi-byte UTF-8 characters (→, │, …, →), so byte length runs ~8 bytes wider than display width. The extra spaces wrap onto the next row and leave the original progress text untouched on the previous one, which is why slog INFO lines and 413 errors appeared concatenated to the progress line in the user's terminal output even though notify was firing.

Switch to emitting "\r\x1b[2K" (carriage return + ANSI "erase entire line") in both render and notify. The terminal handles the column accounting, so the clear is exact regardless of UTF-8 byte/column mismatch and regardless of terminal width. Drops the need for lastLen-based padding entirely. Add a regression test that verifies the clear escape lands before the message text.

Sessions

bcd665909dfaView transcript

Changes

2

56 unmodified lines

57
58
59
60
61
62
63
64
65
66
67
68
69
3 unmodified lines

73
74
75
69
76
77
78
79
44 unmodified lines

124
125
126
120
121
122
123
124
127
128
129
130

56 unmodified lines

}
}
// clearLine is the ANSI escape sequence that erases the entire current
// terminal line and parks the cursor at column 0. Used in place of a
// run of N spaces because tracked byte length doesn't match display
// width when the progress line contains multi-byte UTF-8 (→, │, …);
// overshooting with spaces can wrap and leave residue on the next row.
const clearLine = "\r\x1b[2K"
// notify writes a one-time message above the live progress line. The
// current frame is cleared first so the message lands on a clean row,
// and lastLen is reset so the next tick redraws the progress below.
3 unmodified lines

p.mu.Lock()
    defer p.mu.Unlock()
    if p.lastLen > 0 {
        fmt.Fprint(p.out, "\r"+strings.Repeat(" ", p.lastLen)+"\r")
        fmt.Fprint(p.out, clearLine)
    }
    fmt.Fprintln(p.out, msg)
    p.lastLen = 0
44 unmodified lines

p.mu.Lock()
    defer p.mu.Unlock()
pad := 0
if p.lastLen > len(line) {
    pad = p.lastLen - len(line)
}
fmt.Fprint(p.out, "\r"+line+strings.Repeat(" ", pad))
fm.Fprint(p.out, clearLine+line)
p.lastLen = len(line)
}

Minternal/syncer/progress.go+9/-6

204 unmodified lines

205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247

204 unmodified lines

}
}
// TestNotifyAfterRenderClearsTheFrame asserts that bytes written to
// notify after a render include a clear-line escape before the message,
// so the slog/sideband line doesn't end up concatenated with the live
// progress frame on the user's terminal.
func TestNotifyAfterRenderClearsTheFrame(t *testing.T) {
    t.Parallel()
    stats := newStats(true)
    stats.setSideDisplay("source", "github.com")
    stats.setSideDisplay("target", "example.test")
    stats.side("source").bytes.Store(2 * 1024 * 1024)
    stats.side("target").bytes.Store(1024 * 1024)
    stats.setPhase("pack 1/4")

var buf bytes.Buffer
    p := newProgressReporter(&buf, stats, 0)
    p.render(false)
    frameEnd := buf.Len()

p.notify("level=INFO msg=\"bootstrap subdividing\"")

tail := buf.String()[frameEnd:]
    // notify must emit a clear-line escape before the message so the
    // previous frame is wiped from the row, plus a trailing newline so
    // subsequent renders draw on a fresh row.
    if !strings.Contains(tail, "\x1b[2K") {
            t.Errorf("notify should clear the line via ANSI 2K, got %q", tail)
    }
    if !strings.HasSuffix(tail, "\n") {
            t.Errorf("notify should terminate with newline, got %q", tail)
    }
    clearIdx := strings.Index(tail, "\x1b[2K")
    msgIdx := strings.Index(tail, "level=INFO")
    if clearIdx < 0 || msgIdx < 0 || clearIdx > msgIdx {
            t.Errorf("clear must precede the message in %q", tail)
    }
}

func TestSessionStderrRoutesMultilineThroughNotify(t *testing.T) {
    t.Parallel()
    stats := newStats(true)