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+c
toCtrl+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>
はモディファイアの指定で、
-
C
orCtrl
-> Control key -
M
orAlt
-> Alt key -
Shift
-> Shift key -
Super
orWin
-> 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 まで頂けると助かります。