"""IPython terminal interface using prompt_toolkit in place of readline""" from __future__ import print_function import sys from IPython.core.interactiveshell import InteractiveShell from IPython.utils.py3compat import PY3, cast_unicode_py2, input from traitlets import Bool, Unicode, Dict from prompt_toolkit.completion import Completer, Completion from prompt_toolkit.enums import DEFAULT_BUFFER from prompt_toolkit.filters import HasFocus, HasSelection from prompt_toolkit.history import InMemoryHistory from prompt_toolkit.shortcuts import create_prompt_application, create_eventloop from prompt_toolkit.interface import CommandLineInterface from prompt_toolkit.key_binding.manager import KeyBindingManager from prompt_toolkit.key_binding.vi_state import InputMode from prompt_toolkit.key_binding.bindings.vi import ViStateFilter from prompt_toolkit.keys import Keys from prompt_toolkit.layout.lexers import PygmentsLexer from prompt_toolkit.styles import PygmentsStyle from pygments.styles import get_style_by_name from pygments.lexers import Python3Lexer, PythonLexer from pygments.token import Token from .pt_inputhooks import get_inputhook_func from .interactiveshell import get_default_editor 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): if not document.current_line.strip(): return 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) class TerminalInteractiveShell(InteractiveShell): colors_force = True pt_cli = None vi_mode = Bool(False, config=True, help="Use vi style keybindings at the prompt", ) mouse_support = Bool(False, config=True, help="Enable mouse support in the prompt" ) 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" ) editor = Unicode(get_default_editor(), config=True, help="Set the editor used by IPython (default to $EDITOR/vi/notepad)." ) def get_prompt_tokens(self, cli): return [ (Token.Prompt, 'In ['), (Token.PromptNum, str(self.execution_count)), (Token.Prompt, ']: '), ] def get_continuation_tokens(self, cli, width): return [ (Token.Prompt, (' ' * (width - 2)) + ': '), ] def init_prompt_toolkit_cli(self): if not sys.stdin.isatty(): # Piped input - e.g. for tests. Fall back to plain non-interactive # output. This is very limited, and only accepts a single line. def prompt(): return cast_unicode_py2(input('In [%d]: ' % self.execution_count)) self.prompt_for_code = prompt return kbmanager = KeyBindingManager.for_prompt(enable_vi_mode=self.vi_mode) insert_mode = ViStateFilter(kbmanager.get_vi_state, InputMode.INSERT) # Ctrl+J == Enter, seemingly @kbmanager.registry.add_binding(Keys.ControlJ, filter=(HasFocus(DEFAULT_BUFFER) & ~HasSelection() & insert_mode )) def _(event): b = event.current_buffer d = b.document if not (d.on_last_line or d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end()): b.newline() return status, indent = self.input_splitter.check_complete(d.text) if (status != 'incomplete') and b.accept_action.is_returnable: b.accept_action.validate_and_handle(event.cli, b) else: b.insert_text('\n' + (' ' * (indent or 0))) @kbmanager.registry.add_binding(Keys.ControlC) def _(event): event.current_buffer.reset() # 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) style_overrides = { Token.Prompt: '#009900', Token.PromptNum: '#00ff00 bold', } if self.highlighting_style: style_cls = get_style_by_name(self.highlighting_style) else: style_cls = get_style_by_name('default') # 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. style_overrides.update({ Token.Number: '#007700', Token.Operator: 'noinherit', Token.String: '#BB6622', Token.Name.Function: '#2080D0', Token.Name.Class: 'bold #2080D0', Token.Name.Namespace: 'bold #2080D0', }) style_overrides.update(self.highlighting_style_overrides) style = PygmentsStyle.from_defaults(pygments_style_cls=style_cls, style_dict=style_overrides) app = create_prompt_application(multiline=True, lexer=PygmentsLexer(Python3Lexer if PY3 else PythonLexer), get_prompt_tokens=self.get_prompt_tokens, # The line below is waiting for a new release of # prompt_toolkit (> 0.57) #get_continuation_tokens=self.get_continuation_tokens, key_bindings_registry=kbmanager.registry, history=history, completer=IPythonPTCompleter(self.Completer), enable_history_search=True, style=style, mouse_support=self.mouse_support, ) self.pt_cli = CommandLineInterface(app, eventloop=create_eventloop(self.inputhook)) def prompt_for_code(self): document = self.pt_cli.run(pre_run=self.pre_prompt) return document.text 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) def __init__(self, *args, **kwargs): super(TerminalInteractiveShell, self).__init__(*args, **kwargs) self.init_prompt_toolkit_cli() self.keep_running = True def ask_exit(self): self.keep_running = False rl_next_input = None def pre_prompt(self): if self.rl_next_input: self.pt_cli.application.buffer.text = cast_unicode_py2(self.rl_next_input) self.rl_next_input = None def interact(self): while self.keep_running: print(self.separate_in, end='') try: code = self.prompt_for_code() except EOFError: if self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'): self.ask_exit() else: if code: self.run_cell(code, store_history=True) 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") _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 if __name__ == '__main__': TerminalInteractiveShell.instance().interact()