xkeysnailでキーリマップする

[xkeysnail](https://github.com/mooz/xkeysnail)でキーコードの交換やワンショットモディファイヤができるとわかったので、試してみました。

- [xkeysnail - もうひとつの Linux 向けキーリマッパ - Qiita](https://qiita.com/mooz@github/items/c5f25f27847333dd0b37)

- [mooz/xkeysnail: Yet another keyboard remapping tool for X environment](https://github.com/mooz/xkeysnail)

Antergos、Xorg、GDMの環境にインストールします。

```shell-session
$ lsb_release -a
LSB Version:    1.4
Distributor ID: Arch
Description:    Arch Linux
Release:        rolling
Codename:       ISO-Rolling
$ uname -r
4.14.15-1-ARCH
$ python --version
Python 3.6.4
```

## 導入

venvで`/opt/xkeysnail`配下にインストールします。

```shell-session
$ sudo python -m venv /opt/xkeysnail
[sudo] miy4 のパスワード:
$ . /opt/xkeysnail/bin/activate
(xkeysnail) $ sudo pip install xkeysnail
Collecting xkeysnail
  Downloading xkeysnail-0.1.0.tar.gz
Collecting evdev (from xkeysnail)
  Downloading evdev-0.7.0.tar.gz
Collecting python-xlib (from xkeysnail)
  Downloading python_xlib-0.21-py2.py3-none-any.whl (123kB)
    100% |████████████████████████████████| 133kB 345kB/s
Collecting six>=1.10.0 (from python-xlib->xkeysnail)
  Downloading six-1.11.0-py2.py3-none-any.whl
Installing collected packages: evdev, six, python-xlib, xkeysnail
  Running setup.py install for evdev ... done
  Running setup.py install for xkeysnail ... done
Successfully installed evdev-0.7.0 python-xlib-0.21 six-1.11.0 xkeysnail-0.1.0
(xkeysnail) $ deactivate
```
~~2019/1/27追記：PyPIとGithubにあるxkeysnailのバージョンが違うので、ご注意ください。リリース版ではないのかもしれませんが、Githubの方が開発が進んでおり、機能追加、バグ修正がされています。~~
2019/11/11追記：2019年5月にPyPIで0.2.0がリリースされています。GitHubとPyPIで機能的には同等のものが導入できるようになっています。

## キーリマップの設定

以下の方針で設定します。

- CapsLockキー
  - 単体で押すと`ESC`、他のキーと一緒に押すと（つまり修飾キーとして使うと）`Ctrl`として使う
  - 単純に`CapsLock`と`Ctrl`を交換するなら、`define_modmap`が使える
- 変換キー
  - 単体で押すと`変換`、修飾キーとして使うと`Ctrl`
- 無変換キー
  - 単体で押すと`無変換`、修飾キーとして使うと`Alt`

右手、左手、それぞれで`Ctrl`と`Alt`を押しやすくなるので、Emacsを使う環境でよく設定しています。

```python:/etc/opt/xkeysnail/config.py
# -*- coding: utf-8 -*-

import re
from xkeysnail.transform import *

#define_modmap({
#    Key.CAPSLOCK: Key.LEFT_CTRL
#})

define_multipurpose_modmap({
    Key.CAPSLOCK: [Key.ESC, Key.LEFT_CTRL],
    Key.MUHENKAN: [Key.MUHENKAN, Key.LEFT_ALT],
    Key.HENKAN: [Key.HENKAN, Key.RIGHT_CTRL]
})
```

これだけ！かんたん！

## 起動：失敗

```shell-session
$ sudo /opt/xkeysnail/bin/xkeysnail /etc/opt/xkeysnail/config.py                    

██╗  ██╗██╗  ██╗███████╗██╗   ██╗                                                                  
╚██╗██╔╝██║ ██╔╝██╔════╝╚██╗ ██╔╝                                                                  
 ╚███╔╝ █████╔╝ █████╗   ╚████╔╝                                                                   
 ██╔██╗ ██╔═██╗ ██╔══╝    ╚██╔╝                                                                    
██╔╝ ██╗██║  ██╗███████╗   ██║                                                                     
╚═╝  ╚═╝╚═╝  ╚═╝╚══════╝   ╚═╝                                                                     
  ███████╗███╗   ██╗ █████╗ ██╗██╗                                                                 
  ██╔════╝████╗  ██║██╔══██╗██║██║                                                                 
  ███████╗██╔██╗ ██║███████║██║██║                                                                 
  ╚════██║██║╚██╗██║██╔══██║██║██║                                                                 
  ███████║██║ ╚████║██║  ██║██║███████╗                                                            
  ╚══════╝╚═╝  ╚═══╝╚═╝  ╚═╝╚═╝╚══════╝                                                            
                             v0.1.0                                                                

Traceback (most recent call last):                                                                 
  File "/opt/xkeysnail/bin/xkeysnail", line 6, in <module>                                         
    cli_main()                                                                                     
  File "/opt/xkeysnail/lib/python3.6/site-packages/xkeysnail/__init__.py", line 44, in cli_main    
    eval_file(args.config)                                                                         
  File "/opt/xkeysnail/lib/python3.6/site-packages/xkeysnail/__init__.py", line 5, in eval_file    
    exec(compile(file.read(), path, 'exec'), globals())                                            
  File "/etc/opt/xkeysnail/config.py", line 4, in <module>                                         
    from xkeysnail.transform import *                                                              
  File "/opt/xkeysnail/lib/python3.6/site-packages/xkeysnail/transform.py", line 13, in <module>   
    def get_active_window_wm_class(display=Xlib.display.Display()):                                
  File "/opt/xkeysnail/lib/python3.6/site-packages/Xlib/display.py", line 89, in __init__          
    self.display = _BaseDisplay(display)                                                           
  File "/opt/xkeysnail/lib/python3.6/site-packages/Xlib/display.py", line 71, in __init__          
    protocol_display.Display.__init__(self, *args, **keys)                                         
  File "/opt/xkeysnail/lib/python3.6/site-packages/Xlib/protocol/display.py", line 167, in __init__
    raise error.DisplayConnectionError(self.display_name, r.reason)                                
Xlib.error.DisplayConnectionError: Can't connect to display ":1": b'No protocol specified\n'       
```

`Can't connect to display ":1"`と言っており、Xサーバに繋がらなかったようです。
xhostで確認すると、rootの接続は許可されていないのがわかります。

```shell-session
$ xhost
access control enabled, only authorized clients can connect
SI:localuser:miy4
```

## 起動：成功

試しに`xhost +SI:localuser:root`でアクセスコントロールリストにrootを追加して、xkeysnailが起動することを確認しました。

```shell-session
$ xhost +SI:localuser:root
localuser:root being added to access control list
$ xhost
access control enabled, only authorized clients can connect
SI:localuser:root
SI:localuser:miy4
$ sudo /opt/xkeysnail/bin/xkeysnail /etc/opt/xkeysnail/config.py

██╗  ██╗██╗  ██╗███████╗██╗   ██╗
╚██╗██╔╝██║ ██╔╝██╔════╝╚██╗ ██╔╝
 ╚███╔╝ █████╔╝ █████╗   ╚████╔╝
 ██╔██╗ ██╔═██╗ ██╔══╝    ╚██╔╝
██╔╝ ██╗██║  ██╗███████╗   ██║
╚═╝  ╚═╝╚═╝  ╚═╝╚══════╝   ╚═╝
  ███████╗███╗   ██╗ █████╗ ██╗██╗
  ██╔════╝████╗  ██║██╔══██╗██║██║
  ███████╗██╔██╗ ██║███████║██║██║
  ╚════██║██║╚██╗██║██╔══██║██║██║
  ███████║██║ ╚████║██║  ██║██║███████╗
  ╚══════╝╚═╝  ╚═══╝╚═╝  ╚═╝╚═╝╚══════╝
                             v0.1.0

No keyboard devices specified via (--devices) option.
xkeysnail picks up keyboard-ish devices from the list below:

-----------------------------------------------------------------------------------
Device               Name                                Phys
-----------------------------------------------------------------------------------
/dev/input/event0    AT Translated Set 2 keyboard        isa0060/serio0/input0
/dev/input/event1    Lid Switch                          PNP0C0D/button/input0
/dev/input/event2    Sleep Button                        PNP0C0E/button/input0
/dev/input/event3    Power Button                        LNXPWRBN/button/input0
/dev/input/event4    PC Speaker                          isa0061/input0
/dev/input/event5    ThinkPad Extra Buttons              thinkpad_acpi/input0
/dev/input/event6    Integrated Camera: Integrated C     usb-0000:00:14.0-8/button
/dev/input/event7    SynPS/2 Synaptics TouchPad          isa0060/serio1/input0
/dev/input/event8    Video Bus                           LNXVIDEO/video/input0
/dev/input/event9    HDA Digital PCBeep                  card0/codec#0/beep0
/dev/input/event10   HDA Intel PCH Mic                   ALSA
/dev/input/event11   HDA Intel PCH Headphone             ALSA
/dev/input/event12   HDA Intel PCH HDMI/DP,pcm=3         ALSA
/dev/input/event13   HDA Intel PCH HDMI/DP,pcm=7         ALSA
/dev/input/event14   HDA Intel PCH HDMI/DP,pcm=8         ALSA
/dev/input/event15   HDA Intel PCH HDMI/DP,pcm=9         ALSA
/dev/input/event16   HDA Intel PCH HDMI/DP,pcm=10        ALSA
/dev/input/event17   TPPS/2 IBM TrackPoint               synaptics-pt/serio0/input0
/dev/input/event18   py-evdev-uinput                     py-evdev-uinput

Okay, now enable remapping on the following device(s):

------------------------------------------------------------------------------
Device               Name                                Phys
------------------------------------------------------------------------------
/dev/input/event0    AT Translated Set 2 keyboard        isa0060/serio0/input0
```

## 自動起動

ここまでをふまえて、ログイン時にxkeysnailを自動的に起動するようにしたいです。
Xサーバへの接続やuinputの読み書きのアクセスコントロールが必要なので、rootでやるのは怖さを感じる。xkeysnail用のユーザを作ることにします。

### 起動用のユーザを作成

```shell-session
$ sudo groupadd uinput
$ sudo useradd -G input,uinput -s /sbin/nologin xkeysnail
```

inputグループは`/dev/input/*`の所有者グループです。
xkeysnailユーザには`/dev/input/*`、`/dev/uinput`へのアクセス権限を与えます。

```:/etc/udev/rules.d/40-udev-xkeysnail.rules
KERNEL=="uinput", GROUP="uinput"
```

```:/etc/modules-load.d/uinput.conf
uinput
```

xkeysnailユーザにsudoし、xkeysnailを実行する時は、パスワードは省略します。

```:/etc/sudoers.d/10-installer
miy4 ALL=(ALL) ALL,\
         (xkeysnail) NOPASSWD: /opt/xkeysnail/bin/xkeysnail
```

`10-installer`はAntergos (Arch Linux)導入時に用意されたsudoersファイルです。

### 設定

GDMはXセッション開始時に`~/.xprofile`を実行するので、ここでxkeysnailを起動します。

```:~/.xprofile
if [ -x /opt/xkeysnail/bin/xkeysnail ]; then
  xhost +SI:localuser:xkeysnail
  sudo -u xkeysnail DISPLAY=:1 /opt/xkeysnail/bin/xkeysnail /etc/opt/xkeysnail/config.py &
fi
```

Linuxを再起動し、GDMからログインし、キーリマップが働いていることを確認します。

## 課題

### Thinkpad keyboardのトラックポイントが効かない

- [ThinkPad Bluetooth ワイヤレス・トラックポイント・キーボード - 日本語 | 周辺機器 | レノボジャパン](https://www3.lenovo.com/jp/ja/deals/sbiz/sbiz-accessory/KEYBOARD-Japanese/p/0B47181)

上記のBluetoothキーボードを使っているのだけど、xkeysnailで同キーボードの`/dev/input/event*`を指定すると、トラックポイントが使えなくなる。キーボードのリマップはできている。

外付けではない、Thinkpad内臓のキーボードではトラックポイントも問題なく使えているので、同キーボード固有の問題なのだと思う。要調査。←keyイベントとmouseイベントが混在したデバイスに対してのxkeysnailの扱いが影響しているようです。fujimisakariさんのコメントを参照