1"""
2Tools for running functions on the terminal above the current application or prompt.
3"""
4
5from __future__ import annotations
6
7from asyncio import Future, ensure_future
8from contextlib import asynccontextmanager
9from typing import AsyncGenerator, Awaitable, Callable, TypeVar
10
11from prompt_toolkit.eventloop import run_in_executor_with_context
12
13from .current import get_app_or_none
14
15__all__ = [
16 "run_in_terminal",
17 "in_terminal",
18]
19
20_T = TypeVar("_T")
21
22
23def run_in_terminal(
24 func: Callable[[], _T], render_cli_done: bool = False, in_executor: bool = False
25) -> Awaitable[_T]:
26 """
27 Run function on the terminal above the current application or prompt.
28
29 What this does is first hiding the prompt, then running this callable
30 (which can safely output to the terminal), and then again rendering the
31 prompt which causes the output of this function to scroll above the
32 prompt.
33
34 ``func`` is supposed to be a synchronous function. If you need an
35 asynchronous version of this function, use the ``in_terminal`` context
36 manager directly.
37
38 :param func: The callable to execute.
39 :param render_cli_done: When True, render the interface in the
40 'Done' state first, then execute the function. If False,
41 erase the interface first.
42 :param in_executor: When True, run in executor. (Use this for long
43 blocking functions, when you don't want to block the event loop.)
44
45 :returns: A `Future`.
46 """
47
48 async def run() -> _T:
49 async with in_terminal(render_cli_done=render_cli_done):
50 if in_executor:
51 return await run_in_executor_with_context(func)
52 else:
53 return func()
54
55 return ensure_future(run())
56
57
58@asynccontextmanager
59async def in_terminal(render_cli_done: bool = False) -> AsyncGenerator[None, None]:
60 """
61 Asynchronous context manager that suspends the current application and runs
62 the body in the terminal.
63
64 .. code::
65
66 async def f():
67 async with in_terminal():
68 call_some_function()
69 await call_some_async_function()
70 """
71 app = get_app_or_none()
72 if app is None or not app._is_running:
73 yield
74 return
75
76 # When a previous `run_in_terminal` call was in progress. Wait for that
77 # to finish, before starting this one. Chain to previous call.
78 previous_run_in_terminal_f = app._running_in_terminal_f
79 new_run_in_terminal_f: Future[None] = Future()
80 app._running_in_terminal_f = new_run_in_terminal_f
81
82 # Wait for the previous `run_in_terminal` to finish.
83 if previous_run_in_terminal_f is not None:
84 await previous_run_in_terminal_f
85
86 # Wait for all CPRs to arrive. We don't want to detach the input until
87 # all cursor position responses have been arrived. Otherwise, the tty
88 # will echo its input and can show stuff like ^[[39;1R.
89 if app.output.responds_to_cpr:
90 await app.renderer.wait_for_cpr_responses()
91
92 # Draw interface in 'done' state, or erase.
93 if render_cli_done:
94 app._redraw(render_as_done=True)
95 else:
96 app.renderer.erase()
97
98 # Disable rendering.
99 app._running_in_terminal = True
100
101 # Detach input.
102 try:
103 with app.input.detach():
104 with app.input.cooked_mode():
105 yield
106 finally:
107 # Redraw interface again.
108 try:
109 app._running_in_terminal = False
110 app.renderer.reset()
111 app._request_absolute_cursor_position()
112 app._redraw()
113 finally:
114 # (Check for `.done()`, because it can be that this future was
115 # cancelled.)
116 if not new_run_in_terminal_f.done():
117 new_run_in_terminal_f.set_result(None)