TL;DR
- Linux (X11) 向けのキーリマップツールをつくった
- Python 3 で書かれており
sudo pip install xkeysnailで入れることができる -
uinputという低レイヤで動作するため「多くのキーリマップツールではうまくリマッ ピングが動かなかった場所」でも動作する - アプリケーションごとにキーバインドを変えたり、複数ストロークのキーにコマンドを 割り当てたり Python の関数をキーに割り当てたりすることもできる
- 詳しくは https://github.com/mooz/xkeysnail で
背景
Firefox が Quantum になり、旧来の XUL ベースのアドオンは動作しなくなった。新たな拡張機能の機構である WebExtensions ではキーボードショートカットに関する API が大幅に制限され、ぼくは拙作のアドオン KeySnail のWebExtensions 化を諦めた。
それからというもの、Firefox でのブラウジングが少し苦痛なものとなった。Emacs キーバインドを無意識のうちに使うたび、新しいウィンドウや印刷ダイアログが無数に開き、虐げられる気分で日々を過ごしていた。
そうこうしているうちに限界になったので、ブラウザの外で何とかしようと思い Linux 向けのキーリマップツールを探すことにした。
なぜまた別のキーリマップツールを作ったのか
現在、世の中ににあるもので、残念ながら自分のユースケースに合うものが見つからなかった。
はじめ id:k0kubun さんの xremapを試し「Ruby で気持ちよくキーリマップができて最高なのではないか?」だと思ったのだが、残念ながら Firefox ではリマップが動作しないことがあったり、動作が重いことがあった。これは調べてみると xremap が高めのレイヤでキーフック/キーエミュレーションをやるためのようで、アプリケーションによってはそうして高いレイヤでエミュレートされたイベントを無視しうるため、結果的にリマップが動作しない場面が出てしまうようだった。
そもそも xremap が高めのレイヤでキーフック/エミュレーションをしているのは「管理者権限 (sudo) を必要とさせたくない」 という design decision のよう。
ぼくは管理者権限を使ってでも、とにかく色んな場所でアプリケーションごとのリマップ機能が欲しかったので、新しく xkeysnail というリマップツールを作ることにした。
xkeysnail の特徴
https://github.com/mooz/xkeysnail
今回作った xkeysnail は、以下のような特徴を持つ。
-
利点
- フレキシブルなキーリマップ機能
- アプリケーションごとのキーバインド設定
-
複数ストロークのキーバインド設定 (例:
Ctrl+x Ctrl+ctoCtrl+q) - キーのリマップだけでなく Python を通じた任意のコマンドの実行
- 高い信頼性のキーリマップ (
uinputを利用し低レイヤで動作するため、ほとんどの場所でリマップが動作する)
-
欠点
-
uinputを利用するため管理者権限での実行が必要となる
-
xkeysnail のキーリマップ機構は pykeymacs をベースにしている。実際は pykeymacs がアプリケーションごとのキーマップの定義や、カスタマイズの仕組みを持たなかったりしたのでゴリゴリ機能を追加していたら、新しいキーリマップツールができていたという流れになる。
インストール
管理者権限と Python 3 が必要。
Ubuntu
sudo apt install python3-pip
sudo pip install xkeysnail
ソースコードからのインストール
git clone --depth 1 https://github.com/mooz/xkeysnail.git
cd xkeysnail
sudo pip install --upgrade .
使い方
sudo xkeysnail config.py
config.py の書き方
(Emacs ライクなキーバインドが必要なだけであれば example/config.py をそのまま利用することをお薦めする)
xkeysnail の設定ファイル config.py には「各アプリケーション」で「どんなキーバインドを定義するか」を書く。これをキーマップと呼ぶ。
キーマップの定義には以下で説明する define_keymap(condition, mappings, name) という関数を使う。
その前にイメージをつかんでもらうため example/config.py の中身を少し抜粋する。
from xkeysnail.transform import *
define_keymap(re.compile("Firefox|Google-chrome"), {
# Ctrl+Alt+j/k to switch next/previous tab
K("C-M-j"): K("C-TAB"),
K("C-M-k"): K("C-Shift-TAB"),
}, "Firefox and Chrome")
define_keymap(re.compile("Zeal"), {
# Ctrl+s to focus search area
K("C-s"): K("C-k"),
}, "Zeal")
define_keymap(lambda wm_class: wm_class not in ("Emacs", "URxvt"), {
# Cancel
K("C-g"): [K("esc"), set_mark(False)],
# Escape
K("C-q"): escape_next_key,
# C-x YYY
K("C-x"): {
# C-x h (select all)
K("h"): [K("C-home"), K("C-a"), set_mark(True)],
# C-x C-f (open)
K("C-f"): K("C-o"),
# C-x C-s (save)
K("C-s"): K("C-s"),
# C-x k (kill tab)
K("k"): K("C-f4"),
# C-x C-c (exit)
K("C-c"): K("M-f4"),
# cancel
K("C-g"): pass_through_key,
# C-x u (undo)
K("u"): [K("C-z"), set_mark(False)],
}
}, "Emacs-like keys")
ここで使っている define_keymap(condition, mappings, name) は以下のような仕様となっ
ている。
define_keymap(condition, mappings, name)
condition にはキーバインド群 mappings を「どのアプリケーションで有効にするか」という条件を指定する。条件は以下のどれか。
- 正規表現 (e.g.,
re.compile("YYY"))- パターン
YYYがアプリケーションのWM_CLASSにマッチした場合にキーマップを有効にする。
- パターン
-
lambda wm_class: some_condition(wm_class)-
WM_CLASSを受けとり True/False を返す関数でキーマップの有効/無効を指定。正規表現では表現しにくい複雑な条件を書きたいときに。
-
-
None: 全てのアプリケーションで有効な「グローバルキーマップ」をあらわす。
mappings には {key: command, key2: command2, ...} という形式の辞書を指定する。key と command は以下の形式。
-
key: コマンドを割り当てたいキーをK("YYY")の形式で書く。- キーのシンタックスは後述の key specification section にて説明。
-
command: キーに割り当てるコマンド。以下のどれかが指定可能。-
K("YYY"): キーイベントをそのアプリケーションに送る。 -
[command1, command2, ...]: 配列を指定すると、コマンドを連続して送る。複数 キーイベントを送りたい場合などに。 -
{ ... }: 辞書を指定すると、サブのキーマップとなる。複数ストロークのキーバインドを定義するときに利用する。詳しくは後述の multiple stroke keys にて。 -
pass_through_key: そのキーをそのままアプリケーションに送る。グローバルなキーマップで定義したコマンドを、特定のアプリケーションでは無効にしたいときに利用する。 -
escape_next_key: 次の1ストロークはアプリケーションにキーイベントをそのまま送る。 - 関数: 関数が指定されると、コマンド実行にその関数が実行される。返り値はさらにコマンドとして解釈される。基本的には None を返しておけばよい (return を省略)。
- UNIX コマンドをキーに割り当てたい場合に有用。
-
name にはキーマップの名前を書く。オプショナルな引数。
Key Specification
キーの記法は K("(<Modifier>-)*<Key>") という構成になっており、
<Modifier> はモディファイアの指定で、
-
CorCtrl-> Control key -
MorAlt-> Alt key -
Shift-> Shift key -
SuperorWin-> Super/Windows key
という割り当てとなっている。
<Key> にキーを書く。各キーの書き方は key.py を参照。
キー記法の例を以下に。
-
K("C-M-j"):Ctrl+Alt+j -
K("Ctrl-m"):Ctrl+m -
K("Win-o"):Super/Windows+o -
K("M-Shift-comma"):Alt+Shift+comma(=Alt+>)
Multiple stroke keys
複数ストロークのキーバインドを指定したい場合は、キーマップをネストさせる。例えば以下の例では C-x C-c に C-q を割り当てている。
define_keymap(None, {
K("C-x"): {
K("C-c"): K("C-q"),
K("C-f"): K("C-q"),
}
})
アプリケーションの WM_CLASS を xprop で調べる
各アプリケーション毎にキーマップを定義する場合、そのアプリケーションが何と言う WM_CLASS を持っているかを調べる必要がある。これには xprop というコマンドを使えば良い。
コマンドラインで
xprop WM_CLASS
とするとカーソルの形が変わるので、あとは対象のアプリケーションをクリックすると、コマンドラインに
WM_CLASS(STRING) = "Navigator", "Firefox"
のような出力がなされる。この2つめ (この例なら Firefox) が xkeysnail における WM_CLASS の値になるので、これを config.py 内で利用する。
例えば
define_keymap(re.compile("Firefox"), {
K("C-x"): {
K("C-c"): K("C-q"),
K("C-f"): K("C-q"),
}
})
というぐあい。
まとめ
xkeysnail という Linux 向けのキーリマッパを作った。個人的には、かなりライフチェンジングなツールになった。バグレポートや要望などあれば GitHub まで頂けると助かります。