run_in_terminal.py 3.5 KB

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