はじめに
自分好みのショートカットキーを設定する際、キーボードの左右のAltキー (Alt_L, Alt_R) を区別して使いたいときがあります。例えば、「Alt_L + m」でChromeを起動して、「Alt_R + m」ではTerminalを起動するといった具合です。
X11で動くアプリケーションを開発する際に、この機能を実装しようとしたところ、一手間工夫が必要でした。X11のキーイベントだけでは左右のAltを区別できないためです。
本記事では、この問題を解決する方法として、QueryKeymap を使ったアプローチを紹介します。また、X11におけるキーボード処理の基礎となる ModMask, KeyCode, KeySym という3つの概念と、キーイベントだけでは区別が難しい理由についても触れます。
本記事では、X11 Protocol, X Window System関連の用語を総称して「X11」と表記します。
結論: QueryKeymapを使う
X11で左右のAltキーを区別する一つの方法として、QueryKeymapリクエスト (Rustのx11rbではquery_keymap()関数) を使うアプローチがあります。
これは、現在押されている全てのキーについて、Press/Releaseの状態を取得できるものです。その状態は32バイトのビットベクトルで表現されており、各ビットが1つのKeyCodeに対応しています。
X11のキーイベント (たとえば Alt+mのKeyPress) では、「Altが押された状態でmが押された」といった情報しか得られません。しかし、query_keymap()を使えば、全キーのPress/Release状態を取得できるので、「Altのうち左のAltが押された状態でmが押された」という事がわかるのです。
下記のように、Alt_L (通常KeyCode 64) と Alt_R (通常KeyCode 108) のビットを個別にチェックすることで区別が可能になります。
// Rustとx11rbを使った実装例
let reply = conn.query_keymap()?.reply()?;
let keys = reply.keys; // 32バイトのビットベクトル
// Alt_LのKeyCode (通常64) とAlt_RのKeyCode (通常108) のビットをチェック
let alt_l_pressed = is_key_pressed(&keys, 64);
let alt_r_pressed = is_key_pressed(&keys, 108);
なぜキーイベントだけでは区別できないのか
結論を書くと、「Alt+mのようなキーコンビネーションを受け取るキーイベント (mのKeyPress) には、Modifierの情報はModMaskしか含まれないから」です。
X11のキーボード処理を理解するには、ModMask, KeyCode, KeySym という3つの概念を知っておくのが大事です。
以下では、それぞれについて説明しつつ、問題について書きます。
ModMask
キーボードには、修飾キー (Modifier) が存在します。他のキーと組み合わせて使用されるキーのことで、代表的なものは、Shift、Control、Altです。
X11では、このようなModifierをModMaskと呼ばれるビットマスクで表現します (X11 Protocol: Common Types)。各Modifierに1ビットが割り当てられており、そのキーが押されているかどうかを示します。
ShiftMask = 0x0001 (2進数: 00000001)
LockMask = 0x0002 (2進数: 00000010)
ControlMask = 0x0004 (2進数: 00000100)
Mod1Mask = 0x0008 (2進数: 00001000) ← 通常はAlt
Mod2Mask = 0x0010 (2進数: 00010000)
Mod3Mask = 0x0020 (2進数: 00100000)
Mod4Mask = 0x0040 (2進数: 01000000)
Mod5Mask = 0x0080 (2進数: 10000000)
(ビットマスクにすることで、複数のキーが同時に押されている状況をビット演算で表現できて、なにかと便利なのだと思います。)
ここで重要なポイントは、Alt_LもAlt_Rもどちらも同じMod1Maskとして扱われる ということです。
KeyCode (物理キー)
KeyCodeは、物理的なキーを表す番号です。
X11 Protocol: Keyboardsによると、KeyCodeの特徴は以下の通りです。
A KEYCODE represents a physical (or logical) key. Keycodes lie in the inclusive range [8,255]. A keycode value carries no intrinsic information, although server implementors may attempt to encode geometry information (for example, matrix) to be interpreted in a server-dependent fashion. The mapping between keys and keycodes cannot be changed using the protocol.
- 8から255の整数値
- 物理的なキーに対応
- KeyCode自体には意味的な情報は含まれない
- X11サーバーの実装に依存する
重要なのは、このレベルではAlt_LとAlt_Rは区別される ということです。例えば、標準的なLinux環境では以下のようになるはずです。
- Alt_LのKeyCode: 64
- Alt_RのKeyCode: 108
(これらの値はxevコマンドで実際に確認できます。)
Altを押した状態でmを押した状況を考えます。まずAlt_LのKeyPressイベント、次にmのKeyPressイベントが順番に発生します。mのKeyPressイベントには、mのKeyCode (58) は含まれますが、Alt_LのKeyCode (64) は含まれません。含まれるのは「Altが押されている」というModMask情報だけです。
KeySym (論理的な記号)
KeySym (Key Symbol) という概念を説明します。X11 Protocol: Keyboardsによると、KeySymはキーキャップに刻印された記号を符号化したものとあります。
A KEYSYM is an encoding of a symbol on the cap of a key. The set of defined KEYSYMs include the character sets Latin-1, Latin-2, …, and Korean as well as a set of symbols common on keyboards (Return, Help, Tab, and so on).
要はKeyCodeに対して割り当てられる論理的な記号名 (a, B, Returnなど) のことで、Alt_LとAlt_Rも、それぞれにKeySymがあり、対応するKeyCodeを持っているということです。
問題の整理
ここまでの内容を整理すると、以下のようになります。
- Alt+mを押すと、Alt_LのKeyPressとmのKeyPressという2つのイベントが発生する
- mのKeyPressイベントには、mのKeyCodeのみが含まれ、Alt_LのKeyCodeは含まれない
- mのKeyPressイベントのstateフィールドには「Altが押されている」というModMask情報しかなく、Alt_LとAlt_Rを区別できない
- Alt_LとAlt_Rは実際には異なるKeyCode (64と108) を持っている
ここで工夫をすれば、「Alt_LとAlt_Rを個別に判定する方法」が実現できそうな気がしてきますよね。
QueryKeymapで左右Altキーを判定する
QueryKeymapの仕組み
X11 Protocolには、現在押されている全てのキーの状態を取得するためのQueryKeymapリクエストがあります (X11 Protocol: QueryKeymap)。
以下では、Rustのx11rbライブラリにあるquery_keymap()関数を使って、X11アプリケーション (WindowManager) 側で左右のAltキーを判定する方法について書きます。
WindowManagerでの実装例
以下のような流れで左右のAltキーを区別しています。
- X11からKeyPressイベント (mキー) を受信
query_keymap()でその瞬間のキーボード全体の状態を取得- Alt_L (KeyCode 64) とAlt_R (KeyCode 108) のビットをチェック
- どちらが押されているかに応じて異なるアプリケーションを起動
fn handle_key_press(
conn: &impl Connection,
event: KeyPressEvent,
alt_l_keycode: u8,
alt_r_keycode: u8,
) -> Result<(), Box<dyn std::error::Error>> {
// mキー (KeyCode 58) が押された場合
if event.detail == 58 {
// その瞬間のキーボード状態を取得
let reply = conn.query_keymap()?.reply()?;
// Alt_LまたはAlt_Rが押されているかチェック
if is_key_pressed(&reply.keys, alt_l_keycode) {
// 左Alt + m → Chromeを起動
std::process::Command::new("google-chrome").spawn()?;
} else if is_key_pressed(&reply.keys, alt_r_keycode) {
// 右Alt + m → Terminalを起動
std::process::Command::new("terminal").spawn()?;
}
}
Ok(())
}
// KeyCodeに対応するビットをチェック
fn is_key_pressed(keys: &[u8; 32], keycode: u8) -> bool {
let byte_index = (keycode / 8) as usize;
let bit_position = keycode % 8;
(keys[byte_index] & (1 << bit_position)) != 0
}
ビットチェックの仕組み
is_key_pressed関数では、以下の計算でKeyCodeに対応するビットをチェックしています。
let byte_index = (keycode / 8) as usize; // どのバイトか
let bit_position = keycode % 8; // バイト内の何ビット目か
(keys[byte_index] & (1 << bit_position)) != 0 // ビットが立っているか
例えば、Alt_L (KeyCode 64) の場合:
byte_index = 64 / 8 = 8bit_position = 64 % 8 = 0keys[8]の0ビット目をチェック
この方法で、任意のKeyCodeの押下状態を確認できます。
他に方法はないのか
他のアプローチとして、以下のような方法も考えられます。
- アプリケーション側での状態管理
Alt_LとAlt_RのKeyPressイベントを監視し、アプリケーション側で「どちらのAltが現在押されているか」を状態として保持する方法です。状態の不整合が起きないように、上手く管理する必要がありそうです。
- xmodmapで別Modifierにマッピング
xmodmapなどのツールを使って、Alt_LとAlt_Rを別々のModifierにマッピングする方法です。この場合、query_keymap()を使わずにModMaskだけで区別できます。しかし、システム全体の設定を変更するため影響範囲が大きそうです。
QueryKeymapのアプローチは、アプリケーション単体で完結し、状態管理の複雑さもありません。キーイベントの度にリクエストするコストはかかりますが、実用上のパフォーマンス問題はなかったため、このアプローチを採用しました。
まとめ
X11で左右のAltキーを区別する方法として、QueryKeymapリクエストを使うアプローチを紹介しました。
キーイベントだけではModMaskレベルの情報しか得られませんが、query_keymap()を使えばKeyCodeレベルで全キーのPress/Release状態を取得できるため、左右の修飾キーを個別に扱うことができます。
他のウィンドウマネージャーでは、左右の修飾キーの区別をどのように実装しているのか、気になるところです。