DM200 Linux のキーボードの不具合の暫定回避

[DM200 Linux のキーボードの不具合](https://qiita.com/alt-core/items/a8d31c625ba4049a59d0) という記事で、DM200 に入れた Linux で複数キーのロールオーバー時に不具合（例：shuと3キー入力するとshauと入力されてしまう）が起こるという話を書きましたが、DM200 Linux を活用すればするほど、どうにもストレスが溜まって仕方が無いので、暫定的な回避を試みてみました。

問題の本質としては、2キーロールオーバーまでしか対応していないキーボードハードウェアに対して、不適切なキーボードドライバが適用されているのが原因のように思われるので、Linux のどこかのコンフィグで本来的には直るものっぽく感じていますが、詳しくないので出来そうな方法で無理矢理解決してみたというお話です。

## やったこと

1. [Linux on Pomera DM200 人柱版 その2](https://www.ekesete.net/log/?p=8940) で /dev/uinput に対応した test_kernel_d32.zip を提供していただいているので、 DM200 のカーネルを更新する。uinput は、仮想キーボード的なものをユーザランドで好きに作れる仕組み。
2. Linux のキーマッピング書き換えソフト x-set-keys を DM200 上でビルド出来るように環境を整える。参考: [Linux の X Window System で動作するキーリマップソフト x-set-keys を作りました - Qiita](https://qiita.com/kawao/items/06f0277fc6de15455808)
3. x-set-keys のソースを書き換えて、dm200 のキーボード不具合を対処し、ビルドする。
4. sudo x-set-keys& でロールオーバー時の不具合はさっぱり解消。

## パッチの内容

方針としては、単純に以下を行います。

* （モディファイアキーを除いて）3キー以上の同時押しが発生している間は、新規のキー押し下げイベントを発生させず、ペンディングする。
* 同時押しが2キー以下になった段階で、ペンディングされているキーがまだ押し下げ状態だったら、それを新規のイベントとして発生させる。

また、x-set-keys の本来の機能であるキーリマップ機能を使わないので、コンフィグファイルを引数に渡さなくても起動するようにちょっとだけいじりました。

キーリピート発生時の処理がまだ怪しいのと、本件の回避だけしたいのならば、本来は x-set-keys をまるまる持ってくる必要はないので、あくまでも暫定回避ということで。。。

何かのご参考になりましたら幸いです。

```patch
diff --git a/src/config.c b/src/config.c
index 775ad65..d86fe93 100644
--- a/src/config.c
+++ b/src/config.c
@@ -36,6 +36,10 @@ gboolean config_load(XSetKeys *xsk, const gchar filepath[])
   KeyCombinationArray *inputs;
   KeyCodeArrayArray *outputs;
 
+  if (filepath == NULL) {
+    return TRUE;
+  }
+
   channel = g_io_channel_new_file(filepath, "r", &error);
   if (!channel) {
     g_critical("Failed to open configuration file(%s): %s",
diff --git a/src/key-code-array.h b/src/key-code-array.h
index 90e3f3a..55b237b 100644
--- a/src/key-code-array.h
+++ b/src/key-code-array.h
@@ -33,6 +33,8 @@ typedef GArray KeyCodeArray;
 #define key_code_array_get_at(array, index)   \
   g_array_index((array), KeyCode, (index))
 #define key_code_array_get_length(array) ((array)->len)
+#define key_code_array_set_size(array, size)                             \
+  g_array_set_size((array), (size))
 
 void key_code_array_free(gpointer array);
 gboolean key_code_array_remove(KeyCodeArray *array, KeyCode key_code);
diff --git a/src/keyboard-device.c b/src/keyboard-device.c
index e245f0a..2a28483 100644
--- a/src/keyboard-device.c
+++ b/src/keyboard-device.c
@@ -40,6 +40,7 @@ static gboolean _is_keyboard(gint fd);
 static gboolean _initialize_keys(Device *device);
 static gboolean _get_ev_bits(gint fd, guint8 ev_bits[]);
 static gboolean _get_key_bits(gint fd, guint8 key_bits[]);
+static guint _count_normal_keys(XSetKeys *xsk, const KeyCodeArray *array);
 static gboolean _handle_input(gpointer user_data);
 static gboolean _handle_event(XSetKeys *xsk, struct input_event *event);
 static gboolean _is_after_repeat_delay(Display *display,
@@ -78,6 +79,7 @@ KeyboardDevice *kd_initialize(XSetKeys *xsk, const gchar *device_filepath)
     return NULL;
   }
   device->pressing_keys = key_code_array_new(6);
+  device->pending_keys = key_code_array_new(6);
   return device;
 }
 
@@ -91,6 +93,9 @@ void kd_finalize(XSetKeys *xsk)
   if (device->pressing_keys) {
     key_code_array_free(device->pressing_keys);
   }
+  if (device->pending_keys) {
+    key_code_array_free(device->pending_keys);
+  }
   if (device->xkb) {
     XkbFreeKeyboard(device->xkb, 0, True);
   }
@@ -264,6 +269,19 @@ static gboolean _handle_input(gpointer user_data)
   return _handle_event(xsk, &event);
 }
 
+static guint _count_normal_keys(XSetKeys *xsk, const KeyCodeArray *array)
+{
+  KeyCode *pointer;
+  guint count = 0;
+
+  for (pointer = &key_code_array_get_at(array, 0); *pointer; pointer++) {
+    if (!ki_is_modifier(&xsk->key_information, *pointer)) {
+      count++;
+    }
+  }
+  return count;
+}
+
 static gboolean _handle_event(XSetKeys *xsk, struct input_event *event)
 {
   KeyboardDevice *device = xsk_get_keyboard_device(xsk);
@@ -280,10 +298,21 @@ static gboolean _handle_event(XSetKeys *xsk, struct input_event *event)
     switch (event->value) {
     case 0:
       key_code_array_remove(device->pressing_keys, event->code);
+      key_code_array_remove(device->pending_keys, event->code);
+      if (_count_normal_keys(xsk, device->pressing_keys) <= 2 &&
+          key_code_array_get_length(device->pending_keys) > 0) {
+        ud_send_key_events(xsk, device->pending_keys, TRUE, FALSE);
+        key_code_array_set_size(device->pending_keys, 0);
+      }
       break;
     case 1:
       key_code_array_add(device->pressing_keys, event->code);
       device->press_start_time = event->time;
+      if (_count_normal_keys(xsk, device->pressing_keys) > 2 &&
+          !ki_is_modifier(&xsk->key_information, event->code)) {
+        key_code_array_add(device->pending_keys, event->code);
+        return TRUE;
+      }
       switch (xsk_handle_key_press(xsk, event->code)) {
       case XSK_CONSUMED:
         return TRUE;
@@ -294,6 +323,9 @@ static gboolean _handle_event(XSetKeys *xsk, struct input_event *event)
       }
       break;
     default:
+      if (_count_normal_keys(xsk, device->pressing_keys) > 2) {
+        return TRUE;
+      }
       is_after_repeat_delay = _is_after_repeat_delay(xsk_get_display(xsk),
                                                      device->xkb,
                                                      &device->press_start_time,
diff --git a/src/keyboard-device.h b/src/keyboard-device.h
index 582f62d..dab5a8f 100644
--- a/src/keyboard-device.h
+++ b/src/keyboard-device.h
@@ -29,6 +29,7 @@
 typedef struct KeyboardDevice_ {
   Device device;
   KeyCodeArray *pressing_keys;
+  KeyCodeArray *pending_keys;
   struct timeval press_start_time;
   XkbDescPtr xkb;
 } KeyboardDevice;
diff --git a/src/main.c b/src/main.c
index 3539149..85e422b 100644
--- a/src/main.c
+++ b/src/main.c
@@ -142,19 +142,17 @@ static gboolean _parse_arguments(gint argc,
     g_print("%s\n", help);
     g_free(help);
     result = FALSE;
-  } else if (argc != 2) {
+  } else if (argc > 2) {
     gchar *help;
 
-    if (argc < 2) {
-      g_critical("Configuration file must be specified");
-    } else {
-      g_critical("Too many arguments");
-    }
+    g_critical("Too many arguments");
 
     help = g_option_context_get_help(context, TRUE, NULL);
     g_print("%s\n", help);
     g_free(help);
     result = FALSE;
+  } else if (argc < 2) {
+    arguments->config_filepath = NULL;
   } else {
     arguments->config_filepath = argv[1];
   }
```

### 設定方法
.xinitrc にて exec i3 する直前に呼び出すように設定。
（sudo をパスワード無しで実行できるようにしている）

ただし、このタイミングで呼び出すと、キーボードレイアウトの設定が
何故か反映されないため、setxkbmap で日本語キーボードに設定し直す。
なお、ctrl:nocaps は、CapsLockをCtrlに上書きする設定。

```
...
sudo /opt/local/bin/fixkb&
setxkbmap -model jp106 -option ctrl:nocaps

exec i3
```

### 覚書
- keyboard-device.c で key-up イベントのフィルタをしていないため、key-down がブロックされたキーの key-up だけ発生しそうだが、uinput-device.c 側の既存の処理で key-down が発生していないキーの key-up が発生しないようにしているため、不整合は発生しない。
- keyboard-device.c で３キー以上押されている際にキーリピートのイベントをブロックしているが、uinput-device.c 側でキーリピートのエミュレーション処理が走るため、最後に key-down が送られたキーでキーリピートは発生する。
- 実用上、モディファイア（shift, ctrl, alt)を含む３キー同時押しをブロックされると困るので、モディファイアを特別扱いしてみているが、モディファイアを含む３キー同時押しでゴーストが発生する組み合わせが存在している可能性はある。