⌨️

QMKで複数のキーボードのキーマップをuser spaceで一元管理する

2023/04/29に公開

自作キーボードにおいて複数のキーボードの管理を簡単にする、userspaceについてまとめます。
なお筆者はQMKで初めてC言語に触れたいち自キユーザであり、職業プログラマでもないため、解釈や用語の間違いがあれば遠慮なくフィードバックをいただけると助かります。

QMK firmwareにおけるuserspaceとは

いつも使うカスタムキーコードやユーザーマクロ、キーマップなんかを、まとめて一箇所で管理するのがuserspaceです。
日本語の情報をあまり見つけられなかったので、日本の自作キーボード界隈ではそんなに知られていないのかもしれません。
https://docs.qmk.fm/#/feature_userspace
新しいキーボードに自分のキーマップを設定する際、私はこれまで別のキーボードからキーマップをコピペしていました。userspaceをもっと早くに知っていればよかったと思い、この記事を書いています。

userspaceを使うとなにが嬉しいのか

userspaceを使うことによりkeyboard_name/keymaps/your_keymap/以下のファイルには最低限の記述をするだけで、オリジナルのキーマップを反映することができます。
キーマップだけでなく、マクロ、レイヤー、コンボ、タップダンスなど、keymap.cに書くコードはたいていuserspaceにまとめることができます。
複数のキーボードで似たようなキーマップを使っている場合は、コードを共有できるのは便利ですよね。

userspaceを作成する

ディレクトリの準備

下記の構造で、自分の名前(githubのユーザ名が推奨)のディレクトリ<name>を用意します。

  • qmk_firmware/users/<name>/
    • readme.md (任意)
    • rules.mk
    • config.h
    • <name>.h
    • <name>.c

最低限の雛形としてusers/_exampleが用意されています。
より高度な活用についてはusers/drashnaなどが参考になるでしょう。

keymap側の準備

userspaceにつけた名前と同じ名前のキーマップは、自動的にuserspaceを読み込みます。
それ以外のキーマップでuserspaceを利用したい場合は、keymaps/your_keymap/rules.mkで読み込むuserspaceを指定します。

keymaps/yourkeymap/rules.mk
USER_NAME := <name>

users/<name>の中身

rules.mk

<name>.cを読み込むようにしておきます。
それ以外は基本的にkeymaps/<name>/rules.mkと同じです。

rules.mk
SRC += <name>.c

config.h

こちらはkeymaps/<name>/config.hと全く同じになります。
TAPPING_TERMなどのハードによって変わらない設定を入れておくと良いでしょう。

<name>.h

これまでkeymaps/<name>/keymap.cに記述していた、カスタムキーコードやレイヤー名などの定義を記述します。
また、keymap.cprocess_record_などを利用するために必要なカスタム関数のプロトタイプ宣言もここで。

<name>.h
// Prototypes for keymap specific customized functions
bool process_record_keymap(uint16_t keycode, keyrecord_t *record);
void matrix_init_keymap(void);
void matrix_scan_keymap(void);
void keyboard_post_init_keymap(void);
layer_state_t layer_state_set_keymap(layer_state_t state);

// Defining names used in layer keycodes and the keymap
enum layer_names {
  _QWERTY = 0,
  _LOWER,
  _RAISE,
  _ADJUST
};

// Defining keycodes used in process_record_ function
enum custom_keycodes {
  EISU = QK_USER, // QMK 0.18以前なら SAFE_RANGE
  KANA,
  ADJUST,
  RGBRST
};

// Defining macro keycodes, wrappers etc used in keymap
#define LOWER MO(_LOWER)
#define RAISE MO(_RAISE)

<name>.c

<name>.cには、keyamp.cの内容のうちキーマップ配列以外を格納することができます。
すべてのキーボードで同じコードを共有する場合はそのまま転記で問題ありません。
keymap.cで上記の*_keymapカスタム関数を利用するためには、_userカスタム関数の最後に読み込ませるために以下のように記述する必要があります。

<name.c>
#include "<name>.h"

__attribute__ ((weak))
bool process_record_keymap(uint16_t keycode, keyrecord_t *record) {
  return true;
}

bool process_record_user(uint16_t keycode, keyrecord_t *record) {
  switch (keycode) {
	case EISU:
	    // 実際の処理
        break;
    ...
    ..
    
  }
  return process_record_keymap(keycode, record);
}

なおuserspaceで*_userカスタム関数を使用するため、keymap.cでは*_keymapを使用する必要があります。

キーマップを一括管理するwrapper

私がuserspaceを使い始めた一番の目的である、キーマップの一元管理を実現するのがwrapperです。
複数のキー配列をひとつのマクロに格納することで、keymap.cの記述を簡略化できます。

wrapperを定義する

wrapperは<name>.hの中で定義します。
マクロの定義は2種類の方法があるようです。

  1. n個のキーを格納する 9(n-1)+7 の長さを持つマクロを使う方法
    • レイアウトに配置したときに見た目が整って見える
    • 含まれるキー数によって長さが違う。間違えると展開されない
    • 定義するときに長さ合わせが面倒
  2. 通常のマクロとして定義する方法
    • マクロ名が含まれるキーの量に左右されないので作りやすい
    • レイアウトに配置したときに物理的なレイアウトと異なる見た目になる
    • 含まれるキーの数がマクロ名からわからない(マクロ名を工夫すればよい?)

例として、以下はQWERTY配列のマクロです。

1の例

<name.h>
     // 1......__2......__3......__4......__5......
#define _________________QWERTY_L1_________________ KC_Q, KC_W, KC_E, KC_R, KC_T
#define _________________QWERTY_L2_________________ KC_A, KC_S, KC_D, KC_F, KC_G
#define _________________QWERTY_L3_________________ KC_Z, KC_X, KC_C, KC_V, KC_B

#define _________________QWERTY_R1_________________ KC_Y, KC_U, KC_I, KC_O, KC_P
#define _________________QWERTY_R2_________________ KC_H, KC_J, KC_K, KC_L, KC_SCLN, KC_QUOT
#define _________________QWERTY_R3_________________ KC_N, KC_M, KC_COMM, KC_DOT, KC_SLSH

2の例

<name.h>
#define QWERTY_L1 KC_Q,    KC_W,    KC_E,    KC_R,    KC_T
#define QWERTY_L2 KC_A,    KC_S,    KC_D,    KC_F,    KC_G
#define QWERTY_L3 KC_Z,    KC_X,    KC_C,    KC_V,    KC_B

#define QWERTY_R1 KC_Y,    KC_U,    KC_I,    KC_O,    KC_P
#define QWERTY_R2 KC_H,    KC_J,    KC_K,    KC_L, KC_SCLN
#define QWERTY_R3 KC_N,    KC_M, KC_COMM,  KC_DOT,  KC_SLH

キーマップの配列をwrapperに対応させる

wrapperを使うためには、keymap.cでレイアウト配列を可変長にする必要があります。
新しくLAYOUT_wrapperを定義し、LAYOUT(__VA_ARGS__)を指定します。
LAYOUTの部分はキーボードによって異なる場合があります。適宜読み替えてください。

keymap.c
#define LAYOUT_wrapper(...) LAYOUT(__VA_ARGS__)

const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
  [0] = LAYOUT_wrapper(

wrapperを使ってキーマップを定義した例

wrapperを使うことで、キーマップの変更を一元管理できる以外にも、keymap.cの見通しが良くなるという利点があります。
例として、こちらは私のCharybdis nanoのキーマップです。

keymap.c

// wrapper 不使用
[_BASE] = LAYOUT(
KC_SLSH , KC_P , KC_O , KC_U , KC_J , KC_K , KC_D , KC_L , KC_C , KC_SCLN , \
LSFT_T(KC_A),LCTL_T(KC_N),LALT_T(KC_E),LGUI_T(KC_I),KC_Y, KC_M,RGUI_T(KC_H),RALT_T(KC_T),RCTL_T(KC_S),RSFT_T(KC_R), \
KC_Q , KC_Z , KC_QUOT , KC_MINS , KC_X , KC_B , KC_F , KC_G , KC_V , KC_W , \
RAISE , MT_SS , KC_BTN1 , KC_BTN2 , LOWER
),

// wrapper#1 使用
[_BASE] = LAYOUT_wrapper(
__________________BASE_L1__________________, __________________BASE_R1__________________, \
__________________BASE_L2__________________, __________________BASE_R2__________________, \
__________________BASE_L3__________________, __________________BASE_R3__________________, \
RAISE , MT_SS , KC_BTN1 , KC_BTN2 , LOWER
),

// wrapper#2 使用
[_BASE] = LAYOUT_wrapper(
BASE_L1, BASE_R1, \
BASE_L2, BASE_R2, \
BASE_L3, BASE_R3, \
RAISE , MT_SS , KC_BTN1 , KC_BTN2 , LOWER
),

このwrapperは配列の途中で使うこともできます。
上記は3x5配列ですが、crkbdなどの3x6に使う場合はこのように使えます。

keymap.c
  [0] = LAYOUT_split_3x6_3(
KC_TAB,  __________________BASE_L1__________________,            __________________BASE_R1__________________, KC_BSPC, \
KC_LCTL, __________________BASE_L2__________________,            __________________BASE_R2__________________, KC_QUOT, \
KC_LSFT, __________________BASE_L3__________________,            __________________BASE_R3__________________, KC_ESC, \
                                KC_LGUI, TL_LOWR,  KC_SPC,     KC_ENT, TL_UPPR, KC_RALT
  ),

おわりに

ここで書いたのはuserspace活用の入り口です。
/users/drashnaを始めとした実際の活用例を見ていただければ、より管理しやすい構成やここで紹介しきれなかった使い方を見つけることができるでしょう。

Discussion