##// END OF EJS Templates
Use system_raw in ptshell...
Thomas Kluyver -
Show More
@@ -1,404 +1,409 b''
1 1 """IPython terminal interface using prompt_toolkit in place of readline"""
2 2 from __future__ import print_function
3 3
4 4 import os
5 5 import sys
6 6 import signal
7 7 import unicodedata
8 8 from warnings import warn
9 9 from wcwidth import wcwidth
10 10
11 11 from IPython.core.error import TryNext
12 12 from IPython.core.interactiveshell import InteractiveShell
13 13 from IPython.utils.py3compat import PY3, cast_unicode_py2, input
14 14 from IPython.utils.terminal import toggle_set_term_title, set_term_title
15 15 from IPython.utils.process import abbrev_cwd
16 16 from traitlets import Bool, CBool, Unicode, Dict
17 17
18 18 from prompt_toolkit.completion import Completer, Completion
19 19 from prompt_toolkit.enums import DEFAULT_BUFFER
20 20 from prompt_toolkit.filters import HasFocus, HasSelection, Condition
21 21 from prompt_toolkit.history import InMemoryHistory
22 22 from prompt_toolkit.shortcuts import create_prompt_application, create_eventloop
23 23 from prompt_toolkit.interface import CommandLineInterface
24 24 from prompt_toolkit.key_binding.manager import KeyBindingManager
25 25 from prompt_toolkit.key_binding.vi_state import InputMode
26 26 from prompt_toolkit.key_binding.bindings.vi import ViStateFilter
27 27 from prompt_toolkit.keys import Keys
28 28 from prompt_toolkit.layout.lexers import Lexer
29 29 from prompt_toolkit.layout.lexers import PygmentsLexer
30 30 from prompt_toolkit.styles import PygmentsStyle
31 31
32 32 from pygments.styles import get_style_by_name
33 33 from pygments.lexers import Python3Lexer, BashLexer, PythonLexer
34 34 from pygments.token import Token
35 35
36 36 from .pt_inputhooks import get_inputhook_func
37 37 from .interactiveshell import get_default_editor, TerminalMagics
38 38
39 39
40 40 class IPythonPTCompleter(Completer):
41 41 """Adaptor to provide IPython completions to prompt_toolkit"""
42 42 def __init__(self, ipy_completer):
43 43 self.ipy_completer = ipy_completer
44 44
45 45 def get_completions(self, document, complete_event):
46 46 if not document.current_line.strip():
47 47 return
48 48
49 49 used, matches = self.ipy_completer.complete(
50 50 line_buffer=document.current_line,
51 51 cursor_pos=document.cursor_position_col
52 52 )
53 53 start_pos = -len(used)
54 54 for m in matches:
55 55 m = unicodedata.normalize('NFC', m)
56 56
57 57 # When the first character of the completion has a zero length,
58 58 # then it's probably a decomposed unicode character. E.g. caused by
59 59 # the "\dot" completion. Try to compose again with the previous
60 60 # character.
61 61 if wcwidth(m[0]) == 0:
62 62 if document.cursor_position + start_pos > 0:
63 63 char_before = document.text[document.cursor_position + start_pos - 1]
64 64 m = unicodedata.normalize('NFC', char_before + m)
65 65
66 66 # Yield the modified completion instead, if this worked.
67 67 if wcwidth(m[0:1]) == 1:
68 68 yield Completion(m, start_position=start_pos - 1)
69 69 continue
70 70
71 71 yield Completion(m, start_position=start_pos)
72 72
73 73
74 74 class IPythonPTLexer(Lexer):
75 75 """
76 76 Wrapper around PythonLexer and BashLexer.
77 77 """
78 78 def __init__(self):
79 79 self.python_lexer = PygmentsLexer(Python3Lexer if PY3 else PythonLexer)
80 80 self.shell_lexer = PygmentsLexer(BashLexer)
81 81
82 82 def lex_document(self, cli, document):
83 83 if document.text.startswith('!'):
84 84 return self.shell_lexer.lex_document(cli, document)
85 85 else:
86 86 return self.python_lexer.lex_document(cli, document)
87 87
88 88
89 89 class TerminalInteractiveShell(InteractiveShell):
90 90 colors_force = True
91 91
92 92 pt_cli = None
93 93
94 94 autoedit_syntax = CBool(False, config=True,
95 95 help="auto editing of files with syntax errors.")
96 96
97 97 confirm_exit = CBool(True, config=True,
98 98 help="""
99 99 Set to confirm when you try to exit IPython with an EOF (Control-D
100 100 in Unix, Control-Z/Enter in Windows). By typing 'exit' or 'quit',
101 101 you can force a direct exit without any confirmation.""",
102 102 )
103 103 vi_mode = Bool(False, config=True,
104 104 help="Use vi style keybindings at the prompt",
105 105 )
106 106
107 107 mouse_support = Bool(False, config=True,
108 108 help="Enable mouse support in the prompt"
109 109 )
110 110
111 111 highlighting_style = Unicode('', config=True,
112 112 help="The name of a Pygments style to use for syntax highlighting"
113 113 )
114 114
115 115 highlighting_style_overrides = Dict(config=True,
116 116 help="Override highlighting format for specific tokens"
117 117 )
118 118
119 119 editor = Unicode(get_default_editor(), config=True,
120 120 help="Set the editor used by IPython (default to $EDITOR/vi/notepad)."
121 121 )
122 122
123 123 term_title = Bool(True, config=True,
124 124 help="Automatically set the terminal title"
125 125 )
126 126 def _term_title_changed(self, name, new_value):
127 127 self.init_term_title()
128 128
129 129 def init_term_title(self):
130 130 # Enable or disable the terminal title.
131 131 if self.term_title:
132 132 toggle_set_term_title(True)
133 133 set_term_title('IPython: ' + abbrev_cwd())
134 134 else:
135 135 toggle_set_term_title(False)
136 136
137 137 def get_prompt_tokens(self, cli):
138 138 return [
139 139 (Token.Prompt, 'In ['),
140 140 (Token.PromptNum, str(self.execution_count)),
141 141 (Token.Prompt, ']: '),
142 142 ]
143 143
144 144 def get_continuation_tokens(self, cli, width):
145 145 return [
146 146 (Token.Prompt, (' ' * (width - 5)) + '...: '),
147 147 ]
148 148
149 149 def init_prompt_toolkit_cli(self):
150 150 if ('IPY_TEST_SIMPLE_PROMPT' in os.environ) or not sys.stdin.isatty():
151 151 # Fall back to plain non-interactive output for tests.
152 152 # This is very limited, and only accepts a single line.
153 153 def prompt():
154 154 return cast_unicode_py2(input('In [%d]: ' % self.execution_count))
155 155 self.prompt_for_code = prompt
156 156 return
157 157
158 158 kbmanager = KeyBindingManager.for_prompt(enable_vi_mode=self.vi_mode)
159 159 insert_mode = ViStateFilter(kbmanager.get_vi_state, InputMode.INSERT)
160 160 # Ctrl+J == Enter, seemingly
161 161 @kbmanager.registry.add_binding(Keys.ControlJ,
162 162 filter=(HasFocus(DEFAULT_BUFFER)
163 163 & ~HasSelection()
164 164 & insert_mode
165 165 ))
166 166 def _(event):
167 167 b = event.current_buffer
168 168 d = b.document
169 169 if not (d.on_last_line or d.cursor_position_row >= d.line_count
170 170 - d.empty_line_count_at_the_end()):
171 171 b.newline()
172 172 return
173 173
174 174 status, indent = self.input_splitter.check_complete(d.text)
175 175
176 176 if (status != 'incomplete') and b.accept_action.is_returnable:
177 177 b.accept_action.validate_and_handle(event.cli, b)
178 178 else:
179 179 b.insert_text('\n' + (' ' * (indent or 0)))
180 180
181 181 @kbmanager.registry.add_binding(Keys.ControlC, filter=HasFocus(DEFAULT_BUFFER))
182 182 def _(event):
183 183 event.current_buffer.reset()
184 184
185 185 supports_suspend = Condition(lambda cli: hasattr(signal, 'SIGTSTP'))
186 186
187 187 @kbmanager.registry.add_binding(Keys.ControlZ, filter=supports_suspend)
188 188 def _(event):
189 189 event.cli.suspend_to_background()
190 190
191 191 @Condition
192 192 def cursor_in_leading_ws(cli):
193 193 before = cli.application.buffer.document.current_line_before_cursor
194 194 return (not before) or before.isspace()
195 195
196 196 # Ctrl+I == Tab
197 197 @kbmanager.registry.add_binding(Keys.ControlI,
198 198 filter=(HasFocus(DEFAULT_BUFFER)
199 199 & ~HasSelection()
200 200 & insert_mode
201 201 & cursor_in_leading_ws
202 202 ))
203 203 def _(event):
204 204 event.current_buffer.insert_text(' ' * 4)
205 205
206 206 # Pre-populate history from IPython's history database
207 207 history = InMemoryHistory()
208 208 last_cell = u""
209 209 for _, _, cell in self.history_manager.get_tail(self.history_load_length,
210 210 include_latest=True):
211 211 # Ignore blank lines and consecutive duplicates
212 212 cell = cell.rstrip()
213 213 if cell and (cell != last_cell):
214 214 history.append(cell)
215 215
216 216 style_overrides = {
217 217 Token.Prompt: '#009900',
218 218 Token.PromptNum: '#00ff00 bold',
219 219 }
220 220 if self.highlighting_style:
221 221 style_cls = get_style_by_name(self.highlighting_style)
222 222 else:
223 223 style_cls = get_style_by_name('default')
224 224 # The default theme needs to be visible on both a dark background
225 225 # and a light background, because we can't tell what the terminal
226 226 # looks like. These tweaks to the default theme help with that.
227 227 style_overrides.update({
228 228 Token.Number: '#007700',
229 229 Token.Operator: 'noinherit',
230 230 Token.String: '#BB6622',
231 231 Token.Name.Function: '#2080D0',
232 232 Token.Name.Class: 'bold #2080D0',
233 233 Token.Name.Namespace: 'bold #2080D0',
234 234 })
235 235 style_overrides.update(self.highlighting_style_overrides)
236 236 style = PygmentsStyle.from_defaults(pygments_style_cls=style_cls,
237 237 style_dict=style_overrides)
238 238
239 239 app = create_prompt_application(multiline=True,
240 240 lexer=IPythonPTLexer(),
241 241 get_prompt_tokens=self.get_prompt_tokens,
242 242 get_continuation_tokens=self.get_continuation_tokens,
243 243 key_bindings_registry=kbmanager.registry,
244 244 history=history,
245 245 completer=IPythonPTCompleter(self.Completer),
246 246 enable_history_search=True,
247 247 style=style,
248 248 mouse_support=self.mouse_support,
249 249 reserve_space_for_menu=6,
250 250 )
251 251
252 252 self.pt_cli = CommandLineInterface(app,
253 253 eventloop=create_eventloop(self.inputhook))
254 254
255 255 def prompt_for_code(self):
256 256 document = self.pt_cli.run(pre_run=self.pre_prompt)
257 257 return document.text
258 258
259 259 def init_io(self):
260 260 if sys.platform not in {'win32', 'cli'}:
261 261 return
262 262
263 263 import colorama
264 264 colorama.init()
265 265
266 266 # For some reason we make these wrappers around stdout/stderr.
267 267 # For now, we need to reset them so all output gets coloured.
268 268 # https://github.com/ipython/ipython/issues/8669
269 269 from IPython.utils import io
270 270 io.stdout = io.IOStream(sys.stdout)
271 271 io.stderr = io.IOStream(sys.stderr)
272 272
273 273 def init_magics(self):
274 274 super(TerminalInteractiveShell, self).init_magics()
275 275 self.register_magics(TerminalMagics)
276 276
277 277 def init_alias(self):
278 278 # The parent class defines aliases that can be safely used with any
279 279 # frontend.
280 280 super(TerminalInteractiveShell, self).init_alias()
281 281
282 282 # Now define aliases that only make sense on the terminal, because they
283 283 # need direct access to the console in a way that we can't emulate in
284 284 # GUI or web frontend
285 285 if os.name == 'posix':
286 286 for cmd in ['clear', 'more', 'less', 'man']:
287 287 self.alias_manager.soft_define_alias(cmd, cmd)
288 288
289 289
290 290 def __init__(self, *args, **kwargs):
291 291 super(TerminalInteractiveShell, self).__init__(*args, **kwargs)
292 292 self.init_prompt_toolkit_cli()
293 293 self.init_term_title()
294 294 self.keep_running = True
295 295
296 296 def ask_exit(self):
297 297 self.keep_running = False
298 298
299 299 rl_next_input = None
300 300
301 301 def pre_prompt(self):
302 302 if self.rl_next_input:
303 303 self.pt_cli.application.buffer.text = cast_unicode_py2(self.rl_next_input)
304 304 self.rl_next_input = None
305 305
306 306 def interact(self):
307 307 while self.keep_running:
308 308 print(self.separate_in, end='')
309 309
310 310 try:
311 311 code = self.prompt_for_code()
312 312 except EOFError:
313 313 if (not self.confirm_exit) \
314 314 or self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'):
315 315 self.ask_exit()
316 316
317 317 else:
318 318 if code:
319 319 self.run_cell(code, store_history=True)
320 320 if self.autoedit_syntax and self.SyntaxTB.last_syntax_error:
321 321 self.edit_syntax_error()
322 322
323 323 def mainloop(self):
324 324 # An extra layer of protection in case someone mashing Ctrl-C breaks
325 325 # out of our internal code.
326 326 while True:
327 327 try:
328 328 self.interact()
329 329 break
330 330 except KeyboardInterrupt:
331 331 print("\nKeyboardInterrupt escaped interact()\n")
332 332
333 333 _inputhook = None
334 334 def inputhook(self, context):
335 335 if self._inputhook is not None:
336 336 self._inputhook(context)
337 337
338 338 def enable_gui(self, gui=None):
339 339 if gui:
340 340 self._inputhook = get_inputhook_func(gui)
341 341 else:
342 342 self._inputhook = None
343 343
344 344 # Methods to support auto-editing of SyntaxErrors:
345 345
346 346 def edit_syntax_error(self):
347 347 """The bottom half of the syntax error handler called in the main loop.
348 348
349 349 Loop until syntax error is fixed or user cancels.
350 350 """
351 351
352 352 while self.SyntaxTB.last_syntax_error:
353 353 # copy and clear last_syntax_error
354 354 err = self.SyntaxTB.clear_err_state()
355 355 if not self._should_recompile(err):
356 356 return
357 357 try:
358 358 # may set last_syntax_error again if a SyntaxError is raised
359 359 self.safe_execfile(err.filename, self.user_ns)
360 360 except:
361 361 self.showtraceback()
362 362 else:
363 363 try:
364 364 with open(err.filename) as f:
365 365 # This should be inside a display_trap block and I
366 366 # think it is.
367 367 sys.displayhook(f.read())
368 368 except:
369 369 self.showtraceback()
370 370
371 371 def _should_recompile(self, e):
372 372 """Utility routine for edit_syntax_error"""
373 373
374 374 if e.filename in ('<ipython console>', '<input>', '<string>',
375 375 '<console>', '<BackgroundJob compilation>',
376 376 None):
377 377 return False
378 378 try:
379 379 if (self.autoedit_syntax and
380 380 not self.ask_yes_no(
381 381 'Return to editor to correct syntax error? '
382 382 '[Y/n] ', 'y')):
383 383 return False
384 384 except EOFError:
385 385 return False
386 386
387 387 def int0(x):
388 388 try:
389 389 return int(x)
390 390 except TypeError:
391 391 return 0
392 392
393 393 # always pass integer line and offset values to editor hook
394 394 try:
395 395 self.hooks.fix_error_editor(e.filename,
396 396 int0(e.lineno), int0(e.offset),
397 397 e.msg)
398 398 except TryNext:
399 399 warn('Could not open editor')
400 400 return False
401 401 return True
402 402
403 # Run !system commands directly, not through pipes, so terminal programs
404 # work correctly.
405 system = InteractiveShell.system_raw
406
407
403 408 if __name__ == '__main__':
404 409 TerminalInteractiveShell.instance().interact()
General Comments 0
You need to be logged in to leave comments. Login now