diff --git a/modules/getreuer/.gitignore b/modules/getreuer/.gitignore new file mode 100644 index 0000000..344bdc0 --- /dev/null +++ b/modules/getreuer/.gitignore @@ -0,0 +1,3 @@ +*.bin +*.hex + diff --git a/modules/getreuer/CODE_OF_CONDUCT.md b/modules/getreuer/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..0bd8761 --- /dev/null +++ b/modules/getreuer/CODE_OF_CONDUCT.md @@ -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 project’s code of conduct. + +If you see someone violating the code of conduct, you are encouraged to address +the behavior directly with those involved. Many issues can be resolved quickly +and easily, and this gives people more control over the outcome of their +dispute. If you are unable to resolve the matter for any reason, or if the +behavior is threatening or harassing, report it. We are dedicated to providing +an environment where participants feel welcome and safe. + +Reports should be directed to *[PROJECT STEWARD NAME(s) AND EMAIL(s)]*, the +Project Steward(s) for *[PROJECT NAME]*. It is the Project Steward’s duty to +receive and address reported violations of the code of conduct. They will then +work with a committee consisting of representatives from the Open Source +Programs Office and the Google Open Source Strategy team. If for any reason you +are uncomfortable reaching out to the Project Steward, please email +opensource@google.com. + +We will investigate every complaint, but you may not receive a direct response. +We will use our discretion in determining when and how to follow up on reported +incidents, which may range from not taking action to permanent expulsion from +the project and project-sponsored spaces. We will notify the accused of the +report and provide them an opportunity to discuss it before any action is taken. +The identity of the reporter will be omitted from the details of the report +supplied to the accused. In potentially harmful situations, such as ongoing +harassment or threats to anyone's safety, we may take action without notice. + +## Attribution + +This Code of Conduct is adapted from the Contributor Covenant, version 1.4, +available at +https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + diff --git a/modules/getreuer/CONTRIBUTING.md b/modules/getreuer/CONTRIBUTING.md new file mode 100644 index 0000000..1d5ee2d --- /dev/null +++ b/modules/getreuer/CONTRIBUTING.md @@ -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 + 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/). + diff --git a/modules/getreuer/LICENSE.txt b/modules/getreuer/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/modules/getreuer/LICENSE.txt @@ -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. diff --git a/modules/getreuer/README.md b/modules/getreuer/README.md new file mode 100644 index 0000000..40a550c --- /dev/null +++ b/modules/getreuer/README.md @@ -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: + + + └── 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. + diff --git a/modules/getreuer/achordion/README.md b/modules/getreuer/achordion/README.md new file mode 100644 index 0000000..18ee43c --- /dev/null +++ b/modules/getreuer/achordion/README.md @@ -0,0 +1,31 @@ +# Achordion + + + + + + + +
Modulegetreuer/achordion
Version2025-03-07
MaintainerPascal Getreuer (@getreuer)
LicenseApache 2.0
Documentation +https://getreuer.info/posts/keyboards/achordion +
+ +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. + diff --git a/modules/getreuer/achordion/achordion.c b/modules/getreuer/achordion/achordion.c new file mode 100644 index 0000000..9aad8b2 --- /dev/null +++ b/modules/getreuer/achordion/achordion.c @@ -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 + * + */ + +#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 diff --git a/modules/getreuer/achordion/achordion.h b/modules/getreuer/achordion/achordion.h new file mode 100644 index 0000000..4b83907 --- /dev/null +++ b/modules/getreuer/achordion/achordion.h @@ -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 + * + */ + +#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 . + * + * @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 diff --git a/modules/getreuer/achordion/introspection.h b/modules/getreuer/achordion/introspection.h new file mode 100644 index 0000000..7dbbebe --- /dev/null +++ b/modules/getreuer/achordion/introspection.h @@ -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" + diff --git a/modules/getreuer/achordion/qmk_module.json b/modules/getreuer/achordion/qmk_module.json new file mode 100644 index 0000000..d0b5687 --- /dev/null +++ b/modules/getreuer/achordion/qmk_module.json @@ -0,0 +1,4 @@ +{ + "module_name": "Achordion", + "maintainer": "getreuer" +} diff --git a/modules/getreuer/custom_shift_keys/README.md b/modules/getreuer/custom_shift_keys/README.md new file mode 100644 index 0000000..1dcb597 --- /dev/null +++ b/modules/getreuer/custom_shift_keys/README.md @@ -0,0 +1,45 @@ +# Custom Shift Keys + + + + + + + +
Modulegetreuer/custom_shift_keys
Version2025-03-07
MaintainerPascal Getreuer (@getreuer)
LicenseApache 2.0
Documentation +https://getreuer.info/posts/keyboards/custom-shift-keys +
+ +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. diff --git a/modules/getreuer/custom_shift_keys/custom_shift_keys.c b/modules/getreuer/custom_shift_keys/custom_shift_keys.c new file mode 100644 index 0000000..4c7c88a --- /dev/null +++ b/modules/getreuer/custom_shift_keys/custom_shift_keys.c @@ -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 + * + */ + +#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. +} + diff --git a/modules/getreuer/custom_shift_keys/custom_shift_keys.h b/modules/getreuer/custom_shift_keys/custom_shift_keys.h new file mode 100644 index 0000000..4636e16 --- /dev/null +++ b/modules/getreuer/custom_shift_keys/custom_shift_keys.h @@ -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 + * + */ + +#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 diff --git a/modules/getreuer/custom_shift_keys/introspection.c b/modules/getreuer/custom_shift_keys/introspection.c new file mode 100644 index 0000000..6835622 --- /dev/null +++ b/modules/getreuer/custom_shift_keys/introspection.c @@ -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 diff --git a/modules/getreuer/custom_shift_keys/introspection.h b/modules/getreuer/custom_shift_keys/introspection.h new file mode 100644 index 0000000..fd09bb8 --- /dev/null +++ b/modules/getreuer/custom_shift_keys/introspection.h @@ -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" + diff --git a/modules/getreuer/custom_shift_keys/qmk_module.json b/modules/getreuer/custom_shift_keys/qmk_module.json new file mode 100644 index 0000000..de4b750 --- /dev/null +++ b/modules/getreuer/custom_shift_keys/qmk_module.json @@ -0,0 +1,4 @@ +{ + "module_name": "Custom Shift Keys", + "maintainer": "getreuer" +} diff --git a/modules/getreuer/doc/banner.jpg b/modules/getreuer/doc/banner.jpg new file mode 100644 index 0000000..479bdad Binary files /dev/null and b/modules/getreuer/doc/banner.jpg differ diff --git a/modules/getreuer/doc/voyager.jpg b/modules/getreuer/doc/voyager.jpg new file mode 100644 index 0000000..19bebfd Binary files /dev/null and b/modules/getreuer/doc/voyager.jpg differ diff --git a/modules/getreuer/keycode_string/README.md b/modules/getreuer/keycode_string/README.md new file mode 100644 index 0000000..5348a42 --- /dev/null +++ b/modules/getreuer/keycode_string/README.md @@ -0,0 +1,40 @@ +# Keycode String + + + + + + + +
Modulegetreuer/keycode_string
Version2025-03-07
MaintainerPascal Getreuer (@getreuer)
LicenseApache 2.0
Documentation +https://getreuer.info/posts/keyboards/keycode-string +
+ +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. + diff --git a/modules/getreuer/keycode_string/introspection.h b/modules/getreuer/keycode_string/introspection.h new file mode 100644 index 0000000..74e0c84 --- /dev/null +++ b/modules/getreuer/keycode_string/introspection.h @@ -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" + diff --git a/modules/getreuer/keycode_string/keycode_string.c b/modules/getreuer/keycode_string/keycode_string.c new file mode 100644 index 0000000..046eacb --- /dev/null +++ b/modules/getreuer/keycode_string/keycode_string.c @@ -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 + * + */ + +#include "keycode_string.h" + +#include + +#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; +} diff --git a/modules/getreuer/keycode_string/keycode_string.h b/modules/getreuer/keycode_string/keycode_string.h new file mode 100644 index 0000000..7b7f1dc --- /dev/null +++ b/modules/getreuer/keycode_string/keycode_string.h @@ -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 + * + */ + +#pragma once + +#include + +#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 diff --git a/modules/getreuer/keycode_string/qmk_module.json b/modules/getreuer/keycode_string/qmk_module.json new file mode 100644 index 0000000..0255628 --- /dev/null +++ b/modules/getreuer/keycode_string/qmk_module.json @@ -0,0 +1,4 @@ +{ + "module_name": "Keycode String", + "maintainer": "getreuer" +} diff --git a/modules/getreuer/mouse_turbo_click/README.md b/modules/getreuer/mouse_turbo_click/README.md new file mode 100644 index 0000000..1d27549 --- /dev/null +++ b/modules/getreuer/mouse_turbo_click/README.md @@ -0,0 +1,37 @@ +# Mouse Turbo Click + + + + + + + +
Modulegetreuer/mouse_turbo_click
Version2025-03-07
MaintainerPascal Getreuer (@getreuer)
LicenseApache 2.0
Documentation +https://getreuer.info/posts/keyboards/mouse-turbo-click +
+ +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. + diff --git a/modules/getreuer/mouse_turbo_click/mouse_turbo_click.c b/modules/getreuer/mouse_turbo_click/mouse_turbo_click.c new file mode 100644 index 0000000..05d55a9 --- /dev/null +++ b/modules/getreuer/mouse_turbo_click/mouse_turbo_click.c @@ -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 + * + */ + +#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; + } +} + diff --git a/modules/getreuer/mouse_turbo_click/qmk_module.json b/modules/getreuer/mouse_turbo_click/qmk_module.json new file mode 100644 index 0000000..73418c1 --- /dev/null +++ b/modules/getreuer/mouse_turbo_click/qmk_module.json @@ -0,0 +1,14 @@ +{ + "module_name": "Mouse Turbo Click", + "maintainer": "getreuer", + "features": { + "deferred_exec": true, + "mousekeys": true + }, + "keycodes": [ + { + "key": "MOUSE_TURBO_CLICK", + "aliases": ["TURBO"] + } + ] +} diff --git a/modules/getreuer/orbital_mouse/README.md b/modules/getreuer/orbital_mouse/README.md new file mode 100644 index 0000000..ff21996 --- /dev/null +++ b/modules/getreuer/orbital_mouse/README.md @@ -0,0 +1,56 @@ +# Orbital Mouse + + + + + + + +
Modulegetreuer/orbital_mouse
Version2025-03-07
MaintainerPascal Getreuer (@getreuer)
LicenseApache 2.0
Documentation +https://getreuer.info/posts/keyboards/orbital-mouse +
+ +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. + diff --git a/modules/getreuer/orbital_mouse/introspection.h b/modules/getreuer/orbital_mouse/introspection.h new file mode 100644 index 0000000..50c80e1 --- /dev/null +++ b/modules/getreuer/orbital_mouse/introspection.h @@ -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" + diff --git a/modules/getreuer/orbital_mouse/orbital_mouse.c b/modules/getreuer/orbital_mouse/orbital_mouse.c new file mode 100644 index 0000000..75369b1 --- /dev/null +++ b/modules/getreuer/orbital_mouse/orbital_mouse.c @@ -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 + * + */ + +#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); +} + diff --git a/modules/getreuer/orbital_mouse/orbital_mouse.h b/modules/getreuer/orbital_mouse/orbital_mouse.h new file mode 100644 index 0000000..45d8aec --- /dev/null +++ b/modules/getreuer/orbital_mouse/orbital_mouse.h @@ -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 + * + */ + +#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, +}; + diff --git a/modules/getreuer/orbital_mouse/qmk_module.json b/modules/getreuer/orbital_mouse/qmk_module.json new file mode 100644 index 0000000..7190ba7 --- /dev/null +++ b/modules/getreuer/orbital_mouse/qmk_module.json @@ -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"} + ] +} diff --git a/modules/getreuer/palettefx/README.md b/modules/getreuer/palettefx/README.md new file mode 100644 index 0000000..2235b1c --- /dev/null +++ b/modules/getreuer/palettefx/README.md @@ -0,0 +1,55 @@ +# PaletteFx + + + + + + + +
Modulegetreuer/palettefx
Version2025-03-07
MaintainerPascal Getreuer (@getreuer)
LicenseApache 2.0
Documentation +https://getreuer.info/posts/keyboards/palettefx +
+ +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. + diff --git a/modules/getreuer/palettefx/introspection.h b/modules/getreuer/palettefx/introspection.h new file mode 100644 index 0000000..08de2ad --- /dev/null +++ b/modules/getreuer/palettefx/introspection.h @@ -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" + diff --git a/modules/getreuer/palettefx/palettefx.c b/modules/getreuer/palettefx/palettefx.c new file mode 100644 index 0000000..6762209 --- /dev/null +++ b/modules/getreuer/palettefx/palettefx.c @@ -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 + * + */ + +#include "palettefx.h" +#include "quantum.h" + +#include + +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__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; +} + diff --git a/modules/getreuer/palettefx/palettefx.h b/modules/getreuer/palettefx/palettefx.h new file mode 100644 index 0000000..a78f599 --- /dev/null +++ b/modules/getreuer/palettefx/palettefx.h @@ -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 + * + */ + +#pragma once + +#include +#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 + diff --git a/modules/getreuer/palettefx/palettefx.inc b/modules/getreuer/palettefx/palettefx.inc new file mode 100644 index 0000000..66019a8 --- /dev/null +++ b/modules/getreuer/palettefx/palettefx.inc @@ -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 + * + */ + +#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__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 + diff --git a/modules/getreuer/palettefx/palettefx_default_config.h b/modules/getreuer/palettefx/palettefx_default_config.h new file mode 100644 index 0000000..1396346 --- /dev/null +++ b/modules/getreuer/palettefx/palettefx_default_config.h @@ -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 + diff --git a/modules/getreuer/palettefx/qmk_module.json b/modules/getreuer/palettefx/qmk_module.json new file mode 100644 index 0000000..b643be0 --- /dev/null +++ b/modules/getreuer/palettefx/qmk_module.json @@ -0,0 +1,7 @@ +{ + "module_name": "PaletteFx", + "maintainer": "getreuer", + "features": { + "rgb_matrix": true + } +} diff --git a/modules/getreuer/palettefx/rules.mk b/modules/getreuer/palettefx/rules.mk new file mode 100644 index 0000000..d4d33b6 --- /dev/null +++ b/modules/getreuer/palettefx/rules.mk @@ -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 diff --git a/modules/getreuer/select_word/README.md b/modules/getreuer/select_word/README.md new file mode 100644 index 0000000..95d8219 --- /dev/null +++ b/modules/getreuer/select_word/README.md @@ -0,0 +1,68 @@ +# Select Word + + + + + + + +
Modulegetreuer/select_word
Version2025-03-07
MaintainerPascal Getreuer (@getreuer)
LicenseApache 2.0
Documentation +https://getreuer.info/posts/keyboards/select-word +
+ +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. + diff --git a/modules/getreuer/select_word/introspection.h b/modules/getreuer/select_word/introspection.h new file mode 100644 index 0000000..e826ac8 --- /dev/null +++ b/modules/getreuer/select_word/introspection.h @@ -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" + diff --git a/modules/getreuer/select_word/qmk_module.json b/modules/getreuer/select_word/qmk_module.json new file mode 100644 index 0000000..964a980 --- /dev/null +++ b/modules/getreuer/select_word/qmk_module.json @@ -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" + ] + } + ] +} diff --git a/modules/getreuer/select_word/select_word.c b/modules/getreuer/select_word/select_word.c new file mode 100644 index 0000000..e174966 --- /dev/null +++ b/modules/getreuer/select_word/select_word.c @@ -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 + * + */ + +#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; +} + diff --git a/modules/getreuer/select_word/select_word.h b/modules/getreuer/select_word/select_word.h new file mode 100644 index 0000000..2255308 --- /dev/null +++ b/modules/getreuer/select_word/select_word.h @@ -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 + * + */ + +#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 diff --git a/modules/getreuer/sentence_case/README.md b/modules/getreuer/sentence_case/README.md new file mode 100644 index 0000000..37307bd --- /dev/null +++ b/modules/getreuer/sentence_case/README.md @@ -0,0 +1,60 @@ +# Sentence Case + + + + + + + +
Modulegetreuer/sentence_case
Version2025-03-07
MaintainerPascal Getreuer (@getreuer)
LicenseApache 2.0
Documentation +https://getreuer.info/posts/keyboards/sentence-case +
+ +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. + diff --git a/modules/getreuer/sentence_case/introspection.h b/modules/getreuer/sentence_case/introspection.h new file mode 100644 index 0000000..7018415 --- /dev/null +++ b/modules/getreuer/sentence_case/introspection.h @@ -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" + diff --git a/modules/getreuer/sentence_case/qmk_module.json b/modules/getreuer/sentence_case/qmk_module.json new file mode 100644 index 0000000..ad1f71e --- /dev/null +++ b/modules/getreuer/sentence_case/qmk_module.json @@ -0,0 +1,9 @@ +{ + "module_name": "Sentence Case", + "maintainer": "getreuer", + "keycodes": [ + {"key": "SENTENCE_CASE_ON"}, + {"key": "SENTENCE_CASE_OFF"}, + {"key": "SENTENCE_CASE_TOGGLE"} + ] +} diff --git a/modules/getreuer/sentence_case/sentence_case.c b/modules/getreuer/sentence_case/sentence_case.c new file mode 100644 index 0000000..e381c81 --- /dev/null +++ b/modules/getreuer/sentence_case/sentence_case.c @@ -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 + * + */ + +#include "sentence_case.h" + +#include + +#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 diff --git a/modules/getreuer/sentence_case/sentence_case.h b/modules/getreuer/sentence_case/sentence_case.h new file mode 100644 index 0000000..5d1a46e --- /dev/null +++ b/modules/getreuer/sentence_case/sentence_case.h @@ -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 + * + */ + +#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 diff --git a/modules/getreuer/socd_cleaner/README.md b/modules/getreuer/socd_cleaner/README.md new file mode 100644 index 0000000..7ab80ca --- /dev/null +++ b/modules/getreuer/socd_cleaner/README.md @@ -0,0 +1,79 @@ +# SOCD Cleaner + + + + + + + +
Modulegetreuer/socd_cleaner
Version2025-03-07
MaintainerPascal Getreuer (@getreuer)
LicenseApache 2.0
Documentation +https://getreuer.info/posts/keyboards/socd-cleaner +
+ +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. + diff --git a/modules/getreuer/socd_cleaner/introspection.c b/modules/getreuer/socd_cleaner/introspection.c new file mode 100644 index 0000000..92c862a --- /dev/null +++ b/modules/getreuer/socd_cleaner/introspection.c @@ -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 diff --git a/modules/getreuer/socd_cleaner/introspection.h b/modules/getreuer/socd_cleaner/introspection.h new file mode 100644 index 0000000..08fc430 --- /dev/null +++ b/modules/getreuer/socd_cleaner/introspection.h @@ -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" + diff --git a/modules/getreuer/socd_cleaner/qmk_module.json b/modules/getreuer/socd_cleaner/qmk_module.json new file mode 100644 index 0000000..6ff3416 --- /dev/null +++ b/modules/getreuer/socd_cleaner/qmk_module.json @@ -0,0 +1,9 @@ +{ + "module_name": "SOCD Cleaner", + "maintainer": "getreuer", + "keycodes": [ + {"key": "SOCDON"}, + {"key": "SOCDOFF"}, + {"key": "SOCDTOG"} + ] +} diff --git a/modules/getreuer/socd_cleaner/socd_cleaner.c b/modules/getreuer/socd_cleaner/socd_cleaner.c new file mode 100644 index 0000000..6721dd2 --- /dev/null +++ b/modules/getreuer/socd_cleaner/socd_cleaner.c @@ -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 + * + */ + +#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; +} + diff --git a/modules/getreuer/socd_cleaner/socd_cleaner.h b/modules/getreuer/socd_cleaner/socd_cleaner.h new file mode 100644 index 0000000..4e0ca9a --- /dev/null +++ b/modules/getreuer/socd_cleaner/socd_cleaner.h @@ -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 + * + */ + +#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 diff --git a/modules/getreuer/tap_flow/README.md b/modules/getreuer/tap_flow/README.md new file mode 100644 index 0000000..0d9f171 --- /dev/null +++ b/modules/getreuer/tap_flow/README.md @@ -0,0 +1,57 @@ +# Tap Flow + + + + + + + +
Modulegetreuer/tap_flow
Version2025-03-15
MaintainerPascal Getreuer (@getreuer)
LicenseApache 2.0
Documentation +https://getreuer.info/posts/keyboards/tap-flow +
+ +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 ms. | +| `TAP_FLOW_DOWN` | `TFLOW_D` | Decrease by 5 ms. | + +Tap Flow's default behavior is: + +* Filtering is done only when a tap-hold press is within `TAP_FLOW_TERM` of the + previous key event, which defaults to 150 ms. Use `TFLOW_U` / `TFLOW_D` + to tune, then define `TAP_FLOW_TERM` in your `config.h` to set the value + printed by `TFLOW_P`. + +* Filtering is done only when both the tap-hold key and the previous key are + among Space, letters AZ, and + punctuations , . ; /. 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 + diff --git a/modules/getreuer/tap_flow/introspection.h b/modules/getreuer/tap_flow/introspection.h new file mode 100644 index 0000000..e929752 --- /dev/null +++ b/modules/getreuer/tap_flow/introspection.h @@ -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" + diff --git a/modules/getreuer/tap_flow/qmk_module.json b/modules/getreuer/tap_flow/qmk_module.json new file mode 100644 index 0000000..949db91 --- /dev/null +++ b/modules/getreuer/tap_flow/qmk_module.json @@ -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" + ] + } + ] +} diff --git a/modules/getreuer/tap_flow/tap_flow.c b/modules/getreuer/tap_flow/tap_flow.c new file mode 100644 index 0000000..b413994 --- /dev/null +++ b/modules/getreuer/tap_flow/tap_flow.c @@ -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 + * + */ + +#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) + diff --git a/modules/getreuer/tap_flow/tap_flow.h b/modules/getreuer/tap_flow/tap_flow.h new file mode 100644 index 0000000..932d94f --- /dev/null +++ b/modules/getreuer/tap_flow/tap_flow.h @@ -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 + * + */ + +#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 + diff --git a/update.sh b/update.sh new file mode 100755 index 0000000..83546b4 --- /dev/null +++ b/update.sh @@ -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