Zellij ターミナルワークスペースと WebAssembly プラグイン

自分は Linux をセットアップして最初に行う設定が tmux 関連だったりするのですが、新しい Windows 機の WSL2 にも秘伝の .tmux.conf を入れつつ、そうだ Zellij を試してみようと思い立ち、良い感じだったので紹介です。

Zellij

A terminal workspace with batteries included

Zellij (ぜりーじゅと読むそうです)は、いわゆるターミナルマルチプレクサーに種別されるソフトウェアで、セッション管理や、ターミナルエミュレータ上での画面分割やタブ制御を行うことができます。

Windows Terminal WSL2 上で動作している Zellij。

tmux よりも新しく作られていることもあり、デフォルトで自動起動スクリプトの出力やシステムクリップボード連携設定などが備わっており、また操作系も画面下部に表示することができるので非常に導入しやすいつくりとなっています。

導入

絶賛開発中 (記事執筆時点 0.36.0) の Rust でできたソフトウェアにて、導入は cargo install を使ってソースからビルドする方法が良いと思います。cargo なのでコマンド一発です。

Zellij User Guide – Installation

The easiest way to install Zellij is through a package for your OS.

If one is not available for your OS, you can download a prebuilt binary or even try Zellij without installing.

Otherwise, you can install it with Cargo.

(cargo を含む)Rust のツールチェインが未導入の場合は rustup で導入します。

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

導入後 PATH などが設定されますので、いったんシェルを落とし上げします。

cargo install で zellij を導入。

cargo install --locked zellij
$ ls -laF ~/.cargo/bin/ | grep zel
-rwxr-xr-x  1 hiromasa hiromasa 17380208  4月 20 17:31 zellij*

なお、cargo install で導入したバイナリーのアップデートは cargo-install-update を使うことで行うことができます。

# cargo-update の導入 (初回のみ)
cargo install cargo-install-update
# アップデート確認
cargo install-update -l
    Polling registry 'https://index.crates.io/'.....

Package         Installed  Latest   Needs update
espup           v0.2.9     v0.4.0   Yes
cargo-generate  v0.18.2    v0.18.2  No
cargo-update    v13.0.1    v13.0.1  No
powerline-rs    v0.2.0     v0.2.0   No
zellij          v0.36.0    v0.36.0  No
# Needs update が Yes なものをアップデート
cargo install-update -a

フォント

Zellij は標準で powerline フォントに対応しています。お使いのターミナルエミュレータのフォントに設定しましょう。自分は HackGen を使わせていただいています。(とてもきれいです…!)

https://github.com/yuru7/HackGen

白源 (はくげん/HackGen) は、プログラミング向け英文フォント Hack と、源ノ角ゴシックの派生フォント源柔ゴシックを合成したプログラミングフォントです。

設定フォントは「源ノ角ゴシックの派生フォント源柔ゴシックに、Powerline フォントを含む Nerd Fonts を合成した」 HackGen_NF_*.zipHackGenConsoleNF-Regular.ttfHackGen Console NF)を設定すると良いと思います。

なお、Windows Terminal だと Ricty 系の powerline フォントの罫線がずれるようです。

自動起動設定

zellij 起動確認後は、シェル設定ファイルに Zellij の自動起動設定を入れておくと便利です。

Zellij User Guide – Autostart on shell creation

Autostart a new zellij shell, if not already inside one. Shell dependent, fish:

自分は .bashrc ですので次のようになります。

echo 'eval "$(zellij setup --generate-auto-start bash)"' >> ~/.bashrc

ちなみに zellij setup --generate-auto-start bash の中身は次のようになっています。tmux だと自分で書かなければいけないので小粋な感じでぐー。

if [[ -z "$ZELLIJ" ]]; then
    if [[ "$ZELLIJ_AUTO_ATTACH" == "true" ]]; then
        zellij attach -c
    else
        zellij
    fi

    if [[ "$ZELLIJ_AUTO_EXIT" == "true" ]]; then
        exit
    fi
fi

というわけで、お好みで ZELLIJ_AUTO_ATTACH を設定すると、Windows Teminal などがダウンしたときにや誤ってタブを落とした時に自動復帰できて良いかと思います。

`.bashrc`

ZELLIJ_AUTO_ATTACH=true
eval "$(zellij setup --generate-auto-start bash)"

関連して Zellij の default key binding に Ctrl + s, s がバッファのサーチに割り当てられていて、自分はうっかりロックを忘れて tty に START/STOP を送ってしまうことがあったので、以下も一緒に入れて tty の制御コード受付を disable にしています。

if [[ -t 0 && $- = *i* ]]
then
    stty -ixon
fi

設定

Zellij はシングルバイナリーで動作するようになっていますが、設定ファイルを使うこともできます。デフォルト設定を最初に出力すると良いかと思います。

Zellij User Guide – Configuration

Zellij uses KDL as its configuration language.

mkdir ~/.config/zellij
# 設定ファイル
zellij setup --dump-config > ~/.config/zellij/config.kdl

自分のデフォルト設定から変えている定義は次の通りです。ほとんど変えてないですねw

// ペインのフレームをボーダレスに
pane_frames false
// 起動時の操作モードをインターフェース locked に
default_mode "locked"
// 配色テーマ
themes {
    tokyo-night-dark {
        fg 169 177 214
        bg 26 27 38
        black 56 62 90
        red 249 51 87
        green 158 206 106
        yellow 224 175 104
        blue 122 162 247
        magenta 187 154 247
        cyan 42 195 222
        white 192 202 245
        orange 255 158 100
    }
}
theme "tokyo-night-dark"

テーマは次のドキュメントからいただいてきたものです。

Zellij User Guide – Themes

Themes can be specified either in the configuration file under the themes section, or directly in a separate file.

レイアウト

Zellij の肝のひとつですが、レイアウトファイルを使うことによりペインに好きなパーツを配置することができます。

mkdir ~/.config/zellij/layouts
# デフォルト画面レイアウトファイル
zellij setup --dump-layout default > ~/.config/zellij/layouts/default.kdl

pain の一番目に後述する自分の WebAssembly プラグインを追加していますが、ファイルの内容は次のようになります。

layout {
    pane size=1 borderless=true {
        plugin location="file:/home/hiromasa/.config/zellij/plugins/zellij-datetime.wasm"
    }
    pane size=1 borderless=true {
        plugin location="zellij:tab-bar"
    }
    pane
    pane size=2 borderless=true {
        plugin location="zellij:status-bar"
    }
}

この定義が起動時の画面レイアウトと対応する形のつくりになっています。タブバーや下部のステータスバーがデフォルトプラグインとして実装されているのが分かります。

レイアウトファイルの記述方法は以下のドキュメントが参考になります。

Zellij User Guide – Layouts

Layouts are text files that define an arrangement of Zellij panes and tabs.

You can read more about creating a layout

また Zellij 起動時の引数でレイアウトファイルを指定できるため、作業によってワークスペースを切り替えるようなことも可能です。

シングルボードコンピュータへの ssh 接続を行い、各種リソースを表示するようなレイアウトを作成した例。(後述していますがこの例で使っている RISC-V Linux な SBC では直接 Zellij が動作しないため、ホスト側から ssh で接続しています。RPi 4A など Arm 機ではリモート側で Zellij を起動できるでしょう)

layout {
    tab name="LPi4A" focus=true {
        pane split_direction="vertical" {
            pane size="60%" close_on_exit=true {
                command "sshpass"
                args "-p" "licheepi" "ssh" "sipeed@172.16.15.201"
            }
            pane size="40%" {
                pane size="25%" close_on_exit=true {
                    command "sshpass"
                    args "-p" "licheepi" "ssh" "sipeed@172.16.15.201" "-t" "watch" "-n5" "df" "-k"
                }
                pane size="16%" close_on_exit=true {
                    command "sshpass"
                    args "-p" "licheepi" "ssh" "sipeed@172.16.15.201" "-t" "watch" "sudo" "cat" "/sys/devices/system/cpu/cpu*/cpufreq/cpuinfo_cur_freq"
                }
                pane size="16%" close_on_exit=true {
                    command "sshpass"
                    args "-p" "licheepi" "ssh" "sipeed@172.16.15.201" "-t" "watch" "sensors"
                }
                pane size="43%" close_on_exit=true {
                    command "sshpass"
                    args "-p" "licheepi" "ssh" "sipeed@172.16.15.201" "-t" "htop"
                }
            }
        }
    }
    tab name="Host"
    default_tab_template {
        pane size=1 borderless=true {
            plugin location="zellij:tab-bar"
        }
        children
        pane size=2 borderless=true {
            plugin location="zellij:status-bar"
        }
    }
}

Ubuntu/Alactirry 上の動作:

Windows Terminal/WSL2 Ubuntu で動作させている様子(WSL2 Ubuntu 上の zellij-server が接続を維持してくれるのでターミナルを落としてしまっても復帰できる例):

ペイン内の操作シンク

クラスタ構成の同一構成複数サーバとかで同じコマンドを入力したい時につかうあれも OK です。tab 操作から sync を選択することで、ペイン全てに同じ操作ができます。

WebAssembly プラグイン拡張

Zellij プラグインは WebAssembly/WASI で作成することができます。勉強がてら 日時 date-time を表示するプラグインを Rust でかいてみました。

https://github.com/h1romas4/zellij-datetime

This plugin adds a date and time pane to [Zellij](https://zellij.dev/), a terminal multiplexer.

amd64 Ubuntu/Alacritty で動作している様子。

なお、システムインターフェースは WASI API の範囲となります。とりあえずまだ TIMEZONE がとれない気がした(たぶん)ので +9 JST 固定でビルドしました。

WASI API 超えをしたい場合のアイディアメモ:(ホスト側で情報を取得するシェルスクリプトを動かして、結果をファイルかソケットでプロキシしていると思います)

https://github.com/yvt/zellij-cpulamp

Minimal CPU activity indicator plugin for Zellij

ファイルシステムへのセキュリティーポリシーなどがどうなっているかなど継続調査。ちなみに、WebAssembly ランタイムは Wasmer (執筆時点 2.3 系) が使われています。

Wasm を使っているため .wasm ファイルは amd64 や Arm といった CPU の違いを気にせず同一バイナリーが使えますので便利です。 .wasm を配置してレイアウトファイルで読み込ませれば動作を開始します。(Wasmer は 3.2 から RISC-V 対応なので残念ながら現時点では RISC-V 機では動作しません)

今回は Rust を使いましたが、言語を選ばずプラグインがかけるのも良いです。

ただ、まだ Rust と Zig 言語以外にはバインディングがなく、そのほかの言語では、引数をもらうのに WASI の stdin から JSON をパースする処理をかく必要がありそうです。Rust のバインディングも内部的にはこの構成になっています。この辺は、WebAssembly の Component Model (Interface Types) の仕様ができれば改善されることでしょう。


今回 Zellij のプラグインをかいていたら、昔よく VZ Editor のマクロをかいていたことを思い出しました。。Zellij 拡張も テトリスやスネークゲームなどもつくれそうな雰囲気です。

これだけ拡張性が高いと心強いですね。良ければお試しください。


関連

今年 WebAssembly でつくった3つのアプリ

WebAssembly Advent Calendar 2019 の 11日目の記事です。

WebAssembly の登場で C/C++/Rust など JavaScript 以外の言語のエコシステムをウェブブラウザーに持ち込むことができるようになり嬉しいな〜ということで、どのくらい動くのかという検証もかねて、3つほどアプリをつくって動作させてみました。

Sapporo.CSS (SaCSS) vol 110 LT 資料(2020/01/25 追加)

Emscripten 編

C言語でかかれたゲーム機メガドライブのエミュレーター Genesis-Plus-GX に WebAssembly 用のインターフェースを追加し、Emscripten でコンパイルして動作させてみました。

かなり重めのサウンドコアエミュレーションを有効にしてコンパイルしているのですが、iOS Safari を含め非常に高速に動作しています。(ベアナックル2が最後までプレイできました 🙂

主に Firefox と iOS Safari で確認しています。 Firefox は7段キーボード世代の ThinkPad(いつのだ…)で動作させていますが 60 fps 維持できています。

ソースコードを以下で公開しています。cmake/make と webpack でビルドできるようになっています。

Genesis-Plus-GX WebAssembly porting

https://github.com/h1romas4/wasm-genplus

ROM を吸い出す環境がない方は、homebrew の ROM が動作するかもしれません。.wasm はコンパイル済みのバイナリをコミットしていますので node だけあれば遊べると思います。

ゲームパッドのアサインは手持ちの XBOX ONE 用になっていますので適当に修正ください。。ちなみに、iOS 13 から PS4/XBOX ONE コントローラーサポートが入りましたが、Safari の GamePad API からも接続できました。

Emscripten 環境で少し詰まったのは次のポイントでした。

Emscripten を webpack からモジュールとして import する方法:

リンカオプションで MODULARIZE=1 を指定。

add_compile_flags(LD
    "-s MODULARIZE=1"
)

JavaScirpt で import して module を取得

import wasm from './genplus.js';

wasm().then(function(module) {
    gens = module;
});

Wasm 側で malloc したメモリーポインタの取得する方法:

モジュールの module.HEAPU8.buffer など HEAP* ビューで取得。

wasm().then(function(module) {
    gens = module;
    // memory allocate
    gens._init();
    // load rom
    fetch(ROM_PATH).then(response => response.arrayBuffer())
    .then(bytes => {
        // create buffer from wasm
        romdata = new Uint8Array(gens.HEAPU8.buffer, gens._get_rom_buffer_ref(bytes.byteLength), bytes.byteLength);
        romdata.set(new Uint8Array(bytes));
        message("TOUCH HERE!");
        initialized = true;
    });
});

大きめの static のアロケートに失敗した場合:

リンカオプションで初期メモリーサイズを指定。

add_compile_flags(LD
    "-s ALLOW_MEMORY_GROWTH=1"
    "-s TOTAL_MEMORY=32MB"
)

Rust / wasm-pack 編 (1)

Rust/wasm-pack で最初につくったアプリです。

Wasm 側でアロケートしたメモリーを仮想 VRAM として、Rust で何も考えずにむちゃ描きしたらどれくらいの速度になるだろうということで試したものになります。

デモサイトから実際に動作するところが見れます。

https://github.com/h1romas4/wasm-canvas-bitblt

sin/cos で画像回転させながらラスタースクロール的な動きをさせていますが、思うままにプログラムをかいているため RGBA の 4Byte 転送を全画素で何度も回していたりします。

ちょっと興味があったのが、速くなるかなと Rust の unsafe のブロック転送を使い、

unsafe {
    ptr::copy_nonoverlapping(
        [color.0, color.1, color.2, 0xff].as_ptr(),
        self.vram.as_mut_ptr().offset(pos),
        4,
    );
}

のようにしてみたのですが、これは Wasm 的には単純なループで展開されてコンパイルされていました。これは今後 Bulk memory operations が入りコンパイラが対応することで改善するかもしれません。

WebAssembly/bulk-memory-operations

Some people have mentioned that memcpy and memmove functions are hot when profiling some WebAssembly benchmarks.

なお、このプログラムは前述の古い ThinkPad T420s では 45fps そこそこでしたが、iPhone X では余裕で 60fps でていました。速い。。

Rust / wasm-pack 編 (2)

最初の Emscripten メガドライブエミュレーターから、ゲーム機の音源部分(FM音源・PSG)を取り出しプログラムを C言語から Rust に移植したものです。

エミュレーターから音源 LSI に発行するコマンドを横取りして保存した、ゲームミュージックを楽しむ VGM という形式のファイルがありますが、それを再生するプレイヤーになっています。

YM2612/SN76489 VGM player by Rust

こちらもデモサイトから動作を見ることができます。自分がつくったサンプル VGM をひとつ入れています。しょぼいですがクリックで鳴ります。本当はもっとすごい楽曲が再生できます。。

.vgm を準備できる方はドラッグアンドドロップしてみてください。(なお全て WebAssembly で処理してますので、サーバーにファイルアップロードはされません。安心してお試しください)

ソースは以下から参照できます。

https://github.com/h1romas4/rust-synth-emulation

デバッグ手法:

プロジェクトを pure Rust 部分と、Wasm 部分に分けて構成しています。現在 WebAssemby のデバッグ環境はまだ整っていませんので、複雑な処理はネイティブで実行できる環境で行うと良さそうです。

Wasm のデバッグ環境については Chrome が DRAWF に対応しつつあるとのことで(まだステップ実行のみ)、今後整っていくのではないかと思います。

Improved WebAssembly debugging in Chrome DevTools

As a first step, DevTools now supports native source mapping using this information, so you can start debugging Wasm modules produced by any of these compilers without resorting to the disassembled format or having to use any custom scripts.

ライブラリの活用:

本プレイヤーアプリですが、.vgz と呼ばれる .gz 圧縮された .vgm ファイルの再生にも対応させています。

WebAssembly/Rust は stdlib でコンパイルできますので、pure Rust の Gzip, and Zlib ライブラリーである flate2 を dependencies に追加してコンパイルして、ファイル展開させてみたところ問題なく動作しました。

[dependencies]
flate2 = "1.0"

この辺は各言語のエコシステムを活用できる Wasm の強みだなと感じます。

Runtime Error: Index out of bounds.:

移植中 Rust のオブジェクトを JavaScript から new した際に、Index out of bounds. が発生してオブジェクトがつくられない事象が発生しました。ぱっと原因が分からなかったため、ソースを削る方向で試していくと、[0; 50000] ほどの配列の初期化の部分で発生していました。

Make stack size configurable

Currently the stack-size for local variables of the generated wasm code is preconfigured to be 1048576 bytes. It is easy to reach this limit,

どうやら stack-size の初期値が小さいということで、.cargo/config に次の記述をして回避しています。

[target.wasm32-unknown-unknown]
rustflags = [
  "-C", "link-args=-z stack-size=32000000",
]

WebAssembly 登場にてウェブブラウザーで好きな言語で、好きなプログラムを動かせるようになって嬉しいです。

今後も継続してウォッチしていきたいと思います。

関連記事

Visual Studio Live Share の活用

Visual Studio Code Advent Calendar 2018 の 16日目です。 🙂

今年もいろいろな分野で活躍してくれた VS Code でしたが、拡張として提供されている Visual Studio Live Share にもずいぶん助けられました。

Visual Studio Live Share は自分の Visual Studio から他の Visual Studio に接続してコード編集することを基本とするコラボレーション用の拡張です。ソースコード以外にも相手ホストのいろいろな機能に接続することができます。

  • ランゲージサーバーへの接続(相手の環境を使ってコード補完ができる)
  • プロジェクトファイル全体の grep 検索
  • 任意のポートにブラウザで接続
  • ターミナルに接続
  • デバッグ実行

自分の環境にソースコードや実行環境を持っていない状態でも、相手の環境を使って簡単にプログラミングを開始することが可能で、相手の環境で発生している不具合を手元で再現させながら修正するといったことが数分の準備で簡単に行えます。

接続はインターネット上に配置された Micsoroft の Live Share サーバーを介して行いますので、残念ながらクローズドな LAN 環境では使うことができませんが、代わりにルーターのポート開放など難しい設定なしに、どんな場所にいても機能を使うことが出来ます。

またクロスプラットフォームにも対応していますので、Windows から macOS に接続して動作させるようなことも可能です。ということで、いくつかの例を紹介してみたいと思います。

ランゲージサーバーとコンソール接続

Rust を編集している Linux をホストに Windows ゲストから接続して、Linux 上にあるソースコードと Rust Language Server に接続してみたところです。

補完も定義表示も相手の環境のものを使っていい感じに動作します。

Windows から Linux への bash の接続も OK で、ショートカットキーや Powerline の制御文字などもそのまま転送してくれます。相手の環境のコマンドがそのまま打てますので、Windows 側からビルドなんかもすることができます。すごい。

デバッガーへのアタッチ

ホスト側で起動したデバッガーにアタッチして、ゲストからステップオーバーやウォッチなどの操作をすることができます。 Linux / Rust / LLDB ホスト環境に Windows ゲストからアタッチした図です。

デバッグコンソールなども転送されてきます。

たとえば、Windows で動く LLDB を準備するのはちょっと大変ですが、このように Linux に接続できると簡単に環境を揃えることができます。(残念ながらゲスト側からデバッグの実行構成を起動することができませんでした。”The host doesn’t allow starting the debugger.” と言われてしまうので何かできそうな気がするのですが、引き続き調査してみます)

任意のポートにブラウザから接続

ウェブ開発をしている時に大きな力を発揮するのが、サーバーへのポート開放です。

今度は Windows で動作している Java ウェブアプリ環境に、macOS から接続してみます。Windows ホストに macOS ゲストから接続した上で、Live Share の Share Server を選択して、開けるポートを “localhost:8080” のように指定します。(この例は SpringBoot アプリケーションなので 8080 で起動します)

あとは macOS ゲスト側のブラウザから http://localhost:8080/ にアクセスするだけです。(localhost なのが不思議な感じですが VS Code がプロキシーして、ホストに接続してくれます)

macOS から Windows のターミナル(Powershell)に接続することもできますので、gradle を動かしてアプリケーションの起動停止をするようなこともでき、ソースコードを編集しながら、ブラウザで動作確認といったことがリモートで簡単にできるようになっています。

今年はお隣におられる方の環境につないでプログラムを修正したり、一番遠くは北海道・福岡間でプログラムの修正を行ったりしました。

特に不具合の修正では、それを手元で再現させる環境をつくるまでに時間がかかるものですが、VS Live Share を使えば不具合が起きている環境に接続してすぐ修正を開始することができます。(これができなかったら修正箇所が分らなかったのではないかということもありました。

VS Code はそれ自体の導入も手軽ですので、そのようなシチュエーションに出会ったら試してみてはいかがでしょうか。

あ、ひとつまえのポストも VS Code ネタですのでよければご一緒にどうぞ。。

AsciidoctorとGradleでつくる文書執筆環境

技術文書を書く環境が欲しくなり、VS Code と Gradle を使って Asciidoc 文書を執筆する環境を整えてみました。 お手軽に構成できて、300ペジくらいの文書でも耐えられそうです。

てなわけでまた来年もよろしくお願いいたします。。 🙂