Skip to content

Instantly share code, notes, and snippets.

@simonLeary42
Last active May 26, 2026 13:32
Show Gist options
  • Select an option

  • Save simonLeary42/594065d2beee66f74767482c5a5176b7 to your computer and use it in GitHub Desktop.

Select an option

Save simonLeary42/594065d2beee66f74767482c5a5176b7 to your computer and use it in GitHub Desktop.
keeps the readline region active after moving the cursor -- https://dev.to/simonleary42/adding-text-selection-to-bash-4dbc
#!/bin/bash
# source me into your shell!
_readline_region_active_navigation_activate_navigation_hook=''
_readline_region_active_navigation_exchange_point_and_mark_raw=''
function _readline_region_active_navigation() {
# use ugly globals because variable becomes unset before _readline_setup_navigation_hook() runs
declare -n activate_navigation_hook=_readline_region_active_navigation_activate_navigation_hook
declare -n exchange_point_and_mark_raw=_readline_region_active_navigation_exchange_point_and_mark_raw
local control='\C-'
# the keybinds below can vary based on your terminal emulator
# the default assumes that you have the alt/option key set to send the ESC character
# for each keybind, the expected output from `cat -v` is shown in the comment
# if your output does not match, these will need to be changed
local alt='\e' # ^[
local left_arrow='\e[D' # ^[[D
local right_arrow='\e[C' # ^[[C
local control_left_arrow='\e[1;5D' # ^[[1;5D
local control_right_arrow='\e[1;5C' # ^[[1;5C
local alt_control_left_arrow='\e[1;7D' # ^[[1;7D
local alt_control_right_arrow='\e[1;7C' # ^[[1;7C
local home='\e[H' # ^[[H
local end='\e[F' # ^[[F
local alt_control_f12='\e[24;7~' # ^[[24;7~
local alt_control_f11='\e[23;7~' # ^[[23;7~
local alt_control_f10='\e[21;7~' # ^[[21;7~
local alt_control_f9='\e[20;7~' # ^[[20;7~
local alt_control_f8='\e[19;7~' # ^[[19;7~
local alt_control_f7='\e[18;7~' # ^[[18;7~
# the keybinds below are subject to personal preference
# the default is for the emacs-style binds to be "smart" and the arrow-key/home/end binds "raw"
local forward_char_raw="$right_arrow"
local backward_char_raw="$left_arrow"
local forward_word_raw="$control_right_arrow"
local backward_word_raw="$control_left_arrow"
local shell_forward_word_raw="$alt_control_right_arrow"
local shell_backward_word_raw="$alt_control_left_arrow"
local beginning_of_line_raw="$home"
local end_of_line_raw="$end"
exchange_point_and_mark_raw='C-xC-x'
local forward_char_smart="${control}f"
local backward_char_smart="${control}b"
local forward_word_smart="${alt}f"
local backward_word_smart="${alt}b"
local shell_forward_word_smart="${alt}${control}f"
local shell_backward_word_smart="${alt}${control}b"
local beginning_of_line_smart="${control}a"
local end_of_line_smart="${control}e"
declare -a abort_smart=(
"${control}g"
"${control}x${control}g"
"${alt}${control}g"
)
declare -a set_mark_smart=(
"${control}@"
"${alt} "
)
# should be unlikely to be pressed by a human by accident
local set_mark_raw="$alt_control_f7"
local abort_raw="$alt_control_f8"
local set_mark_hook="$alt_control_f9"
local abort_hook="$alt_control_f10"
local setup_navigation_hook="$alt_control_f11"
activate_navigation_hook="$alt_control_f12"
# end config
####################################################################################################
# begin procedure
bind '"'"$set_mark_raw"'": set-mark'
bind '"'"$exchange_point_and_mark_raw"'": exchange-point-and-mark'
bind '"'"$abort_raw"'": abort'
function _readline_set_mark_hook() { READLINE_KEEP_REGION_ACTIVE=1; }
function _readline_abort_hook() { READLINE_KEEP_REGION_ACTIVE=0; }
bind -x '"'"$set_mark_hook"'": _readline_set_mark_hook'
bind -x '"'"$abort_hook"'": _readline_abort_hook'
local keybind
for keybind in "${set_mark_smart[@]}"; do
bind '"'"$keybind"'": "'"${set_mark_hook}${set_mark_raw}"'"'
done
for keybind in "${abort_smart[@]}"; do
bind '"'"$keybind"'": "'"${abort_hook}${abort_raw}"'"'
done
function _readline_setup_navigation_hook() {
# use ugly globals because variable becomes unset before _readline_setup_navigation_hook() runs
declare -n activate_navigation_hook=_readline_region_active_navigation_activate_navigation_hook
declare -n exchange_point_and_mark_raw=_readline_region_active_navigation_exchange_point_and_mark_raw
if [[ "${READLINE_KEEP_REGION_ACTIVE:-0}" == 1 ]]; then
# exchange-point-and-mark also activates the region
# if you run it twice, it cancels itself out and leaves the region active
bind '"'"$activate_navigation_hook"'": "'"${exchange_point_and_mark_raw}${exchange_point_and_mark_raw}"'"'
else
bind -r "$activate_navigation_hook"
fi
}
bind -x '"'"$setup_navigation_hook"'": _readline_setup_navigation_hook'
bind '"'"$backward_char_raw"'": backward-char'
bind '"'"$backward_word_raw"'": backward-word'
bind '"'"$beginning_of_line_raw"'": beginning-of-line'
bind '"'"$end_of_line_raw"'": end-of-line'
bind '"'"$forward_char_raw"'": forward-char'
bind '"'"$forward_word_raw"'": forward-word'
bind '"'"$shell_backward_word_raw"'": shell-backward-word'
bind '"'"$shell_forward_word_raw"'": shell-forward-word'
bind '"'"$backward_char_smart"'": "'"${setup_navigation_hook}${backward_char_raw}${activate_navigation_hook}"'"'
bind '"'"$backward_word_smart"'": "'"${setup_navigation_hook}${backward_word_raw}${activate_navigation_hook}"'"'
bind '"'"$beginning_of_line_smart"'": "'"${setup_navigation_hook}${beginning_of_line_raw}${activate_navigation_hook}"'"'
bind '"'"$end_of_line_smart"'": "'"${setup_navigation_hook}${end_of_line_raw}${activate_navigation_hook}"'"'
bind '"'"$forward_char_smart"'": "'"${setup_navigation_hook}${forward_char_raw}${activate_navigation_hook}"'"'
bind '"'"$forward_word_smart"'": "'"${setup_navigation_hook}${forward_word_raw}${activate_navigation_hook}"'"'
bind '"'"$shell_backward_word_smart"'": "'"${setup_navigation_hook}${shell_backward_word_raw}${activate_navigation_hook}"'"'
bind '"'"$shell_forward_word_smart"'": "'"${setup_navigation_hook}${shell_forward_word_raw}${activate_navigation_hook}"'"'
}
_readline_region_active_navigation
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment