Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/prompt_toolkit/application/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions src/prompt_toolkit/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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:
Expand All @@ -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:
"""
Expand Down Expand Up @@ -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:
Expand Down
81 changes: 81 additions & 0 deletions tests/test_application.py
Original file line number Diff line number Diff line change
@@ -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
Loading