Skip to content
Go back

X11で左右のAltキーを区別する

はじめに

自分好みのショートカットキーを設定する際、キーボードの左右の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.

重要なのは、このレベルではAlt_LAlt_Rは区別される ということです。例えば、標準的なLinux環境では以下のようになるはずです。

(これらの値は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_LAlt_Rも、それぞれにKeySymがあり、対応するKeyCodeを持っているということです。

問題の整理

ここまでの内容を整理すると、以下のようになります。

  1. Alt+mを押すと、Alt_LのKeyPressとmのKeyPressという2つのイベントが発生する
  2. mのKeyPressイベントには、mのKeyCodeのみが含まれ、Alt_LのKeyCodeは含まれない
  3. mのKeyPressイベントのstateフィールドには「Altが押されている」というModMask情報しかなく、Alt_LとAlt_Rを区別できない
  4. 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キーを区別しています。

  1. X11からKeyPressイベント (mキー) を受信
  2. query_keymap() でその瞬間のキーボード全体の状態を取得
  3. Alt_L (KeyCode 64) とAlt_R (KeyCode 108) のビットをチェック
  4. どちらが押されているかに応じて異なるアプリケーションを起動
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) の場合:

この方法で、任意のKeyCodeの押下状態を確認できます。

他に方法はないのか

他のアプローチとして、以下のような方法も考えられます。

  1. アプリケーション側での状態管理

Alt_LとAlt_RのKeyPressイベントを監視し、アプリケーション側で「どちらのAltが現在押されているか」を状態として保持する方法です。状態の不整合が起きないように、上手く管理する必要がありそうです。

  1. xmodmapで別Modifierにマッピング

xmodmapなどのツールを使って、Alt_LとAlt_Rを別々のModifierにマッピングする方法です。この場合、query_keymap()を使わずにModMaskだけで区別できます。しかし、システム全体の設定を変更するため影響範囲が大きそうです。

QueryKeymapのアプローチは、アプリケーション単体で完結し、状態管理の複雑さもありません。キーイベントの度にリクエストするコストはかかりますが、実用上のパフォーマンス問題はなかったため、このアプローチを採用しました。

まとめ

X11で左右のAltキーを区別する方法として、QueryKeymapリクエストを使うアプローチを紹介しました。

キーイベントだけではModMaskレベルの情報しか得られませんが、query_keymap()を使えばKeyCodeレベルで全キーのPress/Release状態を取得できるため、左右の修飾キーを個別に扱うことができます。

他のウィンドウマネージャーでは、左右の修飾キーの区別をどのように実装しているのか、気になるところです。

参考


Share this post on:

Previous Post
dotfilesで開発環境を管理する
Next Post
AIに作らせたコードからElispを学ぶ