git:20260503.36bbbd6 to git:20261022.04eff2b

1 added, 9 removed. Audit A to A.

# configure-macro-keyboard Skill
> End-to-end Karabiner workflow for cheap 3-key USB-C/Bluetooth macro pads. Identifies the device, writes a device-scoped Karabiner rule, handles dual-transport (USB + BT) configurations, and packages reusable patterns (`simultaneous` matchers, tap-vs-double-tap pairs, `device_if`).
**Hub**: [Plugin CLAUDE.md](../../CLAUDE.md) | **Sibling skills**: [emit-fn-key-on-macos](../emit-fn-key-on-macos/CLAUDE.md) · [diagnose-hid-keycodes](../diagnose-hid-keycodes/CLAUDE.md)
## What This Skill Owns
| File | Role | Edit policy |
| ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `SKILL.md` | User-invocable instructions for configuring a new pad. Loaded into context when the skill fires. | Edit when the 5-step workflow changes shape. Keep terse — references carry the depth. |
| `references/raw/karabiner-rule.json` | **SSoT for the live MacroKeyBot rule.** Verbatim 12-manipulator export. | Always update in lockstep with `~/.config/karabiner/karabiner.json`. The repo file is the source — patch live config from it, not the other way around. |
| `references/02-usb-wired-configuration.md` | Live USB rule walkthrough + Karabiner-side tap/double-tap mechanism + troubleshooting matrix. Notes the BT bottom-button asymmetry but defers details to `08-`. | Keep mapping table, abridged JSON, and "How the tap/double-tap pattern works" section in sync with `raw/karabiner-rule.json`. When changing the bottom button on USB, also check whether the BT path needs a parallel update (and vice versa). |
| `references/08-bluetooth-configuration.md` | Live BT rule walkthrough (mode-4 firmware) + numbered manipulator structure. **Documents the bottom-button asymmetry**: pad firmware emits two different keycodes for single-vs-double-tap on the bottom button (`equal_sign` / `Option+Z`), so manipulators 11 + 12 are immediate translations, NOT a software-discrimination pair. | When the rule changes, update the structure list (1-12), the "Why USB and BT use different mechanisms" explanation, and the "Switching Modes" / "Other BT Modes" sections that reference manipulator numbers. Re-test both transports of any button you change in EventViewer to catch new firmware-side discrimination. |
| `references/09-turnkey-walkthrough.md` | Copy-paste-ready 30-minute MacroKeyBot recipe with VID/PID placeholders. | The full JSON in this file is meant to be lifted as-is. Keep it equivalent to `raw/karabiner-rule.json` modulo the `macrokeybot_*` vs `jieli_*` variable rename. |
| `references/03-patterns.md` | Reusable techniques (live examples are device-specific; the patterns are not). | When a new live example proves a pattern, add it under the pattern's "Live examples" line. Don't fork patterns per device. |
| `references/04-anti-patterns.md` | Dead-ends + historical narrative of the original Ctrl+C → Fn rule. | Append-only. Don't rewrite history when the rule evolves. |
| `references/overview.md` | TL;DR mapping table + device signatures. | First place to update when the live mapping changes. |
| `references/05-bluetooth-roadmap.md` | Historical pre-pairing roadmap. | Don't update — frozen in time. |
| `references/06-bluetooth-landscape-survey.md` | 2026 ecosystem context. | Update only on major ecosystem shifts. |
| `references/07-bluetooth-toolbox.md` | Tier-ranked FOSS BT tools. | Update when a tool is replaced or its install command changes. |
| `references/raw/{lsusb-verbose,system-profiler,system-profiler-bluetooth,ioreg-hid-device}.txt` | Frozen hardware dumps from 2026-04-21. | Re-capture only if the pad firmware or macOS HID stack changes meaningfully. |
| `references/raw/karabiner-bluetooth-device.json` | Karabiner's view of the paired Free3-P. | Re-capture when the BT pairing identity changes. |
## Critical Invariants
1. **`raw/karabiner-rule.json` and `~/.config/karabiner/karabiner.json` MUST stay in sync.** When updating the rule, edit the repo file first, then patch the live config (the live config has 4 sibling rules — never replace the whole file). Backup the live config before patching: `cp ~/.config/karabiner/karabiner.json ~/.config/karabiner/karabiner.json.bak.$(date +%Y%m%d-%H%M%S)`.
2. **Manipulator counts are doc-load-bearing.** The number "12" appears in `02-`, `08-`, `09-`, `overview.md`, `SKILL.md`, plugin `CLAUDE.md`, plugin `README.md`, plugin `CHANGELOG.md`, and this file. When the count changes, grep all docs and update consistently — `grep -rEn "[0-9]+[ -]manipulator|[0-9]+ manipulators|twelve manipulator|ten manipulator|eight manipulator" plugins/macro-keyboard/`. Also update the `comm`-based self-check command in `plugins/CLAUDE.md` if you add new entries to the plugin list.
3. **Each Karabiner-side-discriminated button needs its own variable name.** Live config uses `jieli_top_tap`, `jieli_middle_tap`, and `jieli_bottom_tap`. Sharing a variable across buttons would let a tap on one arm the double-tap detector on another. Variables are NOT used for the BT bottom button — that path uses pad-firmware-decided keycode translation instead (see invariant #8).
4. **`device_if` is non-optional on every manipulator.** Dropping it remaps the MacBook's built-in keyboard and breaks Apple's native keys. Both VID/PID identifiers (USB + BT) must be in the `identifiers` array.
5. **`apple_vendor_top_case_key_code: keyboard_fn` is the only path to real Fn.** `key_code: fn` and `modifiers: ["fn"]` are no-ops. See sibling skill `emit-fn-key-on-macos/CLAUDE.md` for the why.
6. **The top-button tap/double-tap is incompatible with Typeless push-to-talk.** Fn fires only after the 200ms detection window expires, so press-and-hold doesn't sustain Fn-down. The live config assumes Typeless is in tap-to-toggle mode. To restore PTT, collapse the top-button pair into a single immediate-Fn manipulator per transport (recipe in `09-turnkey-walkthrough.md`'s "Adapt for your pad" bullet).
7. **The bottom-button tap/double-tap defeats arrow-key auto-repeat on both transports.** On USB the single-tap target (`up_arrow`) fires once after the 200ms detection window; on BT the pad's firmware emits one discrete `equal_sign` event per gesture. Either way: no auto-repeat stream. Acceptable for cursor-nudge; not for fast-scroll. To restore auto-repeat on USB, collapse the pair into a single immediate-`up_arrow` manipulator. On BT you can't restore it — the firmware doesn't emit a stream.
8. **The bottom button uses ASYMMETRIC mechanisms across transports** (verified 2026-05-02 via Karabiner-EventViewer). USB: pad emits `Ctrl+X` for every press → Karabiner-side software discrimination via `set_variable` + `to_delayed_action` (manipulators 9 + 10). BT mode 4: pad firmware does its own double-tap detection → emits `equal_sign` for single tap, `Option+Z` for double tap → Karabiner just translates each immediately (manipulators 11 + 12, no variable, no delayed action). **Always re-test both transports of every button before assuming software discrimination is needed** — see `04-anti-patterns.md` → "Assuming the pad emits the same keycode on single-tap and double-tap on every transport". Top + middle buttons do NOT have this asymmetry; they use software discrimination on both transports.
- ## Recent Changes
-
- - **2026-05-02 (afternoon, 2nd revision)** — Bottom button targets revised to `up_arrow` (single tap) / `down_arrow` (double tap). During this change, **discovered via Karabiner-EventViewer that the pad's BT firmware does its own double-tap discrimination on the bottom button only** — emitting `equal_sign` for single tap and `Option+Z` for double tap (top + middle still emit `page_up` / `page_down` on every press). Restructured the BT bottom path: removed the software discrimination (manipulators 11 + 12 no longer use `variable_if` or `to_delayed_action`); replaced with two simple immediate-translation manipulators (`equal_sign` → `up_arrow`, `Option+Z` → `down_arrow`). USB bottom path kept its software discrimination unchanged (USB still emits `Ctrl+X` on every press). Manipulator count stays at 12 but the structural meaning shifted. Live config backup: `~/.config/karabiner/karabiner.json.bak.before-bottom-arrows-20260502-141718`. New pattern documented in `03-patterns.md` → "Translate pad-firmware-decided keycodes". New anti-pattern entry in `04-anti-patterns.md` → "Assuming the pad emits the same keycode on single-tap and double-tap on every transport".
- - **2026-05-02 (morning, 1st revision)** — Bottom button gained tap/double-tap pair (single → `down_arrow` cursor nudge, double → `Cmd+Delete` line-clear). Manipulator count: 10 → 12. New variable: `jieli_bottom_tap`. No-auto-repeat caveat documented. Live config backup: `~/.config/karabiner/karabiner.json.bak.before-bottom-tap-20260502-130639`. (Superseded by afternoon revision.)
- - **2026-04-24** — Top button gained tap/double-tap pair (single → Fn for Typeless toggle, double → `Cmd+V` paste). Manipulator count: 8 → 10. New variable: `jieli_top_tap`. PTT-incompatibility caveat documented across all relevant references. Live config backup: `~/.config/karabiner/karabiner.json.bak.before-top-tap-20260424-143528`.
- - **2026-04-23** — Middle button gained tap/double-tap pair (single → `Shift+Return`, double → `Return`). Initial introduction of the `set_variable` + `to_delayed_action` pattern.
- - **2026-04-21** — Bluetooth support added (mode-4 firmware: page_up / page_down / equal_sign). Same rule, dual `device_if` identifiers.
-
## Common Edits
- **Change a button's target keycode**: edit `to[0]` in the relevant manipulator(s) in `raw/karabiner-rule.json`, then patch the live config. For software-discriminated tap/double-tap pairs (top + middle on both transports; bottom on USB), the single-tap target is in `to_delayed_action.to_if_invoked[0]` and the double-tap target is in the detector manipulator's `to[0]`. For the BT bottom path (firmware-discriminated, manipulators 11 + 12), the single-tap target is in manipulator 11's `to[0]` and the double-tap target is in manipulator 12's `to[0]` — both are simple, no delayed action.
- **Tune the double-tap window**: change `parameters.basic.to_delayed_action_delay_milliseconds` (default 200ms). Top, middle, and bottom-USB pairs tune independently. **The BT bottom path is NOT tunable from Karabiner** — its window is set by the pad's firmware.
- **Add a 4th button binding**: append a manipulator with the new `from` keycode. Reuse the same `device_if` identifiers. Test both transports in EventViewer to see whether the pad's firmware discriminates single-vs-double-tap on the new button (if it emits a different keycode on double-tap, use the firmware-translation pattern; otherwise use the software-discrimination pattern).
- **Support a new pad**: add its USB VID/PID and BT VID/PID to every manipulator's `device_if.identifiers` array. If the pad emits different keycodes, add transport-specific manipulators (don't try to alias). **Always test both transports of every button for firmware-side double-tap discrimination before assuming software discrimination is needed** — see `04-anti-patterns.md`.
## Validation
```bash
# Rule JSON parses + manipulator count is what you expect
/usr/bin/env python3 -c "import json; d=json.load(open('plugins/macro-keyboard/skills/configure-macro-keyboard/references/raw/karabiner-rule.json')); print(len(d['manipulators']))"
# Live config matches
/usr/bin/env python3 -c "
import json
- live = json.load(open('/Users/terryli/.config/karabiner/karabiner.json'))
+ live = json.load(open('/Users/<you>/.config/karabiner/karabiner.json'))
for r in live['profiles'][0]['complex_modifications']['rules']:
if r['description'].startswith('Jieli/Free3-P'):
print(len(r['manipulators']))
"
# Karabiner reloaded the file
tail -5 /var/log/karabiner/core_service.log
# Look for: "core_configuration is updated."
```