Tomoya's Blog (ja)

Rarely updated ;)

ErgoDoxにVim風の操作を覚えさせてみた

2017-12-11

この記事は CAMPHOR- Advent Calendar 2017 12日目の記事です.


ある日,京都市内某所 で「一度Vimを使ってしまうと普通のエディタには戻れなさそう」なんて話があったが,僕も実はそのような人に含まれてしまう.Vim以外の文字入力画面でついVimのコマンドを打ってしまい,その都度「ああ,Vimじゃなかった?」と思うのである. そこで,社会復帰を目指して Vim以外で文字入力するときの快適性を追い求めて,少し前に に買ったErgoDox EZに,ファームウェアの限界までVimキーバインドに対応するキープレスを吐かせてみた.

基礎知識

ErgoDox EZに乗っているqmkファームウェアには,今回実現したいことに使えそうな,以下のような機能がある.

レイヤー

qmkファームウェアには「レイヤー」の概念がある.これは,キーマップを層状に定義し,それらの層を重ねたときに最も上にあるキーコードを,そのキーを押したときにマシンに送信されるキーコードとするものである.モーダルな入力の実現に使えそう.

僕のキーボードではレイヤー0がANSI配列に似た何か,レイヤー1が記号類を集めたもの,レイヤー2がマウス操作,レイヤー3がゲーム用と埋まっており,Vim風操作の実現に使えるのはレイヤー4以降となる.

Tap Dance

あるキーを何回連続して押すかで,マシンに送られるキーコードを変化させるqmkの機能である."dd"など,行を対象とするVimのコマンドを実現するときに役立ってくれそう.

Leader Key

Vimの":"のように,そのキーを押した後に続くキープレスがコマンドとなるものである.":w" などの実現に必要なのは言わずもがな.

簡単なやつ

移動

これは,Vim風入力を行うレイヤーにて,キーボード上の本来はh, j, k, lの位置にあるキーに,KC_LEFT , KC_DOWN , KC_UP , KC_RIGHT を順番に割り当てて完成.

i, a

これらは,基本的にはVim風入力を行うレイヤーから脱却する命令をマクロで割り当てれば良い.具体的には

iの場合

iの位置にあるキーに,レイヤー0に戻るキーコードを割り当てるだけ.

aの場合

SEND_STRING (SS_TAP(X_RIGHT));
layer_off(4);

と,カーソルを1文字右にずらしてからレイヤー4を無効にする (== レイヤー0に戻る) だけ.コードの解説をすると,SEND_STRING() は引数の文字を送信する関数 (型マクロ) で,その中の SS_TAP(X_RIGHT) が,文字列として表せない特殊なキープレスを送る仕組み,その仕組みを使って右キーを送ったものである.qmkのマクロの使用は マクロのドキュメント をご参照あれ.

o

これは,行末に移動し,改行し,レイヤー4を解除するだけ.コードは省略.

と,ここまではシンプル.

すこし複雑

ここから複数キーの同時押しが出てきて少し複雑になる.うーん,ちょっと考えたらできそうだ.

I, A

これらは,iやaに行頭・行末への移動を追加しただけのものであるが,一点注意が必要だった.qmkの使用上複数のキーを同時に押下したときに1つのコマンドを実行するようには登録できない (複数キーから1つのコマンドへの射は不可) なのでVim風コマンドに使用しているレイヤー4ではShiftキー (僕のレイアウトでは親指のキーの一つ) を,それを押さえている間はレイヤー5が有効になるキーに割り当て直す必要があった.この元で,マクロ内で

  • 適切にカーソルを移動 (X_HOME,X_ENDなど)
  • レイヤー5,レイヤー4の順で無効化

を行い,レイヤー5のi, aの位置に割り当てることでI, Aの動作が実現できた.

O

これはoと違い,現在の行の1行上に空行を挿入し,更に挿入モードにするコマンド.つまり,カーソルを行頭に移し,改行し,カーソルを1行上に動かすことで実現できる.I, Aと同じく,レイヤー5のoキーの位置にこのマクロを割り当てた.

状態の概念が出てきた

これまでのコマンドはいつ実行しても動作が同じだったが,ここからは直前の状態や今の状態によって動作が変わり,複雑になる.無事に再現できるのだろうか...

v

Visual modeにするあれ.ずばり,

SEND_STRING (SS_DOWN(X_LSHIFT));
SEND_STRING (SS_TAP(X_LEFT));
is_visual_mode = true;

Shiftを押さえたままにし,カーソルを1文字右に移動する.さらに,Visual modeであることを示す内部のフラグを立てる.このフラグを

厳密にはVisual mode内からiは効果を持たないが,ここではフラグが立ったままレイヤー4に戻ってしまう.見逃して...って残念なほどにバグっているけど,これは今後の課題.

V

うーん.行移動をするたびに,行き先の行全体を選択範囲に追加する挙動を実現しなければならない.問題はカーソルが行の中央にあるときにVisual line modeに入ったときで,カーソルを行頭に動かしてからShiftを押さえ,行末に移動させて行を選択すると,下の行に移動するときはちゃんとスタートした行を含めすべての行が選択されるが,上に移動したときはスタートした行の選択が外れる.開始時の動きを行末->行頭とすると,上下が入れ替わってやはり同じ問題が発生する.多分スタートの行をまたいで行き来するときに最初のShiftを押さえる前後の行頭・行末の移動を適切にやり直せばいいのだろうが,今回は時間的制約により割愛.悔しい.

Visual mode, Visual line modeからの脱却

フラグを下ろし,Shiftが押しっぱなしになっていたのを解除する (SS_UP(X_LSHIFT)) ようなマクロをEscキーに登録して対応.

x, y

これはNormal modeであるか,Visual系のモードであるかで動作が変わる.

Normal mode

SEND_STRING (SS_DOWN(X_LSHIFT));
SEND_STRING (SS_TAP(X_RIGHT));
SEND_STRING (SS_UP(X_LSHIFT));
SEND_STRING (SS_LCTRL("x"));
SEND_STRING (SS_TAP(X_LEFT));
is_clipboard_visual_line = false;

Shiftを押さえながら右にカーソルを移動し1文字選択し,その状態でCtrl+Xを送信.

Visual系のモード

Visualモードにあるということは何かしらの文字列が選択されている状態なので,

SEND_STRING (SS_UP(X_LSHIFT));
SEND_STRING (SS_LCTRL("x"));
is_visual_mode = false;
if (is_visual_line_mode) {
  is_clipboard_visual_line = true;
} else {
  is_clipboard_visual_line = false;
}

で行けるはず.ポイントとして,コピーしたときに行単位でコピーしたか否かを保持しているが,これによってpやPの挙動が変わってくるので結構重要.

いずれの場合も,yのときはCtrl+XをCtrl+Cに変更するだけ.いや,厳密にはVisualモードから抜けたときのカーソルの位置を合わせるために移動方向を保持しなければならない.これでもLinux以外ではShift+矢印キーで範囲選択をしているときにShiftを離したときの挙動が変わりそうだから,もっと複雑になりそう... Visual modeは闇だった.

p

これもややこしい.今クリップボードに入っている内容がVisual line modeで切り取られたか,それ以外で切り取られたかで挙動が変わる.この状態は,ちゃんと保存してあるので大丈夫 (is_clipboard_visual_line なんてものがあったよね)

Visual line modeでない場合

カーソルを右に1文字動かして,Ctrl+Vで実現.pは今の文字の次の文字の位置から貼り付けを開始するので.

Visual line modeである場合

SEND_STRING (SS_TAP(X_HOME)SS_TAP(X_ENTER));
SEND_STRING (SS_LCTRL("v"));

oの空行挿入の処理のあとに,その位置に貼り付ける.

また,Pは,pで右,下としたところをそれぞれ上,左とするだけで実現可能.

繰り返しを要するコマンド

Vimのコマンドで,行単位ではたらくコマンドは当該のキーを2回連続で押すことで発動される.それらの実現にTap Danceを使用した.

Tap Danceの有効化

ドキュメント に従って,レイアウトの rules.mk ファイルに TAP_DANCE_ENABLE = yes を追記し, config.h を作り #define TAPPING_TERM 200 とタップを検知するWindowの長さを定義した. 次に,以下のようなenumとarray定義

enum tap_dance_keycodes {
  VIM_DD,
  VIM_YY
};

qk_tap_dance_action_t tap_dance_actions[] = {
  [VIM_DD] = ACTION_TAP_DANCE_FN (vim_dd),
  [VIM_YY] = ACTION_TAP_DANCE_FN (vim_yy)
};

後はキーレイアウトにキーコードを書くところに,TD(VIM_DD) といった具合に書いてやるといいみたいだ.

dd

void vim_dd(qk_tap_dance_state_t *state, void *user_data) {
  if (state->count == 2) {
    SEND_STRING (SS_TAP(X_HOME));
    SEND_STRING (SS_DOWN(X_LSHIFT));
    SEND_STRING (SS_TAP(X_END));
    SEND_STRING (SS_UP(X_LSHIFT));
    SEND_STRING (SS_TAP(X_HOME));
    SEND_STRING (SS_DOWN(X_LSHIFT));
    SEND_STRING (SS_TAP(X_END));
    SEND_STRING (SS_UP(X_LSHIFT));
    SEND_STRING (SS_LCTRL("c"));
    SEND_STRING (SS_TAP(X_HOME));
    is_clipboard_visual_line = true;
    SEND_STRING (SS_LCTRL("x"));
    SEND_STRING (SS_TAP(X_HOME));
    is_clipboard_visual_line = true;
  }
}

タップ数がちょうど2なら現在の行をコピー, is_clipboard_visual_line = true; も忘れずに行う. SEQ_ONE_KEY(KC_W) { SEND_STRING (SS_LCTRL("s")); }

yy

こちらはyの動作も一緒に入れる必要があるからもう少し長い.と言ってもすでにあるものの組み合わせ.

void vim_yy(qk_tap_dance_state_t *state, void *user_data) {
  if (state->count == 1) {
    /* yのところで書いたコード */
  } else if (state->count == 2) {
    /* ddのコードの s/"x"/"c"/gをしたもの */
  }
}

":"から始まるコマンド

最後に,Leader Keyの機能を使って":"から始まるコマンドを実装.準備としてドキュメント に従ってLeader Keyとするキーを選択する.":"にしたいので,レイヤー5の";"の位置のキーにする.ドキュメントにある matrix_scan_user() はこのとき追加した.なんだこりゃ?

:w

SEQ_ONE_KEY(KC_W) {
  SEND_STRING (SS_LCTRL("s"));
}

Leader Keyの直後にwを押すと,Ctrl+Sを入力するマクロが走るという,簡単なもの.

ついでのoやeも同様に定義.

成果物

このブログ記事の公開時点で,レイアウトファイルのレポジトリ のmasterにこれらの変更が反映されている.使ってみたいって人は使ってみてね.

本当は動作結果を動画にまとめて載せるべきなのだろうが,ちょっと面倒なんで... よく前述の某所 に持ってきているので,触ってみたい人は一度来てみてくださーい.

まとめ

これで一般的な文字入力画面でよく使用するVim風の操作が行えるようになったから,Wordも使えるし,GitHubのIssueにコメントも書けるし,Wordpressでブログの記事も書けるし,Google検索もストレスフリーに行えるし,無事に社会復帰できそうである...ってむしろ社会不適合が悪化しているのではないだろうか.

あ,Visual系のモードで範囲選択した状態での貼り付けの挙動が間違っている.は〜,まだまだ道は長い...


追記 2018-10-02: qmkのドキュメントへのリンクが入れていたので修正した.