tap flow is now in QMK
This commit is contained in:
parent
2a66bfd455
commit
da622df01f
61 changed files with 0 additions and 5818 deletions
3
modules/getreuer/.gitignore
vendored
3
modules/getreuer/.gitignore
vendored
|
@ -1,3 +0,0 @@
|
||||||
*.bin
|
|
||||||
*.hex
|
|
||||||
|
|
|
@ -1,94 +0,0 @@
|
||||||
# Code of Conduct
|
|
||||||
|
|
||||||
## Our Pledge
|
|
||||||
|
|
||||||
In the interest of fostering an open and welcoming environment, we as
|
|
||||||
contributors and maintainers pledge to making participation in our project and
|
|
||||||
our community a harassment-free experience for everyone, regardless of age, body
|
|
||||||
size, disability, ethnicity, gender identity and expression, level of
|
|
||||||
experience, education, socio-economic status, nationality, personal appearance,
|
|
||||||
race, religion, or sexual identity and orientation.
|
|
||||||
|
|
||||||
## Our Standards
|
|
||||||
|
|
||||||
Examples of behavior that contributes to creating a positive environment
|
|
||||||
include:
|
|
||||||
|
|
||||||
* Using welcoming and inclusive language
|
|
||||||
* Being respectful of differing viewpoints and experiences
|
|
||||||
* Gracefully accepting constructive criticism
|
|
||||||
* Focusing on what is best for the community
|
|
||||||
* Showing empathy towards other community members
|
|
||||||
|
|
||||||
Examples of unacceptable behavior by participants include:
|
|
||||||
|
|
||||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
|
||||||
advances
|
|
||||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
|
||||||
* Public or private harassment
|
|
||||||
* Publishing others' private information, such as a physical or electronic
|
|
||||||
address, without explicit permission
|
|
||||||
* Other conduct which could reasonably be considered inappropriate in a
|
|
||||||
professional setting
|
|
||||||
|
|
||||||
## Our Responsibilities
|
|
||||||
|
|
||||||
Project maintainers are responsible for clarifying the standards of acceptable
|
|
||||||
behavior and are expected to take appropriate and fair corrective action in
|
|
||||||
response to any instances of unacceptable behavior.
|
|
||||||
|
|
||||||
Project maintainers have the right and responsibility to remove, edit, or reject
|
|
||||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
|
||||||
not aligned to this Code of Conduct, or to ban temporarily or permanently any
|
|
||||||
contributor for other behaviors that they deem inappropriate, threatening,
|
|
||||||
offensive, or harmful.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
This Code of Conduct applies both within project spaces and in public spaces
|
|
||||||
when an individual is representing the project or its community. Examples of
|
|
||||||
representing a project or community include using an official project e-mail
|
|
||||||
address, posting via an official social media account, or acting as an appointed
|
|
||||||
representative at an online or offline event. Representation of a project may be
|
|
||||||
further defined and clarified by project maintainers.
|
|
||||||
|
|
||||||
This Code of Conduct also applies outside the project spaces when the Project
|
|
||||||
Steward has a reasonable belief that an individual's behavior may have a
|
|
||||||
negative impact on the project or its community.
|
|
||||||
|
|
||||||
## Conflict Resolution
|
|
||||||
|
|
||||||
We do not believe that all conflict is bad; healthy debate and disagreement
|
|
||||||
often yield positive results. However, it is never okay to be disrespectful or
|
|
||||||
to engage in behavior that violates the project’s code of conduct.
|
|
||||||
|
|
||||||
If you see someone violating the code of conduct, you are encouraged to address
|
|
||||||
the behavior directly with those involved. Many issues can be resolved quickly
|
|
||||||
and easily, and this gives people more control over the outcome of their
|
|
||||||
dispute. If you are unable to resolve the matter for any reason, or if the
|
|
||||||
behavior is threatening or harassing, report it. We are dedicated to providing
|
|
||||||
an environment where participants feel welcome and safe.
|
|
||||||
|
|
||||||
Reports should be directed to *[PROJECT STEWARD NAME(s) AND EMAIL(s)]*, the
|
|
||||||
Project Steward(s) for *[PROJECT NAME]*. It is the Project Steward’s duty to
|
|
||||||
receive and address reported violations of the code of conduct. They will then
|
|
||||||
work with a committee consisting of representatives from the Open Source
|
|
||||||
Programs Office and the Google Open Source Strategy team. If for any reason you
|
|
||||||
are uncomfortable reaching out to the Project Steward, please email
|
|
||||||
opensource@google.com.
|
|
||||||
|
|
||||||
We will investigate every complaint, but you may not receive a direct response.
|
|
||||||
We will use our discretion in determining when and how to follow up on reported
|
|
||||||
incidents, which may range from not taking action to permanent expulsion from
|
|
||||||
the project and project-sponsored spaces. We will notify the accused of the
|
|
||||||
report and provide them an opportunity to discuss it before any action is taken.
|
|
||||||
The identity of the reporter will be omitted from the details of the report
|
|
||||||
supplied to the accused. In potentially harmful situations, such as ongoing
|
|
||||||
harassment or threats to anyone's safety, we may take action without notice.
|
|
||||||
|
|
||||||
## Attribution
|
|
||||||
|
|
||||||
This Code of Conduct is adapted from the Contributor Covenant, version 1.4,
|
|
||||||
available at
|
|
||||||
https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
# How to Contribute
|
|
||||||
|
|
||||||
We'd love to accept your patches and contributions to this project. There are
|
|
||||||
just a few small guidelines you need to follow.
|
|
||||||
|
|
||||||
## Contributor License Agreement
|
|
||||||
|
|
||||||
Contributions to this project must be accompanied by a Contributor License
|
|
||||||
Agreement (CLA). You (or your employer) retain the copyright to your
|
|
||||||
contribution; this simply gives us permission to use and redistribute your
|
|
||||||
contributions as part of the project. Head over to
|
|
||||||
<https://cla.developers.google.com/> to see your current agreements on file or
|
|
||||||
to sign a new one.
|
|
||||||
|
|
||||||
You generally only need to submit a CLA once, so if you've already submitted one
|
|
||||||
(even if it was for a different project), you probably don't need to do it
|
|
||||||
again.
|
|
||||||
|
|
||||||
## Code Reviews
|
|
||||||
|
|
||||||
All submissions, including submissions by project members, require review. We
|
|
||||||
use GitHub pull requests for this purpose. Consult
|
|
||||||
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
|
|
||||||
information on using pull requests.
|
|
||||||
|
|
||||||
## Community Guidelines
|
|
||||||
|
|
||||||
This project follows
|
|
||||||
[Google's Open Source Community Guidelines](https://opensource.google/conduct/).
|
|
||||||
|
|
|
@ -1,202 +0,0 @@
|
||||||
|
|
||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
|
||||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
||||||
replaced with your own identifying information. (Don't include
|
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright [yyyy] [name of copyright owner]
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
http://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.
|
|
|
@ -1,103 +0,0 @@
|
||||||
# @getreuer's QMK community modules
|
|
||||||
|
|
||||||
(This is not an officially supported Google product.)
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
| Module | Description |
|
|
||||||
|-------------------------------------------|-------------------------------------------------------|
|
|
||||||
| [Achordion](./achordion/) | Customize the tap-hold decision. |
|
|
||||||
| [Custom Shift Keys](./custom_shift_keys/) | Customize what keycode is produced when shifted. |
|
|
||||||
| [Keycode String](./keycode_string/) | Format QMK keycodes as human-readable strings. |
|
|
||||||
| [Mouse Turbo Click](./mouse_turbo_click/) | Click the mouse rapidly. |
|
|
||||||
| [Orbital Mouse](./orbital_mouse/) | A polar approach to mouse key control. |
|
|
||||||
| [PaletteFx](./palettefx/) | Palette-based animated RGB matrix lighting effects. |
|
|
||||||
| [Select Word](./select_word/) | Convenient word and line selection. |
|
|
||||||
| [Sentence Case](./sentence_case/) | Automatically capitalize sentences. |
|
|
||||||
| [SOCD Cleaner](./socd_cleaner/) | SOCD filtering for fast gaming inputs. |
|
|
||||||
| [Tap Flow](./tap_flow/) | Disable HRMs during fast typing (Global Quick Tap). |
|
|
||||||
|
|
||||||
|
|
||||||
## What is this?
|
|
||||||
|
|
||||||
This repo contains my community modules for [Quantum Mechanical Keyboard
|
|
||||||
(QMK)](https://docs.qmk.fm) firmware, used on custom keyboards like the ZSA
|
|
||||||
Voyager pictured above. I use most of these modules myself in [my QMK
|
|
||||||
keymap](https://github.com/getreuer/qmk-keymap).
|
|
||||||
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This repo uses the Apache License 2.0 except where otherwise indicated. See the
|
|
||||||
[LICENSE file](LICENSE.txt) for details.
|
|
||||||
|
|
||||||
|
|
||||||
## How to install
|
|
||||||
|
|
||||||
This repo makes use of [Community
|
|
||||||
Modules](https://getreuer.info/posts/keyboards/qmk-community-modules/index.html)
|
|
||||||
support added in QMK Firmware 0.28.0, released on 2025-02-27. [Update your QMK
|
|
||||||
set
|
|
||||||
up](https://docs.qmk.fm/newbs_git_using_your_master_branch#updating-your-master-branch)
|
|
||||||
to get the latest. If you have it, there will be a `modules` folder inside your
|
|
||||||
`qmk_firmware` folder.
|
|
||||||
|
|
||||||
**Step 1. Download modules.** Run these shell commands to download the
|
|
||||||
modules, replacing `/path/to/qmk_firmware` with the path of your
|
|
||||||
"`qmk_firmware`" folder:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cd /path/to/qmk_firmware
|
|
||||||
mkdir -p modules
|
|
||||||
git submodule add https://github.com/getreuer/qmk-modules.git modules/getreuer
|
|
||||||
git submodule update --init --recursive
|
|
||||||
```
|
|
||||||
|
|
||||||
Or if using [External
|
|
||||||
Userspace](https://docs.qmk.fm/newbs_external_userspace), replace the first
|
|
||||||
line with `cd /path/to/your/external/userspace`.
|
|
||||||
|
|
||||||
Or if you don't want to use git, [download a .zip of this
|
|
||||||
repo](https://github.com/getreuer/qmk-modules/archive/refs/heads/main.zip) into
|
|
||||||
the `modules` folder. Unzip it, then rename the resulting `qmk-modules-main`
|
|
||||||
folder to `getreuer`.
|
|
||||||
|
|
||||||
In any case, the installed directory structure is like this:
|
|
||||||
|
|
||||||
<QMK_FIRMWARE or QMK_USERSPACE>
|
|
||||||
└── modules
|
|
||||||
└── getreuer
|
|
||||||
├── achordion
|
|
||||||
├── custom_shift_keys
|
|
||||||
├── keycode_string
|
|
||||||
└── ...
|
|
||||||
|
|
||||||
**Step 2. Add modules to keymap.json.** Add a module to your keymap by writing a
|
|
||||||
file `keymap.json` in your keymap folder with the content
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"modules": ["getreuer/tap_flow"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Or if a `keymap.json` already exists, merge the `"modules"` line into it. Add
|
|
||||||
multiple modules like:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"modules": ["getreuer/tap_flow", "getreuer/sentence_case"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Follow the modules' documentation for any further specific set up.
|
|
||||||
|
|
||||||
**Step 3. Update the firmware.** Compile and flash the firmware as usual. If
|
|
||||||
there are build errors, try running `qmk clean` and compiling again for a clean
|
|
||||||
build.
|
|
||||||
|
|
||||||
|
|
||||||
## How to uninstall
|
|
||||||
|
|
||||||
Remove the modules from `keymap.json` and delete the `modules/getreuer` folder.
|
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
# Achordion
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<tr><td><b>Module</b></td><td><tt>getreuer/achordion</tt></td></tr>
|
|
||||||
<tr><td><b>Version</b></td><td>2025-03-07</td></tr>
|
|
||||||
<tr><td><b>Maintainer</b></td><td>Pascal Getreuer (@getreuer)</td></tr>
|
|
||||||
<tr><td><b>License</b></td><td><a href="../LICENSE.txt">Apache 2.0</a></td></tr>
|
|
||||||
<tr><td><b>Documentation</b></td><td>
|
|
||||||
<a href="https://getreuer.info/posts/keyboards/achordion">https://getreuer.info/posts/keyboards/achordion</a>
|
|
||||||
</td></tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
This is a community module adaptation of
|
|
||||||
[Achordion](https://getreuer.info/posts/keyboards/achordion), a customizable
|
|
||||||
"opposite hands" rule implementation for tap-hold keys. Achordion is the
|
|
||||||
predecessor of QMK core feature [Chordal
|
|
||||||
Hold](https://docs.qmk.fm/tap_hold#chordal-hold).
|
|
||||||
|
|
||||||
Add the following to your `keymap.json` to use Achordion:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"modules": ["getreuer/achordion"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Optionally, Achordion can be customized through several callbacks and config
|
|
||||||
options. See the [Achordion
|
|
||||||
documentation](https://getreuer.info/posts/keyboards/achordion) for how to do
|
|
||||||
that and further details.
|
|
||||||
|
|
|
@ -1,380 +0,0 @@
|
||||||
// Copyright 2022-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.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @file achordion.c
|
|
||||||
* @brief Achordion community module implementation
|
|
||||||
*
|
|
||||||
* For full documentation, see
|
|
||||||
* <https://getreuer.info/posts/keyboards/achordion>
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "achordion.h"
|
|
||||||
|
|
||||||
#pragma message \
|
|
||||||
"Achordion has evolved into core QMK feature Chordal Hold! To use it, update your QMK set up and see https://docs.qmk.fm/tap_hold#chordal-hold"
|
|
||||||
|
|
||||||
ASSERT_COMMUNITY_MODULES_MIN_API_VERSION(1, 0, 0);
|
|
||||||
|
|
||||||
// Copy of the `record` and `keycode` args for the current active tap-hold key.
|
|
||||||
static keyrecord_t tap_hold_record;
|
|
||||||
static uint16_t tap_hold_keycode = KC_NO;
|
|
||||||
// Timeout timer. When it expires, the key is considered held.
|
|
||||||
static uint16_t hold_timer = 0;
|
|
||||||
// Eagerly applied mods, if any.
|
|
||||||
static uint8_t eager_mods = 0;
|
|
||||||
// Flag to determine whether another key is pressed within the timeout.
|
|
||||||
static bool pressed_another_key_before_release = false;
|
|
||||||
|
|
||||||
#ifdef ACHORDION_STREAK
|
|
||||||
// Timer for typing streak
|
|
||||||
static uint16_t streak_timer = 0;
|
|
||||||
#else
|
|
||||||
// When disabled, is_streak is never true
|
|
||||||
#define is_streak false
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Achordion's current state.
|
|
||||||
enum {
|
|
||||||
// A tap-hold key is pressed, but hasn't yet been settled as tapped or held.
|
|
||||||
STATE_UNSETTLED,
|
|
||||||
// Achordion is inactive.
|
|
||||||
STATE_RELEASED,
|
|
||||||
// Active tap-hold key has been settled as tapped.
|
|
||||||
STATE_TAPPING,
|
|
||||||
// Active tap-hold key has been settled as held.
|
|
||||||
STATE_HOLDING,
|
|
||||||
// This state is set while calling `process_record()`, which will recursively
|
|
||||||
// call `process_achordion()`. This state is checked so that we don't process
|
|
||||||
// events generated by Achordion and potentially create an infinite loop.
|
|
||||||
STATE_RECURSING,
|
|
||||||
};
|
|
||||||
static uint8_t achordion_state = STATE_RELEASED;
|
|
||||||
|
|
||||||
#ifdef ACHORDION_STREAK
|
|
||||||
static void update_streak_timer(uint16_t keycode, keyrecord_t* record) {
|
|
||||||
if (achordion_streak_continue(keycode)) {
|
|
||||||
// We use 0 to represent an unset timer, so `| 1` to force a nonzero value.
|
|
||||||
streak_timer = record->event.time | 1;
|
|
||||||
} else {
|
|
||||||
streak_timer = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Presses or releases eager_mods through process_action(), which skips the
|
|
||||||
// usual event handling pipeline. The action is considered as a mod-tap hold or
|
|
||||||
// release, with Retro Tapping if enabled.
|
|
||||||
static void process_eager_mods_action(void) {
|
|
||||||
action_t action;
|
|
||||||
action.code = ACTION_MODS_TAP_KEY(
|
|
||||||
eager_mods, QK_MOD_TAP_GET_TAP_KEYCODE(tap_hold_keycode));
|
|
||||||
process_action(&tap_hold_record, action);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calls `process_record()` with state set to RECURSING.
|
|
||||||
static void recursively_process_record(keyrecord_t* record, uint8_t state) {
|
|
||||||
achordion_state = STATE_RECURSING;
|
|
||||||
#if defined(POINTING_DEVICE_ENABLE) && defined(POINTING_DEVICE_AUTO_MOUSE_ENABLE)
|
|
||||||
int8_t mouse_key_tracker = get_auto_mouse_key_tracker();
|
|
||||||
#endif
|
|
||||||
process_record(record);
|
|
||||||
#if defined(POINTING_DEVICE_ENABLE) && defined(POINTING_DEVICE_AUTO_MOUSE_ENABLE)
|
|
||||||
set_auto_mouse_key_tracker(mouse_key_tracker);
|
|
||||||
#endif
|
|
||||||
achordion_state = state;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sends hold press event and settles the active tap-hold key as held.
|
|
||||||
static void settle_as_hold(void) {
|
|
||||||
if (eager_mods) {
|
|
||||||
// If eager mods are being applied, nothing needs to be done besides
|
|
||||||
// updating the state.
|
|
||||||
dprintln("Achordion: Settled eager mod as hold.");
|
|
||||||
achordion_state = STATE_HOLDING;
|
|
||||||
} else {
|
|
||||||
// Create hold press event.
|
|
||||||
dprintln("Achordion: Plumbing hold press.");
|
|
||||||
recursively_process_record(&tap_hold_record, STATE_HOLDING);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sends tap press and release and settles the active tap-hold key as tapped.
|
|
||||||
static void settle_as_tap(void) {
|
|
||||||
if (eager_mods) { // Clear eager mods if set.
|
|
||||||
#if defined(RETRO_TAPPING) || defined(RETRO_TAPPING_PER_KEY)
|
|
||||||
#ifdef DUMMY_MOD_NEUTRALIZER_KEYCODE
|
|
||||||
neutralize_flashing_modifiers(get_mods());
|
|
||||||
#endif // DUMMY_MOD_NEUTRALIZER_KEYCODE
|
|
||||||
#endif // defined(RETRO_TAPPING) || defined(RETRO_TAPPING_PER_KEY)
|
|
||||||
tap_hold_record.event.pressed = false;
|
|
||||||
// To avoid falsely triggering Retro Tapping, process eager mods release as
|
|
||||||
// a regular mods release rather than a mod-tap release.
|
|
||||||
action_t action;
|
|
||||||
action.code = ACTION_MODS(eager_mods);
|
|
||||||
process_action(&tap_hold_record, action);
|
|
||||||
eager_mods = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
dprintln("Achordion: Plumbing tap press.");
|
|
||||||
tap_hold_record.event.pressed = true;
|
|
||||||
tap_hold_record.tap.count = 1; // Revise event as a tap.
|
|
||||||
tap_hold_record.tap.interrupted = true;
|
|
||||||
// Plumb tap press event.
|
|
||||||
recursively_process_record(&tap_hold_record, STATE_TAPPING);
|
|
||||||
|
|
||||||
send_keyboard_report();
|
|
||||||
#if TAP_CODE_DELAY > 0
|
|
||||||
wait_ms(TAP_CODE_DELAY);
|
|
||||||
#endif // TAP_CODE_DELAY > 0
|
|
||||||
|
|
||||||
dprintln("Achordion: Plumbing tap release.");
|
|
||||||
tap_hold_record.event.pressed = false;
|
|
||||||
// Plumb tap release event.
|
|
||||||
recursively_process_record(&tap_hold_record, STATE_TAPPING);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool process_record_achordion(uint16_t keycode, keyrecord_t* record) {
|
|
||||||
// Don't process events that Achordion generated.
|
|
||||||
if (achordion_state == STATE_RECURSING) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine whether the current event is for a mod-tap or layer-tap key.
|
|
||||||
const bool is_mt = IS_QK_MOD_TAP(keycode);
|
|
||||||
const bool is_tap_hold = is_mt || IS_QK_LAYER_TAP(keycode);
|
|
||||||
// Check that this is a normal key event, don't act on combos.
|
|
||||||
const bool is_key_event = IS_KEYEVENT(record->event);
|
|
||||||
|
|
||||||
// Event while no tap-hold key is active.
|
|
||||||
if (achordion_state == STATE_RELEASED) {
|
|
||||||
if (is_tap_hold && record->tap.count == 0 && record->event.pressed &&
|
|
||||||
is_key_event) {
|
|
||||||
// A tap-hold key is pressed and considered by QMK as "held".
|
|
||||||
const uint16_t timeout = achordion_timeout(keycode);
|
|
||||||
if (timeout > 0) {
|
|
||||||
achordion_state = STATE_UNSETTLED;
|
|
||||||
// Save info about this key.
|
|
||||||
tap_hold_keycode = keycode;
|
|
||||||
tap_hold_record = *record;
|
|
||||||
hold_timer = record->event.time + timeout;
|
|
||||||
pressed_another_key_before_release = false;
|
|
||||||
eager_mods = 0;
|
|
||||||
|
|
||||||
if (is_mt) { // Apply mods immediately if they are "eager."
|
|
||||||
const uint8_t mod = mod_config(QK_MOD_TAP_GET_MODS(keycode));
|
|
||||||
if (
|
|
||||||
#if defined(CAPS_WORD_ENABLE)
|
|
||||||
// Since eager mods bypass normal event handling, Caps Word does
|
|
||||||
// not work as expected with eager Shift. So we don't apply Shift
|
|
||||||
// eagerly while Caps Word is on.
|
|
||||||
!(is_caps_word_on() && (mod & MOD_LSFT) != 0) &&
|
|
||||||
#endif // defined(CAPS_WORD_ENABLE)
|
|
||||||
achordion_eager_mod(mod)) {
|
|
||||||
eager_mods = mod;
|
|
||||||
process_eager_mods_action();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dprintf("Achordion: Key 0x%04X pressed.%s\n", keycode,
|
|
||||||
eager_mods ? " Set eager mods." : "");
|
|
||||||
return false; // Skip default handling.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef ACHORDION_STREAK
|
|
||||||
update_streak_timer(keycode, record);
|
|
||||||
#endif
|
|
||||||
return true; // Otherwise, continue with default handling.
|
|
||||||
} else if (record->event.pressed && tap_hold_keycode != keycode) {
|
|
||||||
// Track whether another key was pressed while using a tap-hold key.
|
|
||||||
pressed_another_key_before_release = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Release of the active tap-hold key.
|
|
||||||
if (keycode == tap_hold_keycode && !record->event.pressed) {
|
|
||||||
if (eager_mods) {
|
|
||||||
dprintln("Achordion: Key released. Clearing eager mods.");
|
|
||||||
tap_hold_record.event.pressed = false;
|
|
||||||
process_eager_mods_action();
|
|
||||||
} else if (achordion_state == STATE_HOLDING) {
|
|
||||||
dprintln("Achordion: Key released. Plumbing hold release.");
|
|
||||||
tap_hold_record.event.pressed = false;
|
|
||||||
// Plumb hold release event.
|
|
||||||
recursively_process_record(&tap_hold_record, STATE_RELEASED);
|
|
||||||
} else if (!pressed_another_key_before_release) {
|
|
||||||
// No other key was pressed between the press and release of the tap-hold
|
|
||||||
// key, plumb a hold press and then a release.
|
|
||||||
dprintln("Achordion: Key released. Plumbing hold press and release.");
|
|
||||||
recursively_process_record(&tap_hold_record, STATE_HOLDING);
|
|
||||||
tap_hold_record.event.pressed = false;
|
|
||||||
recursively_process_record(&tap_hold_record, STATE_RELEASED);
|
|
||||||
} else {
|
|
||||||
dprintln("Achordion: Key released.");
|
|
||||||
}
|
|
||||||
|
|
||||||
achordion_state = STATE_RELEASED;
|
|
||||||
tap_hold_keycode = KC_NO;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (achordion_state == STATE_UNSETTLED && record->event.pressed) {
|
|
||||||
#ifdef ACHORDION_STREAK
|
|
||||||
const uint16_t s_timeout =
|
|
||||||
achordion_streak_chord_timeout(tap_hold_keycode, keycode);
|
|
||||||
const bool is_streak =
|
|
||||||
streak_timer && s_timeout &&
|
|
||||||
!timer_expired(record->event.time, (streak_timer + s_timeout));
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Press event occurred on a key other than the active tap-hold key.
|
|
||||||
|
|
||||||
// If the other key is *also* a tap-hold key and considered by QMK to be
|
|
||||||
// held, then we settle the active key as held. This way, things like
|
|
||||||
// chording multiple home row modifiers will work, but let's our logic
|
|
||||||
// consider simply a single tap-hold key as "active" at a time.
|
|
||||||
//
|
|
||||||
// Otherwise, we call `achordion_chord()` to determine whether to settle the
|
|
||||||
// tap-hold key as tapped vs. held. We implement the tap or hold by plumbing
|
|
||||||
// events back into the handling pipeline so that QMK features and other
|
|
||||||
// user code can see them. This is done by calling `process_record()`, which
|
|
||||||
// in turn calls most handlers including `process_record_user()`.
|
|
||||||
if (!is_streak &&
|
|
||||||
(!is_key_event || (is_tap_hold && record->tap.count == 0) ||
|
|
||||||
achordion_chord(tap_hold_keycode, &tap_hold_record, keycode,
|
|
||||||
record))) {
|
|
||||||
settle_as_hold();
|
|
||||||
|
|
||||||
#ifdef REPEAT_KEY_ENABLE
|
|
||||||
// Edge case involving LT + Repeat Key: in a sequence of "LT down, other
|
|
||||||
// down" where "other" is on the other layer in the same position as
|
|
||||||
// Repeat or Alternate Repeat, the repeated keycode is set instead of the
|
|
||||||
// the one on the switched-to layer. Here we correct that.
|
|
||||||
if (get_repeat_key_count() != 0 && IS_QK_LAYER_TAP(tap_hold_keycode)) {
|
|
||||||
record->keycode = KC_NO; // Forget the repeated keycode.
|
|
||||||
clear_weak_mods();
|
|
||||||
}
|
|
||||||
#endif // REPEAT_KEY_ENABLE
|
|
||||||
} else {
|
|
||||||
settle_as_tap();
|
|
||||||
|
|
||||||
#ifdef ACHORDION_STREAK
|
|
||||||
update_streak_timer(keycode, record);
|
|
||||||
if (is_streak && is_key_event && is_tap_hold && record->tap.count == 0) {
|
|
||||||
// If we are in a streak and resolved the current tap-hold key as a tap
|
|
||||||
// consider the next tap-hold key as active to be resolved next.
|
|
||||||
update_streak_timer(tap_hold_keycode, &tap_hold_record);
|
|
||||||
const uint16_t timeout = achordion_timeout(keycode);
|
|
||||||
tap_hold_keycode = keycode;
|
|
||||||
tap_hold_record = *record;
|
|
||||||
hold_timer = record->event.time + timeout;
|
|
||||||
achordion_state = STATE_UNSETTLED;
|
|
||||||
pressed_another_key_before_release = false;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
recursively_process_record(record, achordion_state); // Re-process event.
|
|
||||||
return false; // Block the original event.
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef ACHORDION_STREAK
|
|
||||||
// update idle timer on regular keys event
|
|
||||||
update_streak_timer(keycode, record);
|
|
||||||
#endif
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void housekeeping_task_achordion(void) {
|
|
||||||
if (achordion_state == STATE_UNSETTLED &&
|
|
||||||
timer_expired(timer_read(), hold_timer)) {
|
|
||||||
settle_as_hold(); // Timeout expired, settle the key as held.
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef ACHORDION_STREAK
|
|
||||||
#define MAX_STREAK_TIMEOUT 800
|
|
||||||
if (streak_timer &&
|
|
||||||
timer_expired(timer_read(), (streak_timer + MAX_STREAK_TIMEOUT))) {
|
|
||||||
streak_timer = 0; // Expired.
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns true if `pos` on the left hand of the keyboard, false if right.
|
|
||||||
static bool on_left_hand(keypos_t pos) {
|
|
||||||
#ifdef SPLIT_KEYBOARD
|
|
||||||
return pos.row < MATRIX_ROWS / 2;
|
|
||||||
#else
|
|
||||||
return (MATRIX_COLS > MATRIX_ROWS) ? pos.col < MATRIX_COLS / 2
|
|
||||||
: pos.row < MATRIX_ROWS / 2;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
bool achordion_opposite_hands(const keyrecord_t* tap_hold_record,
|
|
||||||
const keyrecord_t* other_record) {
|
|
||||||
return on_left_hand(tap_hold_record->event.key) !=
|
|
||||||
on_left_hand(other_record->event.key);
|
|
||||||
}
|
|
||||||
|
|
||||||
// By default, use the BILATERAL_COMBINATIONS rule to consider the tap-hold key
|
|
||||||
// "held" only when it and the other key are on opposite hands.
|
|
||||||
__attribute__((weak)) bool achordion_chord(uint16_t tap_hold_keycode,
|
|
||||||
keyrecord_t* tap_hold_record,
|
|
||||||
uint16_t other_keycode,
|
|
||||||
keyrecord_t* other_record) {
|
|
||||||
return achordion_opposite_hands(tap_hold_record, other_record);
|
|
||||||
}
|
|
||||||
|
|
||||||
// By default, the timeout is 1000 ms for all keys.
|
|
||||||
__attribute__((weak)) uint16_t achordion_timeout(uint16_t tap_hold_keycode) {
|
|
||||||
return 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
// By default, Shift and Ctrl mods are eager, and Alt and GUI are not.
|
|
||||||
__attribute__((weak)) bool achordion_eager_mod(uint8_t mod) {
|
|
||||||
return (mod & (MOD_LALT | MOD_LGUI)) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef ACHORDION_STREAK
|
|
||||||
__attribute__((weak)) bool achordion_streak_continue(uint16_t keycode) {
|
|
||||||
// If any mods other than shift or AltGr are held, don't continue the streak
|
|
||||||
if (get_mods() & (MOD_MASK_CG | MOD_BIT_LALT)) return false;
|
|
||||||
// This function doesn't get called for holds, so convert to tap version of
|
|
||||||
// keycodes
|
|
||||||
if (IS_QK_MOD_TAP(keycode)) keycode = QK_MOD_TAP_GET_TAP_KEYCODE(keycode);
|
|
||||||
if (IS_QK_LAYER_TAP(keycode)) keycode = QK_LAYER_TAP_GET_TAP_KEYCODE(keycode);
|
|
||||||
// Regular letters and punctuation continue the streak.
|
|
||||||
if (keycode >= KC_A && keycode <= KC_Z) return true;
|
|
||||||
switch (keycode) {
|
|
||||||
case KC_DOT:
|
|
||||||
case KC_COMMA:
|
|
||||||
case KC_QUOTE:
|
|
||||||
case KC_SPACE:
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// All other keys end the streak
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
__attribute__((weak)) uint16_t achordion_streak_chord_timeout(
|
|
||||||
uint16_t tap_hold_keycode, uint16_t next_keycode) {
|
|
||||||
return achordion_streak_timeout(tap_hold_keycode);
|
|
||||||
}
|
|
||||||
|
|
||||||
__attribute__((weak)) uint16_t
|
|
||||||
achordion_streak_timeout(uint16_t tap_hold_keycode) {
|
|
||||||
return 200;
|
|
||||||
}
|
|
||||||
#endif
|
|
|
@ -1,167 +0,0 @@
|
||||||
// Copyright 2022-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.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @file achordion.h
|
|
||||||
* @brief Achordion community module: Customizing the tap-hold decision.
|
|
||||||
*
|
|
||||||
* Overview
|
|
||||||
* --------
|
|
||||||
*
|
|
||||||
* This module customizes when tap-hold keys are considered held vs. tapped
|
|
||||||
* based on the next pressed key, like Manna Harbour's Bilateral Combinations or
|
|
||||||
* ZMK's positional hold. The library works on top of QMK's existing tap-hold
|
|
||||||
* implementation. You define mod-tap and layer-tap keys as usual and use
|
|
||||||
* Achordion to fine-tune the behavior.
|
|
||||||
*
|
|
||||||
* When QMK settles a tap-hold key as held, Achordion intercepts the event.
|
|
||||||
* Achordion then revises the event as a tap or passes it along as a hold:
|
|
||||||
*
|
|
||||||
* * Chord condition: On the next key press, a customizable `achordion_chord()`
|
|
||||||
* function is called, which takes the tap-hold key and the next key pressed
|
|
||||||
* as args. When the function returns true, the tap-hold key is settled as
|
|
||||||
* held, and otherwise as tapped.
|
|
||||||
*
|
|
||||||
* * Timeout: If no other key press occurs within a timeout, the tap-hold key
|
|
||||||
* is settled as held. This is customizable with `achordion_timeout()`.
|
|
||||||
*
|
|
||||||
* Achordion only changes the behavior when QMK considered the key held. It
|
|
||||||
* changes some would-be holds to taps, but no taps to holds.
|
|
||||||
*
|
|
||||||
* @note Some QMK features handle events before the point where Achordion can
|
|
||||||
* intercept them, particularly: Combos, Key Lock, and Dynamic Macros. It's
|
|
||||||
* still possible to use these features and Achordion in your keymap, but beware
|
|
||||||
* they might behave poorly when used simultaneously with tap-hold keys.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* For full documentation, see
|
|
||||||
* <https://getreuer.info/posts/keyboards/achordion>
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "quantum.h"
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Optional callback to customize which key chords are considered "held".
|
|
||||||
*
|
|
||||||
* In your keymap.c, define the callback
|
|
||||||
*
|
|
||||||
* bool achordion_chord(uint16_t tap_hold_keycode,
|
|
||||||
* keyrecord_t* tap_hold_record,
|
|
||||||
* uint16_t other_keycode,
|
|
||||||
* keyrecord_t* other_record) {
|
|
||||||
* // Conditions...
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* This callback is called if while `tap_hold_keycode` is pressed,
|
|
||||||
* `other_keycode` is pressed. Return true if the tap-hold key should be
|
|
||||||
* considered held, or false to consider it tapped.
|
|
||||||
*
|
|
||||||
* @param tap_hold_keycode Keycode of the tap-hold key.
|
|
||||||
* @param tap_hold_record keyrecord_t from the tap-hold press event.
|
|
||||||
* @param other_keycode Keycode of the other key.
|
|
||||||
* @param other_record keyrecord_t from the other key's press event.
|
|
||||||
* @return True if the tap-hold key should be considered held.
|
|
||||||
*/
|
|
||||||
bool achordion_chord(uint16_t tap_hold_keycode, keyrecord_t* tap_hold_record,
|
|
||||||
uint16_t other_keycode, keyrecord_t* other_record);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Optional callback to define a timeout duration per keycode.
|
|
||||||
*
|
|
||||||
* In your keymap.c, define the callback
|
|
||||||
*
|
|
||||||
* uint16_t achordion_timeout(uint16_t tap_hold_keycode) {
|
|
||||||
* // ...
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* The callback determines Achordion's timeout duration for `tap_hold_keycode`
|
|
||||||
* in units of milliseconds. The timeout be in the range 0 to 32767 ms (upper
|
|
||||||
* bound is due to 16-bit timer limitations). Use a timeout of 0 to bypass
|
|
||||||
* Achordion.
|
|
||||||
*
|
|
||||||
* @param tap_hold_keycode Keycode of the tap-hold key.
|
|
||||||
* @return Timeout duration in milliseconds in the range 0 to 32767.
|
|
||||||
*/
|
|
||||||
uint16_t achordion_timeout(uint16_t tap_hold_keycode);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Optional callback defining which mods are "eagerly" applied.
|
|
||||||
*
|
|
||||||
* This callback defines which mods are "eagerly" applied while a mod-tap
|
|
||||||
* key is still being settled. This is helpful to reduce delay particularly when
|
|
||||||
* using mod-tap keys with an external mouse.
|
|
||||||
*
|
|
||||||
* Define this callback in your keymap.c. The default callback is eager for
|
|
||||||
* Shift and Ctrl, and not for Alt and GUI:
|
|
||||||
*
|
|
||||||
* bool achordion_eager_mod(uint8_t mod) {
|
|
||||||
* return (mod & (MOD_LALT | MOD_LGUI)) == 0;
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* @note `mod` should be compared with `MOD_` prefixed codes, not `KC_` codes,
|
|
||||||
* described at <https://docs.qmk.fm/mod_tap>.
|
|
||||||
*
|
|
||||||
* @param mod Modifier `MOD_` code.
|
|
||||||
* @return True if the modifier should be eagerly applied.
|
|
||||||
*/
|
|
||||||
bool achordion_eager_mod(uint8_t mod);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the args come from keys on opposite hands.
|
|
||||||
*
|
|
||||||
* @param tap_hold_record keyrecord_t from the tap-hold key's event.
|
|
||||||
* @param other_record keyrecord_t from the other key's event.
|
|
||||||
* @return True if the keys are on opposite hands.
|
|
||||||
*/
|
|
||||||
bool achordion_opposite_hands(const keyrecord_t* tap_hold_record,
|
|
||||||
const keyrecord_t* other_record);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Suppress tap-hold mods within a *typing streak* by defining
|
|
||||||
* ACHORDION_STREAK. This can help preventing accidental mod
|
|
||||||
* activation when performing a fast tapping sequence.
|
|
||||||
* This is inspired by
|
|
||||||
* https://sunaku.github.io/home-row-mods.html#typing-streaks
|
|
||||||
*
|
|
||||||
* Enable with:
|
|
||||||
*
|
|
||||||
* #define ACHORDION_STREAK
|
|
||||||
*
|
|
||||||
* Adjust the maximum time between key events before modifiers can be enabled
|
|
||||||
* by defining the following callback in your keymap.c:
|
|
||||||
*
|
|
||||||
* uint16_t achordion_streak_chord_timeout(
|
|
||||||
* uint16_t tap_hold_keycode, uint16_t next_keycode) {
|
|
||||||
* return 200; // Default of 200 ms.
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
#ifdef ACHORDION_STREAK
|
|
||||||
uint16_t achordion_streak_chord_timeout(uint16_t tap_hold_keycode,
|
|
||||||
uint16_t next_keycode);
|
|
||||||
|
|
||||||
bool achordion_streak_continue(uint16_t keycode);
|
|
||||||
|
|
||||||
/** @deprecated Use `achordion_streak_chord_timeout()` instead. */
|
|
||||||
uint16_t achordion_streak_timeout(uint16_t tap_hold_keycode);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
}
|
|
||||||
#endif
|
|
|
@ -1,17 +0,0 @@
|
||||||
// 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.
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "achordion.h"
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"module_name": "Achordion",
|
|
||||||
"maintainer": "getreuer",
|
|
||||||
"url": "https://getreuer.info/posts/keyboards/achordion"
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
# Custom Shift Keys
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<tr><td><b>Module</b></td><td><tt>getreuer/custom_shift_keys</tt></td></tr>
|
|
||||||
<tr><td><b>Version</b></td><td>2025-03-07</td></tr>
|
|
||||||
<tr><td><b>Maintainer</b></td><td>Pascal Getreuer (@getreuer)</td></tr>
|
|
||||||
<tr><td><b>License</b></td><td><a href="../LICENSE.txt">Apache 2.0</a></td></tr>
|
|
||||||
<tr><td><b>Documentation</b></td><td>
|
|
||||||
<a href="https://getreuer.info/posts/keyboards/custom-shift-keys">https://getreuer.info/posts/keyboards/custom-shift-keys</a>
|
|
||||||
</td></tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
This is a community module adaptation of [Custom Shift
|
|
||||||
Keys](https://getreuer.info/posts/keyboards/custom-shift-keys), a light
|
|
||||||
alternative to QMK's [Key Overrides](https://docs.qmk.fm/features/key_overrides)
|
|
||||||
for customizing what keycode is produced when a key is shifted.
|
|
||||||
|
|
||||||
Add the following to your `keymap.json` to use Custom Shift Keys:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"modules": ["getreuer/custom_shift_keys"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Then in your `keymap.c`, define how keys are shifted with the
|
|
||||||
`custom_shift_keys` array. Each row defines one key. The first keycode is the
|
|
||||||
keycode as it appears in your layout and determines what is typed normally. The
|
|
||||||
shifted_keycode is what you want the key to type when shifted. An example:
|
|
||||||
|
|
||||||
```c
|
|
||||||
const custom_shift_key_t custom_shift_keys[] = {
|
|
||||||
{KC_DOT , KC_QUES}, // Shift . is ?
|
|
||||||
{KC_COMM, KC_EXLM}, // Shift , is !
|
|
||||||
{KC_MINS, KC_EQL }, // Shift - is =
|
|
||||||
{KC_COLN, KC_SCLN}, // Shift : is ;
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
For instance, the first row defines that when `KC_DOT` is pressed with Shift
|
|
||||||
held, keycode `KC_QUES` is sent.
|
|
||||||
|
|
||||||
See the [Custom Shift Keys
|
|
||||||
documentation](https://getreuer.info/posts/keyboards/custom-shift-keys) for
|
|
||||||
configuration options and further details.
|
|
|
@ -1,94 +0,0 @@
|
||||||
// Copyright 2021-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.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @file custom_shift_keys.c
|
|
||||||
* @brief Custom Shift Keys community module implementation
|
|
||||||
*
|
|
||||||
* For full documentation, see
|
|
||||||
* <https://getreuer.info/posts/keyboards/custom-shift-keys>
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "custom_shift_keys.h"
|
|
||||||
|
|
||||||
ASSERT_COMMUNITY_MODULES_MIN_API_VERSION(1, 0, 0);
|
|
||||||
|
|
||||||
// Defined in introspection.c.
|
|
||||||
uint16_t custom_shift_keys_count(void);
|
|
||||||
const custom_shift_key_t* custom_shift_keys_get(uint16_t index);
|
|
||||||
|
|
||||||
bool process_record_custom_shift_keys(uint16_t keycode, keyrecord_t *record) {
|
|
||||||
static uint16_t registered_keycode = KC_NO;
|
|
||||||
|
|
||||||
// If a custom shift key is registered, then this event is either releasing
|
|
||||||
// it or manipulating another key at the same time. Either way, we release
|
|
||||||
// the currently registered key.
|
|
||||||
if (registered_keycode != KC_NO) {
|
|
||||||
unregister_code16(registered_keycode);
|
|
||||||
registered_keycode = KC_NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (record->event.pressed) { // Press event.
|
|
||||||
const uint8_t saved_mods = get_mods();
|
|
||||||
#ifndef NO_ACTION_ONESHOT
|
|
||||||
const uint8_t mods = saved_mods | get_weak_mods() | get_oneshot_mods();
|
|
||||||
#else
|
|
||||||
const uint8_t mods = saved_mods | get_weak_mods();
|
|
||||||
#endif // NO_ACTION_ONESHOT
|
|
||||||
#if CUSTOM_SHIFT_KEYS_LAYER_MASK != 0
|
|
||||||
const uint8_t layer = read_source_layers_cache(record->event.key);
|
|
||||||
#endif // CUSTOM_SHIFT_KEYS_LAYER_MASK
|
|
||||||
if ((mods & MOD_MASK_SHIFT) != 0 // Shift is held.
|
|
||||||
#if CUSTOM_SHIFT_KEYS_NEGMODS != 0
|
|
||||||
// Nothing in CUSTOM_SHIFT_KEYS_NEGMODS is held.
|
|
||||||
&& (mods & (CUSTOM_SHIFT_KEYS_NEGMODS)) == 0
|
|
||||||
#endif // CUSTOM_SHIFT_KEYS_NEGMODS != 0
|
|
||||||
#if CUSTOM_SHIFT_KEYS_LAYER_MASK != 0
|
|
||||||
// Pressed key is on a layer appearing in the layer mask.
|
|
||||||
&& ((1 << layer) & (CUSTOM_SHIFT_KEYS_LAYER_MASK)) != 0
|
|
||||||
#endif // CUSTOM_SHIFT_KEYS_LAYER_MASK
|
|
||||||
) {
|
|
||||||
// Continue default handling if this is a tap-hold key being held.
|
|
||||||
if ((IS_QK_MOD_TAP(keycode) || IS_QK_LAYER_TAP(keycode)) &&
|
|
||||||
record->tap.count == 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search for a custom shift key whose keycode is `keycode`.
|
|
||||||
for (int i = 0; i < (int)custom_shift_keys_count(); ++i) {
|
|
||||||
const custom_shift_key_t* custom_shift_key = custom_shift_keys_get(i);
|
|
||||||
if (keycode == custom_shift_key->keycode) {
|
|
||||||
registered_keycode = custom_shift_key->shifted_keycode;
|
|
||||||
if (IS_QK_MODS(registered_keycode) && // Should keycode be shifted?
|
|
||||||
(QK_MODS_GET_MODS(registered_keycode) & MOD_LSFT) != 0) {
|
|
||||||
register_code16(registered_keycode); // If so, press it directly.
|
|
||||||
} else {
|
|
||||||
// Otherwise cancel shift mods, press the key, and restore mods.
|
|
||||||
del_weak_mods(MOD_MASK_SHIFT);
|
|
||||||
#ifndef NO_ACTION_ONESHOT
|
|
||||||
del_oneshot_mods(MOD_MASK_SHIFT);
|
|
||||||
#endif // NO_ACTION_ONESHOT
|
|
||||||
unregister_mods(MOD_MASK_SHIFT);
|
|
||||||
register_code16(registered_keycode);
|
|
||||||
set_mods(saved_mods);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true; // Continue with default handling.
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
// Copyright 2021-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.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @file custom_shift_keys.h
|
|
||||||
* @brief Custom Shift Keys community module: customize how keys shift.
|
|
||||||
*
|
|
||||||
* This library implements custom shift keys, keys where you can customize what
|
|
||||||
* keycode is produced when shifted. In your keymap.c, define a table of custom
|
|
||||||
* shift keys like
|
|
||||||
*
|
|
||||||
* const custom_shift_key_t custom_shift_keys[] = {
|
|
||||||
* {KC_DOT , KC_QUES}, // Shift . is ?
|
|
||||||
* {KC_COMM, KC_EXLM}, // Shift , is !
|
|
||||||
* {KC_MINS, KC_EQL }, // Shift - is =
|
|
||||||
* {KC_COLN, KC_SCLN}, // Shift : is ;
|
|
||||||
* };
|
|
||||||
*
|
|
||||||
* Each row defines one key. The first field is the keycode as it appears in
|
|
||||||
* your layout and determines what is typed normally. The second entry is what
|
|
||||||
* you want the key to type when shifted.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* For full documentation, see
|
|
||||||
* <https://getreuer.info/posts/keyboards/custom-shift-keys>
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "quantum.h"
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom shift key entry. The `keycode` field is the keycode as it appears in
|
|
||||||
* your layout and determines what is typed normally. The `shifted_keycode` is
|
|
||||||
* what you want the key to type when shifted.
|
|
||||||
*/
|
|
||||||
typedef struct {
|
|
||||||
uint16_t keycode;
|
|
||||||
uint16_t shifted_keycode;
|
|
||||||
} custom_shift_key_t;
|
|
||||||
|
|
||||||
/** Table of custom shift keys. */
|
|
||||||
extern const custom_shift_key_t custom_shift_keys[];
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
}
|
|
||||||
#endif
|
|
|
@ -1,37 +0,0 @@
|
||||||
// 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.
|
|
||||||
|
|
||||||
#ifdef COMMUNITY_MODULE_CUSTOM_SHIFT_KEYS_ENABLE
|
|
||||||
|
|
||||||
uint16_t custom_shift_keys_count_raw(void) {
|
|
||||||
return ARRAY_SIZE(custom_shift_keys);
|
|
||||||
}
|
|
||||||
|
|
||||||
__attribute__((weak)) uint16_t custom_shift_keys_count(void) {
|
|
||||||
return custom_shift_keys_count_raw();
|
|
||||||
}
|
|
||||||
|
|
||||||
const custom_shift_key_t* custom_shift_keys_get_raw(uint16_t index) {
|
|
||||||
if (index >= custom_shift_keys_count_raw()) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
return &custom_shift_keys[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
__attribute__((weak)) const custom_shift_key_t* custom_shift_keys_get(
|
|
||||||
uint16_t index) {
|
|
||||||
return custom_shift_keys_get_raw(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif // COMMUNITY_MODULE_CUSTOM_SHIFT_KEYS_ENABLE
|
|
|
@ -1,17 +0,0 @@
|
||||||
// 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.
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "custom_shift_keys.h"
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"module_name": "Custom Shift Keys",
|
|
||||||
"maintainer": "getreuer",
|
|
||||||
"url": "https://getreuer.info/posts/keyboards/custom-shift-keys"
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 46 KiB |
Binary file not shown.
Before Width: | Height: | Size: 62 KiB |
|
@ -1,40 +0,0 @@
|
||||||
# Keycode String
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<tr><td><b>Module</b></td><td><tt>getreuer/keycode_string</tt></td></tr>
|
|
||||||
<tr><td><b>Version</b></td><td>2025-03-07</td></tr>
|
|
||||||
<tr><td><b>Maintainer</b></td><td>Pascal Getreuer (@getreuer)</td></tr>
|
|
||||||
<tr><td><b>License</b></td><td><a href="../LICENSE.txt">Apache 2.0</a></td></tr>
|
|
||||||
<tr><td><b>Documentation</b></td><td>
|
|
||||||
<a href="https://getreuer.info/posts/keyboards/keycode-string">https://getreuer.info/posts/keyboards/keycode-string</a>
|
|
||||||
</td></tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
This is a community module adaptation of [Keycode
|
|
||||||
String](https://getreuer.info/posts/keyboards/keycode-string), a utility to
|
|
||||||
convert QMK keycodes to human-readable strings. It's much nicer to read names
|
|
||||||
like "`LT(2,KC_D)`" than numerical codes like "`0x4207`."
|
|
||||||
|
|
||||||
Add the following to your `keymap.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"modules": ["getreuer/keycode_string"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Then use `get_keycode_string(keycode)` like:
|
|
||||||
|
|
||||||
```c
|
|
||||||
dprintf("kc=%s\n", get_keycode_string(keycode));
|
|
||||||
```
|
|
||||||
|
|
||||||
Many common QMK keycodes are understood out of the box by
|
|
||||||
`get_keycode_string()`, but not all. Optionally, use `KEYCODE_STRING_NAMES_USER`
|
|
||||||
in keymap.c to define names for additional keycodes or override how any keycode
|
|
||||||
is formatted.
|
|
||||||
|
|
||||||
See the [Keycode String
|
|
||||||
documentation](https://getreuer.info/posts/keyboards/keycode-string) for further
|
|
||||||
details.
|
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
// 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.
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "keycode_string.h"
|
|
||||||
#include "print.h"
|
|
||||||
|
|
|
@ -1,510 +0,0 @@
|
||||||
// Copyright 2024-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.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @file keycode_string.c
|
|
||||||
* @brief Keycode String community module implementation
|
|
||||||
*
|
|
||||||
* @note When parsing keycodes, avoid hardcoded numerical codes or twiddling
|
|
||||||
* bits directly, use QMK's APIs instead. Keycode encoding is internal to QMK
|
|
||||||
* and may change between versions.
|
|
||||||
*
|
|
||||||
* For full documentation, see
|
|
||||||
* <https://getreuer.info/posts/keyboards/keycode-string>
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "keycode_string.h"
|
|
||||||
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#include "quantum.h"
|
|
||||||
|
|
||||||
ASSERT_COMMUNITY_MODULES_MIN_API_VERSION(1, 0, 0);
|
|
||||||
|
|
||||||
typedef int_fast8_t index_t;
|
|
||||||
|
|
||||||
// clang-format off
|
|
||||||
/** Packs a 7-char keycode name, ignoring the third char, as 3 words. */
|
|
||||||
#define KEYCODE_NAME7(c0, c1, unused_c2, c3, c4, c5, c6) \
|
|
||||||
((uint16_t)c0) | (((uint16_t)c1) << 8), \
|
|
||||||
((uint16_t)c3) | (((uint16_t)c4) << 8), \
|
|
||||||
((uint16_t)c5) | (((uint16_t)c6) << 8)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Names of some common keycodes.
|
|
||||||
*
|
|
||||||
* Each (keycode, name) entry is stored flat in 8 bytes in PROGMEM. Names in
|
|
||||||
* this table must be at most 7 chars long and have an underscore '_' for the
|
|
||||||
* third char. This underscore is assumed and not actually stored.
|
|
||||||
*/
|
|
||||||
static const uint16_t common_names[] PROGMEM = {
|
|
||||||
KC_TRNS, KEYCODE_NAME7('K', 'C', '_', 'T', 'R', 'N', 'S'),
|
|
||||||
KC_ENT , KEYCODE_NAME7('K', 'C', '_', 'E', 'N', 'T', 0 ),
|
|
||||||
KC_ESC , KEYCODE_NAME7('K', 'C', '_', 'E', 'S', 'C', 0 ),
|
|
||||||
KC_BSPC, KEYCODE_NAME7('K', 'C', '_', 'B', 'S', 'P', 'C'),
|
|
||||||
KC_TAB , KEYCODE_NAME7('K', 'C', '_', 'T', 'A', 'B', 0 ),
|
|
||||||
KC_SPC , KEYCODE_NAME7('K', 'C', '_', 'S', 'P', 'C', 0 ),
|
|
||||||
KC_MINS, KEYCODE_NAME7('K', 'C', '_', 'M', 'I', 'N', 'S'),
|
|
||||||
KC_EQL , KEYCODE_NAME7('K', 'C', '_', 'E', 'Q', 'L', 0 ),
|
|
||||||
KC_LBRC, KEYCODE_NAME7('K', 'C', '_', 'L', 'B', 'R', 'C'),
|
|
||||||
KC_RBRC, KEYCODE_NAME7('K', 'C', '_', 'R', 'B', 'R', 'C'),
|
|
||||||
KC_BSLS, KEYCODE_NAME7('K', 'C', '_', 'B', 'S', 'L', 'S'),
|
|
||||||
KC_NUHS, KEYCODE_NAME7('K', 'C', '_', 'N', 'U', 'H', 'S'),
|
|
||||||
KC_SCLN, KEYCODE_NAME7('K', 'C', '_', 'S', 'C', 'L', 'N'),
|
|
||||||
KC_QUOT, KEYCODE_NAME7('K', 'C', '_', 'Q', 'U', 'O', 'T'),
|
|
||||||
KC_GRV , KEYCODE_NAME7('K', 'C', '_', 'G', 'R', 'V', 0 ),
|
|
||||||
KC_COMM, KEYCODE_NAME7('K', 'C', '_', 'C', 'O', 'M', 'M'),
|
|
||||||
KC_DOT , KEYCODE_NAME7('K', 'C', '_', 'D', 'O', 'T', 0 ),
|
|
||||||
KC_SLSH, KEYCODE_NAME7('K', 'C', '_', 'S', 'L', 'S', 'H'),
|
|
||||||
KC_CAPS, KEYCODE_NAME7('K', 'C', '_', 'C', 'A', 'P', 'S'),
|
|
||||||
KC_PSCR, KEYCODE_NAME7('K', 'C', '_', 'P', 'S', 'C', 'R'),
|
|
||||||
KC_PAUS, KEYCODE_NAME7('K', 'C', '_', 'P', 'A', 'U', 'S'),
|
|
||||||
KC_INS , KEYCODE_NAME7('K', 'C', '_', 'I', 'N', 'S', 0 ),
|
|
||||||
KC_HOME, KEYCODE_NAME7('K', 'C', '_', 'H', 'O', 'M', 'E'),
|
|
||||||
KC_PGUP, KEYCODE_NAME7('K', 'C', '_', 'P', 'G', 'U', 'P'),
|
|
||||||
KC_DEL , KEYCODE_NAME7('K', 'C', '_', 'D', 'E', 'L', 0 ),
|
|
||||||
KC_END , KEYCODE_NAME7('K', 'C', '_', 'E', 'N', 'D', 0 ),
|
|
||||||
KC_PGDN, KEYCODE_NAME7('K', 'C', '_', 'P', 'G', 'D', 'N'),
|
|
||||||
KC_RGHT, KEYCODE_NAME7('K', 'C', '_', 'R', 'G', 'H', 'T'),
|
|
||||||
KC_LEFT, KEYCODE_NAME7('K', 'C', '_', 'L', 'E', 'F', 'T'),
|
|
||||||
KC_DOWN, KEYCODE_NAME7('K', 'C', '_', 'D', 'O', 'W', 'N'),
|
|
||||||
KC_UP , KEYCODE_NAME7('K', 'C', '_', 'U', 'P', 0 , 0 ),
|
|
||||||
KC_NUBS, KEYCODE_NAME7('K', 'C', '_', 'N', 'U', 'B', 'S'),
|
|
||||||
KC_HYPR, KEYCODE_NAME7('K', 'C', '_', 'H', 'Y', 'P', 'R'),
|
|
||||||
KC_MEH , KEYCODE_NAME7('K', 'C', '_', 'M', 'E', 'H', 0 ),
|
|
||||||
#ifdef EXTRAKEY_ENABLE
|
|
||||||
KC_WHOM, KEYCODE_NAME7('K', 'C', '_', 'W', 'H', 'O', 'M'),
|
|
||||||
KC_WBAK, KEYCODE_NAME7('K', 'C', '_', 'W', 'B', 'A', 'K'),
|
|
||||||
KC_WFWD, KEYCODE_NAME7('K', 'C', '_', 'W', 'F', 'W', 'D'),
|
|
||||||
KC_WSTP, KEYCODE_NAME7('K', 'C', '_', 'W', 'S', 'T', 'P'),
|
|
||||||
KC_WREF, KEYCODE_NAME7('K', 'C', '_', 'W', 'R', 'E', 'F'),
|
|
||||||
KC_MNXT, KEYCODE_NAME7('K', 'C', '_', 'M', 'N', 'X', 'T'),
|
|
||||||
KC_MPRV, KEYCODE_NAME7('K', 'C', '_', 'M', 'P', 'R', 'V'),
|
|
||||||
KC_MPLY, KEYCODE_NAME7('K', 'C', '_', 'M', 'P', 'L', 'Y'),
|
|
||||||
KC_MUTE, KEYCODE_NAME7('K', 'C', '_', 'M', 'U', 'T', 'E'),
|
|
||||||
KC_VOLU, KEYCODE_NAME7('K', 'C', '_', 'V', 'O', 'L', 'U'),
|
|
||||||
KC_VOLD, KEYCODE_NAME7('K', 'C', '_', 'V', 'O', 'L', 'D'),
|
|
||||||
#endif // EXTRAKEY_ENABLE
|
|
||||||
#ifdef MOUSEKEY_ENABLE
|
|
||||||
MS_LEFT, KEYCODE_NAME7('M', 'S', '_', 'L', 'E', 'F', 'T'),
|
|
||||||
MS_RGHT, KEYCODE_NAME7('M', 'S', '_', 'R', 'G', 'H', 'T'),
|
|
||||||
MS_UP , KEYCODE_NAME7('M', 'S', '_', 'U', 'P', 0 , 0 ),
|
|
||||||
MS_DOWN, KEYCODE_NAME7('M', 'S', '_', 'D', 'O', 'W', 'N'),
|
|
||||||
MS_WHLL, KEYCODE_NAME7('M', 'S', '_', 'W', 'H', 'L', 'L'),
|
|
||||||
MS_WHLR, KEYCODE_NAME7('M', 'S', '_', 'W', 'H', 'L', 'R'),
|
|
||||||
MS_WHLU, KEYCODE_NAME7('M', 'S', '_', 'W', 'H', 'L', 'U'),
|
|
||||||
MS_WHLD, KEYCODE_NAME7('M', 'S', '_', 'W', 'H', 'L', 'D'),
|
|
||||||
#endif // MOUSEKEY_ENABLE
|
|
||||||
#ifdef SWAP_HANDS_ENABLE
|
|
||||||
SH_ON , KEYCODE_NAME7('S', 'H', '_', 'O', 'N', 0 , 0 ),
|
|
||||||
SH_OFF , KEYCODE_NAME7('S', 'H', '_', 'O', 'F', 'F', 0 ),
|
|
||||||
SH_MON , KEYCODE_NAME7('S', 'H', '_', 'M', 'O', 'N', 0 ),
|
|
||||||
SH_MOFF, KEYCODE_NAME7('S', 'H', '_', 'M', 'O', 'F', 'F'),
|
|
||||||
SH_TOGG, KEYCODE_NAME7('S', 'H', '_', 'T', 'O', 'G', 'G'),
|
|
||||||
SH_TT , KEYCODE_NAME7('S', 'H', '_', 'T', 'T', 0 , 0 ),
|
|
||||||
# if !defined(NO_ACTION_ONESHOT)
|
|
||||||
SH_OS , KEYCODE_NAME7('S', 'H', '_', 'O', 'S', 0 , 0 ),
|
|
||||||
# endif // !defined(NO_ACTION_ONESHOT)
|
|
||||||
#endif // SWAP_HANDS_ENABLE
|
|
||||||
#ifdef LEADER_ENABLE
|
|
||||||
QK_LEAD, KEYCODE_NAME7('Q', 'K', '_', 'L', 'E', 'A', 'D'),
|
|
||||||
#endif // LEADER_ENABLE
|
|
||||||
#ifdef TRI_LAYER_ENABLE
|
|
||||||
TL_LOWR, KEYCODE_NAME7('T', 'L', '_', 'L', 'O', 'W', 'R'),
|
|
||||||
TL_UPPR, KEYCODE_NAME7('T', 'L', '_', 'U', 'P', 'P', 'R'),
|
|
||||||
#endif // TRI_LAYER_ENABLE
|
|
||||||
#ifdef GRAVE_ESC_ENABLE
|
|
||||||
QK_GESC, KEYCODE_NAME7('Q', 'K', '_', 'G', 'E', 'S', 'C'),
|
|
||||||
#endif // GRAVE_ESC_ENABLE
|
|
||||||
#ifdef CAPS_WORD_ENABLE
|
|
||||||
CW_TOGG, KEYCODE_NAME7('C', 'W', '_', 'T', 'O', 'G', 'G'),
|
|
||||||
#endif // CAPS_WORD_ENABLE
|
|
||||||
#ifdef LAYER_LOCK_ENABLE
|
|
||||||
QK_LLCK, KEYCODE_NAME7('Q', 'K', '_', 'L', 'L', 'C', 'K'),
|
|
||||||
#endif // LAYER_LOCK_ENABLE
|
|
||||||
EE_CLR , KEYCODE_NAME7('E', 'E', '_', 'C', 'L', 'R', 0 ),
|
|
||||||
QK_BOOT, KEYCODE_NAME7('Q', 'K', '_', 'B', 'O', 'O', 'T'),
|
|
||||||
DB_TOGG, KEYCODE_NAME7('D', 'B', '_', 'T', 'O', 'G', 'G'),
|
|
||||||
};
|
|
||||||
// clang-format on
|
|
||||||
|
|
||||||
/** Users can override this to define names of additional keycodes. */
|
|
||||||
__attribute__((weak)) const keycode_string_name_t* keycode_string_names_data_user = NULL;
|
|
||||||
__attribute__((weak)) uint16_t keycode_string_names_size_user = 0;
|
|
||||||
/** Names of the 4 mods on each hand. */
|
|
||||||
static const char mod_names[] PROGMEM = "CTL\0SFT\0ALT\0GUI";
|
|
||||||
/** Internal buffer for holding a stringified keycode. */
|
|
||||||
static char buffer[32];
|
|
||||||
#define BUFFER_MAX_LEN (sizeof(buffer) - 1)
|
|
||||||
static index_t buffer_len;
|
|
||||||
|
|
||||||
/** Finds the name of a keycode in `common_names` or returns NULL. */
|
|
||||||
static const char* search_common_names(uint16_t keycode) {
|
|
||||||
static uint8_t buffer[8];
|
|
||||||
|
|
||||||
for (int_fast16_t offset = 0; offset < ARRAY_SIZE(common_names); offset += 4) {
|
|
||||||
if (keycode == pgm_read_word(common_names + offset)) {
|
|
||||||
const uint16_t w0 = pgm_read_word(common_names + offset + 1);
|
|
||||||
const uint16_t w1 = pgm_read_word(common_names + offset + 2);
|
|
||||||
const uint16_t w2 = pgm_read_word(common_names + offset + 3);
|
|
||||||
buffer[0] = (uint8_t)w0;
|
|
||||||
buffer[1] = (uint8_t)(w0 >> 8);
|
|
||||||
buffer[2] = '_';
|
|
||||||
buffer[3] = (uint8_t)w1;
|
|
||||||
buffer[4] = (uint8_t)(w1 >> 8);
|
|
||||||
buffer[5] = (uint8_t)w2;
|
|
||||||
buffer[6] = (uint8_t)(w2 >> 8);
|
|
||||||
buffer[7] = 0;
|
|
||||||
return (const char*)buffer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Finds the name of a keycode in table or returns NULL.
|
|
||||||
*
|
|
||||||
* @param data Pointer to table to be searched.
|
|
||||||
* @param size Numer of entries in the table.
|
|
||||||
* @return Name string for the keycode, or NULL if not found.
|
|
||||||
*/
|
|
||||||
static const char* search_table(
|
|
||||||
const keycode_string_name_t* data, uint16_t size, uint16_t keycode) {
|
|
||||||
if (data != NULL) {
|
|
||||||
for (uint16_t i = 0; i < size; ++i) {
|
|
||||||
if (data[i].keycode == keycode) {
|
|
||||||
return data[i].name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Formats `number` in `base`, either 10 or 16. */
|
|
||||||
static char* number_string(uint16_t number, int8_t base) {
|
|
||||||
static char result[7];
|
|
||||||
result[sizeof(result) - 1] = '\0';
|
|
||||||
index_t i = sizeof(result) - 1;
|
|
||||||
do {
|
|
||||||
const uint8_t digit = number % base;
|
|
||||||
number /= base;
|
|
||||||
result[--i] = (digit < 10) ? (char)(digit + UINT8_C('0'))
|
|
||||||
: (char)(digit + (UINT8_C('A') - 10));
|
|
||||||
} while (number > 0 && i > 0);
|
|
||||||
|
|
||||||
if (base == 16 && i >= 2) {
|
|
||||||
result[--i] = 'x';
|
|
||||||
result[--i] = '0';
|
|
||||||
}
|
|
||||||
return result + i;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Appends `str` to `buffer`, truncating if the result would overflow. */
|
|
||||||
static void append(const char* str) {
|
|
||||||
char* dest = buffer + buffer_len;
|
|
||||||
index_t i;
|
|
||||||
for (i = 0; buffer_len + i < BUFFER_MAX_LEN && str[i]; ++i) {
|
|
||||||
dest[i] = str[i];
|
|
||||||
}
|
|
||||||
buffer_len += i;
|
|
||||||
buffer[buffer_len] = '\0';
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Same as append(), but where `str` is a PROGMEM string. */
|
|
||||||
static void append_P(const char* str) {
|
|
||||||
char* dest = buffer + buffer_len;
|
|
||||||
index_t i;
|
|
||||||
for (i = 0; buffer_len + i < BUFFER_MAX_LEN; ++i) {
|
|
||||||
const char c = pgm_read_byte(&str[i]);
|
|
||||||
if (c == '\0') {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
dest[i] = c;
|
|
||||||
}
|
|
||||||
buffer_len += i;
|
|
||||||
buffer[buffer_len] = '\0';
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Appends a single char to `buffer` if there is space. */
|
|
||||||
static void append_char(char c) {
|
|
||||||
if (buffer_len < BUFFER_MAX_LEN) {
|
|
||||||
buffer[buffer_len] = c;
|
|
||||||
buffer[++buffer_len] = '\0';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Formats `number` in `base`, either 10 or 16, and appends it to `buffer`. */
|
|
||||||
static void append_number(uint16_t number, int8_t base) {
|
|
||||||
append(number_string(number, base));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Stringifies 5-bit mods and appends it to `buffer`. */
|
|
||||||
static void append_5_bit_mods(uint8_t mods) {
|
|
||||||
const bool is_rhs = mods > 15;
|
|
||||||
const uint8_t csag = mods & 15;
|
|
||||||
if (csag != 0 && (csag & (csag - 1)) == 0) { // One mod is set.
|
|
||||||
append_P(PSTR("MOD_"));
|
|
||||||
append_char(is_rhs ? 'R' : 'L');
|
|
||||||
append_P(&mod_names[4 * biton(csag)]);
|
|
||||||
} else { // Fallback: write the mod as a hex value.
|
|
||||||
append_number(mods, 16);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Writes a keycode of the format `name` + "(" + `param` + ")".
|
|
||||||
* @note `name` is a PROGMEM string, `param` is not.
|
|
||||||
*/
|
|
||||||
static void append_unary_keycode(const char* name, const char* param) {
|
|
||||||
append_P(name);
|
|
||||||
append_char('(');
|
|
||||||
append(param);
|
|
||||||
append_char(')');
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Stringifies `keycode` and appends it to `buffer`. */
|
|
||||||
static void append_keycode(uint16_t keycode) {
|
|
||||||
// In case there is overlap among tables, search `keycode_string_names_user`
|
|
||||||
// first so that it takes precedence.
|
|
||||||
const char* keycode_name = search_table(
|
|
||||||
keycode_string_names_data_user, keycode_string_names_size_user, keycode);
|
|
||||||
if (keycode_name) {
|
|
||||||
append(keycode_name);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
keycode_name = search_common_names(keycode);
|
|
||||||
if (keycode_name) {
|
|
||||||
append(keycode_name);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keycode <= 255) { // Basic keycodes.
|
|
||||||
switch (keycode) {
|
|
||||||
// Modifiers KC_LSFT, KC_RCTL, etc.
|
|
||||||
case MODIFIER_KEYCODE_RANGE: {
|
|
||||||
const uint8_t i = keycode - KC_LCTL;
|
|
||||||
const bool is_rhs = i > 3;
|
|
||||||
append_P(PSTR("KC_"));
|
|
||||||
append_char(is_rhs ? 'R' : 'L');
|
|
||||||
append_P(&mod_names[4 * (i & 3)]);
|
|
||||||
} return;
|
|
||||||
|
|
||||||
// Letters A-Z.
|
|
||||||
case KC_A ... KC_Z:
|
|
||||||
append_P(PSTR("KC_"));
|
|
||||||
append_char((char)(keycode + (UINT8_C('A') - KC_A)));
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Digits 0-9 (NOTE: Unlike the ASCII order, KC_0 comes *after* KC_9.)
|
|
||||||
case KC_1 ... KC_0:
|
|
||||||
append_P(PSTR("KC_"));
|
|
||||||
append_char('0' + (char)((keycode - (KC_1 - 1)) % 10));
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Keypad digits.
|
|
||||||
case KC_KP_1 ... KC_KP_0:
|
|
||||||
append_P(PSTR("KC_KP_"));
|
|
||||||
append_char('0' + (char)((keycode - (KC_KP_1 - 1)) % 10));
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Function keys. F1-F12 and F13-F24 are coded in separate ranges.
|
|
||||||
case KC_F1 ... KC_F12:
|
|
||||||
append_P(PSTR("KC_F"));
|
|
||||||
append_number(keycode - (KC_F1 - 1), 10);
|
|
||||||
return;
|
|
||||||
|
|
||||||
case KC_F13 ... KC_F24:
|
|
||||||
append_P(PSTR("KC_F"));
|
|
||||||
append_number(keycode - (KC_F13 - 13), 10);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// clang-format off
|
|
||||||
switch (keycode) {
|
|
||||||
// A modified keycode, like S(KC_1) for Shift + 1 = !. This implementation
|
|
||||||
// only covers modified keycodes where one modifier is applied, e.g. a
|
|
||||||
// Ctrl + Shift + kc or Hyper + kc keycode is not formatted.
|
|
||||||
case QK_MODS ... QK_MODS_MAX: {
|
|
||||||
uint8_t mods = QK_MODS_GET_MODS(keycode);
|
|
||||||
const bool is_rhs = mods > 15;
|
|
||||||
mods &= 15;
|
|
||||||
if (mods != 0 && (mods & (mods - 1)) == 0) { // One mod is set.
|
|
||||||
const char* name = &mod_names[4 * biton(mods)];
|
|
||||||
if (is_rhs) {
|
|
||||||
append_char('R');
|
|
||||||
append_P(name);
|
|
||||||
} else {
|
|
||||||
append_char(pgm_read_byte(&name[0]));
|
|
||||||
}
|
|
||||||
append_char('(');
|
|
||||||
append_keycode(QK_MODS_GET_BASIC_KEYCODE(keycode));
|
|
||||||
append_char(')');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} break;
|
|
||||||
|
|
||||||
#if !defined(NO_ACTION_ONESHOT)
|
|
||||||
// One-shot mod OSM(mod) key.
|
|
||||||
case QK_ONE_SHOT_MOD ... QK_ONE_SHOT_MOD_MAX:
|
|
||||||
append_P(PSTR("OSM("));
|
|
||||||
append_5_bit_mods(QK_ONE_SHOT_MOD_GET_MODS(keycode));
|
|
||||||
append_char(')');
|
|
||||||
return;
|
|
||||||
#endif // !defined(NO_ACTION_ONESHOT)
|
|
||||||
|
|
||||||
// Various layer switch keys.
|
|
||||||
case QK_LAYER_TAP ... QK_LAYER_TAP_MAX: // Layer-tap LT(layer,kc) key.
|
|
||||||
append_P(PSTR("LT("));
|
|
||||||
append_number(QK_LAYER_TAP_GET_LAYER(keycode), 10);
|
|
||||||
append_char(',');
|
|
||||||
append_keycode(QK_LAYER_TAP_GET_TAP_KEYCODE(keycode));
|
|
||||||
append_char(')');
|
|
||||||
return;
|
|
||||||
|
|
||||||
case QK_LAYER_MOD ... QK_LAYER_MOD_MAX: // LM(layer,mod) key.
|
|
||||||
append_P(PSTR("LM("));
|
|
||||||
append_number(QK_LAYER_MOD_GET_LAYER(keycode), 10);
|
|
||||||
append_char(',');
|
|
||||||
append_5_bit_mods(QK_LAYER_MOD_GET_MODS(keycode));
|
|
||||||
append_char(')');
|
|
||||||
return;
|
|
||||||
|
|
||||||
case QK_TO ... QK_TO_MAX: // TO(layer) key.
|
|
||||||
append_unary_keycode(PSTR("TO"),
|
|
||||||
number_string(QK_TO_GET_LAYER(keycode), 10));
|
|
||||||
return;
|
|
||||||
|
|
||||||
case QK_MOMENTARY ... QK_MOMENTARY_MAX: // MO(layer) key.
|
|
||||||
append_unary_keycode(PSTR("MO"),
|
|
||||||
number_string(QK_MOMENTARY_GET_LAYER(keycode), 10));
|
|
||||||
return;
|
|
||||||
|
|
||||||
case QK_DEF_LAYER ... QK_DEF_LAYER_MAX: // DF(layer) key.
|
|
||||||
append_unary_keycode(PSTR("DF"),
|
|
||||||
number_string(QK_DEF_LAYER_GET_LAYER(keycode), 10));
|
|
||||||
return;
|
|
||||||
|
|
||||||
case QK_TOGGLE_LAYER ... QK_TOGGLE_LAYER_MAX: // TG(layer) key.
|
|
||||||
append_unary_keycode(PSTR("TG"),
|
|
||||||
number_string(QK_TOGGLE_LAYER_GET_LAYER(keycode), 10));
|
|
||||||
return;
|
|
||||||
|
|
||||||
#if !defined(NO_ACTION_ONESHOT)
|
|
||||||
case QK_ONE_SHOT_LAYER ... QK_ONE_SHOT_LAYER_MAX: // OSL(layer) key.
|
|
||||||
append_unary_keycode(PSTR("OSL"),
|
|
||||||
number_string(QK_ONE_SHOT_LAYER_GET_LAYER(keycode), 10));
|
|
||||||
return;
|
|
||||||
#endif // !defined(NO_ACTION_ONESHOT)
|
|
||||||
|
|
||||||
case QK_LAYER_TAP_TOGGLE ... QK_LAYER_TAP_TOGGLE_MAX: // TT(layer) key.
|
|
||||||
append_unary_keycode(PSTR("TT"),
|
|
||||||
number_string(QK_LAYER_TAP_TOGGLE_GET_LAYER(keycode), 10));
|
|
||||||
return;
|
|
||||||
|
|
||||||
// PDF(layer) key.
|
|
||||||
case QK_PERSISTENT_DEF_LAYER ... QK_PERSISTENT_DEF_LAYER_MAX:
|
|
||||||
append_unary_keycode(PSTR("PDF"),
|
|
||||||
number_string(QK_PERSISTENT_DEF_LAYER_GET_LAYER(keycode), 10));
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Mod-tap MT(mod,kc) key. This implementation formats the MT keys where
|
|
||||||
// one modifier is applied. For MT keys with multiple modifiers, the mod
|
|
||||||
// arg is written numerically as a hex code.
|
|
||||||
case QK_MOD_TAP ... QK_MOD_TAP_MAX: {
|
|
||||||
const uint8_t mods = QK_MOD_TAP_GET_MODS(keycode);
|
|
||||||
const bool is_rhs = mods > 15;
|
|
||||||
const uint8_t csag = mods & 15;
|
|
||||||
if (csag != 0 && (csag & (csag - 1)) == 0) { // One mod is set.
|
|
||||||
append_char(is_rhs ? 'R' : 'L');
|
|
||||||
append_P(&mod_names[4 * biton(csag)]);
|
|
||||||
append_P(PSTR("_T("));
|
|
||||||
} else if (mods == MOD_HYPR) {
|
|
||||||
append_P(PSTR("HYPR_T("));
|
|
||||||
} else if (mods == MOD_MEH) {
|
|
||||||
append_P(PSTR("MEH_T("));
|
|
||||||
} else {
|
|
||||||
append_P(PSTR("MT("));
|
|
||||||
append_number(mods, 16);
|
|
||||||
append_char(',');
|
|
||||||
}
|
|
||||||
append_keycode(QK_MOD_TAP_GET_TAP_KEYCODE(keycode));
|
|
||||||
append_char(')');
|
|
||||||
} return;
|
|
||||||
|
|
||||||
case QK_TAP_DANCE ... QK_TAP_DANCE_MAX: // Tap dance TD(i) key.
|
|
||||||
append_unary_keycode(PSTR("TD"),
|
|
||||||
number_string(QK_TAP_DANCE_GET_INDEX(keycode), 10));
|
|
||||||
return;
|
|
||||||
|
|
||||||
#ifdef UNICODE_ENABLE
|
|
||||||
case QK_UNICODE ... QK_UNICODE_MAX: // Unicode UC(codepoint) key.
|
|
||||||
append_unary_keycode(PSTR("UC"),
|
|
||||||
number_string(QK_UNICODE_GET_CODE_POINT(keycode), 16));
|
|
||||||
return;
|
|
||||||
#elif defined(UNICODEMAP_ENABLE)
|
|
||||||
case QK_UNICODEMAP ... QK_UNICODEMAP_MAX: // Unicode Map UM(i) key.
|
|
||||||
append_unary_keycode(PSTR("UM"),
|
|
||||||
number_string(QK_UNICODEMAP_GET_INDEX(keycode), 10));
|
|
||||||
return;
|
|
||||||
|
|
||||||
case QK_UNICODEMAP_PAIR ... QK_UNICODEMAP_PAIR_MAX: { // UP(i,j) key.
|
|
||||||
const uint8_t i = QK_UNICODEMAP_PAIR_GET_UNSHIFTED_INDEX(keycode);
|
|
||||||
const uint8_t j = QK_UNICODEMAP_PAIR_GET_SHIFTED_INDEX(keycode);
|
|
||||||
append_P(PSTR("UP("));
|
|
||||||
append_number(i, 10);
|
|
||||||
append_char(',');
|
|
||||||
append_number(j, 10);
|
|
||||||
append_char(')');
|
|
||||||
} return;
|
|
||||||
#endif
|
|
||||||
#ifdef MOUSEKEY_ENABLE
|
|
||||||
case MS_BTN1 ... MS_BTN8: // Mouse button keycode.
|
|
||||||
append_P(PSTR("MS_BTN"));
|
|
||||||
append_number(keycode - (MS_BTN1 - 1), 10);
|
|
||||||
return;
|
|
||||||
#endif // MOUSEKEY_ENABLE
|
|
||||||
#ifdef SWAP_HANDS_ENABLE
|
|
||||||
case QK_SWAP_HANDS ... QK_SWAP_HANDS_MAX: // Swap Hands SH_T(kc) key.
|
|
||||||
if (!IS_SWAP_HANDS_KEYCODE(keycode)) {
|
|
||||||
append_P(PSTR("SH_T("));
|
|
||||||
append_keycode(QK_SWAP_HANDS_GET_TAP_KEYCODE(keycode));
|
|
||||||
append_char(')');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
#endif // SWAP_HANDS_ENABLE
|
|
||||||
|
|
||||||
case KB_KEYCODE_RANGE: // Keyboard range keycode.
|
|
||||||
append_P(PSTR("QK_KB_"));
|
|
||||||
append_number(keycode - QK_KB_0, 10);
|
|
||||||
return;
|
|
||||||
|
|
||||||
case USER_KEYCODE_RANGE: // User range keycode.
|
|
||||||
append_P(PSTR("QK_USER_"));
|
|
||||||
append_number(keycode - QK_USER_0, 10);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// clang-format on
|
|
||||||
|
|
||||||
append_number(keycode, 16); // Fallback: write keycode as hex value.
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* get_keycode_string(uint16_t keycode) {
|
|
||||||
buffer_len = 0;
|
|
||||||
buffer[0] = '\0';
|
|
||||||
append_keycode(keycode);
|
|
||||||
return buffer;
|
|
||||||
}
|
|
|
@ -1,134 +0,0 @@
|
||||||
// Copyright 2024-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.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @file keycode_string.h
|
|
||||||
* @brief Keycode String community model: format keycodes as readable strings.
|
|
||||||
*
|
|
||||||
* Example use: Output the keycode and other event information to debug logging.
|
|
||||||
* This supposes the Console is enabled (see https://docs.qmk.fm/faq_debug).
|
|
||||||
*
|
|
||||||
* bool process_record_user(uint16_t keycode, keyrecord_t* record) {
|
|
||||||
* const uint8_t layer = read_source_layers_cache(record->event.key);
|
|
||||||
* xprintf("L%-2u: %-7s kc=%s\n",
|
|
||||||
* layer, record->event.pressed ? "press" : "release",
|
|
||||||
* get_keycode_string(keycode));
|
|
||||||
*
|
|
||||||
* // Macros...
|
|
||||||
* return true;
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* For full documentation, see
|
|
||||||
* <https://getreuer.info/posts/keyboards/keycode-string>
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Formats a QMK keycode as a human-readable string.
|
|
||||||
*
|
|
||||||
* Given a keycode, like `KC_A`, this function returns a formatted string, like
|
|
||||||
* "KC_A". This is useful for debugging and diagnostics so that keys are more
|
|
||||||
* easily identified than they would be by raw numerical codes.
|
|
||||||
*
|
|
||||||
* @note The returned char* string should be used right away. The string memory
|
|
||||||
* is reused and will be overwritten by the next call to `get_keycode_string()`.
|
|
||||||
*
|
|
||||||
* Many common QMK keycodes are understood by this function, but not all.
|
|
||||||
* Recognized keycodes include:
|
|
||||||
*
|
|
||||||
* - Most basic keycodes, including letters `KC_A` - `KC_Z`, digits `KC_0` -
|
|
||||||
* `KC_9`, function keys `KC_F1` - `KC_F24`, and modifiers like `KC_LSFT`.
|
|
||||||
*
|
|
||||||
* - Modified basic keycodes, like `S(KC_1)` (Shift + 1 = !).
|
|
||||||
*
|
|
||||||
* - `MO`, `TO`, `TG`, `TT`, `DF`, `PDF`, `OSL`, `LM(layer,mod)`,
|
|
||||||
* `LT(layer,kc)` layer switches.
|
|
||||||
*
|
|
||||||
* - One-shot mod `OSM(mod)` keycodes.
|
|
||||||
*
|
|
||||||
* - Mod-tap `MT(mod, kc)` keycodes.
|
|
||||||
*
|
|
||||||
* - Tap dance keycodes `TD(i)`.
|
|
||||||
*
|
|
||||||
* - Swap hands keycodes `SH_T(kc)`, `SH_TOGG`, etc.
|
|
||||||
*
|
|
||||||
* - Unicode `UC(codepoint)` and Unicode Map `UM(i)` and `UP(i,j)` keycodes.
|
|
||||||
*
|
|
||||||
* - Keyboard range keycodes `QK_KB_*`.
|
|
||||||
*
|
|
||||||
* - User range (SAFE_RANGE) keycodes `QK_USER_*`.
|
|
||||||
*
|
|
||||||
* Keycodes involving mods like `OSM`, `LM`, `MT` are fully supported only where
|
|
||||||
* a single mod is applied.
|
|
||||||
*
|
|
||||||
* Unrecognized keycodes are printed numerically as hex values like `0x1ABC`.
|
|
||||||
*
|
|
||||||
* Optionally, use `KEYCODE_STRING_NAMES_USER` to define names for additional
|
|
||||||
* keycodes or override how any of the above are formatted.
|
|
||||||
*
|
|
||||||
* @param keycode QMK keycode.
|
|
||||||
* @return Stringified keycode.
|
|
||||||
*/
|
|
||||||
const char* get_keycode_string(uint16_t keycode);
|
|
||||||
|
|
||||||
#define KEYCODE_STRING_NAME(kc) {(kc), #kc}
|
|
||||||
|
|
||||||
/** Defines a human-readable name for a keycode. */
|
|
||||||
typedef struct {
|
|
||||||
uint16_t keycode;
|
|
||||||
const char* name;
|
|
||||||
} keycode_string_name_t;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Defines names for additional keycodes for `get_keycode_string()`.
|
|
||||||
*
|
|
||||||
* Define `KEYCODE_STRING_NAMES_USER` in your keymap.c to add names for
|
|
||||||
* additional keycodes to `keycode_string()`. This table may also be used to
|
|
||||||
* override how `keycode_string()` formats a keycode. For example, supposing
|
|
||||||
* keymap.c defines `MYMACRO1` and `MYMACRO2` as custom keycodes:
|
|
||||||
*
|
|
||||||
* KEYCODE_STRING_NAMES_USER(
|
|
||||||
* KEYCODE_STRING_NAME(MYMACRO1),
|
|
||||||
* KEYCODE_STRING_NAME(MYMACRO2),
|
|
||||||
* KEYCODE_STRING_NAME(KC_EXLM),
|
|
||||||
* );
|
|
||||||
*
|
|
||||||
* The above defines names for `MYMACRO1` and `MYMACRO2`, and overrides
|
|
||||||
* `KC_EXLM` to format as "KC_EXLM" instead of the default "S(KC_1)".
|
|
||||||
*/
|
|
||||||
#define KEYCODE_STRING_NAMES_USER(...) \
|
|
||||||
static const keycode_string_name_t keycode_string_names_user[] = \
|
|
||||||
{__VA_ARGS__}; \
|
|
||||||
uint16_t keycode_string_names_size_user = \
|
|
||||||
sizeof(keycode_string_names_user) / sizeof(keycode_string_name_t); \
|
|
||||||
const keycode_string_name_t* keycode_string_names_data_user = \
|
|
||||||
keycode_string_names_user
|
|
||||||
|
|
||||||
/** Helper to define a keycode_string_name_t. */
|
|
||||||
#define KEYCODE_STRING_NAME(kc) {(kc), #kc}
|
|
||||||
// clang-format on
|
|
||||||
|
|
||||||
extern const keycode_string_name_t* keycode_string_names_data_user;
|
|
||||||
extern uint16_t keycode_string_names_size_user;
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
}
|
|
||||||
#endif
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"module_name": "Keycode String",
|
|
||||||
"maintainer": "getreuer",
|
|
||||||
"url": "https://getreuer.info/posts/keyboards/keycode-string"
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
# Mouse Turbo Click
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<tr><td><b>Module</b></td><td><tt>getreuer/mouse_turbo_click</tt></td></tr>
|
|
||||||
<tr><td><b>Version</b></td><td>2025-03-07</td></tr>
|
|
||||||
<tr><td><b>Maintainer</b></td><td>Pascal Getreuer (@getreuer)</td></tr>
|
|
||||||
<tr><td><b>License</b></td><td><a href="../LICENSE.txt">Apache 2.0</a></td></tr>
|
|
||||||
<tr><td><b>Documentation</b></td><td>
|
|
||||||
<a href="https://getreuer.info/posts/keyboards/mouse-turbo-click">https://getreuer.info/posts/keyboards/mouse-turbo-click</a>
|
|
||||||
</td></tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
This is a community module adaptation of [Mouse Turbo
|
|
||||||
Click](https://getreuer.info/posts/keyboards/mouse-turbo-click) to click the
|
|
||||||
mouse rapidly.
|
|
||||||
|
|
||||||
Add the following to your `keymap.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"modules": ["getreuer/mouse_turbo_click"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Then use the keycode `TURBO` somewhere in your layout. This key clicks the mouse
|
|
||||||
rapidly, implemented using mouse keys and a periodic callback function:
|
|
||||||
|
|
||||||
* Pressing and holding the Turbo Click button sends rapid mouse clicks,
|
|
||||||
about 12 clicks per second.
|
|
||||||
|
|
||||||
* Quickly double tapping the Turbo Click button "locks" it. Rapid mouse
|
|
||||||
clicks are sent until the Turbo Click button is tapped again.
|
|
||||||
|
|
||||||
Optionally, the click rate and keycode can be customized. See the [Mouse Turbo
|
|
||||||
Click documentation](https://getreuer.info/posts/keyboards/mouse-turbo-click)
|
|
||||||
for details.
|
|
||||||
|
|
|
@ -1,121 +0,0 @@
|
||||||
// Copyright 2022-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.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @file mouse_turbo_click.c
|
|
||||||
* @brief Mouse Turbo Click community module: click the mouse rapidly
|
|
||||||
*
|
|
||||||
* This module implements a "Turbo Click" button that clicks the mouse rapidly,
|
|
||||||
* implemented using mouse keys and a periodic callback function:
|
|
||||||
*
|
|
||||||
* * Pressing and holding the Turbo Click button sends rapid mouse clicks,
|
|
||||||
* about 12 clicks per second.
|
|
||||||
*
|
|
||||||
* * Quickly double tapping the Turbo Click button "locks" it. Rapid mouse
|
|
||||||
* clicks are sent until the Turbo Click button is tapped again.
|
|
||||||
*
|
|
||||||
* For full documentation, see
|
|
||||||
* <https://getreuer.info/posts/keyboards/mouse-turbo-click>
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "quantum.h"
|
|
||||||
|
|
||||||
ASSERT_COMMUNITY_MODULES_MIN_API_VERSION(1, 0, 0);
|
|
||||||
|
|
||||||
// The keycode to be repeatedly clicked, `MS_BTN1` mouse button 1 by default.
|
|
||||||
#ifndef MOUSE_TURBO_CLICK_KEY
|
|
||||||
#define MOUSE_TURBO_CLICK_KEY MS_BTN1
|
|
||||||
#endif // MOUSE_TURBO_CLICK_KEY
|
|
||||||
|
|
||||||
// The click period in milliseconds. For instance a period of 200 ms would be 5
|
|
||||||
// clicks per second. Smaller period implies faster clicking.
|
|
||||||
//
|
|
||||||
// WARNING: The keyboard might become unresponsive if the period is too small.
|
|
||||||
// I suggest setting this no smaller than 10.
|
|
||||||
#ifndef MOUSE_TURBO_CLICK_PERIOD
|
|
||||||
#define MOUSE_TURBO_CLICK_PERIOD 80
|
|
||||||
#endif // MOUSE_TURBO_CLICK_PERIOD
|
|
||||||
|
|
||||||
static deferred_token click_token = INVALID_DEFERRED_TOKEN;
|
|
||||||
static bool click_registered = false;
|
|
||||||
|
|
||||||
// Callback used with deferred execution. It alternates between registering and
|
|
||||||
// unregistering (pressing and releasing) `MOUSE_TURBO_CLICK_KEY`.
|
|
||||||
static uint32_t turbo_click_callback(uint32_t trigger_time, void* cb_arg) {
|
|
||||||
if (click_registered) {
|
|
||||||
unregister_code16(MOUSE_TURBO_CLICK_KEY);
|
|
||||||
click_registered = false;
|
|
||||||
} else {
|
|
||||||
click_registered = true;
|
|
||||||
register_code16(MOUSE_TURBO_CLICK_KEY);
|
|
||||||
}
|
|
||||||
return MOUSE_TURBO_CLICK_PERIOD / 2; // Execute again in half a period.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Starts Turbo Click, begins the `turbo_click_callback()` callback.
|
|
||||||
static void turbo_click_start(void) {
|
|
||||||
if (click_token == INVALID_DEFERRED_TOKEN) {
|
|
||||||
uint32_t next_delay_ms = turbo_click_callback(0, NULL);
|
|
||||||
click_token = defer_exec(next_delay_ms, turbo_click_callback, NULL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stops Turbo Click, cancels the callback.
|
|
||||||
static void turbo_click_stop(void) {
|
|
||||||
if (click_token != INVALID_DEFERRED_TOKEN) {
|
|
||||||
cancel_deferred_exec(click_token);
|
|
||||||
click_token = INVALID_DEFERRED_TOKEN;
|
|
||||||
if (click_registered) {
|
|
||||||
// If `MOUSE_TURBO_CLICK_KEY` is currently registered, release it.
|
|
||||||
unregister_code16(MOUSE_TURBO_CLICK_KEY);
|
|
||||||
click_registered = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool process_record_mouse_turbo_click(uint16_t keycode, keyrecord_t* record) {
|
|
||||||
static bool locked = false;
|
|
||||||
static bool tapped = false;
|
|
||||||
static uint16_t tap_timer = 0;
|
|
||||||
|
|
||||||
if (keycode == MOUSE_TURBO_CLICK) {
|
|
||||||
if (record->event.pressed) { // Turbo Click key was pressed.
|
|
||||||
if (tapped && !timer_expired(record->event.time, tap_timer)) {
|
|
||||||
// If the key was recently tapped, lock turbo click.
|
|
||||||
locked = true;
|
|
||||||
} else if (locked) {
|
|
||||||
// Otherwise if currently locked, unlock and stop.
|
|
||||||
locked = false;
|
|
||||||
tapped = false;
|
|
||||||
turbo_click_stop();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// Set that the first tap occurred in a potential double tap.
|
|
||||||
tapped = true;
|
|
||||||
tap_timer = record->event.time + TAPPING_TERM;
|
|
||||||
|
|
||||||
turbo_click_start();
|
|
||||||
} else if (!locked) {
|
|
||||||
// If not currently locked, stop on key release.
|
|
||||||
turbo_click_stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
// On an event with any other key, reset the double tap state.
|
|
||||||
tapped = false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
{
|
|
||||||
"module_name": "Mouse Turbo Click",
|
|
||||||
"maintainer": "getreuer",
|
|
||||||
"url": "https://getreuer.info/posts/keyboards/mouse-turbo-click",
|
|
||||||
"features": {
|
|
||||||
"deferred_exec": true,
|
|
||||||
"mousekeys": true
|
|
||||||
},
|
|
||||||
"keycodes": [
|
|
||||||
{
|
|
||||||
"key": "MOUSE_TURBO_CLICK",
|
|
||||||
"aliases": ["TURBO"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
# Orbital Mouse
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<tr><td><b>Module</b></td><td><tt>getreuer/orbital_mouse</tt></td></tr>
|
|
||||||
<tr><td><b>Version</b></td><td>2025-03-07</td></tr>
|
|
||||||
<tr><td><b>Maintainer</b></td><td>Pascal Getreuer (@getreuer)</td></tr>
|
|
||||||
<tr><td><b>License</b></td><td><a href="../LICENSE.txt">Apache 2.0</a></td></tr>
|
|
||||||
<tr><td><b>Documentation</b></td><td>
|
|
||||||
<a href="https://getreuer.info/posts/keyboards/orbital-mouse">https://getreuer.info/posts/keyboards/orbital-mouse</a>
|
|
||||||
</td></tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
This is a community module adaptation of [Orbital
|
|
||||||
Mouse](https://getreuer.info/posts/keyboards/orbital-mouse) for a polar approach
|
|
||||||
to mouse control. Orbital Mouse replaces QMK Mouse Keys. The pointer moves
|
|
||||||
according to a heading direction. Two keys move forward and backward along that
|
|
||||||
direction while another two keys steer.
|
|
||||||
|
|
||||||
Add the following to your `keymap.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"modules": ["getreuer/orbital_mouse"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Then use the "`OM_*`" Orbital Mouse keycodes in your layout.
|
|
||||||
|
|
||||||
| Keycode | Aliases | Description |
|
|
||||||
|-------------|-------------|------------------------------------------------|
|
|
||||||
| `OM_U` | `MS_UP` | Move forward. |
|
|
||||||
| `OM_D` | `MS_DOWN` | Move backward. |
|
|
||||||
| `OM_L` | `MS_LEFT` | Steer left (counter-clockwise). |
|
|
||||||
| `OM_R` | `MS_RGHT` | Steer right (clockwise). |
|
|
||||||
| `OM_BTN`*n* | `MS_BTN`*n* | Press mouse button *n*, for *n* = 1, ..., 8. |
|
|
||||||
| `OM_W_U` | `MS_WHLU` | Mouse wheel up. |
|
|
||||||
| `OM_W_D` | `MS_WHLD` | Mouse wheel down. |
|
|
||||||
| `OM_W_L` | `MS_WHLL` | Mouse wheel left. |
|
|
||||||
| `OM_W_R` | `MS_WHLR` | Mouse wheel right. |
|
|
||||||
| `OM_SLOW` | | Slow mode. Movement is slower while held. |
|
|
||||||
| `OM_SEL`*n* | | Select mouse button *n*, for *n* = 1, ..., 8. |
|
|
||||||
| `OM_BTNS` | | Press the selected mouse button. |
|
|
||||||
| `OM_DBLS` | | Double click the selected mouse button. |
|
|
||||||
| `OM_HLDS` | | Hold the selected mouse button. |
|
|
||||||
| `OM_RELS` | | Release the selected mouse button. |
|
|
||||||
|
|
||||||
A suggested right-handed layout for Orbital Mouse control is
|
|
||||||
|
|
||||||
OM_W_U , OM_BTNS, OM_U , OM_DBLS, _______,
|
|
||||||
OM_W_D , OM_L , OM_D , OM_R , OM_SLOW,
|
|
||||||
OM_RELS, OM_HLDS, OM_SEL1, OM_SEL2, OM_SEL3,
|
|
||||||
|
|
||||||
Optionally, there are config options to customize Sentence Case. See the
|
|
||||||
[Orbital Mouse
|
|
||||||
documentation](https://getreuer.info/posts/keyboards/orbital-mouse) for details.
|
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
// 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.
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "orbital_mouse.h"
|
|
||||||
|
|
|
@ -1,364 +0,0 @@
|
||||||
// Copyright 2023-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.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @file orbital_mouse.c
|
|
||||||
* @brief Orbital Mouse community module implementation
|
|
||||||
*
|
|
||||||
* For documentation, see
|
|
||||||
* <https://getreuer.info/posts/keyboards/orbital-mouse>
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "orbital_mouse.h"
|
|
||||||
|
|
||||||
#ifndef ORBITAL_MOUSE_RADIUS
|
|
||||||
#define ORBITAL_MOUSE_RADIUS 36
|
|
||||||
#endif // ORBITAL_MOUSE_RADIUS
|
|
||||||
#ifndef ORBITAL_MOUSE_SLOW_MOVE_FACTOR
|
|
||||||
#define ORBITAL_MOUSE_SLOW_MOVE_FACTOR 0.333
|
|
||||||
#endif // ORBITAL_MOUSE_SLOW_MOVE_FACTOR
|
|
||||||
#ifndef ORBITAL_MOUSE_SLOW_TURN_FACTOR
|
|
||||||
#define ORBITAL_MOUSE_SLOW_TURN_FACTOR 0.5
|
|
||||||
#endif // ORBITAL_MOUSE_SLOW_TURN_FACTOR
|
|
||||||
#ifndef ORBITAL_MOUSE_WHEEL_SPEED
|
|
||||||
#define ORBITAL_MOUSE_WHEEL_SPEED 0.2
|
|
||||||
#endif // ORBITAL_MOUSE_WHEEL_SPEED
|
|
||||||
#ifndef ORBITAL_MOUSE_DBL_DELAY_MS
|
|
||||||
#define ORBITAL_MOUSE_DBL_DELAY_MS 50
|
|
||||||
#endif // ORBITAL_MOUSE_DBL_DELAY_MS
|
|
||||||
#ifndef ORBITAL_MOUSE_SPEED_CURVE
|
|
||||||
#define ORBITAL_MOUSE_SPEED_CURVE \
|
|
||||||
{24, 24, 24, 32, 58, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66}
|
|
||||||
// | | | | |
|
|
||||||
// t = 0.000 1.024 2.048 3.072 3.840 s
|
|
||||||
#endif // ORBITAL_MOUSE_SPEED_CURVE
|
|
||||||
#ifndef ORBITAL_MOUSE_INTERVAL_MS
|
|
||||||
#define ORBITAL_MOUSE_INTERVAL_MS 16
|
|
||||||
#endif // ORBITAL_MOUSE_INTERVAL_MS
|
|
||||||
|
|
||||||
#if !(0 <= ORBITAL_MOUSE_RADIUS && ORBITAL_MOUSE_RADIUS <= 63)
|
|
||||||
#error "Invalid ORBITAL_MOUSE_RADIUS. Value must be in [0, 63]."
|
|
||||||
#endif
|
|
||||||
|
|
||||||
enum {
|
|
||||||
/** Number of distinct angles. */
|
|
||||||
NUM_ANGLES = 64,
|
|
||||||
/** Number of intervals in speed curve table. */
|
|
||||||
NUM_SPEED_CURVE_INTERVALS = 16,
|
|
||||||
/** Orbit radius in pixels as a Q6.2 value. */
|
|
||||||
RADIUS_Q6_2 = (uint8_t)((ORBITAL_MOUSE_RADIUS) * 4 + 0.5),
|
|
||||||
/** Slow mode movement speed factor as a Q.8 value. */
|
|
||||||
SLOW_MOVE_FACTOR_Q_8 = (ORBITAL_MOUSE_SLOW_MOVE_FACTOR) < 0.99
|
|
||||||
? ((uint8_t)((ORBITAL_MOUSE_SLOW_MOVE_FACTOR) * 256 + 0.5)) : 255,
|
|
||||||
/** Slow mode turn speed factor as a Q.8 value. */
|
|
||||||
SLOW_TURN_FACTOR_Q_8 = (ORBITAL_MOUSE_SLOW_TURN_FACTOR) < 0.99
|
|
||||||
? ((uint8_t)((ORBITAL_MOUSE_SLOW_TURN_FACTOR) * 256 + 0.5)) : 255,
|
|
||||||
/** Wheel speed in steps/frame as a Q2.6 value. */
|
|
||||||
WHEEL_SPEED_Q2_6 = (ORBITAL_MOUSE_WHEEL_SPEED) < 3.99
|
|
||||||
? ((uint8_t)((ORBITAL_MOUSE_WHEEL_SPEED) * 64 + 0.5)) : 255,
|
|
||||||
/** Double click delay in units of intervals. */
|
|
||||||
DOUBLE_CLICK_DELAY_INTERVALS =
|
|
||||||
(ORBITAL_MOUSE_DBL_DELAY_MS) / (ORBITAL_MOUSE_INTERVAL_MS),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Masks for the `held_keys` bitfield.
|
|
||||||
enum {
|
|
||||||
HELD_U = 1,
|
|
||||||
HELD_D = 2,
|
|
||||||
HELD_L = 4,
|
|
||||||
HELD_R = 8,
|
|
||||||
HELD_W_U = 16,
|
|
||||||
HELD_W_D = 32,
|
|
||||||
HELD_W_L = 64,
|
|
||||||
HELD_W_R = 128,
|
|
||||||
};
|
|
||||||
|
|
||||||
static const uint8_t init_speed_curve[NUM_SPEED_CURVE_INTERVALS] =
|
|
||||||
ORBITAL_MOUSE_SPEED_CURVE;
|
|
||||||
static struct {
|
|
||||||
report_mouse_t report;
|
|
||||||
// Current speed curve, should point to a table of 16 values.
|
|
||||||
const uint8_t* speed_curve;
|
|
||||||
// Time when the Orbital Mouse task function should next run.
|
|
||||||
uint16_t timer;
|
|
||||||
// Fractional displacement of the cursor as Q7.8 values.
|
|
||||||
int16_t x;
|
|
||||||
int16_t y;
|
|
||||||
// Fractional displacement of the mouse wheel as Q9.6 values.
|
|
||||||
int16_t wheel_x;
|
|
||||||
int16_t wheel_y;
|
|
||||||
// Current cursor movement speed as a Q9.6 value.
|
|
||||||
int16_t speed;
|
|
||||||
// Bitfield tracking which movement keys are currently held.
|
|
||||||
uint8_t held_keys;
|
|
||||||
// Cursor movement time, counted in number of intervals.
|
|
||||||
uint8_t move_t;
|
|
||||||
// Cursor movement direction, 1 => forward, -1 => backward.
|
|
||||||
int8_t move_dir;
|
|
||||||
// Steering direction, 1 => counter-clockwise, -1 => clockwise.
|
|
||||||
int8_t steer_dir;
|
|
||||||
// Mouse wheel movement directions.
|
|
||||||
int8_t wheel_x_dir;
|
|
||||||
int8_t wheel_y_dir;
|
|
||||||
// Heading direction as a Q6.8 value, with 0 => up, 16 * 256 => left, etc.
|
|
||||||
uint16_t angle;
|
|
||||||
// Selected mouse button as a base-0 index.
|
|
||||||
uint8_t selected_button;
|
|
||||||
// Tracks double click action.
|
|
||||||
uint8_t double_click_frame;
|
|
||||||
// When true, movement and turning are slower.
|
|
||||||
bool slow;
|
|
||||||
} state = {.speed_curve = init_speed_curve};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fixed-point sine with specified amplitude and phase.
|
|
||||||
*
|
|
||||||
* @param amplitude Nonnegative Q6.2 value.
|
|
||||||
* @param phase Value in [0, 63].
|
|
||||||
* @returns Result as a Q6.8 value.
|
|
||||||
*/
|
|
||||||
static int16_t scaled_sin(uint8_t amplitude, uint8_t phase) {
|
|
||||||
// Look up table covers half a cycle of a sine wave.
|
|
||||||
static const uint8_t lut[NUM_ANGLES / 2] PROGMEM = {
|
|
||||||
0, 25, 50, 74, 98, 120, 142, 162, 180, 197, 212,
|
|
||||||
225, 236, 244, 250, 254, 255, 254, 250, 244, 236, 225,
|
|
||||||
212, 197, 180, 162, 142, 120, 98, 74, 50, 25};
|
|
||||||
// amplitude Q6.2 and lut is Q0.8. Shift down by 2 so that the result is Q6.8.
|
|
||||||
int16_t value = (int16_t)(((uint16_t)amplitude
|
|
||||||
* pgm_read_byte(lut + (phase & (NUM_ANGLES / 2 - 1))) + 2) >> 2);
|
|
||||||
return ((NUM_ANGLES / 2) & phase) == 0 ? value : -value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Computes fixed-point cosine. */
|
|
||||||
static int16_t scaled_cos(uint8_t amplitude, uint8_t phase) {
|
|
||||||
return scaled_sin(amplitude, phase + (NUM_ANGLES / 4));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Wakes the Orbital Mouse task. */
|
|
||||||
static void wake_orbital_mouse_task(void) {
|
|
||||||
if (!state.timer) {
|
|
||||||
state.timer = timer_read() | 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Converts a keycode to a mask for the `held_keys` bitfield. */
|
|
||||||
static uint8_t keycode_to_held_mask(uint16_t keycode) {
|
|
||||||
switch (keycode) {
|
|
||||||
case OM_U: return HELD_U;
|
|
||||||
case OM_D: return HELD_D;
|
|
||||||
case OM_L: return HELD_L;
|
|
||||||
case OM_R: return HELD_R;
|
|
||||||
case OM_W_U: return HELD_W_U;
|
|
||||||
case OM_W_D: return HELD_W_D;
|
|
||||||
case OM_W_L: return HELD_W_L;
|
|
||||||
case OM_W_R: return HELD_W_R;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Presses mouse button i, with i being a base-0 index. */
|
|
||||||
static void press_mouse_button(uint8_t i, bool pressed) {
|
|
||||||
if (i >= 8) {
|
|
||||||
i = state.selected_button;
|
|
||||||
}
|
|
||||||
const uint8_t mask = 1 << i;
|
|
||||||
if (pressed) {
|
|
||||||
state.report.buttons |= mask;
|
|
||||||
} else {
|
|
||||||
state.report.buttons &= ~mask;
|
|
||||||
}
|
|
||||||
wake_orbital_mouse_task();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Selects mouse button i. */
|
|
||||||
static void select_mouse_button(uint8_t i) {
|
|
||||||
state.selected_button = i;
|
|
||||||
// Reset buttons and double-click state when switching selection.
|
|
||||||
state.report.buttons = 0;
|
|
||||||
state.double_click_frame = 0;
|
|
||||||
wake_orbital_mouse_task();
|
|
||||||
}
|
|
||||||
|
|
||||||
static int8_t get_dir_from_held_keys(uint8_t bit_shift) {
|
|
||||||
static const int8_t dir[4] = {0, 1, -1, 0};
|
|
||||||
return dir[(state.held_keys >> bit_shift) & 3];
|
|
||||||
}
|
|
||||||
|
|
||||||
void set_orbital_mouse_speed_curve(const uint8_t* speed_curve) {
|
|
||||||
state.speed_curve = (speed_curve != NULL) ? speed_curve : init_speed_curve;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t get_orbital_mouse_angle(void) {
|
|
||||||
return (state.angle >> 8) & (NUM_ANGLES - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void set_orbital_mouse_angle_fractional(uint16_t angle) {
|
|
||||||
state.x += scaled_sin(RADIUS_Q6_2, state.angle >> 8);
|
|
||||||
state.y += scaled_cos(RADIUS_Q6_2, state.angle >> 8);
|
|
||||||
state.angle = angle;
|
|
||||||
state.x -= scaled_sin(RADIUS_Q6_2, angle >> 8);
|
|
||||||
state.y -= scaled_cos(RADIUS_Q6_2, angle >> 8);
|
|
||||||
wake_orbital_mouse_task();
|
|
||||||
}
|
|
||||||
|
|
||||||
void set_orbital_mouse_angle(uint8_t angle) {
|
|
||||||
set_orbital_mouse_angle_fractional((uint16_t)angle << 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool process_record_orbital_mouse(uint16_t keycode, keyrecord_t* record) {
|
|
||||||
if (!(IS_MOUSE_KEYCODE(keycode) ||
|
|
||||||
(OM_SLOW <= keycode && keycode <= OM_SEL8))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t held_mask = keycode_to_held_mask(keycode);
|
|
||||||
if (held_mask != 0) {
|
|
||||||
// Update `held_keys` bitfield.
|
|
||||||
if (record->event.pressed) {
|
|
||||||
state.held_keys |= held_mask;
|
|
||||||
} else {
|
|
||||||
state.held_keys &= ~held_mask;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch (keycode) {
|
|
||||||
case OM_BTN1 ... OM_BTN8:
|
|
||||||
press_mouse_button(keycode - OM_BTN1, record->event.pressed);
|
|
||||||
return false;
|
|
||||||
case OM_BTNS:
|
|
||||||
press_mouse_button(255, record->event.pressed);
|
|
||||||
return false;
|
|
||||||
case OM_HLDS:
|
|
||||||
if (record->event.pressed) {
|
|
||||||
press_mouse_button(255, true);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
case OM_RELS:
|
|
||||||
if (record->event.pressed) {
|
|
||||||
press_mouse_button(255, false);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
case OM_DBLS:
|
|
||||||
if (record->event.pressed) {
|
|
||||||
state.double_click_frame = 1;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case OM_SLOW:
|
|
||||||
state.slow = record->event.pressed;
|
|
||||||
return false;
|
|
||||||
case OM_SEL1 ... OM_SEL8:
|
|
||||||
if (record->event.pressed) {
|
|
||||||
select_mouse_button(keycode - OM_SEL1);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update cursor movement direction.
|
|
||||||
const int8_t dir = get_dir_from_held_keys(0);
|
|
||||||
if (state.move_dir != dir) {
|
|
||||||
state.move_dir = dir;
|
|
||||||
state.move_t = 0;
|
|
||||||
}
|
|
||||||
// Update steering direction.
|
|
||||||
state.steer_dir = get_dir_from_held_keys(2);
|
|
||||||
// Update wheel movement.
|
|
||||||
state.wheel_y_dir = get_dir_from_held_keys(4);
|
|
||||||
state.wheel_x_dir = get_dir_from_held_keys(6);
|
|
||||||
wake_orbital_mouse_task();
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void housekeeping_task_orbital_mouse(void) {
|
|
||||||
const uint16_t now = timer_read();
|
|
||||||
if (!state.timer || !timer_expired(now, state.timer)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool active = false;
|
|
||||||
|
|
||||||
// Update position if moving.
|
|
||||||
if (state.move_dir) {
|
|
||||||
// Update speed, interpolated from speed_curve.
|
|
||||||
if (state.move_t <= 16 * (NUM_SPEED_CURVE_INTERVALS - 1)) {
|
|
||||||
if (state.move_t == 0) {
|
|
||||||
state.speed = (int16_t)state.speed_curve[0] * 16;
|
|
||||||
} else {
|
|
||||||
const uint8_t i = (state.move_t - 1) / 16;
|
|
||||||
state.speed += (int16_t)state.speed_curve[i + 1]
|
|
||||||
- (int16_t)state.speed_curve[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
++state.move_t;
|
|
||||||
}
|
|
||||||
// Round and cast from Q9.6 to Q6.2.
|
|
||||||
uint8_t speed = (state.speed + 8) / 16;
|
|
||||||
if (state.slow) {
|
|
||||||
speed = ((uint16_t)speed) * (1 + (uint16_t)SLOW_MOVE_FACTOR_Q_8) >> 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.x -= state.move_dir * scaled_sin(speed, state.angle >> 8);
|
|
||||||
state.y -= state.move_dir * scaled_cos(speed, state.angle >> 8);
|
|
||||||
active = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update heading angle if steering.
|
|
||||||
if (state.steer_dir) {
|
|
||||||
int16_t angle_step = state.slow ? SLOW_TURN_FACTOR_Q_8 : 256;
|
|
||||||
if (state.steer_dir == -1) {
|
|
||||||
angle_step = -angle_step;
|
|
||||||
}
|
|
||||||
set_orbital_mouse_angle_fractional(state.angle + angle_step);
|
|
||||||
active = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update mouse wheel if active.
|
|
||||||
if (state.wheel_x_dir || state.wheel_y_dir) {
|
|
||||||
state.wheel_x -= state.wheel_x_dir * WHEEL_SPEED_Q2_6;
|
|
||||||
state.wheel_y += state.wheel_y_dir * WHEEL_SPEED_Q2_6;
|
|
||||||
active = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update double click action.
|
|
||||||
if (state.double_click_frame) {
|
|
||||||
++state.double_click_frame;
|
|
||||||
const uint8_t mask = 1 << state.selected_button;
|
|
||||||
switch (state.double_click_frame) {
|
|
||||||
case 2:
|
|
||||||
case 3:
|
|
||||||
case 4 + DOUBLE_CLICK_DELAY_INTERVALS:
|
|
||||||
state.report.buttons ^= mask;
|
|
||||||
break;
|
|
||||||
case 5 + DOUBLE_CLICK_DELAY_INTERVALS:
|
|
||||||
state.report.buttons &= ~mask;
|
|
||||||
state.double_click_frame = 0;
|
|
||||||
}
|
|
||||||
active = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Schedule when task should run again, or go to sleep if inactive.
|
|
||||||
state.timer = active ? ((now + ORBITAL_MOUSE_INTERVAL_MS) | 1) : 0;
|
|
||||||
|
|
||||||
// Set whole part of movement deltas in report and retain fractional parts.
|
|
||||||
state.report.x = state.x / 256;
|
|
||||||
state.report.y = state.y / 256;
|
|
||||||
state.x -= (int16_t)state.report.x * 256;
|
|
||||||
state.y -= (int16_t)state.report.y * 256;
|
|
||||||
state.report.h = state.wheel_x / 64;
|
|
||||||
state.report.v = state.wheel_y / 64;
|
|
||||||
state.wheel_x -= (int16_t)state.report.h * 64;
|
|
||||||
state.wheel_y -= (int16_t)state.report.v * 64;
|
|
||||||
host_mouse_send(&state.report);
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,93 +0,0 @@
|
||||||
// Copyright 2023-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.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @file orbital_mouse.h
|
|
||||||
* @brief Orbital Mouse community module: a polar approach to mouse control.
|
|
||||||
*
|
|
||||||
* Orbital Mouse is a module that replaces QMK Mouse Keys. The pointer moves
|
|
||||||
* according to a heading direction. Two keys move forward and backward along
|
|
||||||
* that direction while another two keys steer.
|
|
||||||
*
|
|
||||||
* For documentation, see
|
|
||||||
* <https://getreuer.info/posts/keyboards/orbital-mouse>
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "quantum.h"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the pointer movement speed curve at run time.
|
|
||||||
*
|
|
||||||
* This function enables dynamically switching between multiple speed curves.
|
|
||||||
*
|
|
||||||
* @param speed_curve Pointer to an array of size 16. If NULL, the speed curve
|
|
||||||
* defined by ORBITAL_MOUSE_SPEED_CURVE is set.
|
|
||||||
*/
|
|
||||||
void set_orbital_mouse_speed_curve(const uint8_t* speed_curve);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the heading direction as a value in 0-63.
|
|
||||||
*
|
|
||||||
* Value 0 is up, and values increase in the counter-clockwise direction.
|
|
||||||
*
|
|
||||||
* 0 = up 32 = down
|
|
||||||
* 8 = up-left 40 = down-right
|
|
||||||
* 16 = left 48 = right
|
|
||||||
* 24 = down-left 56 = up-right
|
|
||||||
*/
|
|
||||||
uint8_t get_orbital_mouse_angle(void);
|
|
||||||
|
|
||||||
/** Sets the heading direction. */
|
|
||||||
void set_orbital_mouse_angle(uint8_t angle);
|
|
||||||
|
|
||||||
// The following defines keycodes for Orbital Mouse. Being a Mouse Keys
|
|
||||||
// replacement, we repurpose the Mouse Keys keycodes (`MS_UP`, `MS_BTN1`,
|
|
||||||
// etc.) for the analogous functions in Orbital Mouse.
|
|
||||||
enum {
|
|
||||||
/** Move forward. */
|
|
||||||
OM_U = MS_UP,
|
|
||||||
/** Move backward. */
|
|
||||||
OM_D = MS_DOWN,
|
|
||||||
/** Steer left (counter-clockwise). */
|
|
||||||
OM_L = MS_LEFT,
|
|
||||||
/** Steer right (clockwise). */
|
|
||||||
OM_R = MS_RGHT,
|
|
||||||
/** Mouse wheel up. */
|
|
||||||
OM_W_U = MS_WHLU,
|
|
||||||
/** Mouse wheel down. */
|
|
||||||
OM_W_D = MS_WHLD,
|
|
||||||
/** Mouse wheel left. */
|
|
||||||
OM_W_L = MS_WHLL,
|
|
||||||
/** Mouse wheel right. */
|
|
||||||
OM_W_R = MS_WHLR,
|
|
||||||
/** Press mouse button 1. */
|
|
||||||
OM_BTN1 = MS_BTN1,
|
|
||||||
/** Press mouse button 2. */
|
|
||||||
OM_BTN2 = MS_BTN2,
|
|
||||||
/** Press mouse button 3. */
|
|
||||||
OM_BTN3 = MS_BTN3,
|
|
||||||
/** Press mouse button 4. */
|
|
||||||
OM_BTN4 = MS_BTN4,
|
|
||||||
/** Press mouse button 5. */
|
|
||||||
OM_BTN5 = MS_BTN5,
|
|
||||||
/** Press mouse button 6. */
|
|
||||||
OM_BTN6 = MS_BTN6,
|
|
||||||
/** Press mouse button 7. */
|
|
||||||
OM_BTN7 = MS_BTN7,
|
|
||||||
/** Press mouse button 8. */
|
|
||||||
OM_BTN8 = MS_BTN8,
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
{
|
|
||||||
"module_name": "Orbital Mouse",
|
|
||||||
"maintainer": "getreuer",
|
|
||||||
"url": "https://getreuer.info/posts/keyboards/orbital-mouse",
|
|
||||||
"features": {
|
|
||||||
"mouse": true
|
|
||||||
},
|
|
||||||
"keycodes": [
|
|
||||||
{"key": "OM_SLOW"},
|
|
||||||
{"key": "OM_BTNS"},
|
|
||||||
{"key": "OM_DBLS"},
|
|
||||||
{"key": "OM_HLDS"},
|
|
||||||
{"key": "OM_RELS"},
|
|
||||||
{"key": "OM_SEL1"},
|
|
||||||
{"key": "OM_SEL2"},
|
|
||||||
{"key": "OM_SEL3"},
|
|
||||||
{"key": "OM_SEL4"},
|
|
||||||
{"key": "OM_SEL5"},
|
|
||||||
{"key": "OM_SEL6"},
|
|
||||||
{"key": "OM_SEL7"},
|
|
||||||
{"key": "OM_SEL8"}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
# PaletteFx
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<tr><td><b>Module</b></td><td><tt>getreuer/palettefx</tt></td></tr>
|
|
||||||
<tr><td><b>Version</b></td><td>2025-03-07</td></tr>
|
|
||||||
<tr><td><b>Maintainer</b></td><td>Pascal Getreuer (@getreuer)</td></tr>
|
|
||||||
<tr><td><b>License</b></td><td><a href="../LICENSE.txt">Apache 2.0</a></td></tr>
|
|
||||||
<tr><td><b>Documentation</b></td><td>
|
|
||||||
<a href="https://getreuer.info/posts/keyboards/palettefx">https://getreuer.info/posts/keyboards/palettefx</a>
|
|
||||||
</td></tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
This is a community module adaptation of
|
|
||||||
[PaletteFx](https://getreuer.info/posts/keyboards/palettefx) for colorful
|
|
||||||
palette-based RGB matrix effects. While most of QMK's built-in RGB matrix
|
|
||||||
effects are based on a single color hue, sampling from a palette of colors
|
|
||||||
allows for more personality. PaletteFx is a suite of custom effects for QMK's
|
|
||||||
RGB Matrix in which the effect colors are sampled from a palette, aka color
|
|
||||||
gradient or color map.
|
|
||||||
|
|
||||||
|
|
||||||
## Add PaletteFx to your keymap
|
|
||||||
|
|
||||||
Add the following to your `keymap.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"modules": ["getreuer/palettefx"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Then in your keymap folder, create a file `rgb_matrix_user.inc` with the
|
|
||||||
following content, or if it already exists, add this as the first line:
|
|
||||||
|
|
||||||
```c
|
|
||||||
#include "palettefx.inc"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Using PaletteFx
|
|
||||||
|
|
||||||
**Selecting effects:** PaletteFx effects are appended to the list of existing
|
|
||||||
RGB Matrix effects. Use the usual `RM_NEXT` / `RM_PREV` keycodes to switch to
|
|
||||||
the PaletteFx effects.
|
|
||||||
|
|
||||||
**Selecting palettes:** PaletteFx effects repurpose the RGB Matrix hue setting to
|
|
||||||
select which palette to use. Use the hue keycodes `RM_HUEU` / `RM_HUED` to cycle
|
|
||||||
through them. The `i`th palette corresponds to hue = `RGB_MATRIX_HUE_STEP * i`.
|
|
||||||
|
|
||||||
|
|
||||||
## Further details
|
|
||||||
|
|
||||||
Optionally, you can define your own palettes and palette-based effects. See the
|
|
||||||
[PaletteFx documentation](https://getreuer.info/posts/keyboards/palettefx) for
|
|
||||||
details.
|
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
// 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.
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "palettefx.h"
|
|
||||||
|
|
|
@ -1,493 +0,0 @@
|
||||||
// Copyright 2024-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.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @file palettefx.c
|
|
||||||
* @brief PaletteFx community module implementation
|
|
||||||
*
|
|
||||||
* For full documentation, see
|
|
||||||
* <https://getreuer.info/posts/keyboards/palettefx>
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "palettefx.h"
|
|
||||||
#include "quantum.h"
|
|
||||||
|
|
||||||
#include <lib/lib8tion/lib8tion.h>
|
|
||||||
|
|
||||||
ASSERT_COMMUNITY_MODULES_MIN_API_VERSION(1, 0, 0);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 16-bit HSV color.
|
|
||||||
*
|
|
||||||
* Represents a color with a 16-bit value in hue-saturation-value (HSV) space.
|
|
||||||
* Components are packed as:
|
|
||||||
*
|
|
||||||
* - Hue: lowest 8 bits.
|
|
||||||
* - Saturation: middle 4 bits.
|
|
||||||
* - Value: highest 4 bits.
|
|
||||||
*/
|
|
||||||
#define HSV16(h, s, v) ((((v) >> 4) << 12) | (((s) >> 4) << 8) | ((h) & 0xff))
|
|
||||||
|
|
||||||
/** Unpacks 16-bit HSV color to hsv_t. */
|
|
||||||
static hsv_t unpack_hsv16(uint16_t hsv16) {
|
|
||||||
return (hsv_t){
|
|
||||||
.h = (uint8_t)hsv16,
|
|
||||||
.s = ((uint8_t)(hsv16 >> 8) & 0x0f) * 17,
|
|
||||||
.v = ((uint8_t)(hsv16 >> 12) & 0x0f) * 17,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** PaletteFx palette color data. */
|
|
||||||
static const uint16_t palettefx_palettes[][16] PROGMEM = {
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_PALETTES) || defined(PALETTEFX_AFTERBURN_ENABLE)
|
|
||||||
// "Afterburn" palette.
|
|
||||||
{
|
|
||||||
HSV16(139, 255, 85),
|
|
||||||
HSV16(134, 255, 85),
|
|
||||||
HSV16(131, 255, 102),
|
|
||||||
HSV16(128, 255, 102),
|
|
||||||
HSV16(127, 187, 102),
|
|
||||||
HSV16(125, 119, 102),
|
|
||||||
HSV16(124, 51, 102),
|
|
||||||
HSV16(125, 0, 119),
|
|
||||||
HSV16( 15, 17, 119),
|
|
||||||
HSV16( 17, 51, 153),
|
|
||||||
HSV16( 18, 102, 170),
|
|
||||||
HSV16( 19, 153, 204),
|
|
||||||
HSV16( 21, 187, 238),
|
|
||||||
HSV16( 23, 238, 255),
|
|
||||||
HSV16( 26, 255, 255),
|
|
||||||
HSV16( 30, 255, 255),
|
|
||||||
},
|
|
||||||
#endif
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_PALETTES) || defined(PALETTEFX_AMBER_ENABLE)
|
|
||||||
// "Amber" palette.
|
|
||||||
{
|
|
||||||
HSV16( 5, 187, 170),
|
|
||||||
HSV16( 15, 221, 170),
|
|
||||||
HSV16( 22, 238, 187),
|
|
||||||
HSV16( 23, 238, 187),
|
|
||||||
HSV16( 25, 255, 204),
|
|
||||||
HSV16( 27, 255, 221),
|
|
||||||
HSV16( 29, 255, 238),
|
|
||||||
HSV16( 29, 255, 255),
|
|
||||||
HSV16( 28, 187, 255),
|
|
||||||
HSV16( 30, 68, 255),
|
|
||||||
HSV16( 32, 34, 255),
|
|
||||||
HSV16( 32, 34, 255),
|
|
||||||
HSV16( 26, 119, 255),
|
|
||||||
HSV16( 24, 238, 255),
|
|
||||||
HSV16( 21, 221, 255),
|
|
||||||
HSV16( 16, 204, 255),
|
|
||||||
},
|
|
||||||
#endif
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_PALETTES) || defined(PALETTEFX_BADWOLF_ENABLE)
|
|
||||||
// "Bad Wolf" palette. Inspired by the Bad Wolf theme by Steve Losh, which is
|
|
||||||
// distributed under MIT/X11 license.
|
|
||||||
// https://github.com/sjl/badwolf/tree/61d75affa51d40213d671edc9c8ff83992d7fd6f
|
|
||||||
{
|
|
||||||
HSV16( 14, 34, 0),
|
|
||||||
HSV16(245, 255, 255),
|
|
||||||
HSV16(245, 255, 255),
|
|
||||||
HSV16(245, 255, 255),
|
|
||||||
HSV16( 14, 34, 0),
|
|
||||||
HSV16( 14, 34, 0),
|
|
||||||
HSV16( 14, 34, 0),
|
|
||||||
HSV16( 14, 34, 0),
|
|
||||||
HSV16( 14, 34, 0),
|
|
||||||
HSV16( 14, 34, 0),
|
|
||||||
HSV16( 24, 17, 136),
|
|
||||||
HSV16( 28, 0, 255),
|
|
||||||
HSV16( 24, 17, 136),
|
|
||||||
HSV16( 14, 34, 0),
|
|
||||||
HSV16( 14, 34, 0),
|
|
||||||
HSV16( 14, 34, 0),
|
|
||||||
},
|
|
||||||
#endif
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_PALETTES) || defined(PALETTEFX_CARNIVAL_ENABLE)
|
|
||||||
// "Carnival" palette.
|
|
||||||
{
|
|
||||||
HSV16(132, 255, 85),
|
|
||||||
HSV16(121, 187, 85),
|
|
||||||
HSV16(108, 170, 102),
|
|
||||||
HSV16( 90, 153, 119),
|
|
||||||
HSV16( 70, 187, 136),
|
|
||||||
HSV16( 57, 255, 153),
|
|
||||||
HSV16( 50, 255, 187),
|
|
||||||
HSV16( 44, 255, 204),
|
|
||||||
HSV16( 39, 255, 238),
|
|
||||||
HSV16( 32, 255, 255),
|
|
||||||
HSV16( 27, 255, 255),
|
|
||||||
HSV16( 22, 255, 255),
|
|
||||||
HSV16( 18, 255, 255),
|
|
||||||
HSV16( 8, 221, 238),
|
|
||||||
HSV16(252, 204, 238),
|
|
||||||
HSV16(241, 255, 221),
|
|
||||||
},
|
|
||||||
#endif
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_PALETTES) || defined(PALETTEFX_CLASSIC_ENABLE)
|
|
||||||
// "Classic" palette.
|
|
||||||
{
|
|
||||||
HSV16(150, 255, 85),
|
|
||||||
HSV16(151, 238, 119),
|
|
||||||
HSV16(160, 136, 119),
|
|
||||||
HSV16(176, 102, 136),
|
|
||||||
HSV16(193, 85, 136),
|
|
||||||
HSV16(216, 85, 136),
|
|
||||||
HSV16(234, 102, 153),
|
|
||||||
HSV16(247, 119, 187),
|
|
||||||
HSV16( 3, 153, 204),
|
|
||||||
HSV16( 10, 204, 221),
|
|
||||||
HSV16( 15, 255, 255),
|
|
||||||
HSV16( 18, 255, 255),
|
|
||||||
HSV16( 23, 255, 255),
|
|
||||||
HSV16( 24, 204, 255),
|
|
||||||
HSV16( 25, 136, 255),
|
|
||||||
HSV16( 26, 85, 255),
|
|
||||||
},
|
|
||||||
#endif
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_PALETTES) || defined(PALETTEFX_DRACULA_ENABLE)
|
|
||||||
// "Dracula" palette. Inspired by the Dracula theme by Zeno Rocha, which is
|
|
||||||
// distributed under MIT license.
|
|
||||||
// https://github.com/dracula/dracula-theme/tree/ac4dc82dab2a3c35e5cac0cd80c97fbf4c2ca986
|
|
||||||
{
|
|
||||||
HSV16(165, 153, 119),
|
|
||||||
HSV16(167, 153, 136),
|
|
||||||
HSV16(170, 170, 187),
|
|
||||||
HSV16(173, 170, 221),
|
|
||||||
HSV16(177, 170, 238),
|
|
||||||
HSV16(183, 170, 255),
|
|
||||||
HSV16(190, 170, 255),
|
|
||||||
HSV16(200, 153, 255),
|
|
||||||
HSV16(216, 153, 255),
|
|
||||||
HSV16(230, 170, 255),
|
|
||||||
HSV16( 2, 153, 255),
|
|
||||||
HSV16( 29, 153, 238),
|
|
||||||
HSV16( 44, 255, 255),
|
|
||||||
HSV16( 53, 238, 255),
|
|
||||||
HSV16( 63, 238, 238),
|
|
||||||
HSV16( 85, 238, 238),
|
|
||||||
},
|
|
||||||
#endif
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_PALETTES) || defined(PALETTEFX_GROOVY_ENABLE)
|
|
||||||
// "Groovy" palette. Inspired by the Gruvbox theme by Pavel Pertsev, which is
|
|
||||||
// distributed under MIT/X11 license.
|
|
||||||
// https://github.com/morhetz/gruvbox/tree/f1ecde848f0cdba877acb0c740320568252cc482
|
|
||||||
{
|
|
||||||
HSV16( 26, 136, 187),
|
|
||||||
HSV16( 23, 85, 102),
|
|
||||||
HSV16( 21, 85, 85),
|
|
||||||
HSV16( 21, 85, 85),
|
|
||||||
HSV16( 21, 85, 85),
|
|
||||||
HSV16( 17, 102, 102),
|
|
||||||
HSV16( 5, 204, 255),
|
|
||||||
HSV16( 4, 255, 255),
|
|
||||||
HSV16( 4, 255, 255),
|
|
||||||
HSV16( 7, 204, 255),
|
|
||||||
HSV16( 23, 136, 187),
|
|
||||||
HSV16( 26, 136, 187),
|
|
||||||
HSV16( 28, 136, 187),
|
|
||||||
HSV16( 50, 238, 238),
|
|
||||||
HSV16( 54, 255, 238),
|
|
||||||
HSV16( 54, 255, 238),
|
|
||||||
},
|
|
||||||
#endif
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_PALETTES) || defined(PALETTEFX_NOTPINK_ENABLE)
|
|
||||||
// "Not Pink" palette.
|
|
||||||
{
|
|
||||||
HSV16( 9, 255, 153),
|
|
||||||
HSV16( 3, 221, 187),
|
|
||||||
HSV16(252, 204, 187),
|
|
||||||
HSV16(248, 204, 187),
|
|
||||||
HSV16(247, 187, 204),
|
|
||||||
HSV16(245, 187, 238),
|
|
||||||
HSV16(244, 170, 255),
|
|
||||||
HSV16(245, 153, 255),
|
|
||||||
HSV16(252, 102, 255),
|
|
||||||
HSV16( 16, 68, 255),
|
|
||||||
HSV16( 40, 34, 255),
|
|
||||||
HSV16( 32, 51, 255),
|
|
||||||
HSV16( 6, 85, 255),
|
|
||||||
HSV16(248, 136, 255),
|
|
||||||
HSV16(247, 187, 255),
|
|
||||||
HSV16(249, 221, 255),
|
|
||||||
},
|
|
||||||
#endif
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_PALETTES) || defined(PALETTEFX_PHOSPHOR_ENABLE)
|
|
||||||
// "Phosphor" palette.
|
|
||||||
{
|
|
||||||
HSV16(116, 102, 34),
|
|
||||||
HSV16(113, 170, 51),
|
|
||||||
HSV16(113, 255, 68),
|
|
||||||
HSV16(110, 255, 68),
|
|
||||||
HSV16(109, 255, 85),
|
|
||||||
HSV16(105, 255, 102),
|
|
||||||
HSV16( 95, 238, 102),
|
|
||||||
HSV16( 85, 238, 119),
|
|
||||||
HSV16( 84, 255, 136),
|
|
||||||
HSV16( 84, 255, 153),
|
|
||||||
HSV16( 83, 255, 187),
|
|
||||||
HSV16( 80, 238, 204),
|
|
||||||
HSV16( 69, 221, 238),
|
|
||||||
HSV16( 46, 204, 255),
|
|
||||||
HSV16( 42, 153, 255),
|
|
||||||
HSV16( 40, 102, 255),
|
|
||||||
},
|
|
||||||
#endif
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_PALETTES) || defined(PALETTEFX_POLARIZED_ENABLE)
|
|
||||||
// "Polarized" palette. Inspired by the Solarized theme by Ethan Schoonover,
|
|
||||||
// which is distributed under MIT license.
|
|
||||||
// https://github.com/altercation/solarized/tree/62f656a02f93c5190a8753159e34b385588d5ff3
|
|
||||||
{
|
|
||||||
HSV16(139, 255, 68),
|
|
||||||
HSV16(139, 221, 85),
|
|
||||||
HSV16(139, 204, 102),
|
|
||||||
HSV16(138, 204, 119),
|
|
||||||
HSV16(137, 204, 136),
|
|
||||||
HSV16(137, 187, 170),
|
|
||||||
HSV16(137, 153, 187),
|
|
||||||
HSV16(132, 170, 187),
|
|
||||||
HSV16(126, 187, 238),
|
|
||||||
HSV16(124, 102, 255),
|
|
||||||
HSV16(124, 102, 255),
|
|
||||||
HSV16(124, 187, 255),
|
|
||||||
HSV16(130, 170, 204),
|
|
||||||
HSV16(137, 153, 187),
|
|
||||||
HSV16(137, 153, 204),
|
|
||||||
HSV16(137, 136, 221),
|
|
||||||
},
|
|
||||||
#endif
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_PALETTES) || defined(PALETTEFX_ROSEGOLD_ENABLE)
|
|
||||||
// "Rose Gold" palette.
|
|
||||||
{
|
|
||||||
HSV16(246, 255, 204),
|
|
||||||
HSV16(250, 238, 221),
|
|
||||||
HSV16(253, 221, 238),
|
|
||||||
HSV16( 1, 221, 255),
|
|
||||||
HSV16( 6, 221, 255),
|
|
||||||
HSV16( 12, 221, 255),
|
|
||||||
HSV16( 18, 204, 255),
|
|
||||||
HSV16( 22, 170, 255),
|
|
||||||
HSV16( 20, 204, 255),
|
|
||||||
HSV16( 17, 221, 255),
|
|
||||||
HSV16( 12, 221, 255),
|
|
||||||
HSV16( 7, 221, 255),
|
|
||||||
HSV16(255, 204, 255),
|
|
||||||
HSV16(248, 204, 255),
|
|
||||||
HSV16(241, 238, 255),
|
|
||||||
HSV16(235, 255, 255),
|
|
||||||
},
|
|
||||||
#endif
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_PALETTES) || defined(PALETTEFX_SPORT_ENABLE)
|
|
||||||
// "Sport" palette.
|
|
||||||
{
|
|
||||||
HSV16(156, 102, 51),
|
|
||||||
HSV16(155, 102, 68),
|
|
||||||
HSV16(155, 102, 68),
|
|
||||||
HSV16(154, 102, 68),
|
|
||||||
HSV16(155, 102, 85),
|
|
||||||
HSV16(156, 85, 102),
|
|
||||||
HSV16(158, 68, 119),
|
|
||||||
HSV16(159, 51, 136),
|
|
||||||
HSV16(158, 17, 153),
|
|
||||||
HSV16(130, 0, 170),
|
|
||||||
HSV16( 49, 17, 204),
|
|
||||||
HSV16( 42, 51, 238),
|
|
||||||
HSV16( 39, 85, 255),
|
|
||||||
HSV16( 37, 119, 255),
|
|
||||||
HSV16( 36, 170, 255),
|
|
||||||
HSV16( 36, 255, 255),
|
|
||||||
},
|
|
||||||
#endif
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_PALETTES) || defined(PALETTEFX_SYNTHWAVE_ENABLE)
|
|
||||||
// "Synthwave" palette.
|
|
||||||
{
|
|
||||||
HSV16(170, 221, 119),
|
|
||||||
HSV16(180, 221, 153),
|
|
||||||
HSV16(196, 238, 153),
|
|
||||||
HSV16(209, 255, 153),
|
|
||||||
HSV16(220, 255, 170),
|
|
||||||
HSV16(227, 255, 204),
|
|
||||||
HSV16(233, 255, 238),
|
|
||||||
HSV16(245, 204, 255),
|
|
||||||
HSV16( 3, 170, 255),
|
|
||||||
HSV16( 13, 187, 255),
|
|
||||||
HSV16( 21, 204, 255),
|
|
||||||
HSV16( 28, 221, 255),
|
|
||||||
HSV16( 43, 255, 255),
|
|
||||||
HSV16( 42, 255, 255),
|
|
||||||
HSV16( 68, 68, 255),
|
|
||||||
HSV16(132, 255, 255),
|
|
||||||
},
|
|
||||||
#endif
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_PALETTES) || defined(PALETTEFX_THERMAL_ENABLE)
|
|
||||||
// "Thermal" palette.
|
|
||||||
{
|
|
||||||
HSV16( 7, 0, 17),
|
|
||||||
HSV16( 8, 0, 17),
|
|
||||||
HSV16( 12, 0, 34),
|
|
||||||
HSV16( 13, 0, 51),
|
|
||||||
HSV16( 15, 17, 51),
|
|
||||||
HSV16( 16, 17, 68),
|
|
||||||
HSV16( 20, 34, 85),
|
|
||||||
HSV16( 95, 0, 102),
|
|
||||||
HSV16(126, 85, 119),
|
|
||||||
HSV16(112, 85, 153),
|
|
||||||
HSV16( 54, 136, 187),
|
|
||||||
HSV16( 39, 238, 221),
|
|
||||||
HSV16( 33, 255, 255),
|
|
||||||
HSV16( 25, 238, 255),
|
|
||||||
HSV16( 13, 238, 255),
|
|
||||||
HSV16( 5, 238, 255),
|
|
||||||
},
|
|
||||||
#endif
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_PALETTES) || defined(PALETTEFX_VIRIDIS_ENABLE)
|
|
||||||
// "Viridis" palette. Inspired by the Viridis colormap by Stefan van der Walt
|
|
||||||
// and Nathaniel Smith, which is distributed under CC0 license.
|
|
||||||
// https://github.com/BIDS/colormap/blob/bc549477db0c12b54a5928087552ad2cf274980f/colormaps.py
|
|
||||||
{
|
|
||||||
HSV16(204, 221, 102),
|
|
||||||
HSV16(194, 187, 119),
|
|
||||||
HSV16(183, 153, 136),
|
|
||||||
HSV16(169, 119, 153),
|
|
||||||
HSV16(155, 153, 153),
|
|
||||||
HSV16(145, 170, 153),
|
|
||||||
HSV16(137, 187, 153),
|
|
||||||
HSV16(130, 204, 153),
|
|
||||||
HSV16(123, 221, 153),
|
|
||||||
HSV16(117, 221, 170),
|
|
||||||
HSV16(108, 187, 187),
|
|
||||||
HSV16( 93, 153, 204),
|
|
||||||
HSV16( 71, 170, 221),
|
|
||||||
HSV16( 56, 221, 221),
|
|
||||||
HSV16( 46, 255, 238),
|
|
||||||
HSV16( 39, 255, 255),
|
|
||||||
},
|
|
||||||
#endif
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_PALETTES) || defined(PALETTEFX_WATERMELON_ENABLE)
|
|
||||||
// "Watermelon" palette.
|
|
||||||
{
|
|
||||||
HSV16( 67, 255, 102),
|
|
||||||
HSV16( 55, 255, 153),
|
|
||||||
HSV16( 47, 255, 204),
|
|
||||||
HSV16( 48, 136, 238),
|
|
||||||
HSV16( 48, 68, 255),
|
|
||||||
HSV16( 45, 34, 255),
|
|
||||||
HSV16( 39, 34, 255),
|
|
||||||
HSV16( 17, 85, 255),
|
|
||||||
HSV16( 8, 187, 255),
|
|
||||||
HSV16( 1, 255, 255),
|
|
||||||
HSV16( 2, 238, 255),
|
|
||||||
HSV16( 2, 238, 255),
|
|
||||||
HSV16( 0, 238, 255),
|
|
||||||
HSV16(253, 255, 238),
|
|
||||||
HSV16(254, 255, 238),
|
|
||||||
HSV16(255, 255, 221),
|
|
||||||
},
|
|
||||||
#endif
|
|
||||||
#if __has_include("palettefx_user.inc") // Include user palettes if present.
|
|
||||||
#include "palettefx_user.inc"
|
|
||||||
#endif
|
|
||||||
};
|
|
||||||
/** Number of palettes. User palettes, if any, are included in the count. */
|
|
||||||
#define NUM_PALETTEFX_PALETTES \
|
|
||||||
(sizeof(palettefx_palettes) / sizeof(*palettefx_palettes))
|
|
||||||
|
|
||||||
// Validate at compile time that `1 <= NUM_PALETTEFX_PALETTES <= 32`. The upper
|
|
||||||
// limit is due to using RGB Matrix's hue config to select palettes.
|
|
||||||
_Static_assert(
|
|
||||||
NUM_PALETTEFX_PALETTES >= 1,
|
|
||||||
"palettefx: No palettes are enabled. To fix: enable all built-in palettes by adding in config.h `#define PALETTE_ENABLE_ALL_PALETTES`, or enable at least one with `#define PALETTE_<name>_ENABLE`, or define at least one custom palette in palettefx_user.inc.");
|
|
||||||
_Static_assert(
|
|
||||||
NUM_PALETTEFX_PALETTES <= 256 / RGB_MATRIX_HUE_STEP,
|
|
||||||
"palettefx: Too many palettes. Up to 32 (= 256 / RGB_MATRIX_HUE_STEP) palettes are supported. Otherwise, some palettes would be unreachable.");
|
|
||||||
|
|
||||||
/** Gets the index of the selected palette. */
|
|
||||||
static uint8_t palettefx_get_palette(void) {
|
|
||||||
uint8_t i =
|
|
||||||
(rgb_matrix_get_hue() / RGB_MATRIX_HUE_STEP) % NUM_PALETTEFX_PALETTES;
|
|
||||||
|
|
||||||
if (256 % (RGB_MATRIX_HUE_STEP * NUM_PALETTEFX_PALETTES) != 0) {
|
|
||||||
// Hue wraps mod 256. If NUM_PALETTEFX_PALETTES is not a power of 2, modulo
|
|
||||||
// 256 wraps would jump, and adjustment is needed to cycle as desired:
|
|
||||||
//
|
|
||||||
// * If the hue is a step or less from 256, assume it has wrapped down
|
|
||||||
// from 0 and the last palette is selected.
|
|
||||||
// * Otherwise, i = ((hue / step) % NUM_PALETTEFX_PALETTES) is used. If
|
|
||||||
// 256 - 2 * step <= hue < 256 - step, hue is set to (step * i).
|
|
||||||
hsv_t hsv = rgb_matrix_get_hsv();
|
|
||||||
if (hsv.h >= 256 - 2 * RGB_MATRIX_HUE_STEP) {
|
|
||||||
if (hsv.h >= 256 - RGB_MATRIX_HUE_STEP) {
|
|
||||||
i = NUM_PALETTEFX_PALETTES - 1;
|
|
||||||
hsv.h = RGB_MATRIX_HUE_STEP * (
|
|
||||||
(256 / (RGB_MATRIX_HUE_STEP * NUM_PALETTEFX_PALETTES)) *
|
|
||||||
NUM_PALETTEFX_PALETTES
|
|
||||||
- 1);
|
|
||||||
} else {
|
|
||||||
i %= NUM_PALETTEFX_PALETTES;
|
|
||||||
hsv.h = RGB_MATRIX_HUE_STEP * i;
|
|
||||||
}
|
|
||||||
rgb_matrix_sethsv_noeeprom(hsv.h, hsv.s, hsv.v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uint16_t* palettefx_get_palette_data(void) {
|
|
||||||
return palettefx_palettes[palettefx_get_palette()];
|
|
||||||
}
|
|
||||||
|
|
||||||
const uint16_t* palettefx_get_palette_data_by_index(uint8_t i) {
|
|
||||||
return palettefx_palettes[i % NUM_PALETTEFX_PALETTES];
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t palettefx_num_palettes(void) {
|
|
||||||
return NUM_PALETTEFX_PALETTES;
|
|
||||||
}
|
|
||||||
|
|
||||||
hsv_t palettefx_interp_color(const uint16_t* palette, uint8_t x) {
|
|
||||||
// Clamp `x` to [8, 247] and subtract 8, mapping to the range [0, 239].
|
|
||||||
x = (x <= 8) ? 0 : ((x < 247) ? (x - 8) : 239);
|
|
||||||
// Get index into the palette, 0 <= i <= 14.
|
|
||||||
const uint8_t i = x >> 4;
|
|
||||||
// Fractional position between i and (i + 1).
|
|
||||||
const uint8_t frac = x << 4;
|
|
||||||
|
|
||||||
// Look up palette colors at i and (i + 1).
|
|
||||||
hsv_t a = unpack_hsv16(pgm_read_word(&palette[i]));
|
|
||||||
hsv_t b = unpack_hsv16(pgm_read_word(&palette[i + 1]));
|
|
||||||
|
|
||||||
// Linearly interpolate in HSV, accounting for wrapping in hue.
|
|
||||||
const uint8_t hue_wrap = 128 & (a.h >= b.h ? (a.h - b.h) : (b.h - a.h));
|
|
||||||
return (hsv_t){
|
|
||||||
.h = lerp8by8(a.h ^ hue_wrap, b.h ^ hue_wrap, frac) ^ hue_wrap,
|
|
||||||
.s = scale8(lerp8by8(a.s, b.s, frac), rgb_matrix_config.hsv.s),
|
|
||||||
.v = scale8(lerp8by8(a.v, b.v, frac), rgb_matrix_config.hsv.v),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
uint16_t palettefx_scaled_time(uint32_t timer, uint8_t scale) {
|
|
||||||
static uint16_t wrap_correction = 0;
|
|
||||||
static uint8_t last_high_byte = 0;
|
|
||||||
const uint8_t high_byte = (uint8_t)(timer >> 16);
|
|
||||||
|
|
||||||
if (last_high_byte != high_byte) {
|
|
||||||
last_high_byte = high_byte;
|
|
||||||
wrap_correction += ((uint16_t)scale) << 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
return scale16by8(timer, scale) + wrap_correction;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,150 +0,0 @@
|
||||||
// Copyright 2024-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.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @file palettefx.h
|
|
||||||
* @brief PaletteFx community module: Add more colors to your keyboard
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* For full documentation, see
|
|
||||||
* <https://getreuer.info/posts/keyboards/palettefx>
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
#include "color.h"
|
|
||||||
#include "palettefx_default_config.h"
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/** Gets the color data for the selected palette. */
|
|
||||||
const uint16_t* palettefx_get_palette_data(void);
|
|
||||||
|
|
||||||
/** Gets the color data for the ith palette. */
|
|
||||||
const uint16_t* palettefx_get_palette_data_by_index(uint8_t i);
|
|
||||||
|
|
||||||
/** Returns the number of palettes. */
|
|
||||||
uint8_t palettefx_num_palettes(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Computes the interpolated HSV palette color at 0 <= x < 256.
|
|
||||||
*
|
|
||||||
* Looks up and linearly interpolates `palette` at 0 <= x < 256. The color
|
|
||||||
* saturation and value are scaled according to rgb_matrix_config.
|
|
||||||
*
|
|
||||||
* @note `palette` must point to a PROGMEM address.
|
|
||||||
*
|
|
||||||
* @param palette Pointer to PROGMEM of a size-16 table of HSV16 colors.
|
|
||||||
* @param x Palette lookup position, a value in 0-255.
|
|
||||||
* @return HSV color.
|
|
||||||
*/
|
|
||||||
hsv_t palettefx_interp_color(const uint16_t* palette, uint8_t x);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Compute a scaled 16-bit time that wraps smoothly.
|
|
||||||
*
|
|
||||||
* Computes `timer` scaled by `scale`, returning the result as a uint16.
|
|
||||||
* Generally, the scaled time would have a discontinuity every ~65 seconds when
|
|
||||||
* `timer` wraps 16-bit range. This is corrected for to wrap smoothly mod 2^16.
|
|
||||||
*
|
|
||||||
* @param timer A 32-bit timer.
|
|
||||||
* @param scale Scale value in 0-255.
|
|
||||||
* @return Scaled time.
|
|
||||||
*/
|
|
||||||
uint16_t palettefx_scaled_time(uint32_t timer, uint8_t scale);
|
|
||||||
|
|
||||||
// The following enum constants may be used to refer to PaletteFx palettes by
|
|
||||||
// name. To set a particular palette programmatically, do e.g.
|
|
||||||
//
|
|
||||||
// void keyboard_post_init_user(void) {
|
|
||||||
// uint8_t i = PALETTEFX_CARNIVAL; // Set Carnival palette at startup.
|
|
||||||
// rgb_matrix_sethsv_noeeprom(RGB_MATRIX_HUE_STEP * i, 255, 255);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// If you have defined additional palettes in palettefx_user.inc, they may be
|
|
||||||
// referred to by `PALETTEFX_USER_0`, `PALETTEFX_USER_1`, etc.
|
|
||||||
enum {
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_PALETTES) || defined(PALETTEFX_AFTERBURN_ENABLE)
|
|
||||||
PALETTEFX_AFTERBURN,
|
|
||||||
#endif
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_PALETTES) || defined(PALETTEFX_AMBER_ENABLE)
|
|
||||||
PALETTEFX_AMBER,
|
|
||||||
#endif
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_PALETTES) || defined(PALETTEFX_BADWOLF_ENABLE)
|
|
||||||
PALETTEFX_BADWOLF,
|
|
||||||
#endif
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_PALETTES) || defined(PALETTEFX_CARNIVAL_ENABLE)
|
|
||||||
PALETTEFX_CARNIVAL,
|
|
||||||
#endif
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_PALETTES) || defined(PALETTEFX_CLASSIC_ENABLE)
|
|
||||||
PALETTEFX_CLASSIC,
|
|
||||||
#endif
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_PALETTES) || defined(PALETTEFX_DRACULA_ENABLE)
|
|
||||||
PALETTEFX_DRACULA,
|
|
||||||
#endif
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_PALETTES) || defined(PALETTEFX_GROOVY_ENABLE)
|
|
||||||
PALETTEFX_GROOVY,
|
|
||||||
#endif
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_PALETTES) || defined(PALETTEFX_NOTPINK_ENABLE)
|
|
||||||
PALETTEFX_NOTPINK,
|
|
||||||
#endif
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_PALETTES) || defined(PALETTEFX_PHOSPHOR_ENABLE)
|
|
||||||
PALETTEFX_PHOSPHOR,
|
|
||||||
#endif
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_PALETTES) || defined(PALETTEFX_POLARIZED_ENABLE)
|
|
||||||
PALETTEFX_POLARIZED,
|
|
||||||
#endif
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_PALETTES) || defined(PALETTEFX_ROSEGOLD_ENABLE)
|
|
||||||
PALETTEFX_ROSEGOLD,
|
|
||||||
#endif
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_PALETTES) || defined(PALETTEFX_SPORT_ENABLE)
|
|
||||||
PALETTEFX_SPORT,
|
|
||||||
#endif
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_PALETTES) || defined(PALETTEFX_SYNTHWAVE_ENABLE)
|
|
||||||
PALETTEFX_SYNTHWAVE,
|
|
||||||
#endif
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_PALETTES) || defined(PALETTEFX_THERMAL_ENABLE)
|
|
||||||
PALETTEFX_THERMAL,
|
|
||||||
#endif
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_PALETTES) || defined(PALETTEFX_VIRIDIS_ENABLE)
|
|
||||||
PALETTEFX_VIRIDIS,
|
|
||||||
#endif
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_PALETTES) || defined(PALETTEFX_WATERMELON_ENABLE)
|
|
||||||
PALETTEFX_WATERMELON,
|
|
||||||
#endif
|
|
||||||
PALETTEFX_USER_0,
|
|
||||||
PALETTEFX_USER_1,
|
|
||||||
PALETTEFX_USER_2,
|
|
||||||
PALETTEFX_USER_3,
|
|
||||||
PALETTEFX_USER_4,
|
|
||||||
PALETTEFX_USER_5,
|
|
||||||
PALETTEFX_USER_6,
|
|
||||||
PALETTEFX_USER_7,
|
|
||||||
PALETTEFX_USER_8,
|
|
||||||
PALETTEFX_USER_9,
|
|
||||||
PALETTEFX_USER_10,
|
|
||||||
PALETTEFX_USER_11,
|
|
||||||
PALETTEFX_USER_12,
|
|
||||||
PALETTEFX_USER_13,
|
|
||||||
PALETTEFX_USER_14,
|
|
||||||
PALETTEFX_USER_15,
|
|
||||||
};
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
|
@ -1,345 +0,0 @@
|
||||||
// Copyright 2024-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.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @file palettefx.inc
|
|
||||||
* @brief PaletteFx community module effects definitions
|
|
||||||
*
|
|
||||||
* For full documentation, see
|
|
||||||
* <https://getreuer.info/posts/keyboards/palettefx>
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifdef COMMUNITY_MODULE_PALETTEFX_ENABLE
|
|
||||||
|
|
||||||
#include "palettefx_default_config.h"
|
|
||||||
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_EFFECTS) || defined(PALETTEFX_GRADIENT_ENABLE)
|
|
||||||
RGB_MATRIX_EFFECT(PALETTEFX_GRADIENT)
|
|
||||||
#endif
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_EFFECTS) || defined(PALETTEFX_FLOW_ENABLE)
|
|
||||||
RGB_MATRIX_EFFECT(PALETTEFX_FLOW)
|
|
||||||
#endif
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_EFFECTS) || defined(PALETTEFX_RIPPLE_ENABLE)
|
|
||||||
RGB_MATRIX_EFFECT(PALETTEFX_RIPPLE)
|
|
||||||
#endif
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_EFFECTS) || defined(PALETTEFX_SPARKLE_ENABLE)
|
|
||||||
RGB_MATRIX_EFFECT(PALETTEFX_SPARKLE)
|
|
||||||
#endif
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_EFFECTS) || defined(PALETTEFX_VORTEX_ENABLE)
|
|
||||||
RGB_MATRIX_EFFECT(PALETTEFX_VORTEX)
|
|
||||||
#endif
|
|
||||||
#if defined(RGB_MATRIX_KEYREACTIVE_ENABLED) && ( \
|
|
||||||
defined(PALETTEFX_ENABLE_ALL_EFFECTS) || defined(PALETTEFX_REACTIVE_ENABLE))
|
|
||||||
RGB_MATRIX_EFFECT(PALETTEFX_REACTIVE)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef RGB_MATRIX_CUSTOM_EFFECT_IMPLS
|
|
||||||
|
|
||||||
#include "palettefx.h"
|
|
||||||
|
|
||||||
#if !(defined(PALETTEFX_ENABLE_ALL_EFFECTS) || \
|
|
||||||
defined(PALETTEFX_GRADIENT_ENABLE) || \
|
|
||||||
defined(PALETTEFX_FLOW_ENABLE) || \
|
|
||||||
defined(PALETTEFX_RIPPLE_ENABLE) || \
|
|
||||||
defined(PALETTEFX_SPARKLE_ENABLE) || \
|
|
||||||
defined(PALETTEFX_VORTEX_ENABLE) || \
|
|
||||||
(defined(RGB_MATRIX_KEYREACTIVE_ENABLED) && \
|
|
||||||
defined(PALETTEFX_REACTIVE_ENABLE)))
|
|
||||||
#pragma message \
|
|
||||||
"palettefx: No palettefx effects are enabled. Enable all effects by adding in config.h `#define PALETTEFX_ENABLE_ALL_EFFECTS`, or enable individual effects with `#define PALETTE_<name>_ENABLE`."
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_EFFECTS) || defined(PALETTEFX_GRADIENT_ENABLE)
|
|
||||||
// "Gradient" static effect. This is essentially a palette-colored version of
|
|
||||||
// RGB_MATRIX_GRADIENT_UP_DOWN. A vertically-sloping gradient is made, with the
|
|
||||||
// highest color on the top keys of keyboard and the lowest color at the bottom.
|
|
||||||
static bool PALETTEFX_GRADIENT(effect_params_t* params) {
|
|
||||||
// On first call, compute and cache the slope of the gradient.
|
|
||||||
static uint8_t gradient_slope = 0;
|
|
||||||
if (!gradient_slope) {
|
|
||||||
uint8_t y_max = 64; // To avoid overflow below, x_max must be at least 64.
|
|
||||||
for (uint8_t i = 0; i < RGB_MATRIX_LED_COUNT; ++i) {
|
|
||||||
if (g_led_config.point[i].y > y_max) {
|
|
||||||
y_max = g_led_config.point[i].y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Compute the quotient `255 / y_max` with 6 fractional bits and rounding.
|
|
||||||
gradient_slope = (64 * 255 + y_max / 2) / y_max;
|
|
||||||
}
|
|
||||||
|
|
||||||
RGB_MATRIX_USE_LIMITS(led_min, led_max);
|
|
||||||
const uint16_t* palette = palettefx_get_palette_data();
|
|
||||||
|
|
||||||
for (uint8_t i = led_min; i < led_max; ++i) {
|
|
||||||
RGB_MATRIX_TEST_LED_FLAGS();
|
|
||||||
const uint8_t y = g_led_config.point[i].y;
|
|
||||||
const uint8_t value = 255 - (((uint16_t)y * (uint16_t)gradient_slope) >> 6);
|
|
||||||
rgb_t rgb = rgb_matrix_hsv_to_rgb(palettefx_interp_color(palette, value));
|
|
||||||
rgb_matrix_set_color(i, rgb.r, rgb.g, rgb.b);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rgb_matrix_check_finished_leds(led_max);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_EFFECTS) || defined(PALETTEFX_FLOW_ENABLE)
|
|
||||||
// "Flow" animated effect. Draws moving wave patterns mimicking the appearance
|
|
||||||
// of flowing liquid. For interesting variety of patterns, space coordinates are
|
|
||||||
// slowly rotated and a function of several sine waves is evaluated.
|
|
||||||
static bool PALETTEFX_FLOW(effect_params_t* params) {
|
|
||||||
RGB_MATRIX_USE_LIMITS(led_min, led_max);
|
|
||||||
const uint16_t* palette = palettefx_get_palette_data();
|
|
||||||
const uint16_t time =
|
|
||||||
palettefx_scaled_time(g_rgb_timer, 1 + rgb_matrix_config.speed / 8);
|
|
||||||
// Compute rotation coefficients with 7 fractional bits.
|
|
||||||
const int8_t rot_c = cos8(time / 4) - 128;
|
|
||||||
const int8_t rot_s = sin8(time / 4) - 128;
|
|
||||||
const uint8_t omega = 32 + sin8(time) / 4;
|
|
||||||
|
|
||||||
for (uint8_t i = led_min; i < led_max; ++i) {
|
|
||||||
RGB_MATRIX_TEST_LED_FLAGS();
|
|
||||||
const uint8_t x = g_led_config.point[i].x;
|
|
||||||
const uint8_t y = g_led_config.point[i].y;
|
|
||||||
|
|
||||||
// Rotate (x, y) by the 2x2 rotation matrix described by rot_c, rot_s.
|
|
||||||
const uint8_t x1 = (uint8_t)((((int16_t)rot_c) * ((int16_t)x)) / 128)
|
|
||||||
- (uint8_t)((((int16_t)rot_s) * ((int16_t)y)) / 128);
|
|
||||||
const uint8_t y1 = (uint8_t)((((int16_t)rot_s) * ((int16_t)x)) / 128)
|
|
||||||
+ (uint8_t)((((int16_t)rot_c) * ((int16_t)y)) / 128);
|
|
||||||
|
|
||||||
uint8_t value = scale8(sin8(x1 - 2 * time), omega) + y1 + time / 4;
|
|
||||||
// Evaluate `sawtooth(value)`.
|
|
||||||
value = 2 * ((value <= 127) ? value : (255 - value));
|
|
||||||
|
|
||||||
rgb_t rgb = rgb_matrix_hsv_to_rgb(palettefx_interp_color(palette, value));
|
|
||||||
rgb_matrix_set_color(i, rgb.r, rgb.g, rgb.b);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rgb_matrix_check_finished_leds(led_max);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_EFFECTS) || defined(PALETTEFX_RIPPLE_ENABLE)
|
|
||||||
// "Ripple" animated effect. Draws circular rings emanating from random points,
|
|
||||||
// simulating water drops falling in a quiet pool.
|
|
||||||
static bool PALETTEFX_RIPPLE(effect_params_t* params) {
|
|
||||||
RGB_MATRIX_USE_LIMITS(led_min, led_max);
|
|
||||||
const uint16_t* palette = palettefx_get_palette_data();
|
|
||||||
|
|
||||||
// Each instance of this struct represents one water drop. For efficiency, at
|
|
||||||
// most 3 drops are active at any time.
|
|
||||||
static struct {
|
|
||||||
uint16_t time;
|
|
||||||
uint8_t x;
|
|
||||||
uint8_t y;
|
|
||||||
uint8_t amplitude;
|
|
||||||
uint8_t scale;
|
|
||||||
uint8_t phase;
|
|
||||||
} drops[3];
|
|
||||||
static uint32_t drop_timer = 0;
|
|
||||||
static uint8_t drops_tail = 0;
|
|
||||||
|
|
||||||
if (params->iter == 0) {
|
|
||||||
if (params->init) {
|
|
||||||
for (uint8_t j = 0; j < 3; ++j) {
|
|
||||||
drops[j].amplitude = 0;
|
|
||||||
}
|
|
||||||
drop_timer = g_rgb_timer;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (drops[drops_tail].amplitude == 0 &&
|
|
||||||
timer_expired32(g_rgb_timer, drop_timer)) {
|
|
||||||
// Spawn a new drop, located at a random LED.
|
|
||||||
const uint8_t i = random8_max(RGB_MATRIX_LED_COUNT);
|
|
||||||
drops[drops_tail].time = (uint16_t)g_rgb_timer;
|
|
||||||
drops[drops_tail].x = g_led_config.point[i].x;
|
|
||||||
drops[drops_tail].y = g_led_config.point[i].y;
|
|
||||||
drops[drops_tail].amplitude = 1;
|
|
||||||
++drops_tail;
|
|
||||||
if (drops_tail == 3) { drops_tail = 0; }
|
|
||||||
drop_timer = g_rgb_timer + 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t amplitude(uint8_t t) { // Drop amplitude as a function of time.
|
|
||||||
if (t <= 55) {
|
|
||||||
return (t < 32) ? (3 + 5 * t) : 192;
|
|
||||||
} else {
|
|
||||||
t = (((uint16_t)(255 - t)) * UINT16_C(123)) >> 7;
|
|
||||||
return scale8(t, t);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (uint8_t j = 0; j < 3; ++j) {
|
|
||||||
if (drops[j].amplitude == 0) { continue; }
|
|
||||||
const uint16_t tick = scale16by8(g_rgb_timer - drops[j].time,
|
|
||||||
1 + rgb_matrix_config.speed / 4);
|
|
||||||
if (tick < 4 * 255) {
|
|
||||||
const uint8_t t = (uint8_t)(tick / 4);
|
|
||||||
drops[j].amplitude = amplitude(t);
|
|
||||||
drops[j].scale = 255 / (1 + t / 2);
|
|
||||||
drops[j].phase = (uint8_t)tick;
|
|
||||||
} else {
|
|
||||||
drops[j].amplitude = 0; // Animation for this drop is complete.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (uint8_t i = led_min; i < led_max; ++i) {
|
|
||||||
RGB_MATRIX_TEST_LED_FLAGS();
|
|
||||||
int16_t value = 128;
|
|
||||||
|
|
||||||
for (uint8_t j = 0; j < 3; ++j) {
|
|
||||||
if (drops[j].amplitude == 0) { continue; }
|
|
||||||
|
|
||||||
const uint8_t x = abs8((g_led_config.point[i].x - drops[j].x) / 2);
|
|
||||||
const uint8_t y = abs8((g_led_config.point[i].y - drops[j].y) / 2);
|
|
||||||
const uint8_t r = sqrt16(x * x + y * y);
|
|
||||||
const uint16_t r_scaled = (uint16_t)r * (uint16_t)drops[j].scale;
|
|
||||||
|
|
||||||
if (r_scaled < 255) {
|
|
||||||
// The drop is made from a radial sine wave modulated by a smooth bump
|
|
||||||
// to localize its spatial extent.
|
|
||||||
const uint8_t bump = scale8(ease8InOutApprox(255 - (uint8_t)r_scaled),
|
|
||||||
drops[j].amplitude);
|
|
||||||
const int8_t wave = (int16_t)cos8(8 * r - drops[j].phase) - 128;
|
|
||||||
value += ((int16_t)wave * (int16_t)bump) / 128;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clip `value` to 0-255 range.
|
|
||||||
if (value < 0) { value = 0; }
|
|
||||||
if (value > 255) { value = 255; }
|
|
||||||
rgb_t rgb =
|
|
||||||
rgb_matrix_hsv_to_rgb(palettefx_interp_color(palette, (uint8_t)value));
|
|
||||||
rgb_matrix_set_color(i, rgb.r, rgb.g, rgb.b);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rgb_matrix_check_finished_leds(led_max);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_EFFECTS) || defined(PALETTEFX_SPARKLE_ENABLE)
|
|
||||||
// "Sparkle" effect. Each LED is animated by a sine wave with pseudorandom
|
|
||||||
// phase, so that the matrix "sparkles." All the LED sines are modulated by a
|
|
||||||
// global amplitude factor, which varies by a slower sine wave, so that the
|
|
||||||
// matrix as a whole periodically brightens and dims.
|
|
||||||
static bool PALETTEFX_SPARKLE(effect_params_t* params) {
|
|
||||||
RGB_MATRIX_USE_LIMITS(led_min, led_max);
|
|
||||||
const uint16_t* palette = palettefx_get_palette_data();
|
|
||||||
const uint8_t time =
|
|
||||||
palettefx_scaled_time(g_rgb_timer, 1 + rgb_matrix_config.speed / 8);
|
|
||||||
const uint8_t amplitude = 128 + sin8(time) / 2;
|
|
||||||
uint16_t rand_state = 1 + params->iter;
|
|
||||||
|
|
||||||
for (uint8_t i = led_min; i < led_max; ++i) {
|
|
||||||
RGB_MATRIX_TEST_LED_FLAGS();
|
|
||||||
// Multiplicative congruential generator for a random phase for each LED.
|
|
||||||
rand_state *= UINT16_C(36563);
|
|
||||||
const uint8_t phase = (uint8_t)(rand_state >> 8);
|
|
||||||
|
|
||||||
const uint8_t value = scale8(sin8(2 * time + phase), amplitude);
|
|
||||||
|
|
||||||
rgb_t rgb = rgb_matrix_hsv_to_rgb(palettefx_interp_color(palette, value));
|
|
||||||
rgb_matrix_set_color(i, rgb.r, rgb.g, rgb.b);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rgb_matrix_check_finished_leds(led_max);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(PALETTEFX_ENABLE_ALL_EFFECTS) || defined(PALETTEFX_VORTEX_ENABLE)
|
|
||||||
// "Vortex" animated effect. LEDs are animated according to a polar function
|
|
||||||
// with the appearance of a spinning vortex centered on k_rgb_matrix_center.
|
|
||||||
static bool PALETTEFX_VORTEX(effect_params_t* params) {
|
|
||||||
RGB_MATRIX_USE_LIMITS(led_min, led_max);
|
|
||||||
const uint16_t* palette = palettefx_get_palette_data();
|
|
||||||
const uint16_t time =
|
|
||||||
palettefx_scaled_time(g_rgb_timer, 1 + rgb_matrix_config.speed / 4);
|
|
||||||
|
|
||||||
for (uint8_t i = led_min; i < led_max; ++i) {
|
|
||||||
RGB_MATRIX_TEST_LED_FLAGS();
|
|
||||||
const int16_t x = g_led_config.point[i].x - k_rgb_matrix_center.x;
|
|
||||||
const int16_t y = g_led_config.point[i].y - k_rgb_matrix_center.y;
|
|
||||||
uint8_t value = sin8(atan2_8(y, x) + time - sqrt16(x * x + y * y) / 2);
|
|
||||||
|
|
||||||
rgb_t rgb = rgb_matrix_hsv_to_rgb(palettefx_interp_color(palette, value));
|
|
||||||
rgb_matrix_set_color(i, rgb.r, rgb.g, rgb.b);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rgb_matrix_check_finished_leds(led_max);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(RGB_MATRIX_KEYREACTIVE_ENABLED) && ( \
|
|
||||||
defined(PALETTEFX_ENABLE_ALL_EFFECTS) || defined(PALETTEFX_REACTIVE_ENABLE))
|
|
||||||
// Reactive animated effect. This effect is "reactive," it responds to key
|
|
||||||
// presses. For each key press, LEDs near the key change momentarily.
|
|
||||||
static bool PALETTEFX_REACTIVE(effect_params_t* params) {
|
|
||||||
RGB_MATRIX_USE_LIMITS(led_min, led_max);
|
|
||||||
const uint16_t* palette = palettefx_get_palette_data();
|
|
||||||
const uint8_t count = g_last_hit_tracker.count;
|
|
||||||
|
|
||||||
uint8_t amplitude(uint8_t t) { // Bump amplitude as a function of time.
|
|
||||||
if (t <= 55) {
|
|
||||||
return (t < 32) ? (4 + 8 * t) : 255;
|
|
||||||
} else {
|
|
||||||
t = (((uint16_t)(255 - t)) * UINT16_C(164)) >> 7;
|
|
||||||
return scale8(t, t);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t hit_amplitude[LED_HITS_TO_REMEMBER] = {0};
|
|
||||||
for (uint8_t j = 0; j < count; ++j) {
|
|
||||||
const uint16_t tick = scale16by8(g_last_hit_tracker.tick[j],
|
|
||||||
1 + rgb_matrix_config.speed / 4);
|
|
||||||
if (tick <= 255) {
|
|
||||||
hit_amplitude[j] = amplitude((uint8_t)tick);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (uint8_t i = led_min; i < led_max; ++i) {
|
|
||||||
RGB_MATRIX_TEST_LED_FLAGS();
|
|
||||||
uint8_t value = 0;
|
|
||||||
|
|
||||||
for (uint8_t j = 0; j < count; ++j) {
|
|
||||||
if (hit_amplitude[j] == 0) { continue; }
|
|
||||||
|
|
||||||
uint8_t dx = abs8((g_led_config.point[i].x - g_last_hit_tracker.x[j]) / 2);
|
|
||||||
uint8_t dy = abs8((g_led_config.point[i].y - g_last_hit_tracker.y[j]) / 2);
|
|
||||||
if (dx < 21 && dy < 21) {
|
|
||||||
const uint16_t dist_sqr = dx * dx + dy * dy;
|
|
||||||
if (dist_sqr < 21 * 21) { // Accumulate a radial bump for each hit.
|
|
||||||
const uint8_t dist = sqrt16(dist_sqr);
|
|
||||||
value = qadd8(value, scale8(255 - 12 * dist, hit_amplitude[j]));
|
|
||||||
// Early loop exit where the value has saturated.
|
|
||||||
if (value == 255) { break; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hsv_t hsv = palettefx_interp_color(palette, value);
|
|
||||||
if (value < 32) { // Make the background dark regardless of palette.
|
|
||||||
hsv.v = scale8(hsv.v, 64 + 6 * value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const rgb_t rgb = rgb_matrix_hsv_to_rgb(hsv);
|
|
||||||
rgb_matrix_set_color(i, rgb.r, rgb.g, rgb.b);
|
|
||||||
}
|
|
||||||
return rgb_matrix_check_finished_leds(led_max);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#endif // RGB_MATRIX_CUSTOM_EFFECT_IMPLS
|
|
||||||
#endif // COMMUNITY_MODULE_PALETTEFX_ENABLE
|
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
// Copyright 2024-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.
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
// If nothing has been configured, default to enabling all effects and palettes.
|
|
||||||
#if !( \
|
|
||||||
__has_include("palettefx_user.inc") \
|
|
||||||
|| defined(PALETTEFX_ENABLE_ALL_EFFECTS) \
|
|
||||||
|| defined(PALETTEFX_ENABLE_ALL_PALETTES) \
|
|
||||||
|| defined(PALETTEFX_GRADIENT_ENABLE) \
|
|
||||||
|| defined(PALETTEFX_FLOW_ENABLE) \
|
|
||||||
|| defined(PALETTEFX_RIPPLE_ENABLE) \
|
|
||||||
|| defined(PALETTEFX_SPARKLE_ENABLE) \
|
|
||||||
|| defined(PALETTEFX_VORTEX_ENABLE) \
|
|
||||||
|| defined(PALETTEFX_REACTIVE_ENABLE) \
|
|
||||||
|| defined(PALETTEFX_AFTERBURN_ENABLE) \
|
|
||||||
|| defined(PALETTEFX_AMBER_ENABLE) \
|
|
||||||
|| defined(PALETTEFX_BADWOLF_ENABLE) \
|
|
||||||
|| defined(PALETTEFX_CARNIVAL_ENABLE) \
|
|
||||||
|| defined(PALETTEFX_CLASSIC_ENABLE) \
|
|
||||||
|| defined(PALETTEFX_DRACULA_ENABLE) \
|
|
||||||
|| defined(PALETTEFX_GROOVY_ENABLE) \
|
|
||||||
|| defined(PALETTEFX_NOTPINK_ENABLE) \
|
|
||||||
|| defined(PALETTEFX_PHOSPHOR_ENABLE) \
|
|
||||||
|| defined(PALETTEFX_POLARIZED_ENABLE) \
|
|
||||||
|| defined(PALETTEFX_ROSEGOLD_ENABLE) \
|
|
||||||
|| defined(PALETTEFX_SPORT_ENABLE) \
|
|
||||||
|| defined(PALETTEFX_SYNTHWAVE_ENABLE) \
|
|
||||||
|| defined(PALETTEFX_THERMAL_ENABLE) \
|
|
||||||
|| defined(PALETTEFX_VIRIDIS_ENABLE) \
|
|
||||||
|| defined(PALETTEFX_WATERMELON_ENABLE) \
|
|
||||||
)
|
|
||||||
#define PALETTEFX_ENABLE_ALL_EFFECTS
|
|
||||||
#define PALETTEFX_ENABLE_ALL_PALETTES
|
|
||||||
#endif
|
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"module_name": "PaletteFx",
|
|
||||||
"maintainer": "getreuer",
|
|
||||||
"url": "https://getreuer.info/posts/keyboards/palettefx",
|
|
||||||
"features": {
|
|
||||||
"rgb_matrix": true
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
# 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.
|
|
||||||
|
|
||||||
RGB_MATRIX_CUSTOM_USER = yes
|
|
|
@ -1,68 +0,0 @@
|
||||||
# Select Word
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<tr><td><b>Module</b></td><td><tt>getreuer/select_word</tt></td></tr>
|
|
||||||
<tr><td><b>Version</b></td><td>2025-03-07</td></tr>
|
|
||||||
<tr><td><b>Maintainer</b></td><td>Pascal Getreuer (@getreuer)</td></tr>
|
|
||||||
<tr><td><b>License</b></td><td><a href="../LICENSE.txt">Apache 2.0</a></td></tr>
|
|
||||||
<tr><td><b>Documentation</b></td><td>
|
|
||||||
<a href="https://getreuer.info/posts/keyboards/select-word">https://getreuer.info/posts/keyboards/select-word</a>
|
|
||||||
</td></tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
This is a community module adaptation of [Select
|
|
||||||
Word](https://getreuer.info/posts/keyboards/select-word) for selecting words and
|
|
||||||
lines, assuming conventional text editing hotkeys.
|
|
||||||
|
|
||||||
## Use
|
|
||||||
|
|
||||||
Add the following to your `keymap.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"modules": ["getreuer/select_word"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Then use one or more of the following keycodes in your layout:
|
|
||||||
|
|
||||||
| Keycode | Short alias | Description |
|
|
||||||
|--------------------|-------------|--------------------------------------------------------|
|
|
||||||
| `SELECT_WORD` | `SELWORD` | Forward word selection. Or with Shift, line selection. |
|
|
||||||
| `SELECT_WORD_BACK` | `SELWBAK` | Backward word selection. |
|
|
||||||
| `SELECT_WORD_LINE` | `SELLINE` | Line selection. |
|
|
||||||
|
|
||||||
Press `SELWORD` to select the current word. Press it again to extend the
|
|
||||||
selection to the following word. The effect is similar to word selection (`W`)
|
|
||||||
in the [Kakoune editor](https://kakoune.org). Or for line selection, press
|
|
||||||
`SELWORD` with shift to select the current line, and press it again to extend
|
|
||||||
the selection to the following line.
|
|
||||||
|
|
||||||
## Mac hotkeys
|
|
||||||
|
|
||||||
Different hotkeys are needed to perform word and line selection on Mac OS. There
|
|
||||||
are several ways that Select Word can be configured to send the appropriate
|
|
||||||
hotkeys:
|
|
||||||
|
|
||||||
* To hardcode to Mac hotkeys, define in your `config.h` file:
|
|
||||||
|
|
||||||
~~~{.c}
|
|
||||||
#define SELECT_WORD_OS_MAC
|
|
||||||
~~~
|
|
||||||
|
|
||||||
* If [OS Detection](https://docs.qmk.fm/features/os_detection) is enabled,
|
|
||||||
Select Word uses it determine which kind of hotkeys to send.
|
|
||||||
|
|
||||||
* For direct control, define in `config.h`:
|
|
||||||
|
|
||||||
~~~{.c}
|
|
||||||
#define SELECT_WORD_OS_DYNAMIC
|
|
||||||
~~~
|
|
||||||
|
|
||||||
Then in `keymap.c`, define the callback `select_word_host_is_mac()`. Return
|
|
||||||
true for Mac hotkeys, false for Windows/Linux.
|
|
||||||
|
|
||||||
See the [Select Word
|
|
||||||
documentation](https://getreuer.info/posts/keyboards/select-word) for further
|
|
||||||
configuration options and details.
|
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
// 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.
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "select_word.h"
|
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
{
|
|
||||||
"module_name": "Select Word",
|
|
||||||
"maintainer": "getreuer",
|
|
||||||
"url": "https://getreuer.info/posts/keyboards/select-word",
|
|
||||||
"keycodes": [
|
|
||||||
{
|
|
||||||
"key": "SELECT_WORD",
|
|
||||||
"aliases": [
|
|
||||||
"SELWORD"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "SELECT_WORD_BACK",
|
|
||||||
"aliases": [
|
|
||||||
"SELWBAK"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "SELECT_LINE",
|
|
||||||
"aliases": [
|
|
||||||
"SELLINE"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,301 +0,0 @@
|
||||||
// Copyright 2021-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.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @file select_word.c
|
|
||||||
* @brief Select Word community module implementation
|
|
||||||
*
|
|
||||||
* For full documentation, see
|
|
||||||
* <https://getreuer.info/posts/keyboards/select-word>
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "select_word.h"
|
|
||||||
|
|
||||||
ASSERT_COMMUNITY_MODULES_MIN_API_VERSION(1, 0, 0);
|
|
||||||
|
|
||||||
// Default to a timeout of 5 seconds.
|
|
||||||
#ifndef SELECT_WORD_TIMEOUT
|
|
||||||
# define SELECT_WORD_TIMEOUT 5000
|
|
||||||
#endif // SELECT_WORD_TIMEOUT
|
|
||||||
|
|
||||||
static int8_t selection_dir = 0;
|
|
||||||
static bool reset_before_next_event = false;
|
|
||||||
static uint8_t registered_hotkey = KC_NO;
|
|
||||||
|
|
||||||
// Macro `IS_MAC` determines whether to use Mac vs. Windows/Linux hotkeys:
|
|
||||||
//
|
|
||||||
// * OS Detection is used if it is enabled.
|
|
||||||
//
|
|
||||||
// * With SELECT_WORD_OS_DYNAMIC, the user may define callback
|
|
||||||
// select_word_host_is_mac().
|
|
||||||
//
|
|
||||||
// * Otherwise, the assumed OS is set at compile time, to Window/Linux by
|
|
||||||
// default, or to Mac with SELECT_WORD_OS_MAC.
|
|
||||||
#if defined(SELECT_WORD_OS_DYNAMIC) || defined(OS_DETECTION_ENABLE)
|
|
||||||
__attribute__((weak)) bool select_word_host_is_mac(void) {
|
|
||||||
# ifdef OS_DETECTION_ENABLE // Use OS Detection if enabled.
|
|
||||||
switch (detected_host_os()) {
|
|
||||||
case OS_LINUX:
|
|
||||||
case OS_WINDOWS:
|
|
||||||
return false;
|
|
||||||
case OS_MACOS:
|
|
||||||
case OS_IOS:
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
# endif // OS_DETECTION_ENABLE
|
|
||||||
# ifdef SELECT_WORD_OS_MAC
|
|
||||||
return true;
|
|
||||||
# else
|
|
||||||
return false;
|
|
||||||
# endif // SELECT_WORD_OS_MAC
|
|
||||||
}
|
|
||||||
# define IS_MAC select_word_host_is_mac()
|
|
||||||
#else
|
|
||||||
# ifdef SELECT_WORD_OS_MAC
|
|
||||||
# define IS_MAC true
|
|
||||||
# else
|
|
||||||
# define IS_MAC false
|
|
||||||
# endif // SELECT_WORD_OS_MAC
|
|
||||||
#endif // defined(SELECT_WORD_OS_DYNAMIC) || defined(OS_DETECTION_ENABLE)
|
|
||||||
|
|
||||||
// Idle timeout timer to reset Select Word after a period of inactivity.
|
|
||||||
#if SELECT_WORD_TIMEOUT > 0
|
|
||||||
# if SELECT_WORD_TIMEOUT < 100 || SELECT_WORD_TIMEOUT > 30000
|
|
||||||
// Constrain timeout to a sensible range. With the 16-bit timer, the longest
|
|
||||||
// representable timeout is 32768 ms, rounded here to 30000 ms = half a minute.
|
|
||||||
# error "select_word: SELECT_WORD_TIMEOUT must be between 100 and 30000 ms"
|
|
||||||
# endif
|
|
||||||
|
|
||||||
static uint16_t idle_timer = 0;
|
|
||||||
|
|
||||||
static void restart_idle_timer(void) {
|
|
||||||
idle_timer = (timer_read() + SELECT_WORD_TIMEOUT) | 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
void housekeeping_task_select_word(void) {
|
|
||||||
if (idle_timer && timer_expired(timer_read(), idle_timer)) {
|
|
||||||
idle_timer = 0;
|
|
||||||
selection_dir = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif // SELECT_WORD_TIMEOUT > 0
|
|
||||||
|
|
||||||
static void clear_all_mods(void) {
|
|
||||||
clear_mods();
|
|
||||||
clear_weak_mods();
|
|
||||||
#ifndef NO_ACTION_ONESHOT
|
|
||||||
clear_oneshot_mods();
|
|
||||||
#endif // NO_ACTION_ONESHOT
|
|
||||||
}
|
|
||||||
|
|
||||||
static void select_word_in_dir(int8_t dir) {
|
|
||||||
// With Windows and Linux (non-Mac) systems:
|
|
||||||
// dir < 0: Backward word selection: Ctrl+Left, Ctrl+Right, Ctrl+Shift+Left.
|
|
||||||
// dir > 0: Forward word selection: Ctrl+Right, Ctrl+Left, Ctrl+Shift+Right.
|
|
||||||
// Or to extend an existing selection:
|
|
||||||
// dir < 0: Backward word selection: Ctrl+Shift+Left.
|
|
||||||
// dir > 0: Forward word selection: Ctrl+Shift+Right.
|
|
||||||
//
|
|
||||||
// With Mac OS, use Alt (Opt) instead of Ctrl:
|
|
||||||
// dir < 0: Backward word selection: Alt+Left, Alt+Right, Alt+Shift+Left.
|
|
||||||
// dir > 0: Forward word selection: Alt+Right, Alt+Left, Alt+Shift+Right.
|
|
||||||
// Or to extend an existing selection:
|
|
||||||
// dir < 0: Backward word selection: Alt+Shift+Left.
|
|
||||||
// dir > 0: Forward word selection: Alt+Shift+Right.
|
|
||||||
reset_before_next_event = false;
|
|
||||||
const uint8_t saved_mods = get_mods();
|
|
||||||
clear_all_mods();
|
|
||||||
|
|
||||||
if (selection_dir && (selection_dir < 0) != (dir < 0)) { // Reversal.
|
|
||||||
send_keyboard_report();
|
|
||||||
tap_code_delay((dir < 0) ? KC_RGHT : KC_LEFT, TAP_CODE_DELAY);
|
|
||||||
}
|
|
||||||
|
|
||||||
add_mods(IS_MAC ? MOD_BIT_LALT : MOD_BIT_LCTRL);
|
|
||||||
|
|
||||||
if (selection_dir == 0) { // Initial selection.
|
|
||||||
send_keyboard_report();
|
|
||||||
send_string_with_delay_P(
|
|
||||||
(dir < 0) ? PSTR(SS_TAP(X_LEFT) SS_TAP(X_RGHT))
|
|
||||||
: PSTR(SS_TAP(X_RGHT) SS_TAP(X_LEFT)),
|
|
||||||
TAP_CODE_DELAY);
|
|
||||||
}
|
|
||||||
|
|
||||||
register_mods(MOD_BIT_LSHIFT);
|
|
||||||
registered_hotkey = (dir < 0) ? KC_LEFT : KC_RGHT;
|
|
||||||
register_code(registered_hotkey);
|
|
||||||
|
|
||||||
set_mods(saved_mods);
|
|
||||||
selection_dir = dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void select_line(void) {
|
|
||||||
// With Windows and Linux (non-Mac) systems:
|
|
||||||
// Home, Shift+End.
|
|
||||||
// Or to extend an existing selection:
|
|
||||||
// Shift+Down.
|
|
||||||
//
|
|
||||||
// With Mac OS, use GUI (Command) + arrows:
|
|
||||||
// GUI+Left, Shift+GUI+Right.
|
|
||||||
// Or to extend an existing selection:
|
|
||||||
// Shift+Down.
|
|
||||||
reset_before_next_event = false;
|
|
||||||
const uint8_t saved_mods = get_mods();
|
|
||||||
clear_all_mods();
|
|
||||||
|
|
||||||
if (selection_dir != 2) {
|
|
||||||
send_keyboard_report();
|
|
||||||
send_string_with_delay_P(
|
|
||||||
IS_MAC ? PSTR(SS_LGUI(SS_TAP(X_LEFT) SS_LSFT(SS_TAP(X_RGHT))))
|
|
||||||
: PSTR(SS_TAP(X_HOME) SS_LSFT(SS_TAP(X_END))),
|
|
||||||
TAP_CODE_DELAY);
|
|
||||||
} else {
|
|
||||||
register_mods(MOD_BIT_LSHIFT);
|
|
||||||
registered_hotkey = KC_DOWN;
|
|
||||||
register_code(KC_DOWN);
|
|
||||||
}
|
|
||||||
|
|
||||||
set_mods(saved_mods);
|
|
||||||
selection_dir = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
void select_word_register(char action) {
|
|
||||||
if (registered_hotkey) {
|
|
||||||
select_word_unregister();
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (action) {
|
|
||||||
case 'W':
|
|
||||||
select_word_in_dir(1);
|
|
||||||
break;
|
|
||||||
case 'B':
|
|
||||||
select_word_in_dir(-1);
|
|
||||||
break;
|
|
||||||
case 'L':
|
|
||||||
select_line();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
#if SELECT_WORD_TIMEOUT > 0
|
|
||||||
idle_timer = 0;
|
|
||||||
#endif // SELECT_WORD_TIMEOUT > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
void select_word_unregister(void) {
|
|
||||||
reset_before_next_event = false;
|
|
||||||
unregister_code(registered_hotkey);
|
|
||||||
|
|
||||||
if (registered_hotkey == KC_DOWN) {
|
|
||||||
// When using line selection to select multiple lines, tap Shift+End (or on
|
|
||||||
// Mac, GUI+Shift+Right) on release to ensure the selection extends to the
|
|
||||||
// end of the current line.
|
|
||||||
const uint8_t saved_mods = get_mods();
|
|
||||||
clear_all_mods();
|
|
||||||
send_keyboard_report();
|
|
||||||
send_string_with_delay_P(
|
|
||||||
IS_MAC ? PSTR(SS_LGUI(SS_LSFT(SS_TAP(X_RGHT))))
|
|
||||||
: PSTR(SS_LSFT(SS_TAP(X_END))),
|
|
||||||
TAP_CODE_DELAY);
|
|
||||||
set_mods(saved_mods);
|
|
||||||
}
|
|
||||||
|
|
||||||
registered_hotkey = KC_NO;
|
|
||||||
#if SELECT_WORD_TIMEOUT > 0
|
|
||||||
restart_idle_timer();
|
|
||||||
#endif // SELECT_WORD_TIMEOUT > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
bool process_record_select_word(uint16_t keycode, keyrecord_t* record) {
|
|
||||||
if (!process_record_select_word_kb(keycode, record)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selection_dir) {
|
|
||||||
if (reset_before_next_event) {
|
|
||||||
selection_dir = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore most modifier and layer switch keys.
|
|
||||||
switch (keycode) {
|
|
||||||
case MODIFIER_KEYCODE_RANGE:
|
|
||||||
case QK_MOMENTARY ... QK_MOMENTARY_MAX:
|
|
||||||
case QK_LAYER_MOD ... QK_LAYER_MOD_MAX:
|
|
||||||
case QK_LAYER_TAP_TOGGLE ... QK_LAYER_TAP_TOGGLE_MAX:
|
|
||||||
case QK_TO ... QK_TO_MAX:
|
|
||||||
case QK_TOGGLE_LAYER ... QK_TOGGLE_LAYER_MAX:
|
|
||||||
#ifndef NO_ACTION_ONESHOT
|
|
||||||
case QK_ONE_SHOT_LAYER ... QK_ONE_SHOT_LAYER_MAX:
|
|
||||||
case QK_ONE_SHOT_MOD ... QK_ONE_SHOT_MOD_MAX:
|
|
||||||
#endif // NO_ACTION_ONESHOT
|
|
||||||
#ifdef LAYER_LOCK_ENABLE
|
|
||||||
case QK_LLCK:
|
|
||||||
#endif // LAYER_LOCK_ENABLE
|
|
||||||
return true;
|
|
||||||
#ifndef NO_ACTION_TAPPING
|
|
||||||
// Ignore hold events on mod-tap and layer-tap keys.
|
|
||||||
case QK_MOD_TAP ... QK_MOD_TAP_MAX:
|
|
||||||
case QK_LAYER_TAP ... QK_LAYER_TAP_MAX:
|
|
||||||
if (record->tap.count == 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
#endif // NO_ACTION_TAPPING
|
|
||||||
}
|
|
||||||
|
|
||||||
reset_before_next_event = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
#if SELECT_WORD_TIMEOUT > 0
|
|
||||||
if (idle_timer) {
|
|
||||||
restart_idle_timer();
|
|
||||||
}
|
|
||||||
#endif // SELECT_WORD_TIMEOUT > 0
|
|
||||||
|
|
||||||
const bool shifted = MOD_MASK_SHIFT & (get_mods() | get_weak_mods()
|
|
||||||
#ifndef NO_ACTION_ONESHOT
|
|
||||||
| get_oneshot_mods()
|
|
||||||
#endif // NO_ACTION_ONESHOT
|
|
||||||
);
|
|
||||||
|
|
||||||
switch (keycode) {
|
|
||||||
case SELECT_WORD:
|
|
||||||
if (record->event.pressed) {
|
|
||||||
select_word_register(shifted ? 'L' : 'W');
|
|
||||||
} else {
|
|
||||||
select_word_unregister();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
|
|
||||||
case SELECT_WORD_BACK:
|
|
||||||
if (record->event.pressed) {
|
|
||||||
select_word_register('B');
|
|
||||||
} else {
|
|
||||||
select_word_unregister();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
|
|
||||||
case SELECT_LINE:
|
|
||||||
if (record->event.pressed) {
|
|
||||||
select_word_register('L');
|
|
||||||
} else {
|
|
||||||
select_word_unregister();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,91 +0,0 @@
|
||||||
// Copyright 2021-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.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @file select_word.h
|
|
||||||
* @brief Select Word community module: select words and lines.
|
|
||||||
*
|
|
||||||
* Overview
|
|
||||||
* --------
|
|
||||||
*
|
|
||||||
* Implements a button that selects the current word, assuming conventional text
|
|
||||||
* editor hotkeys. Pressing it again extends the selection to the following
|
|
||||||
* word. The effect is similar to word selection (W) in the Kakoune editor.
|
|
||||||
*
|
|
||||||
* Pressing the button with shift selects the current line, and pressing the
|
|
||||||
* button again extends the selection to the following line.
|
|
||||||
*
|
|
||||||
* For full documentation, see
|
|
||||||
* <https://getreuer.info/posts/keyboards/select-word>
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "quantum.h"
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Registers (presses) selection `action`.
|
|
||||||
*
|
|
||||||
* The `action` argument in these functions specifies the type of selection:
|
|
||||||
*
|
|
||||||
* 'W' = word selection
|
|
||||||
* 'B' = backward word selection, left of the cursor
|
|
||||||
* 'L' = line selection
|
|
||||||
*
|
|
||||||
* A selection is first registered with `select_word_register(action)`. This
|
|
||||||
* should be followed by a call to `select_word_unregister()` to unregister the
|
|
||||||
* hotkeys. The point of these separate register and unregister calls is to
|
|
||||||
* enable holding the hotkey as a means to extend the selection range.
|
|
||||||
*
|
|
||||||
* @warning Forgetting to unregister results in stuck keys:
|
|
||||||
* `select_word_register()` must be followed by `select_word_unregister()`.
|
|
||||||
*
|
|
||||||
* @param action Type of selection to perform.
|
|
||||||
*/
|
|
||||||
void select_word_register(char action);
|
|
||||||
|
|
||||||
/** Unregisters (releases) selection hotkey. */
|
|
||||||
void select_word_unregister(void);
|
|
||||||
|
|
||||||
/** Registers and unregisters ("taps") selection `action.` */
|
|
||||||
static inline void select_word_tap(char action) {
|
|
||||||
select_word_register(action);
|
|
||||||
wait_ms(TAP_CODE_DELAY);
|
|
||||||
select_word_unregister();
|
|
||||||
}
|
|
||||||
|
|
||||||
#if defined(SELECT_WORD_OS_DYNAMIC) || defined(OS_DETECTION_ENABLE)
|
|
||||||
/**
|
|
||||||
* @brief Callback for whether the host uses Mac vs. Windows/Linux hotkeys.
|
|
||||||
*
|
|
||||||
* Optionally, this callback may be defined to indicate dynamically whether the
|
|
||||||
* keyboard is being used with a Mac or non-Mac system.
|
|
||||||
*
|
|
||||||
* For instance suppose layer 0 is your base layer for Windows and layer 1 is
|
|
||||||
* your base layer for Mac. Indicate this by adding in keymap.c:
|
|
||||||
*
|
|
||||||
* bool select_word_host_is_mac(void) {
|
|
||||||
* return IS_LAYER_ON(1); // Supposing layer 1 = base layer for Mac.
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
bool select_word_host_is_mac(void);
|
|
||||||
#endif // defined(SELECT_WORD_OS_DYNAMIC) || defined(OS_DETECTION_ENABLE)
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
}
|
|
||||||
#endif
|
|
|
@ -1,60 +0,0 @@
|
||||||
# Sentence Case
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<tr><td><b>Module</b></td><td><tt>getreuer/sentence_case</tt></td></tr>
|
|
||||||
<tr><td><b>Version</b></td><td>2025-03-07</td></tr>
|
|
||||||
<tr><td><b>Maintainer</b></td><td>Pascal Getreuer (@getreuer)</td></tr>
|
|
||||||
<tr><td><b>License</b></td><td><a href="../LICENSE.txt">Apache 2.0</a></td></tr>
|
|
||||||
<tr><td><b>Documentation</b></td><td>
|
|
||||||
<a href="https://getreuer.info/posts/keyboards/sentence-case">https://getreuer.info/posts/keyboards/sentence-case</a>
|
|
||||||
</td></tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
This is a community module adaptation of [Sentence
|
|
||||||
Case](https://getreuer.info/posts/keyboards/sentence-case) to automatically
|
|
||||||
capitalize the first letter of sentences. This reduces how often you need to use
|
|
||||||
the Shift keys, which is convenient particularly if you use home row mods or
|
|
||||||
Auto Shift.
|
|
||||||
|
|
||||||
Add the following to your `keymap.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"modules": ["getreuer/sentence_case"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
NOTE: One-shot keys must be enabled.
|
|
||||||
|
|
||||||
Then, simply type as usual but without shifting at the start of sentences. The
|
|
||||||
feature detects when new sentences begin and capitalizes automatically.
|
|
||||||
|
|
||||||
In detecting new sentences, Sentence Case matches patterns like
|
|
||||||
|
|
||||||
"a. a"
|
|
||||||
"a. a"
|
|
||||||
"a? a"
|
|
||||||
"a!' 'a"
|
|
||||||
|
|
||||||
but not
|
|
||||||
|
|
||||||
"a... a"
|
|
||||||
"a.a. a"
|
|
||||||
|
|
||||||
Additionally by default, abbreviations "`vs.`" and "`etc.`" are exceptionally
|
|
||||||
detected as not real sentence endings.
|
|
||||||
|
|
||||||
Sentence Case is on by default. The following keycodes change its status. Use
|
|
||||||
function `is_sentence_case_on()` to query its status.
|
|
||||||
|
|
||||||
| Keycode | Description |
|
|
||||||
|------------------------|---------------------------|
|
|
||||||
| `SENTENCE_CASE_ON` | Turn Sentence Case on. |
|
|
||||||
| `SENTENCE_CASE_OFF` | Turn Sentence Case off. |
|
|
||||||
| `SENTENCE_CASE_TOGGLE` | Toggle Sentence Case. |
|
|
||||||
|
|
||||||
Optionally, you can use the callback `sentence_case_check_ending()` to define
|
|
||||||
other exceptions, and there are callbacks and config options to customize
|
|
||||||
Sentence Case. See the [Sentence Case
|
|
||||||
documentation](https://getreuer.info/posts/keyboards/sentence-case) for details.
|
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
// 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.
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "sentence_case.h"
|
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
{
|
|
||||||
"module_name": "Sentence Case",
|
|
||||||
"maintainer": "getreuer",
|
|
||||||
"url": "https://getreuer.info/posts/keyboards/sentence-case",
|
|
||||||
"keycodes": [
|
|
||||||
{"key": "SENTENCE_CASE_ON"},
|
|
||||||
{"key": "SENTENCE_CASE_OFF"},
|
|
||||||
{"key": "SENTENCE_CASE_TOGGLE"}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,380 +0,0 @@
|
||||||
// Copyright 2022-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.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @file sentence_case.c
|
|
||||||
* @brief Sentence Case community module implementation
|
|
||||||
*
|
|
||||||
* For full documentation, see
|
|
||||||
* <https://getreuer.info/posts/keyboards/sentence-case>
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "sentence_case.h"
|
|
||||||
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#if defined(NO_ACTION_ONESHOT)
|
|
||||||
// One-shot keys must be enabled for Sentence Case. One-shot keys are enabled
|
|
||||||
// by default, but are disabled by `#define NO_ACTION_ONESHOT` in config.h. If
|
|
||||||
// your config.h includes such a line, please remove it.
|
|
||||||
#error "sentence_case: Please enable oneshot."
|
|
||||||
#else
|
|
||||||
|
|
||||||
ASSERT_COMMUNITY_MODULES_MIN_API_VERSION(1, 0, 0);
|
|
||||||
|
|
||||||
// Default to a timeout of 5 seconds.
|
|
||||||
#ifndef SENTENCE_CASE_TIMEOUT
|
|
||||||
# define SENTENCE_CASE_TIMEOUT 5000
|
|
||||||
#endif // SENTENCE_CASE_TIMEOUT
|
|
||||||
|
|
||||||
// Number of keys of state history to retain for backspacing.
|
|
||||||
#define STATE_HISTORY_SIZE 6
|
|
||||||
|
|
||||||
// clang-format off
|
|
||||||
/** States in matching the beginning of a sentence. */
|
|
||||||
enum {
|
|
||||||
STATE_INIT, /**< Initial enabled state. */
|
|
||||||
STATE_WORD, /**< Within a word. */
|
|
||||||
STATE_ABBREV, /**< Within an abbreviation like "e.g.". */
|
|
||||||
STATE_ENDING, /**< Sentence ended. */
|
|
||||||
STATE_PRIMED, /**< "Primed" state, in the space following an ending. */
|
|
||||||
STATE_DISABLED, /**< Sentence Case is disabled. */
|
|
||||||
};
|
|
||||||
// clang-format on
|
|
||||||
|
|
||||||
#if SENTENCE_CASE_TIMEOUT > 0
|
|
||||||
static uint16_t idle_timer = 0;
|
|
||||||
#endif // SENTENCE_CASE_TIMEOUT > 0
|
|
||||||
#if SENTENCE_CASE_BUFFER_SIZE > 1
|
|
||||||
static uint16_t key_buffer[SENTENCE_CASE_BUFFER_SIZE] = {0};
|
|
||||||
#endif // SENTENCE_CASE_BUFFER_SIZE > 1
|
|
||||||
static uint8_t state_history[STATE_HISTORY_SIZE];
|
|
||||||
static uint16_t suppress_key = KC_NO;
|
|
||||||
static uint8_t sentence_state = STATE_INIT;
|
|
||||||
|
|
||||||
// Sets the current state to `new_state`.
|
|
||||||
static void set_sentence_state(uint8_t new_state) {
|
|
||||||
#if !defined(NO_DEBUG) && defined(SENTENCE_CASE_DEBUG)
|
|
||||||
if (debug_enable && sentence_state != new_state) {
|
|
||||||
static const char* state_names[] = {
|
|
||||||
"INIT", "WORD", "ABBREV", "ENDING", "PRIMED", "DISABLED",
|
|
||||||
};
|
|
||||||
dprintf("Sentence case: %s\n", state_names[new_state]);
|
|
||||||
}
|
|
||||||
#endif // !NO_DEBUG && SENTENCE_CASE_DEBUG
|
|
||||||
|
|
||||||
const bool primed = (new_state == STATE_PRIMED);
|
|
||||||
if (primed != (sentence_state == STATE_PRIMED)) {
|
|
||||||
sentence_case_primed(primed);
|
|
||||||
}
|
|
||||||
sentence_state = new_state;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void clear_state_history(void) {
|
|
||||||
#if SENTENCE_CASE_TIMEOUT > 0
|
|
||||||
idle_timer = 0;
|
|
||||||
#endif // SENTENCE_CASE_TIMEOUT > 0
|
|
||||||
memset(state_history, STATE_INIT, sizeof(state_history));
|
|
||||||
if (sentence_state != STATE_DISABLED) {
|
|
||||||
set_sentence_state(STATE_INIT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void sentence_case_clear(void) {
|
|
||||||
clear_state_history();
|
|
||||||
suppress_key = KC_NO;
|
|
||||||
#if SENTENCE_CASE_BUFFER_SIZE > 1
|
|
||||||
memset(key_buffer, 0, sizeof(key_buffer));
|
|
||||||
#endif // SENTENCE_CASE_BUFFER_SIZE > 1
|
|
||||||
}
|
|
||||||
|
|
||||||
void sentence_case_on(void) {
|
|
||||||
if (sentence_state == STATE_DISABLED) {
|
|
||||||
sentence_state = STATE_INIT;
|
|
||||||
sentence_case_clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void sentence_case_off(void) {
|
|
||||||
if (sentence_state != STATE_DISABLED) {
|
|
||||||
set_sentence_state(STATE_DISABLED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void sentence_case_toggle(void) {
|
|
||||||
if (sentence_state != STATE_DISABLED) {
|
|
||||||
sentence_case_off();
|
|
||||||
} else {
|
|
||||||
sentence_case_on();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool is_sentence_case_on(void) { return sentence_state != STATE_DISABLED; }
|
|
||||||
bool is_sentence_case_primed(void) { return sentence_state == STATE_PRIMED; }
|
|
||||||
|
|
||||||
#if SENTENCE_CASE_TIMEOUT > 0
|
|
||||||
#if SENTENCE_CASE_TIMEOUT < 100 || SENTENCE_CASE_TIMEOUT > 30000
|
|
||||||
// Constrain timeout to a sensible range. With the 16-bit timer, the longest
|
|
||||||
// representable timeout is 32768 ms, rounded here to 30000 ms = half a minute.
|
|
||||||
#error "sentence_case: SENTENCE_CASE_TIMEOUT must be between 100 and 30000 ms"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
void housekeeping_task_sentence_case(void) {
|
|
||||||
if (idle_timer && timer_expired(timer_read(), idle_timer)) {
|
|
||||||
clear_state_history(); // Timed out; clear all state.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif // SENTENCE_CASE_TIMEOUT > 0
|
|
||||||
|
|
||||||
bool process_record_sentence_case(uint16_t keycode, keyrecord_t* record) {
|
|
||||||
// Only process while enabled, and only process press events.
|
|
||||||
if (sentence_state == STATE_DISABLED || !record->event.pressed) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
#if SENTENCE_CASE_TIMEOUT > 0
|
|
||||||
idle_timer = (record->event.time + SENTENCE_CASE_TIMEOUT) | 1;
|
|
||||||
#endif // SENTENCE_CASE_TIMEOUT > 0
|
|
||||||
|
|
||||||
switch (keycode) {
|
|
||||||
case SENTENCE_CASE_ON:
|
|
||||||
sentence_case_on();
|
|
||||||
return false;
|
|
||||||
case SENTENCE_CASE_OFF:
|
|
||||||
sentence_case_off();
|
|
||||||
return false;
|
|
||||||
case SENTENCE_CASE_TOGGLE:
|
|
||||||
sentence_case_toggle();
|
|
||||||
return false;
|
|
||||||
|
|
||||||
case KC_LCTL ... KC_RGUI: // Ignore mod keys.
|
|
||||||
case QK_ONE_SHOT_MOD ... QK_ONE_SHOT_MOD_MAX: // Ignore one-shot mod.
|
|
||||||
// Ignore MO, TO, TG, TT, OSL, TL layer switch keys.
|
|
||||||
case QK_MOMENTARY ... QK_MOMENTARY_MAX:
|
|
||||||
case QK_TO ... QK_TO_MAX:
|
|
||||||
case QK_TOGGLE_LAYER ... QK_TOGGLE_LAYER_MAX:
|
|
||||||
case QK_LAYER_TAP_TOGGLE ... QK_LAYER_TAP_TOGGLE_MAX:
|
|
||||||
case QK_ONE_SHOT_LAYER ... QK_ONE_SHOT_LAYER_MAX: // Ignore one-shot layer.
|
|
||||||
#ifdef TRI_LAYER_ENABLE // Ignore Tri Layer keys.
|
|
||||||
case QK_TRI_LAYER_LOWER:
|
|
||||||
case QK_TRI_LAYER_UPPER:
|
|
||||||
#endif // TRI_LAYER_ENABLE
|
|
||||||
return true;
|
|
||||||
|
|
||||||
#ifndef NO_ACTION_TAPPING
|
|
||||||
case QK_MOD_TAP ... QK_MOD_TAP_MAX:
|
|
||||||
if (record->tap.count == 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
keycode = QK_MOD_TAP_GET_TAP_KEYCODE(keycode);
|
|
||||||
break;
|
|
||||||
#ifndef NO_ACTION_LAYER
|
|
||||||
case QK_LAYER_TAP ... QK_LAYER_TAP_MAX:
|
|
||||||
if (record->tap.count == 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
keycode = QK_LAYER_TAP_GET_TAP_KEYCODE(keycode);
|
|
||||||
break;
|
|
||||||
#endif // NO_ACTION_LAYER
|
|
||||||
#endif // NO_ACTION_TAPPING
|
|
||||||
|
|
||||||
#ifdef SWAP_HANDS_ENABLE
|
|
||||||
case QK_SWAP_HANDS ... QK_SWAP_HANDS_MAX:
|
|
||||||
if (IS_SWAP_HANDS_KEYCODE(keycode) || record->tap.count == 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
keycode = QK_SWAP_HANDS_GET_TAP_KEYCODE(keycode);
|
|
||||||
break;
|
|
||||||
#endif // SWAP_HANDS_ENABLE
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keycode == KC_BSPC) {
|
|
||||||
// Backspace key pressed. Rewind the state and key buffers.
|
|
||||||
set_sentence_state(state_history[STATE_HISTORY_SIZE - 1]);
|
|
||||||
|
|
||||||
memmove(state_history + 1, state_history, STATE_HISTORY_SIZE - 1);
|
|
||||||
state_history[0] = STATE_INIT;
|
|
||||||
#if SENTENCE_CASE_BUFFER_SIZE > 1
|
|
||||||
memmove(key_buffer + 1, key_buffer,
|
|
||||||
(SENTENCE_CASE_BUFFER_SIZE - 1) * sizeof(uint16_t));
|
|
||||||
key_buffer[0] = KC_NO;
|
|
||||||
#endif // SENTENCE_CASE_BUFFER_SIZE > 1
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uint8_t mods = get_mods() | get_weak_mods() | get_oneshot_mods();
|
|
||||||
uint8_t new_state = STATE_INIT;
|
|
||||||
|
|
||||||
// We search for sentence beginnings using a simple finite state machine. It
|
|
||||||
// matches things like "a. a" and "a. a" but not "a.. a" or "a.a. a". The
|
|
||||||
// state transition matrix is:
|
|
||||||
//
|
|
||||||
// 'a' '.' ' ' '\''
|
|
||||||
// +-------------------------------------
|
|
||||||
// INIT | WORD INIT INIT INIT
|
|
||||||
// WORD | WORD ENDING INIT WORD
|
|
||||||
// ABBREV | ABBREV ABBREV INIT ABBREV
|
|
||||||
// ENDING | ABBREV INIT PRIMED ENDING
|
|
||||||
// PRIMED | match! INIT PRIMED PRIMED
|
|
||||||
char code = sentence_case_press_user(keycode, record, mods);
|
|
||||||
#if defined SENTENCE_CASE_DEBUG
|
|
||||||
dprintf("Sentence Case: code = '%c' (%d)\n", code, (int)code);
|
|
||||||
#endif // SENTENCE_CASE_DEBUG
|
|
||||||
switch (code) {
|
|
||||||
case '\0': // Current key should be ignored.
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case 'a': // Current key is a letter.
|
|
||||||
switch (sentence_state) {
|
|
||||||
case STATE_ABBREV:
|
|
||||||
case STATE_ENDING:
|
|
||||||
new_state = STATE_ABBREV;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case STATE_PRIMED:
|
|
||||||
// This is the start of a sentence.
|
|
||||||
if (keycode != suppress_key) {
|
|
||||||
suppress_key = keycode;
|
|
||||||
set_oneshot_mods(MOD_BIT(KC_LSFT)); // Shift mod to capitalize.
|
|
||||||
new_state = STATE_WORD;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
new_state = STATE_WORD;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case '.': // Current key is sentence-ending punctuation.
|
|
||||||
switch (sentence_state) {
|
|
||||||
case STATE_WORD:
|
|
||||||
new_state = STATE_ENDING;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
new_state = STATE_ABBREV;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ' ': // Current key is a space.
|
|
||||||
if (sentence_state == STATE_PRIMED ||
|
|
||||||
(sentence_state == STATE_ENDING
|
|
||||||
#if SENTENCE_CASE_BUFFER_SIZE > 1
|
|
||||||
&& sentence_case_check_ending(key_buffer)
|
|
||||||
#endif // SENTENCE_CASE_BUFFER_SIZE > 1
|
|
||||||
)) {
|
|
||||||
new_state = STATE_PRIMED;
|
|
||||||
suppress_key = KC_NO;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case '\'': // Current key is a quote.
|
|
||||||
new_state = sentence_state;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Slide key_buffer and state_history buffers one element to the left.
|
|
||||||
// Optimization note: Using manual loops instead of memmove() here saved
|
|
||||||
// ~100 bytes on AVR.
|
|
||||||
#if SENTENCE_CASE_BUFFER_SIZE > 1
|
|
||||||
for (int8_t i = 0; i < SENTENCE_CASE_BUFFER_SIZE - 1; ++i) {
|
|
||||||
key_buffer[i] = key_buffer[i + 1];
|
|
||||||
}
|
|
||||||
#endif // SENTENCE_CASE_BUFFER_SIZE > 1
|
|
||||||
for (int8_t i = 0; i < STATE_HISTORY_SIZE - 1; ++i) {
|
|
||||||
state_history[i] = state_history[i + 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
#if SENTENCE_CASE_BUFFER_SIZE > 1
|
|
||||||
key_buffer[SENTENCE_CASE_BUFFER_SIZE - 1] = keycode;
|
|
||||||
if (new_state == STATE_ENDING && !sentence_case_check_ending(key_buffer)) {
|
|
||||||
#if defined SENTENCE_CASE_DEBUG
|
|
||||||
dprintf("Not a real ending.\n");
|
|
||||||
#endif // SENTENCE_CASE_DEBUG
|
|
||||||
new_state = STATE_INIT;
|
|
||||||
}
|
|
||||||
#endif // SENTENCE_CASE_BUFFER_SIZE > 1
|
|
||||||
state_history[STATE_HISTORY_SIZE - 1] = sentence_state;
|
|
||||||
|
|
||||||
set_sentence_state(new_state);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool sentence_case_just_typed_P(const uint16_t* buffer, const uint16_t* pattern,
|
|
||||||
int8_t pattern_len) {
|
|
||||||
#if SENTENCE_CASE_BUFFER_SIZE > 1
|
|
||||||
buffer += SENTENCE_CASE_BUFFER_SIZE - pattern_len;
|
|
||||||
for (int8_t i = 0; i < pattern_len; ++i) {
|
|
||||||
if (buffer[i] != pgm_read_word(pattern + i)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
#else
|
|
||||||
return false;
|
|
||||||
#endif // SENTENCE_CASE_BUFFER_SIZE > 1
|
|
||||||
}
|
|
||||||
|
|
||||||
__attribute__((weak)) bool sentence_case_check_ending(const uint16_t* buffer) {
|
|
||||||
#if SENTENCE_CASE_BUFFER_SIZE >= 5
|
|
||||||
// Don't consider the abbreviations "vs." and "etc." to end the sentence.
|
|
||||||
if (SENTENCE_CASE_JUST_TYPED(KC_SPC, KC_V, KC_S, KC_DOT) ||
|
|
||||||
SENTENCE_CASE_JUST_TYPED(KC_SPC, KC_E, KC_T, KC_C, KC_DOT)) {
|
|
||||||
return false; // Not a real sentence ending.
|
|
||||||
}
|
|
||||||
#endif // SENTENCE_CASE_BUFFER_SIZE >= 5
|
|
||||||
return true; // Real sentence ending; capitalize next letter.
|
|
||||||
}
|
|
||||||
|
|
||||||
__attribute__((weak)) char sentence_case_press_user(uint16_t keycode,
|
|
||||||
keyrecord_t* record,
|
|
||||||
uint8_t mods) {
|
|
||||||
if ((mods & ~(MOD_MASK_SHIFT | MOD_BIT(KC_RALT))) == 0) {
|
|
||||||
const bool shifted = mods & MOD_MASK_SHIFT;
|
|
||||||
switch (keycode) {
|
|
||||||
case KC_A ... KC_Z:
|
|
||||||
return 'a'; // Letter key.
|
|
||||||
|
|
||||||
case KC_DOT: // . is punctuation, Shift . is a symbol (>)
|
|
||||||
return !shifted ? '.' : '#';
|
|
||||||
case KC_1:
|
|
||||||
case KC_SLSH:
|
|
||||||
return shifted ? '.' : '#';
|
|
||||||
case KC_EXLM:
|
|
||||||
case KC_QUES:
|
|
||||||
return '.';
|
|
||||||
case KC_2 ... KC_0: // 2 3 4 5 6 7 8 9 0
|
|
||||||
case KC_AT ... KC_RPRN: // @ # $ % ^ & * ( )
|
|
||||||
case KC_MINS ... KC_SCLN: // - = [ ] backslash ;
|
|
||||||
case KC_UNDS ... KC_COLN: // _ + { } | :
|
|
||||||
case KC_GRV:
|
|
||||||
case KC_COMM:
|
|
||||||
return '#'; // Symbol key.
|
|
||||||
|
|
||||||
case KC_SPC:
|
|
||||||
return ' '; // Space key.
|
|
||||||
|
|
||||||
case KC_QUOT:
|
|
||||||
return '\''; // Quote key.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise clear Sentence Case to initial state.
|
|
||||||
sentence_case_clear();
|
|
||||||
return '\0';
|
|
||||||
}
|
|
||||||
|
|
||||||
__attribute__((weak)) void sentence_case_primed(bool primed) {}
|
|
||||||
|
|
||||||
#endif // NO_ACTION_ONESHOT
|
|
|
@ -1,206 +0,0 @@
|
||||||
// Copyright 2022-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.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @file sentence_case.h
|
|
||||||
* @brief Sentence Case community module: automatic sentence capitalization.
|
|
||||||
*
|
|
||||||
* This module automatically capitalizes the first letter of sentences, reducing
|
|
||||||
* the need to explicitly use shift. To use it, you simply type as usual but
|
|
||||||
* without shifting at the start of sentences. The feature detects when new
|
|
||||||
* sentences begin and capitalizes automatically.
|
|
||||||
*
|
|
||||||
* Sentence Case matches patterns like
|
|
||||||
*
|
|
||||||
* "a. a"
|
|
||||||
* "a. a"
|
|
||||||
* "a? a"
|
|
||||||
* "a!' 'a"
|
|
||||||
*
|
|
||||||
* but not
|
|
||||||
*
|
|
||||||
* "a... a"
|
|
||||||
* "a.a. a"
|
|
||||||
*
|
|
||||||
* Additionally by default, abbreviations "vs." and "etc." are exceptionally
|
|
||||||
* detected as not real sentence endings. You can use the callback
|
|
||||||
* `sentence_case_check_ending()` to define other exceptions.
|
|
||||||
*
|
|
||||||
* @note One-shot keys must be enabled.
|
|
||||||
*
|
|
||||||
* For full documentation, see
|
|
||||||
* <https://getreuer.info/posts/keyboards/sentence-case>
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "quantum.h"
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// The size of the keycode buffer for `sentence_case_check_ending()`. It must be
|
|
||||||
// at least as large as the longest pattern checked. If less than 2, buffering
|
|
||||||
// is disabled and the callback is not called.
|
|
||||||
#ifndef SENTENCE_CASE_BUFFER_SIZE
|
|
||||||
#define SENTENCE_CASE_BUFFER_SIZE 8
|
|
||||||
#endif // SENTENCE_CASE_BUFFER_SIZE
|
|
||||||
|
|
||||||
void sentence_case_on(void); /**< Enables Sentence Case. */
|
|
||||||
void sentence_case_off(void); /**< Disables Sentence Case. */
|
|
||||||
void sentence_case_toggle(void); /**< Toggles Sentence Case. */
|
|
||||||
bool is_sentence_case_on(void); /**< Gets whether currently enabled. */
|
|
||||||
bool is_sentence_case_primed(void); /**< Whether currently primed. */
|
|
||||||
void sentence_case_clear(void); /**< Clears Sentence Case to initial state. */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Optional callback to indicate primed state.
|
|
||||||
*
|
|
||||||
* This callback gets called when Sentence Case changes to or from a "primed"
|
|
||||||
* state, useful to indicate with an LED or otherwise that the next letter typed
|
|
||||||
* will be capitalized.
|
|
||||||
*/
|
|
||||||
void sentence_case_primed(bool primed);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Optional callback to determine whether there is a real sentence ending.
|
|
||||||
*
|
|
||||||
* When a sentence-ending punctuation key is typed, this callback is called to
|
|
||||||
* determine whether it is a real sentence ending, meaning the first letter of
|
|
||||||
* the following word should be capitalized. For instance, abbreviations like
|
|
||||||
* "vs." are usually not real sentence endings. The input argument is a buffer
|
|
||||||
* of the last SENTENCE_CASE_BUFFER_SIZE keycodes. Returning true means it is a
|
|
||||||
* real sentence ending; returning false means it is not.
|
|
||||||
*
|
|
||||||
* The default implementation checks for the abbreviations "vs." and "etc.":
|
|
||||||
*
|
|
||||||
* bool sentence_case_check_ending(const uint16_t* buffer) {
|
|
||||||
* // Don't consider "vs." and "etc." to end the sentence.
|
|
||||||
* if (SENTENCE_CASE_JUST_TYPED(KC_SPC, KC_V, KC_S, KC_DOT) ||
|
|
||||||
* SENTENCE_CASE_JUST_TYPED(KC_SPC, KC_E, KC_T, KC_C, KC_DOT)) {
|
|
||||||
* return false; // Not a real sentence ending.
|
|
||||||
* }
|
|
||||||
* return true; // Real sentence ending; capitalize next letter.
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* @note This callback is used only if `SENTENCE_CASE_BUFFER_SIZE >= 2`.
|
|
||||||
* Otherwise it has no effect.
|
|
||||||
*
|
|
||||||
* @param buffer Buffer of the last `SENTENCE_CASE_BUFFER_SIZE` keycodes.
|
|
||||||
* @return whether there is a real sentence ending.
|
|
||||||
*/
|
|
||||||
bool sentence_case_check_ending(const uint16_t* buffer);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Macro to be used in `sentence_case_check_ending()`.
|
|
||||||
*
|
|
||||||
* Returns true if a given pattern of keys was just typed by comparing with the
|
|
||||||
* keycode buffer. This is useful for defining exceptions in
|
|
||||||
* `sentence_case_check_ending()`.
|
|
||||||
*
|
|
||||||
* For example, `SENTENCE_CASE_JUST_TYPED(KC_SPC, KC_V, KC_S, KC_DOT)` returns
|
|
||||||
* true if " vs." were the last four keys typed.
|
|
||||||
*
|
|
||||||
* @note The pattern must be no longer than `SENTENCE_CASE_BUFFER_SIZE`.
|
|
||||||
*/
|
|
||||||
#define SENTENCE_CASE_JUST_TYPED(...) \
|
|
||||||
({ \
|
|
||||||
static const uint16_t PROGMEM pattern[] = {__VA_ARGS__}; \
|
|
||||||
sentence_case_just_typed_P(buffer, pattern, \
|
|
||||||
sizeof(pattern) / sizeof(uint16_t)); \
|
|
||||||
})
|
|
||||||
bool sentence_case_just_typed_P(const uint16_t* buffer, const uint16_t* pattern,
|
|
||||||
int8_t pattern_len);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Optional callback defining which keys are letter, punctuation, etc.
|
|
||||||
*
|
|
||||||
* This callback may be useful if you type non-US letters or have customized the
|
|
||||||
* shift behavior of the punctuation keys. The return value tells Sentence Case
|
|
||||||
* how to interpret the key:
|
|
||||||
*
|
|
||||||
* 'a' Key is a letter, by default KC_A to KC_Z. If occurring at the start of
|
|
||||||
* a sentence, Sentence Case applies shift to capitalize it.
|
|
||||||
*
|
|
||||||
* '.' Key is sentence-ending punctuation. Default: . ? !
|
|
||||||
*
|
|
||||||
* '#' Key types a backspaceable character that isn't part of a word.
|
|
||||||
* Default includes - = [ ] ; ' , < > / _ + @ # $ % ^ & * ( ) { } digits
|
|
||||||
*
|
|
||||||
* ' ' Key is a space. Default: KC_SPC
|
|
||||||
*
|
|
||||||
* '\'' Key is a quote or double quote character. Default: KC_QUOT.
|
|
||||||
*
|
|
||||||
* '\0' Sentence Case should ignore this key.
|
|
||||||
*
|
|
||||||
* If a hotkey or navigation key is pressed (or another key that performs an
|
|
||||||
* action that backspace doesn't undo), then the callback should call
|
|
||||||
* `sentence_case_clear()` to clear the state and then return '\0'.
|
|
||||||
*
|
|
||||||
* The default callback is:
|
|
||||||
*
|
|
||||||
* char sentence_case_press_user(uint16_t keycode,
|
|
||||||
* keyrecord_t* record,
|
|
||||||
* uint8_t mods) {
|
|
||||||
* if ((mods & ~(MOD_MASK_SHIFT | MOD_BIT(KC_RALT))) == 0) {
|
|
||||||
* const bool shifted = mods & MOD_MASK_SHIFT;
|
|
||||||
* switch (keycode) {
|
|
||||||
* case KC_A ... KC_Z:
|
|
||||||
* return 'a'; // Letter key.
|
|
||||||
*
|
|
||||||
* case KC_DOT: // . is punctuation, Shift . is a symbol (>)
|
|
||||||
* return !shifted ? '.' : '#';
|
|
||||||
* case KC_1:
|
|
||||||
* case KC_SLSH:
|
|
||||||
* return shifted ? '.' : '#';
|
|
||||||
* case KC_EXLM:
|
|
||||||
* case KC_QUES:
|
|
||||||
* return '.';
|
|
||||||
* case KC_2 ... KC_0: // 2 3 4 5 6 7 8 9 0
|
|
||||||
* case KC_AT ... KC_RPRN: // @ # $ % ^ & * ( )
|
|
||||||
* case KC_MINS ... KC_SCLN: // - = [ ] backslash ;
|
|
||||||
* case KC_UNDS ... KC_COLN: // _ + { } | :
|
|
||||||
* case KC_GRV:
|
|
||||||
* case KC_COMM:
|
|
||||||
* return '#'; // Symbol key.
|
|
||||||
*
|
|
||||||
* case KC_SPC:
|
|
||||||
* return ' '; // Space key.
|
|
||||||
*
|
|
||||||
* case KC_QUOT:
|
|
||||||
* return '\''; // Quote key.
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* // Otherwise clear Sentence Case to initial state.
|
|
||||||
* sentence_case_clear();
|
|
||||||
* return '\0';
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* To customize, copy the above function into your keymap and add/remove
|
|
||||||
* keycodes to the above cases.
|
|
||||||
*
|
|
||||||
* @param keycode Current keycode.
|
|
||||||
* @param record record_t for the current press event.
|
|
||||||
* @param mods equal to `get_mods() | get_weak_mods() | get_oneshot_mods()`
|
|
||||||
* @return char code 'a', '.', '#', ' ', or '\0' indicating how the key is to be
|
|
||||||
* interpreted as described above.
|
|
||||||
*/
|
|
||||||
char sentence_case_press_user(uint16_t keycode, keyrecord_t* record,
|
|
||||||
uint8_t mods);
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
}
|
|
||||||
#endif
|
|
|
@ -1,79 +0,0 @@
|
||||||
# SOCD Cleaner
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<tr><td><b>Module</b></td><td><tt>getreuer/socd_cleaner</tt></td></tr>
|
|
||||||
<tr><td><b>Version</b></td><td>2025-03-07</td></tr>
|
|
||||||
<tr><td><b>Maintainer</b></td><td>Pascal Getreuer (@getreuer)</td></tr>
|
|
||||||
<tr><td><b>License</b></td><td><a href="../LICENSE.txt">Apache 2.0</a></td></tr>
|
|
||||||
<tr><td><b>Documentation</b></td><td>
|
|
||||||
<a href="https://getreuer.info/posts/keyboards/socd-cleaner">https://getreuer.info/posts/keyboards/socd-cleaner</a>
|
|
||||||
</td></tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
This is a community module adaptation of [SOCD Cleaner
|
|
||||||
Case](https://getreuer.info/posts/keyboards/socd-cleaner) for Simultaneous
|
|
||||||
Opposing Cardinal Directions (SOCD) filtering. What this mouthful of a name
|
|
||||||
means is that when two keys of opposing direction are held at the same time, a
|
|
||||||
rule is applied to decide which key is sent to the computer. Such filtering is
|
|
||||||
popular for fast inputs on the WASD keys in gaming.
|
|
||||||
|
|
||||||
**Caution: Check game rules before using.** Notably, [Counter-Strike does not
|
|
||||||
allow SOCD
|
|
||||||
filtering](https://store.steampowered.com/news/app/730/view/6500469346429600836).
|
|
||||||
It is your responsibility to disable SOCD Cleaner where it is prohibited.
|
|
||||||
|
|
||||||
## Add SOCD to your keymap
|
|
||||||
|
|
||||||
Add the following to your `keymap.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"modules": ["getreuer/socd_cleaner"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Then in your `keymap.c`, add:
|
|
||||||
|
|
||||||
```c
|
|
||||||
socd_cleaner_t socd_opposing_pairs[] = {
|
|
||||||
{{KC_W, KC_S}, SOCD_CLEANER_LAST},
|
|
||||||
{{KC_A, KC_D}, SOCD_CLEANER_LAST},
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
These lines specify that SOCD filtering is to be performed on the WASD keys
|
|
||||||
(referred to by keycodes `KC_W`, `KC_A`, `KC_S`, `KC_D`) with last input
|
|
||||||
priority resolution (`SOCD_CLEANER_LAST`). If you want to do something else,
|
|
||||||
this is where to change that.
|
|
||||||
|
|
||||||
Resolution strategies:
|
|
||||||
|
|
||||||
* `SOCD_CLEANER_LAST`: (Recommended) Last input priority with reactivation. The
|
|
||||||
last key pressed wins. If the last key is released while the opposing key is
|
|
||||||
still held, the opposing key is reactivated. Rapid alternating inputs can be
|
|
||||||
made. Repeatedly tapping the `D` key while `A` is held sends `ADADADAD`.
|
|
||||||
|
|
||||||
* `SOCD_CLEANER_NEUTRAL`: Neutral resolution. When both keys are pressed, they
|
|
||||||
cancel and neither is sent.
|
|
||||||
|
|
||||||
* `SOCD_CLEANER_0_WINS`: Key 0 always wins, the first key listed in defining the
|
|
||||||
opposing pair.
|
|
||||||
|
|
||||||
* `SOCD_CLEANER_1_WINS`: Key 1 always wins, the second key listed.
|
|
||||||
|
|
||||||
* `SOCD_CLEANER_OFF`: SOCD filtering is disabled for this key pair.
|
|
||||||
|
|
||||||
SOCD Cleaner is enabled by default. Optionally, use these keycodes to enable and
|
|
||||||
disable SOCD Cleaner globally for all opposing pairs:
|
|
||||||
|
|
||||||
| Keycode | Description |
|
|
||||||
|-----------|---------------------------|
|
|
||||||
| `SOCDON` | Turn SOCD Cleaner on. |
|
|
||||||
| `SOCDOFF` | Turn SOCD Cleaner off. |
|
|
||||||
| `SOCDTOG` | Toggle SOCD Cleaner. |
|
|
||||||
|
|
||||||
|
|
||||||
See the [SOCD Cleaner
|
|
||||||
documentation](https://getreuer.info/posts/keyboards/socd-cleaner) for further
|
|
||||||
explanation and details.
|
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
// 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.
|
|
||||||
|
|
||||||
#ifdef COMMUNITY_MODULE_SOCD_CLEANER_ENABLE
|
|
||||||
|
|
||||||
uint16_t socd_opposing_pairs_count_raw(void) {
|
|
||||||
return ARRAY_SIZE(socd_opposing_pairs);
|
|
||||||
}
|
|
||||||
|
|
||||||
__attribute__((weak)) uint16_t socd_opposing_pairs_count(void) {
|
|
||||||
return socd_opposing_pairs_count_raw();
|
|
||||||
}
|
|
||||||
|
|
||||||
socd_cleaner_t* socd_opposing_pairs_get_raw(uint16_t index) {
|
|
||||||
if (index >= socd_opposing_pairs_count_raw()) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
return &socd_opposing_pairs[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
__attribute__((weak)) socd_cleaner_t* socd_opposing_pairs_get(uint16_t index) {
|
|
||||||
return socd_opposing_pairs_get_raw(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif // COMMUNITY_MODULE_SOCD_CLEANER_ENABLE
|
|
|
@ -1,17 +0,0 @@
|
||||||
// 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.
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "socd_cleaner.h"
|
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
{
|
|
||||||
"module_name": "SOCD Cleaner",
|
|
||||||
"maintainer": "getreuer",
|
|
||||||
"url": "https://getreuer.info/posts/keyboards/socd-cleaner",
|
|
||||||
"keycodes": [
|
|
||||||
{"key": "SOCDON"},
|
|
||||||
{"key": "SOCDOFF"},
|
|
||||||
{"key": "SOCDTOG"}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,116 +0,0 @@
|
||||||
// Copyright 2024-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.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @file socd_cleaner.c
|
|
||||||
* @brief SOCD Cleaner community module implementation
|
|
||||||
*
|
|
||||||
* For full documentation, see
|
|
||||||
* <https://getreuer.info/posts/keyboards/socd-cleaner>
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "socd_cleaner.h"
|
|
||||||
|
|
||||||
ASSERT_COMMUNITY_MODULES_MIN_API_VERSION(1, 0, 0);
|
|
||||||
|
|
||||||
// Defined in introspection.c.
|
|
||||||
uint16_t socd_opposing_pairs_count(void);
|
|
||||||
socd_cleaner_t* socd_opposing_pairs_get(uint16_t index);
|
|
||||||
|
|
||||||
bool socd_cleaner_enabled = true;
|
|
||||||
|
|
||||||
static void update_key(uint8_t keycode, bool press) {
|
|
||||||
if (press) {
|
|
||||||
add_key(keycode);
|
|
||||||
} else {
|
|
||||||
del_key(keycode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool process_opposing_pair(
|
|
||||||
uint16_t keycode, keyrecord_t* record, socd_cleaner_t* state) {
|
|
||||||
if (!state || !state->resolution ||
|
|
||||||
(keycode != state->keys[0] && keycode != state->keys[1])) {
|
|
||||||
return true; // Quick return when disabled or on unrelated events.
|
|
||||||
}
|
|
||||||
|
|
||||||
// The current event corresponds to index `i`, 0 or 1, in the SOCD key pair.
|
|
||||||
const uint8_t i = (keycode == state->keys[1]);
|
|
||||||
const uint8_t opposing = i ^ 1; // Index of the opposing key.
|
|
||||||
|
|
||||||
// Track which keys are physically held (vs. keys in the report).
|
|
||||||
state->held[i] = record->event.pressed;
|
|
||||||
|
|
||||||
// Perform SOCD resolution for events where the opposing key is held.
|
|
||||||
if (state->held[opposing]) {
|
|
||||||
switch (state->resolution) {
|
|
||||||
case SOCD_CLEANER_LAST: // Last input priority with reactivation.
|
|
||||||
// If the current event is a press, then release the opposing key.
|
|
||||||
// Otherwise if this is a release, then press the opposing key.
|
|
||||||
update_key(state->keys[opposing], !state->held[i]);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SOCD_CLEANER_NEUTRAL: // Neutral resolution.
|
|
||||||
// Same logic as SOCD_CLEANER_LAST, but skip default handling so that
|
|
||||||
// the current key has no effect while the opposing key is held.
|
|
||||||
update_key(state->keys[opposing], !state->held[i]);
|
|
||||||
// Send updated report (normally, default handling would do this).
|
|
||||||
send_keyboard_report();
|
|
||||||
return false; // Skip default handling.
|
|
||||||
|
|
||||||
case SOCD_CLEANER_0_WINS: // Key 0 wins.
|
|
||||||
case SOCD_CLEANER_1_WINS: // Key 1 wins.
|
|
||||||
if (opposing == (state->resolution - SOCD_CLEANER_0_WINS)) {
|
|
||||||
// The opposing key is the winner. The current key has no effect.
|
|
||||||
return false; // Skip default handling.
|
|
||||||
} else {
|
|
||||||
// The current key is the winner. Update logic is same as above.
|
|
||||||
update_key(state->keys[opposing], !state->held[i]);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true; // Continue default handling to press/release current key.
|
|
||||||
}
|
|
||||||
|
|
||||||
bool process_record_socd_cleaner(uint16_t keycode, keyrecord_t* record) {
|
|
||||||
switch (keycode) {
|
|
||||||
case SOCDON: // Turn SOCD Cleaner on.
|
|
||||||
if (record->event.pressed) {
|
|
||||||
socd_cleaner_enabled = true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
case SOCDOFF: // Turn SOCD Cleaner off.
|
|
||||||
if (record->event.pressed) {
|
|
||||||
socd_cleaner_enabled = false;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
case SOCDTOG: // Toggle SOCD Cleaner.
|
|
||||||
if (record->event.pressed) {
|
|
||||||
socd_cleaner_enabled = !socd_cleaner_enabled;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (socd_cleaner_enabled) {
|
|
||||||
for (int i = 0; i < (int)socd_opposing_pairs_count(); ++i) {
|
|
||||||
socd_cleaner_t* state = socd_opposing_pairs_get(i);
|
|
||||||
if (!process_opposing_pair(keycode, record, state)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,83 +0,0 @@
|
||||||
// Copyright 2024-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.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @file socd_cleaner.h
|
|
||||||
* @brief SOCD Cleaner community module: enhance WASD for fast inputs for gaming
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* SOCD Cleaner is a QMK module for Simultaneous Opposing Cardinal Directions
|
|
||||||
* (SOCD) filtering, which is popular for fast inputs on the WASD keys in
|
|
||||||
* gaming. When two keys of opposing direction are physically held at the same
|
|
||||||
* time, a rule is applied to decide which key is sent to the computer.
|
|
||||||
*
|
|
||||||
* As controls vary across games, there are multiple possible SOCD resolution
|
|
||||||
* strategies. SOCD Cleaner implements the following resolutions:
|
|
||||||
*
|
|
||||||
* - SOCD_CLEANER_LAST: (Recommended) Last input priority with reactivation.
|
|
||||||
* The last key pressed wins. Rapid alternating inputs can be made.
|
|
||||||
* Repeatedly tapping the D key while A is held sends "ADADADAD."
|
|
||||||
*
|
|
||||||
* - SOCD_CLEANER_NEUTRAL: Neutral resolution. When both keys are pressed, they
|
|
||||||
* cancel and neither is sent.
|
|
||||||
*
|
|
||||||
* - SOCD_CLEANER_0_WINS: Key 0 always wins, the first key listed in defining
|
|
||||||
* the `socd_cleaner_t`.
|
|
||||||
*
|
|
||||||
* - SOCD_CLEANER_1_WINS: Key 1 always wins, the second key listed.
|
|
||||||
*
|
|
||||||
* If you don't know what to pick, SOCD_CLEANER_LAST is recommended. The
|
|
||||||
* resolution strategy on a `socd_cleaner_t` may be changed at run time by
|
|
||||||
* assigning to `.resolution`.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* For full documentation, see
|
|
||||||
* <https://getreuer.info/posts/keyboards/socd-cleaner>
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "quantum.h"
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif
|
|
||||||
|
|
||||||
enum socd_cleaner_resolution {
|
|
||||||
// Disable SOCD filtering for this key pair.
|
|
||||||
SOCD_CLEANER_OFF,
|
|
||||||
// Last input priority with reactivation.
|
|
||||||
SOCD_CLEANER_LAST,
|
|
||||||
// Neutral resolution. When both keys are pressed, they cancel.
|
|
||||||
SOCD_CLEANER_NEUTRAL,
|
|
||||||
// Key 0 always wins.
|
|
||||||
SOCD_CLEANER_0_WINS,
|
|
||||||
// Key 1 always wins.
|
|
||||||
SOCD_CLEANER_1_WINS,
|
|
||||||
// Sentinel to count the number of resolution strategies.
|
|
||||||
SOCD_CLEANER_NUM_RESOLUTIONS,
|
|
||||||
};
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
uint8_t keys[2]; // Basic keycodes for the two opposing keys.
|
|
||||||
uint8_t resolution; // Resolution strategy.
|
|
||||||
bool held[2]; // Tracks which keys are physically held.
|
|
||||||
} socd_cleaner_t;
|
|
||||||
|
|
||||||
/** Determines globally whether SOCD cleaner is enabled. */
|
|
||||||
extern bool socd_cleaner_enabled;
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
}
|
|
||||||
#endif
|
|
|
@ -1,59 +0,0 @@
|
||||||
# Tap Flow
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<tr><td><b>Module</b></td><td><tt>getreuer/tap_flow</tt></td></tr>
|
|
||||||
<tr><td><b>Version</b></td><td>2025-04-08</td></tr>
|
|
||||||
<tr><td><b>Maintainer</b></td><td>Pascal Getreuer (@getreuer)</td></tr>
|
|
||||||
<tr><td><b>License</b></td><td><a href="../LICENSE.txt">Apache 2.0</a></td></tr>
|
|
||||||
<tr><td><b>Documentation</b></td><td>
|
|
||||||
<a href="https://getreuer.info/posts/keyboards/tap-flow">https://getreuer.info/posts/keyboards/tap-flow</a>
|
|
||||||
</td></tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
This module is an implementation of "global quick tap" (GQT), aka "require
|
|
||||||
prior idle," for tap-hold keys. It is particularly useful for home row mods to
|
|
||||||
avoid accidental mod triggers in fast typing.
|
|
||||||
|
|
||||||
To use this module, add the following to your `keymap.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"modules": ["getreuer/tap_flow"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Tap Flow's term can be tuned on the fly with the following keycodes:
|
|
||||||
|
|
||||||
| Keycode | Alias | Description |
|
|
||||||
|-------------------|-----------|-----------------------------------|
|
|
||||||
| `TAP_FLOW_PRINT` | `TFLOW_P` | Type the current value. |
|
|
||||||
| `TAP_FLOW_UP` | `TFLOW_U` | Increase by 5 ms. |
|
|
||||||
| `TAP_FLOW_DOWN` | `TFLOW_D` | Decrease by 5 ms. |
|
|
||||||
|
|
||||||
Tap Flow's default behavior is:
|
|
||||||
|
|
||||||
* Filtering is done only when a tap-hold press is within `TAP_FLOW_TERM` of the
|
|
||||||
previous key event, which defaults to 150 ms. Use `TFLOW_U` / `TFLOW_D`
|
|
||||||
to tune, then define `TAP_FLOW_TERM` in your `config.h` to set the value
|
|
||||||
printed by `TFLOW_P`.
|
|
||||||
|
|
||||||
* Filtering is done only when both the tap-hold key and the previous key are
|
|
||||||
among <kbd>Space</kbd>, letters <kbd>A</kbd>–<kbd>Z</kbd>, and
|
|
||||||
punctuations <kbd>,</kbd> <kbd>.</kbd> <kbd>;</kbd> <kbd>/</kbd>.
|
|
||||||
|
|
||||||
Define the `is_tap_flow_key()` or `get_tap_flow_term()` callbacks to customize.
|
|
||||||
|
|
||||||
Tap Flow modifies the tap-hold decision such that when a tap-hold key is pressed
|
|
||||||
within a short timeout of the preceding key, the tapping function is used. The
|
|
||||||
assumption is that during fast typing, only the tap function of tap-hold keys is
|
|
||||||
desired (though perhaps with an exception for Shift or AltGr, noted below),
|
|
||||||
whereas the hold functions (mods and layers) are used in isolation, or at least
|
|
||||||
with a brief pause preceding the tap-hold key press.
|
|
||||||
|
|
||||||
Optionally, the feature can be customized with the `is_tap_flow_key()` and
|
|
||||||
`get_tap_flow_term()` callbacks. In this way, exceptions may be made for Shift
|
|
||||||
and AltGr (or whatever you wish) to use a shorter time or to disable filtering
|
|
||||||
for those keys entirely.
|
|
||||||
|
|
||||||
For full documentation, see
|
|
||||||
<https://getreuer.info/posts/keyboards/tap-flow>
|
|
|
@ -1,17 +0,0 @@
|
||||||
// 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.
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "tap_flow.h"
|
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
{
|
|
||||||
"module_name": "Tap Flow",
|
|
||||||
"maintainer": "getreuer",
|
|
||||||
"url": "https://getreuer.info/posts/keyboards/tap-flow",
|
|
||||||
"keycodes": [
|
|
||||||
{
|
|
||||||
"key": "TAP_FLOW_PRINT",
|
|
||||||
"aliases": [
|
|
||||||
"TFLOW_P"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "TAP_FLOW_UP",
|
|
||||||
"aliases": [
|
|
||||||
"TFLOW_U"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "TAP_FLOW_DOWN",
|
|
||||||
"aliases": [
|
|
||||||
"TFLOW_D"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,216 +0,0 @@
|
||||||
// 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.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @file tap_flow.c
|
|
||||||
* @brief Tap Flow module implementation
|
|
||||||
*
|
|
||||||
* For full documentation, see
|
|
||||||
* <https://getreuer.info/posts/keyboards/tap-flow>
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "tap_flow.h"
|
|
||||||
|
|
||||||
ASSERT_COMMUNITY_MODULES_MIN_API_VERSION(1, 0, 0);
|
|
||||||
|
|
||||||
// Either the Combos feature or Repeat Key (or both) need to be enabled. Either
|
|
||||||
// of these features enables the .keycode field in keyrecord_t, which the
|
|
||||||
// tap_flow implementation relies on.
|
|
||||||
#if !defined(COMBO_ENABLE) && !defined(REPEAT_KEY_ENABLE)
|
|
||||||
#error "tap_flow: Please enable Combos (COMBO_ENABLE = true) or Repeat Key (REPEAT_KEY_ENABLE = yes), or both, in rules.mk."
|
|
||||||
#else
|
|
||||||
|
|
||||||
uint32_t last_input = 0;
|
|
||||||
uint16_t g_tap_flow_term = TAP_FLOW_TERM;
|
|
||||||
|
|
||||||
static uint16_t prev_keycode = KC_NO;
|
|
||||||
// Events bypass tap_flow when there are unsettled LT keys in action_tapping's
|
|
||||||
// waiting_queue. Particularly, supposing an LT settles as held, the layer state
|
|
||||||
// will change and buffered events following the LT will be reconsidered as keys
|
|
||||||
// on that layer. That may change whether tap_flow is enabled or the timeout
|
|
||||||
// to use on those keys. We don't know in advance how the LT will settle.
|
|
||||||
static uint16_t settle_timer = 0;
|
|
||||||
static uint8_t is_tapped[(MATRIX_ROWS * MATRIX_COLS + 7) / 8] = {0};
|
|
||||||
|
|
||||||
static uint16_t get_tap_keycode(uint16_t keycode) {
|
|
||||||
switch (keycode) {
|
|
||||||
case QK_MOD_TAP ... QK_MOD_TAP_MAX:
|
|
||||||
return QK_MOD_TAP_GET_TAP_KEYCODE(keycode);
|
|
||||||
#ifndef NO_ACTION_LAYER
|
|
||||||
case QK_LAYER_TAP ... QK_LAYER_TAP_MAX:
|
|
||||||
return QK_LAYER_TAP_GET_TAP_KEYCODE(keycode);
|
|
||||||
#endif // NO_ACTION_LAYER
|
|
||||||
}
|
|
||||||
return keycode;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef COMBO_ENABLE
|
|
||||||
#include "keymap_introspection.h"
|
|
||||||
|
|
||||||
static bool is_combo_key(uint16_t keycode) {
|
|
||||||
for (uint16_t i = 0; i < combo_count(); ++i) {
|
|
||||||
const uint16_t* keys = combo_get(i)->keys;
|
|
||||||
uint16_t key;
|
|
||||||
do {
|
|
||||||
key = pgm_read_word(keys++);
|
|
||||||
if (key == keycode) { return true; }
|
|
||||||
} while (key != COMBO_END);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
#define is_combo_key(keycode) false
|
|
||||||
#endif // COMBO_ENABLE
|
|
||||||
|
|
||||||
void housekeeping_task_tap_flow(void) {
|
|
||||||
if (settle_timer && timer_expired(timer_read(), settle_timer)) {
|
|
||||||
#ifdef TAP_FLOW_DEBUG
|
|
||||||
dprintf("tap_flow: settled.\n");
|
|
||||||
#endif // TAP_FLOW_DEBUG
|
|
||||||
settle_timer = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool pre_process_record_tap_flow(uint16_t keycode, keyrecord_t* record) {
|
|
||||||
const keypos_t pos = record->event.key;
|
|
||||||
|
|
||||||
if (IS_KEYEVENT(record->event) && pos.row < MATRIX_ROWS
|
|
||||||
&& pos.col < MATRIX_COLS &&
|
|
||||||
(IS_QK_MOD_TAP(keycode) || IS_QK_LAYER_TAP(keycode))) {
|
|
||||||
// The event is on an MT or LT with a valid matrix position.
|
|
||||||
const uint16_t tap_keycode = get_tap_keycode(keycode);
|
|
||||||
|
|
||||||
// Determine the key's index in the bit arrays.
|
|
||||||
const uint16_t index = pos.row * MATRIX_COLS + pos.col;
|
|
||||||
const uint16_t array_index = index / 8;
|
|
||||||
const uint8_t bit_mask = UINT8_C(1) << (index % 8);
|
|
||||||
|
|
||||||
if (record->event.pressed) { // On press.
|
|
||||||
const uint32_t idle_time = timer_elapsed32(last_input);
|
|
||||||
uint16_t tap_flow_term = get_tap_flow(keycode, record, prev_keycode);
|
|
||||||
if (tap_flow_term > 500) {
|
|
||||||
tap_flow_term = 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!settle_timer && !is_combo_key(keycode) &&
|
|
||||||
idle_time < 500 && idle_time < tap_flow_term) {
|
|
||||||
#ifdef TAP_FLOW_DEBUG
|
|
||||||
dprintf("tap_flow: %02x%02xd within term (%u < %u) converted to tap.\n",
|
|
||||||
pos.row, pos.col, (uint16_t)idle_time, tap_flow_term);
|
|
||||||
#endif // TAP_FLOW_DEBUG
|
|
||||||
|
|
||||||
// Rewrite the event as a press of the tap keycode. This way, it
|
|
||||||
// bypasses the usual action_tapping logic.
|
|
||||||
record->keycode = tap_keycode;
|
|
||||||
// Record this key as tapped.
|
|
||||||
is_tapped[array_index] |= bit_mask;
|
|
||||||
} else {
|
|
||||||
// Otherwise if this is an LT key, track when it will settle according
|
|
||||||
// to its tapping term.
|
|
||||||
// NOTE: To be precise, the key could settle before the tapping term.
|
|
||||||
// This is an approximation.
|
|
||||||
#ifdef TAP_FLOW_DEBUG
|
|
||||||
if (settle_timer) {
|
|
||||||
dprintf("tap_flow: %02x%02xd unchanged (unsettled state).\n",
|
|
||||||
pos.row, pos.col);
|
|
||||||
} else if (is_combo_key(keycode)) {
|
|
||||||
dprintf("tap_flow: %02x%02xd unchanged (combo key).\n",
|
|
||||||
pos.row, pos.col);
|
|
||||||
} else {
|
|
||||||
dprintf("tap_flow: %02x%02xd unchanged (outside time).\n",
|
|
||||||
pos.row, pos.col);
|
|
||||||
}
|
|
||||||
#endif // TAP_FLOW_DEBUG
|
|
||||||
|
|
||||||
if (IS_QK_LAYER_TAP(keycode)) {
|
|
||||||
const uint16_t term = GET_TAPPING_TERM(keycode, record);
|
|
||||||
const uint16_t now = timer_read();
|
|
||||||
if (!settle_timer || term > TIMER_DIFF_16(settle_timer, now)) {
|
|
||||||
settle_timer = (now + term) | 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if ((is_tapped[array_index] & bit_mask) != 0) { // On tap release.
|
|
||||||
#ifdef TAP_FLOW_DEBUG
|
|
||||||
dprintf("tap_flow: %02x%02xu tap release.\n", pos.row, pos.col);
|
|
||||||
#endif // TAP_FLOW_DEBUG
|
|
||||||
|
|
||||||
// Rewrite the event as a release of the tap keycode.
|
|
||||||
record->keycode = tap_keycode;
|
|
||||||
// Record the key as released.
|
|
||||||
is_tapped[array_index] &= ~bit_mask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (record->event.pressed) { // Track the previous key press.
|
|
||||||
prev_keycode = keycode;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update last input time. Ignore mods and mod-tap keys in this update to
|
|
||||||
// allow for chording multiple mods for hotkeys like "Ctrl+Shift+arrow".
|
|
||||||
if (!IS_MODIFIER_KEYCODE(keycode) && !IS_QK_MOD_TAP(keycode)) {
|
|
||||||
last_input = timer_read32();
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool process_record_tap_flow(uint16_t keycode, keyrecord_t* record) {
|
|
||||||
if (record->event.pressed) {
|
|
||||||
switch (keycode) {
|
|
||||||
case TAP_FLOW_PRINT:
|
|
||||||
send_string(get_u16_str(g_tap_flow_term, ' '));
|
|
||||||
return false;
|
|
||||||
case TAP_FLOW_UP:
|
|
||||||
g_tap_flow_term += 5;
|
|
||||||
return false;
|
|
||||||
case TAP_FLOW_DOWN:
|
|
||||||
g_tap_flow_term -= 5;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// By default, enable Tap Flow for Space, A-Z, or main alphas area punctuation.
|
|
||||||
__attribute__((weak)) bool is_tap_flow_key(uint16_t keycode) {
|
|
||||||
switch (get_tap_keycode(keycode)) {
|
|
||||||
case KC_SPC:
|
|
||||||
case KC_A ... KC_Z:
|
|
||||||
case KC_DOT:
|
|
||||||
case KC_COMM:
|
|
||||||
case KC_SCLN:
|
|
||||||
case KC_SLSH:
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
__attribute__((weak)) uint16_t get_tap_flow_term(
|
|
||||||
uint16_t keycode, keyrecord_t* record, uint16_t prev_keycode) {
|
|
||||||
return get_tap_flow(keycode, record, prev_keycode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// By default, enable filtering when both the tap-hold key and previous key
|
|
||||||
// return true for `is_tap_flow_key()`.
|
|
||||||
__attribute__((weak)) uint16_t get_tap_flow(
|
|
||||||
uint16_t keycode, keyrecord_t* record, uint16_t prev_keycode) {
|
|
||||||
if (is_tap_flow_key(keycode) && is_tap_flow_key(prev_keycode)) {
|
|
||||||
return g_tap_flow_term;
|
|
||||||
}
|
|
||||||
return 0; // Disable Tap Flow.
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif // !defined(COMBO_ENABLE) && !defined(REPEAT_KEY_ENABLE)
|
|
||||||
|
|
|
@ -1,124 +0,0 @@
|
||||||
// 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.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @file tap_flow.h
|
|
||||||
* @brief Tap Flow module: disable HRMs during fast typing
|
|
||||||
*
|
|
||||||
* This module is an implementation of "global quick tap" (GQT), aka "require
|
|
||||||
* prior idle," for tap-hold keys. It is particularly useful for home row mods
|
|
||||||
* to avoid accidental mod triggers in fast typing.
|
|
||||||
*
|
|
||||||
* Tap Flow modifies the tap-hold decision such that when a tap-hold key is
|
|
||||||
* pressed within a short timeout of the preceding key, the tapping function is
|
|
||||||
* used. The assumption is that during fast typing, only the tap function of
|
|
||||||
* tap-hold keys is desired (though perhaps with an exception for Shift or
|
|
||||||
* AltGr, noted below), whereas the hold functions (mods and layers) are
|
|
||||||
* used in isolation, or at least with a brief pause preceding the tap-hold key
|
|
||||||
* press.
|
|
||||||
*
|
|
||||||
* Optionally, the feature can be customized with the `get_tap_flow()` callback.
|
|
||||||
* In this way, exceptions may be made for Shift and AltGr (or whatever you
|
|
||||||
* wish) to use a shorter time or to disable filtering for those keys entirely.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* For full documentation, see
|
|
||||||
* <https://getreuer.info/posts/keyboards/tap-flow>
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "quantum.h"
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef TAP_FLOW_TERM
|
|
||||||
# define TAP_FLOW_TERM 150
|
|
||||||
#endif // TAP_FLOW_TERM
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Optional callback to customize where Tap Flow is enabled.
|
|
||||||
*
|
|
||||||
* Tap Flow is constrained to certain keys by the following rule: this callback
|
|
||||||
* is called for both the tap-hold key *and* the key press immediately preceding
|
|
||||||
* it. If the callback returns true for both keycodes, Tap Flow may apply.
|
|
||||||
*
|
|
||||||
* The default implementation of this callback corresponds to
|
|
||||||
*
|
|
||||||
* bool is_tap_flow_key(uint16_t keycode) {
|
|
||||||
* switch (keycode) {
|
|
||||||
* case QK_MOD_TAP ... QK_MOD_TAP_MAX:
|
|
||||||
* keycode = QK_MOD_TAP_GET_TAP_KEYCODE(keycode);
|
|
||||||
* break;
|
|
||||||
* case QK_LAYER_TAP ... QK_LAYER_TAP_MAX:
|
|
||||||
* keycode = QK_LAYER_TAP_GET_TAP_KEYCODE(keycode);
|
|
||||||
* break;
|
|
||||||
* }
|
|
||||||
* switch (keycode) {
|
|
||||||
* case KC_SPC:
|
|
||||||
* case KC_A ... KC_Z:
|
|
||||||
* case KC_DOT:
|
|
||||||
* case KC_COMM:
|
|
||||||
* case KC_SCLN:
|
|
||||||
* case KC_SLSH:
|
|
||||||
* return true;
|
|
||||||
* }
|
|
||||||
* return false;
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* @param keycode Keycode of the key.
|
|
||||||
* @return Whether to enable Tap Flow for this key.
|
|
||||||
*/
|
|
||||||
bool is_tap_flow_key(uint16_t keycode);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Optional callback to customize filtering.
|
|
||||||
*
|
|
||||||
* Tap Flow acts only when key events are closer together than this time.
|
|
||||||
*
|
|
||||||
* Return a time of 0 to disable filtering. In this way, Tap Flow may be
|
|
||||||
* disabled for certain tap-hold keys, or when following certain previous keys.
|
|
||||||
*
|
|
||||||
* The default implementation of this callback is
|
|
||||||
*
|
|
||||||
* uint16_t get_tap_flow_term(uint16_t keycode, keyrecord_t* record,
|
|
||||||
* uint16_t prev_keycode) {
|
|
||||||
* if (is_tap_flow_key(keycode) && is_tap_flow_key(prev_keycode)) {
|
|
||||||
* return g_tap_flow_term;
|
|
||||||
* }
|
|
||||||
* return 0;
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* NOTE: If both `is_tap_flow_key()` and `get_tap_flow_term()` are defined, then
|
|
||||||
* `get_tap_flow_term()` takes precedence.
|
|
||||||
*
|
|
||||||
* @param keycode Keycode of the tap-hold key.
|
|
||||||
* @param record keyrecord_t of the tap-hold event.
|
|
||||||
* @param prev_keycode Keycode of the previously pressed key.
|
|
||||||
* @return Timeout in milliseconds.
|
|
||||||
*/
|
|
||||||
uint16_t get_tap_flow_term(uint16_t keycode, keyrecord_t* record,
|
|
||||||
uint16_t prev_keycode);
|
|
||||||
|
|
||||||
/** @deprecated Use `get_tap_flow_term()` instead. */
|
|
||||||
uint16_t get_tap_flow(uint16_t keycode, keyrecord_t* record,
|
|
||||||
uint16_t prev_keycode);
|
|
||||||
|
|
||||||
extern uint16_t g_tap_flow_term;
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
# update the modules
|
|
||||||
rm -rf modules/getreuer
|
|
||||||
git clone https://github.com/getreuer/qmk-modules.git modules/getreuer
|
|
||||||
rm -rf modules/getreuer/.git
|
|
||||||
git add modules/getreuer
|
|
Loading…
Add table
Add a link
Reference in a new issue