1. 概要
このスクリプトは、ターミナル上で動かしたマウスに合わせて文字を出力します(お絵描きができます)
- 左ボタンでドラッグ中は「*」を、右ボタンでドラッグすると「@」を出力します
-
c
キーで画面をクリア、Ctrl+C
で終了 - 画面の最下部にマウスの座標(行と列の位置)を表示します
2. 動作環境
- Bash(Windowsであればgit bash等。Linuxへのssh接続でも動作します)
- xterm互換(マウスサポート有)ターミナル(GNOME Terminal、iTerm2 、Windows Terminalなど)
3. 予備知識
3-1. ターミナルのrawモードについて
通常(カノニカルモード
)、ターミナルは「1行入力が完了するまで、アプリケーションに渡さない」動作をしています。bashにコマンドを入力する際、Enterキーを押すまでbashには何も通知されません。
そのため、カノニカルモード
ではターミナル自身が押されたキーを表示(echo)します。
ターミナルをrawモード
に切り替えると、キー入力毎にアプリケーションに通知が行われるようになります。例えばvimのようにキーを押したタイミングでモード切り替えが行えるようになります。
ターミナルは押されたキーを表示せずに、アプリケーションへ通知を行います。キー入力を受け取ったアプリケーションが画面描画を行います。
お絵描きシェルスクリプトではrawモード
に切り替えて、キーボード入力を処理しています
(キーボード入力をread
で随時チェックしているため、必ずしもrawモード
に切り替えなくても動作はします・・・・)
3-2. ターミナルのマウスサポートについて
マウスをサポートしているターミナルでは、キーボード入力と同様にマウス入力を受け取ることができます。
(マウス入力を表す、特別なエスケープシーケンスを受け取ります)
- マウス移動時、ターミナルが入力を受け取れるようにするため、エスケープシーケンスを送ります
# Any-event tracking を有効化 (\033[?1003h)
echo -ne "\033[?1003h"
※ \033
はESC
の8進表記です(\e
や\x1b
と書いても同じ)
- ボタンのデータ形式
# ESC+[+M+ボタン情報+X座標+Y座標]
# 合計6byteのバイナリ形式
\x1b[Mbxy
- ESC:0x1B (エスケープシーケンス開始)
- [M:マウスレポート開始(固定)
-
<b>
:ボタンコード+32 (以降、元の値に戻すには-32をする必要がある) -
<x>
:列番号+32 -
<y>
:行番号+32
ボタンコード
<b> の値 |
意味 |
---|---|
0 |
左ボタン押下 |
1 |
中ボタン押下 |
2 |
右ボタン押下 |
3 |
ボタン解放 |
32 |
左ボタン押下状態でマウス移動(ドラッグ) |
34 |
右ボタン押下状態でマウス移動(ドラッグ) |
列番号、行番号について
文字単位での位置を表しています(ドット単位の座標ではない)
# 例えば、10列(x)、20行(y)の位置で左クリックした場合、以下のデータを受け取る
# ボタン0 → <b> = 0+32 = 32(0x20)
# x=10 → <x> = 10+32 = 42(0x2A)
# y=20 → <y> = 20+32 = 52(0x34)
echo -ne "\x1b[M\x20\x2A\x34"
3-3. キーボードやマウス入力のデータをシェルスクリプトで受信するには?
下記のように記載することで、入力データを読み取り、変数にセットすることができます
IFS= read -rsn1 char
# 押されたキーを表示
echo $char
部分 | 意味 |
---|---|
IFS= |
フィールド区切り文字(Internal Field Separator)を空にして、読み取り時に分割されるのを防ぐ |
read |
入力を読み取って変数に代入するBashのビルトインコマンド |
-r |
バックスラッシュ(\ )を特別扱いせず、そのまま読み取る |
-s |
入力をエコーしない(画面に表示されないようにする) |
-n1 |
1文字だけ読み取る。n1 の代わりに n3 にすれば3文字読み取りになる |
char |
読み取った文字を代入する変数名 |
4. スクリプト全体
#!/bin/bash
# ============================================================
# xterm互換ターミナルでAny-event tracking(マウスイベント追跡) を有効にし、
# 左ドラッグをするとその位置に "*"、右ドラッグは"#" を出力するサンプルスクリプト
# ============================================================
# stty設定を保存しておく
orig_stty=$(stty -g)
cleanup() {
# マウストラッキングを解除
echo -ne "\033[?1003l"
# stty 設定を復元
stty "$orig_stty"
echo -e "\nマウスモードを解除しました。終了します。"
exit
}
clear() {
printf '\033[2J\033[H' # 画面をクリア&カーソルを左上に移動
printf "左ボタンを押しながら移動すると '*'、右ボタンだと'@'を出力します。\r\n"
printf "終了するには[Ctrl+C]、画面をクリアするには [c] キーを押してください。\r\n"
}
trap cleanup EXIT INT TERM
# ------------------------------------------------------------
# 1. Any-event tracking を有効化 (\e[?1003h)
# (マウスが移動しただけでもイベントを受け取るモード)
# ------------------------------------------------------------
echo -ne "\033[?1003h"
# ------------------------------------------------------------
# 2. ターミナルを raw モード & echo オフ
# (入力をバッファせず、画面にも表示しない)
# ------------------------------------------------------------
stty raw -echo
# 画面をクリアして説明を表示する
clear
# ------------------------------------------------------------
# 3. メインループ: 入力バイトを解析
# ------------------------------------------------------------
while true; do
IFS= read -rsn1 char
# c キーでクリア
if [[ $char == "c" ]]; then
clear
fi
# ESC(0x1B,033) の場合、マウスイベントかどうかをチェック
if [[ $char == $'\033' ]]; then
# 続く2文字を読み取る
read -rsn2 seq
if [[ $seq == "[M" ]]; then
# マウスデータ3バイト (ボタン情報, x, y)
read -rsn3 mouse_data
button_code=$(printf "%d" "'${mouse_data:0:1}")
x_coord=$(printf "%d" "'${mouse_data:1:1}")
y_coord=$(printf "%d" "'${mouse_data:2:1}")
b=$(( button_code - 32 )) # ボタンコード
x=$(( x_coord - 32 )) # X座標
y=$(( y_coord - 32 )) # &座標
# ボタン判定
case $b in
32) # 左ボタンでドラッグしている場合
# (x, y) に 赤色で"*" を描画
printf "\033[31m\033[%d;%dH*\033[0m" "$y" "$x"
;;
34) # 右ボタンでドラッグしている場合
# (x, y) に 黄色で"@" を描画
printf "\033[1;33m\033[%d;%dH@\033[0m" "$y" "$x"
;;
esac
# ターミナル下部にマウスの位置情報を表示する
printf "\033[999;1H" # 画面の下へ移動
echo -n "button=$b, x=$x, y=$y "
fi
fi
done
4.1 orig_stty=$(stty -g)
現在のターミナル設定を保存
(スクリプト終了時に stty "$orig_stty"
で元の状態に復元できるようにするため)
4.2 cleanup()
関数
- 端末を元の状態に戻す処理
cleanup() {
# マウストラッキングを解除
echo -ne "\033[?1003l"
# stty 設定を復元
stty "$orig_stty"
echo -e "\nマウスモードを解除しました。終了します。"
exit
}
trap cleanup EXIT INT TERM
で、プログラム終了時(EXIT)や、Ctrl+C(INT)時に端末の状態を元に戻します
4.3 clear()
関数
-
\033[2J
で画面全体クリア、\033[H
でカーソルを左上へ移動してから、メッセージを出力します
clear() {
printf '\033[2J\033[H'
printf "左ボタンを押しながら移動すると '*'、右ボタンだと'@'を出力します。\r\n"
printf "終了するには[Ctrl+C]、画面をクリアするには [c] キーを押してください。\r\n"
}
5. まとめ
-
Any‑event tracking (
033[?1003h
) でマウス移動を含む全イベントを取得できるようにする - 受信したマウスイベント
ESC [ M b x y
を解析し、ボタンの状態b
と座標(x,y)
に分解する - 左ドラッグ時は赤「*」、右ドラッグ時は黄「@」をマウスの位置に描画する
-
c
キーで画面クリア、Ctrl+C
で元のターミナル設定に戻して終了する