Skip to content

Instantly share code, notes, and snippets.

@maxnikulin
Created July 19, 2024 11:20
Show Gist options
  • Save maxnikulin/3c33c67c058f9091a05581dc593a58fd to your computer and use it in GitHub Desktop.
Save maxnikulin/3c33c67c058f9091a05581dc593a58fd to your computer and use it in GitHub Desktop.
Allow forward-search-history (Control-s) in BASH command promp
# Allow readline forward-search-history (C-s) in bash command prompt
# Should be sourced from a bashrc file.
# Copyright (C) 2024 Max Nikulin
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# <http://www.gnu.org/licenses/>
[ -t 0 ] || return
[[ "$PS0" != *_readline_ixon* ]] || return
# Skip for dumb terminals, e.g. Emacs shell buffers.
[[ ":$SHELLOPTS:" =~ :vi:|:emacs: ]] || return
# Settings
# Flow control state for BASH prompt.
# Use empty value (not unset) to disable separate state management.
# Alternatively "-ixon" (default for this script) disables flow control
# for command line prompt
# and "ixon" forces flow control even if it is disabled
# while foreground commands are running.
# READLINE_IXON_PROMPT=
# Non-empty value to report stty ixon state on each step
# READLINE_IXON_DEBUG=1
# Motivation
# The readline library allows backward and forward search
# in BASH prompt. In default emacs keymap they are bound
# to Control-r and Control-s accordingly. Unfortunately Control-s
# conflicts with terminal control characters.
# Control-s and Control-q act as pause (stop) and start output
# when flow control is active (default).
# In readline Control-s is bound to forward-search-history
# and Control-q to quoted-insert.
# Terminal control characters have higher priority though.
# While Control-v may be used instead of Control-q in BASH prompt,
# unavailable forward search is sometimes inconvenient.
#
# Flow control may be disabled completely by
#
# stty -ixon
#
# (and "stty ixon" enables it), but sometimes it is necessary
# to pause verbose output of some commands.
# <https://wiki.archlinux.org/title/readline#History>
# suggests Escape s or Meta-s (Alt-s) binding
# as an alternative to disabling flow control,
# but it causes divergence from Emacs and it is too much
# taking into account Control-f search shortcut in other applications.
#
# This script is intended to manage independent flow control states
# for prompt and for commands in BASH.
# Emacs and Vim disables flow control state, so Control-s works there.
# Some applications do not bother, for example
# Control-q does not allow to exit from rtorrent.
#
# While prompt is active, flow control is managed through
# key bindings. Control-x Control-s enables it and activates
# Control-s and Control-q for pause/resume output.
# To disable it use Control-x Shift-S that starts search.
# Utilities like stty may be used to change flow control
# state for commands.
# Control-x Shift-Q disables separate state management.
# Internals
# BASH variables controlling user prompt are used to save
# state before prompt appears and restore it before command
# execution. It is tricky because BASH manages terminal state
# itself. It saves flow control state after execution
# of PROMPT_COMMAND and after expansion of PS1.
# State is restored before PS0 expansion and command execution.
# As a result, state change caused by stty command in response to
# a key binding is discarded when command is executed.
# A peculiar case is stty executed in PROMPT_COMMAND.
# It is discarded before PS1 expansion, but it may be restored
# back if some *built-in*, e.g. sleep 10, is interrupted by Control-c.
# That is why it is safer to set flow control state
# in PS1, not in PROMPT_COMMAND. Otherwise it is hard to track
# state for command execution.
_readline_ixon()
{
case "${1:-}" in
setup)
# Set prompt variables
# Save original prompts
if [ -n "${READLINE_IXON_DEBUG:+1}" ]; then
# SC2034 (warning): READLINE_IXON_PS1 appears unused.
# Verify use (or export if used externally).
# shellcheck disable=SC2034
READLINE_IXON_PS1="$PS1"
# shellcheck disable=SC2034
READLINE_IXON_PS0="$PS0"
fi
# Disable flow control for BASH prompt by default
: "${READLINE_IXON_PROMPT=-ixon}"
PROMPT_COMMAND+=("_readline_ixon cmd")
PS1='${READLINE_IXON_PROMPT:+$(_readline_ixon prompt)}'"$PS1"
# Expanded before command execution
# SC2016 (info): Expressions don't expand in single quotes,
# use double quotes for that.
# shellcheck disable=SC2016
PS0='${READLINE_IXON_OUT:+$(_readline_ixon out)}'"$PS0"
_readline_ixon atexit '_readline_ixon exit'
if [ -n "${READLINE_IXON_DEBUG:+1}" ]; then
# SC2016 (info): Expressions don't expand in single quotes,
# use double quotes for that.
# shellcheck disable=SC2016
PS0='${READLINE_IXON_DEBUG:+$(_readline_ixon out-debug)\n}'"$PS0"
PS1='${READLINE_IXON_DEBUG:+$(_readline_ixon prompt-debug)\n}'"$PS1"
fi
;;
cmd)
# Save flow control state after command execution.
# Called from PROMPT_COMMAND.
# Can not be merged with "prompt" because
# it needs to clear or (silently) set variable
# that does not work from prompt command expansion.
if [ -n "${READLINE_IXON_DEBUG:+1}" ]; then
printf '[cmd a=%5s p=%5s o=%5s]\n' \
"$(_readline_ixon actual)" \
"${READLINE_IXON_PROMPT:- }" \
"${READLINE_IXON_OUT:- }"
fi
if [ -z "$READLINE_IXON_PROMPT" ]; then
READLINE_IXON_OUT=
else
READLINE_IXON_OUT="$(stty -a | grep -o '[-]\?ixon')"
fi
;;
prompt)
# Activate state for user prompt.
# Called from PS1.
# Just call stty. It can not be done from "cmd"
# (PROMPT_COMMAND) because BASH saves
# terminal state after it and restores the state
# if C-c interrupts a purely built-in command
# (e.g. "sleep 10").
# That case prompt state, not command one is stored
# in READLINE_IXON_OUT.
stty "$READLINE_IXON_PROMPT" 2>/dev/null
;;
prompt-debug)
if [ -n "${READLINE_IXON_DEBUG:+1}" ]; then
printf "[ps1 %5s%4s%5s]" \
"$(_readline_ixon actual)" \
"${READLINE_IXON_PROMPT:+ to }"\
"${READLINE_IXON_PROMPT}"
fi
;;
out)
# Activate state for command execution.
# Called from PS0.
stty "$READLINE_IXON_OUT" 2>/dev/null
;;
out-debug)
if [ -n "${READLINE_IXON_DEBUG:+1}" ]; then
printf "[out %5s%4s%5s]" \
"$(_readline_ixon actual)" \
"${READLINE_IXON_OUT:+ to }"\
"${READLINE_IXON_OUT}"
fi
;;
disable)
# Disable flow control state management.
# Bound to Control-x Shift-Q by default.
if [ -n "${READLINE_IXON_DEBUG:+1}" ]; then
printf "[ X %5s (%5s)]\n" \
"$(_readline_ixon actual)" \
"$READLINE_IXON_OUT"
fi
READLINE_IXON_PROMPT=
;;
off)
# Turns off flow control state.
# Bound to a key sequence that is called from
# Control-x Shift-S macro by default.
READLINE_IXON_PROMPT=-ixon
if [ -n "${READLINE_IXON_DEBUG:+1}" ]; then
printf "[off %5s to %5s]\n" \
"$(_readline_ixon actual)" \
"$READLINE_IXON_PROMPT"
fi
stty "$READLINE_IXON_PROMPT" 2>/dev/null
;;
on)
# Turns on flow control state.
# Bound to Control-x Control-s by default.
# TODO Can output be stopped by some command?
READLINE_IXON_PROMPT=ixon
if [ -n "${READLINE_IXON_DEBUG:+1}" ]; then
printf "[on %5s to %5s]\n" \
"$(_readline_ixon actual)" \
"$READLINE_IXON_PROMPT"
fi
stty "$READLINE_IXON_PROMPT" 2>/dev/null
;;
actual)
# Get current control state for debug output
stty -a | grep -o "[-]\?ixon"
;;
exit)
# Restore ixon state on exit without modification
# of last executed command exit code.
local retval="$?"
[ -z "$READLINE_IXON_OUT" ] || stty "$READLINE_IXON_OUT"
return "$retval"
;;
atexit)
# Add EXIT trap handler.
# In BASH it is executed for signals causing exit as well.
# Arguments:
# - $2: handler to add
local handler
# trap -- QUOTED_HANDLER EXIT
eval "handler=($(trap -p EXIT))"
handler="${handler[2]}"
# Prepend since another handler may exit.
# Newline to avoid tests for trailing semicolon.
trap -- "$2${handler:+
}$handler" EXIT
;;
*)
# Avoid prompt contamination in the case of errors in the script
;;
esac
}
# Comment out to if you are happy with custom shortcut
# to start forward search.
_readline_ixon setup
# Key bindings may help in the case of verbose output
# from some background process.
case ":$SHELLOPTS:" in
*:emacs:*)
# Control-x Control-s Control-s
# to enable flow control and to stop output.
# Extra Control-s must be hit in addition to
# enabling flow control by Control-x Control-s since
# stop command should be sent to terminal and
# a readline macro is not a workaround here.
bind -m emacs -x '"\C-x\C-s": _readline_ixon on'
# Control-x Shift-Q
# to disable separated flow control states for prompt
# and for command output.
bind -m emacs -x '"\C-xQ": _readline_ixon disable'
# Control-x Shift-S
# to disable flow control and to start forward-search-history
# Control-x Control-s can not be used here because
# flow control has not disabled by "stty -ixon" yet
# Repeating Control-x Shift-S aborts search,
# so Control-s must be used for next match.
# Unsure if isearch-terminators readline variable
# may help.
# Control-x s is not used since it is bound to
# spell-correct-word
# A helper (some inconvenient key sequence)
# and a macro that invokes disable and starts search.
bind -m emacs -x '"\C-xXS": _readline_ixon off'
bind -m emacs '"\C-xS": "\C-xXS\C-s"'
;;
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment