diff --git a/src/prompt_toolkit/application/application.py b/src/prompt_toolkit/application/application.py index 5e9c6a997..26e8ed85a 100644 --- a/src/prompt_toolkit/application/application.py +++ b/src/prompt_toolkit/application/application.py @@ -416,7 +416,8 @@ def reset(self) -> None: self._background_tasks: set[Task[None]] = set() - self.renderer.reset() + if self.renderer.has_rendered: + self.renderer.reset() self.key_processor.reset() self.layout.reset() self.vi_state.reset() @@ -750,7 +751,8 @@ def flush_input() -> None: # _redraw has a good chance to fail if it calls widgets # with bad code. Make sure to reset the renderer # anyway. - self.renderer.reset() + if self.renderer.has_rendered: + self.renderer.reset() # Unset `is_running`, this ensures that possibly # scheduled draws won't paint during the following diff --git a/src/prompt_toolkit/renderer.py b/src/prompt_toolkit/renderer.py index fcfde223e..f9a78e984 100644 --- a/src/prompt_toolkit/renderer.py +++ b/src/prompt_toolkit/renderer.py @@ -363,6 +363,7 @@ def __init__( self._mouse_support_enabled = False self._bracketed_paste_enabled = False self._cursor_key_mode_reset = False + self._has_rendered = False # Future set when we are waiting for a CPR flag. self._waiting_for_cpr_futures: deque[Future[None]] = deque() @@ -428,6 +429,7 @@ def reset(self, _scroll: bool = False, leave_alternate_screen: bool = True) -> N # Flush output. `disable_mouse_support` needs to write to stdout. self.output.flush() + self._has_rendered = self._in_alternate_screen @property def last_rendered_screen(self) -> Screen | None: @@ -437,6 +439,11 @@ def last_rendered_screen(self) -> Screen | None: """ return self._last_screen + @property + def has_rendered(self) -> bool: + """Whether the renderer currently has terminal state to reset.""" + return self._has_rendered + @property def height_is_known(self) -> bool: """ @@ -598,6 +605,7 @@ def render( won't print any changes to this part. """ output = self.output + self._has_rendered = True # Enter alternate screen. if self.full_screen and not self._in_alternate_screen: diff --git a/tests/test_application.py b/tests/test_application.py new file mode 100644 index 000000000..3bbae6f05 --- /dev/null +++ b/tests/test_application.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +import asyncio + +import pytest + +from prompt_toolkit.application import Application +from prompt_toolkit.input import create_pipe_input +from prompt_toolkit.output import DummyOutput +from prompt_toolkit.renderer import Renderer + + +def test_renderer_is_not_reset_repeatedly_during_application_lifecycle( + monkeypatch, +) -> None: + reset_calls = 0 + reset_calls_before_render: list[int] = [] + original_reset = Renderer.reset + + def reset(self: Renderer, *args: object, **kwargs: object) -> None: + nonlocal reset_calls + reset_calls += 1 + original_reset(self, *args, **kwargs) + + monkeypatch.setattr(Renderer, "reset", reset) + + with create_pipe_input() as input: + app: Application[None] = Application( + input=input, + output=DummyOutput(), + before_render=lambda _: reset_calls_before_render.append(reset_calls), + ) + app.run(pre_run=lambda: app.exit()) + + assert reset_calls_before_render == [1, 1] + assert reset_calls == 2 + + +def test_resize_redraw_failure_quits_full_screen() -> None: + class FullScreenOutput(DummyOutput): + def __init__(self) -> None: + self.alternate_screen_entered = 0 + self.alternate_screen_quit = 0 + + def enter_alternate_screen(self) -> None: + self.alternate_screen_entered += 1 + + def quit_alternate_screen(self) -> None: + self.alternate_screen_quit += 1 + + output = FullScreenOutput() + redraw_error = RuntimeError("redraw failed") + render_count = 0 + + def before_render(app: Application[None]) -> None: + nonlocal render_count + render_count += 1 + + if render_count == 2: + app.exit(exception=redraw_error) + + if render_count >= 2: + raise redraw_error + + with create_pipe_input() as input: + app: Application[None] = Application( + full_screen=True, + input=input, + output=output, + before_render=before_render, + ) + + def resize() -> None: + with pytest.raises(RuntimeError, match="redraw failed"): + app._on_resize() + + with pytest.raises(RuntimeError, match="redraw failed"): + app.run(pre_run=lambda: asyncio.get_running_loop().call_soon(resize)) + + assert output.alternate_screen_entered == 1 + assert output.alternate_screen_quit == 1