How can I setup a hybrid readline with emacs insert mode and vi command mode?

I read @Tom Hale's answers here and here.

I think instead of moving vi-insert bindings to emacs, the better approach is to move emacs bindings to vi-insert. The reason is because vi-command has bindings that switch to vi-insert and it's hard to emulate the functionality to make it switch to emacs mode.

For example, the A command in vi-command defaults to vi-append-eol (appending at the end of the line and switch to vi-insert).

You can't make A switch to emacs mode because it is bound to a function and not a macro.

For example, this wouldn't work

"A": vi-append-eol emacs-editing-mode

Nor this, using @Tom Hale's answer

"A": vi-append-eol "\ee"

You could do this:

"A": "$a\ee"

But now that depends on the "a", vi-append-mode command, which also needs to be rebound. "a" could be move forward then "i". "i" could just switch to emacs. There's a whole chain of commands to translate into macros which is a pain to do.

So you are better off moving the emacs bindings to vi-insert.


So we want to set vi-insert bindings that are unique to emacs And we want to make a decision on which binding to use if they have different bindings for the same key sequence. If they have the exact same binding, we ignore them.

This can be done with this command

comm -3\
  <(INPUTRC=/dev/null bash -c 'bind -pm emacs' |
    LC_ALL='C' grep -vE '^#|: (do-lowercase-version|self-insert)$' |
    sort) \
  <(INPUTRC=/dev/null bash -c 'bind -pm vi-insert' |
    LC_ALL='C' grep -vE '^#|: (do-lowercase-version|self-insert)$' |
    sort) | cat

The reason why the | cat is there is explained here

The -3 throws away the "exact same bindings" So you go through this list and look for bindings on the left column. For each binding on the left column:

If there is a duplicate binding for the same key sequence, such as

"\C-d": delete-char
    "\C-d": vi-eof-maybe

Choose one of them. If you want the vi-insert one (on the right), you can delete both lines, because we will be adding these bindings to vi-insert which already has the vi-insert binding. If you want the emacs one (on the left), delete the vi-insert one.

If there is a unique binding on the right column (vi-insert), such as

    "\e": vi-movement-mode

delete it because `vi-insert already has it.

The rest of the bindings will be on the left column (emacs). Leave these alone because we will add these to vi-insert.


Here is my .inputrc with the emacs bindings I selected added to vi-insert.

I decided not to use the "kj" to switch to vi-command in @Tom Hale's answer because it can be done with "\ee" which takes you from emacs to vi-insert then another "\e" which takes you from vi-insert to vi-command. There are actually words other than blackjack containing kj and words with jk (mostly place names)

I kept "\C-d": delete-char and threw away "\C-d": vi-eof-maybe. Because I could just use Enter for vi-eof-maybe and I don't want to accidentally quit readline by pressing "\C-d". This means to delete the "\C-d": vi-eof-maybe binding because we are overriding the vi-eof-maybe binding in vi-insert mode with the delete-char binding.

I kept "\C-n": menu-complete instead of "\C-n": next-history because I could just use down arrow for next-history. This means to delete both bindings because vi-insert already has the menu-complete binding.

I kept "\C-p": menu-complete-backward instead of "\C-p": previous-history because I could press up arrow for previous-history. This means to delete both bindings because vi-insert already has the menu-complete-backward binding.

I kept "\C-w": vi-unix-word-rubout instead of "\C-w": unix-word-rubout. I don't know what's the difference. I just stuck with the vi-insert one. This means to delete both bindings because vi-insert already has the vi-unix-word-rubout binding.

I kept "\e": vi-movement-mode. This means to delete this binding because vi-insert already has the vi-movement-mode binding.

set editing-mode vi

set keymap emacs
"\ee": vi-editing-mode

set keymap vi-command
"\ee": emacs-editing-mode

# key bindings to get out of vi-editing-mode
set keymap vi-insert
"\ee": emacs-editing-mode

# emacs keybindings in vi-insert mode
"\C-@": set-mark
"\C-]": character-search
"\C-_": undo
"\C-a": beginning-of-line
"\C-b": backward-char
"\C-d": delete-char
"\C-e": end-of-line
"\C-f": forward-char
"\C-g": abort
"\C-k": kill-line
"\C-l": clear-screen
"\C-o": operate-and-get-next
"\C-q": quoted-insert
"\C-x!": possible-command-completions
"\C-x$": possible-variable-completions
"\C-x(": start-kbd-macro
"\C-x)": end-kbd-macro
"\C-x*": glob-expand-word
"\C-x/": possible-filename-completions
"\C-x@": possible-hostname-completions
"\C-x\C-?": backward-kill-line
"\C-x\C-e": edit-and-execute-command
"\C-x\C-g": abort
"\C-x\C-r": re-read-init-file
"\C-x\C-u": undo
"\C-x\C-v": display-shell-version
"\C-x\C-x": exchange-point-and-mark
"\C-xe": call-last-kbd-macro
"\C-xg": glob-list-expansions
"\C-x~": possible-username-completions
"\e ": set-mark
"\e!": complete-command
"\e#": insert-comment
"\e$": complete-variable
"\e&": tilde-expand
"\e*": insert-completions
"\e-": digit-argument
"\e.": insert-last-argument
"\e.": yank-last-arg
"\e/": complete-filename
"\e0": digit-argument
"\e1": digit-argument
"\e2": digit-argument
"\e3": digit-argument
"\e4": digit-argument
"\e5": digit-argument
"\e6": digit-argument
"\e7": digit-argument
"\e8": digit-argument
"\e9": digit-argument
"\e<": beginning-of-history
"\e=": possible-completions
"\e>": end-of-history
"\e?": possible-completions
"\e@": complete-hostname
"\e\C-?": backward-kill-word
"\e\C-]": character-search-backward
"\e\C-e": shell-expand-line
"\e\C-g": abort
"\e\C-h": backward-kill-word
"\e\C-i": dynamic-complete-history
"\e\C-r": revert-line
"\e\C-y": yank-nth-arg
"\e\\": delete-horizontal-space
"\e\e": complete
"\e^": history-expand-line
"\e_": insert-last-argument
"\e_": yank-last-arg
"\eb": backward-word
"\ec": capitalize-word
"\ed": kill-word
"\ef": forward-word
"\eg": glob-complete-word
"\el": downcase-word
"\en": non-incremental-forward-search-history
"\ep": non-incremental-reverse-search-history
"\er": revert-line
"\et": transpose-words
"\eu": upcase-word
"\ey": yank-pop
"\e{": complete-into-braces
"\e~": complete-username

UPDATE

I think I made it a little better. For a while, I turned back on the "jk" mooshing to switch to vi command because pressing "\e" to switch to vi-command had a delay since a lot of emacs commands moved over to vi-insert uses "\e" as a leader.

This shows the "jk" mooshing commented out. I currently use "\ee" to cycle modes. I didn't unbind the "\e" to switch from vi-insert to vi-command because I don't see a need. So it has the effect where if you press "\e" in vi-insert and wait, you will go to vi-command.

To get from vi-command to vi-insert, you'll just press one of the commands such as "A" or "i" so allowing cycling between 3 modes won't hurt because you can also just cycle between the 2 vi modes.

set keymap emacs
"\ee": vi-editing-mode

set keymap vi-command
"\ee": emacs-editing-mode

# key bindings to get out of vi-editing-mode
set keymap vi-insert
# Choose one of these editor switching modes
# 
# moosh jk to switch
#"\ee": emacs-editing-mode
#"\ejk": vi-movement-mode
#"\ekj": vi-movement-mode
#
# "\ee" to cycle
# can unmap "\e" to switch to vi-command but don't see a need
#"\e":
"\ee": vi-movement-mode

UPDATE

In vi-insert, "\C-w" is bound to `vi-unix-word-rubout, which stops at word boundaries or something. I didn't like that functionality.

For example, try this

$ cannot-delete'
# press left arrow to go back behind the single quote
# press \C-w in vi-insert to try to delete cannot-delete, it won't work

this bug report describes the problem, although I don't have a problem with the example provided.

So you can bind "\C-w" to the emacs unix-word-rubout to fix this.

To rebind "\C-w", you might need to unbind defaults.

# In .bashrc
stty werase undef

If you would like to unbind all defaults:

# In .inputrc
set bind-tty-special-chars off

Not sure if it matters, but I am on macOS so there are other default bindings I remove.

Then in your .inputrc:

"\C-w": unix-word-rubout

The following .inputrc lines allow Meta / Alt+E to switch between emacs and vi-insert modes.

Mooshing both j and k simultaneously will take you to vi-command mode.

Note: The only English word with "kj" is "blackjack", no words contain "jk")

set show-mode-in-prompt on

set keymap emacs
"\ee": vi-editing-mode
"jk": "\eejk"
"kj": "\eejk"

set keymap vi-insert
"\ee": emacs-editing-mode
"jk": vi-movement-mode
"kj": vi-movement-mode

set keymap vi-command
"\ee": emacs-editing-mode

Note: In bash v4.3.11(1), if you add a binding under keymap emacs to vi-movement-mode to try to switch straight to the vi-command keymap, the prompt doesn't update if you have show-mode-in-prompt on, hence this work-around is needed.


Interesting factoids:

There are only 4 bindings unique to vi-insert mode, which can be easily added to emacs mode:

"\C-d": vi-eof-maybe
"\C-n": menu-complete
"\C-p": menu-complete-backward
"\e": vi-movement-mode

However note that the following are the default emacs bindings:

"\C-d": delete-char
"\C-n": next-history
"\C-p": previous-history

Which I resolved as follows:

set keymap emacs
"\e": "kj" # needs to be below "kj" mapping
"\C-d": delete-char # eof-maybe: ^D does nothing if there is text on the line
"\C-n": menu-complete
"\C-p": menu-complete-backward
"\C-y": previous-history # historY
"\e\C-y": previous-history

Old answer

This is how I did it before I could go directly from emacs to vi-command. It involves importing all the default emacs commands into the vi-insert keymap.

Get the default emacs-standard and vi-insert mappings:

INPUTRC=~/dev/null bash -c 'bind -pm emacs-standard' | grep -vE '^#|: (do-lowercase-version|self-insert)$' | sort > emacs-standard
INPUTRC=~/dev/null bash -c 'bind -pm vi-insert' | grep -vE '^#|: (do-lowercase-version|self-insert)$' | sort > vi-insert

Get only the additions from emacs-standard:

comm -23  emacs-standard vi-insert > vi-insert-emacs-additions

Then, in your ~/.inputrc, add the content of vi-insert-emacs-additions under the lines:

(echo set editing-mode vi && echo set keymap vi-insert && cat vi-insert-emacs-additions) >> ~/.inputrc

For your convenience, on bash 4.3.11(1)-release, vi-insert-emacs-additions contents are:

"\C-a": beginning-of-line
"\C-b": backward-char
"\C-]": character-search
"\C-d": delete-char
"\C-e": end-of-line
"\C-f": forward-char
"\C-g": abort
"\C-k": kill-line
"\C-l": clear-screen
"\C-n": next-history
"\C-o": operate-and-get-next
"\C-p": previous-history
"\C-q": quoted-insert
"\C-@": set-mark
"\C-_": undo
"\C-x\C-?": backward-kill-line
"\C-x\C-e": edit-and-execute-command
"\C-x\C-g": abort
"\C-x\C-r": re-read-init-file
"\C-x\C-u": undo
"\C-x\C-v": display-shell-version
"\C-x\C-x": exchange-point-and-mark
"\C-xe": call-last-kbd-macro
"\C-x)": end-kbd-macro
"\C-xg": glob-list-expansions
"\C-x*": glob-expand-word
"\C-x!": possible-command-completions
"\C-x/": possible-filename-completions
"\C-x@": possible-hostname-completions
"\C-x~": possible-username-completions
"\C-x$": possible-variable-completions
"\C-x(": start-kbd-macro
"\e0": digit-argument
"\e1": digit-argument
"\e2": digit-argument
"\e3": digit-argument
"\e4": digit-argument
"\e5": digit-argument
"\e6": digit-argument
"\e7": digit-argument
"\e8": digit-argument
"\e9": digit-argument
"\eb": backward-word
"\e<": beginning-of-history
"\e\C-?": backward-kill-word
"\ec": capitalize-word
"\e\C-]": character-search-backward
"\e\C-e": shell-expand-line
"\e\C-g": abort
"\e\C-h": backward-kill-word
"\e\C-i": dynamic-complete-history
"\e!": complete-command
"\e/": complete-filename
"\e@": complete-hostname
"\e{": complete-into-braces
"\e~": complete-username
"\e$": complete-variable
"\e\C-r": revert-line
"\e\C-y": yank-nth-arg
"\e\\": delete-horizontal-space
"\e-": digit-argument
"\ed": kill-word
"\e\e": complete
"\e>": end-of-history
"\ef": forward-word
"\eg": glob-complete-word
"\e^": history-expand-line
"\e#": insert-comment
"\e*": insert-completions
"\e_": insert-last-argument
"\e.": insert-last-argument
"\el": downcase-word
"\en": non-incremental-forward-search-history
"\ep": non-incremental-reverse-search-history
"\e=": possible-completions
"\e?": possible-completions
"\er": revert-line
"\e ": set-mark
"\e&": tilde-expand
"\et": transpose-words
"\eu": upcase-word
"\e_": yank-last-arg
"\e.": yank-last-arg
"\ey": yank-pop

Note: If you add a binding under keymap emacs to vi-movement-mode to try to switch straight to vi-command mode, the prompt doesn't update if you have show-mode-in-prompt on.

This is why the above solution adds emacs bindings to vi-insert. This makes for a longer .inputrc, but is required for a complete solution.

Tags:

Shell

Readline