Source code for xonsh.shells.ptk_shell.input_hook

"""Serve the built-in ``input()`` from a prompt_toolkit prompt.

CPython's ``input()`` only gets line editing when the ``readline`` module
has been imported: importing it installs ``PyOS_ReadlineFunctionPointer``,
which ``input()`` calls whenever stdin and stdout are interactive. The
prompt_toolkit shell never imports ``readline``, so ``input()`` falls back
to a plain read from the terminal in canonical mode. There the kernel
assembles the line and caps it at ``MAX_CANON`` — 1024 bytes on macOS and
the BSDs, 4096 on Linux. Everything past the cap is discarded, *including*
the terminating newline, so the read never completes and the call hangs
until the user interrupts it. ``getpass.getpass()`` has the same problem:
it clears ``ECHO`` but leaves canonical mode on.

Routing ``input()`` through prompt_toolkit puts the terminal in raw mode
for the duration of the read, which removes the limit and adds the usual
line editing, kill/yank and history keys.

The hook is installed only while the interactive prompt_toolkit shell is
running its command loop (see ``PromptToolkitShell.cmdloop``), and it hands
the call back to the original ``input()`` whenever a prompt_toolkit prompt
cannot be driven safely — off the main thread, without a terminal, or
while another prompt_toolkit application already owns the screen.
"""

import builtins
import sys
import threading

from xonsh.built_ins import XSH

_NOT_GIVEN = object()


[docs] class PTKInputHook: """Callable replacement for ``builtins.input``. Parameters ---------- shell : PromptToolkitShell The shell that owns this hook. Only used to read the editing mode, so the ``input()`` prompt matches the shell's key bindings. """ def __init__(self, shell=None): self.shell = shell self.installed = False self._original = None self._session = None
[docs] def install(self): """Replace ``builtins.input`` with this hook.""" if self.installed: return self._original = builtins.input builtins.input = self self.installed = True
[docs] def restore(self): """Put the original ``builtins.input`` back.""" if not self.installed: return # Someone may have replaced ``input`` after us (a xontrib, ``pdb``, # user code). Their hook wins — dropping ours on the floor is better # than clobbering theirs. if builtins.input is self: builtins.input = self._original self.installed = False self._session = None
[docs] def usable(self): """Whether a prompt_toolkit prompt can be driven right now.""" # Undeclared on purpose: an escape hatch, not a documented setting. if not XSH.env.get("XONSH_PTK_INPUT_HOOK", True): return False # prompt_toolkit needs the main thread: it installs signal handlers # and runs an event loop. Callable aliases execute in worker threads. if threading.current_thread() is not threading.main_thread(): return False try: if not (sys.stdin.isatty() and sys.stdout.isatty()): return False except (AttributeError, OSError, ValueError): # Detached, closed or replaced streams. return False from prompt_toolkit.application.current import get_app_or_none app = get_app_or_none() if app is not None and app.is_running: # Called from a completer, a key binding or a prompt formatter, # i.e. from inside a running application. Nesting would deadlock. return False return True
[docs] def prompt(self, message): """Read one line through a dedicated prompt_toolkit session.""" if self._session is None: from prompt_toolkit.enums import EditingMode from prompt_toolkit.history import InMemoryHistory from prompt_toolkit.shortcuts.prompt import PromptSession editing_mode = ( EditingMode.VI if XSH.env.get("VI_MODE") else EditingMode.EMACS ) # A session of its own, never the shell's: reusing the session # that drives the command prompt hangs, and answers to ``input()`` # have no business in the command history. self._session = PromptSession( history=InMemoryHistory(), editing_mode=editing_mode ) # Grey fish-style suggestion from earlier answers, accepted with the # right arrow — the same mechanic as the command prompt, honoring the # same setting. ``PromptSession`` loads the key bindings that accept a # suggestion on its own. Read per call so the setting can be flipped # while the session is running. from prompt_toolkit.auto_suggest import AutoSuggestFromHistory auto_suggest = ( AutoSuggestFromHistory() if XSH.env.get("XONSH_PROMPT_AUTO_SUGGEST") else None ) return self._session.prompt(message, auto_suggest=auto_suggest)
def __call__(self, prompt=_NOT_GIVEN, /): """Read a line, matching the semantics of the built-in ``input()``.""" if not self.usable(): if prompt is _NOT_GIVEN: return self._original() return self._original(prompt) try: return self.prompt("" if prompt is _NOT_GIVEN else str(prompt)) except (EOFError, KeyboardInterrupt): # Ctrl-D / Ctrl-C: the built-in raises these too. raise except Exception: # A broken prompt_toolkit prompt must never make ``input()`` # unusable — fall back to the interpreter's own implementation. if prompt is _NOT_GIVEN: return self._original() return self._original(prompt)