diff --git a/CHANGELOG.md b/CHANGELOG.md index e53205666f..4251130f10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Fixed + +- Added `CURSOR_PREV_LINE` (`\e[nF`) and `CURSOR_PREV_LINE` (`\e[nG`) to `ControlType`. +- Changed `Console.render_lines` to include `ControlType.ERASE_IN_LINE` at the end of each line. +- Changed `Console.render_lines` to include a `ControlType.CLEAR` sequence at the end of the last line, if present. +- Changed `LiveRender.position_cursor`'s cursor-moving logic and removed line clearing from there. + + +To achieve this, I had to introduce `\e[nF` (move to the start of n lines up) in `ControlType` and I also included the not-used `\e[nG` (move to the start of n lines down) for completeness. I also had to modify `ControlType.CLEAR` to accept a parameter and backfixed any cases where it was already used. + ## [14.2.0] - 2025-10-09 ### Changed diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 4b04786b9c..8436520345 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -94,3 +94,4 @@ The following people have contributed to the development of Rich: - [Jonathan Helmus](https://github.com/jjhelmus) - [Brandon Capener](https://github.com/bcapener) - [Alex Zheng](https://github.com/alexzheng111) +- [Dimitris Krestos](https://github.com/dimitriskres) \ No newline at end of file diff --git a/rich/console.py b/rich/console.py index 994adfc069..aeb4d79797 100644 --- a/rich/console.py +++ b/rich/console.py @@ -55,7 +55,7 @@ from .region import Region from .scope import render_scope from .screen import Screen -from .segment import Segment +from .segment import Segment, ControlType from .style import Style, StyleType from .styled import Styled from .terminal_theme import DEFAULT_TERMINAL_THEME, SVG_EXPORT_THEME, TerminalTheme @@ -1395,6 +1395,11 @@ def render_lines( render_height, ) ) + + for line in lines: + erase_control = Control((ControlType.ERASE_IN_LINE, 0)) + line.append(erase_control.segment) + if render_options.height is not None: extra_lines = render_options.height - len(lines) if extra_lines > 0: @@ -2043,6 +2048,8 @@ def _write_buffer(self) -> None: """Write the buffer to the output file.""" with self._lock: + self._buffer.append(Control((ControlType.CLEAR, 0)).segment) + if self.record and not self._buffer_index: with self._record_buffer_lock: self._record_buffer.extend(self._buffer[:]) @@ -2062,7 +2069,6 @@ def _write_buffer(self) -> None: use_legacy_windows_render = ( fileno in _STD_STREAMS_OUTPUT ) - if use_legacy_windows_render: from rich._win32_console import LegacyWindowsTerm from rich._windows_renderer import legacy_windows_render diff --git a/rich/control.py b/rich/control.py index 248b0f595a..486decb1a4 100644 --- a/rich/control.py +++ b/rich/control.py @@ -29,13 +29,15 @@ ControlType.BELL: lambda: "\x07", ControlType.CARRIAGE_RETURN: lambda: "\r", ControlType.HOME: lambda: "\x1b[H", - ControlType.CLEAR: lambda: "\x1b[2J", + ControlType.CLEAR: lambda param: f"\x1b[{param}J", ControlType.ENABLE_ALT_SCREEN: lambda: "\x1b[?1049h", ControlType.DISABLE_ALT_SCREEN: lambda: "\x1b[?1049l", ControlType.SHOW_CURSOR: lambda: "\x1b[?25h", ControlType.HIDE_CURSOR: lambda: "\x1b[?25l", ControlType.CURSOR_UP: lambda param: f"\x1b[{param}A", ControlType.CURSOR_DOWN: lambda param: f"\x1b[{param}B", + ControlType.CURSOR_PREV_LINE: lambda param: f"\x1b[{param}F", + ControlType.CURSOR_NEXT_LINE: lambda param: f"\x1b[{param}E", ControlType.CURSOR_FORWARD: lambda param: f"\x1b[{param}C", ControlType.CURSOR_BACKWARD: lambda param: f"\x1b[{param}D", ControlType.CURSOR_MOVE_TO_COLUMN: lambda param: f"\x1b[{param+1}G", @@ -144,7 +146,7 @@ def move_to(cls, x: int, y: int) -> "Control": @classmethod def clear(cls) -> "Control": """Clear the screen.""" - return cls(ControlType.CLEAR) + return cls((ControlType.CLEAR, 2)) @classmethod def show_cursor(cls, show: bool) -> "Control": diff --git a/rich/live_render.py b/rich/live_render.py index d3da5111d8..d2ec971716 100644 --- a/rich/live_render.py +++ b/rich/live_render.py @@ -46,17 +46,9 @@ def position_cursor(self) -> Control: """ if self._shape is not None: _, height = self._shape - return Control( - ControlType.CARRIAGE_RETURN, - (ControlType.ERASE_IN_LINE, 2), - *( - ( - (ControlType.CURSOR_UP, 1), - (ControlType.ERASE_IN_LINE, 2), - ) - * (height - 1) - ) - ) + if height > 1: + return Control((ControlType.CURSOR_PREV_LINE, height - 1)) + return Control(ControlType.CARRIAGE_RETURN) return Control() def restore_cursor(self) -> Control: diff --git a/rich/segment.py b/rich/segment.py index edcb52dd3f..4b9071c2c1 100644 --- a/rich/segment.py +++ b/rich/segment.py @@ -51,6 +51,8 @@ class ControlType(IntEnum): CURSOR_MOVE_TO = 14 ERASE_IN_LINE = 15 SET_WINDOW_TITLE = 16 + CURSOR_PREV_LINE = 17 + CURSOR_NEXT_LINE = 18 ControlCode = Union[