⌨️

QMKで疑似USキーマップ

に公開

会社から貸与されているWindowsラップトップは本体のキーボードが日本語配列のものですが、私は外付けで Keyball44 あるいは cocot46plus を繋げて英語配列で使うため、これまでOSの設定を英語配列(US)にして使っていました。

オフィス出社時に会議室へ移動するとき、PCと一緒に外付けキーボードを持ち運ぶのが億劫になると、キーの印字と論理配列の違いを脳内変換しつつPC本体のキーボードを使っていた(OSの設定を日本語配列に変えるのはOS再起動が必要で面倒なためやらない)のですが、若干の不便さを感じていました。

ふと、OSの設定は日本語配列(JIS)のまま、外付けキーボードの側で英語配列っぽく使えるようにキーマップを設定してやればいいんじゃないかと思い付いたので、実際にやってみたことを紹介します。

対処の概要

Keyball44 を例としてやった内容を説明していきます。ベースとなるキーマップは 僕のKeyball44のキーマップのその後を紹介します で紹介していたものです(keymaps配列のデフォルトレイヤ分を記事末尾に掲載しています)。

同じキーボードを私物のMacBookに繋ぐこともあり、そちらはUS配列のままでよいので、OS Detection を使い Windows の場合に Key Overrides を使って一部のキーの動作を上書きして擬似的にUS配列を実現します。

対処が必要な記号

まず作戦立てのためkeymap_japanese.hを眺めると、JIS配列とUS配列で配置が違う以下の記号の入力を考慮する必要があるとわかります。

@^&*():=+_'"`~\|[{]}

US配列で想定されるキー操作でこれらの記号が入力されるよう、Key Overridesの設定を入れていくのが基本方針です。

ただし私のキーマップの場合、デフォルトレイヤの Mod-Tap 利用箇所がこれらの記号と絡んでいたので、Key Overrides だけでは対処できず、デフォルトレイヤをもう1枚用意して起動時に切り替えることを合わせて行う必要がありました。詳しくは後ほど触れます。

では、単純なパターンから順に説明していきましょう。

Shift付加時の動作を置換

; は JP_SCLN = KC_SCLN なのでそれ自体は置換不要で、Shift付加時の Shift+; の動作を置換すればよいです。key_overridesに以下の設定をすればOKです。

keymap.c
#include "process_key_override.h"
#include "keymap_japanese.h"
const key_override_t **key_overrides = (const key_override_t *[]){
    &ko_make_basic(MOD_MASK_SHIFT, KC_SCLN, JP_COLN), // デフォルトレイヤで Shift を押しつつ KC_SCLN で : を入力
    NULL
};

Shift+数字の動作を置換

Shift+8はJIS配列だと ( が入力されますが、これをUS配列で期待される * に置き換えます。私のキーマップでは数字レイヤで Shift を付加した場合と、記号レイヤで Shift 付加したキーコードを発行する場合があるので、両者を置き換えます。

keymap.c (key_overrides)
    &ko_make_basic(MOD_MASK_SHIFT, KC_8, JP_ASTR), // 数字レイヤで Shift を押しつつ KC_8 で * を入力
    &ko_make_basic(0, S(KC_8), JP_ASTR), // 記号レイヤの S(KC_8) で * を入力

JP_ASTR の場合、S(JP_ASTR) = S(S(JP_COLN)) = S(JP_COLN) = JP_ASTR であり問題ないのですが、JP_AT と JP_CIRC については S(JP_AT) = JP_GRV != JP_AT、 S(JP_CIRC) = JP_TILD != JP_CIRC なので、記号レイヤでさらに Shift を付加した場合に別の文字が入力されてしまいます。記号レイヤで Shift を付加した場合もUS配列風にふるまってくれるよう、もうひとつ設定を加えます。

keymap.c (key_overrides)
    &ko_make_basic(MOD_MASK_SHIFT, KC_2, JP_AT), // 数字レイヤで Shift を押しつつ KC_2 で @ を入力
    &ko_make_basic(MOD_MASK_SHIFT, S(KC_2), JP_AT), // 記号レイヤで Shift を押しつつ S(KC_2) で @ を入力
    &ko_make_basic(0, S(KC_2), JP_AT), // 記号レイヤの S(KC_2) で @ を入力

JP_CIRC への置換についても JP_AT の場合と同様です。

Shiftなし、あり両方の置換

'" (Shift+') に対しては、まず数字レイヤでShiftなしの場合を JP_QUOT に置換しますが、Shift付加時の動作は別途置換するよう negative modifier mask に MOD_MASK_SHIFT を指定します。あとは Shift 付加時の動作置換を数字レイヤと記号レイヤの両方で効かせればOKです。

keymap.c (key_overrides)
    &ko_make_with_layers_and_negmods(0, KC_QUOT, JP_QUOT, ~0, (uint8_t) MOD_MASK_SHIFT), // 数字レイヤの KC_QUOT で ' を入力
    &ko_make_basic(MOD_MASK_SHIFT, KC_QUOT, JP_DQUO), // 数字レイヤで Shift を押しつつ KC_QUOT で " を入力
    &ko_make_basic(0, S(KC_QUOT), JP_DQUO), // 記号レイヤの S(KC_QUOT) で " を入力

Shift + Mod-Tap の置換

Mod-Tap になっているキーの扱いその1。RCTL_T(KC_MINS) については JP_MINS = KC_MINS ですが、 S(KC_MINS) != JP_UNDS なので Shift 付加時のみ置換が必要です。Shift を付加した場合については Mod-Tap の動作をしなくてよいと割り切って置換します。

keymap.c (key_overrides)
    &ko_make_basic(MOD_MASK_SHIFT, RCTL_T(KC_MINS), JP_UNDS), // _

Mod-Tap のタップ時のキーを置換

Mod-Tap になっているキーの扱いその2。デフォルトレイヤの LT(1,KC_EQL) については JP_EQL = S(JP_MINS) != KC_EQL でタップ時の動作の置換が必要ですが、key_overridesでは Mod-Tap を維持したまま置換することはできません。

Mod-Tap のドキュメントに process_record_user() でタップ動作を置き換える方法が書いてあるのでそれを応用して、ShiftなしありをそれぞれJP_EQLとJP_PLUSに置き換えます。

https://docs.qmk.fm/mod_tap#changing-tap-function

keymap.c
bool process_record_user(uint16_t keycode, keyrecord_t *record) {
    switch (keycode) {
        case LT(1,KC_EQL):
            if (key_override_is_enabled()) {
                if (record->tap.count) {
                    static uint16_t kc;
                    if (record->event.pressed) {
                        uint8_t mod_state = get_mods();
                        if (mod_state & MOD_MASK_SHIFT) {
                            // ShiftありをJP_PLUSに置換
                            del_mods(MOD_MASK_SHIFT);
                            kc = JP_PLUS;
                        } else if (mod_state & MOD_MASK_CTRL) {
                            // ここは次節の拡大操作のためのコード
                            del_mods(MOD_MASK_SHIFT);
                            kc = JP_SCLN;
                        } else {
                            // ShiftなしをJP_EQLに置換
                            kc = JP_EQL;
                        }
                        register_code16(kc);
                        set_mods(mod_state);
                        return false;
                    } else if (kc) {
                        unregister_code16(kc);
                        kc = 0;
                        return false;
                    }
                }
            }
            break;
        /* 以下略 */

Key Overrides の有効無効で疑似USキーマップの有効無効を切り替えているので、ここも key_override_is_enabled() が真の場合にキーの置換をしています。

Mod-Tapのドキュメント中のサンプルコードでは tap_code16(kc) を使っているのですが、それだと「タターン」とタップ+ホールドしてもキーリピートが効かなかったので register_code16(kc)unregister_code16(kc) を呼ぶようにしています。

(実装例の中に MOD_MASK_CTRL が効いていたときの処理が混じっていますが、次節の内容です)

https://zenn.dev/yoichi/articles/qmk-quick-tap-term

拡大縮小

Chrome などで Ctrl++, Ctrl+- で表示しているコンテンツの拡大縮小ができますが、+, - と同じキーに割り当てられているキーを使った場合も同様の動きをします。つまり、

  • JIS配列だと Ctrl+; は Ctrl++ と同じ、Ctrl+= は Ctrl+- と同じ動き
  • US配列だと Ctrl+= は Ctrl++ と同じ。Ctrl+_ は Ctrl+- と同じ動き

となります。疑似US配列では Ctrl+= は Ctrl++ と同じく拡大になって欲しいのですが、OSはJIS配列だと思っているので Ctrl+- と同じく縮小になってしまいます。

これもついでに対処しています。私のキーマップだと LT(1,KC_EQL) に適用する必要があったので実装例は前節のコードに含めています。

  • Slack だと拡大に割りあたっているのは Ctrl++ のみ(Ctrl+; は効かない)なので、Shift+Ctrl+= でそれを発行できるように
  • notepad だと拡大に割りあたっているのは Ctrl+; のみ(Ctrl++ は効かない)なので、Ctrl+= でそれを発行できるように

となるよう、MOD_MASK_SHIFTがあればJP_PLUSに読み替え、MOD_MASK_CTRLがあればJP_SCLNに読み替えの順で条件分岐させています。

CapsLock

あまり使うことはないですが、一応置換しておきます。

keymap.c (key_overrides)
    &ko_make_basic(0, KC_CAPS, JP_CAPS), // (CapsLock)

IMEトグル

QK_USER_0 に Alt + ` 相当の動作をさせていたのを、単に JP_ZKHK を発行するだけに変更しました。

keymap.c
bool process_record_user(uint16_t keycode, keyrecord_t *record) {
    switch (keycode) {
        case QK_USER_0:
            switch (detected_host_os()) {
            case OS_WINDOWS:
                if (record->event.pressed) {
                    if (key_override_is_enabled()) {
                        // OSがJIS配列設定の場合、全角/半角
                        tap_code16(JP_ZKHK);
                    } else {
                        // OSがUS配列設定の場合、Alt+'
                        register_code16(KC_RALT);
                        wait_ms(10);
                        tap_code16(KC_GRV);
                        unregister_code16(KC_RALT);
                    }
                }
                return false;
            case OS_MACOS:
                /* 以下略 */

まとめ

Windows環境でOSの設定はJIS配列とした状態で、外付けのキーボードをUS配列っぽく使えるようにしてみました。本体のキーボードはキーの印字の通りJIS配列として使えるので、オフィス出社したときの会議室移動での不便さが減ると期待しています。

Keyball44 で使っている実際のコードを以下に載せておきます。

Keyball44 での実装例

OS 検出による動作切り替え

keymap.c
uint32_t os_detect_callback(uint32_t trigger_time, void *cb_arg) {
    switch (detected_host_os()) {
        case OS_WINDOWS:
            key_override_on();
            break;
        case OS_MACOS:
            key_override_off();
            break;
        default:
            break;
    }
    return 0;
}

void keyboard_post_init_user(void) {
    defer_exec(100, os_detect_callback, NULL);
}

key_overrides

keymap.c
const key_override_t **key_overrides = (const key_override_t *[]){
    &ko_make_basic(MOD_MASK_SHIFT, KC_2, JP_AT), // @
    &ko_make_basic(MOD_MASK_SHIFT, S(KC_2), JP_AT), // @
    &ko_make_basic(0, S(KC_2), JP_AT), // @

    &ko_make_basic(MOD_MASK_SHIFT, KC_6, JP_CIRC), // ^
    &ko_make_basic(MOD_MASK_SHIFT, S(KC_6), JP_CIRC), // ^
    &ko_make_basic(0, S(KC_6), JP_CIRC), // ^

    &ko_make_basic(MOD_MASK_SHIFT, KC_7, JP_AMPR), // &
    &ko_make_basic(0, S(KC_7), JP_AMPR), // &

    &ko_make_basic(MOD_MASK_SHIFT, KC_8, JP_ASTR), // *
    &ko_make_basic(0, S(KC_8), JP_ASTR), // *

    &ko_make_basic(MOD_MASK_SHIFT, KC_9, JP_LPRN), // (
    &ko_make_basic(0, S(KC_9), JP_LPRN), // (

    &ko_make_basic(MOD_MASK_SHIFT, KC_0, JP_RPRN), // )
    &ko_make_basic(0, S(KC_0), JP_RPRN), // )

    &ko_make_basic(MOD_MASK_SHIFT, KC_SCLN, JP_COLN), // :

    // LT(1,KC_EQL) is handled in process_record_user

    // we can apply overrides but loose RCTL_T() effect
    &ko_make_basic(MOD_MASK_SHIFT, RCTL_T(KC_MINS), JP_UNDS), // _

    &ko_make_with_layers_and_negmods(0, KC_QUOT, JP_QUOT, ~0, (uint8_t) MOD_MASK_SHIFT), // '
    &ko_make_basic(MOD_MASK_SHIFT, KC_QUOT, JP_DQUO), // "
    &ko_make_basic(0, S(KC_QUOT), JP_DQUO), // "

    &ko_make_with_layers_and_negmods(0, KC_GRV, JP_GRV, ~0, (uint8_t) MOD_MASK_SHIFT), // `
    &ko_make_basic(MOD_MASK_SHIFT, KC_GRV, JP_TILD), // ~
    &ko_make_basic(0, S(KC_GRV), JP_TILD), // ~

    &ko_make_with_layers_and_negmods(0, KC_BSLS, JP_BSLS, ~0, (uint8_t) MOD_MASK_SHIFT), // (backslash)
    &ko_make_basic(MOD_MASK_SHIFT, KC_BSLS, JP_PIPE), // |
    &ko_make_basic(0, S(KC_BSLS), JP_PIPE), // |

    &ko_make_with_layers_and_negmods(0, KC_LBRC, JP_LBRC, ~0, (uint8_t) MOD_MASK_SHIFT), // [
    &ko_make_basic(MOD_MASK_SHIFT, KC_LBRC, JP_LCBR), // {
    &ko_make_basic(0, S(KC_LBRC), JP_LCBR), // {

    &ko_make_with_layers_and_negmods(0, KC_RBRC, JP_RBRC, ~0, (uint8_t) MOD_MASK_SHIFT), // ]
    &ko_make_basic(MOD_MASK_SHIFT, KC_RBRC, JP_RCBR), // }
    &ko_make_basic(0, S(KC_RBRC), JP_RCBR), // }

    &ko_make_basic(0, KC_CAPS, JP_CAPS), // (CapsLock)

    NULL
};

keymaps

keymap.c
const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
  // default layer (US)
  [0] = LAYOUT_universal(
 LT(2,KC_TAB), KC_Q     , KC_W     , KC_E    , KC_R     , KC_T     ,                                         KC_Y     , KC_U     , KC_I     , KC_O     , KC_P     , LT(1,KC_EQL),
LCTL_T(KC_ESC),KC_A     , KC_S     , KC_D    , KC_F     , KC_G     ,                                         KC_H     , KC_J     , KC_K     , KC_L     , KC_SCLN  , RCTL_T(KC_MINS),
    KC_LSFT  , KC_Z     , KC_X     , KC_C    , KC_V     , KC_B     ,                                         KC_N     , KC_M     , KC_COMM  , KC_DOT   , KC_SLSH  , KC_RSFT  ,
                  KC_LALT  , KC_LGUI , KC_BTN1  ,    LT(1,KC_SPC)  , MO(3)    ,                   KC_BSPC,LT(2,KC_ENT), _______       , _______  , KC_RGUI
  ),

  /* 以下略 */

Discussion