Emacsでキーボードイベントを扱う方法まとめ

Emacsを使い始めると、誰でもあまり意識せずにキーボードイベントを扱っていると思います。
特にdefine-keyやlocal-set-keyなどのキーバインドを設定する際には、とりあえず人のコピペでちょっと変更すればなんとかなるし、kbdマクロを使ったり使わなかったりも私の場合は適当でした。
ですが、自分でelispを書き始めるとキーボードイベントをうまく扱う必要が出てきました。

もともとEmacsではキーボードイベントを(歴史的な背景から)複数のデータ形式で扱っていて、そのため複雑になっています。
http://www.bookshelf.jp/texi/elisp-manual-20-2.5-jp/elisp_21.html#SEC303にはキーボードイベントを扱うための方針が書いてあるのですが「とりあえずベクトルで扱っておいて必要に応じてリストに変換してね」ぐらいのことしか書いてないです。

やるべきことは全くその通りなんですが

  1. どの場合に変換する必要があるのか
  2. どの関数でどの形式からどの形式に変換できるのか

とかがさっぱりわかりません。

そこで今回、備忘録もかねてまとめてみました。

イベントを表すデータ形式

一つ目は、データ形式の一覧です。
「M-C-a」と「C-a」と「a」の連続するキーボードシーケンスをそれぞれのデータ形式で表しました。
単一イベントに関しては、シーケンスではなくlast-input-eventでとれる個々のイベントを参考にしてます。

種類 実際の例 補足
文字列 "\M-\C-a\C-aa" "\C-:"や"\M-\C-:" などはエラー
文字列b "M-C-a C-a a" "C-:"や"M-C-:" などもOK
リスト (134217729 1 97)
ベクトル [134217729 1 97]
単一イベント 134217729

注意すべきは文字列としての表現が2種類存在することと、文字列aに関しては表せる範囲について、制限があるということです。
そのため、範囲外のキーを含む場合は他のデータ形式で表す必要があり、そのため扱いが複雑になっています。

イベント列を入力とする関数/変数

二つ目はイベント列を入力とする関数と変数(unread-command-events)です。
変数については本来、入力も出力も関係ないですが、setqで値をセットして意味がありそうなものという意味で挙げました。
入力の「文字列a or ベクトル」というのは文字列aとベクトルどちらでも受け付けることを表しています。

関数/変数 入力
define-key 文字列a or ベクトル
key-binding 文字列a or ベクトル
describe-bindings 文字列a or ベクトル
lookup-key 文字列a or ベクトル
unread-command-events リスト

注意すべきはunread-command-eventsで、他の関数が「文字列a」と「ベクトル」を受け付けるのに対して「リスト」しか受け付けません。
unread-command-eventsを使用する場合は、次に説明する変換系の関数を使ってリストに変換してから使用しましょう。

イベント列を入力、出力とする関数(変換系の関数)

三つ目はイベントを入力として、イベント列を出力する関数(イベント列を変換する関数)です。
出力の「文字列a or ベクトル」というのはキーボードシーケンス内にメタキーやASCIIを含まない場合は文字列、含む場合はベクトルになります。
たとえば、「C-a」と「a」のシーケンスの場合は文字列"\C-aa"(印字されると"^Aa")、「C-:」と「a」のシーケンスの場合はベクトル[67108922 97]となります。

関数 入力 出力 補足
kbd,read-kbd-macro 文字列b 文字列a or ベクトル 
key-description 文字列a or ベクトル 文字列b
listify-key-sequence 文字列a or ベクトル リスト
vconcat 文字列a or ベクトル or リスト ベクトル (vconcat "\C-aa" '(1 2))
append 文字列a or ベクトル or リスト リスト (append "\C-aa" [1 2] nil)
vector 単一イベント ベクトル
list 単一イベント リスト
single-key-description 単一イベント 文字列b

文字列aかベクトルか分からないものをベクトルに統一するには、vconcatを使うとよいです。

文字列aかベクトルか分からないものをリストに統一するには、appendもしくはlistify-key-sequenceを使うとよいです。
使い分けとして、変換だけならlistify-key-sequence、ついでに要素を加えるときにはappendを使うといいと思います。

文字列bへの変換は使いどころが想像しづらいかもしれません。
すぐに思い浮かぶのはデバッグなど人が読むための表示に便利ですが、他にも文字列からシンボルを生成するときなど確実に文字列として変換しておきたいときに便利だと思います。

たとえば、https://github.com/uk-ar/key-combo:key-comboでは通常のようにキーの登録ができない(prefixとして使用するキー自体にはコマンドは登録できない)ので、仮想的なキーマップを作ってます。
そこでは、キーシーケンスをベクトルではなく文字列bから作ったシンボルで表しています。
(特殊な使い方なので、参考になるかどうか…)

イベント列を出力とする関数/変数

四つ目はイベント列を出力とする関数と変数です。
違いが分かりやすいように「C-:」と「a」のシーケンスを入力した場合の値(またはコマンドの起動に使用した場合)と「C-a」と「a」の場合の値も記載します。

関数/変数 出力 範囲外文字あり 範囲外文字なし 補足
read-event 単一イベント 67108922 or 97 1 or 97 入力待ち、値は複数回実行した場合
read-key-sequence 文字列a or ベクトル [67108922 97] "\C-aa" 入力待ち
read-key-sequence-vector ベクトル [67108922 97] [1 97] 入力待ち
last-input-event 単一イベント 97 97 read-eventなどで値が変わる
last-command-event 単一イベント 97 97 read-eventなどでも値が不変
this-command-keys 文字列a or ベクトル [67108922 97] "\C-aa"
this-command-keys-vector ベクトル [67108922 97] [1 97]

まとめ

キーボード入力を読み込むelispではread-key-sequence-vectorやthis-command-keys-vectorを使用しておけば、ベクトルに確定できるので内部で扱いやすいです。

ユーザーの設定を読み込むelispは表現できる範囲が広いため文字列bで表現できることが重要です。
そのためには(define-keyのように)文字列aもベクトルも受け付けるようにしておいてkbdマクロを使わせるか、直接文字列bを読み込み可能にして内部でread-kbd-macroなどを使って変換する方法があります。
どちらにしても変換後は文字列aで渡ってくる可能性が高いため、要素を加えるなどベクトルで扱いたい場合はvconcatで変換する必要があります。
(read-kbd-macro-vectorが欲しい…)

簡単にまとめるつもりが、長くなってしまいました…
極力動かしながらやってますが、間違いもあるかもしれないので、ツッコミをお待ちしております。
最後に一応動作を確認するためのコードも貼ってみます。

(local-set-key (kbd "C-a a")
                (lambda ()
                  (interactive)
                  (message "lce:%S tck:%S tckv:%S lie:%S"
                           last-command-event
                           (this-command-keys)
                           (this-command-keys-vector)
                           last-input-event)
                  (let ((re (read-event)))
                    (message "lce:%S tck:%S tckv:%S lie:%S re:%S"
                             last-command-event
                             (this-command-keys)
                             (this-command-keys-vector)
                             last-input-event
                             re)
                    )))

追記(2012/2/15)

基本的にベクトルで扱う前提で書きましたが、場合によってはリストで扱う方が便利な場合もあると思います。
理由はリストの方が操作関数が多く、簡潔に書けることがあるからです。
例えばキーの重複をなくしたいときにadd-to-listを使えたり、要素を先頭に追加するときconsやpushが使えます。