36b5559b997cedd14a352aa70891558936b8b3a3

Author
Drashna Jaelre <drashna@live.com>
Committer
GitHub <noreply@github.com>
Date

Message

[Core] Add Layer Lock feature (#23430)

Co-authored-by: Daniel <1767914+iamdanielv@users.noreply.github.com>
Co-authored-by: Pascal Getreuer <getreuer@google.com>
Co-authored-by: Pascal Getreuer <50221757+getreuer@users.noreply.github.com>

Diff

This diff is truncated to protect this page.

  1diff --git a/builddefs/generic_features.mk b/builddefs/generic_features.mk
  2index dc34a642307d7c0c6797e59f35ceaada54041432..f14f44087702c7e15e7c07ffc9649193012e6d99 100644
  3--- a/builddefs/generic_features.mk
  4+++ b/builddefs/generic_features.mk
  5@@ -36,6 +36,7 @@ GENERIC_FEATURES = \
  6     HAPTIC \
  7     KEY_LOCK \
  8     KEY_OVERRIDE \
  9+    LAYER_LOCK \
 10     LEADER \
 11     MAGIC \
 12     MOUSEKEY \
 13diff --git a/data/constants/keycodes/keycodes_0.0.6_quantum.hjson b/data/constants/keycodes/keycodes_0.0.6_quantum.hjson
 14index 814cd66a5b127bd5441535eee2c6faa8f2de6318..be3285f9e437ea1c1bbd5e85b224d515ddb45c22 100644
 15--- a/data/constants/keycodes/keycodes_0.0.6_quantum.hjson
 16+++ b/data/constants/keycodes/keycodes_0.0.6_quantum.hjson
 17@@ -3,5 +3,12 @@
 18         "0x7C20": "!delete!", // old QK_OUTPUT_AUTO
 19         "0x7C21": "!delete!", // old QK_OUTPUT_USB
 20         "0x7C22": "!delete!", // old QK_OUTPUT_BLUETOOTH
 21+        "0x7C7B": {
 22+            "group": "quantum",
 23+            "key": "QK_LAYER_LOCK",
 24+            "aliases": [
 25+                "QK_LLCK"
 26+            ]
 27+        }
 28     }
 29 }
 30diff --git a/data/mappings/info_config.hjson b/data/mappings/info_config.hjson
 31index 4c895cf5d505c7e2946d9852000287924a53a5b7..8d3e3c7f0ee9afcfda7ac4d2d3b9f4a54209d500 100644
 32--- a/data/mappings/info_config.hjson
 33+++ b/data/mappings/info_config.hjson
 34@@ -64,6 +64,9 @@
 35     "WEAR_LEVELING_BACKING_SIZE": {"info_key": "eeprom.wear_leveling.backing_size", "value_type": "int", "to_json": false},
 36     "WEAR_LEVELING_LOGICAL_SIZE": {"info_key": "eeprom.wear_leveling.logical_size", "value_type": "int", "to_json": false},
 37 
 38+    // Layer locking
 39+    "LAYER_LOCK_IDLE_TIMEOUT": {"info_key": "layer_lock.timeout", "value_type": "int"},
 40+
 41     // Indicators
 42     "LED_CAPS_LOCK_PIN": {"info_key": "indicators.caps_lock"},
 43     "LED_NUM_LOCK_PIN": {"info_key": "indicators.num_lock"},
 44diff --git a/data/schemas/keyboard.jsonschema b/data/schemas/keyboard.jsonschema
 45index 9f1f6dd74a58a03ad513642959bffb39e4924736..ec87680fa0594b8519aad9a8c28e471da80f1a72 100644
 46--- a/data/schemas/keyboard.jsonschema
 47+++ b/data/schemas/keyboard.jsonschema
 48@@ -375,6 +375,12 @@
 49             }
 50         },
 51         "keycodes": {"$ref": "qmk.definitions.v1#/keycode_decl_array"},
 52+        "layer_lock": {
 53+            "type": "object",
 54+            "properties": {
 55+                "timeout": {"$ref": "qmk.definitions.v1#/unsigned_int"}
 56+            }
 57+        },
 58         "layout_aliases": {
 59             "type": "object",
 60             "additionalProperties": {"$ref": "qmk.definitions.v1#/layout_macro"}
 61diff --git a/docs/_sidebar.json b/docs/_sidebar.json
 62index 7f353ad351d0e4287c5bd1f5ad9b9da886f5f6be..15c3cbec317afba382351dc554b997702d7d8e2f 100644
 63--- a/docs/_sidebar.json
 64+++ b/docs/_sidebar.json
 65@@ -123,6 +123,7 @@
 66                     { "text": "Key Lock", "link": "/features/key_lock" },
 67                     { "text": "Key Overrides", "link": "/features/key_overrides" },
 68                     { "text": "Layers", "link": "/feature_layers" },
 69+                    { "text": "Layer Lock", "link": "/features/layer_lock" },
 70                     { "text": "One Shot Keys", "link": "/one_shot_keys" },
 71                     { "text": "OS Detection", "link": "/features/os_detection" },
 72                     { "text": "Raw HID", "link": "/features/rawhid" },
 73diff --git a/docs/feature_layers.md b/docs/feature_layers.md
 74index 30ab7132226201b5f5bcb749be0a2003d617a27c..45f02fe536d69a8cd79067d561cc67944cea3d7a 100644
 75--- a/docs/feature_layers.md
 76+++ b/docs/feature_layers.md
 77@@ -17,6 +17,9 @@ These functions allow you to activate layers in various ways. Note that layers a
 78diff --git a/docs/features/layer_lock.md b/docs/features/layer_lock.md
 79new file mode 100644
 80index 0000000000000000000000000000000000000000..aaf323acccde77b9a9069df732a9f15b0b743db0
 81--- /dev/null
 82+++ b/docs/features/layer_lock.md
 83@@ -0,0 +1,139 @@
 84+# Layer Lock
 85+
 86+Some [layer switches](../feature_layers#switching-and-toggling-layers) access
 87+the layer by holding the key, including momentary layer `MO(layer)` and layer
 88+tap `LT(layer, key)` keys. You may sometimes need to stay on the layer for a
 89+long period of time. Layer Lock "locks" the current layer to stay on, supposing
 90+it was accessed by one of:
 91+
 92+ * `MO(layer)` momentary layer switch
 93+ * `LT(layer, key)` layer tap
 94+ * `OSL(layer)` one-shot layer
 95+ * `TT(layer)` layer tap toggle
 96+ * `LM(layer, mod)` layer-mod key (the layer is locked, but not the mods)
 97+
 98+Press the Layer Lock key again to unlock the layer. Additionally, when a layer
 99+is locked, layer switch keys that turn off the layer such as `TO(other_layer)`
100+will unlock it.
101+
102+
103+## How do I enable Layer Lock
104+
105+In your rules.mk, add:
106+
107+```make
108+LAYER_LOCK_ENABLE = yes
109+```
110+
111+Pick a key in your keymap on a layer you intend to lock, and assign it the
112+keycode `QK_LAYER_LOCK` (short alias `QK_LLCK`). Note that locking the base
113+layer has no effect, so typically, this key is used on layers above the base
114+layer.
115+
116+
117+## Example use
118+
119+Consider a keymap with the following base layer.
120+
121+![Base layer with a MO(NAV) key.](https://i.imgur.com/DkEhj9x.png)
122+
123+The highlighted key is a momentary layer switch `MO(NAV)`. Holding it accesses a
124+navigation layer.
125+
126+![Nav layer with a Layer Lock key.](https://i.imgur.com/2wUZNWk.png)
127+
128+
129+Holding the NAV key is fine for brief use, but awkward to continue holding when
130+using navigation functions continuously. The Layer Lock key comes to the rescue:
131+
132+1. Hold the NAV key, activating the navigation layer.
133+2. Tap Layer Lock.
134+3. Release NAV. The navigation layer stays on.
135+4. Make use of the arrow keys, etc.
136+5. Tap Layer Lock or NAV again to turn the navigation layer back off.
137+
138+A variation that would also work is to put the Layer Lock key on the base layer
139+and make other layers transparent (`KC_TRNS`) in that position. Pressing the
140+Layer Lock key locks (or unlocks) the highest active layer, regardless of which
141+layer the Layer Lock key is on.
142+
143+
144+## Idle timeout
145+
146+Optionally, Layer Lock may be configured to unlock if the keyboard is idle
147+for some time. In config.h, define `LAYER_LOCK_IDLE_TIMEOUT` in units of
148+milliseconds:
149+
150+```c
151+#define LAYER_LOCK_IDLE_TIMEOUT 60000  // Turn off after 60 seconds.
152+```
153+
154+
155+## Functions
156+
157+Use the following functions to query and manipulate the layer lock state.
158+
159+| Function                   | Description                        |
160+|----------------------------|------------------------------------|
161+| `is_layer_locked(layer)`   | Checks whether `layer` is locked.  |
162+| `layer_lock_on(layer)`     | Locks and turns on `layer`.        |
163+| `layer_lock_off(layer)`    | Unlocks and turns off `layer`.     |
164+| `layer_lock_invert(layer)` | Toggles whether `layer` is locked. |
165+
166+
167+## Representing the current Layer Lock state
168+
169+There is an optional callback `layer_lock_set_user()` that gets called when a
170+layer is locked or unlocked. This is useful to represent the current lock state
171+for instance by setting an LED. In keymap.c, define
172+
173+```c
174+bool layer_lock_set_user(layer_state_t locked_layers) {
175+  // Do something like `set_led(is_layer_locked(NAV));`
176+  return true;
177+}
178+```
179+
180+The argument `locked_layers` is a bitfield in which the kth bit is on if the kth
181+layer is locked. Alternatively, you can use `is_layer_locked(layer)` to check if
182+a given layer is locked.
183diff --git a/docs/keycodes.md b/docs/keycodes.md
184index 3665747a0bf8bf53e3e4146c5f11519fd4b295d4..cae6418266a2295bf29f9123b3bbdb4316c5e11e 100644
185--- a/docs/keycodes.md
186+++ b/docs/keycodes.md
187@@ -387,6 +387,14 @@ See also: [Key Lock](features/key_lock)
188 |---------|--------------------------------------------------------------|
189 |`QK_LOCK`|Hold down the next key pressed, until the key is pressed again|
190 
191+## Layer Lock {#layer-lock}
192+
193+See also: [Layer Lock](features/layer_lock)
194+
195+|Key            |Aliases  |Description                       |
196+|---------------|---------|----------------------------------|
197+|`QK_LAYER_LOCK`|`QK_LLCK`|Locks or unlocks the highest layer|
198+
199 ## Layer Switching {#layer-switching}
200 
201 See also: [Layer Switching](feature_layers#switching-and-toggling-layers)
202diff --git a/quantum/keyboard.c b/quantum/keyboard.c
203index df1dc1c3ee09e9c57fe9056b3c1468d6304365ec..8db81a4b391ba7fa95d2f4368672088fab073c7c 100644
204--- a/quantum/keyboard.c
205+++ b/quantum/keyboard.c
206@@ -140,6 +140,9 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
207 #ifdef OS_DETECTION_ENABLE
208 #    include "os_detection.h"
209 #endif
210+#if defined(LAYER_LOCK_ENABLE) && LAYER_LOCK_IDLE_TIMEOUT > 0
211+#    include "layer_lock.h"
212+#endif // LAYER_LOCK_ENABLE
213 
214 static uint32_t last_input_modification_time = 0;
215 uint32_t        last_input_activity_time(void) {
216@@ -655,6 +658,10 @@ void quantum_task(void) {
217 #ifdef SECURE_ENABLE
218     secure_task();
219 #endif
220+
221+#if defined(LAYER_LOCK_ENABLE) && LAYER_LOCK_IDLE_TIMEOUT > 0
222+    layer_lock_task();
223+#endif
224 }
225 
226 /** \brief Main task that is repeatedly called as fast as possible. */
227diff --git a/quantum/keycodes.h b/quantum/keycodes.h
228index 51b7eb6c9a4219c4429c900ed65e4b45180d987c..e9da5105ce46e27e238ece62ac1810dc7aeff484 100644
229--- a/quantum/keycodes.h
230+++ b/quantum/keycodes.h
231@@ -759,6 +759,7 @@ enum qk_keycode_defines {
232     QK_TRI_LAYER_UPPER = 0x7C78,
233     QK_REPEAT_KEY = 0x7C79,
234     QK_ALT_REPEAT_KEY = 0x7C7A,
235+    QK_LAYER_LOCK = 0x7C7B,
236     QK_KB_0 = 0x7E00,
237     QK_KB_1 = 0x7E01,
238     QK_KB_2 = 0x7E02,
239@@ -1445,6 +1446,7 @@ enum qk_keycode_defines {
240     TL_UPPR    = QK_TRI_LAYER_UPPER,
241     QK_REP     = QK_REPEAT_KEY,
242     QK_AREP    = QK_ALT_REPEAT_KEY,
243+    QK_LLCK    = QK_LAYER_LOCK,
244 };
245 
246 // Range Helpers
247@@ -1501,7 +1503,7 @@ enum qk_keycode_defines {
248 #define IS_UNDERGLOW_KEYCODE(code) ((code) >= QK_UNDERGLOW_TOGGLE && (code) <= QK_UNDERGLOW_SPEED_DOWN)
249 #define IS_RGB_KEYCODE(code) ((code) >= RGB_MODE_PLAIN && (code) <= RGB_MODE_TWINKLE)
250 #define IS_RGB_MATRIX_KEYCODE(code) ((code) >= QK_RGB_MATRIX_ON && (code) <= QK_RGB_MATRIX_SPEED_DOWN)
251-#define IS_QUANTUM_KEYCODE(code) ((code) >= QK_BOOTLOADER && (code) <= QK_ALT_REPEAT_KEY)
252+#define IS_QUANTUM_KEYCODE(code) ((code) >= QK_BOOTLOADER && (code) <= QK_LAYER_LOCK)
253 #define IS_KB_KEYCODE(code) ((code) >= QK_KB_0 && (code) <= QK_KB_31)
254 #define IS_USER_KEYCODE(code) ((code) >= QK_USER_0 && (code) <= QK_USER_31)
255 
256@@ -1527,6 +1529,6 @@ enum qk_keycode_defines {
257 #define UNDERGLOW_KEYCODE_RANGE             QK_UNDERGLOW_TOGGLE ... QK_UNDERGLOW_SPEED_DOWN
258 #define RGB_KEYCODE_RANGE                   RGB_MODE_PLAIN ... RGB_MODE_TWINKLE
259 #define RGB_MATRIX_KEYCODE_RANGE            QK_RGB_MATRIX_ON ... QK_RGB_MATRIX_SPEED_DOWN
260-#define QUANTUM_KEYCODE_RANGE               QK_BOOTLOADER ... QK_ALT_REPEAT_KEY
261+#define QUANTUM_KEYCODE_RANGE               QK_BOOTLOADER ... QK_LAYER_LOCK
262 #define KB_KEYCODE_RANGE                    QK_KB_0 ... QK_KB_31
263 #define USER_KEYCODE_RANGE                  QK_USER_0 ... QK_USER_31
264diff --git a/quantum/layer_lock.c b/quantum/layer_lock.c
265new file mode 100644
266index 0000000000000000000000000000000000000000..9ee3c307dcaaacefa2c52be327948bfa88564438
267--- /dev/null
268+++ b/quantum/layer_lock.c
269@@ -0,0 +1,81 @@
270+// Copyright 2022-2023 Google LLC
271+//
272+// Licensed under the Apache License, Version 2.0 (the "License");
273+// you may not use this file except in compliance with the License.
274+// You may obtain a copy of the License at
275+//
276+//     https://www.apache.org/licenses/LICENSE-2.0
277+//
278+// Unless required by applicable law or agreed to in writing, software
279+// distributed under the License is distributed on an "AS IS" BASIS,
280+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
281+// See the License for the specific language governing permissions and
282+// limitations under the License.
283+
284+#include "layer_lock.h"
285+#include "quantum_keycodes.h"
286+
287+#ifndef NO_ACTION_LAYER
288+// The current lock state. The kth bit is on if layer k is locked.
289+layer_state_t locked_layers = 0;
290+
291+// Layer Lock timer to disable layer lock after X seconds inactivity
292+#    if defined(LAYER_LOCK_IDLE_TIMEOUT) && LAYER_LOCK_IDLE_TIMEOUT > 0
293+uint32_t layer_lock_timer = 0;
294+
295+void layer_lock_task(void) {
296+    if (locked_layers && timer_elapsed32(layer_lock_timer) > LAYER_LOCK_IDLE_TIMEOUT) {
297+        layer_lock_all_off();
298+        layer_lock_timer = timer_read32();
299+    }
300+}
301+#    endif // LAYER_LOCK_IDLE_TIMEOUT > 0
302+
303+bool is_layer_locked(uint8_t layer) {
304+    return locked_layers & ((layer_state_t)1 << layer);
305+}
306+
307+void layer_lock_invert(uint8_t layer) {
308+    const layer_state_t mask = (layer_state_t)1 << layer;
309+    if ((locked_layers & mask) == 0) { // Layer is being locked.
310+#    ifndef NO_ACTION_ONESHOT
311+        if (layer == get_oneshot_layer()) {
312+            reset_oneshot_layer(); // Reset so that OSL doesn't turn layer off.
313+        }
314+#    endif // NO_ACTION_ONESHOT
315+        layer_on(layer);
316+#    if defined(LAYER_LOCK_IDLE_TIMEOUT) && LAYER_LOCK_IDLE_TIMEOUT > 0
317+        layer_lock_timer = timer_read32();
318+#    endif   // LAYER_LOCK_IDLE_TIMEOUT > 0
319+    } else { // Layer is being unlocked.
320+        layer_off(layer);
321+    }
322+    layer_lock_set_kb(locked_layers ^= mask);
323+}
324+
325+// Implement layer_lock_on/off by deferring to layer_lock_invert.
326+void layer_lock_on(uint8_t layer) {
327+    if (!is_layer_locked(layer)) {
328+        layer_lock_invert(layer);
329+    }
330+}
331+
332+void layer_lock_off(uint8_t layer) {
333+    if (is_layer_locked(layer)) {
334+        layer_lock_invert(layer);
335+    }
336+}
337+
338+void layer_lock_all_off(void) {
339+    layer_and(~locked_layers);
340+    locked_layers = 0;
341+    layer_lock_set_kb(locked_layers);
342+}
343+
344+__attribute__((weak)) bool layer_lock_set_kb(layer_state_t locked_layers) {
345+    return layer_lock_set_user(locked_layers);
346+}
347+__attribute__((weak)) bool layer_lock_set_user(layer_state_t locked_layers) {
348+    return true;
349+}
350+#endif // NO_ACTION_LAYER
351diff --git a/quantum/layer_lock.h b/quantum/layer_lock.h
352new file mode 100644
353index 0000000000000000000000000000000000000000..6d7285da2a610534bd2be82f3672622d25605b59
354--- /dev/null
355+++ b/quantum/layer_lock.h
356@@ -0,0 +1,135 @@
357+// Copyright 2022-2023 Google LLC
358+//
359+// Licensed under the Apache License, Version 2.0 (the "License");
360+// you may not use this file except in compliance with the License.
361+// You may obtain a copy of the License at
362+//
363+//     https://www.apache.org/licenses/LICENSE-2.0
364+//
365+// Unless required by applicable law or agreed to in writing, software
366+// distributed under the License is distributed on an "AS IS" BASIS,
367+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
368+// See the License for the specific language governing permissions and
369+// limitations under the License.
370+
371+/**
372+ * @file layer_lock.h
373+ * @brief Layer Lock, a key to stay in the current layer.
374+ *
375+ * Overview
376+ * --------
377+ *
378+ * Layers are often accessed by holding a button, e.g. with a momentary layer
379+ * switch `MO(layer)` or layer tap `LT(layer, key)` key. But you may sometimes
380+ * want to "lock" or "toggle" the layer so that it stays on without having to
381+ * hold down a button. One way to do that is with a tap-toggle `TT` layer key,
382+ * but here is an alternative.
383+ *
384+ * This library implements a "Layer Lock key". When tapped, it "locks" the
385+ * highest layer to stay active, assuming the layer was activated by one of the
386+ * following keys:
387+ *
388+ *  * `MO(layer)` momentary layer switch
389+ *  * `LT(layer, key)` layer tap
390+ *  * `OSL(layer)` one-shot layer
391+ *  * `TT(layer)` layer tap toggle
392+ *  * `LM(layer, mod)` layer-mod key (the layer is locked, but not the mods)
393+ *
394+ * Tapping the Layer Lock key again unlocks and turns off the layer.
395+ *
396+ * @note When a layer is "locked", other layer keys such as `TO(layer)` or
397+ * manually calling `layer_off(layer)` will override and unlock the layer.
398+ *
399+ * Configuration
400+ * -------------
401+ *
402+ * Optionally, a timeout may be defined so that Layer Lock disables
403+ * automatically if not keys are pressed for `LAYER_LOCK_IDLE_TIMEOUT`
404+ * milliseconds. Define `LAYER_LOCK_IDLE_TIMEOUT` in your config.h, for instance
405+ *
406+ *     #define LAYER_LOCK_IDLE_TIMEOUT 60000  // Turn off after 60 seconds.
407+ *
408+ * and call `layer_lock_task()` from your `matrix_scan_user()` in keymap.c:
409+ *
410+ *     void matrix_scan_user(void) {
411+ *       layer_lock_task();
412+ *       // Other tasks...
413+ *     }
414+ *
415+ * For full documentation, see
416+ * <https://getreuer.info/posts/keyboards/layer-lock>
417+ */
418+
419+#pragma once
420+
421+#include <stdint.h>
422+#include <stdbool.h>
423+#include "action_layer.h"
424+#include "action_util.h"
425+
426+/**
427+ * Handler function for Layer Lock.
428+ *
429+ * In your keymap, define a custom keycode to use for Layer Lock. Then handle
430+ * Layer Lock from your `process_record_user` function by calling
431+ * `process_layer_lock`, passing your custom keycode for the `lock_keycode` arg:
432+ *
433+ *     #include "features/layer_lock.h"
434+ *
435+ *     bool process_record_user(uint16_t keycode, keyrecord_t* record) {
436+ *       if (!process_layer_lock(keycode, record, LLOCK)) { return false; }
437+ *       // Your macros ...
438+ *
439+ *       return true;
440+ *     }
441+ */
442+
443+#ifndef NO_ACTION_LAYER
444+/** Returns true if `layer` is currently locked. */
445+bool is_layer_locked(uint8_t layer);
446+
447+/** Locks and turns on `layer`. */
448+void layer_lock_on(uint8_t layer);
449+
450+/** Unlocks and turns off `layer`. */
451+void layer_lock_off(uint8_t layer);
452+
453+/** Unlocks and turns off all locked layers. */
454+void layer_lock_all_off(void);
455+
456diff --git a/quantum/process_keycode/process_layer_lock.c b/quantum/process_keycode/process_layer_lock.c
457new file mode 100644
458index 0000000000000000000000000000000000000000..1e36d8844e83fa1eb9cd1f1859815b0ea245d5d4
459--- /dev/null
460+++ b/quantum/process_keycode/process_layer_lock.c
461@@ -0,0 +1,95 @@
462+// Copyright 2022-2023 Google LLC
463+//
464+// Licensed under the Apache License, Version 2.0 (the "License");
465+// you may not use this file except in compliance with the License.
466+// You may obtain a copy of the License at
467+//
468+//     https://www.apache.org/licenses/LICENSE-2.0
469+//
470+// Unless required by applicable law or agreed to in writing, software
471+// distributed under the License is distributed on an "AS IS" BASIS,
472+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
473+// See the License for the specific language governing permissions and
474+// limitations under the License.
475+
476+/**
477+ * @file layer_lock.c
478+ * @brief Layer Lock implementation
479+ *
480+ * For full documentation, see
481+ * <https://getreuer.info/posts/keyboards/layer-lock>
482+ */
483+
484+#include "layer_lock.h"
485+#include "process_layer_lock.h"
486+#include "quantum_keycodes.h"
487+#include "action_util.h"
488+
489+// The current lock state. The kth bit is on if layer k is locked.
490+extern layer_state_t locked_layers;
491+#if defined(LAYER_LOCK_IDLE_TIMEOUT) && LAYER_LOCK_IDLE_TIMEOUT > 0
492+extern uint32_t layer_lock_timer;
493+#endif
494+
495+// Handles an event on an `MO` or `TT` layer switch key.
496+static bool handle_mo_or_tt(uint8_t layer, keyrecord_t* record) {
497+    if (is_layer_locked(layer)) {
498+        if (record->event.pressed) { // On press, unlock the layer.
499+            layer_lock_invert(layer);
500+        }
501+        return false; // Skip default handling.
502+    }
503+    return true;
504+}
505+
506+bool process_layer_lock(uint16_t keycode, keyrecord_t* record) {
507+#ifndef NO_ACTION_LAYER
508+#    if defined(LAYER_LOCK_IDLE_TIMEOUT) && LAYER_LOCK_IDLE_TIMEOUT > 0
509+    layer_lock_timer = timer_read32();
510+#    endif // LAYER_LOCK_IDLE_TIMEOUT > 0
511+
512+    // The intention is that locked layers remain on. If something outside of
513+    // this feature turned any locked layers off, unlock them.
514+    if ((locked_layers & ~layer_state) != 0) {
515+        layer_lock_set_kb(locked_layers &= layer_state);
516+    }
517+
518+    if (keycode == QK_LAYER_LOCK) {
519+        if (record->event.pressed) { // The layer lock key was pressed.
520+            layer_lock_invert(get_highest_layer(layer_state));
521+        }
522+        return false;
523+    }
524+
525+    switch (keycode) {
526+        case QK_MOMENTARY ... QK_MOMENTARY_MAX: // `MO(layer)` keys.
527+            return handle_mo_or_tt(QK_MOMENTARY_GET_LAYER(keycode), record);
528+
529+        case QK_LAYER_TAP_TOGGLE ... QK_LAYER_TAP_TOGGLE_MAX: // `TT(layer)`.
530+            return handle_mo_or_tt(QK_LAYER_TAP_TOGGLE_GET_LAYER(keycode), record);
531+
532+        case QK_LAYER_MOD ... QK_LAYER_MOD_MAX: { // `LM(layer, mod)`.
533+            uint8_t layer = QK_LAYER_MOD_GET_LAYER(keycode);
534+            if (is_layer_locked(layer)) {
535+                if (record->event.pressed) { // On press, unlock the layer.
536+                    layer_lock_invert(layer);
537+                } else { // On release, clear the mods.
538+                    clear_mods();
539+                    send_keyboard_report();
540+                }
541+                return false; // Skip default handling.
542+            }
543+        } break;
544+
545+#    ifndef NO_ACTION_TAPPING
546+        case QK_LAYER_TAP ... QK_LAYER_TAP_MAX: // `LT(layer, key)` keys.
547+            if (record->tap.count == 0 && !record->event.pressed && is_layer_locked(QK_LAYER_TAP_GET_LAYER(keycode))) {
548+                // Release event on a held layer-tap key where the layer is locked.
549+                return false; // Skip default handling so that layer stays on.
550+            }
551+            break;
552+#    endif // NO_ACTION_TAPPING
553+    }
554+#endif // NO_ACTION_LAYER
555+    return true;
556+}
557diff --git a/quantum/process_keycode/process_layer_lock.h b/quantum/process_keycode/process_layer_lock.h
558new file mode 100644
559index 0000000000000000000000000000000000000000..b54c0f6f106284ecb7e82976f6889f63f7e0ebca
560--- /dev/null
561+++ b/quantum/process_keycode/process_layer_lock.h
562@@ -0,0 +1,69 @@
563+// Copyright 2022-2023 Google LLC
564+//
565+// Licensed under the Apache License, Version 2.0 (the "License");
566+// you may not use this file except in compliance with the License.
567+// You may obtain a copy of the License at
568+//
569+//     https://www.apache.org/licenses/LICENSE-2.0
570+//
571+// Unless required by applicable law or agreed to in writing, software
572+// distributed under the License is distributed on an "AS IS" BASIS,
573+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
574+// See the License for the specific language governing permissions and
575+// limitations under the License.
576+
577+/**
578+ * @file layer_lock.h
579+ * @brief Layer Lock, a key to stay in the current layer.
580+ *
581+ * Overview
582+ * --------
583+ *
584+ * Layers are often accessed by holding a button, e.g. with a momentary layer
585+ * switch `MO(layer)` or layer tap `LT(layer, key)` key. But you may sometimes
586+ * want to "lock" or "toggle" the layer so that it stays on without having to
587+ * hold down a button. One way to do that is with a tap-toggle `TT` layer key,
588+ * but here is an alternative.
589+ *
590+ * This library implements a "Layer Lock key". When tapped, it "locks" the
591+ * highest layer to stay active, assuming the layer was activated by one of the
592+ * following keys:
593+ *
594+ *  * `MO(layer)` momentary layer switch
595+ *  * `LT(layer, key)` layer tap
596+ *  * `OSL(layer)` one-shot layer
597+ *  * `TT(layer)` layer tap toggle
598+ *  * `LM(layer, mod)` layer-mod key (the layer is locked, but not the mods)
599+ *
600+ * Tapping the Layer Lock key again unlocks and turns off the layer.
601+ *
602+ * @note When a layer is "locked", other layer keys such as `TO(layer)` or
603+ * manually calling `layer_off(layer)` will override and unlock the layer.
604+ *
605+ * Configuration
606+ * -------------
607+ *
608+ * Optionally, a timeout may be defined so that Layer Lock disables
609+ * automatically if not keys are pressed for `LAYER_LOCK_IDLE_TIMEOUT`
610+ * milliseconds. Define `LAYER_LOCK_IDLE_TIMEOUT` in your config.h, for instance
611+ *
612+ *     #define LAYER_LOCK_IDLE_TIMEOUT 60000  // Turn off after 60 seconds.
613+ *
614+ * and call `layer_lock_task()` from your `matrix_scan_user()` in keymap.c:
615+ *
616+ *     void matrix_scan_user(void) {
617+ *       layer_lock_task();
618+ *       // Other tasks...
619+ *     }
620+ *
621+ * For full documentation, see
622+ * <https://getreuer.info/posts/keyboards/layer-lock>
623+ */
624+
625+#pragma once
626+
627+#include <stdint.h>
628+#include <stdbool.h>
629+#include "action.h"
630+
631+bool process_layer_lock(uint16_t keycode, keyrecord_t* record);
632diff --git a/quantum/quantum.c b/quantum/quantum.c
633index 874df1593a2b95bf2f7ca51fbaa0e92a90d24018..811ad2e71520d2419b61703354c9609a212019d8 100644
634--- a/quantum/quantum.c
635+++ b/quantum/quantum.c
636@@ -76,6 +76,10 @@
637 #    include "process_unicode_common.h"
638 #endif
639 
640+#ifdef LAYER_LOCK_ENABLE
641+#    include "process_layer_lock.h"
642+#endif // LAYER_LOCK_ENABLE
643+
644 #ifdef AUDIO_ENABLE
645 #    ifndef GOODBYE_SONG
646 #        define GOODBYE_SONG SONG(GOODBYE_SOUND)
647@@ -400,6 +404,9 @@ bool process_record_quantum(keyrecord_t *record) {
648 #ifdef TRI_LAYER_ENABLE
649             process_tri_layer(keycode, record) &&
650 #endif
651+#ifdef LAYER_LOCK_ENABLE
652+            process_layer_lock(keycode, record) &&
653+#endif
654 #ifdef BLUETOOTH_ENABLE
655             process_connection(keycode, record) &&
656 #endif
657diff --git a/quantum/quantum.h b/quantum/quantum.h
658index b60d8a86bf7aaa7e72f79ce2ffa2cd174d9f9c73..71cf900f2de0fe64a81376836738d32c99fd4677 100644
659--- a/quantum/quantum.h
660+++ b/quantum/quantum.h
661@@ -240,6 +240,10 @@ extern layer_state_t layer_state;
662 #    include "os_detection.h"
663 #endif
664 
665+#ifdef LAYER_LOCK_ENABLE
666+#    include "layer_lock.h"
667+#endif // LAYER_LOCK_ENABLE
668+
669 void set_single_default_layer(uint8_t default_layer);
670 void set_single_persistent_default_layer(uint8_t default_layer);
671 
672diff --git a/tests/layer_lock/config.h b/tests/layer_lock/config.h
673new file mode 100644
674index 0000000000000000000000000000000000000000..25d0b20c0ead4778825e9486ef7b55cd219c9a7e
675--- /dev/null
676+++ b/tests/layer_lock/config.h
677@@ -0,0 +1,8 @@
678+// Copyright 2021 Christopher Courtney, aka Drashna Jael're  (@drashna) <drashna@live.com>
679+// SPDX-License-Identifier: GPL-2.0-or-later
680+
681+#pragma once
682+
683+#include "test_common.h"
684+
685+#define LAYER_LOCK_IDLE_TIMEOUT 1000
686diff --git a/tests/layer_lock/test.mk b/tests/layer_lock/test.mk
687new file mode 100644
688index 0000000000000000000000000000000000000000..05771e4dbf89d57d5b0ba4488274868aeb87077c
689--- /dev/null
690+++ b/tests/layer_lock/test.mk
691@@ -0,0 +1,8 @@
692+# Copyright 2021 Christopher Courtney, aka Drashna Jael're  (@drashna) <drashna@live.com>
693+# SPDX-License-Identifier: GPL-2.0-or-later
694+
695+# --------------------------------------------------------------------------------
696+# Keep this file, even if it is empty, as a marker that this folder contains tests
697+# --------------------------------------------------------------------------------
698+
699+LAYER_LOCK_ENABLE = yes
700diff --git a/tests/layer_lock/test_layer_lock.cpp b/tests/layer_lock/test_layer_lock.cpp
701new file mode 100644
702index 0000000000000000000000000000000000000000..00742c3b43618e0fd9a71a260af613cacb98733c
703--- /dev/null
704+++ b/tests/layer_lock/test_layer_lock.cpp
705@@ -0,0 +1,284 @@
706+// Copyright 2021 Christopher Courtney, aka Drashna Jael're  (@drashna) <drashna@live.com>
707+// SPDX-License-Identifier: GPL-2.0-or-later
708+
709+#include "keycodes.h"
710+#include "test_common.hpp"
711+
712+using testing::_;
713+
714+class LayerLock : public TestFixture {};
715+
716+TEST_F(LayerLock, LayerLockState) {
717+    TestDriver driver;
718+    KeymapKey  key_a = KeymapKey(0, 0, 0, KC_A);
719+    KeymapKey  key_b = KeymapKey(1, 0, 0, KC_B);
720+    KeymapKey  key_c = KeymapKey(2, 0, 0, KC_C);
721+    KeymapKey  key_d = KeymapKey(3, 0, 0, KC_C);
722+
723+    set_keymap({key_a, key_b, key_c, key_d});
724+
725+    EXPECT_FALSE(is_layer_locked(1));
726+    EXPECT_FALSE(is_layer_locked(2));
727+    EXPECT_FALSE(is_layer_locked(3));
728+
729+    layer_lock_invert(1); // Layer 1: unlocked -> locked
730+    layer_lock_on(2);     // Layer 2: unlocked -> locked
731+    layer_lock_off(3);    // Layer 3: stays unlocked
732+
733+    // Layers 1 and 2 are now on.
734+    EXPECT_TRUE(layer_state_is(1));
735+    EXPECT_TRUE(layer_state_is(2));
736+    // Layers 1 and 2 are now locked.
737+    EXPECT_TRUE(is_layer_locked(1));
738+    EXPECT_TRUE(is_layer_locked(2));
739+    EXPECT_FALSE(is_layer_locked(3));
740+
741+    layer_lock_invert(1); // Layer 1: locked -> unlocked
742+    layer_lock_on(2);     // Layer 2: stays locked
743+    layer_lock_on(3);     // Layer 3: unlocked -> locked
744+
745+    EXPECT_FALSE(layer_state_is(1));
746+    EXPECT_TRUE(layer_state_is(2));
747+    EXPECT_TRUE(layer_state_is(3));
748+    EXPECT_FALSE(is_layer_locked(1));
749+    EXPECT_TRUE(is_layer_locked(2));
750+    EXPECT_TRUE(is_layer_locked(3));
751+
752+    layer_lock_invert(1); // Layer 1: unlocked -> locked
753+    layer_lock_off(2);    // Layer 2: locked -> unlocked
754+
755+    EXPECT_TRUE(layer_state_is(1));
756+    EXPECT_FALSE(layer_state_is(2));
757+    EXPECT_TRUE(layer_state_is(3));
758+    EXPECT_TRUE(is_layer_locked(1));
759+    EXPECT_FALSE(is_layer_locked(2));
760+    EXPECT_TRUE(is_layer_locked(3));
761+
762+    layer_lock_all_off(); // Layers 1 and 3: locked -> unlocked
763+
764+    EXPECT_FALSE(layer_state_is(1));
765+    EXPECT_FALSE(layer_state_is(2));
766+    EXPECT_FALSE(layer_state_is(3));
767+    EXPECT_FALSE(is_layer_locked(1));
768+    EXPECT_FALSE(is_layer_locked(2));
769+    EXPECT_FALSE(is_layer_locked(3));
770+}
771+
772+TEST_F(LayerLock, LayerLockMomentaryTest) {
773+    TestDriver driver;
774+    KeymapKey  key_layer = KeymapKey(0, 0, 0, MO(1));
775+    KeymapKey  key_a     = KeymapKey(0, 1, 0, KC_A);
776+    KeymapKey  key_trns  = KeymapKey(1, 0, 0, KC_TRNS);
777+    KeymapKey  key_ll    = KeymapKey(1, 1, 0, QK_LAYER_LOCK);
778+
779+    set_keymap({key_layer, key_a, key_trns, key_ll});
780+
781+    EXPECT_NO_REPORT(driver);
782+    key_layer.press();
783+    run_one_scan_loop();
784+    EXPECT_TRUE(layer_state_is(1));
785+    EXPECT_FALSE(is_layer_locked(1));
786+    VERIFY_AND_CLEAR(driver);
787+
788+    EXPECT_NO_REPORT(driver);
789+    tap_key(key_ll);
790+    EXPECT_TRUE(layer_state_is(1));
791+    EXPECT_TRUE(is_layer_locked(1));
792+    VERIFY_AND_CLEAR(driver);
793+
794+    EXPECT_NO_REPORT(driver);
795+    key_layer.release();
796+    run_one_scan_loop();
797+    EXPECT_TRUE(layer_state_is(1));
798+    EXPECT_TRUE(is_layer_locked(1));
799+    VERIFY_AND_CLEAR(driver);
800+
801+    // Pressing Layer Lock again unlocks the lock.
802+    EXPECT_NO_REPORT(driver);
803+    key_ll.press();
804+    run_one_scan_loop();
805diff --git a/tests/test_common/keycode_table.cpp b/tests/test_common/keycode_table.cpp
806index a520dd3f2bf06fc7543fdce81bf8aeb54cedbc39..ae1a9edcd0fc8c0d165e3991db4ee09aad5965bb 100644
807--- a/tests/test_common/keycode_table.cpp
808+++ b/tests/test_common/keycode_table.cpp
809@@ -699,6 +699,7 @@ std::map<uint16_t, std::string> KEYCODE_ID_TABLE = {
810     {QK_TRI_LAYER_UPPER, "QK_TRI_LAYER_UPPER"},
811     {QK_REPEAT_KEY, "QK_REPEAT_KEY"},
812     {QK_ALT_REPEAT_KEY, "QK_ALT_REPEAT_KEY"},
813+    {QK_LAYER_LOCK, "QK_LAYER_LOCK"},
814     {QK_KB_0, "QK_KB_0"},
815     {QK_KB_1, "QK_KB_1"},
816     {QK_KB_2, "QK_KB_2"},