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