Terminal Takeaway πŸ₯‘

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

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"
}
2 Likes