Tmuxinatorの代替ツールをDenoで作った

Tmuxinator の代替CLIツール qtmut *1 を作りました。

github.com

1. これはなに

Tmuxのセッションを新規に作成する際の、windowやpaneの初期設定をYAML形式のファイルで記述できるツールです。

2. 使い方

セッションを開始するディレクトリ (-c オプションで指定されたディレクトリ、未指定の場合はカレントディレクトリ) に以下のような .qtmut.yaml ファイルを作成すると、そのファイルに記述されたようにTmuxのwindowやpaneが作成されます。

# .qtmut.yaml
active:
  window: main
  pane: 1

windows:
  - name: main
    layout: even-horizontal
    panes:
      - command:
      - command: vim
  - layout: tile
    panes:
      - command: bundle exec rails s
      - command: tail -f logs/development.log

作成されたTmuxのセッション

3. なぜ自作したか

ツールを自作したモチベーション、およびTmuxinatorなどの既存ツールとの差異について説明するために、私の普段のTmuxの利用方法について以下で述べます。

3.1. 普段のTmuxの利用方法

私は普段以下のようなZLEウィジェットキーバインドを用いて、GitリポジトリごとにTmuxセッションを立ち上げ、または切り替えています。

select-ghq-session() {
    local root="$(ghq root)"
    local selected="$(ghq list | sort | fzf --exit-0 --preview="fzf-preview-git ${(q)root}/{}" --preview-window="right:60%")"

    if [ -z "$selected" ]; then
        return
    fi

    local repo_dir="$(ghq list --exact --full-path "$selected")"
    local session_name="$(sed -E 's/[:. ]/-/g' <<<"$selected")"

    if [ -z "$TMUX" ]; then
        BUFFER="tmux new-session -A -s ${(q)session_name} -c ${(q)repo_dir}"
        zle accept-line
    elif [ "$(tmux display-message -p "#S")" = "$session_name" ] && [ "$PWD" != "$repo_dir" ]; then
        BUFFER="cd ${(q)repo_dir}"
        zle accept-line
    else
        tmux new-session -d -s "$session_name" -c "$repo_dir" 2>/dev/null
        tmux switch-client -t "$session_name"
    fi
    zle -R -c # refresh screen
}

zle -N select-ghq-session

bindkey "^G" select-ghq-session # C-g

このZLEウィジェットは、ghq で管理されたGitリポジトリFZF を用いて選択し、選択されたリポジトリに対応するTmuxセッションにアタッチします (セッションが存在しない場合は新規に立ち上げられます)。

3.2. Tmuxinator の導入

ここで、「特定のリポジトリを開いたときはペインの分割やコマンドの実行を自動的に行いたい」という要求が発生しました。 その種の、Tmuxセッションの立ち上げを設定ファイル化するための最も有名なツールに Tmuxinator があります。

通常 Tmuxinator はセッションのコンフィグを特定のディレクトリ ($XDG_CONFIG_HOME/tmuxinator/ または ~/.tmuxinator/) に保存する必要があります。 しかしながら、私の場合コンフィグを複数のプロジェクト間や異なるPC間で共有するようなことは全く無いため、.envrc などと同じようにプロジェクトのディレクトリ内に配置したいと考えました。

そこで、<プロジェクトディレクトリ>/.tmuxinator.yml が存在する場合、それを用いてTmuxinatorを実行するように上記のZLEウィジェットを変更しました。

 select-ghq-session() {
     local root="$(ghq root)"
     local selected="$(ghq list | sort | fzf --exit-0 --preview="fzf-preview-git ${(q)root}/{}" --preview-window="right:60%")"

     if [ -z "$selected" ]; then
         return
     fi

     local repo_dir="$(ghq list --exact --full-path "$selected")"
     local session_name="$(sed -E 's/[:. ]/-/g' <<<"$selected")"
+    local tmuxinator_config="$repo_dir/.tmuxinator.yml"

     if [ -z "$TMUX" ]; then
-        BUFFER="tmux new-session -A -s ${(q)session_name} -c ${(q)repo_dir}"
+        if [ -f "$tmuxinator_config" ]; then
+            BUFFER="tmuxinator start -a -n ${(q)session_name} -p ${(q)tmuxinator_config}"
+        else
+            BUFFER="tmux new-session -A -s ${(q)session_name} -c ${(q)repo_dir}"
+        fi
         zle accept-line
-    elif [ "$(tmux display-message -p "#S")" = "$session_name" ] && [ "$PWD" != "$repo_dir" ]; then
+    elif [ "$(tmux display-message -p "#S")" != "$session_name" ]; then
+        if [ -f "$tmuxinator_config" ]; then
+            tmuxinator start -a -n "$session_name" -p "$tmuxinator_config"
+        else
+            tmux new-session -d -s "$session_name" -c "$repo_dir" 2>/dev/null
+            tmux switch-client -t "$session_name"
+        fi
+    elif [ "$PWD" != "$repo_dir" ]; then
         BUFFER="cd ${(q)repo_dir}"
         zle accept-line
-    else
-        tmux new-session -d -s "$session_name" -c "$repo_dir" 2>/dev/null
-        tmux switch-client -t "$session_name"
     fi
     zle -R -c # refresh screen
 }

これは概ね期待通りの動作をしてくれましたが、一方でTmuxinatorの想定している用途からやや離れるために、コンフィグファイル (.tmuxinator.yml) の設定に若干の冗長さがある、ZLEウィジェットの条件分岐が複雑になるなどの不満点がありました。 また、個人的に gem install を可能な限りしたくありません。

そこで、以下の記事を参考に簡単なTmuxinatorの代替ツールを作成することにしました。

pocke.hatenablog.com

3.3. Tmuxinator と qtmut の思想の違い

Tmuxinator が単一の設定ディレクトリ内に存在する複数の設定ファイルからTmuxセッションを立ち上げるのに対し、qtmut はカレントディレクトリ (または -c オプションを用いて指定される作業ディレクトリ) に設置された .qtmut.yaml ファイルを用いてTmuxセッションを作成します。 つまり、私のニーズによく適合するように設計されています。

また、設定ファイルの構造もTmuxinatorに比べ自然に感じるように構成しました。

また、カレントディレクトリ (あるいは指定された作業ディレクトリ) に .qtmut.yaml が存在しない場合は単にTmuxセッションの立ち上げのみを行うため、ZLEウィジェットも幾分か簡略化されました。

 select-ghq-session() {
     local root="$(ghq root)"
     local selected="$(ghq list | sort | fzf --exit-0 --preview="fzf-preview-git ${(q)root}/{}" --preview-window="right:60%")"
     if [ -z "$selected" ]; then
         return
     fi
 
     local repo_dir="$(ghq list --exact --full-path "$selected")"
     local session_name="$(sed -E 's/[:. ]/-/g' <<<"$selected")"
-    local tmuxinator_config="$repo_dir/.tmuxinator.yml"
 
     if [ -z "$TMUX" ]; then
-        if [ -f "$tmuxinator_config" ]; then
-            BUFFER="tmuxinator start -a -n ${(q)session_name} -p ${(q)tmuxinator_config}"
-        else
-            BUFFER="tmux new-session -A -s ${(q)session_name} -c ${(q)repo_dir}"
-        fi
+        BUFFER="qtmut -s ${(q)session_name} -c ${(q)repo_dir}"
         zle accept-line
     elif [ "$(tmux display-message -p "#S")" != "$session_name" ]; then
-        if [ -f "$tmuxinator_config" ]; then
-            tmuxinator start -a -n "$session_name" -p "$tmuxinator_config"
-        else
-            tmux new-session -d -s "$session_name" -c "$repo_dir" 2>/dev/null
-            tmux switch-client -t "$session_name"
-        fi
+        qtmut -s "$session_name" -c "$repo_dir"
     elif [ "$PWD" != "$repo_dir" ]; then
         BUFFER="cd ${(q)repo_dir}"
         zle accept-line
     fi
     zle -R -c # refresh screen
 }

2. なぜDenoを選択したか

qtmut は Deno を用いて作成しました。

特にTypeScriptはシェルスクリプトに比べ、YAMLなどの構造化されたデータ構造を扱うのが容易です。 Denoは処理系のインストールが簡単 (deno の実行可能ファイルをPATHの通ったディレクトリに設置するだけ) で依存関係も自動的に解決されるため、小さなツールであればシェルスクリプト代わりに書いていくのもありかなと思いDenoを選択しました。

3. まとめ

Denoを用い、Tmuxinatorとは若干思想を異にする小さな代替ツール qtmut を作成しました。

Rustを使う必要があるほど速度の要求されない用途であれば、ZSHプラグイン周りでも今後はDenoが選択肢に入ってくるのではないでしょうか?

*1:qtμt というマンガから取りました。https://manga.line.me/product/periodic?id=Z0000153