Skip to content

Commit f8f5cc0

Browse files
committed
Use an alt screen to protect scroll buffer
The new and pretty arkade progress output is polluting scroll- back. Thanks to @welteki for pointing this out. The new version uses an alternative screen, then prints the final version of the screen buffer to the terminal, to protect the scrollback. This also works fine in a web terminal, tested with slicer box and the web UI. Signed-off-by: Alex Ellis (OpenFaaS Ltd) <alexellis2@gmail.com>
1 parent ae8f780 commit f8f5cc0

File tree

2 files changed

+103
-8
lines changed

2 files changed

+103
-8
lines changed

Makefile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ test:
2424
e2e:
2525
CGO_ENABLED=0 go test github.com/alexellis/arkade/pkg/get -cover --tags e2e -v
2626

27+
.PHONY: dist-local
28+
dist-local:
29+
mkdir -p bin
30+
CGO_ENABLED=0 go build -ldflags $(LDFLAGS) -o bin/arkade
31+
2732
.PHONY: dist
2833
dist:
2934
mkdir -p bin

cmd/get.go

Lines changed: 98 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -242,18 +242,26 @@ and provides a fast and easy alternative to a package manager.`,
242242
out = os.Stderr
243243
}
244244

245-
// Hide cursor during live rendering.
245+
// Use alternate screen buffer for TTY progress so that
246+
// intermediate frames don't pollute scrollback. The final
247+
// completed state is printed as static text after leaving
248+
// the alternate screen.
246249
if tty && renderProgress {
247-
fmt.Fprint(out, "\033[?25l")
248-
defer fmt.Fprint(out, "\033[?25h\n")
250+
fmt.Fprint(out, "\033[?1049h") // enter alternate screen
251+
fmt.Fprint(out, "\033[?25l") // hide cursor
249252
}
250253

251-
// Restore cursor on signal.
252-
go func() {
253-
<-signalChan
254+
leaveAltScreen := func() {
254255
if tty && renderProgress {
255-
fmt.Fprint(out, "\033[?25h\n")
256+
fmt.Fprint(out, "\033[?25h") // restore cursor
257+
fmt.Fprint(out, "\033[?1049l") // leave alternate screen
256258
}
259+
}
260+
261+
// Restore terminal on signal.
262+
go func() {
263+
<-signalChan
264+
leaveAltScreen()
257265
os.Exit(2)
258266
}()
259267

@@ -315,9 +323,14 @@ and provides a fast and easy alternative to a package manager.`,
315323
if renderProgress {
316324
if tty {
317325
renderTTY(out, progress, parallel)
326+
leaveAltScreen()
327+
// Print a static final frame into the main scrollback.
328+
renderTTYFinal(out, progress)
318329
} else {
319330
renderPlain(out, progress)
320331
}
332+
} else if tty {
333+
leaveAltScreen()
321334
}
322335

323336
if firstErr != nil {
@@ -558,6 +571,79 @@ func renderTTY(out io.Writer, progress []toolProgress, parallel int) {
558571
fmt.Fprint(out, b.String())
559572
}
560573

574+
// ── Static final frame (printed after leaving alternate screen) ────
575+
//
576+
// Shows the completed state of each tool as static text in the main
577+
// scrollback. No cursor movement or screen clearing.
578+
579+
func renderTTYFinal(out io.Writer, progress []toolProgress) {
580+
var b strings.Builder
581+
582+
b.WriteString("\033[1m Arkade by Alex Ellis - https://github.com/sponsors/alexellis\033[0m\n\n")
583+
584+
nameW := 10
585+
for i := range progress {
586+
l := len(progress[i].name)
587+
if progress[i].version != "" {
588+
l += len(progress[i].version) + 3
589+
}
590+
if l > nameW {
591+
nameW = l
592+
}
593+
}
594+
if nameW > 40 {
595+
nameW = 40
596+
}
597+
598+
barW := 20
599+
600+
for i := range progress {
601+
p := &progress[i]
602+
read := atomic.LoadInt64(&p.bytesRead)
603+
total := atomic.LoadInt64(&p.totalBytes)
604+
605+
displayName := p.name
606+
if p.version != "" {
607+
displayName = fmt.Sprintf("%s (%s)", p.name, p.version)
608+
}
609+
610+
switch p.status {
611+
case stDone:
612+
pct := 100
613+
if total > 0 {
614+
pct = int(math.Round(float64(read) / float64(total) * 100))
615+
if pct > 100 {
616+
pct = 100
617+
}
618+
}
619+
bar := renderBar(int64(pct), barW)
620+
sizeStr := units.HumanSize(float64(read))
621+
speed := float64(0)
622+
if p.elapsed > 0 {
623+
speed = float64(read) / p.elapsed.Seconds()
624+
}
625+
elapsed := fmtDuration(p.elapsed)
626+
b.WriteString(fmt.Sprintf(
627+
" \033[32m✔\033[0m %-*s %3d%% %s %8s %9s/s %s\n",
628+
nameW, displayName, pct, bar, sizeStr, units.HumanSize(speed), elapsed))
629+
630+
case stFailed:
631+
errMsg := ""
632+
if p.err != nil {
633+
errMsg = p.err.Error()
634+
if len(errMsg) > 40 {
635+
errMsg = errMsg[:40] + "…"
636+
}
637+
}
638+
b.WriteString(fmt.Sprintf(
639+
" \033[31m✘\033[0m %-*s \033[31mfailed: %s\033[0m\n",
640+
nameW, p.name, errMsg))
641+
}
642+
}
643+
644+
fmt.Fprint(out, b.String())
645+
}
646+
561647
// ── Non-TTY / plain renderer ───────────────────────────────────────
562648
//
563649
// Prints each state change on its own line, no ANSI, no overwrites.
@@ -580,7 +666,11 @@ func renderPlain(out io.Writer, progress []toolProgress) {
580666
case stResolving:
581667
fmt.Fprintf(out, "[resolving] %s\n", p.name)
582668
case stDownloading:
583-
fmt.Fprintf(out, "[downloading] %s\n", p.name)
669+
displayName := p.name
670+
if p.version != "" {
671+
displayName = fmt.Sprintf("%s (%s)", p.name, p.version)
672+
}
673+
fmt.Fprintf(out, "[downloading] %s\n", displayName)
584674
case stDone:
585675
read := atomic.LoadInt64(&p.bytesRead)
586676
sizeStr := units.HumanSize(float64(read))

0 commit comments

Comments
 (0)