Compare commits

...

63 Commits

Author SHA1 Message Date
rootiest fe35602f65 Merge pull request 'chore: ignore ai session files in any directory' (#24) from chore/ai-sessions into main
Reviewed-on: #24
2026-04-23 21:07:01 +00:00
rootiest 7a2b47a265 chore: ignore ai session files in any directory 2026-04-23 17:05:52 -04:00
rootiest 3b497e6eba Merge pull request 'feat(q5_max): expand chord table with Greek alphabet and math symbols' (#22) from feat/chords into main
Reviewed-on: #22
2026-04-23 03:08:14 +00:00
rootiest 4a1533b189 feat(q5_max): expand chord table with Greek alphabet and math symbols
Add 21 Greek letters (gamma–psi, completing the full alphabet), six
blackboard-bold number-set symbols (ℝ ℕ ℤ ℚ ℂ ℵ), and 44 new math
entries covering calculus (∫ ∂ ∇ ∑ ∏), set theory (∈ ∉ ⊂ ⊆ ∪ ∩ ∅),
logic (∀ ∃ ∧ ∨ ⟹ ⟺), geometry (∠ ⊥ ∥), and floor/ceiling brackets.

Aliases provided for longer sequences (eps→ε, prt→∂, ups→υ, omi→ο,
ang→∠).  Disambiguation notes added for mu/mute, sub/subeq, sup/supeq.
2026-04-23 03:01:42 +00:00
rootiest e8433ba5d9 Merge pull request 'feat(q5_max): add speculative hold for caps_mod key' (#23) from feat/speculative-hold into main
Reviewed-on: #23
2026-04-23 02:59:37 +00:00
rootiest 73de490e53 feat(q5_max): Add speculative hold for caps_mod key 2026-04-22 22:36:16 -04:00
Pascal Getreuer 363b43cafd [Bug][Core] Fix Speculative Hold to enable also right-handed RSFT, RCTL by default. (#25797) 2026-04-15 03:48:33 -04:00
Pascal Getreuer 848972b7be [Core] Speculative Hold option for mod-taps: hold mods instantly while unsettled. (#25572) 2026-04-15 03:48:26 -04:00
rootiest 20a605ce1f chore(claude): Adds voice output rules to CLAUDE.md 2026-04-15 03:39:41 -04:00
rootiest 0fa528b3dd feat(q5_max): add CMD_BATTERY HID push and query for wireless battery level
Adds a new HID_CMD_BATTERY (0x44) command to the shared qmk-host protocol.
When the keyboard is in wireless transport mode (BT or 2.4G) the firmware
now pushes the current battery percentage to the host:

- Periodic push every 5 minutes via timer_read32()/timer_elapsed32(); fires
  immediately on the first matrix_scan_user() call after boot so qmk-host
  gets a reading as soon as the keyboard connects over USB in wireless mode.
- Redundant sends are suppressed: g_last_sent_bat tracks the last value
  transmitted and the packet is skipped when the level has not changed.
- Query-response path: qmk-host can send CMD_BATTERY with HID_FLAG_QUERY at
  any time; the keyboard replies with the current percentage or
  HID_BATT_UNAVAILABLE (0xFF) when in USB transport mode.
- All battery and transport code is guarded by #ifdef LK_WIRELESS_ENABLE so
  the keymap compiles cleanly on any wired-only build.

Firmware size delta: +160 bytes (well within flash budget).
2026-04-15 03:37:18 -04:00
rootiest 66c73eef89 Merge pull request 'fix(q5_max): switch fallback combo to LCK_BASE for reliable layer recovery' (#21) from fix/combo-key into main
Reviewed-on: #21
2026-04-14 02:09:56 +00:00
rootiest c498d5025c fix(q5_max): ensure fallback combo overrides layer locks
Change the fallback combo's action from TO(BASE) to LCK_BASE.
Since LCK_BASE explicitly clears the locked_layers bitmask,
it ensures that the keyboard actually returns to the BASE
layer even if a layer was previously locked.

The previous TO(BASE) action only updated the temporary layer
state, which was immediately overwritten by the persistent
locked_layers bitmask in layer_state_set_user().
2026-04-13 21:40:20 -04:00
rootiest ff45e8e0d6 Merge pull request 'feat(k17_max): enable Keychron RGB and fix EEPROM persistence' (#20) from dev/k17 into main
Reviewed-on: #20
2026-04-13 17:43:19 +00:00
rootiest 1b0e83b1d6 feat(k17_max): enable Keychron RGB and fix EEPROM persistence
Port the Keychron RGB EEPROM persistence work from the Q5 Max (PR #18)
to the K17 Max ANSI Encoder RGB variant.

- Add KEYCHRON_RGB_ENABLE = yes to rules.mk
- Define default_per_key_led[] (red Esc/CapsLock, yellow modifiers and
  numpad, blue alpha keys) and default_region[] for the 103-LED layout
  in ansi_encoder/rgb/rgb.c; both are extern'd by keychron_rgb.c
- Add wireless_enter_connected_kb() hook and eeconfig_init_custom_rgb()
  call in keyboard_post_init_kb() in k17_max.c, matching the Q5 Max
  pattern for boot-time and transport-change RGB persistence
- Define VIA_EEPROM_MAGIC_ADDR 552 in ansi_encoder/rgb/config.h to pin
  VIA keymap storage past the Keychron EEPROM data region end (540 for
  K17 with 103 LEDs); prevents keymap corruption if the KB data block
  grows in future
2026-04-13 13:20:47 -04:00
rootiest ad41de2f54 Merge pull request 'docs: expand CLAUDE.md with K17 Max, branch names, and EEPROM notes' (#19) from chore/claude-docs into main
Reviewed-on: #19
2026-04-13 17:02:11 +00:00
rootiest b4a76db6f3 docs: expand CLAUDE.md with K17 Max, branch names, and EEPROM notes
- Rename dev branch references from q5_dev to dev/q5; add dev/k17 for
  K17 Max work
- Add K17 Max build/flash commands using the correct rgb subvariant path
  (keychron/k17_max/ansi_encoder/rgb) to distinguish from the white LED
  variant
- Add EEPROM Layout Notes section documenting the Q5 Max address map,
  VIA_EEPROM_MAGIC_ADDR pinning rules, and EECONFIG_KB_DATA_SIZE #undef
  requirement
- Add Keychron RGB section capturing lessons from the persistence work:
  when to call eeconfig_init_custom_rgb(), version stamp placement,
  kc_rgb_save() mode persistence, retail_demo_enable 0xFF bug, and
  extern array requirements
- Add DIP Switch section explaining the frame overlay approach and why
  direct rgb_matrix_mode() calls must be avoided
2026-04-13 12:57:14 -04:00
rootiest 2da8bab7a5 Merge pull request 'feat(q5_max): enable Keychron RGB and fix EEPROM persistence' (#18) from feat/keychron-rgb-persistence into main
Reviewed-on: #18
2026-04-13 16:37:06 +00:00
rootiest c93249f1b2 feat(q5_max): enable Keychron RGB and fix EEPROM persistence
Enable KEYCHRON_RGB_ENABLE for the Q5 Max, wiring up PER_KEY_RGB and
MIXED_RGB effects, and fix a cascade of EEPROM bugs that caused the
Launcher-configured RGB mode to revert to the default heatmap on every
power cycle and wireless transport change.

Keychron RGB enablement:
- Add KEYCHRON_RGB_ENABLE = yes to rules.mk
- Define default_per_key_led[] and default_region[] for the ANSI Encoder
  layout in ansi_encoder.c (extern'd by keychron_rgb.c)
- Fix missing #include "eeconfig_custom_rgb.h" in mixed_rgb.c and
  rgb_matrix_kb.inc so EECONFIG_SIZE_CUSTOM_RGB is in scope for the
  compile guards that gate the custom effects
- Add #undef EECONFIG_KB_DATA_SIZE before the Keychron redefinition in
  eeconfig_kb.h to suppress redefinition of QMK's default-zero value

EEPROM persistence fixes (keychron_rgb.c):
- Fix retail_demo_enable never being written to EEPROM in
  eeconfig_reset_custom_rgb(): original code used eeprom_read_block
  instead of eeprom_update_block, leaving 0xFF on freshly-flashed
  boards; retail_demo_task() treats any non-zero value as "demo active"
  and forces the mode to CUSTOM_MIXED_RGB every scan
- Clamp retail_demo_enable > 1 to 0 on load to recover boards already
  affected by the above bug
- Move EECONFIG_KEYBOARD version stamp from eeconfig_init_custom_rgb()
  (load path) to eeconfig_reset_custom_rgb() (reset/write path) so the
  version is only stamped when valid defaults are actually written
- Call eeconfig_update_rgb_matrix() in kc_rgb_save() so the current QMK
  RGB mode is persisted alongside Keychron data; without this,
  rgb_matrix_init() (called on every transport change) reloads the
  compile-time default RGB_MATRIX_TYPING_HEATMAP from EEPROM

Transport-change persistence (q5_max.c):
- Call eeconfig_init_custom_rgb() in keyboard_post_init_kb() so Keychron
  RGB arrays are loaded from EEPROM on every boot instead of being
  zero-initialised
- Add wireless_enter_connected_kb() hook: re-applies the EEPROM-saved
  QMK RGB mode after BT/2.4G reconnect in case the reconnect sequence
  resets the in-RAM mode before the display settles

DIP switch Win-side override (keymap.c):
- Replace rgb_matrix_mode() / rgb_matrix_sethsv() calls (which write to
  EEPROM and permanently overwrite the Launcher mode) with a
  dip_win_active flag; rgb_matrix_indicators_advanced_user() paints all
  LEDs white each frame when the flag is set, leaving the active effect
  and EEPROM untouched

VIA keymap address pinning (config.h):
- Define VIA_EEPROM_MAGIC_ADDR 544 to anchor VIA keymap storage at a
  fixed EEPROM offset; without this, growth in EECONFIG_KB_DATA_SIZE
  silently shifts the keymap block, corrupting stored layouts (observed
  as layer-0 keys reverting to KC_TRNS / KC_NONE on boot)
2026-04-13 12:32:10 -04:00
rootiest 1d60306b66 Merge pull request 'feat(q5_max): map dip-switch to RGB effects (heatmap / solid white)' (#17) from feat/rbg-switch into main 2026-04-11 06:16:09 +00:00
rootiest 0cf22be07c feat(q5_max): map dip-switch to RGB effects (heatmap / solid white)
Replace the Win/Mac default-layer switch with an RGB effect toggle:
- Win side → solid white backlight
- Mac side → typing heatmap

Add a weak dip_switch_update_keymap() hook in q5_max.c to work around
factory_test.c already owning dip_switch_update_user().
2026-04-11 02:14:34 -04:00
rootiest eedf234ab3 Merge pull request 'fix(q5_max): revert backspace to KC_BSPC, keep BSP_DEL for later use' (#16) from fix/shft-bkspc into main 2026-04-11 05:01:42 +00:00
rootiest 5764e38e73 Merge pull request 'feat(q5_max): add per-key tapping term with 175ms for home/end tap-dance' (#15) from feat/tap-term into main 2026-04-11 05:01:37 +00:00
rootiest 03765e0faa Merge pull request 'feat(q5_max): shift-backspace sends Delete (BSP_DEL)' (#14) from feat/shft-bkspc into main 2026-04-11 05:01:32 +00:00
rootiest f9972b2fee Merge pull request 'feat(k17): add tap-chord dance to K17 Max' (#13) from feat/k17/tap-chords into main 2026-04-11 05:01:26 +00:00
rootiest bc1556547d Merge pull request 'feat(q5_max): implement bi-directional Raw HID protocol' (#12) from feat/raw-hid into main 2026-04-11 05:01:21 +00:00
rootiest 287a0d1cbc Merge pull request 'fix(caps-mod): fix errant modifier taps and dummy keypress for combos' (#10) from fix/caps-mod into main 2026-04-11 04:52:40 +00:00
rootiest eaa284d557 Merge pull request 'chore(dict): add autocorrect dictionary entries' (#9) from chore/dict into main 2026-04-11 04:52:34 +00:00
rootiest 1c2886c052 Merge pull request 'chore: use direnv to manage python venv' (#8) from chore/dir-env into main 2026-04-11 04:52:28 +00:00
rootiest 622a20bb0d fix(q5): switch backspace back to normal
Keep the BSP_DEL for later use.
2026-04-11 00:48:18 -04:00
rootiest 49af06fcac feat(q5_max): add per-key tapping term with 50ms for home/end tap-dance
Define TAPPING_TERM (200ms default) and TAPPING_TERM_PER_KEY in config.h,
then implement get_tapping_term() to set a tight 50ms window for TD_HOME_END
so a single Home tap never accidentally resolves as End.
2026-04-11 00:48:15 -04:00
rootiest 52e75ecc6a feat(q5_max): add shift-backspace → delete key (BSP_DEL)
Replaces KC_BSPC on the BASE layer with a custom BSP_DEL keycode:
tap sends Backspace, Shift+tap strips shift and sends Delete.
2026-04-11 00:07:54 -04:00
rootiest 5f9121c0d2 fix(k17): remove dead custom keycode stubs from tap-chord dance
CK_CTRL_K_C and CK_CTRL_K_D were leftover from an earlier failed
attempt using ACTION_TAP_DANCE_DOUBLE with custom keycodes. Now that
TD_CHORDS uses ACTION_TAP_DANCE_FN with SEND_STRING, they are unused.
2026-04-11 00:07:52 -04:00
rootiest 6d6eee5571 feat(k17): add tap-chords to keychron k17_max 2026-04-10 23:13:36 -04:00
rootiest df671b656b fix(q5_max): fix RAW_EPSIZE and via_command_kb hook conflict
Two build errors:

1. RAW_EPSIZE undeclared — usb_descriptor.h is not in scope when
   keymap.c is compiled through Keychron's build path. Replace all
   uses with HID_PACKET_SIZE (= 32), now defined in hid_protocol.h.

2. via_command_kb duplicate symbol — keychron_raw_hid.c already defines
   via_command_kb (non-weak) so we cannot redefine it in keymap.c.

Fix by adding a weak kc_raw_hid_rx_kb() extension hook to
keychron_raw_hid.c (following the same pattern as kc_rgb_matrix_rx).
kc_raw_hid_rx() now calls this hook from its default case instead of
returning false directly. The keymap overrides kc_raw_hid_rx_kb() to
handle our custom HID command range (0x40-0x7E).
2026-04-10 15:10:14 -04:00
rootiest 6fd5179c55 feat(q5_max): implement bi-directional Raw HID protocol
Add hid_protocol.h defining a shared 32-byte packet structure for the
qmk-host bridge application (command IDs 0x40-0x7E, clear of VIA's
range). Implement via_command_kb() in keymap.c to intercept incoming
packets: LAYER_SYNC applies a new active layer, VOLUME and BRIGHTNESS
store host-reported values for future RGB indicators, and ACTIVE_APP
is stubbed for a later commit. Layer state changes are broadcast to the
host via raw_hid_send() from layer_state_set_user(), guarded by
g_hid_recv_active to prevent echo loops when the change itself
originated from HID.
2026-04-10 14:41:58 -04:00
rootiest 9b2cd0cb32 chore: ignore gemini session file
Session file is only relevant during development and unique to the local
machine.
2026-04-10 14:19:59 -04:00
rootiest d4bd903423 fix(chatter): fix key chatter with stricter tap terms and debounce 2026-04-10 14:19:09 -04:00
rootiest c0d868dca3 fix(caps-mod) implement dummy keypress for capsmod combos
Send KC_F24 when modifiers are used with CAPS_MOD to avoid the modifiers
being treated as lone taps.
2026-04-09 00:28:34 -04:00
rootiest d8dccd67a6 fix(caps-mod): fixes errant taps of modifiers
Prevents modifiers (GUI and ALT) from firing a tap signal when they are
used in conjunction with the CAPS_MOD key.
2026-04-09 00:19:04 -04:00
rootiest 360e642fcf chore(dict): add thiink and fiish to dictionary 2026-04-09 00:16:49 -04:00
rootiest 4cfa85ee8e chore(dict): generate dictionary file
Create a autocorrect_dictionary.txt file which is used to generate the
autocorrect_data.h file.
2026-04-09 00:16:20 -04:00
rootiest 41fe2a11bd chore: use direnv to manage python venv 2026-04-09 00:12:20 -04:00
rootiest d52d55e442 Merge pull request 'chore: cleanup dev artifacts — CLAUDE.md update and remove superpowers notes' (#7) from chore/cleanup-dev-artifacts into main 2026-04-08 05:08:20 +00:00
rootiest aad85d9930 chore: remove superpowers development notes
Features are merged with descriptive commit messages and PR summaries;
these ephemeral planning docs are no longer needed.
2026-04-08 01:07:58 -04:00
rootiest 0c28ba889f docs(CLAUDE.md): document delete-on-merge pattern for stacked PRs 2026-04-08 01:07:58 -04:00
rootiest 846d07966f Merge pull request 'chore/feat(q5_max): gitignore update, layer control, and CAPS_MOD' (#6) from feat/layer-control into main 2026-04-08 04:57:09 +00:00
rootiest 5c0fb31e41 Merge pull request 'feat(q5_max): CAPS_MOD — multi-function CapsLock key with CapsWord, Autocorrect, and RGB indicators' (#4) from feat/caps-mod into feat/layer-control 2026-04-08 04:50:36 +00:00
rootiest 14747159fa Merge pull request 'chore: remove upstream GitHub Actions workflows' (#5) from chore/remove-actions into main 2026-04-08 04:50:17 +00:00
rootiest d2a67b2bb3 fix(q5_max): invert autocorrect LED indicator — purple when autocorrect is OFF 2026-04-08 00:47:29 -04:00
rootiest 0210caf90e feat(q5_max): add RGB indicators for CapsWord/Autocorrect/CapsLock on LED 55 2026-04-08 00:47:29 -04:00
rootiest 6045cfa866 fix(q5_max): add clarifying comments to CAPS_MOD release handler 2026-04-08 00:47:29 -04:00
rootiest b77eea4192 feat(q5_max): implement CAPS_MOD tap/hold logic (ESC/Ctrl/CapsLock/CapsWord/Autocorrect) 2026-04-08 00:47:29 -04:00
rootiest bab766887a fix(q5_max): restore void casts for CAPS_MOD vars (QMK uses -Werror=unused-variable) 2026-04-08 00:47:28 -04:00
rootiest a2ef1712ab feat(q5_max): add CAPS_MOD keycode and replace KC_CAPS in all layers 2026-04-08 00:47:28 -04:00
rootiest 0865d7ed26 docs: update capslock-mod plan to reflect autocorrect_data.h already defines AUTOCORRECT_MIN_LENGTH 2026-04-08 00:47:28 -04:00
rootiest 3d283cdf48 fix(q5_max): remove duplicate AUTOCORRECT_MIN_LENGTH define from config.h 2026-04-08 00:47:28 -04:00
rootiest aeae6d485d feat(q5_max): enable CAPS_WORD and AUTOCORRECT features 2026-04-08 00:47:28 -04:00
rootiest 926628bc4f docs: add Caps Lock mod design spec
Spec for CAPS_MOD custom keycode: tap=ESC, hold=Ctrl, Shift+tap=CapsLock,
Alt+tap=CapsWord, GUI+tap=Autocorrect toggle. Includes RGB indicators and
feature enablement plan.
2026-04-08 00:47:28 -04:00
rootiest 45276ac042 feat(q5_max): add layer-lock keycodes for persistent layer activation
Introduces LCK_FN1–LCK_FN4, LCK_CTL, and LCK_BASE custom keycodes that
toggle a layer into a locked state so it stays active after momentary keys
are released. A second press on the same lock key or pressing LCK_BASE
clears all locks and returns to BASE.
2026-04-08 00:47:22 -04:00
rootiest 423e544097 feat(q5_max): add ESC key RGB indicator for active layer
Implements rgb_matrix_indicators_advanced_user to colour the ESC key
(LED 0) based on the current layer: dark on BASE, blue on FN1, green
on FN2, orange on FN3, purple on FN4, and red on KEEB_CTL.
2026-04-08 00:47:22 -04:00
rootiest 86cad58f34 feat(q5_max): add COMM+DOT+SLSH fallback combo to return to BASE
Adds a three-key combo (,  .  /) that fires TO(BASE), providing an
emergency escape hatch when stuck on an unknown layer. COMBO_ONLY_FROM_LAYER
is set to 0 in the keymap config.h so keycodes are always resolved from
BASE, making the combo reliable regardless of the active layer.
2026-04-08 00:47:22 -04:00
rootiest e897c73ff4 chore: ignore Claude session and session memory ID files
Pull Request Labeler / triage (pull_request) Successful in 4s
2026-04-08 00:47:06 -04:00
rootiest 949d34941a chore: remove upstream GitHub Actions workflows
Pull Request Labeler / triage (pull_request) Successful in 4s
Keychron's CI workflows don't apply to this personal fork and trigger
unwanted runs. Remove the entire .github/workflows directory.
2026-04-08 00:46:53 -04:00
74 changed files with 15733 additions and 967 deletions
+18
View File
@@ -0,0 +1,18 @@
<!-- TTS_VOICE_OUTPUT_START -->
## Voice Output (TTS)
When responding, wrap your natural language prose in `«tts»...«/tts»` markers for text-to-speech.
Rules:
- ONLY wrap conversational prose meant to be spoken aloud
- Do NOT wrap: code, file paths, commands, tool output, URLs, lists, errors
- Keep markers on same line as text (no line breaks inside)
Examples:
✓ «tts»I'll help you fix that bug.«/tts»
✓ «tts»The tests are passing.«/tts» Here's what changed:
✗ «tts»src/Header.tsx«/tts» (file path - don't wrap)
✗ «tts»npm install«/tts» (command - don't wrap)
<!-- TTS_VOICE_OUTPUT_END -->
+5
View File
@@ -0,0 +1,5 @@
# Set the root of the virtual environment
export VIRTUAL_ENV=$PWD/.venv
# Add the venv's bin folder to the start of your PATH
PATH_add .venv/bin
-41
View File
@@ -1,41 +0,0 @@
name: Bug report
description: Create a report to help us improve QMK Firmware.
title: "[Bug] "
labels: ["bug", "help wanted"]
body:
- type: markdown
attributes:
value: |
Provide a general summary of the bug in the title above.
- type: textarea
attributes:
label: Describe the Bug
description: A clear and concise description of what the bug is.
- type: input
attributes:
label: Keyboard Used
description: The name of the keyboard from the `make` or `qmk compile`/`qmk flash` commands, eg. `planck/rev6`.
- type: input
attributes:
label: Link to product page (if applicable)
- type: input
attributes:
label: Operating System
- type: textarea
attributes:
label: qmk doctor Output
description: Output from running the `qmk doctor` command.
render: text
- type: checkboxes
attributes:
label: Is AutoHotKey / Karabiner installed
options:
- label: AutoHotKey (Windows)
- label: Karabiner (macOS)
- type: input
attributes:
label: Other keyboard-related software installed
- type: textarea
attributes:
label: Additional Context
description: Add any other relevant information about the problem here.
-8
View File
@@ -1,8 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: QMK Discord
url: https://discord.gg/Uq7gcHh
about: Ask questions, discuss issues and features. Chill.
- name: OLKB Subreddit
url: https://www.reddit.com/r/olkb
about: All things OLKB and QMK.
@@ -1,24 +0,0 @@
name: Feature request
description: Suggest a new feature or changes to existing features.
title: "[Feature Request] "
labels: ["enhancement", "help wanted"]
body:
- type: markdown
attributes:
value: |
Provide a general summary of the changes you want in the title above.
Please refrain from asking maintainers to add support for specific keyboards -- it is unlikely they will have hardware available, and will not be able to help.
Your best bet is to take the initiative, add support, then submit a PR yourself.
- type: checkboxes
attributes:
label: Feature Request Type
options:
- label: Core functionality
- label: Add-on hardware support (eg. audio, RGB, OLED screen, etc.)
- label: Alteration (enhancement/optimization) of existing feature(s)
- label: New behavior
- type: textarea
attributes:
label: Description
description: A few sentences describing what it is that you'd like to see in QMK. Additional information (such as links to spec sheets, licensing info, other related issues or PRs, etc) would be helpful.
-19
View File
@@ -1,19 +0,0 @@
name: Other issues
description: Anything else that doesn't fall into the above categories.
labels: ["help wanted", "question"]
body:
- type: markdown
attributes:
value: |
Provide a general summary of the changes you want in the title above.
- type: markdown
attributes:
value: |
Please check [https://docs.qmk.fm/#/support](https://docs.qmk.fm/#/support) for additional resources first. If that doesn't answer your question, choose the bug report template instead, as that may be more appropriate.
Please refrain from asking maintainers to add support for specific keyboards -- it is unlikely they will have hardware available, and will not be able to help.
Your best bet is to take the initiative, add support, then submit a PR yourself.
- type: textarea
attributes:
label: Issue Description
description: Describe your issue in as much detail as possible.
-11
View File
@@ -1,11 +0,0 @@
---
name: Blank issue
about: If you're 100% sure that you don't need one of the other issue templates, use
this one instead.
title: ''
labels: help wanted, question
assignees: ''
---
-35
View File
@@ -1,35 +0,0 @@
<!--- Provide a general summary of your changes in the title above. -->
<!--- This template is entirely optional and can be removed, but is here to help both you and us. -->
<!--- Anything on lines wrapped in comments like these will not show up in the final text. -->
## Description
<!--- Describe your changes in detail here. -->
## Types of Changes
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply. -->
- [ ] Core
- [ ] Bugfix
- [ ] New feature
- [ ] Enhancement/optimization
- [ ] Keyboard (addition or update)
- [ ] Keymap/layout/userspace (addition or update)
- [ ] Documentation
## Issues Fixed or Closed by This PR
*
## Checklist
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [ ] My code follows the code style of this project: [**C**](https://docs.qmk.fm/#/coding_conventions_c), [**Python**](https://docs.qmk.fm/#/coding_conventions_python)
- [ ] I have read the [**PR Checklist** document](https://docs.qmk.fm/#/pr_checklist) and have made the appropriate changes.
- [ ] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly.
- [ ] I have read the [**CONTRIBUTING** document](https://docs.qmk.fm/#/contributing).
- [ ] I have added tests to cover my changes.
- [ ] I have tested the changes and verified that they work and don't break anything (as well as I can manage).
-9
View File
@@ -1,9 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
labels: CI
reviewers:
- "qmk/collaborators"
schedule:
interval: "daily"
-46
View File
@@ -1,46 +0,0 @@
core:
- quantum/**/*
- tmk_core/**/*
- drivers/**/*
- tests/**/*
- util/**/*
- platforms/**/*
- builddefs/**/*
- Makefile
- '*.mk'
dependencies:
- any:
- 'lib/**/*'
- '!lib/python/**/*'
keyboard:
- any:
- 'keyboards/**/*'
- '!keyboards/**/keymaps/**/*'
keymap:
- users/**/*
- layouts/**/*
- keyboards/**/keymaps/**/*
via:
- keyboards/**/keymaps/via/*
cli:
- requirements.txt
- lib/python/**/*
python:
- '**/*.py'
documentation:
- docs/**/*
translation:
- docs/fr-fr/**/*
- docs/es/**/*
- docs/ja/**/*
- docs/he-il/**/*
- docs/pt-br/**/*
- docs/zh-cn/**/*
- docs/de/**/*
- docs/ru-ru/**/*
CI:
- .github/**/*
dd:
- data/constants/**/*
- data/mappings/**/*
- data/schemas/**/*
-50
View File
@@ -1,50 +0,0 @@
name: Update API Data
permissions:
contents: read
on:
push:
branches:
- master
- develop
paths:
- 'keyboards/**'
- 'layouts/community/**'
- 'lib/python/**'
- 'data/**'
- '.github/workflows/api.yml'
workflow_dispatch:
jobs:
api_data:
runs-on: ubuntu-latest
container: ghcr.io/qmk/qmk_cli
# protect against those who work in their fork on 'important' branches
if: github.repository == 'qmk/qmk_firmware'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
persist-credentials: false
- name: Install dependencies
run: |
pip3 install -r requirements-dev.txt
- name: Generate API Data
run: |
qmk generate-api
- name: Upload API Data
uses: jakejarvis/s3-sync-action@master
with:
args: --acl public-read --follow-symlinks --delete
env:
AWS_S3_BUCKET: ${{ github.ref == 'refs/heads/develop' && secrets['API_SPACE_DEVELOP'] || secrets['API_SPACE_MASTER'] }}
AWS_ACCESS_KEY_ID: ${{ secrets.SPACES_ACCESS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.SPACES_SECRET_KEY }}
AWS_S3_ENDPOINT: https://nyc3.digitaloceanspaces.com
SOURCE_DIR: '.build/api_data'
-20
View File
@@ -1,20 +0,0 @@
name: Automatic Approve
permissions: {}
on:
schedule:
- cron: "*/5 * * * *"
jobs:
automatic_approve:
runs-on: ubuntu-latest
if: github.repository == 'qmk/qmk_firmware'
steps:
- uses: mheap/automatic-approve-action@v1
with:
token: ${{ secrets.QMK_BOT_TOKEN }}
workflows: "format.yml,lint.yml,unit_test.yml"
dangerous_files: "lib/python/,Makefile,paths.mk,builddefs/"
-38
View File
@@ -1,38 +0,0 @@
name: Essential files modified
permissions:
contents: write
on:
workflow_dispatch:
push:
branches:
- master
paths:
- builddefs/**/*
- drivers/**/*
- platforms/**/*
- quantum/**/*
- tests/**/*
- tmk_core/**/*
- util/**/*
- Makefile
- '*.mk'
jobs:
tag:
runs-on: ubuntu-latest
# protect against those who develop with their fork on master
if: github.repository == 'qmk/qmk_firmware'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Bump version and push tag
uses: anothrNick/github-tag-action@1.66.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DEFAULT_BUMP: 'patch'
-74
View File
@@ -1,74 +0,0 @@
name: CI Builds
permissions:
contents: read
on:
push:
branches: [master, develop]
workflow_dispatch:
inputs:
branch:
type: choice
description: 'Branch to build'
options: [master, develop]
concurrency: ci_build-${{ github.event.inputs.branch || github.ref_name }}
jobs:
ci_builds:
if: github.repository == 'qmk/qmk_firmware'
name: "CI Build"
runs-on: self-hosted
timeout-minutes: 1380
strategy:
fail-fast: false
matrix:
keymap: [default, via]
container: ghcr.io/qmk/qmk_cli
steps:
- name: Disable safe.directory check
run : git config --global --add safe.directory '*'
- uses: actions/checkout@v4
with:
submodules: recursive
ref: ${{ github.event.inputs.branch || github.ref }}
- name: Install dependencies
run: pip3 install -r requirements.txt
- name: Run `qmk mass-compile` (keymap ${{ matrix.keymap }})
run: |
export NCPUS=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || getconf _NPROCESSORS_ONLN 2>/dev/null)
qmk mass-compile -t -j $NCPUS -km ${{ matrix.keymap }} -e DUMP_CI_METADATA=yes || touch .failed
# Generate the step summary markdown
./util/ci/generate_failure_markdown.sh > $GITHUB_STEP_SUMMARY || true
# Truncate to a maximum of 1MB to deal with GitHub workflow limit
truncate --size='<960K' $GITHUB_STEP_SUMMARY || true
# Exit with failure if the compilation stage failed
[ ! -f .failed ] || exit 1
- name: 'Upload artifacts'
uses: actions/upload-artifact@v4
if: always()
with:
name: artifacts-${{ github.event.inputs.branch || github.ref_name }}-${{ matrix.keymap }}
if-no-files-found: ignore
path: |
*.bin
*.hex
*.uf2
.build/failed.*
- name: 'CI Discord Notification'
if: always()
working-directory: util/ci/
env:
DISCORD_WEBHOOK: ${{ secrets.CI_DISCORD_WEBHOOK }}
run: |
python3 -m pip install -r requirements.txt
python3 ./discord-results.py --branch ${{ github.event.inputs.branch || github.ref_name }} --keymap ${{ matrix.keymap }} --url ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
-34
View File
@@ -1,34 +0,0 @@
name: CLI CI
permissions:
contents: read
on:
push:
branches:
- master
- develop
pull_request:
paths:
- 'lib/python/**'
- 'requirements.txt'
- '.github/workflows/cli.yml'
jobs:
test:
runs-on: ubuntu-latest
container: ghcr.io/qmk/qmk_cli
steps:
- name: Disable safe.directory check
run : git config --global --add safe.directory '*'
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install dependencies
run: pip3 install -r requirements-dev.txt
- name: Run tests
run: qmk pytest
-37
View File
@@ -1,37 +0,0 @@
name: Update develop after master merge
permissions:
contents: write
on:
push:
branches:
- master
jobs:
develop_update:
runs-on: ubuntu-latest
if: github.repository == 'qmk/qmk_firmware'
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.QMK_BOT_TOKEN }}
fetch-depth: 0
- name: Disable automatic eol conversion
run: |
echo "* -text" > .git/info/attributes
- name: Checkout develop
run: |
git fetch origin master develop
git checkout develop
- name: Update develop from master
run: |
git config --global user.name "QMK Bot"
git config --global user.email "hello@qmk.fm"
git merge origin/master
git push origin develop
-46
View File
@@ -1,46 +0,0 @@
name: Generate Docs
permissions:
contents: write
on:
push:
branches:
- master
paths:
- 'tmk_core/**'
- 'quantum/**'
- 'platforms/**'
- 'docs/**'
- '.github/workflows/docs.yml'
jobs:
generate:
runs-on: ubuntu-latest
container: ghcr.io/qmk/qmk_cli
# protect against those who develop with their fork on master
if: github.repository == 'qmk/qmk_firmware'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Install dependencies
run: |
apt-get update && apt-get install -y rsync nodejs npm doxygen
npm install -g moxygen
- name: Build docs
run: |
qmk --verbose generate-docs
- name: Deploy
uses: JamesIves/github-pages-deploy-action@v4.5.0
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BASE_BRANCH: master
BRANCH: gh-pages
FOLDER: .build/docs
GIT_CONFIG_EMAIL: hello@qmk.fm
@@ -1,43 +0,0 @@
name: Update feature branches after develop merge
permissions:
contents: write
on:
push:
branches:
- develop
jobs:
feature_branch_update:
runs-on: ubuntu-latest
if: github.repository == 'qmk/qmk_firmware'
strategy:
matrix:
branch:
- xap
- riot
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.QMK_BOT_TOKEN }}
fetch-depth: 0
- name: Disable automatic eol conversion
run: |
echo "* -text" > .git/info/attributes
- name: Checkout branch
run: |
git fetch origin develop ${{ matrix.branch }}
git checkout ${{ matrix.branch }}
- name: Update branch from develop
run: |
git config --global user.name "QMK Bot"
git config --global user.email "hello@qmk.fm"
git merge origin/develop
git push origin ${{ matrix.branch }}
-55
View File
@@ -1,55 +0,0 @@
name: PR Lint Format
permissions:
contents: read
on:
pull_request:
paths:
- 'drivers/**'
- 'lib/arm_atsam/**'
- 'lib/lib8tion/**'
- 'lib/python/**'
- 'platforms/**'
- 'quantum/**'
- 'tests/**'
- 'tmk_core/**'
jobs:
lint:
runs-on: ubuntu-latest
container: ghcr.io/qmk/qmk_cli
steps:
- name: Disable safe.directory check
run : git config --global --add safe.directory '*'
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install dependencies
run: |
pip3 install -r requirements-dev.txt
- name: Get changed files
id: file_changes
uses: tj-actions/changed-files@v40
- name: Run qmk formatters
shell: 'bash {0}'
run: |
echo '${{ steps.file_changes.outputs.added_files}}' '${{ steps.file_changes.outputs.modified_files}}' > ~/files_changed.txt
qmk format-c --core-only $(< ~/files_changed.txt) || true
qmk format-python $(< ~/files_changed.txt) || true
qmk format-text $(< ~/files_changed.txt) || true
- name: Fail when formatting required
run: |
git diff
for file in $(git diff --name-only); do
echo "File '${file}' Requires Formatting"
echo "::error file=${file}::Requires Formatting"
done
test -z "$(git diff --name-only)"
-59
View File
@@ -1,59 +0,0 @@
name: Lint Format
permissions:
contents: read
on:
push:
branches:
- master
- develop
jobs:
lint:
runs-on: ubuntu-latest
container: ghcr.io/qmk/qmk_cli
steps:
- name: Disable safe.directory check
run : git config --global --add safe.directory '*'
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Disable automatic eol conversion
run: |
echo "* -text" > .git/info/attributes
- name: Install dependencies
run: |
pip3 install -r requirements-dev.txt
- name: Run qmk formatters
shell: 'bash {0}'
run: |
qmk format-c -a
qmk format-python -a
qmk format-text -a
git diff
- uses: rlespinasse/github-slug-action@v3.x
- name: Become QMK Bot
run: |
git config user.name 'QMK Bot'
git config user.email 'hello@qmk.fm'
- name: Create Pull Request
uses: peter-evans/create-pull-request@v5
if: ${{ github.repository == 'qmk/qmk_firmware'}}
with:
token: ${{ secrets.QMK_BOT_TOKEN }}
delete-branch: true
branch: bugfix/format_${{ env.GITHUB_REF_SLUG }}
author: QMK Bot <hello@qmk.fm>
committer: QMK Bot <hello@qmk.fm>
commit-message: Format code according to conventions
title: '[CI] Format code according to conventions'
-18
View File
@@ -1,18 +0,0 @@
name: "Pull Request Labeler"
permissions:
contents: read
pull-requests: write
on:
pull_request_target:
types: [opened, synchronize, reopened, ready_for_review, locked]
jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v4
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
configuration-path: '.github/labeler.yml'
-83
View File
@@ -1,83 +0,0 @@
name: PR Lint keyboards
permissions:
contents: read
on:
pull_request:
paths:
- 'keyboards/**'
jobs:
lint:
runs-on: ubuntu-latest
container: ghcr.io/qmk/qmk_cli
steps:
- name: Disable safe.directory check
run : git config --global --add safe.directory '*'
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install dependencies
run: pip3 install -r requirements-dev.txt
- name: Get changed files
id: file_changes
uses: tj-actions/changed-files@v40
- name: Print info
run: |
git rev-parse --short HEAD
echo ${{ github.event.pull_request.base.sha }}
echo '${{ steps.file_changes.outputs.all_changed_files}}'
- name: Run qmk lint
if: always()
shell: 'bash {0}'
run: |
QMK_CHANGES=$(echo -e '${{ steps.file_changes.outputs.all_changed_files}}' | sed 's/ /\n/g')
QMK_KEYBOARDS=$(qmk list-keyboards)
exit_code=0
for KB in $QMK_KEYBOARDS; do
KEYBOARD_CHANGES=$(echo "$QMK_CHANGES" | grep -E '^(keyboards/'${KB}'/)')
if [[ -z "$KEYBOARD_CHANGES" ]]; then
# skip as no changes for this keyboard
continue
fi
KEYMAP_ONLY=$(echo "$KEYBOARD_CHANGES" | grep -cv /keymaps/)
if [[ $KEYMAP_ONLY -gt 0 ]]; then
echo "linting ${KB}"
qmk lint --keyboard ${KB} && qmk info -l --keyboard ${KB}
exit_code=$(($exit_code + $?))
fi
done
qmk format-text ${{ steps.file_changes.outputs.all_changed_files}} || true
for file in ${{ steps.file_changes.outputs.all_changed_files}}; do
if ! git diff --quiet $file; then
echo "File '${file}' Requires Formatting"
echo "::error file=${file}::Requires Formatting"
exit_code=$(($exit_code + 1))
fi
done
if [[ $exit_code -gt 255 ]]; then
exit 255
fi
exit $exit_code
- name: Verify keyboard aliases
if: always()
shell: 'bash {0}'
run: |
git reset --hard
git clean -xfd
qmk ci-validate-aliases
-36
View File
@@ -1,36 +0,0 @@
name: PR Regenerate Files
permissions:
contents: read
on:
pull_request:
paths:
- 'data/constants/**'
- 'lib/python/**'
jobs:
regen:
runs-on: ubuntu-latest
container: ghcr.io/qmk/qmk_cli
steps:
- name: Disable safe.directory check
run : git config --global --add safe.directory '*'
- uses: actions/checkout@v4
- name: Run qmk generators
run: |
util/regen.sh
git diff
- name: Fail when regeneration required
run: |
git diff
for file in $(git diff --name-only); do
echo "File '${file}' Requires Regeneration"
echo "::error file=${file}::Requires Regeneration"
done
test -z "$(git diff --name-only)"
-46
View File
@@ -1,46 +0,0 @@
name: Regenerate Files
permissions:
contents: write
on:
push:
branches:
- master
- develop
jobs:
regen:
runs-on: ubuntu-latest
container: ghcr.io/qmk/qmk_cli
steps:
- name: Disable safe.directory check
run : git config --global --add safe.directory '*'
- uses: actions/checkout@v4
- name: Run qmk generators
run: |
util/regen.sh
git diff
- uses: rlespinasse/github-slug-action@v3.x
- name: Become QMK Bot
run: |
git config user.name 'QMK Bot'
git config user.email 'hello@qmk.fm'
- name: Create Pull Request
uses: peter-evans/create-pull-request@v5
if: ${{ github.repository == 'qmk/qmk_firmware'}}
with:
token: ${{ secrets.QMK_BOT_TOKEN }}
delete-branch: true
branch: bugfix/regen_${{ env.GITHUB_REF_SLUG }}
author: QMK Bot <hello@qmk.fm>
committer: QMK Bot <hello@qmk.fm>
commit-message: Regenerate Files
title: '[CI] Regenerate Files'
-66
View File
@@ -1,66 +0,0 @@
name: 'Close stale issues and PRs'
permissions:
issues: write
pull-requests: write
actions: write
on:
schedule:
- cron: '30 1 * * *'
workflow_dispatch:
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@main
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
remove-stale-when-updated: true
exempt-draft-pr: true
ascending: true
operations-per-run: 150
stale-issue-label: stale
days-before-issue-stale: 90
days-before-issue-close: 30
exempt-issue-labels: bug,in progress,on hold,discussion,to do
stale-issue-message: >
This issue has been automatically marked as stale because it has not had activity in the
last 90 days. It will be closed in the next 30 days unless it is tagged properly or other activity
occurs.
For maintainers: Please label with `bug`, `in progress`, `on hold`, `discussion` or `to do` to prevent
the issue from being re-flagged.
close-issue-message: >
This issue has been automatically closed because it has not had activity in the last 30 days.
If this issue is still valid, re-open the issue and let us know.
// [stale-action-closed]
stale-pr-label: stale
days-before-pr-stale: 45
days-before-pr-close: 30
exempt-pr-labels: bug,awaiting review,breaking_change,in progress,on hold
stale-pr-message: >
Thank you for your contribution!
This pull request has been automatically marked as stale because it has not had
activity in the last 45 days. It will be closed in 30 days if no further activity occurs.
Please feel free to give a status update now, or re-open when it's ready.
For maintainers: Please label with `bug`, `awaiting review`, `breaking_change`, `in progress`, or `on hold`
to prevent the issue from being re-flagged.
close-pr-message: >
Thank you for your contribution!
This pull request has been automatically closed because it has not had activity in the last 30 days.
Please feel free to give a status update now, ping for review, or re-open when it's ready.
// [stale-action-closed]
-35
View File
@@ -1,35 +0,0 @@
name: Unit Tests
permissions:
contents: read
on:
push:
branches:
- master
- develop
pull_request:
paths:
- 'builddefs/**'
- 'quantum/**'
- 'platforms/**'
- 'tmk_core/**'
- 'tests/**'
- '*.mk'
- 'Makefile'
- '.github/workflows/unit_test.yml'
jobs:
test:
runs-on: ubuntu-latest
container: ghcr.io/qmk/qmk_cli
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install dependencies
run: pip3 install -r requirements-dev.txt
- name: Run tests
run: make test:all
+6
View File
@@ -112,5 +112,11 @@ compile_commands.json
via*.json
/keyboards/**/keymaps/vial/*
# AI Sessions
.gemini_session
.claude_session
# Keep firmware file
!keyboards/keychron/*/firmware/*.bin
/.remember/tmp/save-session.pid
/.direnv/CACHEDIR.TAG
+44 -6
View File
@@ -1,13 +1,16 @@
# CLAUDE.md - QMK Development (Keychron Q5 Max)
# CLAUDE.md - QMK Development (Keychron Q5 Max / K17 Max)
Guidelines and commands for the customized Keychron Q5 Max firmware project based on the `wireless_playground` fork.
Guidelines and commands for the customized Keychron firmware project based on the `wireless_playground` fork.
## Project Scope
* **Origin:** [git.rootiest.dev/rootiest/qmk_firmware](https://git.rootiest.dev/rootiest/qmk_firmware)
* **Upstream:** [github.com/Keychron/qmk_firmware](https://github.com/Keychron/qmk_firmware) (branch: `wireless_playground`)
* **Primary Keyboard:** Keychron Q5 Max (ANSI Encoder)
* **Development Workflow:** Work is performed on the `q5_dev` branch before merging to `main`.
* **Secondary Keyboard:** Keychron K17 Max (occasionally)
* **Development Branches:**
* `dev/q5` — Q5 Max work-in-progress, merges to `main`
* `dev/k17` — K17 Max work-in-progress, merges to `main`
* **Feature Goals:** Tap-Dance, expanded layers, advanced Chording, Unicode support, and Auto-correct.
## Build and Flash Commands
@@ -28,6 +31,9 @@ Every `qmk` command below assumes the venv is active (or prefix each with
```bash
# Build the Q5 Max ANSI Encoder firmware
qmk compile -kb keychron/q5_max/ansi_encoder -km via
# Build the K17 Max firmware (rgb variant; separate 'white' LED variant exists)
qmk compile -kb keychron/k17_max/ansi_encoder/rgb -km via
```
### Flashing
@@ -35,6 +41,9 @@ qmk compile -kb keychron/q5_max/ansi_encoder -km via
```bash
# Flash the Q5 Max (requires the board to be in bootloader mode)
qmk flash -kb keychron/q5_max/ansi_encoder -km via
# Flash the K17 Max (requires the board to be in bootloader mode)
qmk flash -kb keychron/k17_max/ansi_encoder/rgb -km via
```
### Environment Setup
@@ -58,12 +67,41 @@ qmk config user.keymap=via
## Development Workflow
1. Verify the current branch is `q5_dev`.
2. Implement features in `keyboards/keychron/q5_max/ansi_encoder/keymaps/via/`.
1. Verify the current branch is `dev/q5` (Q5 Max) or `dev/k17` (K17 Max).
2. Implement features in the relevant keymap directory:
* Q5 Max: `keyboards/keychron/q5_max/ansi_encoder/keymaps/via/`
* K17 Max: `keyboards/keychron/k17_max/ansi_encoder/rgb/keymaps/via/`
3. Test compilation locally before committing.
4. Ensure `rules.mk` has the necessary flags enabled (e.g., `TAP_DANCE_ENABLE = yes`, `UNICODE_ENABLE = yes`).
## Git Conventions
* Use conventional commits (`feat:`, `fix:`, `docs:`, `chore:`, etc.) scoped to the keyboard where relevant (e.g. `feat(q5_max):`).
* Use conventional commits (`feat:`, `fix:`, `docs:`, `chore:`, etc.) scoped to the keyboard where relevant (e.g. `feat(q5_max):`, `fix(k17_max):`).
* Do **not** include `Co-Authored-By: Claude` trailers in commit messages.
### Chained / Stacked PRs
When merging a chain of PRs (e.g. `A → main`, `B → A`, `C → B`), always **delete the branch after each merge**. Gitea (and GitHub) will automatically retarget any open PRs pointing at the deleted branch to the branch it was merged into. This keeps the chain collapsing cleanly into `main` without manual retargeting or cleanup PRs.
## EEPROM Layout Notes
The Q5 Max uses wear-leveling EEPROM (STM32F401). Key layout facts:
* `EECONFIG_RGB_MATRIX` is at bytes 2431; byte 0 packs `mode[7:2] | enable[1:0]`.
* Keychron custom RGB data (effect list, regions, per-key colours, retail demo flag) lives in `EECONFIG_KB_DATABLOCK` immediately after `EECONFIG_BASE_SIZE` (37 bytes).
* `VIA_EEPROM_MAGIC_ADDR` is pinned to **544** in `ansi_encoder/config.h`. Do not lower this value — it must stay above `EECONFIG_BASE_SIZE + EECONFIG_KB_DATA_SIZE`. If Keychron EEPROM grows, raise 544 accordingly and clear EEPROM on the board.
* `EECONFIG_KB_DATA_SIZE` is computed in `eeconfig_kb.h` and requires an `#undef` before the `#define` to suppress QMK's default-zero value.
## Keychron RGB (`KEYCHRON_RGB_ENABLE`)
Enabled via `KEYCHRON_RGB_ENABLE = yes` in `rules.mk`. Key behavioural notes:
* `eeconfig_init_custom_rgb()` **loads** Keychron RGB state from EEPROM into RAM. It must be called in `keyboard_post_init_kb()` and in `wireless_enter_connected_kb()`; without it the arrays are zero-initialised and Launcher settings are lost on every boot or transport change.
* `eeconfig_reset_custom_rgb()` **writes** defaults to EEPROM and stamps the version. The version stamp (`eeprom_update_dword(EECONFIG_KEYBOARD, ...)`) belongs here only — not in the load path.
* `kc_rgb_save()` must call `eeconfig_update_rgb_matrix()` to persist the QMK RGB mode alongside the Keychron custom data; otherwise `rgb_matrix_init()` (triggered on every transport change by `REINIT_LED_DRIVER = 1`) reloads the compile-time default `RGB_MATRIX_TYPING_HEATMAP`.
* `retail_demo_enable` is a single byte. A bug in the original Keychron code used `eeprom_read_block` instead of `eeprom_update_block` in `eeconfig_reset_custom_rgb()`, leaving `0xFF` on freshly-flashed boards. `retail_demo_task()` treats any non-zero value as "demo active" and forces `CUSTOM_MIXED_RGB` every scan. The load path now clamps `> 1` to `0` and re-writes the byte as a one-time recovery.
* `default_per_key_led[]` and `default_region[]` must be defined in board-specific code (e.g. `ansi_encoder.c`) — `keychron_rgb.c` declares them `extern`.
## DIP Switch (Win/Mac)
The Win-side dip switch uses a **frame overlay** rather than calling `rgb_matrix_mode()`. A `dip_win_active` flag is set on switch change; `rgb_matrix_indicators_advanced_user()` paints all LEDs white each frame when the flag is set. This avoids writing to EEPROM and preserves the Launcher-configured effect, which would otherwise be overwritten by the direct mode call.
+1
View File
@@ -195,6 +195,7 @@
"PERMISSIVE_HOLD_PER_KEY": {"info_key": "tapping.permissive_hold_per_key", "value_type": "bool"},
"RETRO_TAPPING": {"info_key": "tapping.retro", "value_type": "bool"},
"RETRO_TAPPING_PER_KEY": {"info_key": "tapping.retro_per_key", "value_type": "bool"},
"SPECULATIVE_HOLD": {"info_key": "tapping.speculative_hold", "value_type": "bool"},
"TAP_CODE_DELAY": {"info_key": "qmk.tap_keycode_delay", "value_type": "int"},
"TAP_HOLD_CAPS_DELAY": {"info_key": "qmk.tap_capslock_delay", "value_type": "int"},
"TAPPING_TERM": {"info_key": "tapping.term", "value_type": "int"},
+33
View File
@@ -489,6 +489,39 @@ Examples:
[Auto Shift,](feature_auto_shift.md) has its own version of `retro tapping` called `retro shift`. It is extremely similar to `retro tapping`, but holding the key past `AUTO_SHIFT_TIMEOUT` results in the value it sends being shifted. Other configurations also affect it differently; see [here](feature_auto_shift.md#retro-shift) for more information.
### Speculative Hold
Speculative Hold makes mod-tap keys more responsive by applying the modifier instantly on keydown, before the tap-hold decision is made. This is especially useful for actions like Shift+Click with a mouse, which can feel laggy with standard mod-taps.
The firmware holds the modifier speculatively. Once the key's behavior is settled:
* If held, the modifier remains active as expected until the key is released.
* If tapped, the speculative modifier is canceled just before the tapping keycode is sent.
Speculative Hold applies the modifier early but does not change the underlying tap-hold decision logic. Speculative Hold is compatible to use in combination with any other tap-hold options.
To enable Speculative Hold, add the following to your `config.h`:
```c
#define SPECULATIVE_HOLD
```
By default, Speculative Hold applies to mod-taps using Shift, Ctrl, or Shift + Ctrl. You can override this behavior by defining the `get_speculative_hold()` callback in your keymap, for instance:
```c
bool get_speculative_hold(uint16_t keycode, keyrecord_t* record) {
switch (keycode) { // These keys may be speculatively held.
case LCTL_T(KC_ESC):
case LSFT_T(KC_Z):
case RSFT_T(KC_SLSH):
return true;
}
return false; // Disable otherwise.
}
```
Some operating systems or applications assign actions to tapping a modifier key by itself, e.g., tapping GUI to open a start menu. Because Speculative Hold sends a lone modifier key press in some cases, it can falsely trigger these actions. To prevent this, set `DUMMY_MOD_NEUTRALIZER_KEYCODE` (and optionally `MODS_TO_NEUTRALIZE`) in your `config.h` in the same way as described above for [Retro Tapping](#retro-tapping).
## Why do we include the key record for the per key functions?
One thing that you may notice is that we include the key record for all of the "per key" functions, and may be wondering why we do that.
+1
View File
@@ -57,5 +57,6 @@
#define EECONFIG_BASE_WIRELESS_CONFIG EECONFIG_END_CUSTOM_RGB
#define EECONFIG_END_WIRELESS_CONFIG (EECONFIG_BASE_WIRELESS_CONFIG + __EECONFIG_SIZE_WIRELESS_CONFIG)
#undef EECONFIG_KB_DATA_SIZE
#define EECONFIG_KB_DATA_SIZE (EECONFIG_END_WIRELESS_CONFIG - EECONFIG_BASE_LANGUAGE)
+6 -1
View File
@@ -85,6 +85,11 @@ void get_firmware_version(uint8_t *data) {
__attribute__((weak)) void kc_rgb_matrix_rx(uint8_t *data, uint8_t length) {}
/* Default no-op implementation; override in keymap.c to handle custom IDs. */
__attribute__((weak)) bool kc_raw_hid_rx_kb(uint8_t *data, uint8_t length) {
return false;
}
bool kc_raw_hid_rx(uint8_t *data, uint8_t length) {
switch (data[0]) {
case KC_GET_PROTOCOL_VERSION:
@@ -185,7 +190,7 @@ bool kc_raw_hid_rx(uint8_t *data, uint8_t length) {
#endif
default:
return false;
return kc_raw_hid_rx_kb(data, length);
}
raw_hid_send(data, length);
@@ -63,3 +63,17 @@ enum {
REPORT_RATE_GET,
REPORT_RATE_SET,
};
/**
* Keyboard-level Raw HID extension hook.
*
* Called by kc_raw_hid_rx() for any command ID not handled by Keychron's own
* protocol (0xA0-0xAB range). The default weak implementation returns false
* so that unrecognised commands fall through to VIA.
*
* Override this in your keymap to handle custom command IDs without
* conflicting with Keychron's via_command_kb() definition.
* Return true if the packet was fully handled (including calling
* raw_hid_send() if a reply is needed), false to let VIA process it.
*/
bool kc_raw_hid_rx_kb(uint8_t *data, uint8_t length);
+17 -2
View File
@@ -76,7 +76,7 @@ void eeconfig_reset_custom_rgb(void) {
eeprom_update_block(&os_ind_cfg, OFFSET_OS_INDICATOR, sizeof(os_ind_cfg));
retail_demo_enable = 0;
eeprom_read_block(&retail_demo_enable, (uint8_t *)(OFFSET_RETAIL_DEMO), sizeof(retail_demo_enable));
eeprom_update_block(&retail_demo_enable, (uint8_t *)(OFFSET_RETAIL_DEMO), sizeof(retail_demo_enable));
per_key_rgb_type = 0;
eeprom_update_block(&per_key_rgb_type, OFFSET_PER_KEY_RGB_TYPE, sizeof(per_key_rgb_type));
@@ -100,15 +100,24 @@ void eeconfig_reset_custom_rgb(void) {
effect_list[1][0].time = 5000;
eeprom_update_block(effect_list, OFFSET_EFFECT_LIST, sizeof(effect_list));
eeprom_update_dword(EECONFIG_KEYBOARD, (EECONFIG_KB_DATA_VERSION));
update_mixed_rgb_effect_count();
}
void eeconfig_init_custom_rgb(void) {
memcpy(per_key_led, default_per_key_led, sizeof(per_key_led));
eeprom_update_dword(EECONFIG_KEYBOARD, (EECONFIG_KB_DATA_VERSION));
eeprom_read_block(&os_ind_cfg, OFFSET_OS_INDICATOR, sizeof(os_ind_cfg));
eeprom_read_block(&retail_demo_enable, (uint8_t *)(OFFSET_RETAIL_DEMO), sizeof(retail_demo_enable));
// Clamp to a valid boolean. eeconfig_reset_custom_rgb() had a bug that
// used eeprom_read_block instead of eeprom_update_block for this byte,
// leaving EEPROM unwritten (often 0xFF on a freshly-flashed board).
// retail_demo_task() treats any non-zero value as "demo active" and forces
// the mode to CUSTOM_MIXED_RGB every scan, preventing mode changes.
if (retail_demo_enable > 1) {
retail_demo_enable = 0;
eeprom_update_block(&retail_demo_enable, (uint8_t *)(OFFSET_RETAIL_DEMO), sizeof(retail_demo_enable));
}
if (os_ind_cfg.hsv.v < 128) os_ind_cfg.hsv.v = 128;
// Load per key rgb led
@@ -283,6 +292,12 @@ static bool kc_rgb_save(void) {
eeprom_update_block(regions, OFFSET_LAYER_FLAGS, RGB_MATRIX_LED_COUNT);
eeprom_update_block(effect_list, OFFSET_EFFECT_LIST, sizeof(effect_list));
// Persist the current QMK RGB mode so it survives transport changes and
// power cycles. Without this, rgb_matrix_init() reloads the EEPROM default
// (RGB_MATRIX_TYPING_HEATMAP) and the Launcher-configured mode is lost.
extern void eeconfig_update_rgb_matrix(void);
eeconfig_update_rgb_matrix();
return true;
}
@@ -14,6 +14,10 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifdef KEYCHRON_RGB_ENABLE
# include "eeconfig_custom_rgb.h"
#endif
#if defined(KEYCHRON_RGB_ENABLE) && defined(EECONFIG_SIZE_CUSTOM_RGB)
#include "quantum.h"
@@ -15,6 +15,9 @@
*/
#include "rgb_matrix_kb_config.h"
#ifdef KEYCHRON_RGB_ENABLE
# include "eeconfig_custom_rgb.h"
#endif
#if defined(KEYCHRON_RGB_ENABLE) && defined(EECONFIG_SIZE_CUSTOM_RGB)
//extern bool MIXED_RGB(effect_params_t *params);
@@ -16,6 +16,14 @@
#pragma once
// Pin VIA keymap storage to a fixed EEPROM address. By default VIA places its
// magic/keymap block immediately after EECONFIG_KB_DATA_SIZE, so any growth in
// the Keychron custom-RGB EEPROM region shifts the keymap silently and corrupts
// the stored layout (observed as layer 0 keys reverting to KC_TRNS on boot).
// 552 is past the current Keychron data region end (540) and leaves headroom
// for further EEPROM additions without requiring another VIA reset.
#define VIA_EEPROM_MAGIC_ADDR 552
#ifdef RGB_MATRIX_ENABLE
/* RGB Matrix driver configuration */
# define RGB_MATRIX_LED_COUNT 103
@@ -21,8 +21,17 @@
enum {
TD_HOME_END,
TD_CHORDS,
};
void dance_chords_finished(tap_dance_state_t *state, void *user_data) {
if (state->count == 1) {
SEND_STRING(SS_LCTL("kc"));
} else {
SEND_STRING(SS_LCTL("kd"));
}
}
enum layers {
MAC_BASE,
MAC_FN,
@@ -33,7 +42,7 @@ enum layers {
// clang-format off
const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
[MAC_BASE] = LAYOUT_104_ansi(
KC_ESC, KC_BRID, KC_BRIU, KC_MCTRL, KC_LNPAD, RGB_VAD, RGB_VAI, KC_MPRV, KC_MPLY, KC_MNXT, KC_MUTE, KC_VOLD, KC_VOLU, KC_SNAP, RGB_MOD, KC_DEL, KC_F13, KC_F14, KC_F15, KC_MUTE,
KC_ESC, KC_BRID, KC_BRIU, KC_MCTRL, KC_LNPAD, RGB_VAD, RGB_VAI, KC_MPRV, KC_MPLY, KC_MNXT, KC_MUTE, KC_VOLD, KC_VOLU, KC_SNAP, RGB_MOD, KC_DEL, TD(TD_CHORDS), KC_F14, KC_F15, KC_MUTE,
KC_GRV, KC_1, KC_2, KC_3, KC_4, KC_5, KC_6, KC_7, KC_8, KC_9, KC_0, KC_MINS, KC_EQL, KC_BSPC, KC_PGUP, KC_NUM, KC_PSLS, KC_PAST, KC_PMNS,
KC_TAB, KC_Q, KC_W, KC_E, KC_R, KC_T, KC_Y, KC_U, KC_I, KC_O, KC_P, KC_LBRC, KC_RBRC, KC_BSLS, KC_PGDN, KC_P7, KC_P8, KC_P9, KC_PPLS,
KC_CAPS, KC_A, KC_S, KC_D, KC_F, KC_G, KC_H, KC_J, KC_K, KC_L, KC_SCLN, KC_QUOT, KC_ENT, TD(TD_HOME_END), KC_P4, KC_P5, KC_P6,
@@ -49,7 +58,7 @@ const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
_______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______ ),
[WIN_BASE] = LAYOUT_104_ansi(
KC_ESC, KC_F1, KC_F2, KC_F3, KC_F4, KC_F5, KC_F6, KC_F7, KC_F8, KC_F9, KC_F10, KC_F11, KC_F12, KC_PSCR, RGB_MOD, KC_DEL, _______, _______, _______, KC_MUTE,
KC_ESC, KC_F1, KC_F2, KC_F3, KC_F4, KC_F5, KC_F6, KC_F7, KC_F8, KC_F9, KC_F10, KC_F11, KC_F12, KC_PSCR, RGB_MOD, KC_DEL, TD(TD_CHORDS), _______, _______, KC_MUTE,
KC_GRV, KC_1, KC_2, KC_3, KC_4, KC_5, KC_6, KC_7, KC_8, KC_9, KC_0, KC_MINS, KC_EQL, KC_BSPC, KC_PGUP, KC_NUM, KC_PSLS, KC_PAST, KC_PMNS,
KC_TAB, KC_Q, KC_W, KC_E, KC_R, KC_T, KC_Y, KC_U, KC_I, KC_O, KC_P, KC_LBRC, KC_RBRC, KC_BSLS, KC_PGDN, KC_P7, KC_P8, KC_P9, KC_PPLS,
KC_CAPS, KC_A, KC_S, KC_D, KC_F, KC_G, KC_H, KC_J, KC_K, KC_L, KC_SCLN, KC_QUOT, KC_ENT, KC_HOME, KC_P4, KC_P5, KC_P6,
@@ -86,4 +95,5 @@ bool process_record_user(uint16_t keycode, keyrecord_t *record) {
tap_dance_action_t tap_dance_actions[] = {
// Tap once for Home, twice for End
[TD_HOME_END] = ACTION_TAP_DANCE_DOUBLE(KC_HOME, KC_END),
[TD_CHORDS] = ACTION_TAP_DANCE_FN(dance_chords_finished),
};
@@ -166,4 +166,38 @@ led_config_t g_led_config = {
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
}
};
#ifdef KEYCHRON_RGB_ENABLE
// Default colour of Per Key RGB
#define DC_RED {HSV_RED}
#define DC_BLU {HSV_BLUE}
#define DC_YLW {HSV_YELLOW}
// 103 LEDs: rows match g_led_config above
// Row 0 (0-18): Fn/media row + numpad cluster top
// Row 1 (19-37): Number row + numpad cluster
// Row 2 (38-56): QWERTY row + numpad cluster
// Row 3 (57-73): ASDF row + numpad cluster
// Row 4 (74-90): ZXCV row + numpad arrows
// Row 5 (91-102): Modifier/bottom row + numpad
HSV default_per_key_led[RGB_MATRIX_LED_COUNT] = {
DC_RED, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW,
DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW,
DC_YLW, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW,
DC_RED, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW,
DC_YLW, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW,
DC_YLW, DC_YLW, DC_YLW, DC_BLU, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW
};
// Default mixed RGB region (all keys in region 0)
uint8_t default_region[RGB_MATRIX_LED_COUNT] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
};
#endif
#endif
+31
View File
@@ -23,6 +23,8 @@
#ifdef LK_WIRELESS_ENABLE
# include "lkbt51.h"
# include "wireless.h"
# include "keychron_wireless_common.h"
# include "battery.h"
#endif
bool dip_switch_update_kb(uint8_t index, bool active) {
@@ -34,6 +36,27 @@ bool dip_switch_update_kb(uint8_t index, bool active) {
return true;
}
#ifdef LK_WIRELESS_ENABLE
// Re-apply the saved QMK RGB mode every time wireless connects.
// During transport changes, rgb_matrix_init() reads the correct mode from
// EEPROM, but something in the BT/2.4G reconnect sequence can reset it
// before the display settles. This hook fires after connection is fully
// established, ensuring the Launcher-configured mode persists.
void wireless_enter_connected_kb(uint8_t host_idx) {
# if defined(RGB_MATRIX_ENABLE) && defined(KEYCHRON_RGB_ENABLE)
extern void eeconfig_init_custom_rgb(void);
eeconfig_init_custom_rgb();
// Re-read the QMK RGB mode from EEPROM and apply if it drifted.
rgb_config_t saved_rgb;
eeprom_read_block(&saved_rgb, EECONFIG_RGB_MATRIX, sizeof(saved_rgb));
if (saved_rgb.mode && saved_rgb.mode != rgb_matrix_get_mode()) {
rgb_matrix_mode_noeeprom(saved_rgb.mode);
}
# endif
}
#endif
void keyboard_post_init_kb(void) {
#ifdef LK_WIRELESS_ENABLE
palSetLineMode(P2P4_MODE_SELECT_PIN, PAL_MODE_INPUT);
@@ -47,6 +70,14 @@ void keyboard_post_init_kb(void) {
encoder_cb_init();
#endif
#if defined(RGB_MATRIX_ENABLE) && defined(KEYCHRON_RGB_ENABLE)
// Load Keychron custom RGB data (effect list, regions, per-key colours)
// from EEPROM into RAM. Without this call the arrays are zero-initialised
// and Launcher settings are lost on every power cycle or transport change.
extern void eeconfig_init_custom_rgb(void);
eeconfig_init_custom_rgb();
#endif
keyboard_post_init_user();
}
+2
View File
@@ -1,3 +1,5 @@
KEYCHRON_RGB_ENABLE = yes
include keyboards/keychron/common/wireless/wireless.mk
include keyboards/keychron/common/keychron_common.mk
@@ -165,4 +165,38 @@ led_config_t g_led_config = {
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
}
};
#ifdef KEYCHRON_RGB_ENABLE
// Default Color of Per Key RGB
#define DC_RED {HSV_RED}
#define DC_BLU {HSV_BLUE}
#define DC_YLW {HSV_YELLOW}
// 101 LEDs: rows match g_led_config above
// Row 0 (0-16): Fn row (Esc, F1-F12, Del, PrtSc, PgUp, PgDn)
// Row 1 (17-35): Number row + numpad cluster top
// Row 2 (36-54): QWERTY row + numpad cluster mid
// Row 3 (55-71): ASDF row + numpad cluster
// Row 4 (72-88): ZXCV row + numpad arrows
// Row 5 (89-100): Modifier/bottom row + numpad
HSV default_per_key_led[RGB_MATRIX_LED_COUNT] = {
DC_RED, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW,
DC_YLW, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW,
DC_YLW, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW,
DC_RED, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_YLW, DC_YLW, DC_YLW, DC_YLW,
DC_YLW, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_BLU, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW,
DC_YLW, DC_YLW, DC_YLW, DC_BLU, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW, DC_YLW
};
// Default mixed RGB region (all keys in region 0)
uint8_t default_region[RGB_MATRIX_LED_COUNT] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
};
#endif
#endif
@@ -45,6 +45,14 @@
#endif
// Pin VIA keymap storage to a fixed EEPROM address. By default VIA places its
// magic/keymap block immediately after EECONFIG_KB_DATA_SIZE, so any growth in
// the Keychron custom-RGB EEPROM region shifts the keymap silently and corrupts
// the stored layout (observed as layer 0 keys reverting to KC_TRNS on boot).
// 544 is past the current Keychron data region and leaves headroom for further
// EEPROM additions without requiring another VIA reset.
#define VIA_EEPROM_MAGIC_ADDR 544
/* Number of layers */
#define DYNAMIC_KEYMAP_LAYER_COUNT 6
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -51,8 +51,83 @@ static const chord_entry_t chord_table[] = {
{"delta", "Δ"}, // capital delta U+0394
{"theta", "θ"}, // small theta U+03B8
{"omega", "Ω"}, // capital omega U+03A9
{"alpha", "α"}, // small alpha U+03B1
{"beta", "β"}, // small beta U+03B2
{"alpha", "α"}, // small alpha U+03B1
{"beta", "β"}, // small beta U+03B2
{"gamma", "γ"}, // small gamma U+03B3
{"epsilon", "ε"}, // small epsilon U+03B5
{"eps", "ε"}, // alias
{"zeta", "ζ"}, // small zeta U+03B6
{"eta", "η"}, // small eta U+03B7
{"iota", "ι"}, // small iota U+03B9
{"kappa", "κ"}, // small kappa U+03BA
{"lambda", "λ"}, // small lambda U+03BB
{"mu", "μ"}, // small mu U+03BC (disambig: "mute")
{"nu", "ν"}, // small nu U+03BD
{"xi", "ξ"}, // small xi U+03BE
{"omicron", "ο"}, // small omicron U+03BF
{"omi", "ο"}, // alias
{"rho", "ρ"}, // small rho U+03C1
{"tau", "τ"}, // small tau U+03C4
{"upsilon", "υ"}, // small upsilon U+03C5
{"ups", "υ"}, // alias
{"phi", "φ"}, // small phi U+03C6
{"chi", "χ"}, // small chi U+03C7
{"psi", "ψ"}, // small psi U+03C8
// ---- Number sets (blackboard bold) ------------------------------------
{"real", ""}, // real numbers U+211D
{"nat", ""}, // natural numbers U+2115
{"intgr", ""}, // integers U+2124
{"rat", ""}, // rational numbers U+211A
{"cmplx", ""}, // complex numbers U+2102
{"aleph", ""}, // aleph U+2135
// ---- Calculus / Analysis -----------------------------------------------
{"integ", ""}, // integral U+222B
{"iint", ""}, // double integral U+222C
{"oint", ""}, // contour integral U+222E
{"partial", ""}, // partial derivative U+2202
{"prt", ""}, // alias
{"nabla", ""}, // nabla / del U+2207
{"sum", ""}, // summation U+2211
{"prod", ""}, // product U+220F
// ---- Set theory --------------------------------------------------------
{"elem", ""}, // element of U+2208
{"notin", ""}, // not element of U+2209
{"sub", ""}, // subset of U+2282 (disambig: "subeq")
{"subeq", ""}, // subset or equal U+2286
{"sup", ""}, // superset of U+2283 (disambig: "supeq")
{"supeq", ""}, // superset or equal U+2287
{"union", ""}, // union U+222A
{"inter", ""}, // intersection U+2229
{"empty", ""}, // empty set U+2205
// ---- Logic -------------------------------------------------------------
{"forall", ""}, // for all U+2200
{"exists", ""}, // there exists U+2203
{"nexist", ""}, // does not exist U+2204
{"land", ""}, // logical and U+2227
{"lor", ""}, // logical or U+2228
{"xor", ""}, // exclusive or U+2295
{"impl", ""}, // implies U+27F9
{"iff", ""}, // if and only if U+27FA
// ---- Geometry / Linear algebra -----------------------------------------
{"angle", ""}, // angle U+2220
{"ang", ""}, // alias
{"perp", ""}, // perpendicular U+22A5
{"parl", ""}, // parallel U+2225
// ---- Floor / ceiling ---------------------------------------------------
{"lfl", ""}, // left floor U+230A
{"rfl", ""}, // right floor U+230B
{"lcl", ""}, // left ceiling U+2308
{"rcl", ""}, // right ceiling U+2309
// ---- Other math --------------------------------------------------------
{"equiv", ""}, // congruent / equiv U+2261
{"prop", ""}, // proportional to U+221D
// ---- Currency ----------------------------------------------------------
{"euro", ""}, // euro U+20AC
@@ -0,0 +1,32 @@
// Copyright 2024 rootiest
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
// Tight, strict combo window for a simultaneous 3-key press.
// This ensures the fallback combo only fires when intended.
#define COMBO_TERM 10
#define COMBO_STRICT_TIMER
// Use a more conservative debounce period (20ms is standard for Keychron)
// but since we switched to 'sym_defer_pk' in rules.mk, this will now
// require 20ms of STABILITY before a keypress is reported.
#define DEBOUNCE 20
// Always resolve combo keycodes from BASE (layer 0) so the
// COMM+DOT+SLSH fallback combo fires regardless of the active layer.
#define COMBO_ONLY_FROM_LAYER 0
// Pressing the Shift key continues Caps Word and inverts the shift state
#define CAPS_WORD_INVERT_ON_SHIFT
// Default tapping term for mod-tap, layer-tap, and tap-dance keys.
#define TAPPING_TERM 200
// Allow per-key overrides via get_tapping_term() in keymap.c.
#define TAPPING_TERM_PER_KEY
// Use right CTRL key to neutralize modifier taps when cancelled.
#define DUMMY_MOD_NEUTRALIZER_KEYCODE KC_RIGHT_CTRL
// Neutralize left ALT and left GUI (Default value)
#define MODS_TO_NEUTRALIZE {MOD_BIT(KC_LEFT_ALT), MOD_BIT(KC_LEFT_GUI)}
@@ -0,0 +1,104 @@
// Copyright 2024 rootiest
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
/*
* qmk-host Shared HID Protocol
* =============================
* Bi-directional Raw HID protocol shared between QMK firmware and the
* qmk-host Rust application. All values MUST match between both sides.
*
* Physical layer (QMK defaults, NOT overridden):
* Usage Page : 0xFF60
* Usage ID : 0x0061
* Packet size: RAW_EPSIZE (32 bytes)
*
* Packet layout:
* Byte 0 : Command ID
* Byte 1 : Source Device ID
* Byte 2 : Flags
* Bytes 3-31: Payload (29 bytes)
*
* Command IDs use the range 0x40-0x7E to avoid any overlap with VIA's
* protocol (0x01-0x3F) or the reserved unhandled sentinel (0xFF).
*/
// ---------------------------------------------------------------------------
// Command IDs
// ---------------------------------------------------------------------------
#define HID_CMD_LAYER_SYNC 0x40u // Layer state sync (keyboard ↔ host ↔ keyboard)
#define HID_CMD_VOLUME 0x41u // System volume level (host → keyboard)
#define HID_CMD_BRIGHTNESS 0x42u // Screen brightness level (host → keyboard)
#define HID_CMD_ACTIVE_APP 0x43u // Active window/app name (reserved — future)
#define HID_CMD_BATTERY 0x44u // Battery level (keyboard → host; wireless mode only)
#define HID_CMD_ACK 0x7Eu // Generic acknowledgement
// ---------------------------------------------------------------------------
// Source Device IDs
// ---------------------------------------------------------------------------
#define HID_DEV_HOST 0x00u // qmk-host Rust application
#define HID_DEV_Q5MAX 0x01u // Keychron Q5 Max (this firmware)
#define HID_DEV_NUMPAD 0x02u // Numpad (future second keyboard)
// ---------------------------------------------------------------------------
// Packet Flags (Byte 2)
// ---------------------------------------------------------------------------
#define HID_FLAG_QUERY 0x01u // Request: send back current state, no change
#define HID_FLAG_RESPONSE 0x02u // Response: this is a reply to a query
// ---------------------------------------------------------------------------
// Byte offsets within the 32-byte packet
// ---------------------------------------------------------------------------
#define HID_OFF_CMD 0u // Command ID
#define HID_OFF_SRC 1u // Source Device ID
#define HID_OFF_FLAGS 2u // Flags
#define HID_OFF_PAYLOAD 3u // Start of payload
// ---------------------------------------------------------------------------
// HID_CMD_LAYER_SYNC payload (from byte HID_OFF_PAYLOAD)
// [0] Active layer index (0 = BASE … 5 = KEEB_CTL)
// [1] Locked layers bitmask (bit N = layer N is locked)
// ---------------------------------------------------------------------------
#define HID_LAYER_OFF_ACTIVE 0u
#define HID_LAYER_OFF_LOCKED 1u
// ---------------------------------------------------------------------------
// HID_CMD_VOLUME payload
// [0] Volume level, 0-100 (%)
// ---------------------------------------------------------------------------
#define HID_VOLUME_OFF_LEVEL 0u
// ---------------------------------------------------------------------------
// HID_CMD_BRIGHTNESS payload
// [0] Brightness level, 0-100 (%)
// ---------------------------------------------------------------------------
#define HID_BRITE_OFF_LEVEL 0u
// ---------------------------------------------------------------------------
// HID_CMD_ACTIVE_APP payload (reserved — no firmware action yet)
// [0..27] Null-terminated UTF-8 application name (max 28 bytes incl. NUL)
// ---------------------------------------------------------------------------
#define HID_APP_NAME_MAX 28u
// ---------------------------------------------------------------------------
// HID_CMD_BATTERY payload
// [0] Battery percentage 0-100, or HID_BATT_UNAVAILABLE when the keyboard
// is in USB transport mode (battery reading not meaningful / charging).
// Keyboard only sends this packet when get_transport() & TRANSPORT_WIRELESS.
// ---------------------------------------------------------------------------
#define HID_BATT_OFF_LEVEL 0u // Payload byte 0: percentage (0-100)
#define HID_BATT_UNAVAILABLE 0xFFu // Sentinel: not in wireless mode
// ---------------------------------------------------------------------------
// Packet size
// ---------------------------------------------------------------------------
// Matches RAW_EPSIZE (QMK raw HID endpoint size = 32 bytes). Defined here
// so keymap code does not depend on usb_descriptor.h being in scope, which
// it is not when compiled through Keychron's build path.
#define HID_PACKET_SIZE 32u
// ---------------------------------------------------------------------------
// Convenience: first byte of payload as an absolute packet index
// ---------------------------------------------------------------------------
#define HID_PAYLOAD(offset) ((uint8_t)((HID_OFF_PAYLOAD) + (offset)))
@@ -17,6 +17,13 @@
#include QMK_KEYBOARD_H
#include "keychron_common.h"
#include "chord_unicode.h"
#include "raw_hid.h"
#include "keychron_raw_hid.h"
#include "hid_protocol.h"
#ifdef LK_WIRELESS_ENABLE
# include "battery.h"
# include "transport.h"
#endif
// Tap Dance declarations
enum {
@@ -28,13 +35,18 @@ enum custom_keycodes {
ALT_TAB_FWD = SAFE_RANGE, // Alt+Tab (forward)
ALT_TAB_BWD, // Alt+Shift+Tab (backward)
CHORD_KEY, // Fn1+LeftAlt → chord/unicode entry mode
LCK_FN1, // Lock/unlock FN1
LCK_FN2, // Lock/unlock FN2
LCK_FN3, // Lock/unlock FN3
LCK_FN4, // Lock/unlock FN4
LCK_CTL, // Lock/unlock KEEB_CTL
LCK_BASE, // Clear all locks and return to BASE
BSP_DEL, // Tap=Backspace, Shift+Tap=Delete
};
// Alt-Tab cycling state
static bool alt_tab_active = false;
static uint16_t alt_tab_timer = 0;
#define ALT_TAB_TIMEOUT 750 // ms to hold Alt after last encoder tick
#define CAPS_MOD MT(MOD_LCTL, KC_ESC)
// Declare layers early so the HID functions below can reference KEEB_CTL.
enum layers {
BASE,
FN1,
@@ -44,55 +56,210 @@ enum layers {
KEEB_CTL,
};
// Alt-Tab cycling state
static bool alt_tab_active = false;
static uint16_t alt_tab_timer = 0;
#define ALT_TAB_TIMEOUT 750 // ms to hold Alt after last encoder tick
// Layer-lock state: bitmask of layers that should stay active even after
// momentary (TT/MO) keys are released.
static layer_state_t locked_layers = 0;
// ---------------------------------------------------------------------------
// Raw HID state
// ---------------------------------------------------------------------------
// Anti-loop guard: set to true while applying a layer change that arrived
// over HID so that layer_state_set_user() does not echo it back.
static bool g_hid_recv_active = false;
// Last highest-layer value we sent via HID. 0xFF = never sent.
// Avoids redundant sends when layer_state_set_user is called multiple times
// with the same effective top layer.
static uint8_t g_last_sent_layer = 0xFF;
// Latest host-reported values (stored for future RGB indicator use).
static uint8_t g_hid_volume = 0;
static uint8_t g_hid_brightness = 0;
// Battery reporting state (wireless mode only).
// Pushes the current percentage every BAT_REPORT_INTERVAL_MS ms.
// Suppresses redundant sends when the level hasn't changed.
#ifdef LK_WIRELESS_ENABLE
# define BAT_REPORT_INTERVAL_MS 300000U // 5 minutes
static uint32_t g_bat_timer = 0;
static bool g_bat_sent_once = false;
static uint8_t g_last_sent_bat = HID_BATT_UNAVAILABLE;
#endif
// Send the current battery percentage to the host.
// Only fires when get_transport() & TRANSPORT_WIRELESS; no-op otherwise.
// Suppresses sends when the level matches the last value sent.
#ifdef LK_WIRELESS_ENABLE
static void hid_send_battery(void) {
if (!(get_transport() & TRANSPORT_WIRELESS)) return;
uint8_t level = battery_get_percentage();
if (level == g_last_sent_bat) return;
g_last_sent_bat = level;
uint8_t data[HID_PACKET_SIZE] = {0};
data[HID_OFF_CMD] = HID_CMD_BATTERY;
data[HID_OFF_SRC] = HID_DEV_Q5MAX;
data[HID_OFF_FLAGS] = 0;
data[HID_PAYLOAD(HID_BATT_OFF_LEVEL)] = level;
raw_hid_send(data, HID_PACKET_SIZE);
}
#endif
// Send the current layer state to the host / bridge application.
static void hid_send_layer_sync(uint8_t layer, uint8_t locked_mask) {
uint8_t data[HID_PACKET_SIZE] = {0};
data[HID_OFF_CMD] = HID_CMD_LAYER_SYNC;
data[HID_OFF_SRC] = HID_DEV_Q5MAX;
data[HID_OFF_FLAGS] = 0;
data[HID_PAYLOAD(HID_LAYER_OFF_ACTIVE)] = layer;
data[HID_PAYLOAD(HID_LAYER_OFF_LOCKED)] = locked_mask;
raw_hid_send(data, HID_PACKET_SIZE);
}
// Handle a Raw HID packet for our custom command range (0x40-0x7E).
// Overrides the weak kc_raw_hid_rx_kb() hook in keychron_raw_hid.c, which is
// called by kc_raw_hid_rx() for any command ID not handled by Keychron's own
// protocol. Must call raw_hid_send() directly for any reply.
bool kc_raw_hid_rx_kb(uint8_t *data, uint8_t length) {
uint8_t cmd = data[HID_OFF_CMD];
// Only intercept our custom command range; let VIA handle everything else.
if (cmd < 0x40u || cmd > 0x7Eu) {
return false;
}
uint8_t flags = data[HID_OFF_FLAGS];
switch (cmd) {
case HID_CMD_LAYER_SYNC: {
if (flags & HID_FLAG_QUERY) {
// Host requests current state — reply without changing anything.
uint8_t resp[HID_PACKET_SIZE] = {0};
resp[HID_OFF_CMD] = HID_CMD_LAYER_SYNC;
resp[HID_OFF_SRC] = HID_DEV_Q5MAX;
resp[HID_OFF_FLAGS] = HID_FLAG_RESPONSE;
resp[HID_PAYLOAD(HID_LAYER_OFF_ACTIVE)] = get_highest_layer(layer_state);
resp[HID_PAYLOAD(HID_LAYER_OFF_LOCKED)] = (uint8_t)locked_layers;
raw_hid_send(resp, HID_PACKET_SIZE);
} else {
// Host or peer keyboard is pushing a new active layer.
uint8_t new_layer = data[HID_PAYLOAD(HID_LAYER_OFF_ACTIVE)];
uint8_t new_locked = data[HID_PAYLOAD(HID_LAYER_OFF_LOCKED)];
if (new_layer <= KEEB_CTL) {
// Set the guard BEFORE calling layer_move() so that the
// resulting layer_state_set_user() call does not echo the
// change back to the host, preventing an infinite loop.
g_hid_recv_active = true;
locked_layers = new_locked;
layer_move(new_layer);
g_hid_recv_active = false;
}
}
break;
}
case HID_CMD_VOLUME: {
// Store the host-reported volume (0-100).
g_hid_volume = data[HID_PAYLOAD(HID_VOLUME_OFF_LEVEL)];
// TODO: drive an RGB volume-bar indicator here in a future commit.
break;
}
case HID_CMD_BRIGHTNESS: {
// Store the host-reported screen brightness (0-100).
g_hid_brightness = data[HID_PAYLOAD(HID_BRITE_OFF_LEVEL)];
// TODO: drive an RGB brightness indicator here in a future commit.
break;
}
case HID_CMD_ACTIVE_APP: {
// Reserved — no action yet. Payload is a null-terminated UTF-8
// application name (up to HID_APP_NAME_MAX bytes).
// TODO: implement active-app handling once the host side is ready.
break;
}
case HID_CMD_BATTERY: {
// Host is querying the current battery level.
// Reply with the current percentage when in wireless mode, or
// HID_BATT_UNAVAILABLE when wired (USB transport / not meaningful).
uint8_t resp[HID_PACKET_SIZE] = {0};
resp[HID_OFF_CMD] = HID_CMD_BATTERY;
resp[HID_OFF_SRC] = HID_DEV_Q5MAX;
resp[HID_OFF_FLAGS] = HID_FLAG_RESPONSE;
#ifdef LK_WIRELESS_ENABLE
resp[HID_PAYLOAD(HID_BATT_OFF_LEVEL)] = (get_transport() & TRANSPORT_WIRELESS) ? battery_get_percentage() : HID_BATT_UNAVAILABLE;
#else
resp[HID_PAYLOAD(HID_BATT_OFF_LEVEL)] = HID_BATT_UNAVAILABLE;
#endif
raw_hid_send(resp, HID_PACKET_SIZE);
break;
}
default:
break;
}
return true; // packet was fully handled by us
}
// CAPS_MOD state: tap=ESC, hold=Ctrl, Shift+tap=CapsLock, Alt+tap=CapsWord, GUI+tap=Autocorrect
// (Refactored to use MT(MOD_LCTL, KC_ESC) with custom tap logic)
// clang-format off
const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
[BASE] = LAYOUT_ansi_101(
KC_ESC, KC_F1, KC_F2, KC_F3, KC_F4, KC_F5, KC_F6, KC_F7, KC_F8, KC_F9, KC_F10, KC_F11, KC_F12, KC_DEL, KC_PSCR, KC_CALC, KC_FIND, KC_MPLY,
KC_GRV, KC_1, KC_2, KC_3, KC_4, KC_5, KC_6, KC_7, KC_8, KC_9, KC_0, KC_MINS, KC_EQL, KC_BSPC, KC_PGUP, KC_NUM, KC_PSLS, KC_PAST, KC_PMNS,
KC_TAB, KC_Q, KC_W, KC_E, KC_R, KC_T, KC_Y, KC_U, KC_I, KC_O, KC_P, KC_LBRC, KC_RBRC, KC_BSLS, KC_PGDN, KC_P7, KC_P8, KC_P9,
KC_CAPS, KC_A, KC_S, KC_D, KC_F, KC_G, KC_H, KC_J, KC_K, KC_L, KC_SCLN, KC_QUOT, KC_ENT, TD(TD_HOME_END), KC_P4, KC_P5, KC_P6, KC_PPLS,
CAPS_MOD, KC_A, KC_S, KC_D, KC_F, KC_G, KC_H, KC_J, KC_K, KC_L, KC_SCLN, KC_QUOT, KC_ENT, TD(TD_HOME_END), KC_P4, KC_P5, KC_P6, KC_PPLS,
KC_LSFT, KC_Z, KC_X, KC_C, KC_V, KC_B, KC_N, KC_M, KC_COMM, KC_DOT, KC_SLSH, KC_RSFT, KC_UP, KC_P1, KC_P2, KC_P3,
KC_LCTL, KC_LGUI, KC_LALT, KC_SPC, TT(FN2), TT(FN1), KC_RCTL, KC_LEFT, KC_DOWN, KC_RGHT, KC_P0, KC_PDOT, KC_PENT),
[FN1] = LAYOUT_ansi_101(
KC_SLEP, KC_BRID, KC_BRIU, KC_MCTRL, KC_LNPAD, RGB_VAD, RGB_VAI, KC_MPRV, KC_MPLY, KC_MNXT, KC_MUTE, KC_VOLD, KC_VOLU, KC_DEL, KC_PSCR, KC_CALC, KC_FIND, KC_MUTE,
KC_GRV, KC_1, KC_2, KC_3, KC_4, KC_5, KC_6, KC_7, KC_8, KC_9, KC_0, KC_MINS, KC_EQL, KC_BSPC, KC_PGUP, KC_NUM, KC_PSLS, KC_PAST, KC_PMNS,
KC_GRV, LCK_FN1, LCK_FN2, LCK_FN3, LCK_FN4, LCK_CTL, KC_6, KC_7, KC_8, KC_9, LCK_BASE, KC_MINS, KC_EQL, KC_BSPC, KC_PGUP, KC_NUM, KC_PSLS, KC_PAST, KC_PMNS,
KC_TAB, KC_Q, KC_W, KC_E, KC_R, KC_T, KC_Y, KC_U, KC_I, KC_O, KC_P, KC_LBRC, KC_RBRC, KC_BSLS, KC_PGDN, KC_P7, KC_P8, KC_P9,
KC_CAPS, KC_A, KC_S, KC_D, KC_F, KC_G, KC_H, KC_J, KC_K, KC_L, KC_SCLN, KC_QUOT, KC_ENT, KC_END, KC_P4, KC_P5, KC_P6, KC_PPLS,
CAPS_MOD, KC_A, KC_S, KC_D, KC_F, KC_G, KC_H, KC_J, KC_K, KC_L, KC_SCLN, KC_QUOT, KC_ENT, KC_END, KC_P4, KC_P5, KC_P6, KC_PPLS,
KC_LSFT, KC_Z, KC_X, KC_C, KC_V, KC_B, KC_N, KC_M, KC_COMM, KC_DOT, KC_SLSH, KC_RSFT, KC_UP, KC_P1, KC_P2, KC_P3,
KC_LCTL, KC_LGUI, CHORD_KEY, KC_SPC, TT(FN3), TG(FN1), OSL(KEEB_CTL), KC_HOME, KC_DOWN, KC_END, KC_P0, KC_PDOT, KC_PENT),
[FN2] = LAYOUT_ansi_101(
KC_PWR, KC_F13, KC_F14, KC_F15, KC_F16, KC_F17, KC_F18, KC_F19, KC_F20, KC_F21, KC_F22, KC_F23, KC_F24, KC_DEL, KC_PSCR, KC_CALC, KC_FIND, KC_MPLY,
KC_GRV, KC_1, KC_2, KC_3, KC_4, KC_5, KC_6, KC_7, KC_8, KC_9, KC_0, KC_MINS, KC_EQL, KC_BSPC, KC_MS_WH_UP, KC_NUM, KC_PSLS, KC_PAST, KC_PMNS,
KC_GRV, LCK_FN1, LCK_FN2, LCK_FN3, LCK_FN4, LCK_CTL, KC_6, KC_7, KC_8, KC_9, LCK_BASE, KC_MINS, KC_EQL, KC_BSPC, KC_MS_WH_UP, KC_NUM, KC_PSLS, KC_PAST, KC_PMNS,
KC_TAB, KC_Q, KC_W, KC_E, KC_R, KC_T, KC_Y, KC_U, KC_I, KC_O, KC_P, KC_LBRC, KC_RBRC, KC_BSLS, KC_MS_WH_DOWN, KC_P7, KC_P8, KC_P9,
KC_CAPS, KC_A, KC_S, KC_D, KC_F, KC_G, KC_H, KC_J, KC_K, KC_L, KC_SCLN, KC_QUOT, KC_ENT, KC_MS_BTN3, KC_P4, KC_P5, KC_P6, KC_PPLS,
CAPS_MOD, KC_A, KC_S, KC_D, KC_F, KC_G, KC_H, KC_J, KC_K, KC_L, KC_SCLN, KC_QUOT, KC_ENT, KC_MS_BTN3, KC_P4, KC_P5, KC_P6, KC_PPLS,
KC_LSFT, KC_Z, KC_X, KC_C, KC_V, KC_B, KC_N, KC_M, KC_COMM, KC_DOT, KC_SLSH, KC_RSFT, KC_MS_UP, KC_P1, KC_P2, KC_P3,
KC_LCTL, KC_LGUI, KC_LALT, KC_SPC, TG(FN2), TT(FN4), KC_RCTL, KC_MS_LEFT, KC_MS_DOWN, KC_MS_RIGHT, KC_MS_BTN1, KC_MS_BTN2, KC_PENT),
[FN3] = LAYOUT_ansi_101(
KC_ESC, KC_BRID, KC_BRIU, KC_MCTRL, KC_LNPAD, RGB_VAD, RGB_VAI, KC_MPRV, KC_MPLY, KC_MNXT, KC_MUTE, KC_VOLD, KC_VOLU, KC_DEL, KC_PSCR, KC_CALC, KC_FIND, KC_MPLY,
KC_GRV, KC_1, KC_2, KC_3, KC_4, KC_5, KC_6, KC_7, KC_8, KC_9, KC_0, KC_MINS, KC_EQL, KC_BSPC, KC_PGUP, KC_NUM, KC_PSLS, KC_PAST, KC_PMNS,
KC_GRV, LCK_FN1, LCK_FN2, LCK_FN3, LCK_FN4, LCK_CTL, KC_6, KC_7, KC_8, KC_9, LCK_BASE, KC_MINS, KC_EQL, KC_BSPC, KC_PGUP, KC_NUM, KC_PSLS, KC_PAST, KC_PMNS,
KC_TAB, KC_Q, KC_W, KC_E, KC_R, KC_T, KC_Y, KC_U, KC_I, KC_O, KC_P, KC_LBRC, KC_RBRC, KC_BSLS, KC_PGDN, KC_P7, KC_P8, KC_P9,
KC_CAPS, KC_A, KC_S, KC_D, KC_F, KC_G, KC_H, KC_J, KC_K, KC_L, KC_SCLN, KC_QUOT, KC_ENT, TD(TD_HOME_END), KC_P4, KC_P5, KC_P6, KC_PPLS,
CAPS_MOD, KC_A, KC_S, KC_D, KC_F, KC_G, KC_H, KC_J, KC_K, KC_L, KC_SCLN, KC_QUOT, KC_ENT, TD(TD_HOME_END), KC_P4, KC_P5, KC_P6, KC_PPLS,
KC_LSFT, KC_Z, KC_X, KC_C, KC_V, KC_B, KC_N, KC_M, KC_COMM, KC_DOT, KC_SLSH, KC_RSFT, KC_UP, KC_P1, KC_P2, KC_P3,
KC_LCTL, KC_LGUI, KC_LALT, KC_SPC, TG(FN3), TT(FN4), KC_RCTL, KC_LEFT, KC_DOWN, KC_RGHT, KC_P0, KC_PDOT, KC_PENT),
[FN4] = LAYOUT_ansi_101(
KC_ESC, KC_BRID, KC_BRIU, KC_MCTRL, KC_LNPAD, RGB_VAD, RGB_VAI, KC_MPRV, KC_MPLY, KC_MNXT, KC_MUTE, KC_VOLD, KC_VOLU, KC_DEL, KC_PSCR, KC_CALC, KC_FIND, KC_MPLY,
KC_GRV, KC_1, KC_2, KC_3, KC_4, KC_5, KC_6, KC_7, KC_8, KC_9, KC_0, KC_MINS, KC_EQL, KC_BSPC, KC_PGUP, KC_NUM, KC_PSLS, KC_PAST, KC_PMNS,
KC_GRV, LCK_FN1, LCK_FN2, LCK_FN3, LCK_FN4, LCK_CTL, KC_6, KC_7, KC_8, KC_9, LCK_BASE, KC_MINS, KC_EQL, KC_BSPC, KC_PGUP, KC_NUM, KC_PSLS, KC_PAST, KC_PMNS,
KC_TAB, KC_Q, KC_W, KC_E, KC_R, KC_T, KC_Y, KC_U, KC_I, KC_O, KC_P, KC_LBRC, KC_RBRC, KC_BSLS, KC_PGDN, KC_P7, KC_P8, KC_P9,
KC_CAPS, KC_A, KC_S, KC_D, KC_F, KC_G, KC_H, KC_J, KC_K, KC_L, KC_SCLN, KC_QUOT, KC_ENT, TD(TD_HOME_END), KC_P4, KC_P5, KC_P6, KC_PPLS,
CAPS_MOD, KC_A, KC_S, KC_D, KC_F, KC_G, KC_H, KC_J, KC_K, KC_L, KC_SCLN, KC_QUOT, KC_ENT, TD(TD_HOME_END), KC_P4, KC_P5, KC_P6, KC_PPLS,
KC_LSFT, KC_Z, KC_X, KC_C, KC_V, KC_B, KC_N, KC_M, KC_COMM, KC_DOT, KC_SLSH, KC_RSFT, KC_UP, KC_P1, KC_P2, KC_P3,
KC_LCTL, KC_LGUI, KC_LALT, KC_SPC, TO(BASE), TG(FN4), KC_RCTL, KC_LEFT, KC_DOWN, KC_RGHT, KC_P0, KC_PDOT, KC_PENT),
[KEEB_CTL] = LAYOUT_ansi_101(
_______, KC_BRID, KC_BRIU, KC_TASK, KC_FILE, RGB_VAD, RGB_VAI, KC_MPRV, KC_MPLY, KC_MNXT, KC_MUTE, KC_VOLD, KC_VOLU, _______, _______, _______, _______, RGB_TOG,
_______, BT_HST1, BT_HST2, BT_HST3, P2P4G, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______,
_______, BT_HST1, BT_HST2, BT_HST3, P2P4G, _______, _______, _______, _______, _______, LCK_BASE, _______, _______, _______, _______, _______, _______, _______, _______,
RGB_TOG, RGB_MOD, RGB_VAI, RGB_HUI, RGB_SAI, RGB_SPI, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______,
_______, RGB_RMOD, RGB_VAD, RGB_HUD, RGB_SAD, RGB_SPD, _______, _______, _______, _______, _______, _______, _______, KC_END, _______, _______, _______, _______,
_______, _______, _______, _______, _______, BAT_LVL, NK_TOGG, _______, _______, _______, _______, _______, _______, _______, _______, _______,
_______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______),
_______, _______, _______, _______, _______, _______, _______, _______, _______, _______, QK_CLEAR_EEPROM, _______, _______),
};
#if defined(ENCODER_MAP_ENABLE)
@@ -108,12 +275,55 @@ const uint16_t PROGMEM encoder_map[][NUM_ENCODERS][2] = {
// clang-format on
// Combos -----------------------------------------------------------------
// COMM + DOT + SLSH → LCK_BASE: emergency fallback to base layer.
// COMBO_ONLY_FROM_LAYER 0 (config.h) ensures these keycodes are always
// resolved from BASE so the combo fires regardless of the active layer.
const uint16_t PROGMEM fallback_combo[] = {KC_COMM, KC_DOT, KC_SLSH, COMBO_END};
combo_t key_combos[] = {
COMBO(fallback_combo, LCK_BASE),
};
// Re-assert locked layers whenever QMK modifies layer state (e.g. TT release).
// Also notifies the host application of the new active layer via Raw HID,
// unless the change was itself triggered by an incoming HID packet (anti-loop).
layer_state_t layer_state_set_user(layer_state_t state) {
state |= locked_layers;
if (!g_hid_recv_active) {
uint8_t top = get_highest_layer(state);
if (top != g_last_sent_layer) {
g_last_sent_layer = top;
hid_send_layer_sync(top, (uint8_t)locked_layers);
}
}
return state;
}
void keyboard_post_init_user(void) {
chord_init();
// Use the Linux unicode input method (Ctrl+Shift+U → hex → Enter).
set_unicode_input_mode(UNICODE_MODE_LINUX);
}
#ifdef DIP_SWITCH_ENABLE
// dip_switch_update_user is claimed by factory_test.c; use the weak
// dip_switch_update_keymap hook added in q5_max.c instead.
// True while the Win-side dip switch is active. The underlying RGB effect
// keeps running unchanged; rgb_matrix_indicators_advanced_user() paints over
// all LEDs with white each frame so neither mode nor EEPROM state is touched.
// Transport changes (which call rgb_matrix_init()) are therefore irrelevant.
static bool dip_win_active = false;
void dip_switch_update_keymap(uint8_t index, bool active) {
if (index == 0) {
dip_win_active = active;
}
}
#endif
bool process_record_user(uint16_t keycode, keyrecord_t *record) {
if (!process_record_keychron_common(keycode, record)) {
return false;
@@ -135,6 +345,86 @@ bool process_record_user(uint16_t keycode, keyrecord_t *record) {
}
switch (keycode) {
case CAPS_MOD:
// Custom tap logic: only intercept if it's a TAP AND a modifier is held.
// If it's a pure hold (Ctrl) or a pure tap (Esc), return true to let
// the MT() core handle it.
if (record->tap.count > 0 && record->event.pressed) {
uint8_t mods = get_mods();
if (mods & (MOD_MASK_GUI | MOD_MASK_ALT)) {
// Neutralize the modifier hold so releasing GUI/Alt doesn't
// trigger an OS "tap" action (like opening the Start menu).
tap_code(DUMMY_MOD_NEUTRALIZER_KEYCODE);
if (mods & MOD_MASK_GUI) {
autocorrect_toggle();
} else {
caps_word_toggle();
}
return false; // suppress default Esc tap
} else if (mods & MOD_MASK_SHIFT) {
tap_code(KC_CAPS);
return false; // suppress default Esc tap
}
}
return true; // let core handle Esc tap or Ctrl hold
case LCK_FN1:
case LCK_FN2:
case LCK_FN3:
case LCK_FN4:
case LCK_CTL:
case LCK_BASE:
if (record->event.pressed) {
uint8_t target;
switch (keycode) {
case LCK_FN1:
target = FN1;
break;
case LCK_FN2:
target = FN2;
break;
case LCK_FN3:
target = FN3;
break;
case LCK_FN4:
target = FN4;
break;
case LCK_CTL:
target = KEEB_CTL;
break;
default:
target = BASE;
break;
}
if (target != BASE && (locked_layers & (1UL << target))) {
// Already locked on this layer — unlock and return to BASE.
locked_layers = 0;
layer_move(BASE);
} else {
// Lock the target layer (clears any other lock first).
locked_layers = 0;
layer_move(target);
if (target != BASE) {
locked_layers = (1UL << target);
}
}
}
return false;
case BSP_DEL:
if (record->event.pressed) {
uint8_t mods = get_mods();
if (mods & MOD_MASK_SHIFT) {
del_mods(MOD_MASK_SHIFT);
tap_code(KC_DEL);
set_mods(mods);
} else {
tap_code(KC_BSPC);
}
}
return false;
case ALT_TAB_FWD:
if (record->event.pressed) {
if (!alt_tab_active) {
@@ -167,10 +457,84 @@ void matrix_scan_user(void) {
alt_tab_active = false;
}
chord_scan();
#ifdef LK_WIRELESS_ENABLE
// Push battery level to host every BAT_REPORT_INTERVAL_MS when wireless.
// First call fires immediately (g_bat_sent_once == false) so the host gets
// a reading as soon as the keyboard connects over USB in wireless mode.
if (!g_bat_sent_once || timer_elapsed32(g_bat_timer) >= BAT_REPORT_INTERVAL_MS) {
g_bat_sent_once = true;
g_bat_timer = timer_read32();
hid_send_battery();
}
#endif
}
// RGB Matrix Indicators --------------------------------------------------
// ESC key (LED index 0) shows which layer is active at a glance.
// BASE stays dark; each FN/control layer gets a distinct colour.
#if defined(RGB_MATRIX_ENABLE)
bool rgb_matrix_indicators_advanced_user(uint8_t led_min, uint8_t led_max) {
# ifdef DIP_SWITCH_ENABLE
// Win-side override: paint all LEDs white so the user gets a clean white
// backlight regardless of which RGB effect is active. The effect keeps
// ticking internally and resumes the moment the switch returns to Mac side.
// Layer and status indicators painted in the rest of this function appear
// on top of the white fill, so they continue to work normally.
if (dip_win_active) {
for (uint8_t i = led_min; i < led_max; i++) {
rgb_matrix_set_color(i, 255, 255, 255);
}
}
# endif
switch (get_highest_layer(layer_state)) {
case FN1:
RGB_MATRIX_INDICATOR_SET_COLOR(0, 0, 128, 255); // blue
break;
case FN2:
RGB_MATRIX_INDICATOR_SET_COLOR(0, 0, 220, 80); // green
break;
case FN3:
RGB_MATRIX_INDICATOR_SET_COLOR(0, 255, 120, 0); // orange
break;
case FN4:
RGB_MATRIX_INDICATOR_SET_COLOR(0, 180, 0, 255); // purple
break;
case KEEB_CTL:
RGB_MATRIX_INDICATOR_SET_COLOR(0, 255, 0, 0); // red
break;
default: // BASE — keep ESC dark
RGB_MATRIX_INDICATOR_SET_COLOR(0, 0, 0, 0);
break;
}
// Caps Lock key (LED 55): shows CapsWord/Autocorrect/CapsLock state.
if (is_caps_word_on()) {
RGB_MATRIX_INDICATOR_SET_COLOR(55, 0, 200, 0); // green: Caps Word active
} else if (!autocorrect_is_enabled()) {
RGB_MATRIX_INDICATOR_SET_COLOR(55, 150, 0, 255); // purple: Autocorrect disabled
} else if (host_keyboard_led_state().caps_lock) {
RGB_MATRIX_INDICATOR_SET_COLOR(55, 255, 255, 255); // white: normal Caps Lock on
} else {
RGB_MATRIX_INDICATOR_SET_COLOR(55, 0, 0, 0); // off
}
return false;
}
#endif // RGB_MATRIX_ENABLE
uint16_t get_tapping_term(uint16_t keycode, keyrecord_t *record) {
switch (keycode) {
case TD(TD_HOME_END):
return 175;
default:
return TAPPING_TERM;
}
}
// Tap Dance definitions
tap_dance_action_t tap_dance_actions[] = {
// Tap once for Home, twice for End
[TD_HOME_END] = ACTION_TAP_DANCE_DOUBLE(KC_HOME, KC_END),
};
};
@@ -1,4 +1,9 @@
VIA_ENABLE = yes
TAP_DANCE_ENABLE = yes
UNICODE_ENABLE = yes
COMBO_ENABLE = yes
CAPS_WORD_ENABLE = yes
AUTOCORRECT_ENABLE = yes
SRC += chord_unicode.c
DEBOUNCE_TYPE = sym_defer_pk
+2 -1
View File
@@ -18,7 +18,8 @@
"nkro" : true,
"rgb_matrix": true,
"raw" : true,
"send_string": true
"send_string": true,
"speculative_hold": true
},
"matrix_pins": {
"cols": ["C6", "C7", "C8", "A14", "A15", "C10", "C11", "C13", "C14", "C15", "C0", "C1", "C2", "C3", "A0", "A1", "A2", "A3", "B10"],
+37 -3
View File
@@ -29,16 +29,42 @@
#ifdef DIP_SWITCH_ENABLE
// Weak hook — override in keymap.c to handle dip-switch events
// without conflicting with factory_test.c's dip_switch_update_user.
__attribute__((weak)) void dip_switch_update_keymap(uint8_t index, bool active) {}
bool dip_switch_update_kb(uint8_t index, bool active) {
if (index == 0) {
default_layer_set(1UL << (active ? 2 : 0));
}
// if (index == 0) {
// default_layer_set(1UL << (active ? 2 : 0));
// }
dip_switch_update_user(index, active);
dip_switch_update_keymap(index, active);
return true;
}
#endif
#ifdef LK_WIRELESS_ENABLE
// Re-apply the saved QMK RGB mode every time wireless connects.
// During transport changes, rgb_matrix_init() reads the correct mode from
// EEPROM, but something in the BT/2.4G reconnect sequence can reset it
// before the display settles. This hook fires after connection is fully
// established, ensuring the Launcher-configured mode persists.
void wireless_enter_connected_kb(uint8_t host_idx) {
# if defined(RGB_MATRIX_ENABLE) && defined(KEYCHRON_RGB_ENABLE)
extern void eeconfig_init_custom_rgb(void);
eeconfig_init_custom_rgb();
// Re-read the QMK RGB mode from EEPROM and apply if it drifted.
rgb_config_t saved_rgb;
eeprom_read_block(&saved_rgb, EECONFIG_RGB_MATRIX, sizeof(saved_rgb));
if (saved_rgb.mode && saved_rgb.mode != rgb_matrix_get_mode()) {
rgb_matrix_mode_noeeprom(saved_rgb.mode);
}
# endif
}
#endif
void keyboard_post_init_kb(void) {
#ifdef LK_WIRELESS_ENABLE
palSetLineMode(P2P4_MODE_SELECT_PIN, PAL_MODE_INPUT);
@@ -52,6 +78,14 @@ void keyboard_post_init_kb(void) {
encoder_cb_init();
#endif
#if defined(RGB_MATRIX_ENABLE) && defined(KEYCHRON_RGB_ENABLE)
// Load Keychron custom RGB data (effect list, regions, per-key colours)
// from EEPROM into RAM. Without this call the arrays are zero-initialised
// and Launcher settings are lost on every power cycle or transport change.
extern void eeconfig_init_custom_rgb(void);
eeconfig_init_custom_rgb();
#endif
keyboard_post_init_user();
}
+2
View File
@@ -1,3 +1,5 @@
KEYCHRON_RGB_ENABLE = yes
include keyboards/keychron/common/wireless/wireless.mk
include keyboards/keychron/common/keychron_common.mk
+5
View File
@@ -270,6 +270,11 @@ void process_record(keyrecord_t *record) {
if (IS_NOEVENT(record->event)) {
return;
}
#ifdef SPECULATIVE_HOLD
if (record->event.pressed) {
speculative_key_settled(record);
}
#endif // SPECULATIVE_HOLD
if (!process_record_quantum(record)) {
#ifndef NO_ACTION_ONESHOT
+1 -1
View File
@@ -38,7 +38,7 @@ extern "C" {
/* tapping count and state */
typedef struct {
bool interrupted : 1;
bool reserved2 : 1;
bool speculated : 1;
bool reserved1 : 1;
bool reserved0 : 1;
uint8_t count : 4;
+172
View File
@@ -5,7 +5,10 @@
#include "action_layer.h"
#include "action_tapping.h"
#include "keycode.h"
#include "keycode_config.h"
#include "quantum_keycodes.h"
#include "timer.h"
#include "wait.h"
#ifndef NO_ACTION_TAPPING
@@ -49,6 +52,21 @@ __attribute__((weak)) bool get_permissive_hold(uint16_t keycode, keyrecord_t *re
}
# endif
# ifdef SPECULATIVE_HOLD
typedef struct {
keypos_t key;
uint8_t mods;
} speculative_key_t;
# define SPECULATIVE_KEYS_SIZE 8
static speculative_key_t speculative_keys[SPECULATIVE_KEYS_SIZE] = {};
static uint8_t num_speculative_keys = 0;
static uint8_t prev_speculative_mods = 0;
static uint8_t speculative_mods = 0;
/** Handler to be called on incoming press events. */
static void speculative_key_press(keyrecord_t *record);
# endif // SPECULATIVE_HOLD
# ifdef HOLD_ON_OTHER_KEY_PRESS_PER_KEY
__attribute__((weak)) bool get_hold_on_other_key_press(uint16_t keycode, keyrecord_t *record) {
return false;
@@ -78,6 +96,13 @@ static void debug_waiting_buffer(void);
* FIXME: Needs doc
*/
void action_tapping_process(keyrecord_t record) {
# ifdef SPECULATIVE_HOLD
prev_speculative_mods = speculative_mods;
if (record.event.pressed) {
speculative_key_press(&record);
}
# endif // SPECULATIVE_HOLD
if (process_tapping(&record)) {
if (IS_EVENT(record.event)) {
ac_dprintf("processed: ");
@@ -94,6 +119,12 @@ void action_tapping_process(keyrecord_t record) {
}
}
# ifdef SPECULATIVE_HOLD
if (speculative_mods != prev_speculative_mods) {
send_keyboard_report();
}
# endif // SPECULATIVE_HOLD
// process waiting_buffer
if (IS_EVENT(record.event) && waiting_buffer_head != waiting_buffer_tail) {
ac_dprintf("---- action_exec: process waiting_buffer -----\n");
@@ -520,6 +551,147 @@ void waiting_buffer_scan_tap(void) {
}
}
# ifdef SPECULATIVE_HOLD
static void debug_speculative_keys(void) {
ac_dprintf("mods = { ");
for (int8_t i = 0; i < num_speculative_keys; ++i) {
ac_dprintf("%02X ", speculative_keys[i].mods);
}
ac_dprintf("}, keys = { ");
for (int8_t i = 0; i < num_speculative_keys; ++i) {
ac_dprintf("%02X%02X ", speculative_keys[i].key.row, speculative_keys[i].key.col);
}
ac_dprintf("}\n");
}
// Find key in speculative_keys. Returns num_speculative_keys if not found.
static int8_t speculative_keys_find(keypos_t key) {
uint8_t i;
for (i = 0; i < num_speculative_keys; ++i) {
if (KEYEQ(speculative_keys[i].key, key)) {
break;
}
}
return i;
}
static void speculative_key_press(keyrecord_t *record) {
if (num_speculative_keys >= SPECULATIVE_KEYS_SIZE) { // Overflow!
ac_dprintf("SPECULATIVE KEYS OVERFLOW: IGNORING EVENT\n");
return; // Don't trigger: speculative_keys is full.
}
if (speculative_keys_find(record->event.key) < num_speculative_keys) {
return; // Don't trigger: key is already in speculative_keys.
}
const uint16_t keycode = get_record_keycode(record, false);
if (!IS_QK_MOD_TAP(keycode)) {
return; // Don't trigger: not a mod-tap key.
}
uint8_t mods = mod_config(QK_MOD_TAP_GET_MODS(keycode));
if ((mods & 0x10) != 0) { // Unpack 5-bit mods to 8-bit representation.
mods <<= 4;
}
if ((~(get_mods() | speculative_mods) & mods) == 0) {
return; // Don't trigger: mods are already active.
}
// Don't do Speculative Hold when there are non-speculated buffered events,
// since that could result in sending keys out of order.
for (uint8_t i = waiting_buffer_tail; i != waiting_buffer_head; i = (i + 1) % WAITING_BUFFER_SIZE) {
if (!waiting_buffer[i].tap.speculated) {
return;
}
}
if (get_speculative_hold(keycode, record)) {
record->tap.speculated = true;
speculative_mods |= mods;
// Remember the keypos and mods associated with this key.
speculative_keys[num_speculative_keys] = (speculative_key_t){
.key = record->event.key,
.mods = mods,
};
++num_speculative_keys;
ac_dprintf("Speculative Hold: ");
debug_speculative_keys();
}
}
uint8_t get_speculative_mods(void) {
return speculative_mods;
}
__attribute__((weak)) bool get_speculative_hold(uint16_t keycode, keyrecord_t *record) {
const uint8_t mods = mod_config(QK_MOD_TAP_GET_MODS(keycode));
return (mods & (MOD_LCTL | MOD_LSFT)) == (mods & (MOD_HYPR));
}
void speculative_key_settled(keyrecord_t *record) {
if (num_speculative_keys == 0) {
return; // Early return when there are no active speculative keys.
}
uint8_t i = speculative_keys_find(record->event.key);
const uint16_t keycode = get_record_keycode(record, false);
if (IS_QK_MOD_TAP(keycode) && record->tap.count == 0) { // MT hold press.
if (i < num_speculative_keys) {
--num_speculative_keys;
const uint8_t cleared_mods = speculative_keys[i].mods;
if (num_speculative_keys) {
speculative_mods &= ~cleared_mods;
// Don't call send_keyboard_report() here; allow default
// handling to reapply the mod before the next report.
// Remove the ith entry from speculative_keys.
for (uint8_t j = i; j < num_speculative_keys; ++j) {
speculative_keys[j] = speculative_keys[j + 1];
}
} else {
speculative_mods = 0;
}
ac_dprintf("Speculative Hold: settled %02x, ", cleared_mods);
debug_speculative_keys();
}
} else { // Tap press event; cancel speculatively-held mod.
if (i >= num_speculative_keys) {
i = 0;
}
// Clear mods for the ith key and all keys that follow.
uint8_t cleared_mods = 0;
for (uint8_t j = i; j < num_speculative_keys; ++j) {
cleared_mods |= speculative_keys[j].mods;
}
num_speculative_keys = i; // Remove ith and following entries.
if ((prev_speculative_mods & cleared_mods) != 0) {
# ifdef DUMMY_MOD_NEUTRALIZER_KEYCODE
neutralize_flashing_modifiers(get_mods() | prev_speculative_mods);
# endif // DUMMY_MOD_NEUTRALIZER_KEYCODE
}
if (num_speculative_keys) {
speculative_mods &= ~cleared_mods;
} else {
speculative_mods = 0;
}
send_keyboard_report();
wait_ms(TAP_CODE_DELAY);
ac_dprintf("Speculative Hold: canceled %02x, ", cleared_mods);
debug_speculative_keys();
}
}
# endif // SPECULATIVE_HOLD
/** \brief Tapping key debug print
*
* FIXME: Needs docs
+30
View File
@@ -46,6 +46,36 @@ bool get_permissive_hold(uint16_t keycode, keyrecord_t *record);
bool get_retro_tapping(uint16_t keycode, keyrecord_t *record);
bool get_hold_on_other_key_press(uint16_t keycode, keyrecord_t *record);
#ifdef SPECULATIVE_HOLD
/** Gets the currently active speculative mods. */
uint8_t get_speculative_mods(void);
/**
* Callback to say if a mod-tap key may be speculatively held.
*
* By default, speculative hold is enabled for mod-tap keys where the mod is
* Ctrl, Shift, and Ctrl+Shift for either hand.
*
* @param keycode Keycode of the mod-tap key.
* @param record Record associated with the mod-tap press event.
* @return True if the mod-tap key may be speculatively held.
*/
bool get_speculative_hold(uint16_t keycode, keyrecord_t *record);
/**
* Handler to be called on press events after tap-holds are settled.
*
* This function is to be called in process_record() in action.c, that is, just
* after tap-hold events are settled as either tapped or held. When `record`
* corresponds to a speculatively-held key, the speculative mod is cleared.
*
* @param record Record associated with the mod-tap press event.
*/
void speculative_key_settled(keyrecord_t *record);
#else
# define get_speculative_mods() 0
#endif // SPECULATIVE_HOLD
#ifdef DYNAMIC_TAPPING_TERM_ENABLE
extern uint16_t g_tapping_term;
#endif
+5
View File
@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "debug.h"
#include "action_util.h"
#include "action_layer.h"
#include "action_tapping.h"
#include "timer.h"
#include "keycode_config.h"
#include <string.h>
@@ -278,6 +279,10 @@ static uint8_t get_mods_for_report(void) {
}
#endif
#ifdef SPECULATIVE_HOLD
mods |= get_speculative_mods();
#endif
#ifdef KEY_OVERRIDE_ENABLE
// These need to be last to be able to properly control key overrides
mods &= ~suppressed_mods;
@@ -0,0 +1,23 @@
/* Copyright 2022 Vladislav Kucheriavykh
* Copyright 2025 Google LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "test_common.h"
#define SPECULATIVE_HOLD
#define DUMMY_MOD_NEUTRALIZER_KEYCODE KC_F24
@@ -0,0 +1,18 @@
# Copyright 2022 Vladislav Kucheriavykh
# Copyright 2025 Google LLC
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
MAGIC_ENABLE = yes
@@ -0,0 +1,352 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <functional>
#include "keyboard_report_util.hpp"
#include "keycode.h"
#include "test_common.hpp"
#include "action_tapping.h"
#include "test_fixture.hpp"
#include "test_keymap_key.hpp"
using testing::_;
using testing::AnyNumber;
using testing::InSequence;
namespace {
// Gets the unpacked 8-bit mods corresponding to a given mod-tap keycode.
uint8_t unpack_mod_tap_mods(uint16_t keycode) {
const uint8_t mods5 = QK_MOD_TAP_GET_MODS(keycode);
return (mods5 & 0x10) != 0 ? (mods5 << 4) : mods5;
}
bool get_speculative_hold_all_mods(uint16_t keycode, keyrecord_t *record) {
return true; // Enable Speculative Hold for all mod-tap keys.
}
// Indirection so that get_speculative_hold() can be
// replaced with other functions in the test cases below.
std::function<bool(uint16_t, keyrecord_t *)> get_speculative_hold_fun = get_speculative_hold_all_mods;
extern "C" bool get_speculative_hold(uint16_t keycode, keyrecord_t *record) {
return get_speculative_hold_fun(keycode, record);
}
class SpeculativeHoldAllMods : public TestFixture {
public:
void SetUp() override {
get_speculative_hold_fun = get_speculative_hold_all_mods;
}
};
TEST_F(SpeculativeHoldAllMods, tap_mod_tap_neutralized) {
TestDriver driver;
InSequence s;
auto mod_tap_key = KeymapKey(0, 1, 0, GUI_T(KC_P));
set_keymap({mod_tap_key});
// Press mod-tap-hold key. Mod is held speculatively.
EXPECT_REPORT(driver, (KC_LGUI));
mod_tap_key.press();
idle_for(10);
VERIFY_AND_CLEAR(driver);
// Release mod-tap-hold key. Speculative mod is neutralized and canceled.
EXPECT_REPORT(driver, (KC_LGUI, DUMMY_MOD_NEUTRALIZER_KEYCODE));
EXPECT_REPORT(driver, (KC_LGUI));
EXPECT_EMPTY_REPORT(driver);
EXPECT_REPORT(driver, (KC_P));
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Idle for tapping term of mod tap hold key.
idle_for(TAPPING_TERM - 10);
VERIFY_AND_CLEAR(driver);
}
TEST_F(SpeculativeHoldAllMods, hold_two_mod_taps) {
TestDriver driver;
InSequence s;
auto mod_tap_key1 = KeymapKey(0, 1, 0, LCTL_T(KC_A));
auto mod_tap_key2 = KeymapKey(0, 2, 0, RALT_T(KC_B));
set_keymap({mod_tap_key1, mod_tap_key2});
// Press first mod-tap key.
EXPECT_REPORT(driver, (KC_LCTL));
mod_tap_key1.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_EQ(get_speculative_mods(), MOD_BIT_LCTRL);
// Press second mod-tap key.
EXPECT_REPORT(driver, (KC_LCTL, KC_RALT));
mod_tap_key2.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_EQ(get_speculative_mods(), MOD_BIT_LCTRL | MOD_BIT_RALT);
EXPECT_NO_REPORT(driver);
idle_for(TAPPING_TERM + 1);
VERIFY_AND_CLEAR(driver);
EXPECT_EQ(get_speculative_mods(), 0);
EXPECT_EQ(get_mods(), MOD_BIT_LCTRL | MOD_BIT_RALT);
// Release first mod-tap key.
EXPECT_REPORT(driver, (KC_RALT));
mod_tap_key1.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Release second mod-tap key.
EXPECT_EMPTY_REPORT(driver);
mod_tap_key2.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
TEST_F(SpeculativeHoldAllMods, two_mod_taps_same_mods) {
TestDriver driver;
InSequence s;
auto mod_tap_key1 = KeymapKey(0, 1, 0, GUI_T(KC_A));
auto mod_tap_key2 = KeymapKey(0, 2, 0, GUI_T(KC_B));
set_keymap({mod_tap_key1, mod_tap_key2});
// Press first mod-tap key.
EXPECT_REPORT(driver, (KC_LGUI));
mod_tap_key1.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Tap second mod-tap key.
EXPECT_NO_REPORT(driver);
mod_tap_key2.press();
run_one_scan_loop();
mod_tap_key2.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Release first mod-tap key.
EXPECT_REPORT(driver, (KC_LGUI, DUMMY_MOD_NEUTRALIZER_KEYCODE));
EXPECT_REPORT(driver, (KC_LGUI));
EXPECT_EMPTY_REPORT(driver);
EXPECT_REPORT(driver, (KC_A));
EXPECT_REPORT(driver, (KC_A, KC_B));
EXPECT_REPORT(driver, (KC_A));
EXPECT_EMPTY_REPORT(driver);
mod_tap_key1.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
TEST_F(SpeculativeHoldAllMods, respects_get_speculative_hold_callback) {
TestDriver driver;
InSequence s;
auto mod_tap_key1 = KeymapKey(0, 0, 0, LSFT_T(KC_A));
auto mod_tap_key2 = KeymapKey(0, 1, 0, LSFT_T(KC_B));
auto mod_tap_key3 = KeymapKey(0, 2, 0, LCTL_T(KC_C));
auto mod_tap_key4 = KeymapKey(0, 3, 0, LCTL_T(KC_D));
auto mod_tap_key5 = KeymapKey(0, 4, 0, RSFT_T(KC_E));
auto mod_tap_key6 = KeymapKey(0, 5, 0, RSFT_T(KC_F));
set_keymap({mod_tap_key1, mod_tap_key2, mod_tap_key3, mod_tap_key4, mod_tap_key5, mod_tap_key6});
// Enable Speculative Hold selectively for some of the keys.
get_speculative_hold_fun = [](uint16_t keycode, keyrecord_t *record) {
switch (keycode) {
case LSFT_T(KC_B):
case LCTL_T(KC_D):
case RSFT_T(KC_F):
return true;
}
return false;
};
for (KeymapKey *mod_tap_key : {&mod_tap_key2, &mod_tap_key4, &mod_tap_key6}) {
SCOPED_TRACE(std::string("mod_tap_key = ") + mod_tap_key->name);
const uint8_t mods = unpack_mod_tap_mods(mod_tap_key->code);
// Long press and release mod_tap_key.
// For these keys where Speculative Hold is enabled, then the mod should
// activate immediately on keydown.
EXPECT_REPORT(driver, (KC_LCTL + biton(mods)));
mod_tap_key->press();
run_one_scan_loop();
EXPECT_EQ(get_speculative_mods(), mods);
EXPECT_EQ(get_mods(), 0);
VERIFY_AND_CLEAR(driver);
EXPECT_NO_REPORT(driver);
idle_for(TAPPING_TERM + 1);
EXPECT_EQ(get_speculative_mods(), 0);
EXPECT_EQ(get_mods(), mods);
VERIFY_AND_CLEAR(driver);
EXPECT_EMPTY_REPORT(driver);
mod_tap_key->release();
idle_for(TAPPING_TERM + 1);
EXPECT_EQ(get_speculative_mods(), 0);
EXPECT_EQ(get_mods(), 0);
VERIFY_AND_CLEAR(driver);
}
for (KeymapKey *mod_tap_key : {&mod_tap_key1, &mod_tap_key3, &mod_tap_key5}) {
SCOPED_TRACE(std::string("mod_tap_key = ") + mod_tap_key->name);
const uint8_t mods = unpack_mod_tap_mods(mod_tap_key->code);
// Long press and release mod_tap_key.
// For these keys where Speculative Hold is disabled, the mod should
// activate when the key has settled after the tapping term.
EXPECT_NO_REPORT(driver);
mod_tap_key->press();
run_one_scan_loop();
EXPECT_EQ(get_speculative_mods(), 0);
EXPECT_EQ(get_mods(), 0);
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_LCTL + biton(mods)));
idle_for(TAPPING_TERM + 1);
EXPECT_EQ(get_speculative_mods(), 0);
EXPECT_EQ(get_mods(), mods);
VERIFY_AND_CLEAR(driver);
EXPECT_EMPTY_REPORT(driver);
mod_tap_key->release();
idle_for(TAPPING_TERM + 1);
EXPECT_EQ(get_speculative_mods(), 0);
EXPECT_EQ(get_mods(), 0);
VERIFY_AND_CLEAR(driver);
}
}
TEST_F(SpeculativeHoldAllMods, respects_magic_mod_config) {
TestDriver driver;
InSequence s;
auto mod_tap_key = KeymapKey(0, 1, 0, CTL_T(KC_P));
set_keymap({mod_tap_key});
keymap_config.swap_lctl_lgui = true;
// Press mod-tap-hold key.
EXPECT_REPORT(driver, (KC_LGUI));
mod_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Release mod-tap-hold key.
EXPECT_REPORT(driver, (KC_LGUI, DUMMY_MOD_NEUTRALIZER_KEYCODE));
EXPECT_REPORT(driver, (KC_LGUI));
EXPECT_EMPTY_REPORT(driver);
EXPECT_REPORT(driver, (KC_P));
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
idle_for(TAPPING_TERM + 1);
VERIFY_AND_CLEAR(driver);
keymap_config.swap_lctl_lgui = false;
// Press mod-tap-hold key.
EXPECT_REPORT(driver, (KC_LCTL));
mod_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Release mod-tap-hold key.
EXPECT_EMPTY_REPORT(driver);
EXPECT_REPORT(driver, (KC_P));
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
TEST_F(SpeculativeHoldAllMods, tap_a_mod_tap_key_while_another_mod_tap_key_is_held) {
TestDriver driver;
InSequence s;
auto first_mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_P));
auto second_mod_tap_key = KeymapKey(0, 2, 0, RSFT_T(KC_A));
set_keymap({first_mod_tap_key, second_mod_tap_key});
// Press first mod-tap-hold key.
EXPECT_REPORT(driver, (KC_LSFT));
first_mod_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Press second tap-hold key.
EXPECT_REPORT(driver, (KC_LSFT, KC_RSFT));
second_mod_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Release second tap-hold key.
EXPECT_NO_REPORT(driver);
second_mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Release first mod-tap-hold key.
EXPECT_EMPTY_REPORT(driver);
EXPECT_REPORT(driver, (KC_P));
EXPECT_REPORT(driver, (KC_P, KC_A));
EXPECT_REPORT(driver, (KC_P));
EXPECT_EMPTY_REPORT(driver);
first_mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
TEST_F(SpeculativeHoldAllMods, tap_mod_tap_key_two_times) {
TestDriver driver;
InSequence s;
auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_P));
set_keymap({mod_tap_key});
// Press mod-tap-hold key.
EXPECT_REPORT(driver, (KC_LSFT));
mod_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Release mod-tap-hold key.
EXPECT_EMPTY_REPORT(driver);
EXPECT_REPORT(driver, (KC_P));
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Press mod-tap-hold key again.
EXPECT_REPORT(driver, (KC_P));
mod_tap_key.press();
idle_for(TAPPING_TERM);
VERIFY_AND_CLEAR(driver);
// Release mod-tap-hold key.
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
} // namespace
@@ -0,0 +1,22 @@
/* Copyright 2022 Vladislav Kucheriavykh
* Copyright 2025 Google LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "test_common.h"
#define SPECULATIVE_HOLD
@@ -0,0 +1,20 @@
# Copyright 2022 Vladislav Kucheriavykh
# Copyright 2025 Google LLC
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
KEY_OVERRIDE_ENABLE = yes
INTROSPECTION_KEYMAP_C = test_keymap.c
@@ -0,0 +1,20 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "quantum.h"
// Shift + Esc = Home
const key_override_t home_esc_override = ko_make_basic(MOD_MASK_SHIFT, KC_ESC, KC_HOME);
const key_override_t *key_overrides[] = {&home_esc_override};
@@ -0,0 +1,535 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <functional>
#include "keyboard_report_util.hpp"
#include "keycode.h"
#include "test_common.hpp"
#include "action_tapping.h"
#include "test_fixture.hpp"
#include "test_keymap_key.hpp"
using testing::_;
using testing::AnyNumber;
using testing::InSequence;
namespace {
bool process_record_user_default(uint16_t keycode, keyrecord_t *record) {
return true;
}
// Indirection so that process_record_user() can be
// replaced with other functions in the test cases below.
std::function<bool(uint16_t, keyrecord_t *)> process_record_user_fun = process_record_user_default;
extern "C" bool process_record_user(uint16_t keycode, keyrecord_t *record) {
return process_record_user_fun(keycode, record);
}
class SpeculativeHoldDefault : public TestFixture {
public:
void SetUp() override {
process_record_user_fun = process_record_user_default;
}
};
TEST_F(SpeculativeHoldDefault, get_speculative_hold) {
keyrecord_t record = {};
// With the default definition of get_speculative_hold(), Speculative Hold
// is enabled for Ctrl and Shift.
EXPECT_TRUE(get_speculative_hold(MT(MOD_LCTL, KC_NO), &record));
EXPECT_TRUE(get_speculative_hold(MT(MOD_LSFT, KC_NO), &record));
EXPECT_TRUE(get_speculative_hold(MT(MOD_LCTL | MOD_LSFT, KC_NO), &record));
EXPECT_TRUE(get_speculative_hold(MT(MOD_RCTL, KC_NO), &record));
EXPECT_TRUE(get_speculative_hold(MT(MOD_RSFT, KC_NO), &record));
EXPECT_TRUE(get_speculative_hold(MT(MOD_RCTL | MOD_RSFT, KC_NO), &record));
EXPECT_FALSE(get_speculative_hold(MT(MOD_LALT, KC_NO), &record));
EXPECT_FALSE(get_speculative_hold(MT(MOD_LGUI, KC_NO), &record));
EXPECT_FALSE(get_speculative_hold(MT(MOD_RALT, KC_NO), &record));
EXPECT_FALSE(get_speculative_hold(MT(MOD_RGUI, KC_NO), &record));
EXPECT_FALSE(get_speculative_hold(MT(MOD_MEH, KC_NO), &record));
EXPECT_FALSE(get_speculative_hold(MT(MOD_HYPR, KC_NO), &record));
}
TEST_F(SpeculativeHoldDefault, tap_mod_tap) {
TestDriver driver;
InSequence s;
static int process_record_user_calls = 0;
auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_P));
set_keymap({mod_tap_key});
process_record_user_fun = [](uint16_t keycode, keyrecord_t *record) {
++process_record_user_calls;
return true;
};
// Press mod-tap-hold key. Mod is held speculatively.
EXPECT_REPORT(driver, (KC_LSFT));
mod_tap_key.press();
idle_for(10);
VERIFY_AND_CLEAR(driver);
EXPECT_EQ(get_speculative_mods(), MOD_BIT_LSHIFT);
// Speculative mod holds and releases are made directly, bypassing regular
// event processing. No calls have been made yet to process_record_user().
EXPECT_EQ(process_record_user_calls, 0);
// Release mod-tap-hold key.
EXPECT_EMPTY_REPORT(driver); // Speculative mod canceled.
EXPECT_REPORT(driver, (KC_P));
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// All mods are released.
EXPECT_EQ(get_speculative_mods(), 0);
EXPECT_EQ(get_mods(), 0);
// Two calls have now been made, for pressing and releasing KC_P.
EXPECT_EQ(process_record_user_calls, 2);
// Idle for tapping term of mod tap hold key.
idle_for(TAPPING_TERM - 10);
VERIFY_AND_CLEAR(driver);
}
TEST_F(SpeculativeHoldDefault, key_overrides) {
TestDriver driver;
InSequence s;
auto mod_tap_key = KeymapKey(0, 1, 0, LSFT_T(KC_A));
auto esc_key = KeymapKey(0, 3, 0, KC_ESC);
set_keymap({mod_tap_key, esc_key});
// Press mod-tap Shift key.
EXPECT_REPORT(driver, (KC_LSFT));
mod_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Press Esc key.
EXPECT_EMPTY_REPORT(driver).Times(AnyNumber());
EXPECT_REPORT(driver, (KC_HOME));
esc_key.press();
idle_for(TAPPING_TERM + 1);
VERIFY_AND_CLEAR(driver);
// Release Esc key.
EXPECT_EMPTY_REPORT(driver).Times(AnyNumber());
EXPECT_REPORT(driver, (KC_LSFT));
esc_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Release mod-tap Shift key.
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
TEST_F(SpeculativeHoldDefault, tap_regular_key_while_mod_tap_key_is_held) {
TestDriver driver;
InSequence s;
auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_P));
auto regular_key = KeymapKey(0, 2, 0, KC_A);
set_keymap({mod_tap_key, regular_key});
// Press mod-tap-hold key.
EXPECT_REPORT(driver, (KC_LSFT));
mod_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Tap regular key.
EXPECT_NO_REPORT(driver);
regular_key.press();
run_one_scan_loop();
regular_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Release mod-tap-hold key.
EXPECT_EMPTY_REPORT(driver);
EXPECT_REPORT(driver, (KC_P));
EXPECT_REPORT(driver, (KC_P, KC_A));
EXPECT_REPORT(driver, (KC_P));
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Idle for tapping term of mod tap hold key.
idle_for(TAPPING_TERM - 3);
VERIFY_AND_CLEAR(driver);
}
TEST_F(SpeculativeHoldDefault, tap_mod_tap_key_two_times) {
TestDriver driver;
InSequence s;
auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_P));
set_keymap({mod_tap_key});
// Press mod-tap-hold key.
EXPECT_REPORT(driver, (KC_LSFT));
mod_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Release mod-tap-hold key.
EXPECT_EMPTY_REPORT(driver);
EXPECT_REPORT(driver, (KC_P));
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Press mod-tap-hold key again.
EXPECT_REPORT(driver, (KC_P));
mod_tap_key.press();
idle_for(TAPPING_TERM);
VERIFY_AND_CLEAR(driver);
// Release mod-tap-hold key.
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
TEST_F(SpeculativeHoldDefault, tap_mod_tap_key_twice_and_hold_on_second_time) {
TestDriver driver;
InSequence s;
auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_P));
set_keymap({mod_tap_key});
// Press mod-tap-hold key.
EXPECT_REPORT(driver, (KC_LSFT));
mod_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Release mod-tap-hold key.
EXPECT_EMPTY_REPORT(driver);
EXPECT_REPORT(driver, (KC_P));
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Press mod-tap-hold key again.
EXPECT_REPORT(driver, (KC_P));
mod_tap_key.press();
idle_for(TAPPING_TERM);
VERIFY_AND_CLEAR(driver);
// Release mod-tap-hold key.
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
TEST_F(SpeculativeHoldDefault, tap_and_hold_mod_tap_key) {
TestDriver driver;
InSequence s;
static int process_record_user_calls = 0;
auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_P));
set_keymap({mod_tap_key});
process_record_user_fun = [](uint16_t keycode, keyrecord_t *record) {
++process_record_user_calls;
return true;
};
// Press mod-tap-hold key.
EXPECT_REPORT(driver, (KC_LSFT));
mod_tap_key.press();
idle_for(TAPPING_TERM - 1);
EXPECT_EQ(get_speculative_mods(), MOD_BIT_LSHIFT);
EXPECT_EQ(get_mods(), 0);
// Speculative mod holds and releases are made directly, bypassing regular
// event processing. No calls have been made yet to process_record_user().
EXPECT_EQ(process_record_user_calls, 0);
idle_for(2);
// Now that the key has settled, one call has been made for the hold event.
EXPECT_EQ(process_record_user_calls, 1);
EXPECT_EQ(get_speculative_mods(), 0);
EXPECT_EQ(get_mods(), MOD_BIT_LSHIFT);
VERIFY_AND_CLEAR(driver);
// Release mod-tap-hold key.
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
EXPECT_EQ(process_record_user_calls, 2);
VERIFY_AND_CLEAR(driver);
}
// Test with layer tap and speculative mod tap keys on the same layer,
// rolling from LT to MT key:
// "LT down, MT down, (wait out tapping term), LT up, MT up."
TEST_F(SpeculativeHoldDefault, lt_mt_same_layer_roll) {
TestDriver driver;
InSequence s;
auto layer_tap_key = KeymapKey(0, 0, 0, LT(1, KC_A));
auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_B));
auto regular_key = KeymapKey(1, 1, 0, KC_C);
set_keymap({layer_tap_key, mod_tap_key, regular_key});
// Press layer tap key.
EXPECT_NO_REPORT(driver);
layer_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Press mod-tap key, after flow tap term but within tapping term. The
// speculative mod activates.
EXPECT_REPORT(driver, (KC_LSFT));
mod_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Wait for the layer tap key to settle.
EXPECT_EMPTY_REPORT(driver);
EXPECT_REPORT(driver, (KC_C));
idle_for(TAPPING_TERM);
VERIFY_AND_CLEAR(driver);
// Release keys.
EXPECT_EMPTY_REPORT(driver);
layer_tap_key.release();
run_one_scan_loop();
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// All mods are released.
EXPECT_EQ(get_mods() | get_speculative_mods(), 0);
}
// Test with layer tap and speculative mod tap keys on the same layer, trying a
// nested press:
// "LT down, MT down, (wait out tapping term), MT up, LT up."
TEST_F(SpeculativeHoldDefault, lt_mt_same_layer_nested_press) {
TestDriver driver;
InSequence s;
auto layer_tap_key = KeymapKey(0, 0, 0, LT(1, KC_A));
auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_B));
auto regular_key = KeymapKey(1, 1, 0, KC_C);
set_keymap({layer_tap_key, mod_tap_key, regular_key});
EXPECT_NO_REPORT(driver);
layer_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_LSFT));
mod_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_EMPTY_REPORT(driver);
EXPECT_REPORT(driver, (KC_C));
run_one_scan_loop();
idle_for(TAPPING_TERM);
VERIFY_AND_CLEAR(driver);
// Release keys: MT first, LT second.
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
layer_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// All mods are released.
EXPECT_EQ(get_mods() | get_speculative_mods(), 0);
}
// Test with layer tap and speculative mod tap keys on the same layer, trying a
// nested press with the MT first:
// "MT down, LT down, (wait out tapping term), LT up, MT up."
TEST_F(SpeculativeHoldDefault, mt_lt_same_layer_nested_press) {
TestDriver driver;
InSequence s;
auto layer_tap_key = KeymapKey(0, 0, 0, LT(1, KC_A));
auto mod_tap_key = KeymapKey(0, 1, 0, SFT_T(KC_B));
auto regular_key = KeymapKey(1, 1, 0, KC_C);
set_keymap({layer_tap_key, mod_tap_key, regular_key});
EXPECT_REPORT(driver, (KC_LSFT));
mod_tap_key.press();
run_one_scan_loop();
EXPECT_NO_REPORT(driver);
layer_tap_key.press();
idle_for(TAPPING_TERM + 1);
VERIFY_AND_CLEAR(driver);
EXPECT_EMPTY_REPORT(driver);
layer_tap_key.release();
run_one_scan_loop();
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// All mods are released.
EXPECT_EQ(get_mods() | get_speculative_mods(), 0);
}
// Test with a speculative mod tap key reached by a layer tap key, rolling from
// LT to MT key:
// "LT down, MT down, (wait out tapping term), LT up, MT up."
TEST_F(SpeculativeHoldDefault, lt_mt_different_layer_roll) {
TestDriver driver;
InSequence s;
auto layer_tap_key = KeymapKey(0, 0, 0, LT(1, KC_A));
auto regular_key = KeymapKey(0, 1, 0, KC_B);
auto placeholder_key = KeymapKey(1, 0, 0, KC_NO);
auto mod_tap_key = KeymapKey(1, 1, 0, SFT_T(KC_C));
set_keymap({layer_tap_key, regular_key, placeholder_key, mod_tap_key});
// Press layer tap key.
EXPECT_NO_REPORT(driver);
layer_tap_key.press();
run_one_scan_loop();
// Press mod tap key.
mod_tap_key.press();
idle_for(TAPPING_TERM);
VERIFY_AND_CLEAR(driver);
// Release keys.
EXPECT_REPORT(driver, (KC_LSFT));
layer_tap_key.release();
idle_for(TAPPING_TERM);
VERIFY_AND_CLEAR(driver);
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// All mods are released.
EXPECT_EQ(get_mods() | get_speculative_mods(), 0);
}
// Test with a speculative mod tap key reached by a layer tap key, slowly
// rolling from LT to MT key:
// "LT down, (wait), MT down, (wait), LT up, MT up."
TEST_F(SpeculativeHoldDefault, lt_mt_different_layer_slow_roll) {
TestDriver driver;
InSequence s;
auto layer_tap_key = KeymapKey(0, 0, 0, LT(1, KC_A));
auto regular_key = KeymapKey(0, 1, 0, KC_B);
auto placeholder_key = KeymapKey(1, 0, 0, KC_NO);
auto mod_tap_key = KeymapKey(1, 1, 0, SFT_T(KC_C));
set_keymap({layer_tap_key, regular_key, placeholder_key, mod_tap_key});
EXPECT_NO_REPORT(driver);
layer_tap_key.press();
idle_for(TAPPING_TERM + 1);
EXPECT_REPORT(driver, (KC_LSFT));
mod_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_EMPTY_REPORT(driver);
EXPECT_REPORT(driver, (KC_B));
EXPECT_EMPTY_REPORT(driver);
layer_tap_key.release();
run_one_scan_loop();
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// All mods are released.
EXPECT_EQ(get_mods() | get_speculative_mods(), 0);
}
// Test with a speculative mod tap key reached by a layer tap key, trying a
// nested press:
// "LT down, MT down, (wait out tapping term), MT up, LT up."
TEST_F(SpeculativeHoldDefault, lt_mt_different_layer_nested_press) {
TestDriver driver;
InSequence s;
auto layer_tap_key = KeymapKey(0, 0, 0, LT(1, KC_A));
auto regular_key = KeymapKey(0, 1, 0, KC_B);
auto placeholder_key = KeymapKey(1, 0, 0, KC_NO);
auto mod_tap_key = KeymapKey(1, 1, 0, SFT_T(KC_C));
set_keymap({layer_tap_key, regular_key, placeholder_key, mod_tap_key});
EXPECT_NO_REPORT(driver);
layer_tap_key.press();
run_one_scan_loop();
mod_tap_key.press();
idle_for(TAPPING_TERM);
VERIFY_AND_CLEAR(driver);
// Release keys.
EXPECT_REPORT(driver, (KC_LSFT));
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
layer_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// All mods are released.
EXPECT_EQ(get_mods() | get_speculative_mods(), 0);
}
// Test with a speculative mod tap key reached by a layer tap key, trying a
// slow nested press:
// "LT down, (wait), MT down, MT up, LT up."
TEST_F(SpeculativeHoldDefault, lt_mt_different_layer_slow_nested_press) {
TestDriver driver;
InSequence s;
auto layer_tap_key = KeymapKey(0, 0, 0, LT(1, KC_A));
auto regular_key = KeymapKey(0, 1, 0, KC_B);
auto placeholder_key = KeymapKey(1, 0, 0, KC_NO);
auto mod_tap_key = KeymapKey(1, 1, 0, SFT_T(KC_C));
set_keymap({layer_tap_key, regular_key, placeholder_key, mod_tap_key});
EXPECT_NO_REPORT(driver);
layer_tap_key.press();
idle_for(TAPPING_TERM + 1);
EXPECT_REPORT(driver, (KC_LSFT));
mod_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_EMPTY_REPORT(driver);
EXPECT_REPORT(driver, (KC_C));
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
layer_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// All mods are released.
EXPECT_EQ(get_mods() | get_speculative_mods(), 0);
}
} // namespace
@@ -0,0 +1,25 @@
/* Copyright 2022 Vladislav Kucheriavykh
* Copyright 2025 Google LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "test_common.h"
#define SPECULATIVE_HOLD
#define FLOW_TAP_TERM 150
#define PERMISSIVE_HOLD
#define DUMMY_MOD_NEUTRALIZER_KEYCODE KC_F24
@@ -0,0 +1,15 @@
# Copyright 2022 Vladislav Kucheriavykh
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,30 @@
/* Copyright 2022 Isaac Elenbaas
* Copyright 2025 Google LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "test_common.h"
#define SPECULATIVE_HOLD
#define PERMISSIVE_HOLD
#define RETRO_SHIFT 2 * TAPPING_TERM
// releases between AUTO_SHIFT_TIMEOUT and TAPPING_TERM are not tested
#define AUTO_SHIFT_TIMEOUT TAPPING_TERM
#define AUTO_SHIFT_MODIFIERS
#define DUMMY_MOD_NEUTRALIZER_KEYCODE KC_F24
@@ -0,0 +1,17 @@
# Copyright 2022 Isaac Elenbaas
# Copyright 2025 Google LLC
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
AUTO_SHIFT_ENABLE = yes
@@ -0,0 +1,689 @@
// Copyright 2022 Isaac Elenbaas
// Copyright 2025 Google LLC
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "keyboard_report_util.hpp"
#include "keycode.h"
#include "test_common.hpp"
#include "action_tapping.h"
#include "test_fixture.hpp"
#include "test_keymap_key.hpp"
extern "C" {
bool get_speculative_hold(uint16_t keycode, keyrecord_t *record) {
return true;
}
bool get_auto_shifted_key(uint16_t keycode, keyrecord_t *record) {
return true;
}
} // extern "C"
using testing::_;
using testing::AnyNumber;
using testing::AnyOf;
using testing::InSequence;
class RetroShiftPermissiveHold : public TestFixture {};
TEST_F(RetroShiftPermissiveHold, tap_regular_key_while_mod_tap_key_is_held_under_tapping_term) {
TestDriver driver;
InSequence s;
auto mod_tap_key = KeymapKey(0, 0, 0, LCTL_T(KC_P));
auto regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, KC_A);
set_keymap({mod_tap_key, regular_key});
// Press mod-tap-hold key.
EXPECT_REPORT(driver, (KC_LCTL));
mod_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Press regular key.
EXPECT_NO_REPORT(driver);
regular_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Release regular key.
EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LCTL))).Times(AnyNumber());
EXPECT_REPORT(driver, (KC_LCTL, KC_A));
EXPECT_REPORT(driver, (KC_LCTL));
regular_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Release mod-tap-hold key.
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
TEST_F(RetroShiftPermissiveHold, tap_mod_tap_key_while_mod_tap_key_is_held_under_tapping_term) {
TestDriver driver;
InSequence s;
auto mod_tap_key = KeymapKey(0, 0, 0, LCTL_T(KC_P));
auto mod_tap_regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, LALT_T(KC_A));
set_keymap({mod_tap_key, mod_tap_regular_key});
// Press mod-tap-hold key.
EXPECT_REPORT(driver, (KC_LCTL));
mod_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Press mod-tap-regular key.
EXPECT_REPORT(driver, (KC_LCTL, KC_LALT));
mod_tap_regular_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Release mod-tap-regular key.
EXPECT_REPORT(driver, (KC_LCTL));
EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LCTL))).Times(AnyNumber());
EXPECT_REPORT(driver, (KC_LCTL, KC_A));
EXPECT_REPORT(driver, (KC_LCTL));
mod_tap_regular_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Release mod-tap-hold key.
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
TEST_F(RetroShiftPermissiveHold, tap_regular_key_while_mod_tap_key_is_held_over_tapping_term) {
TestDriver driver;
InSequence s;
auto mod_tap_key = KeymapKey(0, 0, 0, LCTL_T(KC_P));
auto regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, KC_A);
set_keymap({mod_tap_key, regular_key});
// Press mod-tap-hold key.
EXPECT_REPORT(driver, (KC_LCTL));
mod_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Press regular key.
EXPECT_NO_REPORT(driver);
regular_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Release regular key.
EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LCTL))).Times(AnyNumber());
EXPECT_REPORT(driver, (KC_LCTL, KC_A));
EXPECT_REPORT(driver, (KC_LCTL));
regular_key.release();
run_one_scan_loop();
idle_for(TAPPING_TERM);
VERIFY_AND_CLEAR(driver);
// Release mod-tap-hold key.
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
TEST_F(RetroShiftPermissiveHold, tap_mod_tap_key_while_mod_tap_key_is_held_over_tapping_term) {
TestDriver driver;
InSequence s;
auto mod_tap_key = KeymapKey(0, 0, 0, LCTL_T(KC_P));
auto mod_tap_regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, LALT_T(KC_A));
set_keymap({mod_tap_key, mod_tap_regular_key});
// Press mod-tap-hold key.
EXPECT_REPORT(driver, (KC_LCTL));
mod_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Press mod-tap-regular key.
EXPECT_REPORT(driver, (KC_LCTL, KC_LALT));
mod_tap_regular_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Release mod-tap-regular key.
EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LCTL))).Times(AnyNumber());
EXPECT_REPORT(driver, (KC_LCTL, KC_A));
EXPECT_REPORT(driver, (KC_LCTL));
mod_tap_regular_key.release();
run_one_scan_loop();
idle_for(TAPPING_TERM);
VERIFY_AND_CLEAR(driver);
// Release mod-tap-hold key.
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
TEST_F(RetroShiftPermissiveHold, hold_regular_key_while_mod_tap_key_is_held_over_tapping_term) {
TestDriver driver;
InSequence s;
auto mod_tap_key = KeymapKey(0, 0, 0, LCTL_T(KC_P));
auto regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, KC_A);
set_keymap({mod_tap_key, regular_key});
// Press mod-tap-hold key.
EXPECT_REPORT(driver, (KC_LCTL));
mod_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Press regular key.
EXPECT_NO_REPORT(driver);
regular_key.press();
run_one_scan_loop();
idle_for(AUTO_SHIFT_TIMEOUT);
VERIFY_AND_CLEAR(driver);
// Release regular key.
// clang-format off
EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
KeyboardReport(KC_LCTL, KC_LSFT),
KeyboardReport(KC_LSFT),
KeyboardReport(KC_LCTL))))
.Times(AnyNumber());
// clang-format on
EXPECT_REPORT(driver, (KC_LCTL, KC_LSFT, KC_A));
// clang-format off
EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
KeyboardReport(KC_LCTL, KC_LSFT),
KeyboardReport(KC_LSFT))))
.Times(AnyNumber());
// clang-format on
EXPECT_REPORT(driver, (KC_LCTL));
regular_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Release mod-tap-hold key.
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
TEST_F(RetroShiftPermissiveHold, hold_mod_tap_key_while_mod_tap_key_is_held_over_tapping_term) {
TestDriver driver;
InSequence s;
auto mod_tap_key = KeymapKey(0, 0, 0, LCTL_T(KC_P));
auto mod_tap_regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, LALT_T(KC_A));
set_keymap({mod_tap_key, mod_tap_regular_key});
// Press mod-tap-hold key.
EXPECT_REPORT(driver, (KC_LCTL));
mod_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Press mod-tap-regular key.
EXPECT_REPORT(driver, (KC_LCTL, KC_LALT));
mod_tap_regular_key.press();
run_one_scan_loop();
idle_for(AUTO_SHIFT_TIMEOUT);
VERIFY_AND_CLEAR(driver);
// Release mod-tap-regular key.
// clang-format off
EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
KeyboardReport(KC_LCTL, KC_LSFT),
KeyboardReport(KC_LSFT),
KeyboardReport(KC_LCTL))))
.Times(AnyNumber());
// clang-format on
EXPECT_REPORT(driver, (KC_LCTL, KC_LSFT, KC_A));
// clang-format off
EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
KeyboardReport(KC_LCTL, KC_LSFT),
KeyboardReport(KC_LSFT))))
.Times(AnyNumber());
// clang-format on
EXPECT_REPORT(driver, (KC_LCTL));
mod_tap_regular_key.release();
run_one_scan_loop();
idle_for(TAPPING_TERM);
VERIFY_AND_CLEAR(driver);
// Release mod-tap-hold key.
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
TEST_F(RetroShiftPermissiveHold, roll_tap_regular_key_while_mod_tap_key_is_held_under_tapping_term) {
TestDriver driver;
InSequence s;
auto mod_tap_key = KeymapKey(0, 0, 0, LCTL_T(KC_P));
auto regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, KC_A);
set_keymap({mod_tap_key, regular_key});
// Press mod-tap-hold key.
EXPECT_REPORT(driver, (KC_LCTL));
mod_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Press regular key.
EXPECT_NO_REPORT(driver);
regular_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Release mod-tap-hold key.
EXPECT_EMPTY_REPORT(driver);
EXPECT_REPORT(driver, (KC_P));
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Release regular key.
EXPECT_REPORT(driver, (KC_A));
EXPECT_EMPTY_REPORT(driver);
regular_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
TEST_F(RetroShiftPermissiveHold, roll_tap_mod_tap_key_while_mod_tap_key_is_held_under_tapping_term) {
TestDriver driver;
InSequence s;
auto mod_tap_key = KeymapKey(0, 0, 0, LCTL_T(KC_P));
auto mod_tap_regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, LALT_T(KC_A));
set_keymap({mod_tap_key, mod_tap_regular_key});
// Press mod-tap-hold key.
EXPECT_REPORT(driver, (KC_LCTL));
mod_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Press mod-tap-regular key.
EXPECT_REPORT(driver, (KC_LCTL, KC_LALT));
mod_tap_regular_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Release mod-tap-hold key.
EXPECT_EMPTY_REPORT(driver);
EXPECT_REPORT(driver, (KC_P));
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Release mod-tap-regular key.
EXPECT_REPORT(driver, (KC_A));
EXPECT_EMPTY_REPORT(driver);
mod_tap_regular_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
TEST_F(RetroShiftPermissiveHold, roll_hold_regular_key_while_mod_tap_key_is_held_under_tapping_term) {
TestDriver driver;
InSequence s;
auto mod_tap_key = KeymapKey(0, 0, 0, LCTL_T(KC_P));
auto regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, KC_A);
set_keymap({mod_tap_key, regular_key});
// Press mod-tap-hold key.
EXPECT_REPORT(driver, (KC_LCTL));
mod_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Press regular key.
EXPECT_NO_REPORT(driver);
regular_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Release mod-tap-hold key.
EXPECT_EMPTY_REPORT(driver);
EXPECT_REPORT(driver, (KC_P));
EXPECT_EMPTY_REPORT(driver);
EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LSFT))).Times(AnyNumber());
EXPECT_REPORT(driver, (KC_LSFT, KC_A));
EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LSFT))).Times(AnyNumber());
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
idle_for(AUTO_SHIFT_TIMEOUT);
VERIFY_AND_CLEAR(driver);
// Release regular key.
EXPECT_NO_REPORT(driver);
regular_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
TEST_F(RetroShiftPermissiveHold, roll_hold_mod_tap_key_while_mod_tap_key_is_held_under_tapping_term) {
TestDriver driver;
InSequence s;
auto mod_tap_key = KeymapKey(0, 0, 0, LCTL_T(KC_P));
auto mod_tap_regular_key = KeymapKey(0, MATRIX_COLS - 1, 0, LALT_T(KC_A));
set_keymap({mod_tap_key, mod_tap_regular_key});
// Press mod-tap-hold key.
EXPECT_REPORT(driver, (KC_LCTL));
mod_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Press mod-tap-regular key.
EXPECT_REPORT(driver, (KC_LCTL, KC_LALT));
mod_tap_regular_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Release mod-tap-hold key.
EXPECT_EMPTY_REPORT(driver);
EXPECT_REPORT(driver, (KC_P));
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Release mod-tap-regular key.
EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LSFT))).Times(AnyNumber());
EXPECT_REPORT(driver, (KC_LSFT, KC_A));
EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LSFT))).Times(AnyNumber());
EXPECT_EMPTY_REPORT(driver);
idle_for(AUTO_SHIFT_TIMEOUT);
mod_tap_regular_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
// Test with layer tap and speculative mod tap keys on the same layer, rolling
// from LT to MT key:
// "LT down, MT down, (wait out tapping term), LT up, MT up."
TEST_F(RetroShiftPermissiveHold, lt_mt_same_layer_roll) {
TestDriver driver;
InSequence s;
auto layer_tap_key = KeymapKey(0, 0, 0, LT(1, KC_A));
auto mod_tap_key = KeymapKey(0, 1, 0, LCTL_T(KC_B));
auto regular_key = KeymapKey(1, 1, 0, KC_C);
set_keymap({layer_tap_key, mod_tap_key, regular_key});
// Press layer tap key.
EXPECT_NO_REPORT(driver);
layer_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Press mod-tap key, after flow tap term but within tapping term. The
// speculative mod activates.
EXPECT_REPORT(driver, (KC_LCTL));
mod_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Wait for the layer tap key to settle.
EXPECT_NO_REPORT(driver);
idle_for(TAPPING_TERM);
VERIFY_AND_CLEAR(driver);
// Release keys.
EXPECT_EMPTY_REPORT(driver);
EXPECT_REPORT(driver, (KC_LSFT, KC_A));
EXPECT_REPORT(driver, (KC_LSFT));
EXPECT_EMPTY_REPORT(driver);
EXPECT_REPORT(driver, (KC_B));
EXPECT_EMPTY_REPORT(driver);
layer_tap_key.release();
run_one_scan_loop();
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// All mods are released.
EXPECT_EQ(get_mods() | get_speculative_mods(), 0);
}
// Test with layer tap and speculative mod tap keys on the same layer, trying
// a nested press from LT to MT key:
// "LT down, MT down, (wait out tapping term), MT up, LT up."
TEST_F(RetroShiftPermissiveHold, lt_mt_same_layer_nested_press) {
TestDriver driver;
InSequence s;
auto layer_tap_key = KeymapKey(0, 0, 0, LT(1, KC_A));
auto mod_tap_key = KeymapKey(0, 1, 0, LCTL_T(KC_B));
auto regular_key = KeymapKey(1, 1, 0, KC_C);
set_keymap({layer_tap_key, mod_tap_key, regular_key});
EXPECT_NO_REPORT(driver);
layer_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_LCTL));
mod_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_NO_REPORT(driver);
run_one_scan_loop();
idle_for(TAPPING_TERM);
VERIFY_AND_CLEAR(driver);
// Release keys: MT first, LT second.
EXPECT_EMPTY_REPORT(driver);
EXPECT_REPORT(driver, (KC_LSFT, KC_C));
EXPECT_REPORT(driver, (KC_LSFT));
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
layer_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// All mods are released.
EXPECT_EQ(get_mods() | get_speculative_mods(), 0);
}
// Test with layer tap and speculative mod tap keys on the same layer, trying
// a nested press with the MT first:
// "MT down, LT down, (wait out tapping term), LT up, MT up."
TEST_F(RetroShiftPermissiveHold, mt_lt_same_layer_nested_press) {
TestDriver driver;
InSequence s;
auto layer_tap_key = KeymapKey(0, 0, 0, LT(1, KC_A));
auto mod_tap_key = KeymapKey(0, 1, 0, LCTL_T(KC_B));
auto regular_key = KeymapKey(1, 1, 0, KC_C);
set_keymap({layer_tap_key, mod_tap_key, regular_key});
EXPECT_REPORT(driver, (KC_LCTL));
mod_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_NO_REPORT(driver);
layer_tap_key.press();
idle_for(TAPPING_TERM + 1);
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_LCTL, KC_LSFT, KC_A));
EXPECT_REPORT(driver, (KC_LCTL, KC_LSFT));
EXPECT_REPORT(driver, (KC_LCTL));
EXPECT_EMPTY_REPORT(driver);
layer_tap_key.release();
run_one_scan_loop();
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// All mods are released.
EXPECT_EQ(get_mods() | get_speculative_mods(), 0);
}
// Test with a speculative mod tap key reached by a layer tap key, rolling from
// LT to MT key:
// "LT down, MT down, (wait out tapping term), LT up, MT up."
TEST_F(RetroShiftPermissiveHold, lt_mt_different_layer_roll) {
TestDriver driver;
InSequence s;
auto layer_tap_key = KeymapKey(0, 0, 0, LT(1, KC_A));
auto regular_key = KeymapKey(0, 1, 0, KC_B);
auto placeholder_key = KeymapKey(1, 0, 0, KC_NO);
auto mod_tap_key = KeymapKey(1, 1, 0, LCTL_T(KC_C));
set_keymap({layer_tap_key, regular_key, placeholder_key, mod_tap_key});
// Press layer tap key.
EXPECT_NO_REPORT(driver);
layer_tap_key.press();
run_one_scan_loop();
// Press mod tap key.
mod_tap_key.press();
idle_for(TAPPING_TERM);
VERIFY_AND_CLEAR(driver);
// Release keys.
EXPECT_REPORT(driver, (KC_A));
EXPECT_EMPTY_REPORT(driver);
layer_tap_key.release();
idle_for(TAPPING_TERM);
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_LSFT, KC_B));
EXPECT_REPORT(driver, (KC_LSFT));
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// All mods are released.
EXPECT_EQ(get_mods() | get_speculative_mods(), 0);
}
// Test with a speculative mod tap key reached by a layer tap key, slowly
// rolling from LT to MT key:
// "LT down, (wait), MT down, (wait), LT up, MT up."
TEST_F(RetroShiftPermissiveHold, lt_mt_different_layer_slow_roll) {
TestDriver driver;
InSequence s;
auto layer_tap_key = KeymapKey(0, 0, 0, LT(1, KC_A));
auto regular_key = KeymapKey(0, 1, 0, KC_B);
auto placeholder_key = KeymapKey(1, 0, 0, KC_NO);
auto mod_tap_key = KeymapKey(1, 1, 0, LCTL_T(KC_C));
set_keymap({layer_tap_key, regular_key, placeholder_key, mod_tap_key});
EXPECT_NO_REPORT(driver);
layer_tap_key.press();
idle_for(TAPPING_TERM + 1);
mod_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_LSFT, KC_A));
EXPECT_REPORT(driver, (KC_LSFT));
EXPECT_EMPTY_REPORT(driver);
EXPECT_REPORT(driver, (KC_B));
EXPECT_EMPTY_REPORT(driver);
layer_tap_key.release();
run_one_scan_loop();
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// All mods are released.
EXPECT_EQ(get_mods() | get_speculative_mods(), 0);
}
// Test with a speculative mod tap key reached by a layer tap key, try a nested
// press:
// "LT down, MT down, (wait out tapping term), MT up, LT up."
TEST_F(RetroShiftPermissiveHold, lt_mt_different_layer_nested_press) {
TestDriver driver;
InSequence s;
auto layer_tap_key = KeymapKey(0, 0, 0, LT(1, KC_A));
auto regular_key = KeymapKey(0, 1, 0, KC_B);
auto placeholder_key = KeymapKey(1, 0, 0, KC_NO);
auto mod_tap_key = KeymapKey(1, 1, 0, LCTL_T(KC_C));
set_keymap({layer_tap_key, regular_key, placeholder_key, mod_tap_key});
EXPECT_NO_REPORT(driver);
layer_tap_key.press();
run_one_scan_loop();
mod_tap_key.press();
idle_for(TAPPING_TERM);
VERIFY_AND_CLEAR(driver);
// Release keys.
EXPECT_REPORT(driver, (KC_LSFT, KC_C));
EXPECT_REPORT(driver, (KC_LSFT));
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
layer_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// All mods are released.
EXPECT_EQ(get_mods() | get_speculative_mods(), 0);
}
// Test with a speculative mod tap key reached by a layer tap key, try a slow
// nested press:
// "LT down, (wait), MT down, MT up, LT up."
TEST_F(RetroShiftPermissiveHold, lt_mt_different_layer_slow_nested_press) {
TestDriver driver;
InSequence s;
auto layer_tap_key = KeymapKey(0, 0, 0, LT(1, KC_A));
auto regular_key = KeymapKey(0, 1, 0, KC_B);
auto placeholder_key = KeymapKey(1, 0, 0, KC_NO);
auto mod_tap_key = KeymapKey(1, 1, 0, LCTL_T(KC_C));
set_keymap({layer_tap_key, regular_key, placeholder_key, mod_tap_key});
EXPECT_NO_REPORT(driver);
layer_tap_key.press();
idle_for(TAPPING_TERM + 1);
mod_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_C));
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
layer_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// All mods are released.
EXPECT_EQ(get_mods() | get_speculative_mods(), 0);
}
@@ -0,0 +1,24 @@
/* Copyright 2022 Vladislav Kucheriavykh
* Copyright 2025 Google LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "test_common.h"
#define SPECULATIVE_HOLD
#define RETRO_TAPPING
#define DUMMY_MOD_NEUTRALIZER_KEYCODE KC_F24
@@ -0,0 +1,15 @@
# Copyright 2022 Vladislav Kucheriavykh
# Copyright 2025 Google LLC
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
@@ -0,0 +1,629 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "keyboard_report_util.hpp"
#include "keycode.h"
#include "test_common.hpp"
#include "action_tapping.h"
#include "test_fixture.hpp"
#include "test_keymap_key.hpp"
using testing::_;
using testing::InSequence;
extern "C" bool get_speculative_hold(uint16_t keycode, keyrecord_t *record) {
return true;
}
class SpeculativeHoldRetroTappingTest : public TestFixture {};
TEST_F(SpeculativeHoldRetroTappingTest, roll_regular_to_lgui_mod) {
TestDriver driver;
InSequence s;
auto mod_tap_key = KeymapKey(0, 1, 0, LGUI_T(KC_P));
auto regular_key = KeymapKey(0, 2, 0, KC_B);
set_keymap({mod_tap_key, regular_key});
EXPECT_REPORT(driver, (KC_B));
regular_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_B, KC_LGUI));
mod_tap_key.press();
idle_for(TAPPING_TERM + 1);
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_LGUI));
regular_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Neutralizer invoked by Speculative Hold.
EXPECT_REPORT(driver, (KC_LGUI, DUMMY_MOD_NEUTRALIZER_KEYCODE));
EXPECT_REPORT(driver, (KC_LGUI));
EXPECT_EMPTY_REPORT(driver);
EXPECT_REPORT(driver, (KC_P));
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
TEST_F(SpeculativeHoldRetroTappingTest, regular_to_mod_under_tap_term) {
TestDriver driver;
InSequence s;
auto mod_tap_key = KeymapKey(0, 1, 0, LSFT_T(KC_A));
auto regular_key = KeymapKey(0, 2, 0, KC_B);
set_keymap({mod_tap_key, regular_key});
EXPECT_REPORT(driver, (KC_B));
regular_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_B, KC_LSFT));
mod_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_LSFT));
regular_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_EMPTY_REPORT(driver);
EXPECT_REPORT(driver, (KC_A));
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
TEST_F(SpeculativeHoldRetroTappingTest, mod_under_tap_term_to_regular) {
TestDriver driver;
InSequence s;
auto mod_tap_key = KeymapKey(0, 1, 0, LGUI_T(KC_P));
auto regular_key = KeymapKey(0, 2, 0, KC_B);
set_keymap({mod_tap_key, regular_key});
EXPECT_REPORT(driver, (KC_LGUI));
mod_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_NO_REPORT(driver);
regular_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Neutralizer invoked by Speculative Hold.
EXPECT_REPORT(driver, (KC_LGUI, DUMMY_MOD_NEUTRALIZER_KEYCODE));
EXPECT_REPORT(driver, (KC_LGUI));
EXPECT_EMPTY_REPORT(driver);
EXPECT_REPORT(driver, (KC_P));
EXPECT_REPORT(driver, (KC_B, KC_P));
EXPECT_REPORT(driver, (KC_B));
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_EMPTY_REPORT(driver);
regular_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
TEST_F(SpeculativeHoldRetroTappingTest, mod_over_tap_term_to_regular) {
TestDriver driver;
InSequence s;
auto mod_tap_key = KeymapKey(0, 1, 0, LSFT_T(KC_A));
auto regular_key = KeymapKey(0, 2, 0, KC_B);
set_keymap({mod_tap_key, regular_key});
EXPECT_REPORT(driver, (KC_LSFT));
mod_tap_key.press();
idle_for(TAPPING_TERM);
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_LSFT, KC_B));
regular_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_B));
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_EMPTY_REPORT(driver);
regular_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
TEST_F(SpeculativeHoldRetroTappingTest, mod_under_tap_term_to_mod_under_tap_term) {
TestDriver driver;
InSequence s;
auto mod_tap_lgui = KeymapKey(0, 1, 0, LGUI_T(KC_P));
auto mod_tap_lsft = KeymapKey(0, 2, 0, LSFT_T(KC_A));
set_keymap({mod_tap_lgui, mod_tap_lsft});
EXPECT_REPORT(driver, (KC_LSFT));
mod_tap_lsft.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_LSFT, KC_LGUI));
mod_tap_lgui.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_EMPTY_REPORT(driver);
EXPECT_REPORT(driver, (KC_A));
EXPECT_EMPTY_REPORT(driver);
mod_tap_lsft.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_P));
EXPECT_EMPTY_REPORT(driver);
mod_tap_lgui.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
TEST_F(SpeculativeHoldRetroTappingTest, mod_over_tap_term_to_mod_under_tap_term) {
TestDriver driver;
InSequence s;
auto mod_tap_lgui = KeymapKey(0, 1, 0, LGUI_T(KC_P));
auto mod_tap_lsft = KeymapKey(0, 2, 0, LSFT_T(KC_A));
set_keymap({mod_tap_lgui, mod_tap_lsft});
EXPECT_REPORT(driver, (KC_LSFT));
mod_tap_lsft.press();
idle_for(TAPPING_TERM);
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_LSFT, KC_LGUI));
mod_tap_lgui.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_NO_REPORT(driver);
mod_tap_lsft.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_LSFT));
EXPECT_REPORT(driver, (KC_LSFT, KC_P));
EXPECT_REPORT(driver, (KC_P));
EXPECT_EMPTY_REPORT(driver);
mod_tap_lgui.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
TEST_F(SpeculativeHoldRetroTappingTest, mod_under_tap_term_to_mod_over_tap_term) {
TestDriver driver;
InSequence s;
auto mod_tap_lgui = KeymapKey(0, 1, 0, LGUI_T(KC_P));
auto mod_tap_lsft = KeymapKey(0, 2, 0, LSFT_T(KC_A));
set_keymap({mod_tap_lgui, mod_tap_lsft});
EXPECT_REPORT(driver, (KC_LSFT));
mod_tap_lsft.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_LSFT, KC_LGUI));
mod_tap_lgui.press();
idle_for(TAPPING_TERM);
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_LGUI));
mod_tap_lsft.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Neutralizer invoked by Retro Tapping.
EXPECT_REPORT(driver, (KC_LGUI, DUMMY_MOD_NEUTRALIZER_KEYCODE));
EXPECT_REPORT(driver, (KC_LGUI));
EXPECT_EMPTY_REPORT(driver);
EXPECT_REPORT(driver, (KC_LSFT));
EXPECT_REPORT(driver, (KC_P, KC_LSFT));
EXPECT_REPORT(driver, (KC_LSFT));
EXPECT_EMPTY_REPORT(driver);
mod_tap_lgui.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
TEST_F(SpeculativeHoldRetroTappingTest, mod_under_tap_term_to_mod_over_tap_term_offset) {
TestDriver driver;
InSequence s;
auto mod_tap_lgui = KeymapKey(0, 1, 0, LGUI_T(KC_P));
auto mod_tap_lsft = KeymapKey(0, 2, 0, LSFT_T(KC_A));
set_keymap({mod_tap_lgui, mod_tap_lsft});
EXPECT_REPORT(driver, (KC_LSFT));
mod_tap_lsft.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_LSFT, KC_LGUI));
mod_tap_lgui.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_EMPTY_REPORT(driver);
EXPECT_REPORT(driver, (KC_A));
EXPECT_EMPTY_REPORT(driver);
mod_tap_lsft.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_LGUI));
// Neutralizer invoked by Retro Tapping.
EXPECT_REPORT(driver, (KC_LGUI, DUMMY_MOD_NEUTRALIZER_KEYCODE));
EXPECT_REPORT(driver, (KC_LGUI));
EXPECT_EMPTY_REPORT(driver);
EXPECT_REPORT(driver, (KC_P));
EXPECT_EMPTY_REPORT(driver);
idle_for(TAPPING_TERM);
mod_tap_lgui.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
TEST_F(SpeculativeHoldRetroTappingTest, mod_over_tap_term_to_mod_over_tap_term) {
TestDriver driver;
InSequence s;
auto mod_tap_lgui = KeymapKey(0, 1, 0, LGUI_T(KC_P));
auto mod_tap_lsft = KeymapKey(0, 2, 0, LSFT_T(KC_A));
set_keymap({mod_tap_lgui, mod_tap_lsft});
EXPECT_REPORT(driver, (KC_LSFT));
mod_tap_lsft.press();
idle_for(TAPPING_TERM);
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_LSFT, KC_LGUI));
mod_tap_lgui.press();
idle_for(TAPPING_TERM);
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_LGUI));
mod_tap_lsft.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Neutralizer invoked by Retro Tapping.
EXPECT_REPORT(driver, (KC_LGUI, DUMMY_MOD_NEUTRALIZER_KEYCODE));
EXPECT_REPORT(driver, (KC_LGUI));
EXPECT_EMPTY_REPORT(driver);
EXPECT_REPORT(driver, (KC_LSFT));
EXPECT_REPORT(driver, (KC_P, KC_LSFT));
EXPECT_REPORT(driver, (KC_LSFT));
EXPECT_EMPTY_REPORT(driver);
mod_tap_lgui.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
TEST_F(SpeculativeHoldRetroTappingTest, mod_to_mod_to_mod) {
TestDriver driver;
InSequence s;
auto mod_tap_lalt = KeymapKey(0, 1, 0, LALT_T(KC_R));
auto mod_tap_lsft = KeymapKey(0, 2, 0, SFT_T(KC_A));
auto mod_tap_lctl = KeymapKey(0, 3, 0, LCTL_T(KC_C));
set_keymap({mod_tap_lalt, mod_tap_lsft, mod_tap_lctl});
EXPECT_REPORT(driver, (KC_LALT));
mod_tap_lalt.press();
idle_for(TAPPING_TERM);
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_LSFT, KC_LALT));
mod_tap_lsft.press();
idle_for(TAPPING_TERM);
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_LSFT));
mod_tap_lalt.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_LCTL, KC_LSFT));
mod_tap_lctl.press();
idle_for(TAPPING_TERM);
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_LCTL));
mod_tap_lsft.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_EMPTY_REPORT(driver);
EXPECT_REPORT(driver, (KC_LSFT));
EXPECT_REPORT(driver, (KC_C, KC_LSFT));
EXPECT_REPORT(driver, (KC_LSFT));
EXPECT_EMPTY_REPORT(driver);
mod_tap_lctl.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
}
// Test with layer tap and speculative mod tap keys on the same layer, rolling
// from LT to MT key:
// "LT down, MT down, (wait out tapping term), LT up, MT up."
TEST_F(SpeculativeHoldRetroTappingTest, lt_mt_same_layer_roll) {
TestDriver driver;
InSequence s;
auto layer_tap_key = KeymapKey(0, 0, 0, LT(1, KC_A));
auto mod_tap_key = KeymapKey(0, 1, 0, LCTL_T(KC_B));
auto regular_key = KeymapKey(1, 1, 0, KC_C);
set_keymap({layer_tap_key, mod_tap_key, regular_key});
// Press layer tap key.
EXPECT_NO_REPORT(driver);
layer_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Press mod-tap key, after flow tap term but within tapping term. The
// speculative mod activates.
EXPECT_REPORT(driver, (KC_LCTL));
mod_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// Wait for the layer tap key to settle.
EXPECT_EMPTY_REPORT(driver);
EXPECT_REPORT(driver, (KC_C));
idle_for(TAPPING_TERM);
VERIFY_AND_CLEAR(driver);
// Release keys.
EXPECT_EMPTY_REPORT(driver);
layer_tap_key.release();
run_one_scan_loop();
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// All mods are released.
EXPECT_EQ(get_mods() | get_speculative_mods(), 0);
}
// Test with layer tap and speculative mod tap keys on the same layer, trying a
// nested press:
// "LT down, MT down, (wait out tapping term), MT up, LT up."
TEST_F(SpeculativeHoldRetroTappingTest, lt_mt_same_layer_nested_press) {
TestDriver driver;
InSequence s;
auto layer_tap_key = KeymapKey(0, 0, 0, LT(1, KC_A));
auto mod_tap_key = KeymapKey(0, 1, 0, LCTL_T(KC_B));
auto regular_key = KeymapKey(1, 1, 0, KC_C);
set_keymap({layer_tap_key, mod_tap_key, regular_key});
EXPECT_NO_REPORT(driver);
layer_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_LCTL));
mod_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_EMPTY_REPORT(driver);
EXPECT_REPORT(driver, (KC_C));
idle_for(TAPPING_TERM);
VERIFY_AND_CLEAR(driver);
// Release keys: MT first, LT second.
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
layer_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// All mods are released.
EXPECT_EQ(get_mods() | get_speculative_mods(), 0);
}
// Test with layer tap and speculative mod tap keys on the same layer, trying a
// nested press with the MT first:
// "MT down, LT down, (wait out tapping term), LT up, MT up."
TEST_F(SpeculativeHoldRetroTappingTest, mt_lt_same_layer_nested_press) {
TestDriver driver;
InSequence s;
auto layer_tap_key = KeymapKey(0, 0, 0, LT(1, KC_A));
auto mod_tap_key = KeymapKey(0, 1, 0, LCTL_T(KC_B));
auto regular_key = KeymapKey(1, 1, 0, KC_C);
set_keymap({layer_tap_key, mod_tap_key, regular_key});
EXPECT_REPORT(driver, (KC_LCTL));
mod_tap_key.press();
run_one_scan_loop();
EXPECT_NO_REPORT(driver);
layer_tap_key.press();
idle_for(TAPPING_TERM + 1);
VERIFY_AND_CLEAR(driver);
EXPECT_REPORT(driver, (KC_LCTL, KC_A));
EXPECT_REPORT(driver, (KC_LCTL));
EXPECT_EMPTY_REPORT(driver);
layer_tap_key.release();
run_one_scan_loop();
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// All mods are released.
EXPECT_EQ(get_mods() | get_speculative_mods(), 0);
}
// Test with a speculative mod tap key reached by a layer tap key, rolling from
// LT to MT key:
// "LT down, MT down, (wait out tapping term), LT up, MT up."
TEST_F(SpeculativeHoldRetroTappingTest, lt_mt_different_layer_roll) {
TestDriver driver;
InSequence s;
auto layer_tap_key = KeymapKey(0, 0, 0, LT(1, KC_A));
auto regular_key = KeymapKey(0, 1, 0, KC_B);
auto placeholder_key = KeymapKey(1, 0, 0, KC_NO);
auto mod_tap_key = KeymapKey(1, 1, 0, LCTL_T(KC_C));
set_keymap({layer_tap_key, regular_key, placeholder_key, mod_tap_key});
// Press layer tap key.
EXPECT_NO_REPORT(driver);
layer_tap_key.press();
run_one_scan_loop();
// Press mod tap key.
mod_tap_key.press();
idle_for(TAPPING_TERM);
VERIFY_AND_CLEAR(driver);
// Release keys.
EXPECT_REPORT(driver, (KC_LCTL));
layer_tap_key.release();
idle_for(TAPPING_TERM);
VERIFY_AND_CLEAR(driver);
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// All mods are released.
EXPECT_EQ(get_mods() | get_speculative_mods(), 0);
}
// Test with a speculative mod tap key reached by a layer tap key, slowly
// rolling from LT to MT key:
// "LT down, (wait), MT down, (wait), LT up, MT up."
TEST_F(SpeculativeHoldRetroTappingTest, lt_mt_different_layer_slow_roll) {
TestDriver driver;
InSequence s;
auto layer_tap_key = KeymapKey(0, 0, 0, LT(1, KC_A));
auto regular_key = KeymapKey(0, 1, 0, KC_B);
auto placeholder_key = KeymapKey(1, 0, 0, KC_NO);
auto mod_tap_key = KeymapKey(1, 1, 0, LCTL_T(KC_C));
set_keymap({layer_tap_key, regular_key, placeholder_key, mod_tap_key});
EXPECT_REPORT(driver, (KC_LCTL));
layer_tap_key.press();
idle_for(TAPPING_TERM + 1);
mod_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_EMPTY_REPORT(driver);
EXPECT_REPORT(driver, (KC_B));
EXPECT_EMPTY_REPORT(driver);
layer_tap_key.release();
run_one_scan_loop();
mod_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// All mods are released.
EXPECT_EQ(get_mods() | get_speculative_mods(), 0);
}
// Test with a speculative mod tap key reached by a layer tap key, trying a
// nested press:
// "LT down, MT down, (wait out tapping term), MT up, LT up."
TEST_F(SpeculativeHoldRetroTappingTest, lt_mt_different_layer_nested_press) {
TestDriver driver;
InSequence s;
auto layer_tap_key = KeymapKey(0, 0, 0, LT(1, KC_A));
auto regular_key = KeymapKey(0, 1, 0, KC_B);
auto placeholder_key = KeymapKey(1, 0, 0, KC_NO);
auto mod_tap_key = KeymapKey(1, 1, 0, LCTL_T(KC_C));
set_keymap({layer_tap_key, regular_key, placeholder_key, mod_tap_key});
EXPECT_NO_REPORT(driver);
layer_tap_key.press();
run_one_scan_loop();
mod_tap_key.press();
idle_for(TAPPING_TERM);
VERIFY_AND_CLEAR(driver);
// Release keys.
EXPECT_REPORT(driver, (KC_LCTL));
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
layer_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// All mods are released.
EXPECT_EQ(get_mods() | get_speculative_mods(), 0);
}
// Test with a speculative mod tap key reached by a layer tap key, slowly making
// a nested press from LT to MT key:
// "LT down, (wait), MT down, MT up, LT up."
TEST_F(SpeculativeHoldRetroTappingTest, lt_mt_different_layer_slow_nested_press) {
TestDriver driver;
InSequence s;
auto layer_tap_key = KeymapKey(0, 0, 0, LT(1, KC_A));
auto regular_key = KeymapKey(0, 1, 0, KC_B);
auto placeholder_key = KeymapKey(1, 0, 0, KC_NO);
auto mod_tap_key = KeymapKey(1, 1, 0, LCTL_T(KC_C));
set_keymap({layer_tap_key, regular_key, placeholder_key, mod_tap_key});
EXPECT_REPORT(driver, (KC_LCTL));
layer_tap_key.press();
idle_for(TAPPING_TERM + 1);
mod_tap_key.press();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
EXPECT_EMPTY_REPORT(driver);
EXPECT_REPORT(driver, (KC_C));
EXPECT_EMPTY_REPORT(driver);
mod_tap_key.release();
run_one_scan_loop();
layer_tap_key.release();
run_one_scan_loop();
VERIFY_AND_CLEAR(driver);
// All mods are released.
EXPECT_EQ(get_mods() | get_speculative_mods(), 0);
}