##// END OF EJS Templates
Write & borrow some inputhooks for prompt_toolkit
Thomas Kluyver -
Show More
@@ -0,0 +1,16 b''
1 import importlib
2 import os
3
4 aliases = {
5 'qt4': 'qt'
6 }
7
8 def get_inputhook_func(gui):
9 if gui in aliases:
10 return get_inputhook_func(aliases[gui])
11
12 if gui == 'qt5':
13 os.environ['QT_API'] = 'pyqt5'
14
15 mod = importlib.import_module('IPython.terminal.pt_inputhooks.'+gui)
16 return mod.inputhook
@@ -0,0 +1,55 b''
1 # Code borrowed from python-prompt-toolkit examples
2 # https://github.com/jonathanslenders/python-prompt-toolkit/blob/77cdcfbc7f4b4c34a9d2f9a34d422d7152f16209/examples/inputhook.py
3
4 # Copyright (c) 2014, Jonathan Slenders
5 # All rights reserved.
6 #
7 # Redistribution and use in source and binary forms, with or without modification,
8 # are permitted provided that the following conditions are met:
9 #
10 # * Redistributions of source code must retain the above copyright notice, this
11 # list of conditions and the following disclaimer.
12 #
13 # * Redistributions in binary form must reproduce the above copyright notice, this
14 # list of conditions and the following disclaimer in the documentation and/or
15 # other materials provided with the distribution.
16 #
17 # * Neither the name of the {organization} nor the names of its
18 # contributors may be used to endorse or promote products derived from
19 # this software without specific prior written permission.
20 #
21 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
22 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24 # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
25 # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
26 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
27 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
28 # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31
32 """
33 PyGTK input hook for prompt_toolkit.
34
35 Listens on the pipe prompt_toolkit sets up for a notification that it should
36 return control to the terminal event loop.
37 """
38
39 import gtk, gobject
40
41 def inputhook(context):
42 """
43 When the eventloop of prompt-toolkit is idle, call this inputhook.
44
45 This will run the GTK main loop until the file descriptor
46 `context.fileno()` becomes ready.
47
48 :param context: An `InputHookContext` instance.
49 """
50 def _main_quit(*a, **kw):
51 gtk.main_quit()
52 return False
53
54 gobject.io_add_watch(context.fileno(), gobject.IO_IN, _main_quit)
55 gtk.main()
@@ -0,0 +1,12 b''
1 """prompt_toolkit input hook for GTK 3
2 """
3
4 from gi.repository import Gtk, GLib
5
6 def _main_quit(*args, **kwargs):
7 Gtk.main_quit()
8 return False
9
10 def inputhook(context):
11 GLib.io_add_watch(context.fileno(), GLib.IO_IN, _main_quit)
12 Gtk.main()
@@ -0,0 +1,11 b''
1 from IPython.external.qt_for_kernel import QtCore, QtGui
2
3 def inputhook(context):
4 app = QtCore.QCoreApplication.instance()
5 if not app:
6 return
7 event_loop = QtCore.QEventLoop(app)
8 notifier = QtCore.QSocketNotifier(context.fileno(), QtCore.QSocketNotifier.Read)
9 notifier.setEnabled(True)
10 notifier.activated.connect(event_loop.exit)
11 event_loop.exec_()
@@ -0,0 +1,93 b''
1 # Code borrowed from ptpython
2 # https://github.com/jonathanslenders/ptpython/blob/86b71a89626114b18898a0af463978bdb32eeb70/ptpython/eventloop.py
3
4 # Copyright (c) 2015, Jonathan Slenders
5 # All rights reserved.
6 #
7 # Redistribution and use in source and binary forms, with or without modification,
8 # are permitted provided that the following conditions are met:
9 #
10 # * Redistributions of source code must retain the above copyright notice, this
11 # list of conditions and the following disclaimer.
12 #
13 # * Redistributions in binary form must reproduce the above copyright notice, this
14 # list of conditions and the following disclaimer in the documentation and/or
15 # other materials provided with the distribution.
16 #
17 # * Neither the name of the {organization} nor the names of its
18 # contributors may be used to endorse or promote products derived from
19 # this software without specific prior written permission.
20 #
21 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
22 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24 # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
25 # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
26 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
27 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
28 # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31
32 """
33 Wrapper around the eventloop that gives some time to the Tkinter GUI to process
34 events when it's loaded and while we are waiting for input at the REPL. This
35 way we don't block the UI of for instance ``turtle`` and other Tk libraries.
36
37 (Normally Tkinter registeres it's callbacks in ``PyOS_InputHook`` to integrate
38 in readline. ``prompt-toolkit`` doesn't understand that input hook, but this
39 will fix it for Tk.)
40 """
41 import time
42
43 import _tkinter
44 try:
45 import tkinter
46 except ImportError:
47 import Tkinter as tkinter # Python 2
48
49 def inputhook(inputhook_context):
50 """
51 Inputhook for Tk.
52 Run the Tk eventloop until prompt-toolkit needs to process the next input.
53 """
54 # Get the current TK application.
55 root = tkinter._default_root
56
57 def wait_using_filehandler():
58 """
59 Run the TK eventloop until the file handler that we got from the
60 inputhook becomes readable.
61 """
62 # Add a handler that sets the stop flag when `prompt-toolkit` has input
63 # to process.
64 stop = [False]
65 def done(*a):
66 stop[0] = True
67
68 root.createfilehandler(inputhook_context.fileno(), _tkinter.READABLE, done)
69
70 # Run the TK event loop as long as we don't receive input.
71 while root.dooneevent(_tkinter.ALL_EVENTS):
72 if stop[0]:
73 break
74
75 root.deletefilehandler(inputhook_context.fileno())
76
77 def wait_using_polling():
78 """
79 Windows TK doesn't support 'createfilehandler'.
80 So, run the TK eventloop and poll until input is ready.
81 """
82 while not inputhook_context.input_is_ready():
83 while root.dooneevent(_tkinter.ALL_EVENTS | _tkinter.DONT_WAIT):
84 pass
85 # Sleep to make the CPU idle, but not too long, so that the UI
86 # stays responsive.
87 time.sleep(.01)
88
89 if root is not None:
90 if hasattr(root, 'createfilehandler'):
91 wait_using_filehandler()
92 else:
93 wait_using_polling()
@@ -1,163 +1,174 b''
1 1 """IPython terminal interface using prompt_toolkit in place of readline"""
2 2 from __future__ import print_function
3 3
4 4 from IPython.core.interactiveshell import InteractiveShell
5 5 from IPython.utils.py3compat import PY3
6 6 from traitlets import Bool, Unicode, Dict
7 7
8 8 from prompt_toolkit.completion import Completer, Completion
9 9 from prompt_toolkit.enums import DEFAULT_BUFFER
10 10 from prompt_toolkit.filters import HasFocus, HasSelection
11 11 from prompt_toolkit.history import InMemoryHistory
12 from prompt_toolkit.shortcuts import create_prompt_application
12 from prompt_toolkit.shortcuts import create_prompt_application, create_eventloop
13 13 from prompt_toolkit.interface import CommandLineInterface
14 14 from prompt_toolkit.key_binding.manager import KeyBindingManager
15 15 from prompt_toolkit.key_binding.vi_state import InputMode
16 16 from prompt_toolkit.key_binding.bindings.vi import ViStateFilter
17 17 from prompt_toolkit.keys import Keys
18 18 from prompt_toolkit.layout.lexers import PygmentsLexer
19 19 from prompt_toolkit.styles import PygmentsStyle
20 20
21 21 from pygments.styles import get_style_by_name
22 22 from pygments.lexers import Python3Lexer, PythonLexer
23 23 from pygments.token import Token
24 24
25 from .pt_inputhooks import get_inputhook_func
26
25 27
26 28 class IPythonPTCompleter(Completer):
27 29 """Adaptor to provide IPython completions to prompt_toolkit"""
28 30 def __init__(self, ipy_completer):
29 31 self.ipy_completer = ipy_completer
30 32
31 33 def get_completions(self, document, complete_event):
32 34 if not document.current_line.strip():
33 35 return
34 36
35 37 used, matches = self.ipy_completer.complete(
36 38 line_buffer=document.current_line,
37 39 cursor_pos=document.cursor_position_col
38 40 )
39 41 start_pos = -len(used)
40 42 for m in matches:
41 43 yield Completion(m, start_position=start_pos)
42 44
43
44 45 class PTInteractiveShell(InteractiveShell):
45 46 colors_force = True
46 47
47 48 pt_cli = None
48 49
49 50 vi_mode = Bool(False, config=True,
50 51 help="Use vi style keybindings at the prompt",
51 52 )
52 53
53 54 highlighting_style = Unicode('', config=True,
54 55 help="The name of a Pygments style to use for syntax highlighting"
55 56 )
56 57
57 58 highlighting_style_overrides = Dict(config=True,
58 59 help="Override highlighting format for specific tokens"
59 60 )
60 61
61 62 def get_prompt_tokens(self, cli):
62 63 return [
63 64 (Token.Prompt, 'In ['),
64 65 (Token.PromptNum, str(self.execution_count)),
65 66 (Token.Prompt, ']: '),
66 67 ]
67 68
68 69 def get_continuation_tokens(self, cli, width):
69 70 return [
70 71 (Token.Prompt, (' ' * (width - 2)) + ': '),
71 72 ]
72 73
73
74 74 def init_prompt_toolkit_cli(self):
75 75 kbmanager = KeyBindingManager.for_prompt(enable_vi_mode=self.vi_mode)
76 76 insert_mode = ViStateFilter(kbmanager.get_vi_state, InputMode.INSERT)
77 77 # Ctrl+J == Enter, seemingly
78 78 @kbmanager.registry.add_binding(Keys.ControlJ,
79 79 filter=(HasFocus(DEFAULT_BUFFER)
80 80 & ~HasSelection()
81 81 & insert_mode
82 82 ))
83 83 def _(event):
84 84 b = event.current_buffer
85 85 if not b.document.on_last_line:
86 86 b.newline()
87 87 return
88 88
89 89 status, indent = self.input_splitter.check_complete(b.document.text)
90 90
91 91 if (status != 'incomplete') and b.accept_action.is_returnable:
92 92 b.accept_action.validate_and_handle(event.cli, b)
93 93 else:
94 94 b.insert_text('\n' + (' ' * (indent or 0)))
95 95
96 96 @kbmanager.registry.add_binding(Keys.ControlC)
97 97 def _(event):
98 98 event.current_buffer.reset()
99 99
100 100 # Pre-populate history from IPython's history database
101 101 history = InMemoryHistory()
102 102 last_cell = u""
103 103 for _, _, cell in self.history_manager.get_tail(self.history_load_length,
104 104 include_latest=True):
105 105 # Ignore blank lines and consecutive duplicates
106 106 cell = cell.rstrip()
107 107 if cell and (cell != last_cell):
108 108 history.append(cell)
109 109
110 110 style_overrides = {
111 111 Token.Prompt: '#009900',
112 112 Token.PromptNum: '#00ff00 bold',
113 113 }
114 114 if self.highlighting_style:
115 115 style_cls = get_style_by_name(self.highlighting_style)
116 116 else:
117 117 style_cls = get_style_by_name('default')
118 118 style_overrides.update({
119 119 Token.Number: '#007700',
120 120 Token.Operator: 'noinherit',
121 121 Token.String: '#BB6622',
122 122 })
123 123 style_overrides.update(self.highlighting_style_overrides)
124 124 style = PygmentsStyle.from_defaults(pygments_style_cls=style_cls,
125 125 style_dict=style_overrides)
126 126
127 127 app = create_prompt_application(multiline=True,
128 128 lexer=PygmentsLexer(Python3Lexer if PY3 else PythonLexer),
129 129 get_prompt_tokens=self.get_prompt_tokens,
130 130 get_continuation_tokens=self.get_continuation_tokens,
131 131 key_bindings_registry=kbmanager.registry,
132 132 history=history,
133 133 completer=IPythonPTCompleter(self.Completer),
134 134 enable_history_search=True,
135 135 style=style,
136 136 )
137 137
138 self.pt_cli = CommandLineInterface(app)
138 self.pt_cli = CommandLineInterface(app,
139 eventloop=create_eventloop(self.inputhook))
139 140
140 141 def __init__(self, *args, **kwargs):
141 142 super(PTInteractiveShell, self).__init__(*args, **kwargs)
142 143 self.init_prompt_toolkit_cli()
143 144 self.keep_running = True
144 145
145 146 def ask_exit(self):
146 147 self.keep_running = False
147 148
148 149 def interact(self):
149 150 while self.keep_running:
150 151 print(self.separate_in, end='')
151 152 try:
152 153 document = self.pt_cli.run()
153 154 except EOFError:
154 155 if self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'):
155 156 self.ask_exit()
156 157
157 158 else:
158 159 if document:
159 160 self.run_cell(document.text, store_history=True)
160 161
162 _inputhook = None
163 def inputhook(self, context):
164 if self._inputhook is not None:
165 self._inputhook(context)
166
167 def enable_gui(self, gui=None):
168 if gui:
169 self._inputhook = get_inputhook_func(gui)
170 else:
171 self._inputhook = None
161 172
162 173 if __name__ == '__main__':
163 174 PTInteractiveShell.instance().interact()
General Comments 0
You need to be logged in to leave comments. Login now