ptshell.py
289 lines
| 10.5 KiB
| text/x-python
|
PythonLexer
Thomas Kluyver
|
r21930 | """IPython terminal interface using prompt_toolkit in place of readline""" | ||
from __future__ import print_function | ||||
Thomas Kluyver
|
r22128 | import os | ||
Thomas Kluyver
|
r21950 | import sys | ||
Thomas Kluyver
|
r21911 | from IPython.core.interactiveshell import InteractiveShell | ||
Thomas Kluyver
|
r22107 | from IPython.utils.py3compat import PY3, cast_unicode_py2, input | ||
Min RK
|
r22124 | from IPython.utils.terminal import toggle_set_term_title, set_term_title | ||
from IPython.utils.process import abbrev_cwd | ||||
Thomas Kluyver
|
r21929 | from traitlets import Bool, Unicode, Dict | ||
Thomas Kluyver
|
r21911 | |||
Thomas Kluyver
|
r21916 | from prompt_toolkit.completion import Completer, Completion | ||
Thomas Kluyver
|
r21923 | from prompt_toolkit.enums import DEFAULT_BUFFER | ||
Thomas Kluyver
|
r22136 | from prompt_toolkit.filters import HasFocus, HasSelection, Condition | ||
Thomas Kluyver
|
r21914 | from prompt_toolkit.history import InMemoryHistory | ||
Thomas Kluyver
|
r21934 | from prompt_toolkit.shortcuts import create_prompt_application, create_eventloop | ||
Thomas Kluyver
|
r21912 | from prompt_toolkit.interface import CommandLineInterface | ||
from prompt_toolkit.key_binding.manager import KeyBindingManager | ||||
Thomas Kluyver
|
r21928 | from prompt_toolkit.key_binding.vi_state import InputMode | ||
from prompt_toolkit.key_binding.bindings.vi import ViStateFilter | ||||
Thomas Kluyver
|
r21912 | from prompt_toolkit.keys import Keys | ||
Thomas Kluyver
|
r21911 | from prompt_toolkit.layout.lexers import PygmentsLexer | ||
Thomas Kluyver
|
r21918 | from prompt_toolkit.styles import PygmentsStyle | ||
Thomas Kluyver
|
r21911 | |||
Thomas Kluyver
|
r21929 | from pygments.styles import get_style_by_name | ||
Thomas Kluyver
|
r21930 | from pygments.lexers import Python3Lexer, PythonLexer | ||
Thomas Kluyver
|
r21911 | from pygments.token import Token | ||
Thomas Kluyver
|
r21934 | from .pt_inputhooks import get_inputhook_func | ||
Thomas Kluyver
|
r22128 | from .interactiveshell import get_default_editor, TerminalMagics | ||
Thomas Kluyver
|
r21934 | |||
Thomas Kluyver
|
r21911 | |||
Min RK
|
r22124 | |||
Thomas Kluyver
|
r21916 | class IPythonPTCompleter(Completer): | ||
"""Adaptor to provide IPython completions to prompt_toolkit""" | ||||
def __init__(self, ipy_completer): | ||||
self.ipy_completer = ipy_completer | ||||
def get_completions(self, document, complete_event): | ||||
Thomas Kluyver
|
r21922 | if not document.current_line.strip(): | ||
return | ||||
Thomas Kluyver
|
r21916 | used, matches = self.ipy_completer.complete( | ||
line_buffer=document.current_line, | ||||
cursor_pos=document.cursor_position_col | ||||
) | ||||
start_pos = -len(used) | ||||
for m in matches: | ||||
yield Completion(m, start_position=start_pos) | ||||
Thomas Kluyver
|
r22112 | class TerminalInteractiveShell(InteractiveShell): | ||
Thomas Kluyver
|
r21920 | colors_force = True | ||
Thomas Kluyver
|
r21911 | pt_cli = None | ||
Thomas Kluyver
|
r21928 | vi_mode = Bool(False, config=True, | ||
help="Use vi style keybindings at the prompt", | ||||
) | ||||
Thomas Kluyver
|
r22055 | mouse_support = Bool(False, config=True, | ||
help="Enable mouse support in the prompt" | ||||
) | ||||
Thomas Kluyver
|
r21929 | highlighting_style = Unicode('', config=True, | ||
help="The name of a Pygments style to use for syntax highlighting" | ||||
) | ||||
highlighting_style_overrides = Dict(config=True, | ||||
help="Override highlighting format for specific tokens" | ||||
) | ||||
Thomas Kluyver
|
r21969 | editor = Unicode(get_default_editor(), config=True, | ||
help="Set the editor used by IPython (default to $EDITOR/vi/notepad)." | ||||
) | ||||
Min RK
|
r22124 | |||
term_title = Bool(True, config=True, | ||||
help="Automatically set the terminal title" | ||||
) | ||||
def _term_title_changed(self, name, new_value): | ||||
self.init_term_title() | ||||
def init_term_title(self): | ||||
# Enable or disable the terminal title. | ||||
if self.term_title: | ||||
toggle_set_term_title(True) | ||||
set_term_title('IPython: ' + abbrev_cwd()) | ||||
else: | ||||
toggle_set_term_title(False) | ||||
Thomas Kluyver
|
r21969 | |||
Thomas Kluyver
|
r21911 | def get_prompt_tokens(self, cli): | ||
return [ | ||||
(Token.Prompt, 'In ['), | ||||
Thomas Kluyver
|
r21918 | (Token.PromptNum, str(self.execution_count)), | ||
Thomas Kluyver
|
r21911 | (Token.Prompt, ']: '), | ||
] | ||||
Thomas Kluyver
|
r21933 | def get_continuation_tokens(self, cli, width): | ||
return [ | ||||
Matthias Bussonnier
|
r22122 | (Token.Prompt, (' ' * (width - 5)) + '...: '), | ||
Thomas Kluyver
|
r21933 | ] | ||
Thomas Kluyver
|
r21911 | def init_prompt_toolkit_cli(self): | ||
Thomas Kluyver
|
r22132 | if ('IPY_TEST_SIMPLE_PROMPT' in os.environ) or not sys.stdin.isatty(): | ||
# Fall back to plain non-interactive output for tests. | ||||
# This is very limited, and only accepts a single line. | ||||
Thomas Kluyver
|
r22107 | def prompt(): | ||
return cast_unicode_py2(input('In [%d]: ' % self.execution_count)) | ||||
self.prompt_for_code = prompt | ||||
return | ||||
Thomas Kluyver
|
r21928 | kbmanager = KeyBindingManager.for_prompt(enable_vi_mode=self.vi_mode) | ||
insert_mode = ViStateFilter(kbmanager.get_vi_state, InputMode.INSERT) | ||||
Thomas Kluyver
|
r21923 | # Ctrl+J == Enter, seemingly | ||
@kbmanager.registry.add_binding(Keys.ControlJ, | ||||
Thomas Kluyver
|
r21928 | filter=(HasFocus(DEFAULT_BUFFER) | ||
& ~HasSelection() | ||||
& insert_mode | ||||
)) | ||||
Thomas Kluyver
|
r21912 | def _(event): | ||
b = event.current_buffer | ||||
Thomas Kluyver
|
r22012 | d = b.document | ||
if not (d.on_last_line or d.cursor_position_row >= d.line_count | ||||
- d.empty_line_count_at_the_end()): | ||||
Thomas Kluyver
|
r21912 | b.newline() | ||
return | ||||
Thomas Kluyver
|
r22012 | status, indent = self.input_splitter.check_complete(d.text) | ||
Thomas Kluyver
|
r21912 | |||
if (status != 'incomplete') and b.accept_action.is_returnable: | ||||
b.accept_action.validate_and_handle(event.cli, b) | ||||
else: | ||||
Thomas Kluyver
|
r21914 | b.insert_text('\n' + (' ' * (indent or 0))) | ||
Thomas Kluyver
|
r21915 | @kbmanager.registry.add_binding(Keys.ControlC) | ||
def _(event): | ||||
event.current_buffer.reset() | ||||
Thomas Kluyver
|
r22136 | @Condition | ||
def cursor_in_leading_ws(cli): | ||||
before = cli.application.buffer.document.current_line_before_cursor | ||||
return (not before) or before.isspace() | ||||
# Ctrl+I == Tab | ||||
@kbmanager.registry.add_binding(Keys.ControlI, | ||||
filter=(HasFocus(DEFAULT_BUFFER) | ||||
& ~HasSelection() | ||||
& insert_mode | ||||
& cursor_in_leading_ws | ||||
)) | ||||
def _(event): | ||||
event.current_buffer.insert_text(' ' * 4) | ||||
Thomas Kluyver
|
r21914 | # Pre-populate history from IPython's history database | ||
history = InMemoryHistory() | ||||
last_cell = u"" | ||||
for _, _, cell in self.history_manager.get_tail(self.history_load_length, | ||||
include_latest=True): | ||||
# Ignore blank lines and consecutive duplicates | ||||
cell = cell.rstrip() | ||||
if cell and (cell != last_cell): | ||||
history.append(cell) | ||||
Thomas Kluyver
|
r21912 | |||
Thomas Kluyver
|
r21929 | style_overrides = { | ||
Thomas Kluyver
|
r21918 | Token.Prompt: '#009900', | ||
Token.PromptNum: '#00ff00 bold', | ||||
Thomas Kluyver
|
r21929 | } | ||
if self.highlighting_style: | ||||
style_cls = get_style_by_name(self.highlighting_style) | ||||
else: | ||||
style_cls = get_style_by_name('default') | ||||
Thomas Kluyver
|
r21939 | # The default theme needs to be visible on both a dark background | ||
# and a light background, because we can't tell what the terminal | ||||
# looks like. These tweaks to the default theme help with that. | ||||
Thomas Kluyver
|
r21929 | style_overrides.update({ | ||
Token.Number: '#007700', | ||||
Token.Operator: 'noinherit', | ||||
Token.String: '#BB6622', | ||||
Thomas Kluyver
|
r21939 | Token.Name.Function: '#2080D0', | ||
Token.Name.Class: 'bold #2080D0', | ||||
Token.Name.Namespace: 'bold #2080D0', | ||||
Thomas Kluyver
|
r21929 | }) | ||
style_overrides.update(self.highlighting_style_overrides) | ||||
style = PygmentsStyle.from_defaults(pygments_style_cls=style_cls, | ||||
style_dict=style_overrides) | ||||
Thomas Kluyver
|
r21918 | |||
Thomas Kluyver
|
r21912 | app = create_prompt_application(multiline=True, | ||
Thomas Kluyver
|
r21930 | lexer=PygmentsLexer(Python3Lexer if PY3 else PythonLexer), | ||
Thomas Kluyver
|
r21912 | get_prompt_tokens=self.get_prompt_tokens, | ||
Thomas Kluyver
|
r22120 | get_continuation_tokens=self.get_continuation_tokens, | ||
Thomas Kluyver
|
r21912 | key_bindings_registry=kbmanager.registry, | ||
Thomas Kluyver
|
r21914 | history=history, | ||
Thomas Kluyver
|
r21916 | completer=IPythonPTCompleter(self.Completer), | ||
Thomas Kluyver
|
r21924 | enable_history_search=True, | ||
Thomas Kluyver
|
r21918 | style=style, | ||
Thomas Kluyver
|
r22055 | mouse_support=self.mouse_support, | ||
Thomas Kluyver
|
r21911 | ) | ||
Thomas Kluyver
|
r21912 | |||
Thomas Kluyver
|
r21934 | self.pt_cli = CommandLineInterface(app, | ||
eventloop=create_eventloop(self.inputhook)) | ||||
Thomas Kluyver
|
r21911 | |||
Thomas Kluyver
|
r22107 | def prompt_for_code(self): | ||
document = self.pt_cli.run(pre_run=self.pre_prompt) | ||||
return document.text | ||||
Thomas Kluyver
|
r21950 | def init_io(self): | ||
if sys.platform not in {'win32', 'cli'}: | ||||
return | ||||
import colorama | ||||
colorama.init() | ||||
# For some reason we make these wrappers around stdout/stderr. | ||||
# For now, we need to reset them so all output gets coloured. | ||||
# https://github.com/ipython/ipython/issues/8669 | ||||
from IPython.utils import io | ||||
io.stdout = io.IOStream(sys.stdout) | ||||
io.stderr = io.IOStream(sys.stderr) | ||||
Thomas Kluyver
|
r22128 | def init_magics(self): | ||
super(TerminalInteractiveShell, self).init_magics() | ||||
self.register_magics(TerminalMagics) | ||||
def init_alias(self): | ||||
# The parent class defines aliases that can be safely used with any | ||||
# frontend. | ||||
super(TerminalInteractiveShell, self).init_alias() | ||||
# Now define aliases that only make sense on the terminal, because they | ||||
# need direct access to the console in a way that we can't emulate in | ||||
# GUI or web frontend | ||||
if os.name == 'posix': | ||||
for cmd in ['clear', 'more', 'less', 'man']: | ||||
self.alias_manager.soft_define_alias(cmd, cmd) | ||||
Thomas Kluyver
|
r21911 | def __init__(self, *args, **kwargs): | ||
Thomas Kluyver
|
r22112 | super(TerminalInteractiveShell, self).__init__(*args, **kwargs) | ||
Thomas Kluyver
|
r21911 | self.init_prompt_toolkit_cli() | ||
Min RK
|
r22124 | self.init_term_title() | ||
Thomas Kluyver
|
r21911 | self.keep_running = True | ||
def ask_exit(self): | ||||
self.keep_running = False | ||||
Thomas Kluyver
|
r21948 | rl_next_input = None | ||
def pre_prompt(self): | ||||
if self.rl_next_input: | ||||
Thomas Kluyver
|
r22011 | self.pt_cli.application.buffer.text = cast_unicode_py2(self.rl_next_input) | ||
Thomas Kluyver
|
r21948 | self.rl_next_input = None | ||
Thomas Kluyver
|
r21911 | def interact(self): | ||
while self.keep_running: | ||||
Thomas Kluyver
|
r21921 | print(self.separate_in, end='') | ||
Thomas Kluyver
|
r21948 | |||
Thomas Kluyver
|
r21915 | try: | ||
Thomas Kluyver
|
r22107 | code = self.prompt_for_code() | ||
Thomas Kluyver
|
r21915 | except EOFError: | ||
if self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'): | ||||
self.ask_exit() | ||||
else: | ||||
Thomas Kluyver
|
r22107 | if code: | ||
self.run_cell(code, store_history=True) | ||||
Thomas Kluyver
|
r21911 | |||
Thomas Kluyver
|
r22054 | def mainloop(self): | ||
# An extra layer of protection in case someone mashing Ctrl-C breaks | ||||
# out of our internal code. | ||||
while True: | ||||
try: | ||||
self.interact() | ||||
break | ||||
except KeyboardInterrupt: | ||||
print("\nKeyboardInterrupt escaped interact()\n") | ||||
Thomas Kluyver
|
r21934 | _inputhook = None | ||
def inputhook(self, context): | ||||
if self._inputhook is not None: | ||||
self._inputhook(context) | ||||
def enable_gui(self, gui=None): | ||||
if gui: | ||||
self._inputhook = get_inputhook_func(gui) | ||||
else: | ||||
self._inputhook = None | ||||
Thomas Kluyver
|
r21911 | |||
if __name__ == '__main__': | ||||
Thomas Kluyver
|
r22112 | TerminalInteractiveShell.instance().interact() | ||