Compare commits
63 Commits
ed0267f4c6
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| fe35602f65 | |||
|
7a2b47a265
|
|||
| 3b497e6eba | |||
| 4a1533b189 | |||
| e8433ba5d9 | |||
|
73de490e53
|
|||
|
363b43cafd
|
|||
|
848972b7be
|
|||
|
20a605ce1f
|
|||
|
0fa528b3dd
|
|||
| 66c73eef89 | |||
|
c498d5025c
|
|||
| ff45e8e0d6 | |||
|
1b0e83b1d6
|
|||
| ad41de2f54 | |||
|
b4a76db6f3
|
|||
| 2da8bab7a5 | |||
|
c93249f1b2
|
|||
| 1d60306b66 | |||
|
0cf22be07c
|
|||
| eedf234ab3 | |||
| 5764e38e73 | |||
| 03765e0faa | |||
| f9972b2fee | |||
| bc1556547d | |||
| 287a0d1cbc | |||
| eaa284d557 | |||
| 1c2886c052 | |||
|
622a20bb0d
|
|||
|
49af06fcac
|
|||
|
52e75ecc6a
|
|||
|
5f9121c0d2
|
|||
|
6d6eee5571
|
|||
|
df671b656b
|
|||
|
6fd5179c55
|
|||
|
9b2cd0cb32
|
|||
|
d4bd903423
|
|||
|
c0d868dca3
|
|||
|
d8dccd67a6
|
|||
|
360e642fcf
|
|||
|
4cfa85ee8e
|
|||
|
41fe2a11bd
|
|||
| d52d55e442 | |||
|
aad85d9930
|
|||
|
0c28ba889f
|
|||
| 846d07966f | |||
| 5c0fb31e41 | |||
| 14747159fa | |||
|
d2a67b2bb3
|
|||
|
0210caf90e
|
|||
|
6045cfa866
|
|||
|
b77eea4192
|
|||
|
bab766887a
|
|||
|
a2ef1712ab
|
|||
|
0865d7ed26
|
|||
|
3d283cdf48
|
|||
|
aeae6d485d
|
|||
|
926628bc4f
|
|||
|
45276ac042
|
|||
|
423e544097
|
|||
|
86cad58f34
|
|||
|
e897c73ff4
|
|||
|
949d34941a
|
@@ -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 -->
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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).
|
||||
@@ -1,9 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
labels: CI
|
||||
reviewers:
|
||||
- "qmk/collaborators"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
@@ -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/**/*
|
||||
@@ -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'
|
||||
@@ -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/"
|
||||
@@ -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'
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
@@ -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)"
|
||||
@@ -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'
|
||||
@@ -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'
|
||||
@@ -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
|
||||
@@ -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)"
|
||||
@@ -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'
|
||||
@@ -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]
|
||||
@@ -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
|
||||
+5
-1
@@ -112,7 +112,11 @@ compile_commands.json
|
||||
via*.json
|
||||
/keyboards/**/keymaps/vial/*
|
||||
|
||||
# AI Sessions
|
||||
.gemini_session
|
||||
.claude_session
|
||||
|
||||
# Keep firmware file
|
||||
!keyboards/keychron/*/firmware/*.bin
|
||||
/.claude_session
|
||||
/.remember/tmp/save-session.pid
|
||||
/.direnv/CACHEDIR.TAG
|
||||
|
||||
@@ -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 24–31; 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.
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -1,325 +0,0 @@
|
||||
# Caps Lock Mod Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Replace the plain Caps Lock key with a smart `CAPS_MOD` key: tap=ESC, hold=Ctrl, Shift+tap=CapsLock, Alt+tap=CapsWord, GUI+tap=Autocorrect toggle, with RGB indicators on the Caps Lock key.
|
||||
|
||||
**Architecture:** Custom keycode `CAPS_MOD` with a timer-based tap/hold split. `process_record_user` starts the timer on press and dispatches actions on release. `matrix_scan_user` promotes a held key to Ctrl once `TAPPING_TERM` elapses. RGB indicators for CapsWord (green), Autocorrect (purple), and host CapsLock (white) are added to the existing `rgb_matrix_indicators_advanced_user` function.
|
||||
|
||||
**Tech Stack:** QMK firmware (C), RGB Matrix, Caps Word, Autocorrect — all built in to QMK.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-04-07-capslock-mod-design.md`
|
||||
|
||||
**Build command** (always activate venv first):
|
||||
```bash
|
||||
source .venv/bin/activate && qmk compile -kb keychron/q5_max/ansi_encoder -km via
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Map
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `keyboards/keychron/q5_max/ansi_encoder/keymaps/via/rules.mk` | Add `CAPS_WORD_ENABLE` and `AUTOCORRECT_ENABLE` |
|
||||
| `keyboards/keychron/q5_max/ansi_encoder/keymaps/via/config.h` | No change needed (`AUTOCORRECT_MIN_LENGTH` already defined in `autocorrect_data.h`) |
|
||||
| `keyboards/keychron/q5_max/ansi_encoder/keymaps/via/autocorrect_data.h` | Commit to git (was untracked; pre-generated dictionary with `AUTOCORRECT_MIN_LENGTH 4` already inside) |
|
||||
| `keyboards/keychron/q5_max/ansi_encoder/keymaps/via/keymap.c` | Add `CAPS_MOD` to enum; add state vars; replace all `KC_CAPS`; add press/release + scan logic; add LED 55 indicators |
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Enable Caps Word and Autocorrect features
|
||||
|
||||
**Files:**
|
||||
- Modify: `keyboards/keychron/q5_max/ansi_encoder/keymaps/via/rules.mk`
|
||||
- Commit (untracked): `keyboards/keychron/q5_max/ansi_encoder/keymaps/via/autocorrect_data.h`
|
||||
- Note: `config.h` needs no change — `AUTOCORRECT_MIN_LENGTH 4` is already defined inside `autocorrect_data.h`
|
||||
|
||||
- [ ] **Step 1: Add feature flags to rules.mk**
|
||||
|
||||
Replace the entire file content with:
|
||||
|
||||
```makefile
|
||||
VIA_ENABLE = yes
|
||||
TAP_DANCE_ENABLE = yes
|
||||
UNICODE_ENABLE = yes
|
||||
COMBO_ENABLE = yes
|
||||
CAPS_WORD_ENABLE = yes
|
||||
AUTOCORRECT_ENABLE = yes
|
||||
SRC += chord_unicode.c
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify autocorrect_data.h is present and has AUTOCORRECT_MIN_LENGTH**
|
||||
|
||||
Check that `keyboards/keychron/q5_max/ansi_encoder/keymaps/via/autocorrect_data.h` exists and contains `#define AUTOCORRECT_MIN_LENGTH`. Do NOT add it to `config.h` — it is already defined in the pre-generated file.
|
||||
|
||||
- [ ] **Step 3: Compile to verify no breakage**
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate && qmk compile -kb keychron/q5_max/ansi_encoder -km via
|
||||
```
|
||||
|
||||
Expected: build succeeds, `.bin` file produced. No errors.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add keyboards/keychron/q5_max/ansi_encoder/keymaps/via/rules.mk \
|
||||
keyboards/keychron/q5_max/ansi_encoder/keymaps/via/config.h
|
||||
git commit -m "feat(q5_max): enable CAPS_WORD and AUTOCORRECT features"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Add CAPS_MOD keycode, state variables, and replace KC_CAPS
|
||||
|
||||
**Files:**
|
||||
- Modify: `keyboards/keychron/q5_max/ansi_encoder/keymaps/via/keymap.c`
|
||||
|
||||
- [ ] **Step 1: Add CAPS_MOD to the custom keycodes enum**
|
||||
|
||||
Find this block (lines ~27–37):
|
||||
|
||||
```c
|
||||
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
|
||||
};
|
||||
```
|
||||
|
||||
Replace with:
|
||||
|
||||
```c
|
||||
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
|
||||
CAPS_MOD, // Tap=ESC, hold=Ctrl, Shift=CapsLock, Alt=CapsWord, GUI=Autocorrect
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add CAPS_MOD state variables**
|
||||
|
||||
Find this block (lines ~44–46):
|
||||
|
||||
```c
|
||||
// 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;
|
||||
```
|
||||
|
||||
Replace with:
|
||||
|
||||
```c
|
||||
// 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;
|
||||
|
||||
// CAPS_MOD state: tap=ESC, hold=Ctrl, Shift+tap=CapsLock, Alt+tap=CapsWord, GUI+tap=Autocorrect
|
||||
static bool caps_mod_held = false;
|
||||
static bool caps_mod_ctrl_registered = false;
|
||||
static uint16_t caps_mod_timer = 0;
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Replace KC_CAPS with CAPS_MOD in all five layers**
|
||||
|
||||
In the keymap arrays, find every occurrence of `KC_CAPS` and replace with `CAPS_MOD`. There are five instances — one at the start of row 3 in each of: BASE, FN1, FN2, FN3, FN4. KEEB_CTL already uses `_______` for that position and stays unchanged.
|
||||
|
||||
Each row looks like:
|
||||
```c
|
||||
KC_CAPS, KC_A, KC_S, ...
|
||||
```
|
||||
Change to:
|
||||
```c
|
||||
CAPS_MOD, KC_A, KC_S, ...
|
||||
```
|
||||
|
||||
Do this for all five layers.
|
||||
|
||||
- [ ] **Step 4: Compile to verify**
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate && qmk compile -kb keychron/q5_max/ansi_encoder -km via
|
||||
```
|
||||
|
||||
Expected: build succeeds. `CAPS_MOD` is defined but not yet handled — QMK will pass it through to `process_record_user` which returns `true` by default, so no errors.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add keyboards/keychron/q5_max/ansi_encoder/keymaps/via/keymap.c
|
||||
git commit -m "feat(q5_max): add CAPS_MOD keycode and replace KC_CAPS in all layers"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Implement tap/hold logic in process_record_user and matrix_scan_user
|
||||
|
||||
**Files:**
|
||||
- Modify: `keyboards/keychron/q5_max/ansi_encoder/keymaps/via/keymap.c`
|
||||
|
||||
- [ ] **Step 1: Add CAPS_MOD case to the switch in process_record_user**
|
||||
|
||||
Find the `switch (keycode)` block in `process_record_user`. It currently starts with `case LCK_FN1:`. Add the `CAPS_MOD` case **before** the `LCK_FN1` case:
|
||||
|
||||
```c
|
||||
case CAPS_MOD:
|
||||
if (record->event.pressed) {
|
||||
caps_mod_held = true;
|
||||
caps_mod_timer = timer_read();
|
||||
} else {
|
||||
if (caps_mod_ctrl_registered) {
|
||||
unregister_code(KC_LCTL);
|
||||
caps_mod_ctrl_registered = false;
|
||||
} else {
|
||||
uint8_t mods = get_mods();
|
||||
if (mods & MOD_MASK_GUI) {
|
||||
autocorrect_toggle();
|
||||
} else if (mods & MOD_MASK_ALT) {
|
||||
caps_word_toggle();
|
||||
} else if (mods & MOD_MASK_SHIFT) {
|
||||
tap_code(KC_CAPS);
|
||||
} else {
|
||||
tap_code(KC_ESC);
|
||||
}
|
||||
}
|
||||
caps_mod_held = false;
|
||||
}
|
||||
return false;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add Ctrl promotion to matrix_scan_user**
|
||||
|
||||
Find `matrix_scan_user`:
|
||||
|
||||
```c
|
||||
void matrix_scan_user(void) {
|
||||
if (alt_tab_active && timer_elapsed(alt_tab_timer) > ALT_TAB_TIMEOUT) {
|
||||
unregister_code(KC_LALT);
|
||||
alt_tab_active = false;
|
||||
}
|
||||
chord_scan();
|
||||
}
|
||||
```
|
||||
|
||||
Replace with:
|
||||
|
||||
```c
|
||||
void matrix_scan_user(void) {
|
||||
if (caps_mod_held && !caps_mod_ctrl_registered
|
||||
&& timer_elapsed(caps_mod_timer) > TAPPING_TERM) {
|
||||
caps_mod_ctrl_registered = true;
|
||||
register_code(KC_LCTL);
|
||||
}
|
||||
if (alt_tab_active && timer_elapsed(alt_tab_timer) > ALT_TAB_TIMEOUT) {
|
||||
unregister_code(KC_LALT);
|
||||
alt_tab_active = false;
|
||||
}
|
||||
chord_scan();
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Compile to verify**
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate && qmk compile -kb keychron/q5_max/ansi_encoder -km via
|
||||
```
|
||||
|
||||
Expected: build succeeds, no errors or warnings about undefined functions. (`autocorrect_toggle`, `caps_word_toggle`, `get_mods`, `MOD_MASK_*` are all QMK builtins.)
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add keyboards/keychron/q5_max/ansi_encoder/keymaps/via/keymap.c
|
||||
git commit -m "feat(q5_max): implement CAPS_MOD tap/hold logic (ESC/Ctrl/CapsLock/CapsWord/Autocorrect)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Add RGB indicators for the Caps Lock key (LED 55)
|
||||
|
||||
**Files:**
|
||||
- Modify: `keyboards/keychron/q5_max/ansi_encoder/keymaps/via/keymap.c`
|
||||
|
||||
- [ ] **Step 1: Add LED 55 indicators to rgb_matrix_indicators_advanced_user**
|
||||
|
||||
Find `rgb_matrix_indicators_advanced_user`. It currently ends with:
|
||||
|
||||
```c
|
||||
default: // BASE — keep ESC dark
|
||||
RGB_MATRIX_INDICATOR_SET_COLOR(0, 0, 0, 0);
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
Replace that closing section with:
|
||||
|
||||
```c
|
||||
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 active
|
||||
} 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;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Compile to verify**
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate && qmk compile -kb keychron/q5_max/ansi_encoder -km via
|
||||
```
|
||||
|
||||
Expected: build succeeds. (`is_caps_word_on`, `autocorrect_is_enabled`, `host_keyboard_led_state` are all QMK builtins available when their features are enabled.)
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add keyboards/keychron/q5_max/ansi_encoder/keymaps/via/keymap.c
|
||||
git commit -m "feat(q5_max): add RGB indicators for CapsWord/Autocorrect/CapsLock on LED 55"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-Review Checklist
|
||||
|
||||
- [x] Tap=ESC → handled in `process_record_user` else branch, `tap_code(KC_ESC)`
|
||||
- [x] Hold=Ctrl → `matrix_scan_user` promotes after `TAPPING_TERM`, released on key-up
|
||||
- [x] Shift+tap=CapsLock → `MOD_MASK_SHIFT` branch, `tap_code(KC_CAPS)`
|
||||
- [x] Alt+tap=CapsWord → `MOD_MASK_ALT` branch, `caps_word_toggle()`
|
||||
- [x] GUI+tap=Autocorrect → `MOD_MASK_GUI` branch, `autocorrect_toggle()`
|
||||
- [x] Green LED for CapsWord → `is_caps_word_on()` branch
|
||||
- [x] Purple LED for Autocorrect → `autocorrect_is_enabled()` branch
|
||||
- [x] White LED for CapsLock → `host_keyboard_led_state().caps_lock` branch
|
||||
- [x] `CAPS_WORD_ENABLE = yes` in rules.mk
|
||||
- [x] `AUTOCORRECT_ENABLE = yes` in rules.mk
|
||||
- [x] `AUTOCORRECT_MIN_LENGTH 4` in config.h
|
||||
- [x] All 5 `KC_CAPS` instances replaced (BASE, FN1, FN2, FN3, FN4)
|
||||
- [x] KEEB_CTL left unchanged (`_______`)
|
||||
- [x] LED index 55 confirmed from `ansi_encoder.c` matrix map (row 3, col 0)
|
||||
@@ -1,130 +0,0 @@
|
||||
# Caps Lock Mod — Design Spec
|
||||
|
||||
**Date:** 2026-04-07
|
||||
**Keyboard:** Keychron Q5 Max (ANSI Encoder)
|
||||
**Keymap:** `keyboards/keychron/q5_max/ansi_encoder/keymaps/via/`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Replace the plain `KC_CAPS` key with a smart `CAPS_MOD` custom keycode that provides
|
||||
tap-vs-hold behavior and modifier-aware tap actions. Enable Caps Word and Autocorrect
|
||||
features with RGB indicators on the Caps Lock key (LED 55).
|
||||
|
||||
---
|
||||
|
||||
## Behavior
|
||||
|
||||
| Action | Result |
|
||||
|---|---|
|
||||
| Tap | Escape |
|
||||
| Hold (past tapping term) | Left Ctrl (held until release) |
|
||||
| Shift + Tap | Toggle normal Caps Lock |
|
||||
| Alt + Tap | Toggle Caps Word |
|
||||
| GUI + Tap | Toggle Autocorrect |
|
||||
|
||||
**Priority for modifier-aware taps:** GUI → Alt → Shift → default (ESC).
|
||||
Modifiers are checked via `get_mods()` at key release time (they remain held by the user).
|
||||
|
||||
---
|
||||
|
||||
## Implementation
|
||||
|
||||
### New custom keycode
|
||||
|
||||
```c
|
||||
CAPS_MOD // added to enum custom_keycodes, after existing entries
|
||||
```
|
||||
|
||||
Replaces every `KC_CAPS` in all layers of `keymap.c`.
|
||||
|
||||
### State variables
|
||||
|
||||
```c
|
||||
static bool caps_mod_held = false;
|
||||
static bool caps_mod_ctrl_registered = false;
|
||||
static uint16_t caps_mod_timer = 0;
|
||||
```
|
||||
|
||||
### `process_record_user` logic
|
||||
|
||||
**On press:**
|
||||
```c
|
||||
caps_mod_held = true;
|
||||
caps_mod_timer = timer_read();
|
||||
```
|
||||
|
||||
**On release:**
|
||||
```c
|
||||
if (caps_mod_ctrl_registered) {
|
||||
unregister_code(KC_LCTL);
|
||||
caps_mod_ctrl_registered = false;
|
||||
} else {
|
||||
uint8_t mods = get_mods();
|
||||
if (mods & MOD_MASK_GUI) {
|
||||
autocorrect_toggle();
|
||||
} else if (mods & MOD_MASK_ALT) {
|
||||
caps_word_toggle();
|
||||
} else if (mods & MOD_MASK_SHIFT) {
|
||||
tap_code(KC_CAPS);
|
||||
} else {
|
||||
tap_code(KC_ESC);
|
||||
}
|
||||
}
|
||||
caps_mod_held = false;
|
||||
```
|
||||
|
||||
### `matrix_scan_user` addition
|
||||
|
||||
```c
|
||||
if (caps_mod_held && !caps_mod_ctrl_registered
|
||||
&& timer_elapsed(caps_mod_timer) > TAPPING_TERM) {
|
||||
caps_mod_ctrl_registered = true;
|
||||
register_code(KC_LCTL);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## RGB Indicators (LED 55 — Caps Lock physical position)
|
||||
|
||||
Added inside `rgb_matrix_indicators_advanced_user`, checked in priority order:
|
||||
|
||||
| State | Color |
|
||||
|---|---|
|
||||
| Caps Word on | Green `(0, 200, 0)` |
|
||||
| Autocorrect on | Purple `(150, 0, 255)` |
|
||||
| Normal Caps Lock on | White `(255, 255, 255)` |
|
||||
| All off | Dark `(0, 0, 0)` |
|
||||
|
||||
Caps Word takes visual priority over host Caps Lock because both can technically be
|
||||
active simultaneously.
|
||||
|
||||
---
|
||||
|
||||
## Feature Enablement
|
||||
|
||||
### `rules.mk` additions
|
||||
```makefile
|
||||
CAPS_WORD_ENABLE = yes
|
||||
AUTOCORRECT_ENABLE = yes
|
||||
```
|
||||
|
||||
### `config.h` addition
|
||||
```c
|
||||
#define AUTOCORRECT_MIN_LENGTH 4
|
||||
```
|
||||
|
||||
`autocorrect_data.h` is already present in the keymap folder. QMK picks it up
|
||||
automatically when `AUTOCORRECT_ENABLE = yes` — no manual `#include` needed.
|
||||
|
||||
---
|
||||
|
||||
## Files Changed
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `keymap.c` | Add `CAPS_MOD` to enum; add state vars; add press/release logic in `process_record_user`; add scan logic in `matrix_scan_user`; add LED 55 indicators in `rgb_matrix_indicators_advanced_user`; replace all `KC_CAPS` with `CAPS_MOD` |
|
||||
| `rules.mk` | Add `CAPS_WORD_ENABLE = yes` and `AUTOCORRECT_ENABLE = yes` |
|
||||
| `config.h` | Add `AUTOCORRECT_MIN_LENGTH 4` |
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,6 +3,30 @@
|
||||
|
||||
#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 {
|
||||
@@ -34,7 +41,19 @@ enum custom_keycodes {
|
||||
LCK_FN4, // Lock/unlock FN4
|
||||
LCK_CTL, // Lock/unlock KEEB_CTL
|
||||
LCK_BASE, // Clear all locks and return to BASE
|
||||
CAPS_MOD, // Tap=ESC, hold=Ctrl, Shift=CapsLock, Alt=CapsWord, GUI=Autocorrect
|
||||
BSP_DEL, // Tap=Backspace, Shift+Tap=Delete
|
||||
};
|
||||
|
||||
#define CAPS_MOD MT(MOD_LCTL, KC_ESC)
|
||||
|
||||
// Declare layers early so the HID functions below can reference KEEB_CTL.
|
||||
enum layers {
|
||||
BASE,
|
||||
FN1,
|
||||
FN2,
|
||||
FN3,
|
||||
FN4,
|
||||
KEEB_CTL,
|
||||
};
|
||||
|
||||
// Alt-Tab cycling state
|
||||
@@ -46,19 +65,151 @@ static uint16_t alt_tab_timer = 0;
|
||||
// momentary (TT/MO) keys are released.
|
||||
static layer_state_t locked_layers = 0;
|
||||
|
||||
// CAPS_MOD state: tap=ESC, hold=Ctrl, Shift+tap=CapsLock, Alt+tap=CapsWord, GUI+tap=Autocorrect
|
||||
static bool caps_mod_held = false;
|
||||
static bool caps_mod_ctrl_registered = false;
|
||||
static uint16_t caps_mod_timer = 0;
|
||||
// ---------------------------------------------------------------------------
|
||||
// Raw HID state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enum layers {
|
||||
BASE,
|
||||
FN1,
|
||||
FN2,
|
||||
FN3,
|
||||
FN4,
|
||||
KEEB_CTL,
|
||||
};
|
||||
// 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] = {
|
||||
@@ -108,7 +259,7 @@ const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
|
||||
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)
|
||||
@@ -125,17 +276,29 @@ const uint16_t PROGMEM encoder_map[][NUM_ENCODERS][2] = {
|
||||
// clang-format on
|
||||
|
||||
// Combos -----------------------------------------------------------------
|
||||
// COMM + DOT + SLSH → TO(BASE): emergency fallback to base layer.
|
||||
// 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, TO(BASE)),
|
||||
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) {
|
||||
return state | locked_layers;
|
||||
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) {
|
||||
@@ -144,6 +307,23 @@ void keyboard_post_init_user(void) {
|
||||
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;
|
||||
@@ -166,28 +346,28 @@ bool process_record_user(uint16_t keycode, keyrecord_t *record) {
|
||||
|
||||
switch (keycode) {
|
||||
case CAPS_MOD:
|
||||
if (record->event.pressed) {
|
||||
caps_mod_held = true;
|
||||
caps_mod_timer = timer_read();
|
||||
} else {
|
||||
if (caps_mod_ctrl_registered) {
|
||||
unregister_code(KC_LCTL);
|
||||
caps_mod_ctrl_registered = false;
|
||||
} else {
|
||||
uint8_t mods = get_mods();
|
||||
// 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 if (mods & MOD_MASK_ALT) {
|
||||
caps_word_toggle();
|
||||
} else if (mods & MOD_MASK_SHIFT) {
|
||||
tap_code(KC_CAPS); // Shift still held → host sees Shift+CapsLock (toggles on most OSes)
|
||||
} else {
|
||||
tap_code(KC_ESC);
|
||||
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
|
||||
}
|
||||
caps_mod_held = false; // cleared in both hold and tap paths
|
||||
}
|
||||
return false;
|
||||
return true; // let core handle Esc tap or Ctrl hold
|
||||
|
||||
case LCK_FN1:
|
||||
case LCK_FN2:
|
||||
@@ -198,12 +378,24 @@ bool process_record_user(uint16_t keycode, keyrecord_t *record) {
|
||||
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;
|
||||
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.
|
||||
@@ -220,6 +412,19 @@ bool process_record_user(uint16_t keycode, keyrecord_t *record) {
|
||||
}
|
||||
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) {
|
||||
@@ -247,16 +452,22 @@ bool process_record_user(uint16_t keycode, keyrecord_t *record) {
|
||||
}
|
||||
|
||||
void matrix_scan_user(void) {
|
||||
if (caps_mod_held && !caps_mod_ctrl_registered
|
||||
&& timer_elapsed(caps_mod_timer) > TAPPING_TERM) {
|
||||
caps_mod_ctrl_registered = true;
|
||||
register_code(KC_LCTL);
|
||||
}
|
||||
if (alt_tab_active && timer_elapsed(alt_tab_timer) > ALT_TAB_TIMEOUT) {
|
||||
unregister_code(KC_LALT);
|
||||
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 --------------------------------------------------
|
||||
@@ -264,6 +475,19 @@ void matrix_scan_user(void) {
|
||||
// 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
|
||||
@@ -287,19 +511,28 @@ bool rgb_matrix_indicators_advanced_user(uint8_t led_min, uint8_t led_max) {
|
||||
|
||||
// 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
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
@@ -5,3 +5,5 @@ COMBO_ENABLE = yes
|
||||
CAPS_WORD_ENABLE = yes
|
||||
AUTOCORRECT_ENABLE = yes
|
||||
SRC += chord_unicode.c
|
||||
|
||||
DEBOUNCE_TYPE = sym_defer_pk
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
KEYCHRON_RGB_ENABLE = yes
|
||||
|
||||
include keyboards/keychron/common/wireless/wireless.mk
|
||||
include keyboards/keychron/common/keychron_common.mk
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
+689
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user