Skip to content

Commit cb9daa2

Browse files
authored
new_installer.py: fix line wrap issues (spack#52193)
In the "active area" of the terminal UI, downwards cursor movement is used instead of `\n` to prevent flickering. The variable `active_area_rows` keeps track on how many lines cursor movement can be used, and any new builds would use `\n` instead (to scroll the terminal when needed). However, after spack#52163 the finished builds were printed *without* truncating to terminal width, and typically exceed it. This means they can consume multiple lines from the "active area" of running builds, and as a result the new "active area" is actualy fewer lines. To fix this, reset `active_area_rows` to 0, which forces `\n` instead of cursor movement. Use the same trick when the terminal resizes. A narrower/wider terminal can result in line wrapping, so better not to rely on cursor movement as much. Signed-off-by: Harmen Stoppels <me@harmenstoppels.nl>
1 parent 0060267 commit cb9daa2

1 file changed

Lines changed: 19 additions & 11 deletions

File tree

lib/spack/spack/new_installer.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1398,24 +1398,32 @@ def update(self, finalize: bool = False) -> None:
13981398
# Build the overview output in a buffer and print all at once to avoid flickering.
13991399
buffer = io.StringIO()
14001400

1401-
# Move cursor up to the start of the display area
1401+
# Move cursor up to the start of the display area assuming the same terminal width. If the
1402+
# terminal resized, lines may have wrapped, and we should've moved up further. We do not
1403+
# try to track that (would require keeping track of each line's width).
14021404
if self.active_area_rows > 0:
14031405
buffer.write(f"\033[{self.active_area_rows}A\r")
14041406

14051407
if self.terminal_size_changed:
14061408
self.terminal_size = self.get_terminal_size()
14071409
self.terminal_size_changed = False
1410+
# After resize, active_area_rows is invalidated due to possible line wrapping. Set to
1411+
# 0 to force newlines instead of cursor movement.
1412+
self.active_area_rows = 0
14081413
max_width, max_height = self.terminal_size
14091414

1410-
self.total_lines = 0
1411-
total_finished = len(self.finished_builds)
1412-
14131415
# First flush the finished builds. These are "persisted" in terminal history.
1414-
for build in self.finished_builds:
1415-
self._render_build(build, buffer, now=now)
1416-
self.finished_builds.clear()
1416+
if self.finished_builds:
1417+
for build in self.finished_builds:
1418+
self._render_build(build, buffer, now=now)
1419+
self._println(buffer, force_newline=True) # should scroll the terminal
1420+
self.finished_builds.clear()
1421+
# Finished builds can span multiple lines, overlapping our "active area", invalidating
1422+
# active_area_rows. Set to 0 to force newlines instead of cursor movement.
1423+
self.active_area_rows = 0
14171424

14181425
# Then a header followed by the active builds. This is the "mutable" part of the display.
1426+
self.total_lines = 0
14191427

14201428
if not finalize:
14211429
if self.color:
@@ -1461,6 +1469,7 @@ def update(self, finalize: bool = False) -> None:
14611469
self._println(buffer, f"{len_builds - i + 1} more...")
14621470
break
14631471
self._render_build(build, buffer, max_width, now=now)
1472+
self._println(buffer)
14641473

14651474
if self.search_mode:
14661475
buffer.write(f"filter> {self.search_term}\033[K")
@@ -1473,18 +1482,18 @@ def update(self, finalize: bool = False) -> None:
14731482
self.stdout.flush()
14741483

14751484
# Update the number of lines drawn for next time. It reflects the number of active builds.
1476-
self.active_area_rows = self.total_lines - total_finished
1485+
self.active_area_rows = self.total_lines
14771486
self.dirty = False
14781487

14791488
# Schedule next UI update
14801489
self.next_update = now + SPINNER_INTERVAL / 2
14811490

1482-
def _println(self, buffer: io.StringIO, line: str = "") -> None:
1491+
def _println(self, buffer: io.StringIO, line: str = "", force_newline: bool = False) -> None:
14831492
"""Print a line to the buffer, handling line clearing and cursor movement."""
14841493
self.total_lines += 1
14851494
if line:
14861495
buffer.write(line)
1487-
if self.total_lines > self.active_area_rows:
1496+
if self.total_lines > self.active_area_rows or force_newline:
14881497
buffer.write("\033[0m\033[K\n") # reset, clear to EOL, newline
14891498
else:
14901499
buffer.write("\033[0m\033[K\033[1B\r") # reset, clear to EOL, move to next line
@@ -1515,7 +1524,6 @@ def _render_build(
15151524
if line_width > max_width:
15161525
break
15171526
buffer.write(component)
1518-
self._println(buffer)
15191527

15201528
def _generate_line_components(
15211529
self, build_info: BuildInfo, static: bool = False, now: float = 0.0

0 commit comments

Comments
 (0)