summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPascal Getreuer <50221757+getreuer@users.noreply.github.com>2022-05-13 23:00:32 -0700
committerGitHub <noreply@github.com>2022-05-14 16:00:32 +1000
commit68b16bba68d79c63bbd649a5be58756e937c96da (patch)
treebe0d3d672c13c080f4da58687b5b1a261f465911
parent90eef4cd153cdc1b00d973e6abb029828f656294 (diff)
[Core] Add Caps Word feature to core (#16588)
Co-authored-by: precondition <57645186+precondition@users.noreply.github.com> Co-authored-by: Drashna Jaelre <drashna@live.com>
-rw-r--r--builddefs/generic_features.mk1
-rw-r--r--builddefs/show_options.mk3
-rw-r--r--data/mappings/info_config.json3
-rw-r--r--data/mappings/info_rules.json1
-rw-r--r--data/schemas/keyboard.jsonschema10
-rw-r--r--docs/_summary.md1
-rw-r--r--docs/feature_caps_word.md156
-rw-r--r--docs/keycodes.md8
-rw-r--r--quantum/caps_word.c80
-rw-r--r--quantum/caps_word.h43
-rw-r--r--quantum/keyboard.c7
-rw-r--r--quantum/process_keycode/process_caps_word.c160
-rw-r--r--quantum/process_keycode/process_caps_word.h37
-rw-r--r--quantum/quantum.c3
-rw-r--r--quantum/quantum.h5
-rw-r--r--quantum/quantum_keycodes.h3
-rw-r--r--tests/caps_word/config.h21
-rw-r--r--tests/caps_word/test.mk19
-rw-r--r--tests/caps_word/test_caps_word.cpp423
19 files changed, 983 insertions, 1 deletions
diff --git a/builddefs/generic_features.mk b/builddefs/generic_features.mk
index 0475a2ff09..c3f1ec0f72 100644
--- a/builddefs/generic_features.mk
+++ b/builddefs/generic_features.mk
@@ -17,6 +17,7 @@ SPACE_CADET_ENABLE ?= yes
GRAVE_ESC_ENABLE ?= yes
GENERIC_FEATURES = \
+ CAPS_WORD \
COMBO \
COMMAND \
DEFERRED_EXEC \
diff --git a/builddefs/show_options.mk b/builddefs/show_options.mk
index 16b69ef0ea..f67d009191 100644
--- a/builddefs/show_options.mk
+++ b/builddefs/show_options.mk
@@ -81,7 +81,8 @@ OTHER_OPTION_NAMES = \
RGBLIGHT_FULL_POWER \
LTO_ENABLE \
PROGRAMMABLE_BUTTON_ENABLE \
- SECURE_ENABLE
+ SECURE_ENABLE \
+ CAPS_WORD_ENABLE
define NAME_ECHO
@printf " %-30s = %-16s # %s\\n" "$1" "$($1)" "$(origin $1)"
diff --git a/data/mappings/info_config.json b/data/mappings/info_config.json
index 02ad3226c2..d9f96b5892 100644
--- a/data/mappings/info_config.json
+++ b/data/mappings/info_config.json
@@ -11,6 +11,8 @@
"BACKLIGHT_BREATHING": {"info_key": "backlight.breathing", "value_type": "bool"},
"BREATHING_PERIOD": {"info_key": "backlight.breathing_period", "value_type": "int"},
"BACKLIGHT_PIN": {"info_key": "backlight.pin"},
+ "BOTH_SHIFTS_TURNS_ON_CAPS_WORD": {"info_key": "caps_word.both_shifts_turns_on", "value_type": "bool"},
+ "CAPS_WORD_IDLE_TIMEOUT": {"info_key": "caps_word.idle_timeout", "value_type": "int"},
"COMBO_COUNT": {"info_key": "combo.count", "value_type": "int"},
"COMBO_TERM": {"info_key": "combo.term", "value_type": "int"},
"DEBOUNCE": {"info_key": "debounce", "value_type": "int"},
@@ -19,6 +21,7 @@
#"DEVICE_VER": {"info_key": "usb.device_version", "value_type": "bcd_version"},
"DESCRIPTION": {"info_key": "keyboard_folder", "value_type": "str", "to_json": false},
"DIODE_DIRECTION": {"info_key": "diode_direction"},
+ "DOUBLE_TAP_SHIFT_TURNS_ON_CAPS_WORD": {"info_key": "caps_word.double_tap_shift_turns_on", "value_type": "bool"},
"FORCE_NKRO": {"info_key": "usb.force_nkro", "value_type": "bool"},
"DYNAMIC_KEYMAP_EEPROM_MAX_ADDR": {"info_key": "dynamic_keymap.eeprom_max_addr", "value_type": "int"},
"DYNAMIC_KEYMAP_LAYER_COUNT": {"info_key": "dynamic_keymap.layer_count", "value_type": "int"},
diff --git a/data/mappings/info_rules.json b/data/mappings/info_rules.json
index 4b0fde5629..a8b39afbd1 100644
--- a/data/mappings/info_rules.json
+++ b/data/mappings/info_rules.json
@@ -10,6 +10,7 @@
"BOARD": {"info_key": "board"},
"BOOTLOADER": {"info_key": "bootloader", "warn_duplicate": false},
"BLUETOOTH": {"info_key": "bluetooth.driver"},
+ "CAPS_WORD_ENABLE": {"info_key": "caps_word.enabled", "value_type": "bool"},
"FIRMWARE_FORMAT": {"info_key": "build.firmware_format"},
"KEYBOARD_SHARED_EP": {"info_key": "usb.shared_endpoint.keyboard", "value_type": "bool"},
"MOUSE_SHARED_EP": {"info_key": "usb.shared_endpoint.mouse", "value_type": "bool"},
diff --git a/data/schemas/keyboard.jsonschema b/data/schemas/keyboard.jsonschema
index 6b1e543e48..aaf6f887d5 100644
--- a/data/schemas/keyboard.jsonschema
+++ b/data/schemas/keyboard.jsonschema
@@ -92,6 +92,16 @@
"enum": ["COL2ROW", "ROW2COL"]
},
"debounce": {"$ref": "qmk.definitions.v1#/unsigned_int"},
+ "caps_word": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {"type": "boolean"},
+ "both_shifts_turns_on": {"type": "boolean"},
+ "double_tap_shift_turns_on": {"type": "boolean"},
+ "idle_timeout": {"$ref": "qmk.definitions.v1#/unsigned_int"},
+ },
+ },
"combo": {
"type": "object",
"properties": {
diff --git a/docs/_summary.md b/docs/_summary.md
index 786685eba4..fed75196b4 100644
--- a/docs/_summary.md
+++ b/docs/_summary.md
@@ -75,6 +75,7 @@
* Software Features
* [Auto Shift](feature_auto_shift.md)
+ * [Caps Word](feature_caps_word.md)
* [Combos](feature_combo.md)
* [Debounce API](feature_debounce_type.md)
* [Key Lock](feature_key_lock.md)
diff --git a/docs/feature_caps_word.md b/docs/feature_caps_word.md
new file mode 100644
index 0000000000..0b71119917
--- /dev/null
+++ b/docs/feature_caps_word.md
@@ -0,0 +1,156 @@
+# Caps Word
+
+It is often useful to type a single word in all capitals, for instance
+abbreviations like "QMK", or in code, identifiers like `KC_SPC`. "Caps Word" is
+a modern alternative to Caps Lock:
+
+* Letters are capitalized while active, and Caps Word automatically disables
+ itself at the end of the word. That is, it stops by default once a space or
+ any key other than `a`--`z`, `0`--`9`, `-`, `_`, delete, or backspace is
+ pressed. Caps Word also disables itself if the keyboard is idle for 5 seconds.
+ This is configurable, see below.
+
+* To avoid requiring a dedicated key for Caps Word, there is an option
+ (`BOTH_SHIFTS_TURNS_ON_CAPS_WORD`) to activate Caps Word by simultaneously
+ pressing both shift keys. See below for other options.
+
+* The implementation does not use the Caps Lock (`KC_CAPS`) keycode. Caps Word
+ works even if you're remapping Caps Lock at the OS level to Ctrl or something
+ else, as Emacs and Vim users often do.
+
+
+## How do I enable Caps Word :id=how-do-i-enable-caps-word
+
+In your `rules.mk`, add:
+
+```make
+CAPS_WORD_ENABLE = yes
+```
+
+Next, use one the following methods to activate Caps Word:
+
+* **Activate by pressing a key**: Use the `CAPS_WORD` keycode (short
+ alias `CAPSWRD`) in your keymap.
+
+* **Activate by pressing Left Shift + Right Shift**: Add `#define
+ BOTH_SHIFTS_TURNS_ON_CAPS_WORD` to config.h. You may also need to disable or
+ reconfigure Command, details below. Then, simultaneously pressing both left
+ and right shifts turns on Caps Word. This method works with the plain
+ `KC_LSFT` and `KC_RSFT` keycodes as well as one-shot shifts and Space Cadet
+ shifts. If your shift keys are mod-taps, hold both shift mod-tap keys until
+ the tapping term, then release them.
+
+* **Activate by double tapping Left Shift**: Add `#define
+ DOUBLE_TAP_SHIFT_TURNS_ON_CAPS_WORD` config.h. Then, double tapping Left Shift
+ turns on Caps Word. This method works with `KC_LSFT` or one-shot Left Shift
+ `OSM(MOD_LSFT)`. To count as a double tap, the maximum time in milliseconds
+ between taps is `TAPPING_TERM`, or if using `TAPPING_TERM_PER_KEY`, the time
+ returned by `get_tapping_term()` for the shift keycode being tapped.
+
+* **Custom activation**: You can activate Caps Word from code by calling
+ `caps_word_on()`. This may be used to activate Caps Word through [a
+ combo](feature_combo.md) or [tap dance](feature_tap_dance.md) or any means
+ you like.
+
+### Troubleshooting: Command :id=troubleshooting-command
+
+When using `BOTH_SHIFTS_TURNS_ON_CAPS_WORD`, you might see a compile message
+**"BOTH_SHIFTS_TURNS_ON_CAPS_WORD and Command should not be enabled at the same
+time, since both use the Left Shift + Right Shift key combination."**
+
+Many keyboards enable the [Command feature](feature_command.md), which by
+default is also activated using the Left Shift + Right Shift key combination. To
+fix this conflict, please disable Command by adding in rules.mk:
+
+```make
+COMMAND_ENABLE = no
+```
+
+Or configure Command to use another key combination like Left Ctrl + Right Ctrl
+by defining `IS_COMMAND()` in config.h:
+
+```c
+// Activate Command with Left Ctrl + Right Ctrl.
+#define IS_COMMAND() (get_mods() == MOD_MASK_CTRL)
+```
+
+
+## Customizing Caps Word :id=customizing-caps-word
+
+### Idle timeout :id=idle-timeout
+
+Caps Word turns off automatically if no keys are pressed for
+`CAPS_WORD_IDLE_TIMEOUT` milliseconds. The default is 5000 (5 seconds).
+Configure the timeout duration in config.h, for instance
+
+```c
+#define CAPS_WORD_IDLE_TIMEOUT 3000 // 3 seconds.
+```
+
+Setting `CAPS_WORD_IDLE_TIMEOUT` to 0 configures Caps Word to never time out.
+Caps Word then remains active indefinitely until a word breaking key is pressed.
+
+
+### Functions :id=functions
+
+Functions to manipulate Caps Word:
+
+| Function | Description |
+|-------------------------|------------------------------------------------|
+| `caps_word_on()` | Turns Caps Word on. |
+| `caps_word_off()` | Turns Caps Word off. |
+| `caps_word_toggle()` | Toggles Caps Word. |
+| `is_caps_word_on()` | Returns true if Caps Word is currently on. |
+
+
+### Configure which keys are "word breaking" :id=configure-which-keys-are-word-breaking
+
+You can define the `caps_word_press_user(uint16_t keycode)` callback to
+configure which keys should be shifted and which keys are considered "word
+breaking" and stop Caps Word.
+
+The callback is called on every key press while Caps Word is active. When the
+key should be shifted (that is, a letter key), the callback should call
+`add_weak_mods(MOD_BIT(KC_LSFT))` to shift the key. Returning true continues the
+current "word," while returning false is "word breaking" and deactivates Caps
+Word. The default callback is
+
+```c
+bool caps_word_press_user(uint16_t keycode) {
+ switch (keycode) {
+ // Keycodes that continue Caps Word, with shift applied.
+ case KC_A ... KC_Z:
+ case KC_MINS:
+ add_weak_mods(MOD_BIT(KC_LSFT)); // Apply shift to next key.
+ return true;
+
+ // Keycodes that continue Caps Word, without shifting.
+ case KC_1 ... KC_0:
+ case KC_BSPC:
+ case KC_DEL:
+ case KC_UNDS:
+ return true;
+
+ default:
+ return false; // Deactivate Caps Word.
+ }
+}
+```
+
+
+### Representing Caps Word state :id=representing-caps-word-state
+
+Define `caps_word_set_user(bool active)` to get callbacks when Caps Word turns
+on or off. This is useful to represent the current Caps Word state, e.g. by
+setting an LED or playing a sound. In your keymap, define
+
+```c
+void caps_word_set_user(bool active) {
+ if (active) {
+ // Do something when Caps Word activates.
+ } else {
+ // Do something when Caps Word deactivates.
+ }
+}
+```
+
diff --git a/docs/keycodes.md b/docs/keycodes.md
index 10652bdb2f..bd5af32dd3 100644
--- a/docs/keycodes.md
+++ b/docs/keycodes.md
@@ -269,6 +269,14 @@ See also: [Bluetooth](feature_bluetooth.md)
|`OUT_USB` |USB only |
|`OUT_BT` |Bluetooth only |
+## Caps Word :id=caps-word
+
+See also: [Caps Word](feature_caps_word.md)
+
+|Key |Aliases |Description |
+|-----------|---------|------------------------------|
+|`CAPS_WORD`|`CAPSWRD`|Toggles Caps Word |
+
## Dynamic Macros :id=dynamic-macros
See also: [Dynamic Macros](feature_dynamic_macros.md)
diff --git a/quantum/caps_word.c b/quantum/caps_word.c
new file mode 100644
index 0000000000..5b83659f28
--- /dev/null
+++ b/quantum/caps_word.c
@@ -0,0 +1,80 @@
+// Copyright 2021-2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "caps_word.h"
+
+/** @brief True when Caps Word is active. */
+static bool caps_word_active = false;
+
+#if CAPS_WORD_IDLE_TIMEOUT > 0
+// Constrain timeout to a sensible range. With 16-bit timers, the longest
+// timeout possible is 32768 ms, rounded here to 30000 ms = half a minute.
+# if CAPS_WORD_IDLE_TIMEOUT < 100 || CAPS_WORD_IDLE_TIMEOUT > 30000
+# error "CAPS_WORD_IDLE_TIMEOUT must be between 100 and 30000 ms"
+# endif
+
+/** @brief Deadline for idle timeout. */
+static uint16_t idle_timer = 0;
+
+void caps_word_task(void) {
+ if (caps_word_active && timer_expired(timer_read(), idle_timer)) {
+ caps_word_off();
+ }
+}
+
+void caps_word_reset_idle_timer(void) {
+ idle_timer = timer_read() + CAPS_WORD_IDLE_TIMEOUT;
+}
+#endif // CAPS_WORD_IDLE_TIMEOUT > 0
+
+void caps_word_on(void) {
+ if (caps_word_active) {
+ return;
+ }
+
+ clear_mods();
+#ifndef NO_ACTION_ONESHOT
+ clear_oneshot_mods();
+#endif // NO_ACTION_ONESHOT
+#if CAPS_WORD_IDLE_TIMEOUT > 0
+ caps_word_reset_idle_timer();
+#endif // CAPS_WORD_IDLE_TIMEOUT > 0
+
+ caps_word_active = true;
+ caps_word_set_user(true);
+}
+
+void caps_word_off(void) {
+ if (!caps_word_active) {
+ return;
+ }
+
+ unregister_weak_mods(MOD_MASK_SHIFT); // Make sure weak shift is off.
+ caps_word_active = false;
+ caps_word_set_user(false);
+}
+
+void caps_word_toggle(void) {
+ if (caps_word_active) {
+ caps_word_off();
+ } else {
+ caps_word_on();
+ }
+}
+
+bool is_caps_word_on(void) {
+ return caps_word_active;
+}
+
+__attribute__((weak)) void caps_word_set_user(bool active) {}
diff --git a/quantum/caps_word.h b/quantum/caps_word.h
new file mode 100644
index 0000000000..b83f73371e
--- /dev/null
+++ b/quantum/caps_word.h
@@ -0,0 +1,43 @@
+// Copyright 2021-2022 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 "quantum.h"
+
+#ifndef CAPS_WORD_IDLE_TIMEOUT
+# define CAPS_WORD_IDLE_TIMEOUT 5000 // Default timeout of 5 seconds.
+#endif // CAPS_WORD_IDLE_TIMEOUT
+
+#if CAPS_WORD_IDLE_TIMEOUT > 0
+/** @brief Matrix scan task for Caps Word feature */
+void caps_word_task(void);
+
+/** @brief Resets timer for Caps Word idle timeout. */
+void caps_word_reset_idle_timer(void);
+#else
+static inline void caps_word_task(void) {}
+#endif // CAPS_WORD_IDLE_TIMEOUT > 0
+
+void caps_word_on(void); /**< Activates Caps Word. */
+void caps_word_off(void); /**< Deactivates Caps Word. */
+void caps_word_toggle(void); /**< Toggles Caps Word. */
+bool is_caps_word_on(void); /**< Gets whether currently active. */
+
+/**
+ * @brief Caps Word set callback.
+ *
+ * @param active True if Caps Word is active, false otherwise
+ */
+void caps_word_set_user(bool active);
diff --git a/quantum/keyboard.c b/quantum/keyboard.c
index 1dccd48e2f..a65f9d6d18 100644
--- a/quantum/keyboard.c
+++ b/quantum/keyboard.c
@@ -108,6 +108,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#ifdef BLUETOOTH_ENABLE
# include "outputselect.h"
#endif
+#ifdef CAPS_WORD_ENABLE
+# include "caps_word.h"
+#endif
static uint32_t last_input_modification_time = 0;
uint32_t last_input_activity_time(void) {
@@ -549,6 +552,10 @@ void quantum_task(void) {
autoshift_matrix_scan();
#endif
+#ifdef CAPS_WORD_ENABLE
+ caps_word_task();
+#endif
+
#ifdef SECURE_ENABLE
secure_task();
#endif
diff --git a/quantum/process_keycode/process_caps_word.c b/quantum/process_keycode/process_caps_word.c
new file mode 100644
index 0000000000..15238f04a1
--- /dev/null
+++ b/quantum/process_keycode/process_caps_word.c
@@ -0,0 +1,160 @@
+// Copyright 2021-2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "process_caps_word.h"
+
+bool process_caps_word(uint16_t keycode, keyrecord_t* record) {
+ if (keycode == CAPSWRD) { // Pressing CAPSWRD toggles Caps Word.
+ if (record->event.pressed) {
+ caps_word_toggle();
+ }
+ return false;
+ }
+
+#ifndef NO_ACTION_ONESHOT
+ const uint8_t mods = get_mods() | get_oneshot_mods();
+#else
+ const uint8_t mods = get_mods();
+#endif // NO_ACTION_ONESHOT
+
+ if (!is_caps_word_on()) {
+ // The following optionally turns on Caps Word by holding left and
+ // right shifts or by double tapping left shift. This way Caps Word
+ // may be used without needing a dedicated key and also without
+ // needing combos or tap dance.
+
+#ifdef BOTH_SHIFTS_TURNS_ON_CAPS_WORD
+ // Many keyboards enable the Command feature by default, which also
+ // uses left+right shift. It can be configured to use a different
+ // key combination by defining IS_COMMAND(). We make a non-fatal
+ // warning if Command is enabled but IS_COMMAND() is *not* defined.
+# if defined(COMMAND_ENABLE) && !defined(IS_COMMAND)
+# pragma message "BOTH_SHIFTS_TURNS_ON_CAPS_WORD and Command should not be enabled at the same time, since both use the Left Shift + Right Shift key combination. Please disable Command, or ensure that `IS_COMMAND` is not set to (get_mods() == MOD_MASK_SHIFT)."
+# else
+ if (mods == MOD_MASK_SHIFT
+# ifdef COMMAND_ENABLE
+ // Don't activate Caps Word at the same time as Command.
+ && !(IS_COMMAND())
+# endif // COMMAND_ENABLE
+ ) {
+ caps_word_on();
+ }
+# endif // defined(COMMAND_ENABLE) && !defined(IS_COMMAND)
+#endif // BOTH_SHIFTS_TURNS_ON_CAPS_WORD
+
+#ifdef DOUBLE_TAP_SHIFT_TURNS_ON_CAPS_WORD
+ // Double tapping left shift turns on Caps Word.
+ //
+ // NOTE: This works with KC_LSFT and one-shot left shift. It
+ // wouldn't make sense with mod-tap or Space Cadet shift since
+ // double tapping would of course trigger the tapping action.
+ if (record->event.pressed) {
+ static bool tapped = false;
+ static uint16_t timer = 0;
+ if (keycode == KC_LSFT || keycode == OSM(MOD_LSFT)) {
+ if (tapped && !timer_expired(record->event.time, timer)) {
+ // Left shift was double tapped, activate Caps Word.
+ caps_word_on();
+ }
+ tapped = true;
+ timer = record->event.time + GET_TAPPING_TERM(keycode, record);
+ } else {
+ tapped = false; // Reset when any other key is pressed.
+ }
+ }
+#endif // DOUBLE_TAP_SHIFT_TURNS_ON_CAPS_WORD
+
+ return true;
+ }
+
+#if CAPS_WORD_IDLE_TIMEOUT > 0
+ caps_word_reset_idle_timer();
+#endif // CAPS_WORD_IDLE_TIMEOUT > 0
+
+ // From here on, we only take action on press events.
+ if (!record->event.pressed) {
+ return true;
+ }
+
+ if (!(mods & ~MOD_MASK_SHIFT)) {
+ switch (keycode) {
+ // Ignore MO, TO, TG, TT, and OSL 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:
+ return true;
+
+#ifndef NO_ACTION_TAPPING
+ case QK_MOD_TAP ... QK_MOD_TAP_MAX:
+ if (record->tap.count == 0) {
+ // Deactivate if a mod becomes active through holding
+ // a mod-tap key.
+ caps_word_off();
+ return true;
+ }
+ keycode &= 0xff;
+ break;
+
+# ifndef NO_ACTION_LAYER
+ case QK_LAYER_TAP ... QK_LAYER_TAP_MAX:
+# endif // NO_ACTION_LAYER
+ if (record->tap.count == 0) {
+ return true;
+ }
+ keycode &= 0xff;
+ break;
+#endif // NO_ACTION_TAPPING
+
+#ifdef SWAP_HANDS_ENABLE
+ case QK_SWAP_HANDS ... QK_SWAP_HANDS_MAX:
+ if (keycode > 0x56F0 || record->tap.count == 0) {
+ return true;
+ }
+ keycode &= 0xff;
+ break;
+#endif // SWAP_HANDS_ENABLE
+ }
+
+ clear_weak_mods();
+ if (caps_word_press_user(keycode)) {
+ send_keyboard_report();
+ return true;
+ }
+ }
+
+ caps_word_off();
+ return true;
+}
+
+__attribute__((weak)) bool caps_word_press_user(uint16_t keycode) {
+ switch (keycode) {
+ // Keycodes that continue Caps Word, with shift applied.
+ case KC_A ... KC_Z:
+ case KC_MINS:
+ add_weak_mods(MOD_BIT(KC_LSFT)); // Apply shift to next key.
+ return true;
+
+ // Keycodes that continue Caps Word, without shifting.
+ case KC_1 ... KC_0:
+ case KC_BSPC:
+ case KC_DEL:
+ case KC_UNDS:
+ return true;
+
+ default:
+ return false; // Deactivate Caps Word.
+ }
+}
diff --git a/quantum/process_keycode/process_caps_word.h b/quantum/process_keycode/process_caps_word.h
new file mode 100644
index 0000000000..f215bbc3a3
--- /dev/null
+++ b/quantum/process_keycode/process_caps_word.h
@@ -0,0 +1,37 @@
+// Copyright 2021-2022 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 "quantum.h"
+#include "caps_word.h"
+
+/**
+ * @brief Process handler for Caps Word feature.
+ *
+ * @param keycode Keycode registered by matrix press, per keymap
+ * @param record keyrecord_t structure
+ * @return true Continue processing keycodes, and send to host
+ * @return false Stop processing keycodes, and don't send to host
+ */
+bool process_caps_word(uint16_t keycode, keyrecord_t* record);
+
+/**
+ * @brief Weak function for user-level Caps Word press modification.
+ *
+ * @param keycode Keycode registered by matrix press, per keymap
+ * @return true Continue Caps Word
+ * @return false Stop Caps Word
+ */
+bool caps_word_press_user(uint16_t keycode);
diff --git a/quantum/quantum.c b/quantum/quantum.c
index c0e801a4bb..b54b46760c 100644
--- a/quantum/quantum.c
+++ b/quantum/quantum.c
@@ -307,6 +307,9 @@ bool process_record_quantum(keyrecord_t *record) {
#ifdef TERMINAL_ENABLE
process_terminal(keycode, record) &&
#endif
+#ifdef CAPS_WORD_ENABLE
+ process_caps_word(keycode, record) &&
+#endif
#ifdef SPACE_CADET_ENABLE
process_space_cadet(keycode, record) &&
#endif
diff --git a/quantum/quantum.h b/quantum/quantum.h
index 7829ec7e0c..92e1af1c40 100644
--- a/quantum/quantum.h
+++ b/quantum/quantum.h
@@ -233,6 +233,11 @@ extern layer_state_t layer_state;
# include "pointing_device.h"
#endif
+#ifdef CAPS_WORD_ENABLE
+# include "caps_word.h"
+# include "process_caps_word.h"
+#endif
+
// For tri-layer
void update_tri_layer(uint8_t layer1, uint8_t layer2, uint8_t layer3);
layer_state_t update_tri_layer_state(layer_state_t state, uint8_t layer1, uint8_t layer2, uint8_t layer3);
diff --git a/quantum/quantum_keycodes.h b/quantum/quantum_keycodes.h
index 5d5c4ed8c4..2f8ee2322e 100644
--- a/quantum/quantum_keycodes.h
+++ b/quantum/quantum_keycodes.h
@@ -602,6 +602,8 @@ enum quantum_keycodes {
SECURE_UNLOCK,
SECURE_TOGGLE,
+ CAPS_WORD,
+
// Start of custom keycode range for keyboards and keymaps - always leave at the end
SAFE_RANGE
};
@@ -964,5 +966,6 @@ enum quantum_keycodes {
#define PB_32 PROGRAMMABLE_BUTTON_32
#define PROGRAMMABLE_BUTTON_MIN PROGRAMMABLE_BUTTON_1
#define PROGRAMMABLE_BUTTON_MAX PROGRAMMABLE_BUTTON_32
+#define CAPSWRD CAPS_WORD
#include "quantum_keycodes_legacy.h"
diff --git a/tests/caps_word/config.h b/tests/caps_word/config.h
new file mode 100644
index 0000000000..0d5cebd778
--- /dev/null
+++ b/tests/caps_word/config.h
@@ -0,0 +1,21 @@
+// Copyright 2022 Google LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 2 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+#pragma once
+
+#include "test_common.h"
+
+#define BOTH_SHIFTS_TURNS_ON_CAPS_WORD
+#define DOUBLE_TAP_SHIFT_TURNS_ON_CAPS_WORD
diff --git a/tests/caps_word/test.mk b/tests/caps_word/test.mk
new file mode 100644
index 0000000000..2509b01858
--- /dev/null
+++ b/tests/caps_word/test.mk
@@ -0,0 +1,19 @@
+# Copyright 2022 Google LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+CAPS_WORD_ENABLE = yes
+COMMAND_ENABLE = no
+SPACE_CADET_ENABLE = yes
+
diff --git a/tests/caps_word/test_caps_word.cpp b/tests/caps_word/test_caps_word.cpp
new file mode 100644
index 0000000000..bcc8c53326
--- /dev/null
+++ b/tests/caps_word/test_caps_word.cpp
@@ -0,0 +1,423 @@
+// Copyright 2022 Google LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 2 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+#include "keyboard_report_util.hpp"
+#include "keycode.h"
+#include "test_common.hpp"
+#include "test_fixture.hpp"
+#include "test_keymap_key.hpp"
+
+using ::testing::_;
+using ::testing::AnyNumber;
+using ::testing::AnyOf;
+using ::testing::InSequence;
+using ::testing::TestParamInfo;
+
+class CapsWord : public TestFixture {
+ public:
+ void SetUp() override {
+ caps_word_off();
+ }
+
+ // Convenience function to tap `key`.
+ void TapKey(KeymapKey key) {
+ key.press();
+ run_one_scan_loop();
+ key.release();
+ run_one_scan_loop();
+ }
+
+ // Taps in order each key in `keys`.
+ template <typename... Ts>
+ void TapKeys(Ts... keys) {
+ for (KeymapKey key : {keys...}) {
+ TapKey(key);
+ }
+ }
+};
+
+// Tests caps_word_on(), _off(), and _toggle() functions.
+TEST_F(CapsWord, OnOffToggleFuns) {
+ TestDriver driver;
+
+ EXPECT_EQ(is_caps_word_on(), false);
+
+ caps_word_on();
+ EXPECT_EQ(is_caps_word_on(), true);
+ caps_word_on();
+ EXPECT_EQ(is_caps_word_on(), true);
+
+ caps_word_off();
+ EXPECT_EQ(is_caps_word_on(), false);
+ caps_word_off();
+ EXPECT_EQ(is_caps_word_on(), false);
+
+ caps_word_toggle();
+ EXPECT_EQ(is_caps_word_on(), true);
+ caps_word_toggle();
+ EXPECT_EQ(is_caps_word_on(), false);
+
+ testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+// Tests the default `caps_word_press_user()` function.
+TEST_F(CapsWord, DefaultCapsWordPressUserFun) {
+ // Spot check some keycodes that continue Caps Word, with shift applied.
+ for (uint16_t keycode : {KC_A, KC_B, KC_Z, KC_MINS}) {
+ SCOPED_TRACE("keycode: " + testing::PrintToString(keycode));
+ clear_weak_mods();
+ EXPECT_TRUE(caps_word_press_user(keycode));
+ EXPECT_EQ(get_weak_mods(), MOD_BIT(KC_LSFT));
+ }
+
+ // Some keycodes that continue Caps Word, without shifting.
+ for (uint16_t keycode : {KC_1, KC_9, KC_0, KC_BSPC, KC_DEL}) {
+ SCOPED_TRACE("keycode: " + testing::PrintToString(keycode));
+ clear_weak_mods();
+ EXPECT_TRUE(caps_word_press_user(keycode));
+ EXPECT_EQ(get_weak_mods(), 0);
+ }
+
+ // Some keycodes that turn off Caps Word.
+ for (uint16_t keycode : {KC_SPC, KC_DOT, KC_COMM, KC_TAB, KC_ESC, KC_ENT}) {
+ SCOPED_TRACE("keycode: " + testing::PrintToString(keycode));
+ EXPECT_FALSE(caps_word_press_user(keycode));
+ }
+}
+
+// Tests that `CAPSWRD` key toggles Caps Word.
+TEST_F(CapsWord, CapswrdKey) {
+ TestDriver driver;
+ KeymapKey key_capswrd(0, 0, 0, CAPSWRD);
+ set_keymap({key_capswrd});
+
+ // No keyboard reports should be sent.
+ EXPECT_CALL(driver, send_keyboard_mock(_)).Times(0);
+
+ TapKey(key_capswrd); // Tap the CAPSWRD key.
+ EXPECT_EQ(is_caps_word_on(), true);
+
+ TapKey(key_capswrd); // Tap the CAPSWRD key again.
+ EXPECT_EQ(is_caps_word_on(), false);
+
+ testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+// Tests that being idle for CAPS_WORD_IDLE_TIMEOUT turns off Caps Word.
+TEST_F(CapsWord, IdleTimeout) {
+ TestDriver driver;
+ KeymapKey key_a(0, 0, 0, KC_A);
+ set_keymap({key_a});
+
+ // Allow any number of reports with no keys or only KC_LSFT.
+ // clang-format off
+ EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
+ KeyboardReport(),
+ KeyboardReport(KC_LSFT))))
+ .Times(AnyNumber());
+ // clang-format on
+
+ // Expect "Shift+A".
+ EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LSFT, KC_A)));
+
+ // Turn on Caps Word and tap "A".
+ caps_word_on();
+ TapKey(key_a);
+
+ testing::Mock::VerifyAndClearExpectations(&driver);
+
+ idle_for(CAPS_WORD_IDLE_TIMEOUT);
+ run_one_scan_loop();
+
+ // Caps Word should be off and mods should be clear.
+ EXPECT_EQ(is_caps_word_on(), false);
+ EXPECT_EQ(get_mods() | get_weak_mods(), 0);
+
+ EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport())).Times(AnyNumber());
+ // Expect unshifted "A".
+ EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_A)));
+ TapKey(key_a);
+
+ testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+// Tests that typing "A, 4, A, 4" produces "Shift+A, 4, Shift+A, 4".
+TEST_F(CapsWord, ShiftsLettersButNotDigits) {
+ TestDriver driver;
+ KeymapKey key_a(0, 0, 0, KC_A);
+ KeymapKey key_4(0, 1, 0, KC_4);
+ set_keymap({key_a, key_4});
+
+ // Allow any number of reports with no keys or only KC_LSFT.
+ // clang-format off
+ EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
+ KeyboardReport(),
+ KeyboardReport(KC_LSFT))))
+ .Times(AnyNumber());
+ // clang-format on
+
+ { // Expect: "Shift+A, 4, Shift+A, 4".
+ InSequence s;
+ EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LSFT, KC_A)));
+ EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_4)));
+ EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LSFT, KC_A)));
+ EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_4)));
+ }
+
+ // Turn on Caps Word and tap "A, 4, A, 4".
+ caps_word_on();
+ TapKeys(key_a, key_4, key_a, key_4);
+
+ testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+// Tests that typing "A, Space, A" produces "Shift+A, Space, A".
+TEST_F(CapsWord, SpaceTurnsOffCapsWord) {
+ TestDriver driver;
+ KeymapKey key_a(0, 0, 0, KC_A);
+ KeymapKey key_spc(0, 1, 0, KC_SPC);
+ set_keymap({key_a, key_spc});
+
+ // Allow any number of reports with no keys or only KC_LSFT.
+ // clang-format off
+ EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
+ KeyboardReport(),
+ KeyboardReport(KC_LSFT))))
+ .Times(AnyNumber());
+ // clang-format on
+
+ { // Expect: "Shift+A, Space, A".
+ InSequence seq;
+ EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LSFT, KC_A)));
+ EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_SPC)));
+ EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_A)));
+ }
+
+ // Turn on Caps Word and tap "A, Space, A".
+ caps_word_on();
+ TapKeys(key_a, key_spc, key_a);
+
+ testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+struct CapsWordBothShiftsParams {
+ std::string name;
+ uint16_t left_shift_keycode;
+ uint16_t right_shift_keycode;
+
+ static const std::string& GetName(const TestParamInfo<CapsWordBothShiftsParams>& info) {
+ return info.param.name;
+ }
+};
+
+// Tests the BOTH_SHIFTS_TURNS_ON_CAPS_WORD method to turn on Caps Word.
+class CapsWordBothShifts : public ::testing::WithParamInterface<CapsWordBothShiftsParams>, public CapsWord {};
+
+// Pressing shifts as "Left down, Right down, Left up, Right up".
+TEST_P(CapsWordBothShifts, PressLRLR) {
+ TestDriver driver;
+ KeymapKey left_shift(0, 0, 0, GetParam().left_shift_keycode);
+ KeymapKey right_shift(0, 1, 0, GetParam().right_shift_keycode);
+ set_keymap({left_shift, right_shift});
+
+ // clang-format off
+ EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
+ KeyboardReport(),
+ KeyboardReport(KC_LSFT),
+ KeyboardReport(KC_RSFT),
+ KeyboardReport(KC_LSFT, KC_RSFT))))
+ .Times(AnyNumber());
+ // clang-format on
+
+ EXPECT_EQ(is_caps_word_on(), false);
+
+ left_shift.press(); // Press both shifts.
+ run_one_scan_loop();
+ right_shift.press();
+
+ // For mod-tap and Space Cadet keys, wait for the tapping term.
+ if (left_shift.code == LSFT_T(KC_A) || left_shift.code == KC_LSPO) {
+ idle_for(TAPPING_TERM);
+ }
+
+ run_one_scan_loop();
+ left_shift.release(); // Release both.
+ run_one_scan_loop();
+ right_shift.release();
+ run_one_scan_loop();
+
+ EXPECT_EQ(is_caps_word_on(), true);
+
+ testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+// Pressing shifts as "Left down, Right down, Right up, Left up".
+TEST_P(CapsWordBothShifts, PressLRRL) {
+ TestDriver driver;
+ KeymapKey left_shift(0, 0, 0, GetParam().left_shift_keycode);
+ KeymapKey right_shift(0, 1, 0, GetParam().right_shift_keycode);
+ set_keymap({left_shift, right_shift});
+
+ // clang-format off
+ EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
+ KeyboardReport(),
+ KeyboardReport(KC_LSFT),
+ KeyboardReport(KC_RSFT),
+ KeyboardReport(KC_LSFT, KC_RSFT))))
+ .Times(AnyNumber());
+ // clang-format on
+
+ EXPECT_EQ(is_caps_word_on(), false);
+
+ left_shift.press(); // Press both shifts.
+ run_one_scan_loop();
+ right_shift.press();
+
+ if (left_shift.code == LSFT_T(KC_A) || left_shift.code == KC_LSPO) {
+ idle_for(TAPPING_TERM);
+ }
+ run_one_scan_loop();
+
+ right_shift.release(); // Release both.
+ run_one_scan_loop();
+ left_shift.release();
+ run_one_scan_loop();
+
+ EXPECT_EQ(is_caps_word_on(), true);
+
+ testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+// clang-format off
+INSTANTIATE_TEST_CASE_P(
+ ShiftPairs,
+ CapsWordBothShifts,
+ ::testing::Values(
+ CapsWordBothShiftsParams{
+ "PlainShifts", KC_LSFT, KC_RSFT},
+ CapsWordBothShiftsParams{
+ "OneshotShifts", OSM(MOD_LSFT), OSM(MOD_RSFT)},
+ CapsWordBothShiftsParams{
+ "SpaceCadetShifts", KC_LSPO, KC_RSPC},
+ CapsWordBothShiftsParams{
+ "ModTapShifts", LSFT_T(KC_A), RSFT_T(KC_B)}
+ ),
+ CapsWordBothShiftsParams::GetName
+ );
+// clang-format on
+
+struct CapsWordDoubleTapShiftParams {
+ std::string name;
+ uint16_t left_shift_keycode;
+
+ static const std::string& GetName(const TestParamInfo<CapsWordDoubleTapShiftParams>& info) {
+ return info.param.name;
+ }
+};
+
+// Tests the DOUBLE_TAP_SHIFT_TURNS_ON_CAPS_WORD method to turn on Caps Word.
+class CapsWordDoubleTapShift : public ::testing::WithParamInterface<CapsWordDoubleTapShiftParams>, public CapsWord {};
+
+// Tests that double tapping activates Caps Word.
+TEST_P(CapsWordDoubleTapShift, Activation) {
+ TestDriver driver;
+ KeymapKey left_shift(0, 0, 0, GetParam().left_shift_keycode);
+ set_keymap({left_shift});
+
+ // clang-format off
+ EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
+ KeyboardReport(),
+ KeyboardReport(KC_LSFT))))
+ .Times(AnyNumber());
+ // clang-format on
+
+ EXPECT_EQ(is_caps_word_on(), false);
+
+ // Tapping shift twice within the tapping term turns on Caps Word.
+ TapKey(left_shift);
+ idle_for(TAPPING_TERM - 10);
+ TapKey(left_shift);
+
+ EXPECT_EQ(is_caps_word_on(), true);
+
+ testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+// Double tap doesn't count if another key is pressed between the taps.
+TEST_P(CapsWordDoubleTapShift, Interrupted) {
+ TestDriver driver;
+ KeymapKey left_shift(0, 0, 0, GetParam().left_shift_keycode);
+ KeymapKey key_a(0, 1, 0, KC_A);
+ set_keymap({left_shift, key_a});
+
+ // clang-format off
+ EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
+ KeyboardReport(),
+ KeyboardReport(KC_LSFT),
+ KeyboardReport(KC_LSFT, KC_A))))
+ .Times(AnyNumber());
+ // clang-format on
+
+ left_shift.press();
+ run_one_scan_loop();
+
+ TapKey(key_a); // 'A' key interrupts the double tap.
+
+ left_shift.release();
+ run_one_scan_loop();
+
+ idle_for(TAPPING_TERM - 10);
+ TapKey(left_shift);
+
+ EXPECT_EQ(is_caps_word_on(), false); // Caps Word is still off.
+ clear_oneshot_mods();
+
+ testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+// Double tap doesn't count if taps are more than tapping term apart.
+TEST_P(CapsWordDoubleTapShift, SlowTaps) {
+ TestDriver driver;
+ KeymapKey left_shift(0, 0, 0, GetParam().left_shift_keycode);
+ set_keymap({left_shift});
+
+ // clang-format off
+ EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
+ KeyboardReport(),
+ KeyboardReport(KC_LSFT))))
+ .Times(AnyNumber());
+ // clang-format on
+
+ TapKey(left_shift);
+ idle_for(TAPPING_TERM + 1);
+ TapKey(left_shift);
+
+ EXPECT_EQ(is_caps_word_on(), false); // Caps Word is still off.
+ clear_oneshot_mods();
+
+ testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+// clang-format off
+INSTANTIATE_TEST_CASE_P(
+ Shifts,
+ CapsWordDoubleTapShift,
+ ::testing::Values(
+ CapsWordDoubleTapShiftParams{"PlainShift", KC_LSFT},
+ CapsWordDoubleTapShiftParams{"OneshotShift", OSM(MOD_LSFT)}
+ ),
+ CapsWordDoubleTapShiftParams::GetName
+ );
+// clang-format on