first comit
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .application import Application
|
||||
from .current import (
|
||||
AppSession,
|
||||
create_app_session,
|
||||
create_app_session_from_tty,
|
||||
get_app,
|
||||
get_app_or_none,
|
||||
get_app_session,
|
||||
set_app,
|
||||
)
|
||||
from .dummy import DummyApplication
|
||||
from .run_in_terminal import in_terminal, run_in_terminal
|
||||
|
||||
__all__ = [
|
||||
# Application.
|
||||
"Application",
|
||||
# Current.
|
||||
"AppSession",
|
||||
"get_app_session",
|
||||
"create_app_session",
|
||||
"create_app_session_from_tty",
|
||||
"get_app",
|
||||
"get_app_or_none",
|
||||
"set_app",
|
||||
# Dummy.
|
||||
"DummyApplication",
|
||||
# Run_in_terminal
|
||||
"in_terminal",
|
||||
"run_in_terminal",
|
||||
]
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,189 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
from contextvars import ContextVar
|
||||
from typing import TYPE_CHECKING, Any, Generator
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from prompt_toolkit.input.base import Input
|
||||
from prompt_toolkit.output.base import Output
|
||||
|
||||
from .application import Application
|
||||
|
||||
__all__ = [
|
||||
"AppSession",
|
||||
"get_app_session",
|
||||
"get_app",
|
||||
"get_app_or_none",
|
||||
"set_app",
|
||||
"create_app_session",
|
||||
"create_app_session_from_tty",
|
||||
]
|
||||
|
||||
|
||||
class AppSession:
|
||||
"""
|
||||
An AppSession is an interactive session, usually connected to one terminal.
|
||||
Within one such session, interaction with many applications can happen, one
|
||||
after the other.
|
||||
|
||||
The input/output device is not supposed to change during one session.
|
||||
|
||||
Warning: Always use the `create_app_session` function to create an
|
||||
instance, so that it gets activated correctly.
|
||||
|
||||
:param input: Use this as a default input for all applications
|
||||
running in this session, unless an input is passed to the `Application`
|
||||
explicitly.
|
||||
:param output: Use this as a default output.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, input: Input | None = None, output: Output | None = None
|
||||
) -> None:
|
||||
self._input = input
|
||||
self._output = output
|
||||
|
||||
# The application will be set dynamically by the `set_app` context
|
||||
# manager. This is called in the application itself.
|
||||
self.app: Application[Any] | None = None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"AppSession(app={self.app!r})"
|
||||
|
||||
@property
|
||||
def input(self) -> Input:
|
||||
if self._input is None:
|
||||
from prompt_toolkit.input.defaults import create_input
|
||||
|
||||
self._input = create_input()
|
||||
return self._input
|
||||
|
||||
@property
|
||||
def output(self) -> Output:
|
||||
if self._output is None:
|
||||
from prompt_toolkit.output.defaults import create_output
|
||||
|
||||
self._output = create_output()
|
||||
return self._output
|
||||
|
||||
|
||||
_current_app_session: ContextVar[AppSession] = ContextVar(
|
||||
"_current_app_session", default=AppSession()
|
||||
)
|
||||
|
||||
|
||||
def get_app_session() -> AppSession:
|
||||
return _current_app_session.get()
|
||||
|
||||
|
||||
def get_app() -> Application[Any]:
|
||||
"""
|
||||
Get the current active (running) Application.
|
||||
An :class:`.Application` is active during the
|
||||
:meth:`.Application.run_async` call.
|
||||
|
||||
We assume that there can only be one :class:`.Application` active at the
|
||||
same time. There is only one terminal window, with only one stdin and
|
||||
stdout. This makes the code significantly easier than passing around the
|
||||
:class:`.Application` everywhere.
|
||||
|
||||
If no :class:`.Application` is running, then return by default a
|
||||
:class:`.DummyApplication`. For practical reasons, we prefer to not raise
|
||||
an exception. This way, we don't have to check all over the place whether
|
||||
an actual `Application` was returned.
|
||||
|
||||
(For applications like pymux where we can have more than one `Application`,
|
||||
we'll use a work-around to handle that.)
|
||||
"""
|
||||
session = _current_app_session.get()
|
||||
if session.app is not None:
|
||||
return session.app
|
||||
|
||||
from .dummy import DummyApplication
|
||||
|
||||
return DummyApplication()
|
||||
|
||||
|
||||
def get_app_or_none() -> Application[Any] | None:
|
||||
"""
|
||||
Get the current active (running) Application, or return `None` if no
|
||||
application is running.
|
||||
"""
|
||||
session = _current_app_session.get()
|
||||
return session.app
|
||||
|
||||
|
||||
@contextmanager
|
||||
def set_app(app: Application[Any]) -> Generator[None, None, None]:
|
||||
"""
|
||||
Context manager that sets the given :class:`.Application` active in an
|
||||
`AppSession`.
|
||||
|
||||
This should only be called by the `Application` itself.
|
||||
The application will automatically be active while its running. If you want
|
||||
the application to be active in other threads/coroutines, where that's not
|
||||
the case, use `contextvars.copy_context()`, or use `Application.context` to
|
||||
run it in the appropriate context.
|
||||
"""
|
||||
session = _current_app_session.get()
|
||||
|
||||
previous_app = session.app
|
||||
session.app = app
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
session.app = previous_app
|
||||
|
||||
|
||||
@contextmanager
|
||||
def create_app_session(
|
||||
input: Input | None = None, output: Output | None = None
|
||||
) -> Generator[AppSession, None, None]:
|
||||
"""
|
||||
Create a separate AppSession.
|
||||
|
||||
This is useful if there can be multiple individual `AppSession`s going on.
|
||||
Like in the case of an Telnet/SSH server.
|
||||
"""
|
||||
# If no input/output is specified, fall back to the current input/output,
|
||||
# whatever that is.
|
||||
if input is None:
|
||||
input = get_app_session().input
|
||||
if output is None:
|
||||
output = get_app_session().output
|
||||
|
||||
# Create new `AppSession` and activate.
|
||||
session = AppSession(input=input, output=output)
|
||||
|
||||
token = _current_app_session.set(session)
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
_current_app_session.reset(token)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def create_app_session_from_tty() -> Generator[AppSession, None, None]:
|
||||
"""
|
||||
Create `AppSession` that always prefers the TTY input/output.
|
||||
|
||||
Even if `sys.stdin` and `sys.stdout` are connected to input/output pipes,
|
||||
this will still use the terminal for interaction (because `sys.stderr` is
|
||||
still connected to the terminal).
|
||||
|
||||
Usage::
|
||||
|
||||
from prompt_toolkit.shortcuts import prompt
|
||||
|
||||
with create_app_session_from_tty():
|
||||
prompt('>')
|
||||
"""
|
||||
from prompt_toolkit.input.defaults import create_input
|
||||
from prompt_toolkit.output.defaults import create_output
|
||||
|
||||
input = create_input(always_prefer_tty=True)
|
||||
output = create_output(always_prefer_tty=True)
|
||||
|
||||
with create_app_session(input=input, output=output) as app_session:
|
||||
yield app_session
|
||||
@@ -0,0 +1,55 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable
|
||||
|
||||
from prompt_toolkit.eventloop import InputHook
|
||||
from prompt_toolkit.formatted_text import AnyFormattedText
|
||||
from prompt_toolkit.input import DummyInput
|
||||
from prompt_toolkit.output import DummyOutput
|
||||
|
||||
from .application import Application
|
||||
|
||||
__all__ = [
|
||||
"DummyApplication",
|
||||
]
|
||||
|
||||
|
||||
class DummyApplication(Application[None]):
|
||||
"""
|
||||
When no :class:`.Application` is running,
|
||||
:func:`.get_app` will run an instance of this :class:`.DummyApplication` instead.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(output=DummyOutput(), input=DummyInput())
|
||||
|
||||
def run(
|
||||
self,
|
||||
pre_run: Callable[[], None] | None = None,
|
||||
set_exception_handler: bool = True,
|
||||
handle_sigint: bool = True,
|
||||
in_thread: bool = False,
|
||||
inputhook: InputHook | None = None,
|
||||
) -> None:
|
||||
raise NotImplementedError("A DummyApplication is not supposed to run.")
|
||||
|
||||
async def run_async(
|
||||
self,
|
||||
pre_run: Callable[[], None] | None = None,
|
||||
set_exception_handler: bool = True,
|
||||
handle_sigint: bool = True,
|
||||
slow_callback_duration: float = 0.5,
|
||||
) -> None:
|
||||
raise NotImplementedError("A DummyApplication is not supposed to run.")
|
||||
|
||||
async def run_system_command(
|
||||
self,
|
||||
command: str,
|
||||
wait_for_enter: bool = True,
|
||||
display_before_text: AnyFormattedText = "",
|
||||
wait_text: str = "",
|
||||
) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def suspend_to_background(self, suspend_group: bool = True) -> None:
|
||||
raise NotImplementedError
|
||||
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
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
|
||||
|
||||
# When a previous `run_in_terminal` call was in progress. Wait for that
|
||||
# to finish, before starting this one. Chain to previous call.
|
||||
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
|
||||
|
||||
# Wait for the previous `run_in_terminal` to finish.
|
||||
if previous_run_in_terminal_f is not None:
|
||||
await previous_run_in_terminal_f
|
||||
|
||||
# Wait for all CPRs to arrive. We don't want to detach the input until
|
||||
# all cursor position responses have been arrived. Otherwise, the tty
|
||||
# will echo its input and can show stuff like ^[[39;1R.
|
||||
if app.output.responds_to_cpr:
|
||||
await app.renderer.wait_for_cpr_responses()
|
||||
|
||||
# Draw interface in 'done' state, or erase.
|
||||
if render_cli_done:
|
||||
app._redraw(render_as_done=True)
|
||||
else:
|
||||
app.renderer.erase()
|
||||
|
||||
# Disable rendering.
|
||||
app._running_in_terminal = True
|
||||
|
||||
# Detach input.
|
||||
try:
|
||||
with app.input.detach():
|
||||
with app.input.cooked_mode():
|
||||
yield
|
||||
finally:
|
||||
# Redraw interface again.
|
||||
try:
|
||||
app._running_in_terminal = False
|
||||
app.renderer.reset()
|
||||
app._request_absolute_cursor_position()
|
||||
app._redraw()
|
||||
finally:
|
||||
new_run_in_terminal_f.set_result(None)
|
||||
Reference in New Issue
Block a user