Skip to content

Keep redrawn progress rows in place instead of repeating them - #137

Merged
graeme merged 2 commits into
mainfrom
fix-in-place-download-ticks
Aug 31, 2026
Merged

Keep redrawn progress rows in place instead of repeating them#137
graeme merged 2 commits into
mainfrom
fix-in-place-download-ticks

Conversation

@graeme

@graeme graeme commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

PR: Fix console progress repeating on multi-package operations

Summary

The console redrew Homebrew's parallel download block on every tick instead of updating it in place, so a multi-package operation filled the pane with duplicated rows. Reported with a screenshot showing the cask list repeated at different byte counts.

Changes

Homebrew's download queue draws N rows, leaves the last without a trailing newline, then rewinds with ESC[<n>F (cursor previous line) to draw the next frame over the top. TerminalLineAssembler handled SGR, ESC[K and the column moves but dropped everything else, so the rewind was lost. That gap caused both symptoms: the block repeated, and the last row spliced into the next frame's first row.

  • TerminalLineAssembler keeps a window of committed rows addressable, sized to the pty's row count, and handles ESC[F, ESC[A and ESC[B. Inside a block, a newline steps the cursor down a row rather than opening one. Movement above the window is ignored rather than writing to a wrong row.
  • TerminalLineEvent.revised carries a row offset. BrewCommandOutputLine and CommandJob.appendOutput route by it, counting back from the end so offsets stay valid after maxOutputLines trims the front.
  • BrewCommandService builds the returned transcript from settled rows rather than every frame.

Testing

  • scripts/test: 780 package tests plus 19 BrewUILint tests
  • 31 new tests: both symptoms, cursor semantics, offset arithmetic, window boundary
  • Integration test driving a real zsh subprocess through the pty
  • swiftformat, swiftlint --strict, BrewUILint, xcodebuild -scheme Brew
  • Manual: brew install node at concurrency 8, on main and on this branch

PR checklist

  • Have you followed this repository's contribution and workflow guidance?
  • Have you explained what changed and why this should land now?
  • Have you run relevant local checks for the changed scope?
  • Are changes scoped and free of unrelated modifications?

  • AI was used to generate or assist with generating this PR.
  • AI traced the missing CPL handling against download_queue.rb and wrote the implementation, tests and this description. Verified manually by reproducing the block on main and confirming one row per download here.

Follow-ups

  • The pty is fixed at 120x40, so brew formats columns for 120 regardless of console width.
  • Absolute positioning, erase-in-display, scroll regions and the alternate screen still need a real screen buffer.

Homebrew's parallel download queue draws N rows, deliberately leaves the last
without a trailing newline, then rewinds with ESC[<n>F (cursor previous line)
to draw the next frame over the top. TerminalLineAssembler handled SGR, ESC[K
and the column moves but dropped everything else, so the rewind was lost. That
one gap caused both reported symptoms: the block repeated every tick, and the
newline-less last row spliced into the next frame's first row.

The assembler now keeps a window of committed rows addressable, sized to the
pty's row count because that is the screen and a terminal cannot address above
it, and handles ESC[F, ESC[A and ESC[B. Inside a block a newline steps the
cursor down a row rather than opening one, which is the rule the fix turns on.
Movement above the window is ignored, degrading to the previous append
behaviour rather than writing to a wrong row.

TerminalLineEvent.revised carries a row offset that BrewCommandOutputLine and
CommandJob.appendOutput route by, counted from the end so offsets stay valid
after maxOutputLines trims the front. BrewCommandService builds the returned
transcript from settled rows rather than every frame; trailing-newline-ness is
tracked separately from completeness so output that ended without a newline
does not gain one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Its only coverage was incidental: the pty tests that assert `printf 'tty'`
returns exactly `tty` are what caught trailing-newline-ness being conflated
with completeness, and a test about TTY detection is the wrong place for that
to surface.

Twelve cases over the value type itself: newline termination, settling a row
the stream ended without a newline, offset-zero replacement, positive-offset
rewrites leaving the target's newline alone, out-of-range offsets, and a
redrawn block recording settled rows rather than every frame. Reintroducing the
original defect fails five of them, including the `printf 'tty'` case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@graeme
graeme merged commit 01af0ab into main Aug 31, 2026
10 checks passed
@graeme
graeme deleted the fix-in-place-download-ticks branch August 31, 2026 05:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants