Linux用キーリマッパーxremapをRustで書き直した

このエントリはRust Advent Calendar (3) 22(-10)日目 の記事です。

5年前にxremapというLinux向けのキーリマッパーを作った (Linux向けの最強のキーリマッパーを作った - k0kubun's blog) のだが、X11のレイヤーで実装したため、GNOMEのActivitiesでリマップが効かなかったり、WaylandではXWayland内でしか動かないといった問題があった。

これらの問題を解決すべく、xkeysnailwayremapといったツールが後に作られたのだが、xkeysnailはWaylandで動かずwayremapはX11で動かない方針なのと、やはり全てのキー入力を中継するようなツールはPythonのような遅い言語よりRustみたいな速い言語で書かれるべきだと思ったので、後発のツール*1の良いところ取りをしながら今回xremapを作り直した。

github.com

Rust版xremapの設計思想

JSON互換な設定ファイル

mitamaeのように、宣言的な記述だけでなくlocal_ruby_blockといった動的な処理をサポートすることを念頭に置いたツールもあるが、xremapに関してはほとんどが静的な設定で十分で、キーの入力以外に必要な処理もそこにごく少数の拡張をいれれば十分であると考えた。Rust版xremapの設定は、あるキー入力を別のキー入力にリマップする設定は以下のようにできるが、

keymap:
  - name: example
    remap:
      M-c: C-c

文字列をキー入力を表現する型として使いつつ、キーを1つだけ持つ連想配列を関数呼び出しのように使うと、ある程度リッチな機能も静的な設定で表現できる((注: この記事を書いている時点ではこの launch 関数は実装してないが、すぐに実装できるようにはしてあって、割とすぐ実装する予定))。

keymap:
  - name: example
    remap:
      M-c: { launch: 'google-chrome' }

なお、C-x C-s のようなキーシーケンスをリマップする場合も、同じ仕組みでremapという関数を利用することで実現されている。

keymap
  - name: example
    remap:
      C-x:
        # 直後の入力でのみ以下のremapを適用
        remap:
          C-s: C-w

そして、JSON互換なことにより、RubyPythonのような言語内のDSLから生成したオブジェクトをxremapの設定にシリアライズするといったことが極めて用意になり、好きな言語が設定に使えるようになる。他のツールはPythonでしか設定できないけど、Rubyの方が好き、という人はいると思う。あと、Ruby DSLを維持するためにmrubyを使い続けようとすると、mrubyのビルドシステムで苦労したり、Rust<->Ruby間のブリッジを書かないといけないのが面倒そうだったので、設定を境界にツールを二分した方がメンテしやすそうだとも考えている。

なお、PoCとしてxremap-rubyxremap-pythonを作っていて、前者はmruby版xremapのDSLに近く、後者はxkeysnailのDSLに近いように作った*2。ただ、一旦リリースしてしまいたかった都合、現時点ではYAML用のドキュメントが一番充実している状態なので、ひとまずYAMLで使い始めていただく方が楽かもしれない。

Wayland対応

Xlibからevdev+uinputベースの実装に切り替えたことで、基本的な機能はWaylandプラットフォームでも動くようになった。ここはxkeysnailやwayremapも同じ。

問題なのが、これらのツールは「現在フォーカスしているウィンドウ」に応じてリマップする機能を提供していることで、これをポータブルにするのが難しい。Waylandはプロトコル内に「現在フォーカスしているウィンドウ」という概念がないようで、これをWaylandで動くコンポーザ全てでポータブルに判定するというのがどうも難しいらしい。Swayが使っているwlrootsというフレームワークを使っているコンポーザは多くあり、wlrootsのプロトコルを話すとそれらのコンポーザには対応できるのだが、GNOMEとかはwlrootsを使っていないので、2021年現在ではそれらに個別対応していく必要がありそう。

で、特にGNOME Waylandはxkeysnailもwayremapも対応してないようなので、それらとの差別化として、xremapはGNOME Waylandの個別対応をいれた。GNOME特有のJavaScriptD-Busに流し込むという奴なんだけど…まあ一切ポータブルじゃないのでその辺のツールがこれに対応してなくても文句いえなさそう。あと、GNOME 41以降では更に違う対応をしないといけないらしい。とりあえずUbuntu 20.04で使われているGNOME 3.36では動くようにしてある。

wayremapはSway向けの個別対応が入っているのだけど、wayremap作者のacro5pianoさん向けに、同じ対応をxremapにも入れた。

ちなみにこれらの個別対応は --features を使って別バイナリとして実装していて、これは何故かというと、libX11は静的リンクが困難ぽいので、動的リンクの依存が非X11向けバイナリには不要なようにするため。

Rustを使ってみた感想

実はRustは5年くらい前に使ってみたことがあるのだが、その時は今回ほど複雑なツールを作らなかったのでまともに入門したわけではなく、実際何も覚えてなかったので、今回初めてRustを学んだ状態に近い。せっかくなので、初学者のフレッシュな感想を書き残しておこうと思う。

難しすぎワロタ

参照以外の方法で値を読もうとするとmoveが発生して、それによりコンパイルが通らなくなる場面には多々遭遇するが、かといって clone()copy() で解決すると負けたような気持ちになるので、なるべく速そうなコードでコンパイルを通すパズルに頭を悩ませる時間が長い。とはいえ、これは他の言語では単にコピーが無限に発生して遅かったりスレッドセーフではない方法で参照を共有したりみたいな方法で解決されているだけの問題に見えるので、ここをサボらないようにするにはどうすればいいか考えること自体は人間が考えるに値する本質的な作業という感じがする。

しかし、これはかなりテクが必要な感じがしており、ある程度便利イディオムのようなものを吸収するまでは、実装しているアプリが解決しようとしている問題とはあまり関係ないところでかなり時間を消費させられる感じがある。例えば、&mut selfなメソッドがあるstructでは、&mut selfでmutableなborrowをしているせいでselfの他のフィールドをimmutable borrowすることができなくなる(と理解している)が、そもそもmutしないようなフィールドはそういうstructの外側で管理するようにすると、そのselfとは独立してimmutable borrowが(複数)可能になり、いろいろやりやすくなる、とか思ったけど、そういうプラクティスをあらかじめ知ってないと辿りつけない設計が必要だったりするように思う。

特にlifetime parameterはなかなか他の言語でお目にかかれない概念なので難しくて、これを使いこなしている人たちは本当にすごいと思う。Goとかは仕様がとてもシンプルな分、ある程度期間をあけて触ってもすぐに理解できる良さがあるが、Rustは高度な言語機能で全てを解決する方に振り切れている感じがしており、Rustで何かを書いた後少し離れて少し忘れてしまった時のことが心配ではある。

コンパイラのエラーメッセージがよくできてる

Ruby 3.1にもerror_highlightというエラー表示を見やすくする機能が入るが、Rustに関してはムーブセマンティクスとlifetime parameterがあちこちで干渉しあってる時にそれらの原因を可視化するというのが明らかに大変そうなのにちゃんと実装されている。lifetime parameterのエラーはコンパイラの指示に従っているだけで一旦コンパイルが通るといったこともあった。まあちゃんと理解してないと次のステップでコンフリクトして詰みとかも普通にあったけど…。

IDEはあった方がいいけどやや不便

Kotlinを書き始めたころ、Vimで快適にKotlinを書くための良い環境がなかったのでそこからIntelliJを併用するようになったのだが、特に学び始めでよくわかってなかったり、importなどそもそも全てを手動で書くのが面倒な感じの言語ではIDEは便利だと思う。モジュールとかimportのやり方をまだ完璧には理解できてないのだが、他の言語とかでも使えるインポートのキーバインドを叩くだけで勝手に適切なimportが挿入されるのは初学者には嬉しい。

で、これはJetBrainsのIDE特有の話なんだけど、JetBrains製エディタのRustプラグインは基本的な機能は揃ってるものの、KotlinやJavaのようなIntelliJの本命っぽい言語に比べるとややサポートが弱い感じがする。例えばunused importをIDEが教えてくれなくて、コンパイラのwarningがIDE上ではどの行のことなのか丁寧に追って消すみたいな作業が必要になる。とはいえ、IntelliJの操作に慣れている点が便利なので、他に便利なRust開発環境が現れることよりもJetBrainsがRustに投資してくれることを祈るばかりである。

? 便利

基本的に難しくて大変だし ; 書くの面倒だなあという印象が強かったけど、Rustのシンタックスで唯一感心したのが ? で、これは例えばGoで以下のように書くよくある奴が、

foo, err := bar()
if err != nil {
  return err
}

以下のように短く書けるという奴。

let foo = bar()?;

僕はどちらかというとエラーバケツリレーよりは例外がある言語の方が基本的には好きなのだが、バケツリレーのための追加記述が ? の1文字で済むというのはとても良いと思う。

まとめ

xremapお試しください。既に常用できているけど、まだ足したいと思ってる機能がいくつかあるので、現時点ではないけど欲しい機能などあればコメントをいただければ優先度を上げたりできるかもしれない。

*1:xkeysnailやwayremapに加え、xremapをRustに書き直したrumap https://dawn.hateblo.jp/entry/rumap もその一つ

*2:が、特にPythonの方はlambdaでのマッチングをJSONに変換するといったことは難しいので、完全互換という風には作っていない