Chris F.A. Johnson
Unix shell programming notes

15 July 2005

Mousetraps in a shell script

While reading the Linux console_codes man page, I came across a section labeled MOUSE TRACKING. Interesting! I read on: The mouse tracking facility is intended to return xterm-compatible mouse status reports. Does that mean, I wondered, that I can use the mouse in shell scripts?

According to the man page, mouse tracking is available in two modes: X10 compatibility mode which sends an escape sequence on button press and normal tracking mode which sends an escape sequence on both button press and release. Both modes also send modifier information.

To test this, I went to a terminal window and entered printf "\e[?9h". I pressed the mouse button and the computer beeped at me and printed  FB. Repeating the mouse click at various points on the screen netted me more beeps and  &% -( 5. =2 H7 T= ]C fG rJ }M.

A mouse click sends six characters: ESC, [, M, <b>, <x>, <y>. The first three characters are common to all mouse events, second three contain the button pressed, and the x and y locations of the mouse. To confirm this, I saved the input in a variable and piped it to hexdump:

$ printf "\e[?9h"
$ read x
^[[M!MO<ENTER>
$ printf "$x" | hexdump -C
00000000  1b 5b 4d 21 4d 4f                       |.[M!MO|
00000006

The first three appear as expected, but what are the final three? According to the man page, the lower two bits of the button character tell which button has been pressed; the upper bits identify the active modifiers. The x and y co-ordinates are the ASCII values to which 32 has been added to take them out of the range of control characters. ! is 1, " is 2, etc..

That gives us a 1 for the mouse button (which means button 2, since 0, 1 and 2 are buttons 1, 2, and 3 respectively, and 4 is release. The x and y co-ordinates are 45 (Ox4d=77; 77−32=45) and 47.

Surprisingly, since I read about mouse tracking in a Linux console_codes man page, these escape codes do not work in any Linux console that I have tried. They work in xterm, rxvt and gnome-terminal on Linux and FreeBSD. I've used them on FreeBSD and NetBSD, via ssh from a Linux rxvt terminal window. They do not work in a konsole window.

We now know that mouse reporting works (in most xterm windows), and we can get information from a mouse click on the standard input. That leaves two questions: how do we read the information into a variable (without having to press return), and how can the button and x,y information be decoded in a shell script?

With bash, the read command can take an argument to specify the number of characters:

read -n6 x

More portably, stty and dd can be used:

_STTY=$(stty -g)          ## Save current terminal settings
printf "\e[?9h"           ## Turn on mouse reporting
stty -echo -icanon        ## Turn off echo and line buffering,
x=$(dd bs=1 count=6 2>/dev/null) ## Read six characters
echo "$x" | hexdump -C    ## Display the characters in hex
printf "\e[?9l"           ## Turn off mouse reporting
stty "$_STTY"             ## Restore terminal

Neither of these is adequate for a real script (not all input will be mouse clicks, and we will want to get single keystrokes), but they suffice to demonstrate the concept.

The next step is to decode the input. For the purpose of this demonstration, we will assume that the six characters do indeed represent a mouse click, and that the first three characters are ESC, [, and M. We are only interested in the last three, so we extract them into three separate variables using POSIX parameter expansion:

m1=${x#???}    ## Remove the first 3 characters
m2=${x#????}   ## Remove the first 4 characters
m3=${x#?????}  ## Remove the first 5 characters

Then we convert the first character of each variable to its ASCII value. This uses a POSIX printf extension, "If the leading character is a single-quote or double-quote, the value shall be the numeric value in the underlying codeset of the character following the single-quote or double-quote."

Since command substitution is slow in all shells except KornShell93, the three assignments are grouped in a single eval statement:

eval "$(printf "mb=%d mx=%d my=%d" "'$m1" "'$m2" "'$m3")"

Finally, we interpret the ASCII values. For the mouse button, we do a bitwise AND 3. For the x and y co-ordinates, we subtract 32:

## Values > 127 are signed, so fix if less than 0
[ $mx -lt 0 ] && mx=$(( 255 + $mx ))
[ $my -lt 0 ] && my=$(( 255 + $my ))

BUTTON=$(( ($mb & 3) + 1 ))
MOUSEX=$(( $mx - 32 ))
MOUSEY=$(( $my - 32 ))

Putting it all together, the script in Listing 1 prints the mouse's co-ordinates at that location whenever you press a mouse button.

There are two buttons on the top row. Clicking the left button toggles the mouse reporting mode between reporting only a button press and reporting the release as well. Clicking the right button exits the script.

Modified 18 Nov 2021