Skip to content

Commit 930dfbd

Browse files
committed
alternate screen
1 parent 122b813 commit 930dfbd

4 files changed

Lines changed: 82 additions & 5 deletions

File tree

docs/source/console.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,24 @@ Since the default pager on most platforms don't support color, Rich will strip c
298298
.. note::
299299
Rich will use the ``PAGER`` environment variable to get the pager command. On Linux and macOS you can set this to ``less -r`` to enable paging with ANSI styles.
300300

301+
Alternate screen
302+
----------------
303+
304+
Terminals support an 'alternate screen' mode which consists of a single screen which doesn't scroll. This mode is typically used for more application-like interfaces. Rich supports this mode via the :meth:`~rich.console.Console.set_alt_screen` method, but it is recommended that you use :meth:`~rich.console.Console.screen` which returns a context manager. The context manager ensures that normal terminal operations are restored on exit.
305+
306+
Here's an example of an alternate screen::
307+
308+
from time import sleep
309+
from rich.console import Console
310+
311+
console = Console()
312+
with console.screen():
313+
console.print(locals())
314+
sleep(5)
315+
console.print("Back to normal terminal")
316+
317+
.. node::
318+
If you ever find yourself stuck in alternate mode after exiting Python code, type ``reset`` in the terminal
301319

302320
Terminal detection
303321
------------------

examples/table_movie.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,11 @@ def beat(length: int = 1) -> None:
7575
console.clear()
7676

7777
with Live(
78-
table_centered, console=console, refresh_per_second=10, vertical_overflow="ellipsis"
78+
table_centered,
79+
console=console,
80+
screen=True,
81+
refresh_per_second=10,
82+
vertical_overflow="ellipsis",
7983
):
8084

8185
with beat(10):

rich/console.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,22 @@ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
250250
self._console._exit_buffer()
251251

252252

253+
class ScreenContext:
254+
"""A context manager that enables an alternative screen. See :meth:`~rich.console.Console.screen` for usage."""
255+
256+
def __init__(self, console: "Console") -> None:
257+
self.console = console
258+
self._changed = False
259+
260+
def __enter__(self) -> "ScreenContext":
261+
self._changed = self.console.set_alt_screen(True)
262+
return self
263+
264+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
265+
if self._changed:
266+
self.console.set_alt_screen(False)
267+
268+
253269
class RenderGroup:
254270
"""Takes a group of renderables and returns a renderable object that renders the group.
255271
@@ -869,14 +885,43 @@ def status(
869885
)
870886
return status_renderable
871887

872-
def show_cursor(self, show: bool = True) -> None:
888+
def show_cursor(self, show: bool = True) -> bool:
873889
"""Show or hide the cursor.
874890
875891
Args:
876892
show (bool, optional): Set visibility of the cursor.
877893
"""
878894
if self.is_terminal and not self.legacy_windows:
879895
self.control("\033[?25h" if show else "\033[?25l")
896+
return True
897+
return False
898+
899+
def set_alt_screen(self, enable: bool = True) -> bool:
900+
"""Enables alternative screen mode.
901+
902+
Note, if you enable this mode, you should ensure that is disabled before
903+
the application exits. See :meth:`~rich.Console.screen` for a context manager
904+
that handles this for you.
905+
906+
Args:
907+
enable (bool, optional): [description]. Defaults to True.
908+
909+
Returns:
910+
bool: True if the control codes were written.
911+
912+
"""
913+
if self.is_terminal and not self.legacy_windows:
914+
self.control("\033[?1049h\033[H" if enable else "\033[?1049l")
915+
return True
916+
return False
917+
918+
def screen(self) -> "ScreenContext":
919+
"""Context manager to enable and disable 'alternative screen' mode.
920+
921+
Returns:
922+
~ScreenContext: Context which enables alternate screen on enter, and disables it on exit.
923+
"""
924+
return ScreenContext(self)
880925

881926
def render(
882927
self, renderable: RenderableType, options: ConsoleOptions = None

rich/live.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class Live(JupyterMixin, RenderHook):
3636
Args:
3737
renderable (RenderableType, optional): The renderable to live display. Defaults to displaying nothing.
3838
console (Console, optional): Optional Console instance. Default will an internal Console instance writing to stdout.
39+
screen (bool, optional): Enable alternate screen mode. Defaults to False.
3940
auto_refresh (bool, optional): Enable auto refresh. If disabled, you will need to call `refresh()` or `update()` with refresh flag. Defaults to True
4041
refresh_per_second (float, optional): Number of times per second to refresh the live display. Defaults to 1.
4142
transient (bool, optional): Clear the renderable on exit. Defaults to False.
@@ -50,6 +51,7 @@ def __init__(
5051
renderable: RenderableType = None,
5152
*,
5253
console: Console = None,
54+
screen: bool = False,
5355
auto_refresh: bool = True,
5456
refresh_per_second: float = 4,
5557
transient: bool = False,
@@ -61,6 +63,8 @@ def __init__(
6163
assert refresh_per_second > 0, "refresh_per_second must be > 0"
6264
self._renderable = renderable
6365
self.console = console if console is not None else get_console()
66+
self._screen = screen
67+
self._alt_screen = False
6468

6569
self._redirect_stdout = redirect_stdout
6670
self._redirect_stderr = redirect_stderr
@@ -102,6 +106,8 @@ def start(self, refresh=False) -> None:
102106
return
103107
self.console.set_live(self)
104108
self._started = True
109+
if self._screen:
110+
self._alt_screen = self.console.set_alt_screen(True)
105111
self.console.show_cursor(False)
106112
self._enable_redirect_io()
107113
self.console.push_render_hook(self)
@@ -128,14 +134,16 @@ def stop(self) -> None:
128134
if self.console.is_terminal:
129135
self.console.line()
130136
finally:
131-
self.console.show_cursor(True)
132137
self._disable_redirect_io()
133138
self.console.pop_render_hook()
139+
self.console.show_cursor(True)
140+
if self._alt_screen:
141+
self.console.set_alt_screen(False)
134142

135143
if self._refresh_thread is not None:
136144
self._refresh_thread.join()
137145
self._refresh_thread = None
138-
if self.transient:
146+
if self.transient and not self._screen:
139147
self.console.control(self._live_render.restore_cursor())
140148
if self.ipy_widget is not None: # pragma: no cover
141149
if self.transient:
@@ -231,7 +239,9 @@ def process_renderables(
231239
with self._lock:
232240
# determine the control command needed to clear previous rendering
233241
renderables = [
234-
self._live_render.position_cursor(),
242+
Control("\033[H")
243+
if self._alt_screen
244+
else self._live_render.position_cursor(),
235245
*renderables,
236246
self._live_render,
237247
]

0 commit comments

Comments
 (0)