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

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 を有効化 (33[?1003h)
echo -ne "33[?1003h"

33ESCの8進表記です(ex1bと書いても同じ)

  • ボタンのデータ形式
# ESC+[+M+ボタン情報+X座標+Y座標]
# 合計6byteのバイナリ形式
x1b[Mbxy
  • ESC:0x1B (エスケープシーケンス開始)
  • [M:マウスレポート開始(固定)
  • :ボタンコード+32 (以降、元の値に戻すには-32をする必要がある)
  • :列番号+32
  • :行番号+32

ボタンコード

の値 意味
0 左ボタン押下
1 中ボタン押下
2 右ボタン押下
3 ボタン解放
32 左ボタン押下状態でマウス移動(ドラッグ)
34 右ボタン押下状態でマウス移動(ドラッグ)

列番号、行番号について

文字単位での位置を表しています(ドット単位の座標ではない)

# 例えば、10列(x)、20行(y)の位置で左クリックした場合、以下のデータを受け取る
# ボタン0 →  = 0+32 = 32(0x20)
# x=10 →  = 10+32 = 42(0x2A)
# y=20 →  = 20+32 = 52(0x34)
echo -ne "x1b[Mx20x2Ax34"

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 "33[?1003l"
  # stty 設定を復元
  stty "$orig_stty"
  echo -e "nマウスモードを解除しました。終了します。"
  exit
}

clear() {
  printf '33[2J33[H' # 画面をクリア&カーソルを左上に移動
  printf "左ボタンを押しながら移動すると '*'、右ボタンだと'@'を出力します。rn"
  printf "終了するには[Ctrl+C]、画面をクリアするには [c] キーを押してください。rn"
}

trap cleanup EXIT INT TERM

# ------------------------------------------------------------
# 1. Any-event tracking を有効化 (e[?1003h)
#    (マウスが移動しただけでもイベントを受け取るモード)
# ------------------------------------------------------------
echo -ne "33[?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 == $'33' ]]; 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 "33[31m33[%d;%dH*33[0m" "$y" "$x"
          ;;
        34) # 右ボタンでドラッグしている場合
          # (x, y) に 黄色で"@" を描画
          printf "33[1;33m33[%d;%dH@33[0m" "$y" "$x"
          ;;
      esac

      # ターミナル下部にマウスの位置情報を表示する
      printf "33[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 "33[?1003l"
  # stty 設定を復元
  stty "$orig_stty"
  echo -e "nマウスモードを解除しました。終了します。"
  exit
}

trap cleanup EXIT INT TERM で、プログラム終了時(EXIT)や、Ctrl+C(INT)時に端末の状態を元に戻します

4.3 clear() 関数

  • 33[2J で画面全体クリア、33[H でカーソルを左上へ移動してから、メッセージを出力します
clear() {
  printf '33[2J33[H' 
  printf "左ボタンを押しながら移動すると '*'、右ボタンだと'@'を出力します。rn"
  printf "終了するには[Ctrl+C]、画面をクリアするには [c] キーを押してください。rn"
}

5. まとめ

  • Any‑event tracking (033[?1003h) でマウス移動を含む全イベントを取得できるようにする
  • 受信したマウスイベント ESC [ M b x y を解析し、ボタンの状態 bと座標 (x,y) に分解する
  • 左ドラッグ時は赤「*」、右ドラッグ時は黄「@」をマウスの位置に描画する
  • cキーで画面クリア、Ctrl+C で元のターミナル設定に戻して終了する

6. 参考



フラッグシティパートナーズ海外不動産投資セミナー 【DMM FX】入金

Source link