Ubuntu で Mac / emacs キーバインディング（キーボードショートカット）を使いたい xkeysnail のススメ

キーボード・ショートカットを変換できる [xkeysnail](https://qiita.com/mooz@github/items/c5f25f27847333dd0b37) は素晴らしいので使おうという話．


Mac では `Ctrl+a` で行頭に移動、`Ctrl+k` で行末までカットといったことが emacs・ターミナルだけではなくあらゆる所で出来る．これを Ubuntu でも実現したい．Gnome で emacs キーバインディングを実現する `gnome-tweak-tool` key-theme=Emacs では全然満足できなかった; ブラウザ内で Windows ライクなショートカットで上書きされてしまい、特に Jupyter Notebook で `Ctrl-a` が「全体を選択」になってしまうのがきつい．xkeysnail では `Ctrl+a` を Home キーだと思わせることで行頭への移動を実現している、このためブラウザの中でも（ウェブアプリが普通の windows式に対応している限り）使える．Ubuntu 要素は特になく Linux 全般で使える.


## 1. インストール

```bash
$ sudo apt install python3-pip
$ sudo pip3 install xkeysnail
```

`pip` は Python のライブラリパッケージ管理ソフト．


## 2. config.py を作る

とりあえずはどこでも良いので `config.py` を作る．[公式 config.py はこちら](https://github.com/mooz/xkeysnail/blob/master/example/config.py)．


## 3. 実行

その config.py のあるディレクトリで:

```
$ xhost +SI:localuser:root
$ sudo xkeysnail config.py
```


### Troubleshooting

##### 1. command not found: xkeysnail


`sudo pip3 install ...` ではなく `pip3 install ...` とやると `~/.local/bin/xkeysnail` にインストールされ、PATH がたぶん通ってない．

```
$ pip3 show xkeysnail
（中略）
Location: /usr/local/lib/python3.6/dist-packages
```

を見るとインストール先がわかる. conda やら pyenv などで多くの環境をつかっているならどの環境にインストールされたか注意．sudo を使った場合と使わなかった場合で環境が異なるかもしれない．sudo pip をつけわすれたのをやり直すのは


```bash
$ pip3 uninstall xkeysnail
$ sudo pip3 install xkeysnail
```

あるいはお好みで PATH を通す、絶対PATHで実行するなど．


##### 2. ImportError: sys.meta_path is None, Python is likely shutting down

もっと上を見てみるともっと具体的なエラーがみつかる．例えば

```
FileNotFoundError: [Errno 2] No such file or directory: 'config.pu'
```

は config.py のファイル名を間違えた．config.py の中でキーの名前を間違えているかもしれない．



##### 3. Xlib.error.DisplayConnectionError: Can't connect to display ":0":

`xhost +` をやっていないと出る。



## 4. config.py を編集する

* キーボードは Apple Magic Keyboard. Emacs 以外では ⌘C で copy, ⌘S で save など command ⌘ を Windows の ctrl のように使いたい.
* しかし、`⌘ space` は 英語日本語の言語切り替えに使い、emacs の mark set `ctrl-space` とは別に使いたい。つまり command=ctrl にしてしまうのはやりすぎ.
* Ubuntu には左⌘ (左 windows key) がすべてのウインドウを小さめに表示する機能に割りて当てられている. なので、左⌘で右⌘を使ってることにして無効化する。
* Caps lock は使わないので他のキーに割り当てて有効に使う。
* Ctrl+a, Ctrl-e, Ctrl-k などの emacs keybindings は公式 config.py のものを使う。これが一番大事。


```python

# -*- coding: utf-8 -*-
import string
from xkeysnail.transform import *

# define timeout for multipurpose_modmap
define_timeout(1)

# [Global modemap] Change modifier keys as in xmodmap
define_modmap({
    Key.CAPSLOCK: Key.ESC,  # caps lock を Esc として使う。Ctrl派が多いかも
    Key.LEFT_META: Key.RIGHT_META, # overview 無効化: 左⌘ → 右⌘ 
})

# Always paste with ⌘-v including terminals
# None は無条件=すべてのアプリケーションを意味する
define_keymap(None, {
    K("Super-v"): [K("C-v"), set_mark(False)],
}, "Paste")


# Mac-like keybindings outside emacs and terminals
# 最低限の emacs keybinding.
# 公式 config.py はもっと emacs 化している. C-x C-s → 保存なんかもできる。
mapping = {
    # Beginning/End of line
    K("C-a"): with_mark(K("home")),
    K("C-e"): with_mark(K("end")),
    
    # Delete
    K("C-d"): [K("delete"), set_mark(False)],
    K("C-h"): with_mark(K("backspace")),
    
    # Kill line
    K("C-k"): [K("Shift-end"), K("C-x"), set_mark(False)],
}

# すべてのアルファベット α について ⌘α を Ctrl+α を押していることにする
for c in string.ascii_lowercase:
    mapping[K('Super-' + c)] = K('C-' + c)

# Emacsとターミナル以外でショートカットを変更する．Hyper は私の使っているターミナルアプリケーション名．
define_keymap(lambda wm_class: wm_class not in ("Emacs", "Hyper", "Gnome-terminal"),
              mapping, "Mac-like")
```

**wm_class** がアプリケーション名で xkeysnail を実行中のターミナルの標準出力をみればわかる。xkeysnail -q オプションをつけるとこの WM_CLASS やキーの表示は抑制される.


### 最低限の Python

Python に馴染みのない人向け．

#### コメント

`#` から行末までコメント．

#### 文字列
* `"文字列"`, `'文字列'` どちらでもOK

#### 辞書 dict

Key - Value ペアを保存するデータ構造; `定義したいショートカット: 実行されるショートカット` を与えるのに使われている.

```python
d = {key1: value1.
     key2: value2,
     key3: value3}
```

最後にカンマ (trailing comma) をつけても大丈夫、なくても良い `{key3: value3,}`.
```

作成後も追加できる:

```
d[key4] = value4
```


#### Tuple, list, set

```python
wm_class not in ("Hyper", "Gnome-terminal", "Emacs")
```

文字列 wm_class が３つのうちどれでもない場合に True になる．否定演算子 not による

```python
not (wm_class in ("Hyper", "Gnome-terminal", "Emacs"))
```
と同じ．どれかなら True にしたいなら

```python
wm_class in ("Hyper", "Gnome-terminal", "Emacs")
```

("a", "b", "c") は変更できない配列 *tuple*. 細かいことを気にしなければ、変更できる配列 *list* ["a", "b", "c"] も 集合 *set* {"a", "b", "c"} も同じ．

ひとつなら、

```python
wm_class == "Hyper"  # 等しい場合に True
wm_class != "Hyper"  # 等しくない場合に True
```

でもいい．要素ひとつの tuple を使う場合は最後のカンマを忘れずに:

```python
wm_class in ("Hyper",)
```

カンマ無しだと

```python
wm_class in "Hyper"
```

と同じになって意味が変わり、「wm_class は "Hyper" の部分文字列ですか？」という意味になる.

```python
"ype" in "Hyper"  # => True
"ype" in ("Hyper")  # => True, ("Hyper") = "Hyper"
"ype" in ("Hyper", )  # => False, "ype" は "Hyper" 
                      # という要素ひとつからなる集合の要素ではない.
```

#### 無名関数 lambda


```python
f = lambda x: x**2
```

は

```python
def f(x):
    return x**2
```

と同じ．アプリケーション名 wm_class に対してキーボード・ショートカットを変更するか否かを返す関数を定義するのに使われている.



### define_keymap 関数

アプリケーション名 *wm_class* によってショートカットを変換するかどうかを変えられる．

```python
def define_keymap(condition, mappings: dict, name: str):
```

条件 *condition* を満たした場合に 辞書 *mappings* で定義されたショートカット変換を行う．*name* は任意の設定の名前．

condition は３種類の指定の仕方がある:

```
condition:
  None: 無条件で適用
  関数: アプリケーション名 wm_class をとって True/False を返す関数
  正規表現: wm_class がマッチすべき正規表現（詳細略）
```

### Key

`Key.CAPSLOCK` とか `Key.ESC` とかの一覧は [key.py](https://github.com/mooz/xkeysnail/blob/master/xkeysnail/key.py) に書いてある．

| f Key  |
| ---- |
|  ESC  |
|  TAB  |
| CAPSLOCK |
| LEFT_CTRL, RIGHT_CTRL |f
| LEFT_SHIFT, RIGHT_SHIFT |

| Key |Apple Magic keyboard | Windows keyboard |
| --- | --- | --- | --- |
| LEFT_META, RIGHT_META | command ⌘ | Win key |
| LEFT_ALT, RIGHT_ALT | option ⌥ | Alt |

### K 関数

K("C-b") で Ctrl+b を表す．C は Ctrl でもよく、LR をつけることで左か右に限定することもできる.　キーを押してみて xkeysnail の標準出力をみればどう書くべきかわかる. Modifier key にはさらに左右どちらでもいい場合の総称や別名（Win は Super でもいいなど）がある.

|  Modifier key  |  str for K |
| ---- | ---- |
| Ctrl ^ | C, Ctrl, LC, RC, LCtrl, RCtrl |
| Shift | Shift, LShift, RShift
| Win/command ⌘ | Win, Super, LWin, RWin, LSuper, RSuper |
| Alt/option ⌥ | Alt, M, LAlt, RAlt, RM, LM |

参考: create_modifiers_from_strings(modifier_strs) in [transform.py](https://github.com/mooz/xkeysnail/blob/master/xkeysnail/transform.py).


## 5. 自動起動

### 実行スクリプトを書く

設定が完了しうまく動いているようならコンピュータ起動時に自動的に実行したい。service として登録する方法と `.config/autostart` を使う方法があるみたい。ここでは後者でいく。起動スクリプトは

```
$ mkdir ~/.xkeysnail
```



に入れることにする。必要なのは起動スクリプトだけだけど[ここ](https://hidakatsuya.hateblo.jp/entry/2019/11/20/215608)に倣って start,stop,restart を作っておく。

```
$HOME/
  .xkeysnail/
    config.py
    start.sh
    stop.sh
    restart.sh
  .config/
    autostart/
      xkeysnail.desktop
```

start.sh

```bash
#!/usr/bin/env bash
xhost +SI:localuser:root
sudo xkeysnail -q $HOME/.xkeysnail/config.py &
```

stop.sh

```bash
#!/usr/bin/env bash

PID=`ps --no-heading -C xkeysnail -o pid | tr -d ' '`

if [ -n "$PID" ]; then
  sudo -u xkeysnail kill $PID
  echo "Stopped xkeysnail to kill $PID"
fi
```

restart.sh

```bash
#!/usr/bin/env bash

cd `dirname $0`
bash stop.sh
bash start.sh
```

start.sh に実行権限をつける:

```bash
$ chmod u+x start.sh
```

stop.sh, start.sh もお好みに応じて。あるいは

```bash
$ bash stop.sh
$ bash restart.sh
```



### パスワードなしで sudo できるようにする

xkeysnail は sudo を必要とするけど自動起動時にパスワードを入力することはできない。
なので xkeysnail だけは sudo 無しで実行できるように `/etc/sudoer` を編集する; 普通のテキストファイルだけど編集には visudo を使えとある。

```
$ sudo visudo
```

```
username ALL=(ALL) NOPASSWD: /usr/local/bin/xkeysnail
```
の一行を追加する。`username` はあなたのユーザー名。 xkeysnail のフルパスがわからなければ、

```
$ which xkeysnail
/usr/local/bin/xkeysnail
```


### 自動起動ファイルを書く

`~/.config/autostart/` に `xkeysnail.desktop` というテキストファイルを作る。

```text
[Desktop Entry]
Type=Application
Version=0.3.0
Name=xkeysnail
GenericName=Keymapper
Exec=/home/username/.xkeysnail/start.sh
Hidden=false
NoDisplay=false
X-GNOME-Autostart-enabled=true
```

ここでも `username` はあなたのユーザー名、`/home/username/.xkeysnail/start.sh` は start.sh をおいた場所。Ubuntu の GUI から Startup Applications Preferences をみつけて起動すると、xkeysnail が新たに加わっているはず。


設定完了。以降、起動すると自動的に xkeysnail が有効になっているはず。



## Reference

作者の記事: https://qiita.com/mooz@github/items/c5f25f27847333dd0b37
GitHub: https://github.com/mooz/xkeysnail
偉大な先人達: 
https://hidakatsuya.hateblo.jp/entry/2019/11/20/215608 and references therein.