In older (I believe pre-5.0) versions of IPython, if I was working on a line/block, and suddenly discovered I needed to investigate something else to finish it, my approach was to hit Ctrl-C, which left the incomplete line/block on screen, but unexecuted, and gave me a fresh prompt. That is, I'd see something like:
In [1]: def foo():
...: stuff^C # <-- Just realized I needed to check something on stuff usage
In [2]: # <-- cursor goes to new line, but old stuff still on terminal
In newer IPython (which seems to have switched from readline
to prompt_toolkit
as the "CLI support framework"), the behavior of Ctrl-C differs; now, instead of giving me a newline, it just resets the current one, discarding everything I've typed and returning the cursor to the beginning of the line.
# Before:
In [1]: def foo():
...: stuff
# After Ctrl-C:
In [1]: # Hey, where'd everything go?
This is extremely annoying, since I can no longer see or copy/paste the code I was working on to resume my work after I've done whatever side task precipitated the need for a fresh prompt.
My question is: Is there any way to restore the old IPython behavior, where Ctrl-C does the following things:
I've searched everywhere, and the most I've found is a bug report comment that mentions this new behavior in passing as "...a change from earlier versions of IPython, but it is intentional."
I haven't been able to find anything documented about modifying the behavior in the IPython or prompt_toolkit
documentation; I've found where a lot of these handlers are installed, but attempts at monkey-patching to alter the current behavior have failed (and frankly, monkey-patching undocumented code means I risk it breaking every upgrade, so I'd like to find some semi-supported fix for this; failing that, hacky monkey-patching is acceptable though).
And after more research, I found what appears to be a supported approach, relying on the IPython keyboard shortcuts documentation (documented slightly differently for 5.x and 6.x).
The solution is to create a file in ~/.ipython/profile_default/startup
(any name, ending with .py
or ipy
is fine, e.g. fixctrlc.py
), and add the following:
def fix_ctrlc():
'''Set up bindings so IPython 5.0+ responds to Ctrl-C as in pre-5.0
Specific behavior is to have Ctrl-C leave existing typed command on
screen and preserved in history. Easily modified to not put in history.
Since this is run as a startup script, all work, including imports,
is done in this function to avoid polluting global namespace.
Updates made as needed at https://stackoverflow.com/a/45600868/364696
'''
from IPython import get_ipython
from prompt_toolkit.enums import DEFAULT_BUFFER
from prompt_toolkit.keys import Keys
from prompt_toolkit.filters import HasFocus, ViInsertMode, EmacsInsertMode
ip = get_ipython()
# Determine if we're on a version of IPython that needs a fix,
# acquire the key bindings registry from the appropriate location,
# and establish closure state appropriate to that version of IPython
try:
try:
# IPython 5-6; render_as_done doesn't exist, but manual print works
registry = ip.pt_cli.application.key_bindings_registry
redraw_args = {}
doprint = True
except AttributeError:
# IPython 7+ (tested through 8.0.1)
# render_as_done necessary, and removes need for print
registry = ip.pt_app.key_bindings
redraw_args = {'render_as_done': True}
doprint = False
except AttributeError:
# On an old version of IPython that doesn't need the fix, or
# a new version that changed the registry location. Nothing to do.
return
def on_ctrlc(event):
text = event.cli.current_buffer.text.rstrip()
if text:
# Update cursor position to last non-space char in buffer (so Ctrl-C
# with cursor in middle of block doesn't lose text typed after cursor)
event.cli.current_buffer.cursor_position = len(text)
event.cli.current_buffer.text = text
# Redraw so cursor in correct position before print
event.cli._redraw(**redraw_args)
# (Optional) Put non-empty partial commands in history, not just left on screen
# Delete to leave them on screen, but not in history
event.cli.current_buffer.append_to_history()
# Print a newline to move us past currently typed text so it's not
# replaced on redraw
if doprint:
print()
# Reset/redraw prompt
event.cli.reset()
# Clear active buffer, leaving you with fresh, empty prompt
event.cli.current_buffer.reset()
registry.add_binding(
Keys.ControlC,
filter=(HasFocus(DEFAULT_BUFFER) & (ViInsertMode() | EmacsInsertMode()))
)(on_ctrlc)
fix_ctrlc()
del fix_ctrlc # Avoid polluting global namespace
Please feel free to contribute if you find a better solution.