Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions docs/HARNESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,16 @@ it survives doc rot.
the in-session loop (Step 8), the sensor became dead code and was
removed. If `--auto` ever comes back, the sensor needs to come back
with it.
- **No archtest rule for scroll-region usage.** `internal/ui/scrollregion.go`
detects support at runtime (`TERM`, TTY, terminal height) and falls back
to the inline `\r\033[K` renderer when unavailable. A static rule can't
see runtime terminal capabilities, so this stays a runtime concern. The
fallback is covered by `TestStickyProgressFallsBackWhenScrollRegionUnsupported`.
- **No terminal scroll-region rendering.** StickyProgress once reserved the
bottom rows via a DECSTBM scroll region (`internal/ui/scrollregion.go`,
since removed) to keep a "sticky" status bar frozen while the log scrolled.
It rendered correctly only on terminals that fully honour scroll regions;
on ones that report a normal `TERM` but don't (e.g. some GPU/block
terminals), the reserved bar was dragged into the log and reprinted on
every tick, so the install read as garbage. Runtime detection couldn't tell
the two apart from `TERM`/TTY/height alone. It now draws a plain in-place
`\r\033[K` status line that trails top-to-bottom output on every terminal.
Don't reintroduce the scroll region.
- **L4 runs on GitHub Actions, not a self-hosted runner.** `macos-14`
runners are Apple Silicon VMs — each job gets a fresh clean macOS
environment, which is exactly what L4 needs. Tart is no longer required.
Expand Down
3 changes: 1 addition & 2 deletions internal/ui/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ live in the `tui/` subpackage.
| File | Lines | Purpose |
|------|-------|---------|
| `ui.go` | 247 | Base styles, color helpers, output helpers (Header/Success/Error/Info/Warn/Muted/DryRun*), huh form wrappers (InputGitConfig, SelectPreset, Confirm, SelectOption, Input) |
| `progress.go` | 246 | StickyProgress: per-package timing, succeeded/failed/skipped counters |
| `progress.go` | 245 | StickyProgress: per-package timing, succeeded/failed/skipped counters, in-place status line |
| `scanprogress.go` | 221 | ScanProgress: step timing, overall counter `[3/8]` |
| `scrollregion.go` | 143 | ANSI scroll-region plumbing for StickyProgress |

## FILES (internal/ui/tui/)

Expand Down
63 changes: 14 additions & 49 deletions internal/ui/progress.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,6 @@ type StickyProgress struct {
succeeded int
failed int
skipped int

// Scroll region rendering (nil when terminal doesn't support it).
region *ScrollRegion
}

var spinnerFrames = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
Expand All @@ -58,10 +55,6 @@ func (sp *StickyProgress) Start() {
sp.mu.Lock()
sp.active = true
sp.startTime = time.Now()
if IsScrollRegionSupported() {
sp.region = NewScrollRegion(2)
sp.region.Start()
}
sp.mu.Unlock()

signal.Stop(sp.sigCh)
Expand Down Expand Up @@ -90,16 +83,19 @@ func (sp *StickyProgress) Start() {
}()
}

// render draws the live status line in place: carriage-return to the start of
// the current line, clear it, then write the spinner, progress count, current
// package, and elapsed time. It writes no newline, so PrintLine's completed
// rows scroll above it and this line trails the output.
//
// This deliberately does NOT reserve a "sticky" bottom bar via a terminal
// scroll region (DECSTBM). That looked nicer where it worked, but it silently
// corrupted on terminals that report a normal TERM yet don't honour scroll
// regions — the reserved bar got dragged into the log and reprinted on every
// tick, so the install read as garbage. A plain in-place status line renders
// top-to-bottom on every terminal, which is the property that matters for a
// one-shot install log. Don't reintroduce the scroll region.
func (sp *StickyProgress) render() {
if sp.region != nil {
sp.region.DrawBottom(sp.formatLines())
return
}
sp.renderInline()
}

// renderInline is the fallback renderer used when scroll region is unavailable.
func (sp *StickyProgress) renderInline() {
spinner := spinnerFrames[sp.spinnerIdx]
pkg := truncate(sp.currentPkg, 20)
elapsed := sp.pkgElapsed()
Expand All @@ -111,27 +107,6 @@ func (sp *StickyProgress) renderInline() {
etaStyle.Render(elapsed))
}

// formatLines returns the two strings to render in the bottom-reserved scroll
// region: a divider and a status line.
func (sp *StickyProgress) formatLines() []string {
cols := 80
if sp.region != nil {
cols = sp.region.Cols()
}

spinner := spinnerFrames[sp.spinnerIdx]
pkg := truncate(sp.currentPkg, 20)
elapsed := sp.pkgElapsed()

divider := strings.Repeat("─", cols)
status := fmt.Sprintf("%s %s %s %s",
spinner,
progressTextStyle.Render(fmt.Sprintf("[%d/%d]", sp.completed+1, sp.total)),
currentPkgStyle.Render(pkg),
etaStyle.Render(elapsed))
return []string{divider, status}
}

// pkgElapsed returns elapsed seconds for the current package, or "" if not yet started.
func (sp *StickyProgress) pkgElapsed() string {
if sp.pkgStart.IsZero() {
Expand Down Expand Up @@ -217,12 +192,7 @@ func (sp *StickyProgress) onInterrupt() (force bool) {
}
sp.aborting = true
sp.active = false
if sp.region != nil {
sp.region.Stop()
sp.region = nil
} else {
fmt.Printf("\r\033[K")
}
fmt.Printf("\r\033[K")
fmt.Println(mutedStyle.Render(" aborting — waiting for the current step to stop · ctrl+c again to force quit"))
return false
}
Expand All @@ -233,12 +203,7 @@ func (sp *StickyProgress) Finish() {
sp.mu.Lock()
defer sp.mu.Unlock()
sp.active = false
if sp.region != nil {
sp.region.Stop()
sp.region = nil
} else {
fmt.Printf("\r\033[K")
}
fmt.Printf("\r\033[K")

elapsed := time.Since(sp.startTime)

Expand Down
18 changes: 10 additions & 8 deletions internal/ui/progress_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,16 +159,18 @@ func TestStickyProgressPkgElapsed(t *testing.T) {
assert.Contains(t, elapsed, "s")
}

func TestStickyProgressFallsBackWhenScrollRegionUnsupported(t *testing.T) {
func TestStickyProgressStartFinishDoNotPanic(t *testing.T) {
// The renderer no longer reserves a scroll region — it draws a plain
// in-place status line that works on every terminal. Start/Finish must run
// cleanly regardless of TERM (here a dumb terminal that supports nothing).
t.Setenv("TERM", "dumb")
sp := NewStickyProgress(3)
sp.Start()
defer sp.Finish()
// Region should NOT have been attached.
sp.mu.Lock()
hasRegion := sp.region != nil
sp.mu.Unlock()
assert.False(t, hasRegion, "scroll region should be disabled on dumb TERM")
assert.NotPanics(t, func() {
sp.Start()
sp.SetCurrent("wget")
sp.IncrementWithStatus(true)
sp.Finish()
})
}

// Ctrl+C during a linear install must not exit from the progress goroutine:
Expand Down
143 changes: 0 additions & 143 deletions internal/ui/scrollregion.go

This file was deleted.

Loading
Loading