Rustとtmux、時々Zellij。

2023-12-25 03:18

こんにちは。 修論の進捗がよくないです。 学会の論文も書かないといけません。 おわー。

この記事は LabBaseテックカレンダー Advent Calendar 2023 の7日めの記事です。 大遅刻すみません。 アドカレ最終日なので気合いで書いてます。 いい論文書くので許してください。

ちなみにLabBaseはインターン先です。 RustでWebが書ける面白い会社です。

ターミナルマルチプレキサ

さていきなりですが、みなさんはターミナルマルチプレキサ、使っていますか?

そもそもターミナルマルチプレキサって何?という人もいるかもしれません。 ターミナルマルチプレキサ(Terminal Multiplexer)とは、一つのターミナル内でターミナルを分割し、切り替えて使えるような環境を提供してくれるものをいいます1

広く利用されているtmuxというターミナルマルチプレキサでは、session、window、paneといった単位でターミナルを分割します。 sessionはwindowの集合、windowはpaneの集合で一つの画面となる、paneは一つの画面をさらに分割するもの、といった感じです。

ターミナルマルチプレキサの利用する利点は、第一に、ターミナルの機能に依存しなくてもターミナル画面を複数に分割して管理することができる、というのがあるでしょう。 つまり、あなたがWindows Terminalを使っていようが、Alacrittyを使っていようが、iTermを使っていようが、いつものキーバインドでいつものターミナル分割・管理ができます。

第二の利点として、セッションの維持機能も挙げられます。 なんらかの理由でターミナルが終了してしまったり、SSH接続が切れてしまっても、ターミナルマルチプレキサのセッションは生き続け、作業中のセッションが失われることはありません。 また、複数端末から同じセッションに繋いだり、ターミナルのログを遡ることができます。

私はSSH接続先にターミナルマルチプレキサをインストールして使うこともあります。 これにより、ターミナルの画面分割をする度にSSH接続をし直さなくても複数ターミナルが管理でき、SSH接続が急に切れても問題なくなります。

著名なターミナルマルチプレキサたち

さて、ではターミナルマルチプレキサにはどのようなものがあるでしょうか?

screen

以前広く使われていたターミナルマルチプレキサにはscreenというものがあります。 最近ではあまり使われていない印象があります。

tmux

これに続いて実質的にターミナルマルチプレキサの覇権を取ったのがtmuxというターミナルマルチプレキサです。

tmuxは非常に多機能で、動作も安定しており、ユーザも多いため、最初に使うのに良い選択肢だと思います。 大体のパッケージマネージャに入っているので、各パッケージマネージャでインストールするだけで使えることが多いのも魅力です。

ですが、tmuxにも厳しいポイントはあります。

まず、複雑なカスタマイズをしたければtmuxコマンドで設定を変更することで行うことが多く、いい感じのAPIなどは提供されていません。 設定ファイルはおそらく独自のもので、シェルスクリプトのような感じなので書くのに苦労はしませんが、多機能ではない印象です。 既存のプラグインやプラグインマネージャもシェルスクリプトで書かれていることが多いです。

また、 tmux/tmux を覗いてみると、実装言語のほとんどがC言語であることがわかります。 モダンな言語で開発されていないというのは、それだけで敬遠されてしまう原因かなと思います。

Zellij

新進気鋭のRust製ターミナルマルチプレキサに Zellij があります。 zellij-org/zellij を見てみると、pure Rustであることがわかります。

さらにPlugin APIが存在し、ZellijのAPIを利用したプラグインを書くこともできます。 プラグインは様々な言語で実装できるようにという理由で、wasmを読み込む形式で実装されています。 面白いですね。

インストールせず試しに実行することもできます。 インストールする場合は cargo install --locked zellij が推奨されており、Rustの環境がインストールされている必要があります。

Zellijの魅力にはデフォルトで綺麗なUIがあります。 tmuxでは様々な設定をしないと表示が綺麗にならないため、デフォルト状態で表示が綺麗というのは好感がもてます。

設定は KDL という言語で行われています。 慣れないと思いますが、表現力豊かでこちらも好印象です。

ですが、新しい故の問題もあります。

まず機能が少ない印象を受けます。

私もtmuxの機能をたくさん使っているわけではないですが、私はセッションの切り替え機能を多用しており、これが実装されていないZellijではセッション切り替えは一回セッションをデタッチしてから別セッションをアタッチする必要があります。 この機能についてはまさに今 実装が議論されている ところなので、これから追加されるとは思いますが、それまでは不便です。

また、tmuxの機能にペインのプレビューを表示しながらセッションを横断してペインの移動が行える機能があるのですが、これはZellijにはありません。 最初自分でプラグインを作ろうと思ったのですが、先述のようにセッションの切り替えコマンドや別セッションの情報取得APIがまだ存在しないので、このプラグインを作成するためにはまずこれらのAPIをZellijに実装する必要があり、結構時間がかかりそうです。

また、ところどころバグもあります。 私の方でも見つけたバグはIssueで報告するようにしていますが、そんなにすぐ解消されるわけではないので、気長に対応していく必要があります。 私の方でもバグ修正を試みていますが、関連コードを全部読むのは時間がかかるため、すぐにはできません。

私の選択

結局どのターミナルマルチプレキサを使うのが良いのか、私は悩んだ末に、tmuxを使うことにしました。

tmuxの設定・プラグイン開発

先述の通り、tmux向けプラグインの多くはシェルスクリプトでtmuxコマンドを実行することで機能を実現しています。 tmux-plugins/tpm などがわかりやすい例だと思います。

bashならほとんどの環境に入っているでしょうし、シェルスクリプトを書くのは理にかなっているように思います。 しかし、macOSデフォルトのbashはバージョンが古く、ビルトインのechoコマンドがUnicodeのコードポイントを指定しての文字出力に対応していませんでした。 私はtmuxの見た目のカスタマイズに Nerd Fonts を多用しているため、Unicodeのコードポイントで文字が出力できないと厳しいものがあります。

そもそもシェルスクリプトでプラグインを書く場合、シェルスクリプト中のコマンドの依存関係が全て満たされていなければなりません。

それ以外にもシェルスクリプトだとつらいポイントがいくつもあります。 つらい。 つらいです。

シェルスクリプトでのプラグイン開発・設定を諦めた私は、PythonやTypeScriptを候補に考えました。 Pythonにはtmuxを操作するためのライブラリが存在しました。 しかし、Pythonでライブラリを利用するには依存関係を満たすために各種インストールなどが必要になるので、手軽さが失われます。 TypeScriptは deno compile などを使うといい感じになりそう!と思いましたが、既存のtmuxの型定義などがありませんでした。 型定義がなくてもいいのですが(元々シェルスクリプトを考えていましたし)、やはりできるだけ型の恩恵を受けたいというのが正直なところです。 最初は自分でTypeScript向けのtmuxの型を書いていましたが、途中で心が折れてやめてしまいました。

Rustとtmux_interfaceクレート

色々考えた末に、最終的にRustで書いてみることにしました。 Rustであれば基本的にビルドしておけばその実行ファイルは使いまわせるでしょうし、それならGitHub Actionなどで自動ビルドを組んでおけばいい話です。

ではどのようにして記述していけば良いでしょうか? tmux_interface クレートは各バージョンのtmuxで使えるコマンドなどの型が定義されているRustのクレートです。 これを使えば、型の恩恵を受けながらtmuxの設定やプラグインが記述できそうです。

実際に設定は次のように書いていきます。

use tmux_interface::*;
type N = Cow<'static, str>;

fn main() {
    let opt = SetGlobalSessionOptions::new()
        .default_shell(None::<N>, Some(shell))
        .mouse(None::<N>, Some(Switch::On))
        .build();
}

うーん、ちょっと冗長ですね。

もっと複雑なことをやってみましょう。 tmuxのカスタマイズで最もやりがいがあるのは、ステータスバーの変更です。 ステータスバーにスタイル付きの文字列を表示するためのツールとして、次のようなものを作ってみました。

#[derive(Debug, Clone)]
pub enum StyledText<'a> {
    Styled(Styles<'a>, Vec<StyledText<'a>>),
    Raw(Cow<'a, str>),
}

impl fmt::Display for StyledText<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Styled(styles, styled_texts) => {
                let is_styled = if styles.len() == 0 { false } else { true };
                if is_styled {
                    write!(
                        f,
                        "#[{}]#[{}]#[{}]",
                        Style::PushDefault,
                        styles,
                        Style::PushDefault
                    )?;
                }
                for st in styled_texts {
                    write!(f, "{}", st)?;
                }
                if is_styled {
                    write!(
                        f,
                        "#[{}]#[{}]#[{}]",
                        Style::PopDefault,
                        STYLE_DEFAULT,
                        Style::PopDefault
                    )?;
                }
                Ok(())
            }
            Self::Raw(raw) => {
                write!(f, "{}", raw)
            }
        }
    }
}

このような仕組みを用意することで、ステータスバーに表示する文字列に対してスタイルを指定するだけで、スタイルの入れ子などにも対応した表示を行うことができます。 これはtmuxの push-defaultpop-default の挙動を利用することで実現しています。 これで簡単にステータスバーに表示する文字列へのスタイル付けなどができるようになりました。

で、結局どういう感じになりました?

すみません、結局設定は気合いで全部シェルスクリプトで書きました……

1

ターミナルマルチプレキサの厳密な定義を調べましたがあまりいい資料がなく、一般に使われている用語というだけのような印象を受けました。ここでは私の主観でいい感じの表現を探して記述していますが、ターミナルマルチプレキサの厳密な定義というわけではない点をご承知おきください。