Created
July 19, 2024 11:20
-
-
Save maxnikulin/3c33c67c058f9091a05581dc593a58fd to your computer and use it in GitHub Desktop.
Allow forward-search-history (Control-s) in BASH command promp
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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