pythonipythonkeyboard-shortcutskeyboardinterruptprompt-toolkit

Leave incomplete line on screen when hitting Ctrl-C in ipython 5.0+


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:

  1. Does not execute the line/block typed so far
  2. Leaves it on the screen
  3. Ability to choose (at config time is fine) whether to add to the history (this would be personal preference; do you want half-formed stuff in the history, or just on the terminal for copy/paste?)
  4. Provides me with a fresh prompt below the text typed so far

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).


Solution

  • 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.