| | """ |
| | Tools for running functions on the terminal above the current application or prompt. |
| | """ |
| |
|
| | from __future__ import annotations |
| |
|
| | from asyncio import Future, ensure_future |
| | from contextlib import asynccontextmanager |
| | from typing import AsyncGenerator, Awaitable, Callable, TypeVar |
| |
|
| | from prompt_toolkit.eventloop import run_in_executor_with_context |
| |
|
| | from .current import get_app_or_none |
| |
|
| | __all__ = [ |
| | "run_in_terminal", |
| | "in_terminal", |
| | ] |
| |
|
| | _T = TypeVar("_T") |
| |
|
| |
|
| | def run_in_terminal( |
| | func: Callable[[], _T], render_cli_done: bool = False, in_executor: bool = False |
| | ) -> Awaitable[_T]: |
| | """ |
| | Run function on the terminal above the current application or prompt. |
| | |
| | What this does is first hiding the prompt, then running this callable |
| | (which can safely output to the terminal), and then again rendering the |
| | prompt which causes the output of this function to scroll above the |
| | prompt. |
| | |
| | ``func`` is supposed to be a synchronous function. If you need an |
| | asynchronous version of this function, use the ``in_terminal`` context |
| | manager directly. |
| | |
| | :param func: The callable to execute. |
| | :param render_cli_done: When True, render the interface in the |
| | 'Done' state first, then execute the function. If False, |
| | erase the interface first. |
| | :param in_executor: When True, run in executor. (Use this for long |
| | blocking functions, when you don't want to block the event loop.) |
| | |
| | :returns: A `Future`. |
| | """ |
| |
|
| | async def run() -> _T: |
| | async with in_terminal(render_cli_done=render_cli_done): |
| | if in_executor: |
| | return await run_in_executor_with_context(func) |
| | else: |
| | return func() |
| |
|
| | return ensure_future(run()) |
| |
|
| |
|
| | @asynccontextmanager |
| | async def in_terminal(render_cli_done: bool = False) -> AsyncGenerator[None, None]: |
| | """ |
| | Asynchronous context manager that suspends the current application and runs |
| | the body in the terminal. |
| | |
| | .. code:: |
| | |
| | async def f(): |
| | async with in_terminal(): |
| | call_some_function() |
| | await call_some_async_function() |
| | """ |
| | app = get_app_or_none() |
| | if app is None or not app._is_running: |
| | yield |
| | return |
| |
|
| | |
| | |
| | previous_run_in_terminal_f = app._running_in_terminal_f |
| | new_run_in_terminal_f: Future[None] = Future() |
| | app._running_in_terminal_f = new_run_in_terminal_f |
| |
|
| | |
| | if previous_run_in_terminal_f is not None: |
| | await previous_run_in_terminal_f |
| |
|
| | |
| | |
| | |
| | if app.output.responds_to_cpr: |
| | await app.renderer.wait_for_cpr_responses() |
| |
|
| | |
| | if render_cli_done: |
| | app._redraw(render_as_done=True) |
| | else: |
| | app.renderer.erase() |
| |
|
| | |
| | app._running_in_terminal = True |
| |
|
| | |
| | try: |
| | with app.input.detach(): |
| | with app.input.cooked_mode(): |
| | yield |
| | finally: |
| | |
| | try: |
| | app._running_in_terminal = False |
| | app.renderer.reset() |
| | app._request_absolute_cursor_position() |
| | app._redraw() |
| | finally: |
| | |
| | |
| | if not new_run_in_terminal_f.done(): |
| | new_run_in_terminal_f.set_result(None) |
| |
|