1"""IPython terminal interface using prompt_toolkit"""
2
3import os
4import sys
5import inspect
6from warnings import warn
7
8from IPython.core.async_helpers import get_asyncio_loop
9from IPython.core.interactiveshell import InteractiveShell, InteractiveShellABC
10from IPython.core.kitty import (
11 display_formatter_default_active_types,
12 terminal_default_mime_renderers,
13)
14from IPython.utils.PyColorize import theme_table
15from IPython.utils.terminal import toggle_set_term_title, set_term_title, restore_term_title
16from IPython.utils.process import abbrev_cwd
17from traitlets import (
18 Any,
19 Bool,
20 Dict,
21 Enum,
22 Float,
23 Instance,
24 Integer,
25 List,
26 Type,
27 Unicode,
28 Union,
29 default,
30 observe,
31 validate,
32 DottedObjectName,
33)
34from traitlets.utils.importstring import import_item
35
36
37from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
38from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode
39from prompt_toolkit.filters import HasFocus, Condition, IsDone
40from prompt_toolkit.formatted_text import PygmentsTokens
41from prompt_toolkit.history import History
42from prompt_toolkit.layout.processors import ConditionalProcessor, HighlightMatchingBracketProcessor
43from prompt_toolkit.output import ColorDepth
44from prompt_toolkit.patch_stdout import patch_stdout
45from prompt_toolkit.shortcuts import PromptSession, CompleteStyle, print_formatted_text
46from prompt_toolkit.styles import DynamicStyle, merge_styles
47from prompt_toolkit.styles.pygments import style_from_pygments_cls, style_from_pygments_dict
48from pygments.styles import get_style_by_name
49from pygments.style import Style
50
51from .debugger import TerminalPdb, Pdb
52from .magics import TerminalMagics
53from .pt_inputhooks import get_inputhook_name_and_func
54from .prompts import Prompts, ClassicPrompts, RichPromptDisplayHook
55from .ptutils import IPythonPTCompleter, IPythonPTLexer
56from .shortcuts import (
57 KEY_BINDINGS,
58 UNASSIGNED_ALLOWED_COMMANDS,
59 create_ipython_shortcuts,
60 create_identifier,
61 RuntimeBinding,
62 add_binding,
63)
64from .shortcuts.filters import KEYBINDING_FILTERS, filter_from_string
65from .shortcuts.auto_suggest import (
66 NavigableAutoSuggestFromHistory,
67 AppendAutoSuggestionInAnyLine,
68)
69
70
71class _NoStyle(Style):
72 pass
73
74
75
76def _backward_compat_continuation_prompt_tokens(
77 method, width: int, *, lineno: int, wrap_count: int
78):
79 """
80 Sagemath use custom prompt and we broke them in 8.19.
81
82 make sure to pass only width if method only support width
83 """
84 sig = inspect.signature(method)
85 extra = {}
86 params = inspect.signature(method).parameters
87 if "lineno" in inspect.signature(method).parameters or any(
88 [p.kind == p.VAR_KEYWORD for p in sig.parameters.values()]
89 ):
90 extra["lineno"] = lineno
91 if "line_number" in inspect.signature(method).parameters or any(
92 [p.kind == p.VAR_KEYWORD for p in sig.parameters.values()]
93 ):
94 extra["line_number"] = lineno
95
96 if "wrap_count" in inspect.signature(method).parameters or any(
97 [p.kind == p.VAR_KEYWORD for p in sig.parameters.values()]
98 ):
99 extra["wrap_count"] = wrap_count
100 return method(width, **extra)
101
102
103def get_default_editor():
104 try:
105 return os.environ['EDITOR']
106 except KeyError:
107 pass
108 except UnicodeError:
109 warn("$EDITOR environment variable is not pure ASCII. Using platform "
110 "default editor.")
111
112 if os.name == 'posix':
113 return 'vi' # the only one guaranteed to be there!
114 else:
115 return "notepad" # same in Windows!
116
117
118# conservatively check for tty
119# overridden streams can result in things like:
120# - sys.stdin = None
121# - no isatty method
122for _name in ('stdin', 'stdout', 'stderr'):
123 _stream = getattr(sys, _name)
124 try:
125 if not _stream or not hasattr(_stream, "isatty") or not _stream.isatty():
126 _is_tty = False
127 break
128 except ValueError:
129 # stream is closed
130 _is_tty = False
131 break
132else:
133 _is_tty = True
134
135
136_use_simple_prompt = ('IPY_TEST_SIMPLE_PROMPT' in os.environ) or (not _is_tty)
137
138def black_reformat_handler(text_before_cursor):
139 """
140 We do not need to protect against error,
141 this is taken care at a higher level where any reformat error is ignored.
142 Indeed we may call reformatting on incomplete code.
143 """
144 import black
145
146 formatted_text = black.format_str(text_before_cursor, mode=black.FileMode())
147 if not text_before_cursor.endswith("\n") and formatted_text.endswith("\n"):
148 formatted_text = formatted_text[:-1]
149 return formatted_text
150
151
152def yapf_reformat_handler(text_before_cursor):
153 from yapf.yapflib import file_resources
154 from yapf.yapflib import yapf_api
155
156 style_config = file_resources.GetDefaultStyleForDir(os.getcwd())
157 formatted_text, was_formatted = yapf_api.FormatCode(
158 text_before_cursor, style_config=style_config
159 )
160 if was_formatted:
161 if not text_before_cursor.endswith("\n") and formatted_text.endswith("\n"):
162 formatted_text = formatted_text[:-1]
163 return formatted_text
164 else:
165 return text_before_cursor
166
167
168class PtkHistoryAdapter(History):
169 """
170 Prompt toolkit has it's own way of handling history, Where it assumes it can
171 Push/pull from history.
172
173 """
174
175 def __init__(self, shell):
176 super().__init__()
177 self.shell = shell
178 self._refresh()
179
180 def append_string(self, string):
181 # we rely on sql for that.
182 self._loaded = False
183 self._refresh()
184
185 def _refresh(self):
186 if not self._loaded:
187 self._loaded_strings = list(self.load_history_strings())
188
189 def load_history_strings(self):
190 last_cell = ""
191 res = []
192 for __, ___, cell in self.shell.history_manager.get_tail(
193 self.shell.history_load_length, include_latest=True
194 ):
195 # Ignore blank lines and consecutive duplicates
196 cell = cell.rstrip()
197 if cell and (cell != last_cell):
198 res.append(cell)
199 last_cell = cell
200 yield from res[::-1]
201
202 def store_string(self, string: str) -> None:
203 pass
204
205class TerminalInteractiveShell(InteractiveShell):
206 mime_renderers = Dict().tag(config=True)
207
208 min_elide = Integer(
209 30, help="minimum characters for filling with ellipsis in file completions. "
210 "Set to 0 or less to completely disable elision."
211 ).tag(config=True)
212 space_for_menu = Integer(
213 6,
214 help="Number of line at the bottom of the screen "
215 "to reserve for the tab completion menu, "
216 "search history, ...etc, the height of "
217 "these menus will at most this value. "
218 "Increase it is you prefer long and skinny "
219 "menus, decrease for short and wide.",
220 ).tag(config=True)
221
222 pt_app: PromptSession | None = None
223 auto_suggest: (
224 AutoSuggestFromHistory | NavigableAutoSuggestFromHistory | None
225 ) = None
226 debugger_history = None
227
228 debugger_history_file = Unicode(
229 "~/.pdbhistory", help="File in which to store and read history"
230 ).tag(config=True)
231
232 simple_prompt = Bool(_use_simple_prompt,
233 help="""Use `raw_input` for the REPL, without completion and prompt colors.
234
235 Useful when controlling IPython as a subprocess, and piping
236 STDIN/OUT/ERR. Known usage are: IPython's own testing machinery,
237 and emacs' inferior-python subprocess (assuming you have set
238 `python-shell-interpreter` to "ipython") available through the
239 built-in `M-x run-python` and third party packages such as elpy.
240
241 This mode default to `True` if the `IPY_TEST_SIMPLE_PROMPT`
242 environment variable is set, or the current terminal is not a tty.
243 Thus the Default value reported in --help-all, or config will often
244 be incorrectly reported.
245 """,
246 ).tag(config=True)
247
248 @property
249 def debugger_cls(self):
250 return Pdb if self.simple_prompt else TerminalPdb
251
252 confirm_exit = Bool(True,
253 help="""
254 Set to confirm when you try to exit IPython with an EOF (Control-D
255 in Unix, Control-Z/Enter in Windows). By typing 'exit' or 'quit',
256 you can force a direct exit without any confirmation.""",
257 ).tag(config=True)
258
259 editing_mode = Unicode('emacs',
260 help="Shortcut style to use at the prompt. 'vi' or 'emacs'.",
261 ).tag(config=True)
262
263 emacs_bindings_in_vi_insert_mode = Bool(
264 True,
265 help="Add shortcuts from 'emacs' insert mode to 'vi' insert mode.",
266 ).tag(config=True)
267
268 modal_cursor = Bool(
269 True,
270 help="""
271 Cursor shape changes depending on vi mode: beam in vi insert mode,
272 block in nav mode, underscore in replace mode.""",
273 ).tag(config=True)
274
275 ttimeoutlen = Float(
276 0.01,
277 help="""The time in milliseconds that is waited for a key code
278 to complete.""",
279 ).tag(config=True)
280
281 timeoutlen = Float(
282 0.5,
283 help="""The time in milliseconds that is waited for a mapped key
284 sequence to complete.""",
285 ).tag(config=True)
286
287 autoformatter = Unicode(
288 None,
289 help="Autoformatter to reformat Terminal code. Can be `'black'`, `'yapf'` or `None`",
290 allow_none=True
291 ).tag(config=True)
292
293 auto_match = Bool(
294 False,
295 help="""
296 Automatically add/delete closing bracket or quote when opening bracket or quote is entered/deleted.
297 Brackets: (), [], {}
298 Quotes: '', \"\"
299 """,
300 ).tag(config=True)
301
302 mouse_support = Bool(False,
303 help="Enable mouse support in the prompt\n(Note: prevents selecting text with the mouse)"
304 ).tag(config=True)
305
306 # We don't load the list of styles for the help string, because loading
307 # Pygments plugins takes time and can cause unexpected errors.
308 highlighting_style = Union(
309 [Unicode("legacy"), Type(klass=Style)],
310 help="""Deprecated, and has not effect, use IPython themes
311
312 The name or class of a Pygments style to use for syntax
313 highlighting. To see available styles, run `pygmentize -L styles`.""",
314 ).tag(config=True)
315
316 @validate('editing_mode')
317 def _validate_editing_mode(self, proposal):
318 if proposal['value'].lower() == 'vim':
319 proposal['value']= 'vi'
320 elif proposal['value'].lower() == 'default':
321 proposal['value']= 'emacs'
322
323 if hasattr(EditingMode, proposal['value'].upper()):
324 return proposal['value'].lower()
325
326 return self.editing_mode
327
328 @observe('editing_mode')
329 def _editing_mode(self, change):
330 if self.pt_app:
331 self.pt_app.editing_mode = getattr(EditingMode, change.new.upper())
332
333 def _set_formatter(self, formatter):
334 if formatter is None:
335 self.reformat_handler = lambda x:x
336 elif formatter == 'black':
337 self.reformat_handler = black_reformat_handler
338 elif formatter == "yapf":
339 self.reformat_handler = yapf_reformat_handler
340 else:
341 raise ValueError
342
343 @observe("autoformatter")
344 def _autoformatter_changed(self, change):
345 formatter = change.new
346 self._set_formatter(formatter)
347
348 @observe('highlighting_style')
349 @observe('colors')
350 def _highlighting_style_changed(self, change):
351 assert change.new == change.new.lower()
352 if change.new != "legacy":
353 warn(
354 "highlighting_style is deprecated since 9.0 and have no effect, use themeing."
355 )
356 return
357
358 def refresh_style(self):
359 self._style = self._make_style_from_name_or_cls("legacy")
360
361 # TODO: deprecate this
362 highlighting_style_overrides = Dict(
363 help="Override highlighting format for specific tokens"
364 ).tag(config=True)
365
366 true_color = Bool(False,
367 help="""Use 24bit colors instead of 256 colors in prompt highlighting.
368 If your terminal supports true color, the following command should
369 print ``TRUECOLOR`` in orange::
370
371 printf \"\\x1b[38;2;255;100;0mTRUECOLOR\\x1b[0m\\n\"
372 """,
373 ).tag(config=True)
374
375 editor = Unicode(get_default_editor(),
376 help="Set the editor used by IPython (default to $EDITOR/vi/notepad)."
377 ).tag(config=True)
378
379 prompts_class = Type(Prompts, help='Class used to generate Prompt token for prompt_toolkit').tag(config=True)
380
381 prompts = Instance(Prompts)
382
383 @default('prompts')
384 def _prompts_default(self):
385 return self.prompts_class(self)
386
387# @observe('prompts')
388# def _(self, change):
389# self._update_layout()
390
391 @default('displayhook_class')
392 def _displayhook_class_default(self):
393 return RichPromptDisplayHook
394
395 term_title = Bool(True,
396 help="Automatically set the terminal title"
397 ).tag(config=True)
398
399 term_title_format = Unicode("IPython: {cwd}",
400 help="Customize the terminal title format. This is a python format string. " +
401 "Available substitutions include {cwd}."
402 ).tag(config=True)
403
404 display_completions = Enum(('column', 'multicolumn','readlinelike'),
405 help= ( "Options for displaying tab completions, 'column', 'multicolumn', and "
406 "'readlinelike'. These options are for `prompt_toolkit`, see "
407 "`prompt_toolkit` documentation for more information."
408 ),
409 default_value='multicolumn').tag(config=True)
410
411 highlight_matching_brackets = Bool(True,
412 help="Highlight matching brackets.",
413 ).tag(config=True)
414
415 extra_open_editor_shortcuts = Bool(False,
416 help="Enable vi (v) or Emacs (C-X C-E) shortcuts to open an external editor. "
417 "This is in addition to the F2 binding, which is always enabled."
418 ).tag(config=True)
419
420 handle_return = Any(None,
421 help="Provide an alternative handler to be called when the user presses "
422 "Return. This is an advanced option intended for debugging, which "
423 "may be changed or removed in later releases."
424 ).tag(config=True)
425
426 enable_history_search = Bool(True,
427 help="Allows to enable/disable the prompt toolkit history search"
428 ).tag(config=True)
429
430 autosuggestions_provider = Unicode(
431 "NavigableAutoSuggestFromHistory",
432 help="Specifies from which source automatic suggestions are provided. "
433 "Can be set to ``'NavigableAutoSuggestFromHistory'`` (:kbd:`up` and "
434 ":kbd:`down` swap suggestions), ``'AutoSuggestFromHistory'``, "
435 " or ``None`` to disable automatic suggestions. "
436 "Default is `'NavigableAutoSuggestFromHistory`'.",
437 allow_none=True,
438 ).tag(config=True)
439 _autosuggestions_provider: Any
440
441 llm_constructor_kwargs = Dict(
442 {},
443 help="""
444 Extra arguments to pass to `llm_provider_class` constructor.
445
446 This is used to – for example – set the `model_id`""",
447 ).tag(config=True)
448
449 llm_prefix_from_history = DottedObjectName(
450 "input_history",
451 help="""\
452 Fully Qualifed name of a function that takes an IPython history manager and
453 return a prefix to pass the llm provider in addition to the current buffer
454 text.
455
456 You can use:
457
458 - no_prefix
459 - input_history
460
461 As default value. `input_history` (default), will use all the input history
462 of current IPython session
463
464 """,
465 ).tag(config=True)
466 _llm_prefix_from_history: Any
467
468 @observe("llm_prefix_from_history")
469 def _llm_prefix_from_history_changed(self, change):
470 name = change.new
471 self._llm_prefix_from_history = name
472 self._set_autosuggestions()
473
474 llm_provider_class = DottedObjectName(
475 None,
476 allow_none=True,
477 help="""\
478 Provisional:
479 This is a provisional API in IPython 8.32, before stabilisation
480 in 9.0, it may change without warnings.
481
482 class to use for the `NavigableAutoSuggestFromHistory` to request
483 completions from a LLM, this should inherit from
484 `jupyter_ai_magics:BaseProvider` and implement
485 `stream_inline_completions`
486 """,
487 ).tag(config=True)
488 _llm_provider_class: Any = None
489
490 @observe("llm_provider_class")
491 def _llm_provider_class_changed(self, change):
492 provider_class = change.new
493 self._llm_provider_class = provider_class
494 self._set_autosuggestions()
495
496 def _set_autosuggestions(self, provider=None):
497 if provider is None:
498 provider = self.autosuggestions_provider
499 # disconnect old handler
500 if self.auto_suggest and isinstance(
501 self.auto_suggest, NavigableAutoSuggestFromHistory
502 ):
503 self.auto_suggest.disconnect()
504 if provider is None:
505 self.auto_suggest = None
506 elif provider == "AutoSuggestFromHistory":
507 self.auto_suggest = AutoSuggestFromHistory()
508 elif provider == "NavigableAutoSuggestFromHistory":
509 # LLM stuff are all Provisional in 8.32
510 if self._llm_provider_class:
511
512 def init_llm_provider():
513 llm_provider_constructor = import_item(self._llm_provider_class)
514 return llm_provider_constructor(**self.llm_constructor_kwargs)
515
516 else:
517 init_llm_provider = None
518 self.auto_suggest = NavigableAutoSuggestFromHistory()
519 # Provisinal in 8.32
520 self.auto_suggest._init_llm_provider = init_llm_provider
521
522 name = self.llm_prefix_from_history
523
524 if name == "no_prefix":
525
526 def no_prefix(history_manager):
527 return ""
528
529 fun = no_prefix
530
531 elif name == "input_history":
532
533 def input_history(history_manager):
534 return "\n".join([s[2] for s in history_manager.get_range()]) + "\n"
535
536 fun = input_history
537
538 else:
539 fun = import_item(name)
540 self.auto_suggest._llm_prefixer = fun
541 else:
542 raise ValueError("No valid provider.")
543 if self.pt_app:
544 self.pt_app.auto_suggest = self.auto_suggest
545
546 @observe("autosuggestions_provider")
547 def _autosuggestions_provider_changed(self, change):
548 provider = change.new
549 self._set_autosuggestions(provider)
550
551 shortcuts = List(
552 trait=Dict(
553 key_trait=Enum(
554 [
555 "command",
556 "match_keys",
557 "match_filter",
558 "new_keys",
559 "new_filter",
560 "create",
561 ]
562 ),
563 per_key_traits={
564 "command": Unicode(),
565 "match_keys": List(Unicode()),
566 "match_filter": Unicode(),
567 "new_keys": List(Unicode()),
568 "new_filter": Unicode(),
569 "create": Bool(False),
570 },
571 ),
572 help="""
573 Add, disable or modifying shortcuts.
574
575 Each entry on the list should be a dictionary with ``command`` key
576 identifying the target function executed by the shortcut and at least
577 one of the following:
578
579 - ``match_keys``: list of keys used to match an existing shortcut,
580 - ``match_filter``: shortcut filter used to match an existing shortcut,
581 - ``new_keys``: list of keys to set,
582 - ``new_filter``: a new shortcut filter to set
583
584 The filters have to be composed of pre-defined verbs and joined by one
585 of the following conjunctions: ``&`` (and), ``|`` (or), ``~`` (not).
586 The pre-defined verbs are:
587
588 {filters}
589
590 To disable a shortcut set ``new_keys`` to an empty list.
591 To add a shortcut add key ``create`` with value ``True``.
592
593 When modifying/disabling shortcuts, ``match_keys``/``match_filter`` can
594 be omitted if the provided specification uniquely identifies a shortcut
595 to be modified/disabled. When modifying a shortcut ``new_filter`` or
596 ``new_keys`` can be omitted which will result in reuse of the existing
597 filter/keys.
598
599 Only shortcuts defined in IPython (and not default prompt-toolkit
600 shortcuts) can be modified or disabled. The full list of shortcuts,
601 command identifiers and filters is available under
602 :ref:`terminal-shortcuts-list`.
603
604 Here is an example:
605
606 .. code::
607
608 c.TerminalInteractiveShell.shortcuts = [
609 {{
610 "new_keys": ["c-q"],
611 "command": "prompt_toolkit:named_commands.capitalize_word",
612 "create": True,
613 }},
614 {{
615 "new_keys": ["c-j"],
616 "command": "prompt_toolkit:named_commands.beginning_of_line",
617 "create": True,
618 }},
619 ]
620
621
622 """.format(
623 filters="\n ".join([f" - ``{k}``" for k in KEYBINDING_FILTERS])
624 ),
625 ).tag(config=True)
626
627 @observe("shortcuts")
628 def _shortcuts_changed(self, change):
629 if self.pt_app:
630 self.pt_app.key_bindings = self._merge_shortcuts(user_shortcuts=change.new)
631
632 def _merge_shortcuts(self, user_shortcuts):
633 # rebuild the bindings list from scratch
634 key_bindings = create_ipython_shortcuts(self)
635
636 # for now we only allow adding shortcuts for a specific set of
637 # commands; this is a security precution.
638 allowed_commands = {
639 create_identifier(binding.command): binding.command
640 for binding in KEY_BINDINGS
641 }
642 allowed_commands.update(
643 {
644 create_identifier(command): command
645 for command in UNASSIGNED_ALLOWED_COMMANDS
646 }
647 )
648 shortcuts_to_skip = []
649 shortcuts_to_add = []
650
651 for shortcut in user_shortcuts:
652 command_id = shortcut["command"]
653 if command_id not in allowed_commands:
654 allowed_commands = "\n - ".join(allowed_commands)
655 raise ValueError(
656 f"{command_id} is not a known shortcut command."
657 f" Allowed commands are: \n - {allowed_commands}"
658 )
659 old_keys = shortcut.get("match_keys", None)
660 old_filter = (
661 filter_from_string(shortcut["match_filter"])
662 if "match_filter" in shortcut
663 else None
664 )
665 matching = [
666 binding
667 for binding in KEY_BINDINGS
668 if (
669 (old_filter is None or binding.filter == old_filter)
670 and (old_keys is None or [k for k in binding.keys] == old_keys)
671 and create_identifier(binding.command) == command_id
672 )
673 ]
674
675 new_keys = shortcut.get("new_keys", None)
676 new_filter = shortcut.get("new_filter", None)
677
678 command = allowed_commands[command_id]
679
680 creating_new = shortcut.get("create", False)
681 modifying_existing = not creating_new and (
682 new_keys is not None or new_filter
683 )
684
685 if creating_new and new_keys == []:
686 raise ValueError("Cannot add a shortcut without keys")
687
688 if modifying_existing:
689 specification = {
690 key: shortcut[key]
691 for key in ["command", "filter"]
692 if key in shortcut
693 }
694 if len(matching) == 0:
695 raise ValueError(
696 f"No shortcuts matching {specification} found in {KEY_BINDINGS}"
697 )
698 elif len(matching) > 1:
699 raise ValueError(
700 f"Multiple shortcuts matching {specification} found,"
701 f" please add keys/filter to select one of: {matching}"
702 )
703
704 matched = matching[0]
705 old_filter = matched.filter
706 old_keys = list(matched.keys)
707 shortcuts_to_skip.append(
708 RuntimeBinding(
709 command,
710 keys=old_keys,
711 filter=old_filter,
712 )
713 )
714
715 if new_keys != []:
716 shortcuts_to_add.append(
717 RuntimeBinding(
718 command,
719 keys=new_keys or old_keys,
720 filter=(
721 filter_from_string(new_filter)
722 if new_filter is not None
723 else (
724 old_filter
725 if old_filter is not None
726 else filter_from_string("always")
727 )
728 ),
729 )
730 )
731
732 # rebuild the bindings list from scratch
733 key_bindings = create_ipython_shortcuts(self, skip=shortcuts_to_skip)
734 for binding in shortcuts_to_add:
735 add_binding(key_bindings, binding)
736
737 return key_bindings
738
739 prompt_includes_vi_mode = Bool(True,
740 help="Display the current vi mode (when using vi editing mode)."
741 ).tag(config=True)
742
743 prompt_line_number_format = Unicode(
744 "",
745 help="The format for line numbering. Will be passed the current line number"
746 " ``line`` (int, 1 based), and the relative line number ``rel_line``."
747 " For example to display both, you can use the following template string:"
748 " ``c.TerminalInteractiveShell.prompt_line_number_format = '{line: 4d}/{rel_line:+03d} | '``"
749 " This will display the current line number, with a leading space and a width of at least 4"
750 " characters, as well as the relative line number, 0-padded and always with a + or - sign."
751 " Note that when using Emacs mode, the prompt of the first line may not update.",
752 ).tag(config=True)
753
754 @observe('term_title')
755 def init_term_title(self, change=None):
756 # Enable or disable the terminal title.
757 if self.term_title and _is_tty:
758 toggle_set_term_title(True)
759 set_term_title(self.term_title_format.format(cwd=abbrev_cwd()))
760 else:
761 toggle_set_term_title(False)
762
763 def restore_term_title(self):
764 if self.term_title and _is_tty:
765 restore_term_title()
766
767 def init_display_formatter(self):
768 super().init_display_formatter()
769 # terminal only supports plain text if not explicitly configured
770 config = self.display_formatter._trait_values["config"]
771 if not (
772 "DisplayFormatter" in config
773 and "active_types" in config["DisplayFormatter"]
774 ):
775 self.display_formatter.active_types = display_formatter_default_active_types
776 if not (
777 "TerminalInteractiveShell" in config
778 and "mime_renderers" in config["TerminalInteractiveShell"]
779 ):
780 self.mime_renderers = terminal_default_mime_renderers
781
782 def init_prompt_toolkit_cli(self):
783 if self.simple_prompt:
784 # Fall back to plain non-interactive output for tests.
785 # This is very limited.
786 def prompt():
787 prompt_text = "".join(x[1] for x in self.prompts.in_prompt_tokens())
788 lines = [input(prompt_text)]
789 prompt_continuation = "".join(
790 x[1] for x in self.prompts.continuation_prompt_tokens()
791 )
792 while self.check_complete("\n".join(lines))[0] == "incomplete":
793 lines.append(input(prompt_continuation))
794 return "\n".join(lines)
795
796 self.prompt_for_code = prompt
797 return
798
799 # Set up keyboard shortcuts
800 key_bindings = self._merge_shortcuts(user_shortcuts=self.shortcuts)
801
802 # Pre-populate history from IPython's history database
803 history = PtkHistoryAdapter(self)
804
805 self.refresh_style()
806 ptk_s = DynamicStyle(lambda: self._style)
807
808 editing_mode = getattr(EditingMode, self.editing_mode.upper())
809
810 self._use_asyncio_inputhook = False
811 self.pt_app = PromptSession(
812 auto_suggest=self.auto_suggest,
813 editing_mode=editing_mode,
814 key_bindings=key_bindings,
815 history=history,
816 completer=IPythonPTCompleter(shell=self),
817 enable_history_search=self.enable_history_search,
818 style=ptk_s,
819 include_default_pygments_style=False,
820 mouse_support=self.mouse_support,
821 enable_open_in_editor=self.extra_open_editor_shortcuts,
822 color_depth=self.color_depth,
823 tempfile_suffix=".py",
824 **self._extra_prompt_options(),
825 )
826 if isinstance(self.auto_suggest, NavigableAutoSuggestFromHistory):
827 self.auto_suggest.connect(self.pt_app)
828
829 def _make_style_from_name_or_cls(self, name_or_cls):
830 """
831 Small wrapper that make an IPython compatible style from a style name
832
833 We need that to add style for prompt ... etc.
834 """
835 assert name_or_cls == "legacy"
836 legacy = self.colors.lower()
837
838 theme = theme_table.get(legacy, None)
839 assert theme is not None, legacy
840
841 if legacy == "nocolor":
842 style_overrides = {}
843 style_cls = _NoStyle
844 else:
845 style_overrides = {**theme.extra_style, **self.highlighting_style_overrides}
846 if theme.base is not None:
847 style_cls = get_style_by_name(theme.base)
848 else:
849 style_cls = _NoStyle
850
851 style = merge_styles(
852 [
853 style_from_pygments_cls(style_cls),
854 style_from_pygments_dict(style_overrides),
855 ]
856 )
857
858 return style
859
860 @property
861 def pt_complete_style(self):
862 return {
863 'multicolumn': CompleteStyle.MULTI_COLUMN,
864 'column': CompleteStyle.COLUMN,
865 'readlinelike': CompleteStyle.READLINE_LIKE,
866 }[self.display_completions]
867
868 @property
869 def color_depth(self):
870 return (ColorDepth.TRUE_COLOR if self.true_color else None)
871
872 def _ptk_prompt_cont(self, width: int, line_number: int, wrap_count: int):
873 return PygmentsTokens(
874 _backward_compat_continuation_prompt_tokens(
875 self.prompts.continuation_prompt_tokens,
876 width,
877 lineno=line_number,
878 wrap_count=wrap_count,
879 )
880 )
881
882 def _extra_prompt_options(self):
883 """
884 Return the current layout option for the current Terminal InteractiveShell
885 """
886 def get_message():
887 return PygmentsTokens(self.prompts.in_prompt_tokens())
888
889 if self.editing_mode == "emacs" and self.prompt_line_number_format == "":
890 # with emacs mode the prompt is (usually) static, so we call only
891 # the function once. With VI mode it can toggle between [ins] and
892 # [nor] so we can't precompute.
893 # here I'm going to favor the default keybinding which almost
894 # everybody uses to decrease CPU usage.
895 # if we have issues with users with custom Prompts we can see how to
896 # work around this.
897 get_message = get_message()
898
899 options = {
900 "complete_in_thread": False,
901 "lexer": IPythonPTLexer(),
902 "reserve_space_for_menu": self.space_for_menu,
903 "message": get_message,
904 "prompt_continuation": self._ptk_prompt_cont,
905 "multiline": True,
906 "complete_style": self.pt_complete_style,
907 "input_processors": [
908 # Highlight matching brackets, but only when this setting is
909 # enabled, and only when the DEFAULT_BUFFER has the focus.
910 ConditionalProcessor(
911 processor=HighlightMatchingBracketProcessor(chars="[](){}"),
912 filter=HasFocus(DEFAULT_BUFFER)
913 & ~IsDone()
914 & Condition(lambda: self.highlight_matching_brackets),
915 ),
916 # Show auto-suggestion in lines other than the last line.
917 ConditionalProcessor(
918 processor=AppendAutoSuggestionInAnyLine(),
919 filter=HasFocus(DEFAULT_BUFFER)
920 & ~IsDone()
921 & Condition(
922 lambda: isinstance(
923 self.auto_suggest,
924 NavigableAutoSuggestFromHistory,
925 )
926 ),
927 ),
928 ],
929 }
930
931 return options
932
933 def prompt_for_code(self):
934 if self.rl_next_input:
935 default = self.rl_next_input
936 self.rl_next_input = None
937 else:
938 default = ''
939
940 # In order to make sure that asyncio code written in the
941 # interactive shell doesn't interfere with the prompt, we run the
942 # prompt in a different event loop.
943 # If we don't do this, people could spawn coroutine with a
944 # while/true inside which will freeze the prompt.
945
946 with patch_stdout(raw=True):
947 if self._use_asyncio_inputhook:
948 # When we integrate the asyncio event loop, run the UI in the
949 # same event loop as the rest of the code. don't use an actual
950 # input hook. (Asyncio is not made for nesting event loops.)
951 asyncio_loop = get_asyncio_loop()
952 text = asyncio_loop.run_until_complete(
953 self.pt_app.prompt_async(
954 default=default, **self._extra_prompt_options()
955 )
956 )
957 else:
958 text = self.pt_app.prompt(
959 default=default,
960 inputhook=self._inputhook,
961 **self._extra_prompt_options(),
962 )
963
964 return text
965
966 def init_io(self):
967 if sys.platform not in {'win32', 'cli'}:
968 return
969
970 import colorama
971 colorama.init()
972
973 def init_magics(self):
974 super().init_magics()
975 self.register_magics(TerminalMagics)
976
977 def init_alias(self):
978 # The parent class defines aliases that can be safely used with any
979 # frontend.
980 super().init_alias()
981
982 # Now define aliases that only make sense on the terminal, because they
983 # need direct access to the console in a way that we can't emulate in
984 # GUI or web frontend
985 if os.name == 'posix':
986 for cmd in ('clear', 'more', 'less', 'man'):
987 self.alias_manager.soft_define_alias(cmd, cmd)
988
989 def __init__(self, *args, **kwargs) -> None:
990 super().__init__(*args, **kwargs)
991 self._set_autosuggestions(self.autosuggestions_provider)
992 self.init_prompt_toolkit_cli()
993 self.init_term_title()
994 self.keep_running = True
995 self._set_formatter(self.autoformatter)
996
997 def ask_exit(self):
998 self.keep_running = False
999
1000 rl_next_input = None
1001
1002 def interact(self):
1003 self.keep_running = True
1004 while self.keep_running:
1005 print(self.separate_in, end='')
1006
1007 try:
1008 code = self.prompt_for_code()
1009 except EOFError:
1010 if (not self.confirm_exit) \
1011 or self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'):
1012 self.ask_exit()
1013
1014 else:
1015 if code:
1016 self.run_cell(code, store_history=True)
1017
1018 def mainloop(self):
1019 # An extra layer of protection in case someone mashing Ctrl-C breaks
1020 # out of our internal code.
1021 while True:
1022 try:
1023 self.interact()
1024 break
1025 except KeyboardInterrupt as e:
1026 print("\n%s escaped interact()\n" % type(e).__name__)
1027 finally:
1028 # An interrupt during the eventloop will mess up the
1029 # internal state of the prompt_toolkit library.
1030 # Stopping the eventloop fixes this, see
1031 # https://github.com/ipython/ipython/pull/9867
1032 if hasattr(self, '_eventloop'):
1033 self._eventloop.stop()
1034
1035 self.restore_term_title()
1036
1037 # try to call some at-exit operation optimistically as some things can't
1038 # be done during interpreter shutdown. this is technically inaccurate as
1039 # this make mainlool not re-callable, but that should be a rare if not
1040 # in existent use case.
1041
1042 self._atexit_once()
1043
1044 _inputhook = None
1045 def inputhook(self, context):
1046 warn(
1047 "inputkook seem unused, and marked for deprecation/Removal as of IPython 9.0. "
1048 "Please open an issue if you are using it.",
1049 category=DeprecationWarning,
1050 stacklevel=2,
1051 )
1052 if self._inputhook is not None:
1053 self._inputhook(context)
1054
1055 active_eventloop: str | None = None
1056
1057 def enable_gui(self, gui: str | None = None) -> None:
1058 if gui:
1059 from ..core.pylabtools import _convert_gui_from_matplotlib
1060
1061 gui = _convert_gui_from_matplotlib(gui)
1062
1063 if self.simple_prompt is True and gui is not None:
1064 if gui == "tk":
1065 print(
1066 "Tk is supported natively when running with `--simple-prompt`; "
1067 "no event loop hook will be installed."
1068 )
1069 else:
1070 print(
1071 f'Cannot install event loop hook for "{gui}" when running with `--simple-prompt`.'
1072 )
1073 print(
1074 "NOTE: Tk is supported natively; use Tk apps and Tk backends with `--simple-prompt`."
1075 )
1076 return
1077
1078 if self._inputhook is None and gui is None:
1079 print("No event loop hook running.")
1080 return
1081
1082 if self._inputhook is not None and gui is not None:
1083 newev, newinhook = get_inputhook_name_and_func(gui)
1084 if self._inputhook == newinhook:
1085 # same inputhook, do nothing
1086 self.log.info(
1087 f"Shell is already running the {self.active_eventloop} eventloop. Doing nothing"
1088 )
1089 return
1090 self.log.warning(
1091 f"Shell is already running a different gui event loop for {self.active_eventloop}. "
1092 "Call with no arguments to disable the current loop."
1093 )
1094 return
1095 if self._inputhook is not None and gui is None:
1096 self.active_eventloop = self._inputhook = None
1097
1098 if gui and (gui not in {None, "webagg"}):
1099 # This hook runs with each cycle of the `prompt_toolkit`'s event loop.
1100 self.active_eventloop, self._inputhook = get_inputhook_name_and_func(gui)
1101 else:
1102 self.active_eventloop = self._inputhook = None
1103
1104 self._use_asyncio_inputhook = gui == "asyncio"
1105
1106 # Run !system commands directly, not through pipes, so terminal programs
1107 # work correctly.
1108 system = InteractiveShell.system_raw
1109
1110 def auto_rewrite_input(self, cmd):
1111 """Overridden from the parent class to use fancy rewriting prompt"""
1112 if not self.show_rewritten_input:
1113 return
1114
1115 tokens = self.prompts.rewrite_prompt_tokens()
1116 if self.pt_app:
1117 print_formatted_text(PygmentsTokens(tokens), end='',
1118 style=self.pt_app.app.style)
1119 print(cmd)
1120 else:
1121 prompt = ''.join(s for t, s in tokens)
1122 print(prompt, cmd, sep='')
1123
1124 _prompts_before = None
1125 def switch_doctest_mode(self, mode):
1126 """Switch prompts to classic for %doctest_mode"""
1127 if mode:
1128 self._prompts_before = self.prompts
1129 self.prompts = ClassicPrompts(self)
1130 elif self._prompts_before:
1131 self.prompts = self._prompts_before
1132 self._prompts_before = None
1133# self._update_layout()
1134
1135
1136InteractiveShellABC.register(TerminalInteractiveShell)
1137
1138if __name__ == '__main__':
1139 TerminalInteractiveShell.instance().interact()