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