【基礎から学ぶ】Karabiner-Elements でタップダンス + レイヤーを実現するまで
はじめに
約2年ぶりに Linux から Mac に戻ってきた オーリア です。Linux も素晴らしい OS だと思いますが、AI の進歩や最新ツールを触りたく macOS に戻しました。特に ChatGPT Desktop が素晴らしく、ホットキーから呼び出して Google 検索の代わりに使用する回数が激増しました。
さて、2年越しに Mac をセットアップしていました。筆者はプログラマーになりたての頃から、分割キーボードを使用しており、長年 Corne Chery を愛用しています。デフォルトではキー数が足りないのでレイヤーでカバーしたり、独自のキーマップを構築したりと、分割キーボードライフを楽しんでいます。今回の Mac のセットアップ時に改めてキーマップを見直し、少し改良を加えました。その中で、一度導入したが最終的に消した機能に、タップダンスを活用したレイヤーがあります。(理由は「おわりに」を参照)
今回は、Karabiner-Elements で「タップダンス + レイヤー」を実現する方法を解説します。具体的には、「かなキーを2回押下 → ホールド → ホールド中はレイヤーに移行」を実現します。ここで「かなキー」は例の1つでしかなく、他のどのようなキーでも代替可能な点に注意してください。この機能を応用すれば、「かなキーを2回押下 → キーを離す → レイヤーに移行」を実現することもできます。その場合はレイヤーに移行した後にキャンセルできないため、Esc をキャンセルに割り当てるなどの工夫が必要になると思います。
前置きが長くなりましたが、さっそく解説していきましょう。まず最初に用語の解説を行い、その後に Karabiner-Element で実現する方法を示します。
用語
レイヤー
レイヤーの動作イメージ [1]
自作キーボードを使用している方なら「レイヤー」という概念を聞いたことがあるでしょう。ある特定のキーを押している間だけ、キーマップを切り替える機能です。例えば、通常では a-z
のアルファベットを入力し、かなキーを押している間は数字の 1-0
やファンクションキーの F1-F10
を入力するような機能です。同じ a
キーでも、通常は a
を押せば a
を入力し、かな + a
を押せば 1
を入力する、といったことが可能です。自作キーボードはキー数が少ないですから、数字行やファンクションキー行を補うためにとても有効な手段です。
タップダンス
タップダンスの動作イメージ
1回押して離せばAキーを入力し、
素早く2回押せばBキーを入力する [2]
自作キーボードを使用している方なら「タップダンス」という言葉を聞いたことがかもしれません。QMK Firmware の機能の1つ であり、キーの複数回押しに対し、それぞれ別の役割を持たせることができます。ネットで調べてみると、2回押しでアプリケーションを起動など、ショートカットキーのように使用している方が多いようです。タップダンスにより、物理的なキー数の制約を超えて、キーを割り当て可能なポイントをさらに増やすことができますす。
実装
まずは Karabiner-Elements の複雑なルールを実現する機能である Complex Modifications を説明し、その後にレイヤーとタップダンスを実現する方法、さらにタップダンス + レイヤーを実現する方法を解説します。Complex Modifications の機能である set_variable
や conditions
なども解説していきます。順序立てて説明しているため、ぜひ最初から読み進めていくことをお勧めします。
Complex Modifications
基本
最初に、Karabiner-Elements の Complex Modifications について説明しておきます。
Complex Modifications は、あるキーとあるキーの組み合わせで別のキー入力を行うなど、複雑なキーマップを実現するための機能です。以下のような特徴を持ちます。
- 設定ファイルを
~/.config/karabiner/assets/complex_modifications
に配置する- 詳細は こちら
- 複数のファイルを配置することも可能
- 設定ファイルの読み込みは
Karabiner-Elements Settings > Complex Modifications > Add predefined rule
から行う - 設定ファイルは JSON で記述する
便利なデバッグ方法
設定ファイルを編集する中で、どのような状態かデバッグしたくなる時があるでしょう。便利な方法を2つ紹介します。
1つめは構文チェックです。Karabiner-Emements は GUI からコードを編集することが可能です。その際、保存した時に無効な文法があれば、エラーメッセージを表示してくれます。この機能を利用して、別のエディタで編集した設定ファイルに対し、以下の順序で構文チェックを行うことができます。
-
Karabiner-Elements Settings > Complex Modifications > Add predefined rule
をクリック - 自作した設定ファイルを読み込む
- 追加されたルールの
Edit
をクリック - 編集せずに
Save
をクリック
構文エラーの表示
2つめは Karabiner-EventViewer です。Karabiner-Elements をインストールすれば、自動的に付いてきます。キー入力やボタンクリックなどのログを、リアルタイムに閲覧することができます。自分が書いた設定により、実際にどようなイベントが送信されているのか確認するための、強力なアシストとなります。
Karabiner-EventViewer に表示されるイベントは、変換後のイベントであることに注意してください。例えば Shift + A
を 1
に変換していた時、A
の入力は表示されず、1
の入力のみ表示されます。
Karabiner-EventViewer
ルール (manipulator) の優先度
Complex Modifications の注意点を紹介します。
Complex Modifications では、衝突するルールが記述された場合、1番最初に適合したルールが処理される ことに注意してください。ルールは定義された上から順番に実行される ためです。ゆえに、Complex Modifications のルールの順序は重要です。重複がないキレイなルールを記述すること一番ですが、複雑なことを実現しようとするとそうもいかなくなります。ハマりやすいポイントだと思うので、心の隅にとどめておいて下さい。
Complex Modifications ではルールの入れ替えが可能
上から順番に判定が行われる
また、1度ルールが適用された場合、後のルールの探索は行わず、適合したルールのみ処理される ことにも注意してください。これにより、装飾キーに対してルールを作成する場合、1つのルールに複数の目的が入り混じった記述をすることがあります。例えば、「Option キーをレイヤーキーとして使用し、さらに Option + A で別の操作を実行する」場合などです。これらを2個の「Option キーを扱うルール」として記述してしまうと、どちらかのルールに適合した時、もう片方のルールは処理されません。この問題を回避するには、1個のルールとしてまとめて記述する必要あります。
レイヤー
Karabiner-Elements でレイヤーを実現する方法を見てみましょう。
ここでは以下のことを実現します。
- 「かな」キーを押した時、ホールド中 (押しっぱなしの状態) はレイヤー1を ON にする
- 「かな」キーを押した時、「かな」キーから指を離せばレイヤー1を OFF にする
- 「かな」キーを押した時、単独で押されれば「かな」キーを入力する
- 「A」キーを押した時、レイヤー1が ON であれば、「1」キーを入力する
{
"title": "Example for Zenn",
"rules": [
{
"description": "Kana to Layer1 if holding, Kana to Kana if alone",
"manipulators": [
{
"from": {
"key_code": "japanese_kana",
"modifiers": { "optional": ["any"] }
},
"to": [
{ "set_variable": { "name": "layer1", "value": true, "key_up_value": false }}
],
"to_if_alone": [
{ "key_code": "japanese_kana" }
],
"type": "basic"
},
{
"from": {
"key_code": "a",
"modifiers": { "optional": ["any"] }
},
"conditions": [
{ "type": "variable_if", "name": "layer1", "value": true }
],
"to": [
{ "key_code": "1" }
],
"type": "basic"
}
]
}
]
}
layer1
変数で、レイヤー1 の ON/OFF を管理しています。
順番に見ていきましょう。
「かな」キーの設定
まず、manipulators
の1個目の要素を解説します。「かな」キーが押された時の設定を記述しています。
{
"from": {
"key_code": "japanese_kana",
"modifiers": { "optional": ["any"] }
},
"to": [
{ "set_variable": { "name": "layer1", "value": true, "key_up_value": false }}
],
"to_if_alone": [
{ "key_code": "japanese_kana" }
],
"type": "basic"
}
from
に判定するキーを指定しています。ここでは「かな」キー ( japanese_kana
) が押された時の設定を記述しています。
from.modifiers
に { "optional": ["any"] }
を指定しています。これにより、他の装飾キーが押されていたときに、その装飾キーを引き継ぐことができます。例えば、Kana + A
が押された時に 1
を入力するように設定していた場合、Kana + Shift + A
が押された時に Shift + 1
を入力することができます。
to
に実行するイベントを指定しています。これにより form
のキー入力を無視し、to
のイベントを実行します。例えば、単純にキーを入力したり、内部変数をセットしたりします。
to.set_variable
に変数を定義しています。今回は名前を layer1
とし、値を boolean
としました。「かな」キーが押された時 leyar1
変数の値を true
にセットし、「かな」キーが離れた時 layer1
変数の値を false
にセットします。ちなみに、key_up_value
の指定はオプショナルです。
to.key_code
に { "key_code": "a" }
のような値を指定していないことに注意してください。このような場合、対象のキーが押された時、単純にキー入力を行いません。ここまでの解説では、「かな」キーは押されても何も入力しません。
to_if_alone
にキーが単独で押された場合のイベントを指定しています。japanese_kana
を指定し、「かな」キーをホールド中に他のキーが押されなければ、そのまま「かな」キーが入力されるように設定しています。
「A」キーの設定
次に、manipulators
の2個目の要素を解説します。レイヤー1のキーマップとして「A」キーが押された時の設定を記述しています。
{
"from": {
"key_code": "a",
"modifiers": { "optional": ["any"] }
},
"conditions": [
{ "type": "variable_if", "name": "layer1", "value": true }
],
"to": [
{ "key_code": "1" }
],
"type": "basic"
}
form
と to
の詳細な説明は割愛します。from
には「A」キーが押された時の設定を記述し、to
には 1
を入力する設定を記述しています。
to.conditions
にイベントが処理される条件を指定しています。条件に合致しない場合、イベントは処理されません。
to.conditions.variable_if
に変数による条件を指定しています。「かな」キーでセットした変数 layer1
を参照し、true
であればイベントを処理します。
レイヤーの実現
改めて、全体のコードを確認してみてください。
全体のコード
{
"title": "Example for Zenn",
"rules": [
{
"description": "Kana to Layer1 if holding, Kana to Kana if alone",
"manipulators": [
{
"from": {
"key_code": "japanese_kana",
"modifiers": { "optional": ["any"] }
},
"to": [
{ "set_variable": {"name": "layer1", "value": true, "key_up_value": false }}
],
"to_if_alone": [
{ "key_code": "japanese_kana" }
],
"type": "basic"
},
{
"from": {
"key_code": "a",
"modifiers": { "optional": ["any"] }
},
"conditions": [
{ "type": "variable_if", "name": "layer1", "value": true }
],
"to": [
{ "key_code": "1" }
],
"type": "basic"
}
]
}
]
}
これらの組み合わせにより、以下の流れでレイヤー機能を実装できます。
- 「かな」キーを押す
-
layer1 = true
がセットされる - 「A」キーを押す
-
1
が入力される - 「A」キーを離す
- 「かな」キーを離す
-
layer1 = false
がセットされる
「かな」キーを離した後に、key_up_value
で leyer1
変数の値を初期化していることに注意してください。これにより、「かな」キーがホールド中に場合のみ、レイヤーを有効化するようにしています。
今回の設定では、レイヤー1の「A」キーしか設定していません。レイヤー1が有効中に「S」「D」キーなど他のキーも異なる入力にしたい場合、「A」キーと同様の設定を追加する必要があります。
タップダンス
Karabiner-Elements でタップダンスを実現する方法を見てみましょう。
ここでは以下のことを実現します。
- 「右の Option」キーをダブルタップ (素早く2回押下) した時、「スペース」キーを入力する
- 「右の Option」キーが押された時、装飾キーの「右の Option」として扱う
{
"title": "Example for Zenn",
"rules": [
{
"description": "Right Option to Space if double tap",
"manipulators": [
{
"from": {
"key_code": "right_option"
},
"conditions": [
{ "type": "variable_if", "name": "tap-right-option", "value": true }
],
"to": [
{ "set_variable": { "name": "tap-right-option", "value": false } },
{ "key_code": "spacebar" }
],
"type": "basic"
},
{
"from": {
"key_code": "right_option",
"modifiers": { "optional": ["any"] }
},
"to": [
{ "set_variable": {"name": "tap-right-option", "value": true } },
{ "key_code": "right_option", "lazy": true }
],
"to_delayed_action": {
"to_if_invoked": [
{ "set_variable": { "name": "tap-right-option", "value": false } }
],
"to_if_canceled": [
{ "set_variable": { "name": "tap-right-option", "value": false } }
]
},
"parameters": {
"basic.to_delayed_action_delay_milliseconds": 300
},
"type": "basic"
}
]
}
]
}
tap-japanese-kana
変数で「2回目のキー入力がダブルタップとして有効となる期間」を管理しています。
こちらも順番に見ていきましょう。
1回目の「右の Option」キーの設定
最初に、manipulators
の 2個目の要素 を解説します。
Complex Modifications のルールは上から順番に適用されていくのでした。そのため、1個目に特殊なケースのルールを記述し、2個目に汎用なケースのルールを記述することで、1回目のキー押下で汎用なルールを適用し、2回目のキー押下で特殊なルールを適用 しています。ややこしいですが、コードと解説を見比べていきながら、理解してみてください。
manipulators
の2個目の要素では、1回目に「右の Option」キーが押された時の設定を記述しています。
{
"from": {
"key_code": "right_option",
"modifiers": { "optional": ["any"] }
},
"to": [
{ "set_variable": {"name": "tap-right-option", "value": true } },
{ "key_code": "right_option", "lazy": true }
],
"to_delayed_action": {
"to_if_invoked": [
{ "set_variable": { "name": "tap-right-option", "value": false } }
],
"to_if_canceled": [
{ "set_variable": { "name": "tap-right-option", "value": false } }
]
},
"parameters": {
"basic.to_delayed_action_delay_milliseconds": 300
},
"type": "basic"
}
from
には、「右の Option」キーが押された時の設定を記述し、to
には tap-japanese-kana
変数の値を true
にセットする設定を記述しています。
to.lazy
に true
を指定している点に注意してください。装飾キーをに対して from
を指定する時、 to
に { "key_code": "right_option" }
のように指定してしまうと、単発の Option 入力で終了してしまいます。{ "lazy": true }
を指定することにより、「右の Option」キーをホールド中に、Option
装飾キーのフラグを立てるようにしています (厳密なキー動作は異なりますが、この理解で大丈夫です)。
また、to
では 変数をセットした後に、lazy なキー入力を行っている 点に注意していください。先に { "key_code": "XXX", "lazy": true }
を行ってしまうと、{ "set_variable": {"name": "XXX", "value": XXX } }
を処理した時に、キーが lazy になりません。必ず先に変数をセットし、後からキー入力の順序にしてください。
to_delayed_action
に、遅延実行するイベントを記述しています。デフォルトの遅延時間は 500 ミリ秒です。to_delayed_action
の処理は独立しており、from
が検知されてから遅延時間が経過した後に このイベントが強制的に処理される ことに注意してください。仮に1秒後に遅延処理される場合、キー操作が終わっている (キーを離した)、キー操作が終わっていない (キーをホールド中) に関わらず、きっちり1秒後にイベントが処理されます。
to_delayed_action.to_if_invoked
に、from
キーが押された後に、他のキーが押されなかった 場合のイベントを指定しています。つまり、キーが単独で操作された場合、遅延時間が経過した後にこのイベントを処理します。ここでは tap-right-option
変数の値を false
にセットしています。
to_delayed_action.to_if_canceled
に、from
キーが押された後に、他のキーが押された 場合のイベントを指定しています。該当する場合、このイベントは遅延時間が経過する前に即座に処理され、逆に to_delayed_action.to_if_invoked
のイベントは処理されないことに注意して下さい。両者の関係は排他的になっています。ここでは tap-right-option
変数の値を false
にセットしています。
parameters.basic.to_delayed_action_delay_milliseconds
に to_delayed_action
の遅延時間を指定しています。前述した通り、遅延時間のデフォルト値は 500 ミリ秒でした。ここでは 300 ミリ秒を指定しています。遅延時間は好みに合わせて調節してください。
まとめると、以下のようにして「ダブルタップが有効であるかの状態」を管理しています。
- ダブルタップが有効と判定されるパターン
- キー押下から 300 ミリ秒以内である
- 「右の Option」キーを押す
-
tap-right-option = true
がセットされる - 「右の Option」キーを離す
- 300 ミリ秒が経過が経過していない
- キー押下から 300 ミリ秒以内である
- ダブルタップが有効と判定されないパターン
- キー押下から 300 ミリ秒が経過する
- 「右の Option」キーが押される
-
tap-right-option = true
がセットされる - 「右の Option」キーを離す
- 300 ミリ秒が経過する
-
tap-right-option = false
がセットされる
- キー押下から 300 ミリ秒以内であるが、他のキーが押された
- 「右の Option」キーが押す
-
tap-right-option = true
がセットされる - 「右の Option」キーを離す
- 300 ミリ秒が経過が経過していない
- 「A」キーを押す
-
tap-right-option = false
がセットされる - 「A」キーを離す
- キー押下から 300 ミリ秒が経過する
2回目の「右の Option」キーの設定
次に、manipulators
の 1個目の要素 を解説します。
{
"from": {
"key_code": "right_option"
},
"conditions": [
{ "type": "variable_if", "name": "tap-right-option", "value": true }
],
"to": [
{ "set_variable": { "name": "tap-right-option", "value": false } },
{ "key_code": "spacebar" }
],
"type": "basic"
}
新しい機能はないため、詳細な説明は割愛します。
from
には、「右の Option」キーが押された時の設定を記述しています。
conditions
には、tap-right-option
変数の値が true
であることの条件を記述しています。
to
には、tap-right-option
変数の値を false
にセットし、「スペース」キーを入力する設定を記述しています。
タップダンスの実現
改めて全体のコードを確認しましょう。
ここでも Complex Modifications のルールは上から順番に適用されていため、1個目に特殊なケースのルールを記述し、2個目に汎用なルールを記述している点に注意してください。2回「右の Option」キーを押したとき、実際に処理される順番は manipulators
の 2個目の要素 → 1個目の要素になっています。
全体のコード
{
"title": "Example for Zenn",
"rules": [
{
"description": "Right Option to Space if double tap",
"manipulators": [
{
"from": {
"key_code": "right_option"
},
"conditions": [
{ "type": "variable_if", "name": "tap-right-option", "value": true }
],
"to": [
{ "set_variable": { "name": "tap-right-option", "value": false } },
{ "key_code": "spacebar" }
],
"type": "basic"
},
{
"from": {
"key_code": "right_option",
"modifiers": { "optional": ["any"] }
},
"to": [
{ "set_variable": {"name": "tap-right-option", "value": true } },
{ "key_code": "right_option", "lazy": true }
],
"to_delayed_action": {
"to_if_invoked": [
{ "set_variable": { "name": "tap-right-option", "value": false } }
],
"to_if_canceled": [
{ "set_variable": { "name": "tap-right-option", "value": false } }
]
},
"parameters": {
"basic.to_delayed_action_delay_milliseconds": 300
},
"type": "basic"
}
]
}
]
}
ダブルタップが実現される流れを整理します。
- ダブルタップが実現されるパターン
- 「右の Option」キーを押してから 300 ミリ秒以内に、「右の Option」キーを押す
- 「右の Option」キーを押す
-
tap-right-option = true
がセットされる - 「右の Option」キーを離す
- 300 ミリ秒が経過が経過していない
- 「右の Option」キーを押す
-
tap-right-option = false
がセットされる -
spacebar
が入力される - 「右の Option」キーを離す
- 「右の Option」キーを押してから 300 ミリ秒以内に、「右の Option」キーを押す
- ダブルタップが実現されないパターン
- 「右の Option」キーを押してから 300 ミリ秒が経過する
- 「右の Option」キーが押される
-
tap-right-option = true
がセットされる - 「右の Option」キーを離す
- 300 ミリ秒が経過する
-
tap-right-option = false
がセットされる
- 「右の Option」キーを押してから 300 ミリ秒以内であるが、他のキーが押された
- 「右の Option」キーが押す
-
tap-right-option = true
がセットされる - 「右の Option」キーを離す
- 300 ミリ秒が経過が経過していない
- 「A」キーを押す
-
tap-right-option = false
がセットされる - 「A」キーを離す
- 「右の Option」キーを押してから 300 ミリ秒が経過する
複雑ですが、実際にキーを押した時・話した時の操作と、処理されるイベントを想像しながら、理解してみてください。
タップダンス + レイヤー
いよいよ、これらの組み合わせである「タップダンス + レイヤー」の設定です。ここまでの解説で理解しできていれば、素直に読み解けるでしょう。
- 「右の Option」キーをダブルタップ (素早く2回押す) した時、Layer1を ON にする
- 「右の Option」キーが押された時、装飾キーの「右の Option」として扱う
- 「A」キーを押した時、レイヤー1が ON であれば、「1」キーを入力する
{
"title": "Example for Zenn",
"rules": [
{
"description": "Right Option to Layer1 if double tap",
"manipulators": [
{
"from": {
"key_code": "right_option"
},
"conditions": [
{ "type": "variable_if", "name": "tap-right-option", "value": true }
],
"to": [
{ "set_variable": { "name": "tap-right-option", "value": false } },
{ "set_variable": { "name": "layer1", "value": true, "key_up_value": false } }
],
"type": "basic"
},
{
"from": {
"key_code": "right_option",
"modifiers": { "optional": ["any"] }
},
"to": [
{ "set_variable": {"name": "tap-right-option", "value": true } },
{ "key_code": "right_option", "lazy": true }
],
"to_delayed_action": {
"to_if_invoked": [
{ "set_variable": { "name": "tap-right-option", "value": false } }
],
"to_if_canceled": [
{ "set_variable": { "name": "tap-right-option", "value": false } }
]
},
"parameters": {
"basic.to_delayed_action_delay_milliseconds": 300
},
"type": "basic"
},
{
"from": {
"key_code": "a",
"modifiers": { "optional": ["any"] }
},
"conditions": [
{ "type": "variable_if", "name": "layer1", "value": true }
],
"to": [
{ "key_code": "comma" }
],
"type": "basic"
}
]
}
]
}
ダブルタップでレイヤーが実現される流れを整理します。
- ダブルタップでレイヤーが実現されるパターン
- 「右の Option」キーを押してから 300 ミリ秒以内に、「右の Option」キーを押す
- 「右の Option」キーを押す
-
tap-right-option = true
がセットされる - 「右の Option」キーを離す
- 300 ミリ秒経過が経過していない
- 「右の Option」キーを押す
-
tap-right-option = false
がセットされる -
layer1 = true
がセットされる - 「A」キーを押す
-
comma
が入力される - 「A」キーを離す
- 「右の Option」キーを離す
-
layer1 = false
がセットされる
- 「右の Option」キーを押してから 300 ミリ秒以内に、「右の Option」キーを押す
- ダブルタップが実現されないパターン
- 「右の Option」キーを押してから 300 ミリ秒が経過する
- 「右の Option」キーが押される
-
tap-right-option = true
がセットされる - 「右の Option」キーを離す
- 300 ミリ秒が経過する
-
tap-right-option = false
がセットされる
- 「右の Option」キーを押してから 300 ミリ秒以内であるが、他のキーが押された
- 「右の Option」キーが押す
-
tap-right-option = true
がセットされる - 「右の Option」キーを離す
- 300 ミリ秒経過が経過していない
- 「Q」キーを押す
-
tap-right-option = false
がセットされる - 「Q」キーを離す
- 「右の Option」キーを押してから 300 ミリ秒が経過する
これにより、ダブルタップでレイヤーを実現することができました。
おわりに
本来は以下のつまづきポイントは個別に解説できれば良かったのですが、余力がなくそのままとなりました。
- 装飾キーの
{ "optional": ["any"] }
の設定 - 装飾キーの
to_if_alone
,to.conditions
とto.set_variable
,to.lazy
の解説 -
to
のto.set_variable
とto.lazy
の定義順による挙動の違い
また、最終的にタップダンスを活用したレイヤー機能を見送った理由を書いておきたいと思います。
それは「 option
キーとの組み合わせで実現するレイヤー」で十分だと途中で気づいたからです。macOS では、Option + Alphabet
で特殊記号を入力できます。例えば Option + A,S,D
を押した時、それぞれ å,ß,∂
が入力されます。私にはそれらの特殊記号が不要だったため、Option + Alpahbet
の組み合わせをそのまま別のレイヤーとして使用することができました。タップダンスは魅力的な機能ですが、ダブルタップという操作には多少なりとも時間がかかります。ワンタップで操作できるよう Option + Alphabet
を利用したレイヤーを選択しました。
さらに Option + Alphabet
でレイヤーを実現すると、to.set_variable
による変数のセットが不要となります。option
キーがレイヤーの起点となり、Option + A,S,D
の変換をそのまま設定すれば良いためです。これにより、変数の管理からも解放されます。書いていて思いましたが、実は to.set_variable
によるレイヤーの実現は、実際には過剰なのかもしれません。
いかがでしたでしょうか。みなさんの自作キーボードライフ、並びに Karabiner-Elements ライフが豊かになれば幸いです。
-
画像は ChatGPT 4o より生成 ↩︎
Discussion