2

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

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271

1 Answers1

6

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.

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271