|
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 |
|
|
|
|
|
|
|
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 a Telnet/SSH server. |
|
""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if input is None: |
|
input = get_app_session()._input |
|
if output is None: |
|
output = get_app_session()._output |
|
|
|
|
|
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 |
|
|