Skip to content

Commit ca38ec6

Browse files
committed
fix(clean): use alt screen to prevent initial render leaking into scrollback
1 parent 9eb4417 commit ca38ec6

1 file changed

Lines changed: 8 additions & 16 deletions

File tree

cmd/clean_view.go

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"fmt"
55
"os"
66
"strings"
7-
"sync"
87
"time"
98

109
tea "github.com/charmbracelet/bubbletea"
@@ -84,12 +83,10 @@ type cleanModel struct {
8483
activePhase int
8584
msgCh <-chan cleanMsg
8685
spinnerTick int
87-
drainedCh chan struct{}
88-
drainOnce sync.Once
8986
width int
9087
}
9188

92-
func newCleanModel(specs []cleanPhaseSpec, msgCh <-chan cleanMsg, drainedCh chan struct{}) *cleanModel {
89+
func newCleanModel(specs []cleanPhaseSpec, msgCh <-chan cleanMsg) *cleanModel {
9390
phases := make([]cleanPhaseModel, len(specs))
9491
idx := make(map[string]int, len(specs))
9592
for i, spec := range specs {
@@ -106,7 +103,6 @@ func newCleanModel(specs []cleanPhaseSpec, msgCh <-chan cleanMsg, drainedCh chan
106103
phases: phases,
107104
phaseIndex: idx,
108105
msgCh: msgCh,
109-
drainedCh: drainedCh,
110106
}
111107
}
112108

@@ -121,7 +117,6 @@ func (m *cleanModel) listen() tea.Cmd {
121117
return func() tea.Msg {
122118
msg, ok := <-m.msgCh
123119
if !ok {
124-
m.drainOnce.Do(func() { close(m.drainedCh) })
125120
return cleanDoneMsg{}
126121
}
127122
return msg
@@ -330,27 +325,25 @@ type cleanView struct {
330325
program *tea.Program
331326
msgCh chan cleanMsg
332327
doneCh chan error
333-
drainedCh chan struct{}
334328
finalModel *cleanModel
335329
// shared
336330
mode output.OutputMode
337331
}
338332

339333
// newCleanView creates a cleanView for the given phases. In TUI mode the
340-
// BubbleTea program is started immediately. In plain mode no program is started.
334+
// BubbleTea program is started immediately in alt-screen mode so the initial
335+
// render never pollutes the terminal scrollback. In plain mode no program is started.
341336
func newCleanView(mode output.OutputMode, specs []cleanPhaseSpec) *cleanView {
342337
v := &cleanView{mode: mode}
343338
if mode == output.ModePlain {
344339
return v
345340
}
346341
msgCh := make(chan cleanMsg, 100)
347-
drainedCh := make(chan struct{})
348-
m := newCleanModel(specs, msgCh, drainedCh)
349-
p := tea.NewProgram(m)
342+
m := newCleanModel(specs, msgCh)
343+
p := tea.NewProgram(m, tea.WithAltScreen())
350344
v.program = p
351345
v.msgCh = msgCh
352346
v.doneCh = make(chan error, 1)
353-
v.drainedCh = drainedCh
354347
go func() {
355348
fm, err := p.Run()
356349
if cm, ok := fm.(*cleanModel); ok {
@@ -409,16 +402,15 @@ func (v *cleanView) AdvancePhase() {
409402
}
410403
}
411404

412-
// Wait closes the message channel, waits for the final message to be rendered,
413-
// quits the BubbleTea program, then prints the final frame so it remains visible.
405+
// Wait closes the message channel, blocks until the BubbleTea program exits,
406+
// then prints the final frame to the primary screen. Alt-screen mode clears
407+
// the TUI on exit, so we must reprint to leave the results visible.
414408
// In plain mode it is a no-op.
415409
func (v *cleanView) Wait() {
416410
if v.mode == output.ModePlain {
417411
return
418412
}
419413
close(v.msgCh)
420-
<-v.drainedCh // last message consumed → final frame rendered
421-
v.program.Quit() // safe to quit now
422414
<-v.doneCh
423415
if v.finalModel != nil {
424416
fmt.Print(v.finalModel.View())

0 commit comments

Comments
 (0)