Fixing Terminal State After Using bind -x in Bash

bashkeyboard shortcutsshell

I define a shell keybinding in my ~/.bashrc like so:

bind -x '"\e\C-w":"/usr/bin/reset"'

This does make Ctrl-Alt-w launch the reset executable. However, it leaves the terminal in a state that resembles the one you get into by pressing Ctrl-v. So, for example, if immediately after resetting in this way I press Ctrl-p, instead of invoking the action of that shortcut (repeat previous command) I get ^P printed.

Any idea what's causing this problem and how to fix it?

EDIT: I am attaching a Python program that reproduces the messed-up terminal state I am trying to fix with the reset. I encounter this condition very often while debugging multi-threaded applications:

import pdb, thread, time

def interrupt():
    time.sleep(2)
    pdb.set_trace()

thread.start_new_thread(interrupt, ())
raw_input('? ')

To reproduce the messed-up state, just run the code above, and press Enter when you see the debugger invoked (it will print a bunch of text, ending with pdb.set_trace(), about two seconds after you run the program and the initial prompt appears).

Best Answer

Running reset clears the terminal display and also resets all input settings to their default. In particular, it sets the input mode to cooked, i.e. the terminal reads one line at a time before sending the whole line to the application (here, the application is bash). The terminal's line editor is an extremely primitive one that only understands backspace, nothing fancier. Bash provides a sophisticated line editor; it switches the terminal to raw mode, where each character is sent to the application as soon as it's typed.

If you find yourself with a messed up terminal (no line edition or no echo at the bash prompt), the easiest way to restore it is to run the command reset or stty sane. Usually you can type them blind and press Return. If that doesn't work (e.g. because the terminal is in cooked mode and the line submission character isn't the default one), you can run reset 2>/dev/pts/42 (terminal reinitialization) or stty sane </dev/pts/42 (input configuration reinitialization) (note the different redirections) where /dev/pts/42 is the terminal that the shell is running in. Finding the terminal name if you can't run commands in it may take a bit of guessing. From within the terminal, the command tty would display it. If you can find the right bash process in the ps output, you want the TTY column, with /dev in front.

Running these commands by typing them at the bash prompt does the right thing, but running them as part of a readline macro not so much. Bash resets the terminal settings each time it prints a new prompt, so what you do during the edition of one line doesn't last until subsequent commands.

Furthermore, if you run reset during the edition of a line, this messes up the parameters that bash relies on: in particular, it sets the terminal mode to cooked, whereas bash line edition requires that the line editor receives the input character by character. Comparing the output of stty during a bash command line edition and while not at a bash prompt, I think these are the settings you need:

bind -x '"\e\C-w": "reset; stty -icrnl -icanon -echo </dev/tty"'

If you're calling reset only to clear the display, call tput rs1 rs2 rs3 rf instead of reset.

As I wrote above, the right way to reset the terminal settings is to run reset at the bash prompt. Running it as part of a key binding doesn't work because bash restores the settings left over by the last application (the one that left the terminal settings in a mess) when it displays the next prompt. I don't think bash has any built-in feature to instead reset terminal settings to a sane default, but you can do that with user configuration if you want, with the following line in your .bashrc:

PROMPT_COMMAND="$PROMPT_COMMAND
stty sane"

If you really want to have a key binding that resets the terminal settings during line edition, you need something more sophisticated. Cause bash to run the reset command at a prompt (as opposed to as part of an editing command) then resume the current edit. This isn't easy to do in bash because bindings can only be readline macros or bash functions but you can't mix the two. The following code binds Ctrl+Meta+W to a readline macro that calls a bash function via a binding, then calls the accept-line readline function via its \C-m binding, and then calls another bash function via another binding. The bind -x bindings can only be assigned to key sequences of length 1 or 2, so I use little-used C-x LETTER combinations for the helper macros.

run_command_during_line_edition () {
  saved_READLINE_LINE=$READLINE_LINE saved_READLINE_POINT=$READLINE_POINT
  READLINE_POINT=0 READLINE_LINE=" $1"
  unset run_command_first
}
restore_saved_command () {
  READLINE_LINE=$saved_READLINE_LINE READLINE_POINT=$saved_READLINE_POINT
  unset saved_READLINE_LINE saved_READLINE_POINT
}
bind -x '"\C-xZ": "restore_saved_command"'
bind -x '"\C-xR": "reset; stty sane -icrnl -icanon -echo; run_command_during_line_edition reset"'
bind '"\e\C-w": "\C-xR\r\C-xZ"'

Again, you probably don't need all that complication — blindly typing reset at the prompt, or including stty sane in your PROMPT_COMMAND, should solve your problem. Oh, or you could switch to zsh where all of this would be a breeze.

Related Question