This commit is contained in:
Christoph Cullmann 2025-03-18 22:27:19 +01:00
parent 4dc5811f00
commit ee0a3c3be0
No known key found for this signature in database
61 changed files with 5743 additions and 0 deletions

3
modules/getreuer/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
*.bin
*.hex

View file

@ -0,0 +1,94 @@
# 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 projects 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 Stewards 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

View file

@ -0,0 +1,30 @@
# 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/).

View file

@ -0,0 +1,202 @@
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.

103
modules/getreuer/README.md Normal file
View file

@ -0,0 +1,103 @@
# @getreuer's QMK community modules
(This is not an officially supported Google product.)
![](doc/banner.jpg)
| 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/achordion"]
}
```
Or if a `keymap.json` already exists, merge the `"modules"` line into it. Add
multiple modules like:
```json
{
"modules": ["getreuer/achordion", "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.

View file

@ -0,0 +1,31 @@
# 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.

View file

@ -0,0 +1,380 @@
// 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

View file

@ -0,0 +1,167 @@
// 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

View file

@ -0,0 +1,17 @@
// 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"

View file

@ -0,0 +1,4 @@
{
"module_name": "Achordion",
"maintainer": "getreuer"
}

View file

@ -0,0 +1,45 @@
# 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.

View file

@ -0,0 +1,94 @@
// 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.
}

View file

@ -0,0 +1,62 @@
// 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

View file

@ -0,0 +1,37 @@
// 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

View file

@ -0,0 +1,17 @@
// 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"

View file

@ -0,0 +1,4 @@
{
"module_name": "Custom Shift Keys",
"maintainer": "getreuer"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View file

@ -0,0 +1,40 @@
# 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.

View file

@ -0,0 +1,18 @@
// 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"

View file

@ -0,0 +1,510 @@
// 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;
}

View file

@ -0,0 +1,134 @@
// 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

View file

@ -0,0 +1,4 @@
{
"module_name": "Keycode String",
"maintainer": "getreuer"
}

View file

@ -0,0 +1,37 @@
# 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.

View file

@ -0,0 +1,121 @@
// 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;
}
}

View file

@ -0,0 +1,14 @@
{
"module_name": "Mouse Turbo Click",
"maintainer": "getreuer",
"features": {
"deferred_exec": true,
"mousekeys": true
},
"keycodes": [
{
"key": "MOUSE_TURBO_CLICK",
"aliases": ["TURBO"]
}
]
}

View file

@ -0,0 +1,56 @@
# 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.

View file

@ -0,0 +1,17 @@
// 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"

View file

@ -0,0 +1,364 @@
// 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);
}

View file

@ -0,0 +1,93 @@
// 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,
};

View file

@ -0,0 +1,22 @@
{
"module_name": "Orbital Mouse",
"maintainer": "getreuer",
"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"}
]
}

View file

@ -0,0 +1,55 @@
# 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.

View file

@ -0,0 +1,17 @@
// 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"

View file

@ -0,0 +1,493 @@
// 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;
}

View file

@ -0,0 +1,150 @@
// 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

View file

@ -0,0 +1,345 @@
// 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

View file

@ -0,0 +1,48 @@
// 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

View file

@ -0,0 +1,7 @@
{
"module_name": "PaletteFx",
"maintainer": "getreuer",
"features": {
"rgb_matrix": true
}
}

View file

@ -0,0 +1,15 @@
# 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

View file

@ -0,0 +1,68 @@
# 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.

View file

@ -0,0 +1,17 @@
// 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"

View file

@ -0,0 +1,24 @@
{
"module_name": "Select Word",
"maintainer": "getreuer",
"keycodes": [
{
"key": "SELECT_WORD",
"aliases": [
"SELWORD"
]
},
{
"key": "SELECT_WORD_BACK",
"aliases": [
"SELWBAK"
]
},
{
"key": "SELECT_LINE",
"aliases": [
"SELLINE"
]
}
]
}

View file

@ -0,0 +1,301 @@
// 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;
}

View file

@ -0,0 +1,91 @@
// 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

View file

@ -0,0 +1,60 @@
# 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.

View file

@ -0,0 +1,17 @@
// 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"

View file

@ -0,0 +1,9 @@
{
"module_name": "Sentence Case",
"maintainer": "getreuer",
"keycodes": [
{"key": "SENTENCE_CASE_ON"},
{"key": "SENTENCE_CASE_OFF"},
{"key": "SENTENCE_CASE_TOGGLE"}
]
}

View file

@ -0,0 +1,380 @@
// 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

View file

@ -0,0 +1,206 @@
// 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

View file

@ -0,0 +1,79 @@
# 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.

View file

@ -0,0 +1,36 @@
// 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

View file

@ -0,0 +1,17 @@
// 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"

View file

@ -0,0 +1,9 @@
{
"module_name": "SOCD Cleaner",
"maintainer": "getreuer",
"keycodes": [
{"key": "SOCDON"},
{"key": "SOCDOFF"},
{"key": "SOCDTOG"}
]
}

View file

@ -0,0 +1,116 @@
// 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;
}

View file

@ -0,0 +1,83 @@
// 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

View file

@ -0,0 +1,57 @@
# 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-03-15</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
priori 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&nbsp;ms. |
| `TAP_FLOW_DOWN` | `TFLOW_D` | Decrease by 5&nbsp;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&nbsp;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>&ndash;<kbd>Z</kbd>, and
punctuations <kbd>,</kbd> <kbd>.</kbd> <kbd>;</kbd> <kbd>/</kbd>. Define the
`get_tap_flow()` callback to customize this logic.
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>

View file

@ -0,0 +1,17 @@
// 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"

View file

@ -0,0 +1,24 @@
{
"module_name": "Tap Flow",
"maintainer": "getreuer",
"keycodes": [
{
"key": "TAP_FLOW_PRINT",
"aliases": [
"TFLOW_P"
]
},
{
"key": "TAP_FLOW_UP",
"aliases": [
"TFLOW_U"
]
},
{
"key": "TAP_FLOW_DOWN",
"aliases": [
"TFLOW_D"
]
}
]
}

View file

@ -0,0 +1,205 @@
// 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
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 = last_input_activity_elapsed();
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;
}
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;
}
// Keycode is a "typing" key: Space, A-Z, or main alphas area punctuation.
static bool is_typing(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;
}
// By default, enable filtering when both the tap-hold key and previous key are
// typing keys, and use the quick tap term.
__attribute__((weak)) uint16_t get_tap_flow(
uint16_t keycode, keyrecord_t* record, uint16_t prev_keycode) {
if (!is_typing(keycode) || !is_typing(prev_keycode)) {
return 0;
}
return g_tap_flow_term;
}
#endif // !defined(COMBO_ENABLE) && !defined(REPEAT_KEY_ENABLE)

View file

@ -0,0 +1,72 @@
// 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
* priori 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 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.
*
* @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 Time in milliseconds.
*/
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

5
update.sh Executable file
View file

@ -0,0 +1,5 @@
# 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