keysnailなき後の世界を生きる

2017年11月のFirefox 57 "Quantum"のリリースによって、FirefoxをEmacs化するアドオン[keysnail](https://github.com/mooz/keysnail)が完全に動かなくなりました。今までも部分的に動かなくなったことはあったのですが、今度という今度は致命的だったようです。

ほとんどのkeysnail難民と同様、筆者もkeysnailなき後の世界を生き抜くための新たな環境構築を模索しました。最終的にそれなりに満足いくものになったのでその結果を共有します。

# 筆者のデスクトップ環境

- Xubuntu Linux 16.04 (xenial)
- xorg 1:7.7+13ubuntu3
- XFCE 4.12.2 


# Vivaldi + Surfingkeys

keysnailがなくなった今となってはFirefoxに固執する理由はないため、[keysnailのissueで情報提供のあった](https://github.com/mooz/keysnail/issues/227#issuecomment-346848976)VivaldiとSurfingkeysアドオンの組み合わせを試してみました。

- [Vivaldiブラウザー | 早くて柔軟性の高いウェブブラウザー](https://vivaldi.com/)

VivaldiはChromeベースのWebブラウザです。いろいろな特徴[^1]がありますが、今回の件で特に有用だったのは、

[^1]: 今回の件とはあまり関係ないですが、タブの挙動を細かくカスタマイズ可能な点も良いです。執筆時点では[Tab Mix Plus](https://addons.mozilla.org/en-US/firefox/addon/tab-mix-plus/)はQuantum未対応なので。。

- ほぼ全てのキーボード・ショートカットをカスタマイズ可能。
- ChromeベースなのでChrome向けアドオンをそのまま使える。

キーボード・ショートカットをカスタマイズすることで、忌まわしき「新しいウィンドウ(`C-n`)」「印刷(`C-p`)」も変更ないし解除できます。それだけでもかなりストレスが下がります。ただし、さすがにキーシーケンスに対するバインディングはできません。

そこで、さらにこれにSurfingkeysアドオンを挿します。

- [brookhong/Surfingkeys: Map your keys for web surfing, expand your browser with javascript and keyboard.](https://github.com/brookhong/Surfingkeys)

SurfingkeysはWebブラウザにvim風のキーバインドを加えるアドオンですが、keysnailと同様、JavaScriptコードで設定できるのが最大の特徴です。

デフォルトの挙動はvim風ですが、Emacsキーバインドを加えた設定を作りました。

- [debug-ito/surfingkeys-settings: My settings for surfingkeys](https://github.com/debug-ito/surfingkeys-settings)

Surfingkeysは設定スクリプトをHTTP(S)で直接読み込めるので、githubやgistに上げておくと便利です。


## 問題点

この時点でだいぶマシになったのですが、以下の問題がありました。

- アドレスバーや検索バーの入力はSurfingkeysの効果が及ばない。
- "404 NotFound"などのページではSurfingkeysの効果が及ばない。(バグかも？)

keysnailのスゴかった点は、アドレスバーだろうがメニューアイテムだろうが、さらにはダウンロードファイルの保存場所を決めるダイアログですらEmacsキーバインドにしてしまっていたところですが、Surfingkeysは(おそらくWebExtension APIの制約で)そこまでできないようです。

この問題に対処するために、Gtk+ 3の設定とX11レベルのキーリマップを行いました。


# Gtk+ 3の設定

筆者の環境のVivaldiはGtk+ 3が使われているようだったので、アドレスバー、検索バー、Webページ上のテキストボックスなどはGtk+ 3の設定が効きます。

Gtk+ではテキスト入力ウィジェットのキーバインドをある程度カスタマイズできます。少なくともUbuntuのGtkパッケージにはデフォルトで"Emacs"テーマが入っているのですが、それを少々自分好みにカスタマイズしたキーバインドを作りました。

- [debug-ito/GtkEmacs-debug-ito: My Emacs-like keybinding settings for Gtk+ 3](https://github.com/debug-ito/GtkEmacs-debug-ito)

設定の適用方法は上記のREADMEを参照。

これにより、テキスト入力時の`C-f`, `C-b`, `C-n`, `C-p`, `M-f`, `M-b`, `C-a`, `C-e`, `C-h`, `C-d`, `C-k`などが効くようになります。


# X11レベルのキーリマップ

NotFoundページなどでSurfingkeysが機能しない問題については、ともかくタブ切り替えやタブ消去などの操作はちゃんとできてほしいところです。また、やや別件ですが`C-m`もあらゆる局面で機能してほしいところです。

ということで、これを解決するためにVivaldiの前段でキー入力イベントを変換(キーリマップ)するようにしました。キーリマップツールには既存のものがあるのですが、拙作の[WildBind](https://github.com/debug-ito/wild-bind)が既に似たような機能を持っていたのでこれを拡張しました。

## キーリマップツールの比較

LinuxやX11環境で利用可能なキーリマップツールを以下の表にまとめます[^2]。

[^2]: なおWindows環境では[AutoHotkey](https://autohotkey.com/)が圧倒的人気っぽいです。

|                                                  |言語   |入力源               |キーシーケンス対応|任意コマンドの実行|任意処理の実行|参照ウィンドウ情報                       |
|:------------------------------------------------:|:-----:|:-------------------:|:----------------:|:----------------:|:------------:|:---------------------------------------:|
|[xremap](https://github.com/k0kubun/xremap)       |Ruby   |X11                  |×                |○                |×            |`WM_CLASS (res_name)`                    |
|[xkeysnail](https://github.com/mooz/xkeysnail)    |Python |Linux Input Subsystem|○                |○                |○            |`WM_CLASS (res_class)`                   |
|[WildBind](https://github.com/debug-ito/wild-bind)|Haskell|X11                  |○                |○                |○            |`WM_CLASS (res_name, res_class), WM_NAME`|

- 言語: 設定を記述するプログラミング言語。
- 入力源: キー入力を検出するために使うシステム。
- キーシーケンス対応: 複数のキー入力からなる入力列に対してバインドを設定できるか。
- 任意コマンドの実行: 入力に応じて任意の外部コマンドを実行できるか。
- 任意処理の実行: 入力に応じて任意の処理(関数など)を実行できるか。
- 参照ウィンドウ情報: キーリマップ設定を切り替える際に参照する情報。いずれのツールも「アクティブウィンドウに応じてキーリマップ(バインド)を動的に変える」という機能を備えており、この情報を参照して設定切り替えの条件を設定できます。

WildBindはX11を入力源としているため、root権限なしで動作する一方、不自然と思われる挙動をする時があります。また、それなりに高機能ですが、そのぶん設定はややめんどくさいです。設定はHaskellプログラムとして書くので~~めんどくささに拍車がかかっています~~とってもtype-safeです。

## WildBindの設定

Vivaldiに関する設定は以下のようにしました(全体の設定は[こちら](https://github.com/debug-ito/wild-bind-debugito)を参照)。

```haskell
{-# LANGUAGE OverloadedStrings #-}

import Data.Monoid (mconcat)
import WildBind (Binding, whenFront)
import WildBind.Seq (toSeq, withPrefix, withCancel, fromSeq)
import WildBind.X11
  ( ActiveWindow, X11Front, XKeyEvent,
    winClass, ctrl, shift
  )
import WildBind.X11.Emulate (remap, remapR)
import WildBind.X11.KeySym  -- for xK_*

vivaldiKey :: X11Front i -> Binding ActiveWindow XKeyEvent
vivaldiKey x11 = whenFront condVivaldi binding
  where
    condVivaldi w = winClass w == "Vivaldi-stable"
    remap' = remap x11
    remap'' = remap x11
    remapR' = remapR x11
    binding = mconcat
              [ remap' (ctrl xK_n) (xK_Down),
                remap' (ctrl xK_p) (xK_Up),
                remap' (ctrl xK_s) (xK_slash),
                remapR' (ctrl xK_m) (xK_Return),
                remapR' (ctrl xK_g) (xK_Escape),
                fromSeq $ withCancel [ctrl xK_g] seq_binding
              ]
    seq_binding = mconcat [ withPrefix [ctrl xK_z] $ toSeq z_binding,
                            withPrefix [ctrl xK_x] $ toSeq x_binding
                          ]
    z_binding = mconcat [ remap'' (ctrl xK_n) (ctrl xK_Page_Down),
                          remap'' (ctrl xK_p) (ctrl xK_Page_Up),
                          remap'' (ctrl xK_c) (ctrl xK_t),
                          remap'' (ctrl xK_k) (ctrl xK_w),
                          remap'' (ctrl xK_slash) (shift $ ctrl xK_T)
                        ]
    x_binding = mconcat [ remap'' (ctrl xK_f) (ctrl xK_o),
                          remap'' (ctrl xK_s) (ctrl xK_s)
                        ]
```

この設定では、まず`C-n`, `C-p`, `C-s`, `C-m`, `C-g`をリマップしています。`C-n`, `C-p`をリマップするのは、アドレスバー入力時の候補選択で使えるようにするためです。

タブの操作は`C-z`プレフィクスにまとめています。また、「ファイルを開く」と「ファイルへ保存」もたまに使うので`C-x`プレフィクスにバインドしました。

詳細なAPIは[WildBind.Seq](https://hackage.haskell.org/package/wild-bind/docs/WildBind-Seq.html)や[WildBind.X11.Emulate](https://hackage.haskell.org/package/wild-bind-x11/docs/WildBind-X11-Emulate.html)をご参照。


# まとめ

自分の環境では

- Vivaldi
- Surfingkeys
- Gtk+ 3
- WildBind

の4つがかりの設定でようやくkeysnailなき後の世界を生き抜ける気がしてきました。改めてkeysnailの偉大さを思い知らされます。