##// END OF EJS Templates
Merge pull request #9778 from takluyver/i9768...
Matthias Bussonnier -
r22732:0845fa68 merge
parent child Browse files
Show More
@@ -1,457 +1,479 b''
1 1 """IPython terminal interface using prompt_toolkit"""
2 2 from __future__ import print_function
3 3
4 4 import os
5 5 import sys
6 6 from warnings import warn
7 7
8 8 from IPython.core.interactiveshell import InteractiveShell, InteractiveShellABC
9 9 from IPython.utils.py3compat import PY3, cast_unicode_py2, input
10 10 from IPython.utils.terminal import toggle_set_term_title, set_term_title
11 11 from IPython.utils.process import abbrev_cwd
12 12 from traitlets import Bool, Unicode, Dict, Integer, observe, Instance, Type, default, Enum
13 13
14 14 from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode
15 15 from prompt_toolkit.filters import (HasFocus, Condition, IsDone)
16 16 from prompt_toolkit.history import InMemoryHistory
17 17 from prompt_toolkit.shortcuts import create_prompt_application, create_eventloop, create_prompt_layout, create_output
18 18 from prompt_toolkit.interface import CommandLineInterface
19 19 from prompt_toolkit.key_binding.manager import KeyBindingManager
20 20 from prompt_toolkit.layout.processors import ConditionalProcessor, HighlightMatchingBracketProcessor
21 21 from prompt_toolkit.styles import PygmentsStyle, DynamicStyle
22 22
23 23 from pygments.styles import get_style_by_name, get_all_styles
24 24 from pygments.token import Token
25 25
26 26 from .debugger import TerminalPdb, Pdb
27 27 from .magics import TerminalMagics
28 28 from .pt_inputhooks import get_inputhook_func
29 29 from .prompts import Prompts, ClassicPrompts, RichPromptDisplayHook
30 30 from .ptutils import IPythonPTCompleter, IPythonPTLexer
31 31 from .shortcuts import register_ipython_shortcuts
32 32
33 33 DISPLAY_BANNER_DEPRECATED = object()
34 34
35 35
36 36 from pygments.style import Style
37 37
38 38 class _NoStyle(Style): pass
39 39
40 40
41 41
42 42 _style_overrides_light_bg = {
43 43 Token.Prompt: '#0000ff',
44 44 Token.PromptNum: '#0000ee bold',
45 45 Token.OutPrompt: '#cc0000',
46 46 Token.OutPromptNum: '#bb0000 bold',
47 47 }
48 48
49 49 _style_overrides_linux = {
50 50 Token.Prompt: '#00cc00',
51 51 Token.PromptNum: '#00bb00 bold',
52 52 Token.OutPrompt: '#cc0000',
53 53 Token.OutPromptNum: '#bb0000 bold',
54 54 }
55 55
56 56
57 57
58 58 def get_default_editor():
59 59 try:
60 60 ed = os.environ['EDITOR']
61 61 if not PY3:
62 62 ed = ed.decode()
63 63 return ed
64 64 except KeyError:
65 65 pass
66 66 except UnicodeError:
67 67 warn("$EDITOR environment variable is not pure ASCII. Using platform "
68 68 "default editor.")
69 69
70 70 if os.name == 'posix':
71 71 return 'vi' # the only one guaranteed to be there!
72 72 else:
73 73 return 'notepad' # same in Windows!
74 74
75 75
76 76 if sys.stdin and sys.stdout and sys.stderr:
77 77 _is_tty = (sys.stdin.isatty()) and (sys.stdout.isatty()) and (sys.stderr.isatty())
78 78 else:
79 79 _is_tty = False
80 80
81 81
82 82 _use_simple_prompt = ('IPY_TEST_SIMPLE_PROMPT' in os.environ) or (not _is_tty)
83 83
84 84 class TerminalInteractiveShell(InteractiveShell):
85 85 space_for_menu = Integer(6, help='Number of line at the bottom of the screen '
86 86 'to reserve for the completion menu'
87 87 ).tag(config=True)
88 88
89 89 def _space_for_menu_changed(self, old, new):
90 90 self._update_layout()
91 91
92 92 pt_cli = None
93 93 debugger_history = None
94 94 _pt_app = None
95 95
96 96 simple_prompt = Bool(_use_simple_prompt,
97 97 help="""Use `raw_input` for the REPL, without completion, multiline input, and prompt colors.
98 98
99 99 Useful when controlling IPython as a subprocess, and piping STDIN/OUT/ERR. Known usage are:
100 100 IPython own testing machinery, and emacs inferior-shell integration through elpy.
101 101
102 102 This mode default to `True` if the `IPY_TEST_SIMPLE_PROMPT`
103 103 environment variable is set, or the current terminal is not a tty.
104 104
105 105 """
106 106 ).tag(config=True)
107 107
108 108 @property
109 109 def debugger_cls(self):
110 110 return Pdb if self.simple_prompt else TerminalPdb
111 111
112 112 confirm_exit = Bool(True,
113 113 help="""
114 114 Set to confirm when you try to exit IPython with an EOF (Control-D
115 115 in Unix, Control-Z/Enter in Windows). By typing 'exit' or 'quit',
116 116 you can force a direct exit without any confirmation.""",
117 117 ).tag(config=True)
118 118
119 119 editing_mode = Unicode('emacs',
120 120 help="Shortcut style to use at the prompt. 'vi' or 'emacs'.",
121 121 ).tag(config=True)
122 122
123 123 mouse_support = Bool(False,
124 124 help="Enable mouse support in the prompt"
125 125 ).tag(config=True)
126 126
127 127 highlighting_style = Unicode('legacy',
128 128 help="The name of a Pygments style to use for syntax highlighting: \n %s" % ', '.join(get_all_styles())
129 129 ).tag(config=True)
130 130
131 131
132 132 @observe('highlighting_style')
133 133 @observe('colors')
134 134 def _highlighting_style_changed(self, change):
135 135 self.refresh_style()
136 136
137 137 def refresh_style(self):
138 138 self._style = self._make_style_from_name(self.highlighting_style)
139 139
140 140
141 141 highlighting_style_overrides = Dict(
142 142 help="Override highlighting format for specific tokens"
143 143 ).tag(config=True)
144 144
145 145 true_color = Bool(False,
146 146 help=("Use 24bit colors instead of 256 colors in prompt highlighting. "
147 147 "If your terminal supports true color, the following command "
148 148 "should print 'TRUECOLOR' in orange: "
149 149 "printf \"\\x1b[38;2;255;100;0mTRUECOLOR\\x1b[0m\\n\"")
150 150 ).tag(config=True)
151 151
152 152 editor = Unicode(get_default_editor(),
153 153 help="Set the editor used by IPython (default to $EDITOR/vi/notepad)."
154 154 ).tag(config=True)
155 155
156 156 prompts_class = Type(Prompts, help='Class used to generate Prompt token for prompt_toolkit').tag(config=True)
157 157
158 158 prompts = Instance(Prompts)
159 159
160 160 @default('prompts')
161 161 def _prompts_default(self):
162 162 return self.prompts_class(self)
163 163
164 164 @observe('prompts')
165 165 def _(self, change):
166 166 self._update_layout()
167 167
168 168 @default('displayhook_class')
169 169 def _displayhook_class_default(self):
170 170 return RichPromptDisplayHook
171 171
172 172 term_title = Bool(True,
173 173 help="Automatically set the terminal title"
174 174 ).tag(config=True)
175 175
176 176 display_completions = Enum(('column', 'multicolumn','readlinelike'),
177 177 help= ( "Options for displaying tab completions, 'column', 'multicolumn', and "
178 178 "'readlinelike'. These options are for `prompt_toolkit`, see "
179 179 "`prompt_toolkit` documentation for more information."
180 180 ),
181 181 default_value='multicolumn').tag(config=True)
182 182
183 183 highlight_matching_brackets = Bool(True,
184 184 help="Highlight matching brackets .",
185 185 ).tag(config=True)
186 186
187 187 @observe('term_title')
188 188 def init_term_title(self, change=None):
189 189 # Enable or disable the terminal title.
190 190 if self.term_title:
191 191 toggle_set_term_title(True)
192 192 set_term_title('IPython: ' + abbrev_cwd())
193 193 else:
194 194 toggle_set_term_title(False)
195 195
196 196 def init_display_formatter(self):
197 197 super(TerminalInteractiveShell, self).init_display_formatter()
198 198 # terminal only supports plain text
199 199 self.display_formatter.active_types = ['text/plain']
200 200
201 201 def init_prompt_toolkit_cli(self):
202 202 if self.simple_prompt:
203 203 # Fall back to plain non-interactive output for tests.
204 204 # This is very limited, and only accepts a single line.
205 205 def prompt():
206 206 return cast_unicode_py2(input('In [%d]: ' % self.execution_count))
207 207 self.prompt_for_code = prompt
208 208 return
209 209
210 210 # Set up keyboard shortcuts
211 211 kbmanager = KeyBindingManager.for_prompt()
212 212 register_ipython_shortcuts(kbmanager.registry, self)
213 213
214 214 # Pre-populate history from IPython's history database
215 215 history = InMemoryHistory()
216 216 last_cell = u""
217 217 for __, ___, cell in self.history_manager.get_tail(self.history_load_length,
218 218 include_latest=True):
219 219 # Ignore blank lines and consecutive duplicates
220 220 cell = cell.rstrip()
221 221 if cell and (cell != last_cell):
222 222 history.append(cell)
223 223
224 224 self._style = self._make_style_from_name(self.highlighting_style)
225 225 style = DynamicStyle(lambda: self._style)
226 226
227 227 editing_mode = getattr(EditingMode, self.editing_mode.upper())
228 228
229 229 self._pt_app = create_prompt_application(
230 230 editing_mode=editing_mode,
231 231 key_bindings_registry=kbmanager.registry,
232 232 history=history,
233 233 completer=IPythonPTCompleter(self.Completer),
234 234 enable_history_search=True,
235 235 style=style,
236 236 mouse_support=self.mouse_support,
237 237 **self._layout_options()
238 238 )
239 239 self._eventloop = create_eventloop(self.inputhook)
240 240 self.pt_cli = CommandLineInterface(
241 241 self._pt_app, eventloop=self._eventloop,
242 242 output=create_output(true_color=self.true_color))
243 243
244 244 def _make_style_from_name(self, name):
245 245 """
246 246 Small wrapper that make an IPython compatible style from a style name
247 247
248 248 We need that to add style for prompt ... etc.
249 249 """
250 250 style_overrides = {}
251 251 if name == 'legacy':
252 252 legacy = self.colors.lower()
253 253 if legacy == 'linux':
254 254 style_cls = get_style_by_name('monokai')
255 255 style_overrides = _style_overrides_linux
256 256 elif legacy == 'lightbg':
257 257 style_overrides = _style_overrides_light_bg
258 258 style_cls = get_style_by_name('pastie')
259 259 elif legacy == 'neutral':
260 260 # The default theme needs to be visible on both a dark background
261 261 # and a light background, because we can't tell what the terminal
262 262 # looks like. These tweaks to the default theme help with that.
263 263 style_cls = get_style_by_name('default')
264 264 style_overrides.update({
265 265 Token.Number: '#007700',
266 266 Token.Operator: 'noinherit',
267 267 Token.String: '#BB6622',
268 268 Token.Name.Function: '#2080D0',
269 269 Token.Name.Class: 'bold #2080D0',
270 270 Token.Name.Namespace: 'bold #2080D0',
271 271 Token.Prompt: '#009900',
272 272 Token.PromptNum: '#00ff00 bold',
273 273 Token.OutPrompt: '#990000',
274 274 Token.OutPromptNum: '#ff0000 bold',
275 275 })
276 276 elif legacy =='nocolor':
277 277 style_cls=_NoStyle
278 278 style_overrides = {}
279 279 else :
280 280 raise ValueError('Got unknown colors: ', legacy)
281 281 else :
282 282 style_cls = get_style_by_name(name)
283 283 style_overrides = {
284 284 Token.Prompt: '#009900',
285 285 Token.PromptNum: '#00ff00 bold',
286 286 Token.OutPrompt: '#990000',
287 287 Token.OutPromptNum: '#ff0000 bold',
288 288 }
289 289 style_overrides.update(self.highlighting_style_overrides)
290 290 style = PygmentsStyle.from_defaults(pygments_style_cls=style_cls,
291 291 style_dict=style_overrides)
292 292
293 293 return style
294 294
295 295 def _layout_options(self):
296 296 """
297 297 Return the current layout option for the current Terminal InteractiveShell
298 298 """
299 299 return {
300 300 'lexer':IPythonPTLexer(),
301 301 'reserve_space_for_menu':self.space_for_menu,
302 302 'get_prompt_tokens':self.prompts.in_prompt_tokens,
303 303 'get_continuation_tokens':self.prompts.continuation_prompt_tokens,
304 304 'multiline':True,
305 305 'display_completions_in_columns': (self.display_completions == 'multicolumn'),
306 306
307 307 # Highlight matching brackets, but only when this setting is
308 308 # enabled, and only when the DEFAULT_BUFFER has the focus.
309 309 'extra_input_processors': [ConditionalProcessor(
310 310 processor=HighlightMatchingBracketProcessor(chars='[](){}'),
311 311 filter=HasFocus(DEFAULT_BUFFER) & ~IsDone() &
312 312 Condition(lambda cli: self.highlight_matching_brackets))],
313 313 }
314 314
315 315 def _update_layout(self):
316 316 """
317 317 Ask for a re computation of the application layout, if for example ,
318 318 some configuration options have changed.
319 319 """
320 320 if self._pt_app:
321 321 self._pt_app.layout = create_prompt_layout(**self._layout_options())
322 322
323 323 def prompt_for_code(self):
324 324 document = self.pt_cli.run(
325 325 pre_run=self.pre_prompt, reset_current_buffer=True)
326 326 return document.text
327 327
328 def enable_win_unicode_console(self):
329 import win_unicode_console
330
331 if PY3:
332 win_unicode_console.enable()
333 else:
334 # https://github.com/ipython/ipython/issues/9768
335 from win_unicode_console.streams import (TextStreamWrapper,
336 stdout_text_transcoded, stderr_text_transcoded)
337
338 class LenientStrStreamWrapper(TextStreamWrapper):
339 def write(self, s):
340 if isinstance(s, bytes):
341 s = s.decode(self.encoding, 'replace')
342
343 self.base.write(s)
344
345 stdout_text_str = LenientStrStreamWrapper(stdout_text_transcoded)
346 stderr_text_str = LenientStrStreamWrapper(stderr_text_transcoded)
347
348 win_unicode_console.enable(stdout=stdout_text_str,
349 stderr=stderr_text_str)
350
328 351 def init_io(self):
329 352 if sys.platform not in {'win32', 'cli'}:
330 353 return
331 354
332 import win_unicode_console
333 import colorama
355 self.enable_win_unicode_console()
334 356
335 win_unicode_console.enable()
357 import colorama
336 358 colorama.init()
337 359
338 360 # For some reason we make these wrappers around stdout/stderr.
339 361 # For now, we need to reset them so all output gets coloured.
340 362 # https://github.com/ipython/ipython/issues/8669
341 363 from IPython.utils import io
342 364 io.stdout = io.IOStream(sys.stdout)
343 365 io.stderr = io.IOStream(sys.stderr)
344 366
345 367 def init_magics(self):
346 368 super(TerminalInteractiveShell, self).init_magics()
347 369 self.register_magics(TerminalMagics)
348 370
349 371 def init_alias(self):
350 372 # The parent class defines aliases that can be safely used with any
351 373 # frontend.
352 374 super(TerminalInteractiveShell, self).init_alias()
353 375
354 376 # Now define aliases that only make sense on the terminal, because they
355 377 # need direct access to the console in a way that we can't emulate in
356 378 # GUI or web frontend
357 379 if os.name == 'posix':
358 380 for cmd in ['clear', 'more', 'less', 'man']:
359 381 self.alias_manager.soft_define_alias(cmd, cmd)
360 382
361 383
362 384 def __init__(self, *args, **kwargs):
363 385 super(TerminalInteractiveShell, self).__init__(*args, **kwargs)
364 386 self.init_prompt_toolkit_cli()
365 387 self.init_term_title()
366 388 self.keep_running = True
367 389
368 390 self.debugger_history = InMemoryHistory()
369 391
370 392 def ask_exit(self):
371 393 self.keep_running = False
372 394
373 395 rl_next_input = None
374 396
375 397 def pre_prompt(self):
376 398 if self.rl_next_input:
377 399 self.pt_cli.application.buffer.text = cast_unicode_py2(self.rl_next_input)
378 400 self.rl_next_input = None
379 401
380 402 def interact(self, display_banner=DISPLAY_BANNER_DEPRECATED):
381 403
382 404 if display_banner is not DISPLAY_BANNER_DEPRECATED:
383 405 warn('interact `display_banner` argument is deprecated since IPython 5.0. Call `show_banner()` if needed.', DeprecationWarning, stacklevel=2)
384 406
385 407 while self.keep_running:
386 408 print(self.separate_in, end='')
387 409
388 410 try:
389 411 code = self.prompt_for_code()
390 412 except EOFError:
391 413 if (not self.confirm_exit) \
392 414 or self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'):
393 415 self.ask_exit()
394 416
395 417 else:
396 418 if code:
397 419 self.run_cell(code, store_history=True)
398 420
399 421 def mainloop(self, display_banner=DISPLAY_BANNER_DEPRECATED):
400 422 # An extra layer of protection in case someone mashing Ctrl-C breaks
401 423 # out of our internal code.
402 424 if display_banner is not DISPLAY_BANNER_DEPRECATED:
403 425 warn('mainloop `display_banner` argument is deprecated since IPython 5.0. Call `show_banner()` if needed.', DeprecationWarning, stacklevel=2)
404 426 while True:
405 427 try:
406 428 self.interact()
407 429 break
408 430 except KeyboardInterrupt:
409 431 print("\nKeyboardInterrupt escaped interact()\n")
410 432
411 433 if hasattr(self, '_eventloop'):
412 434 self._eventloop.close()
413 435
414 436 _inputhook = None
415 437 def inputhook(self, context):
416 438 if self._inputhook is not None:
417 439 self._inputhook(context)
418 440
419 441 def enable_gui(self, gui=None):
420 442 if gui:
421 443 self._inputhook = get_inputhook_func(gui)
422 444 else:
423 445 self._inputhook = None
424 446
425 447 # Run !system commands directly, not through pipes, so terminal programs
426 448 # work correctly.
427 449 system = InteractiveShell.system_raw
428 450
429 451 def auto_rewrite_input(self, cmd):
430 452 """Overridden from the parent class to use fancy rewriting prompt"""
431 453 if not self.show_rewritten_input:
432 454 return
433 455
434 456 tokens = self.prompts.rewrite_prompt_tokens()
435 457 if self.pt_cli:
436 458 self.pt_cli.print_tokens(tokens)
437 459 print(cmd)
438 460 else:
439 461 prompt = ''.join(s for t, s in tokens)
440 462 print(prompt, cmd, sep='')
441 463
442 464 _prompts_before = None
443 465 def switch_doctest_mode(self, mode):
444 466 """Switch prompts to classic for %doctest_mode"""
445 467 if mode:
446 468 self._prompts_before = self.prompts
447 469 self.prompts = ClassicPrompts(self)
448 470 elif self._prompts_before:
449 471 self.prompts = self._prompts_before
450 472 self._prompts_before = None
451 473 self._update_layout()
452 474
453 475
454 476 InteractiveShellABC.register(TerminalInteractiveShell)
455 477
456 478 if __name__ == '__main__':
457 479 TerminalInteractiveShell.instance().interact()
General Comments 0
You need to be logged in to leave comments. Login now