##// END OF EJS Templates
Use ansi code for soon-to-be released prompt_toolkit....
Matthias Bussonnier -
Show More
@@ -1,530 +1,530 b''
1 1 """IPython terminal interface using prompt_toolkit"""
2 2
3 3 import os
4 4 import sys
5 5 import warnings
6 6 from warnings import warn
7 7
8 8 from IPython.core.interactiveshell import InteractiveShell, InteractiveShellABC
9 9 from IPython.utils import io
10 10 from IPython.utils.py3compat import input
11 11 from IPython.utils.terminal import toggle_set_term_title, set_term_title
12 12 from IPython.utils.process import abbrev_cwd
13 13 from traitlets import (
14 14 Bool, Unicode, Dict, Integer, observe, Instance, Type, default, Enum, Union,
15 15 Any,
16 16 )
17 17
18 18 from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode
19 19 from prompt_toolkit.filters import (HasFocus, Condition, IsDone)
20 20 from prompt_toolkit.formatted_text import PygmentsTokens
21 21 from prompt_toolkit.history import InMemoryHistory
22 22 from prompt_toolkit.layout.processors import ConditionalProcessor, HighlightMatchingBracketProcessor
23 23 from prompt_toolkit.output import ColorDepth
24 24 from prompt_toolkit.patch_stdout import patch_stdout
25 25 from prompt_toolkit.shortcuts import PromptSession, CompleteStyle, print_formatted_text
26 26 from prompt_toolkit.styles import DynamicStyle, merge_styles
27 27 from prompt_toolkit.styles.pygments import style_from_pygments_cls, style_from_pygments_dict
28 28
29 29 from pygments.styles import get_style_by_name
30 30 from pygments.style import Style
31 31 from pygments.token import Token
32 32
33 33 from .debugger import TerminalPdb, Pdb
34 34 from .magics import TerminalMagics
35 35 from .pt_inputhooks import get_inputhook_name_and_func
36 36 from .prompts import Prompts, ClassicPrompts, RichPromptDisplayHook
37 37 from .ptutils import IPythonPTCompleter, IPythonPTLexer
38 38 from .shortcuts import create_ipython_shortcuts
39 39
40 40 DISPLAY_BANNER_DEPRECATED = object()
41 41
42 42
43 43 class _NoStyle(Style): pass
44 44
45 45
46 46
47 47 _style_overrides_light_bg = {
48 48 Token.Prompt: '#0000ff',
49 49 Token.PromptNum: '#0000ee bold',
50 50 Token.OutPrompt: '#cc0000',
51 51 Token.OutPromptNum: '#bb0000 bold',
52 52 }
53 53
54 54 _style_overrides_linux = {
55 55 Token.Prompt: '#00cc00',
56 56 Token.PromptNum: '#00bb00 bold',
57 57 Token.OutPrompt: '#cc0000',
58 58 Token.OutPromptNum: '#bb0000 bold',
59 59 }
60 60
61 61 def get_default_editor():
62 62 try:
63 63 return os.environ['EDITOR']
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 # conservatively check for tty
76 76 # overridden streams can result in things like:
77 77 # - sys.stdin = None
78 78 # - no isatty method
79 79 for _name in ('stdin', 'stdout', 'stderr'):
80 80 _stream = getattr(sys, _name)
81 81 if not _stream or not hasattr(_stream, 'isatty') or not _stream.isatty():
82 82 _is_tty = False
83 83 break
84 84 else:
85 85 _is_tty = True
86 86
87 87
88 88 _use_simple_prompt = ('IPY_TEST_SIMPLE_PROMPT' in os.environ) or (not _is_tty)
89 89
90 90 class TerminalInteractiveShell(InteractiveShell):
91 91 space_for_menu = Integer(6, help='Number of line at the bottom of the screen '
92 92 'to reserve for the completion menu'
93 93 ).tag(config=True)
94 94
95 95 pt_app = None
96 96 debugger_history = None
97 97
98 98 simple_prompt = Bool(_use_simple_prompt,
99 99 help="""Use `raw_input` for the REPL, without completion and prompt colors.
100 100
101 101 Useful when controlling IPython as a subprocess, and piping STDIN/OUT/ERR. Known usage are:
102 102 IPython own testing machinery, and emacs inferior-shell integration through elpy.
103 103
104 104 This mode default to `True` if the `IPY_TEST_SIMPLE_PROMPT`
105 105 environment variable is set, or the current terminal is not a tty."""
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\n(Note: prevents selecting text with the mouse)"
125 125 ).tag(config=True)
126 126
127 127 # We don't load the list of styles for the help string, because loading
128 128 # Pygments plugins takes time and can cause unexpected errors.
129 129 highlighting_style = Union([Unicode('legacy'), Type(klass=Style)],
130 130 help="""The name or class of a Pygments style to use for syntax
131 131 highlighting. To see available styles, run `pygmentize -L styles`."""
132 132 ).tag(config=True)
133 133
134 134
135 135 @observe('highlighting_style')
136 136 @observe('colors')
137 137 def _highlighting_style_changed(self, change):
138 138 self.refresh_style()
139 139
140 140 def refresh_style(self):
141 141 self._style = self._make_style_from_name_or_cls(self.highlighting_style)
142 142
143 143
144 144 highlighting_style_overrides = Dict(
145 145 help="Override highlighting format for specific tokens"
146 146 ).tag(config=True)
147 147
148 148 true_color = Bool(False,
149 149 help=("Use 24bit colors instead of 256 colors in prompt highlighting. "
150 150 "If your terminal supports true color, the following command "
151 151 "should print 'TRUECOLOR' in orange: "
152 152 "printf \"\\x1b[38;2;255;100;0mTRUECOLOR\\x1b[0m\\n\"")
153 153 ).tag(config=True)
154 154
155 155 editor = Unicode(get_default_editor(),
156 156 help="Set the editor used by IPython (default to $EDITOR/vi/notepad)."
157 157 ).tag(config=True)
158 158
159 159 prompts_class = Type(Prompts, help='Class used to generate Prompt token for prompt_toolkit').tag(config=True)
160 160
161 161 prompts = Instance(Prompts)
162 162
163 163 @default('prompts')
164 164 def _prompts_default(self):
165 165 return self.prompts_class(self)
166 166
167 167 # @observe('prompts')
168 168 # def _(self, change):
169 169 # self._update_layout()
170 170
171 171 @default('displayhook_class')
172 172 def _displayhook_class_default(self):
173 173 return RichPromptDisplayHook
174 174
175 175 term_title = Bool(True,
176 176 help="Automatically set the terminal title"
177 177 ).tag(config=True)
178 178
179 179 term_title_format = Unicode("IPython: {cwd}",
180 180 help="Customize the terminal title format. This is a python format string. " +
181 181 "Available substitutions are: {cwd}."
182 182 ).tag(config=True)
183 183
184 184 display_completions = Enum(('column', 'multicolumn','readlinelike'),
185 185 help= ( "Options for displaying tab completions, 'column', 'multicolumn', and "
186 186 "'readlinelike'. These options are for `prompt_toolkit`, see "
187 187 "`prompt_toolkit` documentation for more information."
188 188 ),
189 189 default_value='multicolumn').tag(config=True)
190 190
191 191 highlight_matching_brackets = Bool(True,
192 192 help="Highlight matching brackets.",
193 193 ).tag(config=True)
194 194
195 195 extra_open_editor_shortcuts = Bool(False,
196 196 help="Enable vi (v) or Emacs (C-X C-E) shortcuts to open an external editor. "
197 197 "This is in addition to the F2 binding, which is always enabled."
198 198 ).tag(config=True)
199 199
200 200 handle_return = Any(None,
201 201 help="Provide an alternative handler to be called when the user presses "
202 202 "Return. This is an advanced option intended for debugging, which "
203 203 "may be changed or removed in later releases."
204 204 ).tag(config=True)
205 205
206 206 enable_history_search = Bool(True,
207 207 help="Allows to enable/disable the prompt toolkit history search"
208 208 ).tag(config=True)
209 209
210 210 @observe('term_title')
211 211 def init_term_title(self, change=None):
212 212 # Enable or disable the terminal title.
213 213 if self.term_title:
214 214 toggle_set_term_title(True)
215 215 set_term_title(self.term_title_format.format(cwd=abbrev_cwd()))
216 216 else:
217 217 toggle_set_term_title(False)
218 218
219 219 def init_display_formatter(self):
220 220 super(TerminalInteractiveShell, self).init_display_formatter()
221 221 # terminal only supports plain text
222 222 self.display_formatter.active_types = ['text/plain']
223 223 # disable `_ipython_display_`
224 224 self.display_formatter.ipython_display_formatter.enabled = False
225 225
226 226 def init_prompt_toolkit_cli(self):
227 227 if self.simple_prompt:
228 228 # Fall back to plain non-interactive output for tests.
229 229 # This is very limited.
230 230 def prompt():
231 231 prompt_text = "".join(x[1] for x in self.prompts.in_prompt_tokens())
232 232 lines = [input(prompt_text)]
233 233 prompt_continuation = "".join(x[1] for x in self.prompts.continuation_prompt_tokens())
234 234 while self.check_complete('\n'.join(lines))[0] == 'incomplete':
235 235 lines.append( input(prompt_continuation) )
236 236 return '\n'.join(lines)
237 237 self.prompt_for_code = prompt
238 238 return
239 239
240 240 # Set up keyboard shortcuts
241 241 key_bindings = create_ipython_shortcuts(self)
242 242
243 243 # Pre-populate history from IPython's history database
244 244 history = InMemoryHistory()
245 245 last_cell = u""
246 246 for __, ___, cell in self.history_manager.get_tail(self.history_load_length,
247 247 include_latest=True):
248 248 # Ignore blank lines and consecutive duplicates
249 249 cell = cell.rstrip()
250 250 if cell and (cell != last_cell):
251 251 history.append_string(cell)
252 252 last_cell = cell
253 253
254 254 self._style = self._make_style_from_name_or_cls(self.highlighting_style)
255 255 self.style = DynamicStyle(lambda: self._style)
256 256
257 257 editing_mode = getattr(EditingMode, self.editing_mode.upper())
258 258
259 259 self.pt_app = PromptSession(
260 260 editing_mode=editing_mode,
261 261 key_bindings=key_bindings,
262 262 history=history,
263 263 completer=IPythonPTCompleter(shell=self),
264 264 enable_history_search = self.enable_history_search,
265 265 style=self.style,
266 266 include_default_pygments_style=False,
267 267 mouse_support=self.mouse_support,
268 268 enable_open_in_editor=self.extra_open_editor_shortcuts,
269 269 color_depth=(ColorDepth.TRUE_COLOR if self.true_color else None),
270 270 **self._extra_prompt_options())
271 271
272 272 def _make_style_from_name_or_cls(self, name_or_cls):
273 273 """
274 274 Small wrapper that make an IPython compatible style from a style name
275 275
276 276 We need that to add style for prompt ... etc.
277 277 """
278 278 style_overrides = {}
279 279 if name_or_cls == 'legacy':
280 280 legacy = self.colors.lower()
281 281 if legacy == 'linux':
282 282 style_cls = get_style_by_name('monokai')
283 283 style_overrides = _style_overrides_linux
284 284 elif legacy == 'lightbg':
285 285 style_overrides = _style_overrides_light_bg
286 286 style_cls = get_style_by_name('pastie')
287 287 elif legacy == 'neutral':
288 288 # The default theme needs to be visible on both a dark background
289 289 # and a light background, because we can't tell what the terminal
290 290 # looks like. These tweaks to the default theme help with that.
291 291 style_cls = get_style_by_name('default')
292 292 style_overrides.update({
293 293 Token.Number: '#007700',
294 294 Token.Operator: 'noinherit',
295 295 Token.String: '#BB6622',
296 296 Token.Name.Function: '#2080D0',
297 297 Token.Name.Class: 'bold #2080D0',
298 298 Token.Name.Namespace: 'bold #2080D0',
299 299 Token.Prompt: '#009900',
300 Token.PromptNum: '#00ff00 bold',
300 Token.PromptNum: '#ansibrightgreen bold',
301 301 Token.OutPrompt: '#990000',
302 Token.OutPromptNum: '#ff0000 bold',
302 Token.OutPromptNum: '#ansibrightred bold',
303 303 })
304 304
305 305 # Hack: Due to limited color support on the Windows console
306 306 # the prompt colors will be wrong without this
307 307 if os.name == 'nt':
308 308 style_overrides.update({
309 309 Token.Prompt: '#ansidarkgreen',
310 310 Token.PromptNum: '#ansigreen bold',
311 311 Token.OutPrompt: '#ansidarkred',
312 312 Token.OutPromptNum: '#ansired bold',
313 313 })
314 314 elif legacy =='nocolor':
315 315 style_cls=_NoStyle
316 316 style_overrides = {}
317 317 else :
318 318 raise ValueError('Got unknown colors: ', legacy)
319 319 else :
320 320 if isinstance(name_or_cls, str):
321 321 style_cls = get_style_by_name(name_or_cls)
322 322 else:
323 323 style_cls = name_or_cls
324 324 style_overrides = {
325 325 Token.Prompt: '#009900',
326 Token.PromptNum: '#00ff00 bold',
326 Token.PromptNum: '#ansibrightgreen bold',
327 327 Token.OutPrompt: '#990000',
328 Token.OutPromptNum: '#ff0000 bold',
328 Token.OutPromptNum: '#ansibrightred bold',
329 329 }
330 330 style_overrides.update(self.highlighting_style_overrides)
331 331 style = merge_styles([
332 332 style_from_pygments_cls(style_cls),
333 333 style_from_pygments_dict(style_overrides),
334 334 ])
335 335
336 336 return style
337 337
338 338 @property
339 339 def pt_complete_style(self):
340 340 return {
341 341 'multicolumn': CompleteStyle.MULTI_COLUMN,
342 342 'column': CompleteStyle.COLUMN,
343 343 'readlinelike': CompleteStyle.READLINE_LIKE,
344 344 }[self.display_completions]
345 345
346 346 def _extra_prompt_options(self):
347 347 """
348 348 Return the current layout option for the current Terminal InteractiveShell
349 349 """
350 350 def get_message():
351 351 return PygmentsTokens(self.prompts.in_prompt_tokens())
352 352
353 353 return {
354 354 'complete_in_thread': False,
355 355 'lexer':IPythonPTLexer(),
356 356 'reserve_space_for_menu':self.space_for_menu,
357 357 'message': get_message,
358 358 'prompt_continuation': (
359 359 lambda width, lineno, is_soft_wrap:
360 360 PygmentsTokens(self.prompts.continuation_prompt_tokens(width))),
361 361 'multiline': True,
362 362 'complete_style': self.pt_complete_style,
363 363
364 364 # Highlight matching brackets, but only when this setting is
365 365 # enabled, and only when the DEFAULT_BUFFER has the focus.
366 366 'input_processors': [ConditionalProcessor(
367 367 processor=HighlightMatchingBracketProcessor(chars='[](){}'),
368 368 filter=HasFocus(DEFAULT_BUFFER) & ~IsDone() &
369 369 Condition(lambda: self.highlight_matching_brackets))],
370 370 'inputhook': self.inputhook,
371 371 }
372 372
373 373 def prompt_for_code(self):
374 374 if self.rl_next_input:
375 375 default = self.rl_next_input
376 376 self.rl_next_input = None
377 377 else:
378 378 default = ''
379 379
380 380 with patch_stdout(raw=True):
381 381 text = self.pt_app.prompt(
382 382 default=default,
383 383 # pre_run=self.pre_prompt,# reset_current_buffer=True,
384 384 **self._extra_prompt_options())
385 385 return text
386 386
387 387 def enable_win_unicode_console(self):
388 388 if sys.version_info >= (3, 6):
389 389 # Since PEP 528, Python uses the unicode APIs for the Windows
390 390 # console by default, so WUC shouldn't be needed.
391 391 return
392 392
393 393 import win_unicode_console
394 394 win_unicode_console.enable()
395 395
396 396 def init_io(self):
397 397 if sys.platform not in {'win32', 'cli'}:
398 398 return
399 399
400 400 self.enable_win_unicode_console()
401 401
402 402 import colorama
403 403 colorama.init()
404 404
405 405 # For some reason we make these wrappers around stdout/stderr.
406 406 # For now, we need to reset them so all output gets coloured.
407 407 # https://github.com/ipython/ipython/issues/8669
408 408 # io.std* are deprecated, but don't show our own deprecation warnings
409 409 # during initialization of the deprecated API.
410 410 with warnings.catch_warnings():
411 411 warnings.simplefilter('ignore', DeprecationWarning)
412 412 io.stdout = io.IOStream(sys.stdout)
413 413 io.stderr = io.IOStream(sys.stderr)
414 414
415 415 def init_magics(self):
416 416 super(TerminalInteractiveShell, self).init_magics()
417 417 self.register_magics(TerminalMagics)
418 418
419 419 def init_alias(self):
420 420 # The parent class defines aliases that can be safely used with any
421 421 # frontend.
422 422 super(TerminalInteractiveShell, self).init_alias()
423 423
424 424 # Now define aliases that only make sense on the terminal, because they
425 425 # need direct access to the console in a way that we can't emulate in
426 426 # GUI or web frontend
427 427 if os.name == 'posix':
428 428 for cmd in ['clear', 'more', 'less', 'man']:
429 429 self.alias_manager.soft_define_alias(cmd, cmd)
430 430
431 431
432 432 def __init__(self, *args, **kwargs):
433 433 super(TerminalInteractiveShell, self).__init__(*args, **kwargs)
434 434 self.init_prompt_toolkit_cli()
435 435 self.init_term_title()
436 436 self.keep_running = True
437 437
438 438 self.debugger_history = InMemoryHistory()
439 439
440 440 def ask_exit(self):
441 441 self.keep_running = False
442 442
443 443 rl_next_input = None
444 444
445 445 def interact(self, display_banner=DISPLAY_BANNER_DEPRECATED):
446 446
447 447 if display_banner is not DISPLAY_BANNER_DEPRECATED:
448 448 warn('interact `display_banner` argument is deprecated since IPython 5.0. Call `show_banner()` if needed.', DeprecationWarning, stacklevel=2)
449 449
450 450 self.keep_running = True
451 451 while self.keep_running:
452 452 print(self.separate_in, end='')
453 453
454 454 try:
455 455 code = self.prompt_for_code()
456 456 except EOFError:
457 457 if (not self.confirm_exit) \
458 458 or self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'):
459 459 self.ask_exit()
460 460
461 461 else:
462 462 if code:
463 463 self.run_cell(code, store_history=True)
464 464
465 465 def mainloop(self, display_banner=DISPLAY_BANNER_DEPRECATED):
466 466 # An extra layer of protection in case someone mashing Ctrl-C breaks
467 467 # out of our internal code.
468 468 if display_banner is not DISPLAY_BANNER_DEPRECATED:
469 469 warn('mainloop `display_banner` argument is deprecated since IPython 5.0. Call `show_banner()` if needed.', DeprecationWarning, stacklevel=2)
470 470 while True:
471 471 try:
472 472 self.interact()
473 473 break
474 474 except KeyboardInterrupt as e:
475 475 print("\n%s escaped interact()\n" % type(e).__name__)
476 476 finally:
477 477 # An interrupt during the eventloop will mess up the
478 478 # internal state of the prompt_toolkit library.
479 479 # Stopping the eventloop fixes this, see
480 480 # https://github.com/ipython/ipython/pull/9867
481 481 if hasattr(self, '_eventloop'):
482 482 self._eventloop.stop()
483 483
484 484 _inputhook = None
485 485 def inputhook(self, context):
486 486 if self._inputhook is not None:
487 487 self._inputhook(context)
488 488
489 489 active_eventloop = None
490 490 def enable_gui(self, gui=None):
491 491 if gui:
492 492 self.active_eventloop, self._inputhook =\
493 493 get_inputhook_name_and_func(gui)
494 494 else:
495 495 self.active_eventloop = self._inputhook = None
496 496
497 497 # Run !system commands directly, not through pipes, so terminal programs
498 498 # work correctly.
499 499 system = InteractiveShell.system_raw
500 500
501 501 def auto_rewrite_input(self, cmd):
502 502 """Overridden from the parent class to use fancy rewriting prompt"""
503 503 if not self.show_rewritten_input:
504 504 return
505 505
506 506 tokens = self.prompts.rewrite_prompt_tokens()
507 507 if self.pt_app:
508 508 print_formatted_text(PygmentsTokens(tokens), end='',
509 509 style=self.pt_app.app.style)
510 510 print(cmd)
511 511 else:
512 512 prompt = ''.join(s for t, s in tokens)
513 513 print(prompt, cmd, sep='')
514 514
515 515 _prompts_before = None
516 516 def switch_doctest_mode(self, mode):
517 517 """Switch prompts to classic for %doctest_mode"""
518 518 if mode:
519 519 self._prompts_before = self.prompts
520 520 self.prompts = ClassicPrompts(self)
521 521 elif self._prompts_before:
522 522 self.prompts = self._prompts_before
523 523 self._prompts_before = None
524 524 # self._update_layout()
525 525
526 526
527 527 InteractiveShellABC.register(TerminalInteractiveShell)
528 528
529 529 if __name__ == '__main__':
530 530 TerminalInteractiveShell.instance().interact()
General Comments 0
You need to be logged in to leave comments. Login now