4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

マウスでターミナルにお絵描きをするシェルスクリプト

Last updated at Posted at 2025-04-26

1. 概要

このスクリプトは、ターミナル上で動かしたマウスに合わせて文字を出力します(お絵描きができます)

  • 左ボタンでドラッグ中は「*」を、右ボタンでドラッグすると「@」を出力します
  • cキーで画面をクリア、Ctrl+Cで終了
  • 画面の最下部にマウスの座標(行と列の位置)を表示します

image.png

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"

\033ESCの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 で元のターミナル設定に戻して終了する

6. 参考

4
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?