Mouse support in terminal with pure BASH
Adding an action to mouse input can be as easy as:
function MouseClick1() {
echo 'Mouse button 1 was clicked!'
}
This has been a pet Shell project of mine for some time and while itβs definitely not finished this is the first easy to use version. Please excuse the lack of code comments and explanations, this project went a mile deep and I honestly wouldnβt know where to start. If thereβs something youβd like to know just ask.
The GIF fps is pretty low so it catches some blank frames but itβs smoother in practice (especially in Alacritty). I also donβt know any tricks yet for making Shell scripts visually performative.
Captures
- L/M/R mouse click
- L/M/R mouse drag
- L/M/R mouse button state (up/down)
- Scroll wheel up/down
- Row/Column Coordinates of mouse actions
mouse-in-terminal demo
mouse-in-terminal
#!/usr/bin/env bash
# To do: <FOSS License here>
# Must be run in the active session to catch inputs
# Examples: `source mouse-in-terminal` or `. mouse-in-terminal`
LogVerbose=
Log(){
[ -n "$LogVerbose" ] && echo "$1"
}
FunctionExists() {
declare -f -F "$1" > /dev/null
return $?
}
# Enable capture
printf '\033[?1000;1002;1006;1015h'
# Declare binds
declare -A Bindings=(
['<0;']='ReadInput MouseClick1 \<0;'
['<1;']='ReadInput MouseClick2 \<1;'
['<2;']='ReadInput MouseClick3 \<2;'
['<32;']='ReadInput MouseDrag1 \<32;'
['<33;']='ReadInput MouseDrag2 \<33;' # Supported in Alacritty
['<34;']='ReadInput MouseDrag3 \<34;'
['<64;']='ReadInput MouseScrollUp \<64;'
['<65;']='ReadInput MouseScrollDown \<65;'
)
# Apply binds
for KeySeq in "${!Bindings[@]}"; do
bind -x "\"\033[$KeySeq\":${Bindings[$KeySeq]}"
done
ReadInput() {
[ -n "$LogVerbose" ] && Log '=== Read Input ==='
declare -A Input=()
Type=$1
Input['Type']=$Type
Axis='X'
Buffer=''
while read -r -n 1 -s Key; do
[ -n "$LogVerbose" ] && Log "Reading:$Key"
Buffer="$Buffer$Key"
if [[ $Key == ';' ]]; then
Axis='Y'
elif [[ $Key =~ [0-9] ]]; then
Input[$Axis]="${Input[$Axis]}$Key"
else
Input['State']=$Key
break
fi
done
if [ -n "$LogVerbose" ]; then
Log "EscSeq=\033[$2$Buffer"
Log "Buffer=$Buffer"
for AKey in "${!Input[@]}"; do
Log "${AKey}=${Input[$AKey]}"
done
fi
# If a function for the type of mouse input exists, run it.
FunctionExists "${Type}" && ${Type}
}
What I used with it to produce the GIF demo
PS1=''
tput civis
tput setaf 2 # Set color to green
clear
function MouseClick1() {
tput setaf 2
DrawBox "${Input['X']}" "${Input['Y']}" "ββββββ Click"
}
function MouseDrag1() {
tput setaf 1
DrawBox "${Input['X']}" "${Input['Y']}" "ββββββ Drag"
}
function MouseClick2() {
tput setaf 5
DrawBox "${Input['X']}" "${Input['Y']}" "ββββββ M-Click"
}
function MouseDrag2() {
tput setaf 5
DrawBox "${Input['X']}" "${Input['Y']}" "ββββββ M-Drag"
}
function MouseClick3() {
tput setaf 4
DrawBox "${Input['X']}" "${Input['Y']}" "ββββββ R-Click"
}
function MouseDrag3() {
tput setaf 3
DrawBox "${Input['X']}" "${Input['Y']}" "ββββββ R-Drag"
}
ScrollY=10
function MouseScrollUp() {
tput setaf 6
(( ScrollY-- ))
DrawBox 3 "$ScrollY" "ββ Scroll Up"
}
function MouseScrollDown() {
tput setaf 6
(( ScrollY++ ))
DrawBox 3 "$ScrollY" "ββ Scroll Down"
}
function DrawBox() {
X=$(( $1 - 3 ))
Y=$(( $2 - 1 ))
# Only draw if coords are different
if [ "$X" != "$LastX" -o "$X" != "$LastY" ]; then
clear
tput home
echo -e "\n x=$X y=$Y state="${Input['State']}""
LastX=$X
LastY=$Y
# Draw new box
for N in 0 1 2; do
DrawAtPos "$(( Y + N ))" "$X" "$3"
done
fi
}
function DrawAtPos(){
printf '\033[s' # Save cursor position
printf "\33[%d;%dH%s" "$@"
printf '\033[u' # Restore cursor position
}
Showcase for attaching a function to a mouse event
# The script will call functions with the following names when
# a mouse event occurs matching what the name implies. If a
# function for the action doesn't exist, it'll do nothing.
# MouseClick1
# MouseClick2
# MouseClick3
# MouseDrag1
# MouseDrag2
# MouseDrag3
# MouseScrollUp
# MouseScrollDown
# To run a function when a user clicks mouse button 1 for
# instance, just name it "MouseClick1", example:
function MouseClick1() {
X="${Input['X']}" # Column action occured on
Y="${Input['Y']}" # Row action occured on
Type="${Input['Type']}" # Source of the action (ex: MouseClick1)
State="${Input['State']}" # M is down, m is up (as read)
if [ "$State" = "m" ]; then HumanReadableState='up'
elif [ "$State" = "M" ]; then HumanReadableState='down'
fi
echo "You did $Type at coordinate $X,$Y with the button $HumanReadableState"
}