マウスでターミナルにお絵描きをし てみよう ~ ちょっとだけターミナルの仕組みも ~ 2025/06/09 俺たちの勉強会 #2 #orestudy
自己紹介:murasuke ・株式会社 ツールラボ 開発部 所属 PHPで自社Webサービスの開発をやっています 開発環境のモダン化(React+Tailwind)が最近の楽しみです ・20年ほどSEやってましたが、プログラムを書く仕事が少なくなり(年齢的に?) さみしくなってきたので・・・ ・今日が初LTです、よろしくお願いします ・今週末、社員旅行の自由時間に、(吞みながら)勝手にLT大会やります!
なぜこんなことをやってみようと思ったのか? 「Windows TerminalがSixel画像表示をサポート」でターミナルは、 文字だけではなく、画像も表示できることを知りました ターミナルで可能な機能 ● ● ● ● 文字の装飾(太字、文字の色の指定、二重下線をひく) 画像の表示(正確にはピクセル単位の出力) 任意の位置への文字出力 マウスイベントの通知など このなかで「マウスイベントを受け取る」、「任意の位置への文字出力」 に着目し、お絵描きプログラム作れたら面白いじゃん!と思った次第です
ちょっと横道① ターミナルエミュレーターの歴史 テレタイプ端末: タイプライターに電話回線を つなげたような機械 キー入力すると即時で相手側にも 印字される コンピューターの入出力にも使わ れるようになる Jamie - Flickr: Telex machine TTY https://commons.wikimedia.org/w/index.php?curid=19282428
ちょっと横道① ターミナルエミュレーターの歴史 ビデオ端末 (VT100など): ディスプレイに文字を表示するようになった端末 ・端末はキーを押しても即時で 画面には表示されない。 接続先のコンピューターから エコーバックを受けて その文字を表示している ・エスケープシーケンスによる 表示の制御、文字装飾も できるようになった
ちょっと横道① ターミナルエミュレーターの歴史 ビデオ端末 (VT100など): ビデオ端末はアプリと直接接続されません。カーネル内にある、 ラインディシプリンが中継し、表示や行単位の編集を受け持っています 行編集の例:「123」を「124」と間違えたので、1文字消して「3」を入力すると ラインディシプリンから「124(BS)(Space)(BS)3」とエコーバックされます ⇒ 「カーソルを戻す(BS)」+「空白で上書き」+「カーソルを戻す」という 3つのアクションが行われることで画面から1文字消去する仕組みになっています
ちょっと横道② ターミナルエミュレーターとTTY(PTY) ターミナルエミュレーター: VT100などの振る舞いをソフトウェアで再現したもの カーネル(ラインディシプリン)は簡易的な (行単位の)編集しか行えません viやシェルといった高機能なアプリはRawモード に切り替え、アプリ自体でエコーバックを行うこと で高度な編集が可能になります お絵かきシェルスクリプトでは、マウスのイベントを随時処理するため、 Rawモードを使います
ここまでのまとめ ● ターミナルは、キー押下を接続先に送信し「エコー」を表示している (自身で「キー入力」を表示しているわけではない) ● マウスもイベントとしてアプリへ送信される (要:エスケープシーケンスでモード切替え) ● ラインディシプリンがバッファリング(行単位の編集)を行っている。 アプリ側で制御するにはRawモードに変更する必要がある
お絵描きシェルスクリプト全体の流れ ● 初期設定(Rawモードへの切替、マウスイベントの有効化) ● メインループ: イベントの受信、ボタンイベントの判断、描画 a. イベントのデータ解析: 座標・ボタン判定 b. 描画: エスケープシーケンスで任意の位置に文字/色を出力 c. ターミナル下部にマウスの位置情報を表示する
1. Rawモードへの切替、マウスイベントの有効化 マウスイベント有効にするため、エスケープシーケンスを送ります ※ `\033`は`ESC`の8進表記です(`\e`や`\x1b`と書いても同じ) stty raw -echo # ターミナルrawモード & echoオフ echo -ne "\033[?1003h" # ボタン状態に関係なく、マウスが動くたびにイベントを送信 echo -ne "\033[?1006h" # SGR形式のイベント通知(座標)有効化
2.メインループ: イベントの受信、ボタンイベントの判断
イベントを読み込み(read)、マウスイベント以外を無視
● ESC:0x1B (エスケープシーケンス開始)
● [<:マウスレポート開始(固定) ・・・この場合だけ次の処理へ
while true; do
IFS= read -rsn1 char
# ESC(0x1B,033) の場合、マウスイベントかどうかをチェック
if [[ $char == $'\033' ]]; then
# 続く2文字を読み取る
IFS= read -rsn2 seq
if [[ $seq == "[<" ]]; then
# マウスイベントを処理する
ターミナルからのイベントを受信(読み取り) 入力されたデータを読み取り、変数(char)にセットします IFS= read -rsn1 char # 押されたキーを表示 echo $char IFS= フィールド区切り文字(Internal Field Separator)を空にして、読み取り時に分割されるのを防ぐ read 入力を読み取って変数に代入するBashのビルトインコマンド -r バックスラッシュ(\)を特別扱いせず、そのまま読み取る -s 入力をエコーしない(画面に表示されないようにする) -n1 1文字だけ読み取る。n1 の代わりに n3 にすれば3文字読み取りになる char 読み取った文字を代入する変数名
a.イベントのデータ解析: 座標・ボタン判定
ボタンコード(b)、座標(x,y)、ボタンの押下状況(M) を変数にセットします
⇒イベントのデータ形式(ESC[<b;x;yM)は次ページで
# マウスイベントデータを読み出す
IFS= read -rsn15 mouse_data
# 最期の(M|m)を取り除く
mouse_data=${mouse_data%%[Mm]*}
# ; で分割して 変数 b, x, y にセット
IFS=';' read -r b x y <<< "$mouse_data"
マウスイベントデータ形式 # ESC [ < b ; x ; y M \x1b[<b;x;yM ● ● ● ● ● ● カンマ(;)で分割して、値を取り出します ESC:0x1B (エスケープシーケンス開始) [<:マウスレポート開始(固定) b:ボタンコード x:列番号 y:行番号 M :M ボタンを押下、m ボタンをリリース b: ボタンコード一覧 0: 左ボタン押下 2: 右ボタン押下 32: 左ボタンドラッグ中 34: 右ボタンドラッグ中 # 例えば、10列(x)、20行(y)の位置で左(0)クリック(M)した場合、以下のデータを受け取る # ボタン0 → <b> = 0 # x=10 → <x> = 10 # y=20 → <y> = 20 # ボタン押下 → M echo -ne "\x1b[<0;10;20M"
b.描画: エスケープシーケンスで任意の位置に文字/色を出力 マウスの位置に文字を出力するため、エスケープシーケンス(\033[行;列H)を使います # ボタン判定 case $b in 0 | 32) # 左ボタンクリック、ドラッグしている場合 # (x, y) に 赤色で"*" を描画 printf "\033[31m\033[%d;%dH*\033[0m" "$y" "$x" ;; 2 | 34) # 右ボタンクリック、ドラッグしている場合 # (x, y) に 黄色で"@" を描画 printf "\033[1;33m\033[%d;%dH@\033[0m" "$y" "$x" ;; esac
c.ターミナル下部にマウスの位置情報を表示する 画面の左下にマウスの情報(座標)を表示します # ターミナル下部にマウスの位置情報を表示する printf "\033[999;1H" # 画面の下へ移動 echo -n "button=$b, x=$x, y=$y "
デモ/結果イメージ 時間があれば実際に お絵描きしてみます
まとめ 今後やってみたいこと ● 文字単位の座標ではなく、ピクセル単位でも座標が取得できる(環境依存) ● 文字単位ではなく、ピクセル単位で描画もできる ⇒ もっと精密な絵を描くことができるので、いつかやりたい ご清聴ありがとうございました