40%未満キーボード Collide39 を作った話

ここ数か月、自作キーボードの沼にはまってしまい、meishi, crkbd, Gherkin, Collide39, cocoa40, Reviung39s と制作してみたが、自分的に Collide39 がかなりお気に入りなので、キーマップの工夫なども含めて記事にしてみる。

f:id:pragma666:20191005170721j:plain

手前から Gherkin, Collide39, Reviung39s


Collide39 は space cat design で販売されている 40% 未満キーボード(30% か、と言われると微妙である)で、Gherkin にマクロパッドを付けるのがコンセプトのキーボードとして設計された、っぽい。
https://spacecat.design/collections/pcbs-cases-kits/products/collide39-pcb-kits
crkbd では多い、Gherkin では少ない、miuni32 ならいけるかもしれないけど、どこで買えばいいか分からない、と考えあぐねいていたところ候補にあがり購入した。Gherkin + マクロパッド的な利用ではなく、左右 6 列× 3 行、B と N の間にスペースを置いた、少し特殊な配列のキーボードとして利用することを考えていた。が、これが私的に正解で、crkbd、Reviung39 といった有能なキーボードを差し置いてメインのキーボードとして数カ月間、仕事を支えていれている。
尚、ケースは strata kb で購入出来る。
https://stratakb.com/store/cases/collide39-case

制作自体は特筆することは無く、表面実装も無いことから簡単な部類に入ると思う。pro micro を表向きに付けないとならない点が唯一のネックだろうか。キースイッチは当初 Gateron の黒軸を付けていたが、最終的に Zeal PC の Zilent v2 67g に落ち着いた。キーキャップは GMK Paperwork。

f:id:pragma666:20191014163952j:plain

さて、軸やキーキャップのカタログを見てカスタマイズの夢想をする時間や、遊舎工房、TALP 等のホームページ/Twitter/実店舗を巡回する以上に、自作キーボードの世界で割く必要のある時間があるとすれば、それはキーマップの設計、妄想である(私の場合)。普通のノート PC も仕事で使うため、あまりに特殊な配列にはしたくないし、10年近く使っていた KINESIS advantage のような使用感も欲しい。最初に買い求めた crkbd より延々と試行錯誤した結果、今は以下のコンセプトのキーマップに落ち着いた:

  • enter, bs は ctrl と組み合わせて実現する(ctrl + m, ctrl + h)。このため、通常の ctrl キーは "コントロールレイヤ" キーとなる
  • 通常の ctrl キーを使う場合はコントロールレイヤキーを素早く2回押す
  • raise レイヤを廃止し、lower(数字、一部記号)のみとする。入力のために2つレイヤがあるのは私的に難し過ぎた。従来 raise で入れていた記号は lower + shift で入れる(lower は raise との対義語なので、fn レイヤ、と名前を改めたが、sym のほうがしっくりくるかもしれない)
  • カーソルキーは fn ではなくコントロールレイヤに置く。意味的にカーソル移動は「コントロール」だから
  • フォームを保つために左右にシフトを設ける。以前の私は左シフトしか使わない人間だったが、右シフトもきちんと使うよう矯正した経緯がある
  • マイナスキーはよく使うので、p の隣に置く(これは KINESIS からそうしていた) 
  • スペースと fn レイヤキーを混合させる

といったところ。コードは以下である:

#include QMK_KEYBOARD_H

#define _QWERTY 0
#define _FN 1
#define _MYCTL 2

enum custom_keycodes {
  KC_QWERTY = SAFE_RANGE,
  KC_LANG,
  KC_FN,
  KC_SP_FN,
  KC_MYCTL,
  KC_KILL,
  KC_LGALT
};

#define XTL_Q  LCTL(KC_Q)
#define XTL_W  LCTL(KC_W)
#define XTL_E  LCTL(KC_E)
#define XTL_R  LCTL(KC_R)
#define XTL_T  LCTL(KC_T)
#define XTL_Y  LCTL(KC_Y)
#define XTL_U  LCTL(KC_U)
#define XTL_I  LCTL(KC_I)
#define XTL_O  LCTL(KC_O)
#define XTL_P  LCTL(KC_P)
#define XTL_A  LCTL(KC_A)
#define XTL_S  LCTL(KC_S)
#define XTL_D  LCTL(KC_D)
#define XTL_F  LCTL(KC_F)
#define XTL_G  LCTL(KC_G)
#define XTL_H  LCTL(KC_H)
#define XTL_J  LCTL(KC_J)
#define XTL_K  LCTL(KC_K)
#define XTL_L  LCTL(KC_L)
#define XTL_Z  LCTL(KC_Z)
#define XTL_X  LCTL(KC_X)
#define XTL_C  LCTL(KC_C)
#define XTL_V  LCTL(KC_V)
#define XTL_B  LCTL(KC_B)
#define XTL_N  LCTL(KC_N)
#define XTL_M  LCTL(KC_M)

const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {

/* Base Layout
 * ,------------------------------------------------------------------------------------------,
 * |  TAB |   Q  |   W  |   E  |   R  |   T  |  ALT |   Y  |   U  |   I  |   O  |   P  |   -  |
 * |------+------+------+------+------+------+------+------+------+------+--------------------|
 * |  CTL |   A  |   S  |   D  |   F  |   G  | LANG |   H  |   J  |   K  |   L  |   ;  |   '  |
 * |------+------+------+------+------+------+------+------+------+------+--------------------|
 * |SHIFT |   Z  |   X  |   C  |   V  |   B  |SPC/FN|   N  |   M  |   ,  |   .  |   /  |SHIFT |
 * `------------------------------------------------------------------------------------------'
 */
[_QWERTY] = LAYOUT(
    KC_TAB,   KC_Q,    KC_W,    KC_E,    KC_R,    KC_T,      KC_LALT,    KC_Y,    KC_U,    KC_I,    KC_O,   KC_P,     KC_MINS,
    KC_MYCTL, KC_A,    KC_S,    KC_D,    KC_F,    KC_G,      KC_LANG,    KC_H,    KC_J,    KC_K,    KC_L,   KC_SCLN,  KC_QUOT,
    KC_LSFT,  KC_Z,    KC_X,    KC_C,    KC_V,    KC_B,      KC_SP_FN,   KC_N,    KC_M,    KC_COMM, KC_DOT, KC_SLSH,  KC_RSFT
),

[_FN] = LAYOUT(
    KC_ESC,   KC_1,    KC_2,    KC_3,    KC_4,    KC_5,      KC_KILL,   KC_6,    KC_7,    KC_8,    KC_9,    KC_0,    KC_EQL,
    KC_MYCTL, KC_F1,   KC_F2,   KC_F3,   KC_F4,   KC_F5,     _______,   _______, KC_GRV,  KC_BSLS, KC_LBRC, KC_RBRC, KC_QUOT,
    KC_LSFT,  KC_F6,   KC_F7,   KC_F8,   KC_F9,   KC_F10,    _______,   _______, _______, _______, _______, _______, KC_RSFT
),

[_MYCTL] = LAYOUT(
    KC_TAB,  _______, XTL_W,   _______,  XTL_R,   XTL_T,     KC_LALT,  _______, _______, _______, _______,  XTL_P,    KC_HOME,
    _______, XTL_A,   XTL_S,   KC_DEL,   KC_PGDN, _______,   KC_BSPC,  KC_BSPC, _______, _______, KC_UP,    KC_RBRC,  KC_END,
    KC_LSFT, XTL_Z,   XTL_X,   XTL_C,    XTL_V,   KC_PGUP,   KC_SPC,   _______, KC_ENT,  KC_LEFT, KC_DOWN,  KC_RIGHT, KC_RSFT
),
};

static bool sp_keeped     = false;
static bool lgalt_keeped  = false;
static bool octl_enabled  = false;
static bool myctl_pressed = false;

uint16_t log_timer = 0;

void
persistent_default_layer_set(uint16_t default_layer) {
  eeconfig_update_default_layer(default_layer);
  default_layer_set(default_layer);
}


void
reset_special_keys(void) {
  sp_keeped = false;
  lgalt_keeped = false;
  octl_enabled = false;
  myctl_pressed = false;
}

bool
process_record_user(uint16_t keycode, keyrecord_t *record) {

  switch (keycode) {
    case KC_SP_FN:
      if (record->event.pressed) {
        sp_keeped = true;
        layer_on(_FN);
      } else {
        layer_off(_FN);
        if (sp_keeped) {
          register_code(KC_SPC);
          unregister_code(KC_SPC);
          sp_keeped = false;
        }
      }
      return false;
    
    case KC_LANG:
      if (record->event.pressed) {
          register_code(KC_LALT);
          register_code(KC_GRV);
          unregister_code(KC_GRV);
          unregister_code(KC_LALT);
          return false;
      }
      break;

    case KC_KILL:
      if (record->event.pressed) {
          register_code(KC_LCTL);
          register_code(KC_LALT);
          register_code(KC_DEL);
          unregister_code(KC_DEL);
          unregister_code(KC_LALT);
          unregister_code(KC_LCTL);
      }
      return false;

    case KC_LGALT:
      if (record->event.pressed) {
        lgalt_keeped = true;
        register_code(KC_LALT);
      } else {
        if (lgalt_keeped) {
          register_code(KC_LALT);
          register_code(KC_GRV);
          unregister_code(KC_GRV);
          lgalt_keeped = false;
        }
        unregister_code(KC_LALT);
      }
      return false;
    
    case KC_MYCTL:
      if (record->event.pressed) {
        if (octl_enabled && (timer_elapsed(log_timer) < 100)) {
          // layer_on(_OCTL);
          register_code(KC_LCTL);
        } else {
          myctl_pressed = true;
          octl_enabled = false;
          layer_on(_MYCTL);
        }
      } else {
        // layer_off(_OCTL);
        unregister_code(KC_LCTL);
        layer_off(_MYCTL);
        if (myctl_pressed) {
          log_timer = timer_read();
          octl_enabled = true;
        }
        myctl_pressed = false;
      }
      return false;

    default:
      if (record->event.pressed) {
        reset_special_keys();
      }
      break;
  }

  return true;
}

qmk に色々とマクロがあることは知っているが、反応速度が遅いので直接制御している。これは crkbd のコードからの流用である。

数か月、概ねこのコンセプトで落ち着いているが、課題はある。
私は vimmer なのだが、泣く泣くカーソルキーは hjkl ではなくコントロールレイヤの l,./ としている。これは h をバックスペースで使っている為で、コントロールレイヤのコンセプトと共存が不可能で非常に悩ましい問題ではある。ただ、レイヤキーと hjkl でカーソル移動とすると、肝心の vi でもカーソル移動にレイヤキーを押していたりして複雑な思いをしたのも事実。
もっとも、カーソルキー問題は誤入力には繋がらず慣れが解決してくれるため大きな問題ではない。一番の課題はスペースキーが fn レイヤキーと兼ねている点だ。スペースを打ち終わる前に次のキーを押してしまうと、スペースがレイヤキーとして解釈されてしまい誤入力の元となる。特に unix コマンドのオプションをハイフンで入れる時に顕著で、例えば ls -l が ls=l となることが多々ある。スペースの両脇にレイヤキーを配置している Reviung39 はこの点をうまく解消してくれるが、一方でスペース/レイヤキー共存は親指の動きに迷いが無くメリットでもあるため悩ましい。タイプ速度は多少遅くなるがスペースを打った後は一呼吸置くことで誤入力は軽減される。または、fn + - は、スペースと - を出力するような専用キーとして定義しても良い。その場合、= は fn + ' になるであろうか。
尚、このキーマップでは、alt と lang は共存可能である(KC_LGALT がそれ)が、今は別々にしている。すなわちまだ1キー分、余りがある。

キーマップを作っていて思うことは、用途に応じて最適なキーマップは異なるということだ。このキーマップはエクセルでの数値入力に適しているとは言えない。また、Windows キーも無い。無理やりぶっこもうと思えば可能かもしれないが、そうしていない。40% 未満の場合、今のキーマップがチートシート無しで覚えられる私的な限界のようだ。まだ挙げていないキーマップのコンセプトがあるとしたら、欲張らないこと、だろうか。例えば、以前は numpad なんていらねーよ、と numpad の存在を dis っていたが、一つのキーボードで何でもしようと思わず、Attack25 のように数字入力とカーソル移動に特化した専用のキーボードを別途持っても良いだろう、と最近思ったりしている(新たにキーボードを作るための大義名分なのかもしれないが)。実際、仮想デスクトップの切り替えは便利なので、meishi にその役割を担わせている。

この記事は Collide39 で書いた。