70e34e491c297231a3f987fd69760d38e79dbfa4

Author
Ryan <fauxpark@gmail.com>
Committer
GitHub <noreply@github.com>
Date

Message

Unicode, Unicodemap and UCIS refactor (#21659)

Diff

This diff is truncated to protect this page.

   1diff --git a/Makefile b/Makefile
   2index c77eadf97d5d683cbcfd2cb0bd1fec0530f12aba..9f2e4636a4b478cbe6752b9d9e749e2915abf766 100644
   3--- a/Makefile
   4+++ b/Makefile
   5@@ -328,7 +328,7 @@ define PARSE_TEST
   6     ifeq ($$(TEST_NAME),all)
   7         MATCHED_TESTS := $$(TEST_LIST)
   8     else
   9-        MATCHED_TESTS := $$(foreach TEST, $$(TEST_LIST),$$(if $$(findstring $$(TEST_NAME), $$(notdir $$(TEST))), $$(TEST),))
  10+        MATCHED_TESTS := $$(foreach TEST, $$(TEST_LIST),$$(if $$(findstring x$$(TEST_NAME)x, x$$(notdir $$(TEST))x), $$(TEST),))
  11     endif
  12     $$(foreach TEST,$$(MATCHED_TESTS),$$(eval $$(call BUILD_TEST,$$(TEST),$$(TEST_TARGET))))
  13 endef
  14diff --git a/builddefs/common_features.mk b/builddefs/common_features.mk
  15index 60fcdc6b93d8eca2c05fb7d1145846cdf803aaed..094eda6fef642a7ad0405a20a8137bf5dd4a1ddf 100644
  16--- a/builddefs/common_features.mk
  17+++ b/builddefs/common_features.mk
  18@@ -784,13 +784,15 @@ endif
  19 ifeq ($(strip $(UCIS_ENABLE)), yes)
  20     OPT_DEFS += -DUCIS_ENABLE
  21     UNICODE_COMMON := yes
  22-    SRC += $(QUANTUM_DIR)/process_keycode/process_ucis.c
  23+    SRC += $(QUANTUM_DIR)/process_keycode/process_ucis.c \
  24+           $(QUANTUM_DIR)/unicode/ucis.c
  25 endif
  26 
  27 ifeq ($(strip $(UNICODEMAP_ENABLE)), yes)
  28     OPT_DEFS += -DUNICODEMAP_ENABLE
  29     UNICODE_COMMON := yes
  30-    SRC += $(QUANTUM_DIR)/process_keycode/process_unicodemap.c
  31+    SRC += $(QUANTUM_DIR)/process_keycode/process_unicodemap.c \
  32+           $(QUANTUM_DIR)/unicode/unicodemap.c
  33 endif
  34 
  35 ifeq ($(strip $(UNICODE_ENABLE)), yes)
  36diff --git a/docs/feature_unicode.md b/docs/feature_unicode.md
  37index 312a0332570e5901b9e30bae356215a241a12596..341084f9260a7f9196289b86371f27c3006fd033 100644
  38--- a/docs/feature_unicode.md
  39+++ b/docs/feature_unicode.md
  40@@ -1,48 +1,77 @@
  41-# Unicode Support
  42+# Unicode :id=unicode
  43 
  44-Unicode characters can be input straight from your keyboard! There are some limitations, however.
  45+With a little help from your OS, practically any Unicode character can be input using your keyboard.
  46 
  47-In order to enable Unicode support on your keyboard, you will need to do the following:
  48+## Caveats :id=caveats
  49 
  50-1. Choose one of three supported Unicode implementations: [Basic Unicode](#basic-unicode), [Unicode Map](#unicode-map), [UCIS](#ucis).
  51-2. Find which [input mode](#input-modes) is the best match for your operating system and setup.
  52-3. [Set](#setting-the-input-mode) the appropriate input mode (or modes) in your configuration.
  53-4. Add Unicode keycodes to your keymap.
  54diff --git a/keyboards/spaceman/2_milk/keymaps/emoji/keymap.c b/keyboards/spaceman/2_milk/keymaps/emoji/keymap.c
  55index 8859573ce5d9fe926cdf53295ae9ebe9e68881f5..26ac20ad5cefa71deee544cfcefb7628f78b2750 100644
  56--- a/keyboards/spaceman/2_milk/keymaps/emoji/keymap.c
  57+++ b/keyboards/spaceman/2_milk/keymaps/emoji/keymap.c
  58@@ -11,7 +11,7 @@ void dance_key_one (tap_dance_state_t *state, void *user_data) {
  59         tap_code(KC_ENTER);
  60         reset_tap_dance (state);
  61     } else if (state->count == 2) {
  62-        cycle_unicode_input_mode(+1);
  63+        unicode_input_mode_step();
  64         reset_tap_dance (state);
  65     }
  66 }
  67diff --git a/quantum/process_keycode/process_ucis.c b/quantum/process_keycode/process_ucis.c
  68index 3aa09d5948f2196aa2a479dae24693b11755d99b..91a038bab1cdf5c36a5abde563161599ed4872df 100644
  69--- a/quantum/process_keycode/process_ucis.c
  70+++ b/quantum/process_keycode/process_ucis.c
  71@@ -15,110 +15,30 @@
  72  */
  73 
  74 #include "process_ucis.h"
  75-#include "unicode.h"
  76-#include "keycode.h"
  77-#include "wait.h"
  78+#include "ucis.h"
  79+#include "keycodes.h"
  80 
  81-ucis_state_t ucis_state;
  82-
  83-void ucis_start(void) {
  84-    ucis_state.count       = 0;
  85-    ucis_state.in_progress = true;
  86-
  87-    ucis_start_user();
  88-}
  89-
  90-__attribute__((weak)) void ucis_start_user(void) {
  91-    register_unicode(0x2328); // ⌨
  92-}
  93-
  94-__attribute__((weak)) void ucis_success(uint8_t symbol_index) {}
  95-
  96-static bool is_uni_seq(char *seq) {
  97-    uint8_t i;
  98-    for (i = 0; seq[i]; i++) {
  99-        uint16_t keycode;
 100-        if ('1' <= seq[i] && seq[i] <= '0') {
 101-            keycode = seq[i] - '1' + KC_1;
 102-        } else {
 103-            keycode = seq[i] - 'a' + KC_A;
 104-        }
 105-        if (i > ucis_state.count || ucis_state.codes[i] != keycode) {
 106+bool process_ucis(uint16_t keycode, keyrecord_t *record) {
 107+    if (ucis_active() && record->event.pressed) {
 108+        bool special = keycode == KC_SPACE || keycode == KC_ENTER || keycode == KC_ESCAPE || keycode == KC_BACKSPACE;
 109+        if (ucis_count() >= UCIS_MAX_INPUT_LENGTH && !special) {
 110             return false;
 111         }
 112-    }
 113-    return ucis_state.codes[i] == KC_ENTER || ucis_state.codes[i] == KC_SPACE;
 114-}
 115-
 116-__attribute__((weak)) void ucis_symbol_fallback(void) {
 117-    for (uint8_t i = 0; i < ucis_state.count - 1; i++) {
 118-        tap_code(ucis_state.codes[i]);
 119-    }
 120-}
 121-
 122-__attribute__((weak)) void ucis_cancel(void) {}
 123-
 124-void register_ucis(const uint32_t *code_points) {
 125-    for (int i = 0; i < UCIS_MAX_CODE_POINTS && code_points[i]; i++) {
 126-        register_unicode(code_points[i]);
 127-    }
 128-}
 129 
 130-bool process_ucis(uint16_t keycode, keyrecord_t *record) {
 131-    if (!ucis_state.in_progress || !record->event.pressed) {
 132-        return true;
 133-    }
 134-
 135-    bool special = keycode == KC_SPACE || keycode == KC_ENTER || keycode == KC_ESCAPE || keycode == KC_BACKSPACE;
 136-    if (ucis_state.count >= UCIS_MAX_SYMBOL_LENGTH && !special) {
 137-        return false;
 138-    }
 139-
 140-    ucis_state.codes[ucis_state.count] = keycode;
 141-    ucis_state.count++;
 142-
 143-    switch (keycode) {
 144-        case KC_BACKSPACE:
 145-            if (ucis_state.count >= 2) {
 146-                ucis_state.count -= 2;
 147-                return true;
 148-            } else {
 149-                ucis_state.count--;
 150-                return false;
 151-            }
 152-
 153-        case KC_SPACE:
 154-        case KC_ENTER:
 155-        case KC_ESCAPE:
 156-            for (uint8_t i = 0; i < ucis_state.count; i++) {
 157-                tap_code(KC_BACKSPACE);
 158-            }
 159-
 160-            if (keycode == KC_ESCAPE) {
 161-                ucis_state.in_progress = false;
 162-                ucis_cancel();
 163-                return false;
 164-            }
 165-
 166-            uint8_t i;
 167-            bool    symbol_found = false;
 168-            for (i = 0; ucis_symbol_table[i].symbol; i++) {
 169-                if (is_uni_seq(ucis_symbol_table[i].symbol)) {
 170-                    symbol_found = true;
 171diff --git a/quantum/process_keycode/process_ucis.h b/quantum/process_keycode/process_ucis.h
 172index 54eb9413d41bac7ccb5634ce5957294a90cbac02..6282df78932d8cc12908f3d5329d4ab0d46e5689 100644
 173--- a/quantum/process_keycode/process_ucis.h
 174+++ b/quantum/process_keycode/process_ucis.h
 175@@ -18,48 +18,6 @@
 176 
 177 #include <stdbool.h>
 178 #include <stdint.h>
 179-
 180 #include "action.h"
 181 
 182-#ifndef UCIS_MAX_SYMBOL_LENGTH
 183-#    define UCIS_MAX_SYMBOL_LENGTH 32
 184-#endif
 185-#ifndef UCIS_MAX_CODE_POINTS
 186-#    define UCIS_MAX_CODE_POINTS 3
 187-#endif
 188-
 189-typedef struct {
 190-    char *   symbol;
 191-    uint32_t code_points[UCIS_MAX_CODE_POINTS];
 192-} ucis_symbol_t;
 193-
 194-typedef struct {
 195-    uint8_t  count;
 196-    uint16_t codes[UCIS_MAX_SYMBOL_LENGTH];
 197-    bool     in_progress : 1;
 198-} ucis_state_t;
 199-
 200-extern ucis_state_t ucis_state;
 201-
 202-// clang-format off
 203-
 204-#define UCIS_TABLE(...) \
 205-    {                   \
 206-        __VA_ARGS__,    \
 207-        { NULL, {} }    \
 208-    }
 209-#define UCIS_SYM(name, ...) \
 210-    { name, {__VA_ARGS__} }
 211-
 212-// clang-format on
 213-
 214-extern const ucis_symbol_t ucis_symbol_table[];
 215-
 216-void ucis_start(void);
 217-void ucis_start_user(void);
 218-void ucis_symbol_fallback(void);
 219-void ucis_success(uint8_t symbol_index);
 220-
 221-void register_ucis(const uint32_t *code_points);
 222-
 223 bool process_ucis(uint16_t keycode, keyrecord_t *record);
 224diff --git a/quantum/process_keycode/process_unicode.c b/quantum/process_keycode/process_unicode.c
 225index 1ec76245a342aab5e4dddc28de580f11840bb2c5..8ee6fcd7fca70e7d18ec19133097bdbb9e9ab760 100644
 226--- a/quantum/process_keycode/process_unicode.c
 227+++ b/quantum/process_keycode/process_unicode.c
 228@@ -16,6 +16,7 @@
 229 
 230 #include "process_unicode.h"
 231 #include "unicode.h"
 232+#include "keycodes.h"
 233 #include "quantum_keycodes.h"
 234 
 235 bool process_unicode(uint16_t keycode, keyrecord_t *record) {
 236diff --git a/quantum/process_keycode/process_unicode.h b/quantum/process_keycode/process_unicode.h
 237index 341bc8d861dcd5968f1d5d2b51f60521365096ab..9a51ffaf7c9c44428c0e510b5ba38e1c6e05e3dc 100644
 238--- a/quantum/process_keycode/process_unicode.h
 239+++ b/quantum/process_keycode/process_unicode.h
 240@@ -18,7 +18,6 @@
 241 
 242 #include <stdbool.h>
 243 #include <stdint.h>
 244-
 245 #include "action.h"
 246 
 247 bool process_unicode(uint16_t keycode, keyrecord_t *record);
 248diff --git a/quantum/process_keycode/process_unicode_common.c b/quantum/process_keycode/process_unicode_common.c
 249index a0b9010027637856fecf6a20792b88dba264052e..f43770977aa2836dcdba810258e9e76b8614dbd4 100644
 250--- a/quantum/process_keycode/process_unicode_common.c
 251+++ b/quantum/process_keycode/process_unicode_common.c
 252@@ -17,7 +17,8 @@
 253 #include "process_unicode_common.h"
 254 #include "unicode.h"
 255 #include "action_util.h"
 256-#include "keycode.h"
 257+#include "keycodes.h"
 258+#include "modifiers.h"
 259 
 260 #if defined(UNICODE_ENABLE)
 261 #    include "process_unicode.h"
 262@@ -32,10 +33,18 @@ bool process_unicode_common(uint16_t keycode, keyrecord_t *record) {
 263         bool shifted = get_mods() & MOD_MASK_SHIFT;
 264         switch (keycode) {
 265             case QK_UNICODE_MODE_NEXT:
 266-                cycle_unicode_input_mode(shifted ? -1 : +1);
 267+                if (shifted) {
 268+                    unicode_input_mode_step_reverse();
 269+                } else {
 270+                    unicode_input_mode_step();
 271+                }
 272                 break;
 273             case QK_UNICODE_MODE_PREVIOUS:
 274-                cycle_unicode_input_mode(shifted ? +1 : -1);
 275+                if (shifted) {
 276+                    unicode_input_mode_step();
 277+                } else {
 278+                    unicode_input_mode_step_reverse();
 279+                }
 280                 break;
 281             case QK_UNICODE_MODE_MACOS:
 282                 set_unicode_input_mode(UNICODE_MODE_MACOS);
 283diff --git a/quantum/process_keycode/process_unicode_common.h b/quantum/process_keycode/process_unicode_common.h
 284index fd09a41818f93a895af2ce77798feed9f12d0b78..046583007930122ca4b929ee43abb260abc393e5 100644
 285--- a/quantum/process_keycode/process_unicode_common.h
 286+++ b/quantum/process_keycode/process_unicode_common.h
 287@@ -18,7 +18,6 @@
 288 
 289 #include <stdbool.h>
 290 #include <stdint.h>
 291-
 292 #include "action.h"
 293 
 294 bool process_unicode_common(uint16_t keycode, keyrecord_t *record);
 295diff --git a/quantum/process_keycode/process_unicodemap.c b/quantum/process_keycode/process_unicodemap.c
 296index 195c093e6e5d2cbec923f550af6bd359c57f3131..a85568521c5d5ecaeea0e82c93c9bab9d7ea3e15 100644
 297--- a/quantum/process_keycode/process_unicodemap.c
 298+++ b/quantum/process_keycode/process_unicodemap.c
 299@@ -15,41 +15,12 @@
 300  */
 301 
 302 #include "process_unicodemap.h"
 303-#include "unicode.h"
 304-#include "quantum_keycodes.h"
 305-#include "keycode.h"
 306-#include "action_util.h"
 307-#include "host.h"
 308-
 309-__attribute__((weak)) uint16_t unicodemap_index(uint16_t keycode) {
 310-    if (keycode >= QK_UNICODEMAP_PAIR) {
 311-        // Keycode is a pair: extract index based on Shift / Caps Lock state
 312-        uint16_t index;
 313-
 314-        uint8_t mods = get_mods() | get_weak_mods();
 315-#ifndef NO_ACTION_ONESHOT
 316-        mods |= get_oneshot_mods();
 317-#endif
 318-
 319-        bool shift = mods & MOD_MASK_SHIFT;
 320-        bool caps  = host_keyboard_led_state().caps_lock;
 321-        if (shift ^ caps) {
 322-            index = QK_UNICODEMAP_PAIR_GET_SHIFTED_INDEX(keycode);
 323-        } else {
 324-            index = QK_UNICODEMAP_PAIR_GET_UNSHIFTED_INDEX(keycode);
 325-        }
 326-
 327-        return index;
 328-    } else {
 329-        // Keycode is a regular index
 330-        return QK_UNICODEMAP_GET_INDEX(keycode);
 331-    }
 332-}
 333+#include "unicodemap.h"
 334+#include "keycodes.h"
 335 
 336 bool process_unicodemap(uint16_t keycode, keyrecord_t *record) {
 337     if (keycode >= QK_UNICODEMAP && keycode <= QK_UNICODEMAP_PAIR_MAX && record->event.pressed) {
 338-        uint32_t code_point = pgm_read_dword(unicode_map + unicodemap_index(keycode));
 339-        register_unicode(code_point);
 340+        register_unicodemap(unicodemap_index(keycode));
 341     }
 342     return true;
 343 }
 344diff --git a/quantum/process_keycode/process_unicodemap.h b/quantum/process_keycode/process_unicodemap.h
 345index 5a3aeb000062b72e1699bea7aaed9f5bc7798dad..f07082e9efa952cdadadc39f2104090e0edbfd85 100644
 346--- a/quantum/process_keycode/process_unicodemap.h
 347+++ b/quantum/process_keycode/process_unicodemap.h
 348@@ -18,11 +18,6 @@
 349 
 350 #include <stdbool.h>
 351 #include <stdint.h>
 352-
 353 #include "action.h"
 354-#include "progmem.h"
 355-
 356-extern const uint32_t unicode_map[] PROGMEM;
 357 
 358-uint16_t unicodemap_index(uint16_t keycode);
 359-bool     process_unicodemap(uint16_t keycode, keyrecord_t *record);
 360+bool process_unicodemap(uint16_t keycode, keyrecord_t *record);
 361diff --git a/quantum/quantum.c b/quantum/quantum.c
 362index 3d440e0de8981c1298f0d74eb582a9e7a13ce1f2..3323a5adb62ef515e656b20a864cdd074ea91a85 100644
 363--- a/quantum/quantum.c
 364+++ b/quantum/quantum.c
 365@@ -68,10 +68,6 @@
 366 #    include "process_unicode_common.h"
 367 #endif
 368 
 369-#ifdef UNICODE_ENABLE
 370-#    include "process_unicode.h"
 371-#endif
 372-
 373 #ifdef VELOCIKEY_ENABLE
 374 #    include "velocikey.h"
 375 #endif
 376diff --git a/quantum/quantum.h b/quantum/quantum.h
 377index 017844c558dacd50e28127bd39f9583795a52edb..4d183e755f28fdaf70ef4939099da38176703a71 100644
 378--- a/quantum/quantum.h
 379+++ b/quantum/quantum.h
 380@@ -97,16 +97,16 @@ extern layer_state_t layer_state;
 381 #    include "leader.h"
 382 #endif
 383 
 384-#ifdef UCIS_ENABLE
 385-#    include "process_ucis.h"
 386+#ifdef UNICODE_COMMON_ENABLE
 387+#    include "unicode.h"
 388 #endif
 389 
 390-#ifdef UNICODEMAP_ENABLE
 391-#    include "process_unicodemap.h"
 392+#ifdef UCIS_ENABLE
 393+#    include "ucis.h"
 394 #endif
 395 
 396-#ifdef UNICODE_COMMON_ENABLE
 397-#    include "unicode.h"
 398+#ifdef UNICODEMAP_ENABLE
 399+#    include "unicodemap.h"
 400 #endif
 401 
 402 #ifdef KEY_OVERRIDE_ENABLE
 403diff --git a/quantum/quantum_keycodes_legacy.h b/quantum/quantum_keycodes_legacy.h
 404index ad078cdad5ce8eb5517c736a118b57100bb10b8e..260ac1c8a443ffcf75fb36a56a2651d3624300ff 100644
 405--- a/quantum/quantum_keycodes_legacy.h
 406+++ b/quantum/quantum_keycodes_legacy.h
 407@@ -55,4 +55,4 @@
 408 #define GUI_TOG QK_MAGIC_TOGGLE_GUI
 409 
 410 #define X(i) UM(i)
 411-#define XP(i, j) UM(i, j)
 412+#define XP(i, j) UP(i, j)
 413diff --git a/quantum/unicode/ucis.c b/quantum/unicode/ucis.c
 414new file mode 100644
 415index 0000000000000000000000000000000000000000..32be78520654d8f8b0e58f68dea75ca86917f186
 416--- /dev/null
 417+++ b/quantum/unicode/ucis.c
 418@@ -0,0 +1,96 @@
 419+// Copyright 2023 QMK
 420+// SPDX-License-Identifier: GPL-2.0-or-later
 421+
 422+#include "ucis.h"
 423+#include "unicode.h"
 424+#include "action.h"
 425+
 426+uint8_t count                        = 0;
 427+bool    active                       = false;
 428+char    input[UCIS_MAX_INPUT_LENGTH] = {0};
 429+
 430+void ucis_start(void) {
 431+    count  = 0;
 432+    active = true;
 433+
 434+    register_unicode(0x2328); // ⌨
 435+}
 436+
 437+bool ucis_active(void) {
 438+    return active;
 439+}
 440+
 441+uint8_t ucis_count(void) {
 442+    return count;
 443+}
 444+
 445+static char keycode_to_char(uint16_t keycode) {
 446+    if (keycode >= KC_A && keycode <= KC_Z) {
 447+        return 'a' + (keycode - KC_A);
 448+    } else if (keycode >= KC_1 && keycode <= KC_9) {
 449+        return '1' + (keycode - KC_1);
 450+    } else if (keycode == KC_0) {
 451+        return '0';
 452+    }
 453+    return 0;
 454+}
 455+
 456+bool ucis_add(uint16_t keycode) {
 457+    char c = keycode_to_char(keycode);
 458+    if (c) {
 459+        input[count++] = c;
 460+        return true;
 461+    }
 462+    return false;
 463+}
 464+
 465+bool ucis_remove_last(void) {
 466+    if (count) {
 467+        count--;
 468+        return true;
 469+    }
 470+
 471+    return false;
 472+}
 473+
 474+static bool match_mnemonic(char *mnemonic) {
 475+    for (uint8_t i = 0; input[i]; i++) {
 476+        if (i > count || input[i] != mnemonic[i]) {
 477+            return false;
 478+        }
 479+    }
 480+    return true;
 481+}
 482+
 483+void ucis_finish(void) {
 484+    uint8_t i     = 0;
 485+    bool    found = false;
 486+    for (; ucis_symbol_table[i].mnemonic; i++) {
 487+        if (match_mnemonic(ucis_symbol_table[i].mnemonic)) {
 488+            found = true;
 489+            break;
 490+        }
 491+    }
 492+
 493+    if (found) {
 494+        for (uint8_t j = 0; j <= count; j++) {
 495+            tap_code(KC_BACKSPACE);
 496+        }
 497+        register_ucis(i);
 498+    }
 499+
 500+    active = false;
 501+}
 502+
 503+void ucis_cancel(void) {
 504+    count  = 0;
 505+    active = false;
 506+}
 507+
 508+void register_ucis(uint8_t index) {
 509+    const uint32_t *code_points = ucis_symbol_table[index].code_points;
 510+
 511+    for (int i = 0; i < UCIS_MAX_CODE_POINTS && code_points[i]; i++) {
 512+        register_unicode(code_points[i]);
 513+    }
 514+}
 515diff --git a/quantum/unicode/ucis.h b/quantum/unicode/ucis.h
 516new file mode 100644
 517index 0000000000000000000000000000000000000000..5a4fa267841cf53c992c32888a4028f7cf67b36a
 518--- /dev/null
 519+++ b/quantum/unicode/ucis.h
 520@@ -0,0 +1,97 @@
 521+// Copyright 2023 QMK
 522+// SPDX-License-Identifier: GPL-2.0-or-later
 523+
 524+#pragma once
 525+
 526+#include <stdbool.h>
 527+#include <stdint.h>
 528+
 529+/**
 530+ * \file
 531+ *
 532+ * \defgroup ucis UCIS
 533+ * \{
 534+ */
 535+
 536+#ifndef UCIS_MAX_INPUT_LENGTH
 537+#    define UCIS_MAX_INPUT_LENGTH 32
 538+#endif
 539+
 540+#ifndef UCIS_MAX_CODE_POINTS
 541+#    define UCIS_MAX_CODE_POINTS 3
 542+#endif
 543+
 544+typedef struct {
 545+    char*    mnemonic;
 546+    uint32_t code_points[UCIS_MAX_CODE_POINTS];
 547+} ucis_symbol_t;
 548+
 549+// clang-format off
 550+
 551+#define UCIS_TABLE(...) { \
 552+    __VA_ARGS__,          \
 553+    { NULL, {} }          \
 554+}
 555+
 556+#define UCIS_SYM(name, ...) {    \
 557+    .mnemonic    = name,         \
 558+    .code_points = {__VA_ARGS__} \
 559+}
 560+
 561+// clang-format on
 562+
 563+extern const ucis_symbol_t ucis_symbol_table[];
 564+
 565+/**
 566+ * \brief Begin the input sequence.
 567+ */
 568+void ucis_start(void);
 569+
 570+/**
 571+ * \brief Whether UCIS is currently active.
 572+ *
 573+ * \return `true` if UCIS is active.
 574+ */
 575+bool ucis_active(void);
 576+
 577+/**
 578+ * \brief Get the number of characters in the input sequence buffer.
 579+ *
 580+ * \return The current input sequence buffer length.
 581+ */
 582+uint8_t ucis_count(void);
 583+
 584+/**
 585+ * \brief Add the given keycode to the input sequence buffer.
 586+ *
 587+ * \param keycode The keycode to add. Must be between `KC_A` and `KC_Z`, or `KC_1` and `KC_0`.
 588+ *
 589+ * \return `true` if the keycode was added.
 590+ */
 591+bool ucis_add(uint16_t keycode);
 592+
 593+/**
 594+ * \brief Remove the last character from the input sequence.
 595+ *
 596+ * \return `true` if the sequence was not empty.
 597+ */
 598+bool ucis_remove_last(void);
 599+
 600+/**
 601+ * Mark the input sequence as complete, and attempt to match.
 602+ */
 603+void ucis_finish(void);
 604+
 605+/**
 606+ * \brief Cancel the input sequence.
 607+ */
 608+void ucis_cancel(void);
 609+
 610+/**
 611+ * Send the code point(s) for the given UCIS index.
 612+ *
 613+ * \param index The index into the UCIS symbol table.
 614+ */
 615+void register_ucis(uint8_t index);
 616+
 617+/** \} */
 618diff --git a/quantum/unicode/unicode.c b/quantum/unicode/unicode.c
 619index f92ba51984d0fdcedf0cb8f16b11dee9cf5a4af6..78a4cad585949cc63644bb0a295162383c1c5ce5 100644
 620--- a/quantum/unicode/unicode.c
 621+++ b/quantum/unicode/unicode.c
 622@@ -73,16 +73,8 @@ static int8_t  selected_count = ARRAY_SIZE(selected);
 623 static int8_t  selected_index;
 624 #endif
 625 
 626-/** \brief unicode input mode set at user level
 627- *
 628- * Run user code on unicode input mode change
 629- */
 630 __attribute__((weak)) void unicode_input_mode_set_user(uint8_t input_mode) {}
 631 
 632-/** \brief unicode input mode set at keyboard level
 633- *
 634- *  Run keyboard code on unicode input mode change
 635- */
 636 __attribute__((weak)) void unicode_input_mode_set_kb(uint8_t input_mode) {
 637     unicode_input_mode_set_user(input_mode);
 638 }
 639@@ -172,6 +164,10 @@ uint8_t get_unicode_input_mode(void) {
 640     return unicode_config.input_mode;
 641 }
 642 
 643+static void persist_unicode_input_mode(void) {
 644+    eeprom_update_byte(EECONFIG_UNICODEMODE, unicode_config.input_mode);
 645+}
 646+
 647 void set_unicode_input_mode(uint8_t mode) {
 648     unicode_config.input_mode = mode;
 649     persist_unicode_input_mode();
 650@@ -182,26 +178,34 @@ void set_unicode_input_mode(uint8_t mode) {
 651     dprintf("Unicode input mode set to: %u\n", unicode_config.input_mode);
 652 }
 653 
 654-void cycle_unicode_input_mode(int8_t offset) {
 655+static void cycle_unicode_input_mode(int8_t offset) {
 656 #if UNICODE_SELECTED_MODES != -1
 657     selected_index = (selected_index + offset) % selected_count;
 658     if (selected_index < 0) {
 659         selected_index += selected_count;
 660     }
 661+
 662     unicode_config.input_mode = selected[selected_index];
 663+
 664 #    if UNICODE_CYCLE_PERSIST
 665     persist_unicode_input_mode();
 666 #    endif
 667+
 668 #    ifdef AUDIO_ENABLE
 669     unicode_play_song(unicode_config.input_mode);
 670 #    endif
 671+
 672     unicode_input_mode_set_kb(unicode_config.input_mode);
 673     dprintf("Unicode input mode cycle to: %u\n", unicode_config.input_mode);
 674 #endif
 675 }
 676 
 677-void persist_unicode_input_mode(void) {
 678-    eeprom_update_byte(EECONFIG_UNICODEMODE, unicode_config.input_mode);
 679+void unicode_input_mode_step(void) {
 680+    cycle_unicode_input_mode(1);
 681+}
 682+
 683+void unicode_input_mode_step_reverse(void) {
 684+    cycle_unicode_input_mode(-1);
 685 }
 686 
 687 __attribute__((weak)) void unicode_input_start(void) {
 688diff --git a/quantum/unicode/unicode.h b/quantum/unicode/unicode.h
 689index fc0e9d3d2b7cee226837633efa6dc318b1304c58..90a54c8b18b5587b9a83465d41a4be84a56c2ab4 100644
 690--- a/quantum/unicode/unicode.h
 691+++ b/quantum/unicode/unicode.h
 692@@ -19,6 +19,13 @@
 693 #include <stdint.h>
 694 #include "unicode_keycodes.h"
 695 
 696+/**
 697+ * \file
 698+ *
 699+ * \defgroup unicode Unicode
 700+ * \{
 701+ */
 702+
 703 typedef union {
 704     uint8_t raw;
 705     struct {
 706@@ -41,21 +48,87 @@ enum unicode_input_modes {
 707     UNICODE_MODE_COUNT // Number of available input modes (always leave at the end)
 708 };
 709 
 710-void    unicode_input_mode_init(void);
 711+void unicode_input_mode_init(void);
 712+
 713+/**
 714+ * \brief Get the current Unicode input mode.
 715+ *
 716+ * \return The currently active Unicode input mode.
 717+ */
 718 uint8_t get_unicode_input_mode(void);
 719-void    set_unicode_input_mode(uint8_t mode);
 720-void    cycle_unicode_input_mode(int8_t offset);
 721-void    persist_unicode_input_mode(void);
 722 
 723+/**
 724+ * \brief Set the Unicode input mode.
 725+ *
 726+ * \param mode The input mode to set.
 727+ */
 728+void set_unicode_input_mode(uint8_t mode);
 729+
 730+/**
 731+ * \brief Change to the next Unicode input mode.
 732+ */
 733+void unicode_input_mode_step(void);
 734+
 735+/**
 736+ * \brief Change to the previous Unicode input mode.
 737+ */
 738+void unicode_input_mode_step_reverse(void);
 739+
 740+/**
 741+ * \brief User-level callback, invoked when the input mode is changed.
 742+ *
 743+ * \param input_mode The new input mode.
 744+ */
 745 void unicode_input_mode_set_user(uint8_t input_mode);
 746+
 747+/**
 748+ * \brief Keyboard-level callback, invoked when the input mode is changed.
 749+ *
 750+ * \param input_mode The new input mode.
 751+ */
 752 void unicode_input_mode_set_kb(uint8_t input_mode);
 753 
 754+/**
 755+ * \brief Begin the Unicode input sequence. The exact behavior depends on the currently selected input mode.
 756+ */
 757 void unicode_input_start(void);
 758+
 759+/**
 760+ * \brief Complete the Unicode input sequence. The exact behavior depends on the currently selected input mode.
 761+ */
 762 void unicode_input_finish(void);
 763+
 764+/**
 765+ * \brief Cancel the Unicode input sequence. The exact behavior depends on the currently selected input mode.
 766+ */
 767 void unicode_input_cancel(void);
 768 
 769+/**
 770+ * \brief Send a 16-bit hex number.
 771+ *
 772+ * \param hex The number to send.
 773+ */
 774 void register_hex(uint16_t hex);
 775+
 776+/**
 777+ * \brief Send a 32-bit hex number.
 778+ *
 779+ * \param hex The number to send.
 780+ */
 781 void register_hex32(uint32_t hex);
 782+
 783+/**
 784+ * \brief Input a single Unicode character. A surrogate pair will be sent if required by the input mode.
 785+ *
 786+ * \param code_point The code point of the character to send.
 787+ */
 788 void register_unicode(uint32_t code_point);
 789 
 790+/**
 791+ * \brief Send a string containing Unicode characters.
 792diff --git a/quantum/unicode/unicodemap.c b/quantum/unicode/unicodemap.c
 793new file mode 100644
 794index 0000000000000000000000000000000000000000..2f618056a2e719018f0e08e7de1adffb6481be40
 795--- /dev/null
 796+++ b/quantum/unicode/unicodemap.c
 797@@ -0,0 +1,43 @@
 798+// Copyright 2023 QMK
 799+// SPDX-License-Identifier: GPL-2.0-or-later
 800+
 801+#include "unicodemap.h"
 802+#include "unicode.h"
 803+#include "keycodes.h"
 804+#include "quantum_keycodes.h"
 805+#include "modifiers.h"
 806+#include "host.h"
 807+#include "action_util.h"
 808+
 809+uint8_t unicodemap_index(uint16_t keycode) {
 810+    if (keycode >= QK_UNICODEMAP_PAIR) {
 811+        // Keycode is a pair: extract index based on Shift / Caps Lock state
 812+        uint16_t index;
 813+
 814+        uint8_t mods = get_mods() | get_weak_mods();
 815+#ifndef NO_ACTION_ONESHOT
 816+        mods |= get_oneshot_mods();
 817+#endif
 818+
 819+        bool shift = mods & MOD_MASK_SHIFT;
 820+        bool caps  = host_keyboard_led_state().caps_lock;
 821+        if (shift ^ caps) {
 822+            index = QK_UNICODEMAP_PAIR_GET_SHIFTED_INDEX(keycode);
 823+        } else {
 824+            index = QK_UNICODEMAP_PAIR_GET_UNSHIFTED_INDEX(keycode);
 825+        }
 826+
 827+        return index;
 828+    } else {
 829+        // Keycode is a regular index
 830+        return QK_UNICODEMAP_GET_INDEX(keycode);
 831+    }
 832+}
 833+
 834+uint32_t unicodemap_get_code_point(uint8_t index) {
 835+    return pgm_read_dword(unicode_map + index);
 836+}
 837+
 838+void register_unicodemap(uint8_t index) {
 839+    register_unicode(unicodemap_get_code_point(index));
 840+}
 841diff --git a/quantum/unicode/unicodemap.h b/quantum/unicode/unicodemap.h
 842new file mode 100644
 843index 0000000000000000000000000000000000000000..00d2aec8fad7988063d264673bda5815cabfd854
 844--- /dev/null
 845+++ b/quantum/unicode/unicodemap.h
 846@@ -0,0 +1,43 @@
 847+// Copyright 2023 QMK
 848+// SPDX-License-Identifier: GPL-2.0-or-later
 849+
 850+#pragma once
 851+
 852+#include <stdint.h>
 853+#include "progmem.h"
 854+
 855+/**
 856+ * \file
 857+ *
 858+ * \defgroup unicodemap Unicode Map
 859+ * \{
 860+ */
 861+
 862+extern const uint32_t unicode_map[] PROGMEM;
 863+
 864+/**
 865+ * \brief Get the index into the `unicode_map` array for the given keycode, respecting shift state for pair keycodes.
 866+ *
 867+ * \param keycode The Unicode Map keycode to get the index of.
 868+ *
 869+ * \return An index into the `unicode_map` array.
 870+ */
 871+uint8_t unicodemap_index(uint16_t keycode);
 872+
 873+/**
 874+ * \brief Get the code point for the given index in the `unicode_map` array.
 875+ *
 876+ * \param index The index into the `unicode_map` array.
 877+ *
 878+ * \return A Unicode code point value.
 879+ */
 880+uint32_t unicodemap_get_code_point(uint8_t index);
 881+
 882+/**
 883+ * \brief Send the code point for the given index in the `unicode_map` array.
 884+ *
 885+ * \param index The index into the `unicode_map` array.
 886+ */
 887+void register_unicodemap(uint8_t index);
 888+
 889+/** \} */
 890diff --git a/tests/test_common/test_driver.cpp b/tests/test_common/test_driver.cpp
 891index f1c52cb7b62bd89ac73c2fa49390bff7e2c4a0a7..0495da8205ac001eb080dadcbe24f34bc325cb3e 100644
 892--- a/tests/test_common/test_driver.cpp
 893+++ b/tests/test_common/test_driver.cpp
 894@@ -60,7 +60,10 @@ void TestDriver::send_extra(report_extra_t* report) {
 895 namespace internal {
 896 void expect_unicode_code_point(TestDriver& driver, uint32_t code_point) {
 897     testing::InSequence seq;
 898-    EXPECT_REPORT(driver, (KC_LCTL, KC_LSFT, KC_U));
 899+    EXPECT_REPORT(driver, (KC_LEFT_CTRL, KC_LEFT_SHIFT));
 900+    EXPECT_REPORT(driver, (KC_LEFT_CTRL, KC_LEFT_SHIFT, KC_U));
 901+    EXPECT_REPORT(driver, (KC_LEFT_CTRL, KC_LEFT_SHIFT));
 902+    EXPECT_EMPTY_REPORT(driver);
 903 
 904     bool print_zero = false;
 905     for (int i = 7; i >= 0; --i) {
 906@@ -71,10 +74,12 @@ void expect_unicode_code_point(TestDriver& driver, uint32_t code_point) {
 907         const uint8_t digit = (code_point >> (i * 4)) & 0xf;
 908         if (digit || print_zero) {
 909             EXPECT_REPORT(driver, (hex_digit_to_keycode(digit)));
 910+            EXPECT_EMPTY_REPORT(driver);
 911             print_zero = true;
 912         }
 913     }
 914 
 915-    EXPECT_REPORT(driver, (KC_SPC));
 916+    EXPECT_REPORT(driver, (KC_SPACE));
 917+    EXPECT_EMPTY_REPORT(driver);
 918 }
 919 } // namespace internal
 920diff --git a/tests/unicode/config.h b/tests/unicode/config.h
 921new file mode 100644
 922index 0000000000000000000000000000000000000000..16f95f7f8db77aa7f57d895eb9b9696e5e53376f
 923--- /dev/null
 924+++ b/tests/unicode/config.h
 925@@ -0,0 +1,8 @@
 926+// Copyright 2023 QMK
 927+// SPDX-License-Identifier: GPL-2.0-or-later
 928+
 929+#pragma once
 930+
 931+#include "test_common.h"
 932+
 933+#define UNICODE_SELECTED_MODES UNICODE_MODE_LINUX, UNICODE_MODE_MACOS
 934diff --git a/tests/unicode/test.mk b/tests/unicode/test.mk
 935new file mode 100644
 936index 0000000000000000000000000000000000000000..1afc79be559c68931cd916da9e816ed62384dd35
 937--- /dev/null
 938+++ b/tests/unicode/test.mk
 939@@ -0,0 +1,5 @@
 940+# --------------------------------------------------------------------------------
 941+# Keep this file, even if it is empty, as a marker that this folder contains tests
 942+# --------------------------------------------------------------------------------
 943+
 944+UNICODE_COMMON = yes
 945diff --git a/tests/unicode/test_unicode.cpp b/tests/unicode/test_unicode.cpp
 946new file mode 100644
 947index 0000000000000000000000000000000000000000..a8500e3ba1dc67fb8dac8133259949ad7f95d483
 948--- /dev/null
 949+++ b/tests/unicode/test_unicode.cpp
 950@@ -0,0 +1,86 @@
 951+// Copyright 2023 QMK
 952+// SPDX-License-Identifier: GPL-2.0-or-later
 953+
 954+#include "keyboard_report_util.hpp"
 955+#include "keycode.h"
 956+#include "test_common.hpp"
 957+#include "test_keymap_key.hpp"
 958+
 959+using testing::_;
 960+
 961+class Unicode : public TestFixture {};
 962+
 963+TEST_F(Unicode, sends_bmp_unicode_sequence) {
 964+    TestDriver driver;
 965+
 966+    set_unicode_input_mode(UNICODE_MODE_LINUX);
 967+
 968+    EXPECT_UNICODE(driver, 0x03A8); // Ψ
 969+    register_unicode(0x03A8);
 970+
 971+    VERIFY_AND_CLEAR(driver);
 972+}
 973+
 974+TEST_F(Unicode, sends_smp_unicode_sequence) {
 975+    TestDriver driver;
 976+
 977+    set_unicode_input_mode(UNICODE_MODE_LINUX);
 978+
 979+    EXPECT_UNICODE(driver, 0x1F9D9); // 🧙
 980+    register_unicode(0x1F9D9);
 981+
 982+    VERIFY_AND_CLEAR(driver);
 983+}
 984+
 985+TEST_F(Unicode, sends_surrogate_pair_for_macos) {
 986+    TestDriver driver;
 987+
 988+    set_unicode_input_mode(UNICODE_MODE_MACOS);
 989+
 990+    // EXPECT_UNICODE() assumes Linux input mode
 991+    {
 992+        testing::InSequence s;
 993+
 994+        // Alt+D83EDDD9 🧙
 995+        EXPECT_REPORT(driver, (KC_LEFT_ALT));
 996+        EXPECT_REPORT(driver, (KC_D, KC_LEFT_ALT));
 997+        EXPECT_REPORT(driver, (KC_LEFT_ALT));
 998+        EXPECT_REPORT(driver, (KC_8, KC_LEFT_ALT));
 999+        EXPECT_REPORT(driver, (KC_LEFT_ALT));
1000+        EXPECT_REPORT(driver, (KC_3, KC_LEFT_ALT));
1001+        EXPECT_REPORT(driver, (KC_LEFT_ALT));
1002+        EXPECT_REPORT(driver, (KC_E, KC_LEFT_ALT));
1003+        EXPECT_REPORT(driver, (KC_LEFT_ALT));
1004+        EXPECT_REPORT(driver, (KC_D, KC_LEFT_ALT));
1005+        EXPECT_REPORT(driver, (KC_LEFT_ALT));
1006+        EXPECT_REPORT(driver, (KC_D, KC_LEFT_ALT));
1007+        EXPECT_REPORT(driver, (KC_LEFT_ALT));
1008+        EXPECT_REPORT(driver, (KC_D, KC_LEFT_ALT));
1009+        EXPECT_REPORT(driver, (KC_LEFT_ALT));
1010+        EXPECT_REPORT(driver, (KC_9, KC_LEFT_ALT));
1011+        EXPECT_REPORT(driver, (KC_LEFT_ALT));
1012+        EXPECT_EMPTY_REPORT(driver);
1013+    }
1014+
1015+    register_unicode(0x1F9D9);
1016+
1017+    VERIFY_AND_CLEAR(driver);
1018+}
1019+
1020+TEST_F(Unicode, sends_unicode_string) {
1021+    TestDriver driver;
1022+
1023+    set_unicode_input_mode(UNICODE_MODE_LINUX);
1024+
1025+    {
1026+        testing::InSequence s;
1027+
1028+        EXPECT_UNICODE(driver, 0xFF31);
1029+        EXPECT_UNICODE(driver, 0xFF2D);
1030+        EXPECT_UNICODE(driver, 0xFF2B);
1031+        EXPECT_UNICODE(driver, 0xFF01);
1032+    }
1033+    send_unicode_string("QMK!");
1034+
1035+    VERIFY_AND_CLEAR(driver);
1036+}
1037diff --git a/tests/unicode/unicode_basic/config.h b/tests/unicode/unicode_basic/config.h
1038new file mode 100644
1039index 0000000000000000000000000000000000000000..16f95f7f8db77aa7f57d895eb9b9696e5e53376f
1040--- /dev/null
1041+++ b/tests/unicode/unicode_basic/config.h
1042@@ -0,0 +1,8 @@
1043+// Copyright 2023 QMK
1044+// SPDX-License-Identifier: GPL-2.0-or-later
1045+
1046+#pragma once
1047+
1048+#include "test_common.h"
1049+
1050+#define UNICODE_SELECTED_MODES UNICODE_MODE_LINUX, UNICODE_MODE_MACOS
1051diff --git a/tests/unicode/unicode_basic/test.mk b/tests/unicode/unicode_basic/test.mk
1052new file mode 100644
1053index 0000000000000000000000000000000000000000..f53051dce67990b6d354ba0fefa68899495147ea
1054--- /dev/null
1055+++ b/tests/unicode/unicode_basic/test.mk
1056@@ -0,0 +1,5 @@
1057+# --------------------------------------------------------------------------------
1058+# Keep this file, even if it is empty, as a marker that this folder contains tests
1059+# --------------------------------------------------------------------------------
1060+
1061+UNICODE_ENABLE = yes
1062diff --git a/tests/unicode/unicode_basic/test_unicode_basic.cpp b/tests/unicode/unicode_basic/test_unicode_basic.cpp
1063new file mode 100644
1064index 0000000000000000000000000000000000000000..598b57277f3d839e66f17a12139bf9e4274db01a
1065--- /dev/null
1066+++ b/tests/unicode/unicode_basic/test_unicode_basic.cpp
1067@@ -0,0 +1,26 @@
1068+// Copyright 2023 QMK
1069+// SPDX-License-Identifier: GPL-2.0-or-later
1070+
1071+#include "keyboard_report_util.hpp"
1072+#include "keycode.h"
1073+#include "test_common.hpp"
1074+#include "test_keymap_key.hpp"
1075+
1076+using testing::_;
1077+
1078+class UnicodeBasic : public TestFixture {};
1079+
1080+TEST_F(UnicodeBasic, sends_unicode_sequence) {
1081+    TestDriver driver;
1082+
1083+    set_unicode_input_mode(UNICODE_MODE_LINUX);
1084+
1085+    auto key_uc = KeymapKey(0, 0, 0, UC(0x03A8)); // Ψ
1086+
1087+    set_keymap({key_uc});
1088+
1089+    EXPECT_UNICODE(driver, 0x03A8);
1090+    tap_key(key_uc);
1091+
1092+    VERIFY_AND_CLEAR(driver);
1093+}
1094diff --git a/tests/unicode/unicode_map/config.h b/tests/unicode/unicode_map/config.h
1095new file mode 100644
1096index 0000000000000000000000000000000000000000..0d86922f0da786340d2895551df28188cb79b27a
1097--- /dev/null
1098+++ b/tests/unicode/unicode_map/config.h
1099@@ -0,0 +1,8 @@
1100+// Copyright 2023 QMK
1101+// SPDX-License-Identifier: GPL-2.0-or-later
1102+
1103+#pragma once
1104+
1105+#include "test_common.h"
1106+
1107+#define UNICODE_SELECTED_MODES UNICODE_MODE_LINUX
1108diff --git a/tests/unicode/unicode_map/test.mk b/tests/unicode/unicode_map/test.mk
1109new file mode 100644
1110index 0000000000000000000000000000000000000000..e6d003479988c10140216608f90245144b5db250
1111--- /dev/null
1112+++ b/tests/unicode/unicode_map/test.mk
1113@@ -0,0 +1,5 @@
1114+# --------------------------------------------------------------------------------
1115+# Keep this file, even if it is empty, as a marker that this folder contains tests
1116+# --------------------------------------------------------------------------------
1117+
1118+UNICODEMAP_ENABLE = yes
1119diff --git a/tests/unicode/unicode_map/test_unicode_map.cpp b/tests/unicode/unicode_map/test_unicode_map.cpp
1120new file mode 100644
1121index 0000000000000000000000000000000000000000..cacd8f3c757c69fbe2c8c4e23775ea2ccd0d3747
1122--- /dev/null
1123+++ b/tests/unicode/unicode_map/test_unicode_map.cpp
1124@@ -0,0 +1,54 @@
1125+// Copyright 2023 QMK
1126+// SPDX-License-Identifier: GPL-2.0-or-later
1127+
1128+#include "keyboard_report_util.hpp"
1129+#include "keycode.h"
1130+#include "test_common.hpp"
1131+#include "test_keymap_key.hpp"
1132+
1133+using testing::_;
1134+
1135+const uint32_t PROGMEM unicode_map[] = {
1136+    0x03A8, // Ψ
1137+    0x2318  // ⌘
1138+};
1139+
1140+class UnicodeMap : public TestFixture {};
1141+
1142+TEST_F(UnicodeMap, sends_unicodemap_code_point_from_keycode) {
1143+    TestDriver driver;
1144+
1145+    auto key_um = KeymapKey(0, 0, 0, UM(0));
1146+
1147+    set_keymap({key_um});
1148+
1149+    EXPECT_UNICODE(driver, 0x03A8);
1150+    tap_key(key_um);
1151+
1152+    VERIFY_AND_CLEAR(driver);
1153+}
1154+
1155+TEST_F(UnicodeMap, sends_unicodemap_pair_from_keycode) {
1156+    TestDriver driver;
1157+
1158+    auto key_shift = KeymapKey(0, 0, 0, KC_LEFT_SHIFT);
1159+    auto key_up    = KeymapKey(0, 1, 0, UP(0, 1));
1160+
1161+    set_keymap({key_shift, key_up});
1162+
1163+    EXPECT_UNICODE(driver, 0x03A8);
1164+    tap_key(key_up);
1165+
1166+    EXPECT_REPORT(driver, (KC_LEFT_SHIFT));
1167+    key_shift.press();
1168+    run_one_scan_loop();
1169+
1170+    EXPECT_UNICODE(driver, 0x2318);
1171+    tap_key(key_up);
1172+
1173+    EXPECT_NO_REPORT(driver);
1174+    key_shift.release();
1175+    run_one_scan_loop();
1176+
1177+    VERIFY_AND_CLEAR(driver);
1178+}
1179diff --git a/tests/unicode/unicode_ucis/config.h b/tests/unicode/unicode_ucis/config.h
1180new file mode 100644
1181index 0000000000000000000000000000000000000000..0d86922f0da786340d2895551df28188cb79b27a
1182--- /dev/null
1183+++ b/tests/unicode/unicode_ucis/config.h
1184@@ -0,0 +1,8 @@
1185+// Copyright 2023 QMK
1186+// SPDX-License-Identifier: GPL-2.0-or-later
1187+
1188+#pragma once
1189+
1190+#include "test_common.h"
1191+
1192+#define UNICODE_SELECTED_MODES UNICODE_MODE_LINUX
1193diff --git a/tests/unicode/unicode_ucis/test.mk b/tests/unicode/unicode_ucis/test.mk
1194new file mode 100644
1195index 0000000000000000000000000000000000000000..d7ac6517583add9305e2b60bd188663325d77fc5
1196--- /dev/null
1197+++ b/tests/unicode/unicode_ucis/test.mk
1198@@ -0,0 +1,5 @@
1199+# --------------------------------------------------------------------------------
1200+# Keep this file, even if it is empty, as a marker that this folder contains tests
1201+# --------------------------------------------------------------------------------
1202+
1203+UCIS_ENABLE = yes
1204diff --git a/tests/unicode/unicode_ucis/test_unicode_ucis.cpp b/tests/unicode/unicode_ucis/test_unicode_ucis.cpp
1205new file mode 100644
1206index 0000000000000000000000000000000000000000..acc6329f857b4272bc982fd58297b5a7b66a1fc2
1207--- /dev/null
1208+++ b/tests/unicode/unicode_ucis/test_unicode_ucis.cpp
1209@@ -0,0 +1,221 @@
1210+// Copyright 2023 QMK
1211+// SPDX-License-Identifier: GPL-2.0-or-later
1212+
1213+#include "keyboard_report_util.hpp"
1214+#include "keycode.h"
1215+#include "test_common.hpp"
1216+#include "test_keymap_key.hpp"
1217+
1218+using testing::_;
1219+
1220+// clang-format off
1221+const ucis_symbol_t ucis_symbol_table[] = UCIS_TABLE(
1222+    UCIS_SYM("qmk", 0x03A8) // Ψ
1223+);
1224+// clang-format on
1225+
1226+class UnicodeUCIS : public TestFixture {};
1227+
1228+TEST_F(UnicodeUCIS, matches_sequence) {
1229+    TestDriver driver;
1230+
1231+    auto key_q     = KeymapKey(0, 0, 0, KC_Q);
1232+    auto key_m     = KeymapKey(0, 1, 0, KC_M);
1233+    auto key_k     = KeymapKey(0, 2, 0, KC_K);
1234+    auto key_enter = KeymapKey(0, 3, 0, KC_ENTER);
1235+
1236+    set_keymap({key_q, key_m, key_k, key_enter});
1237+
1238+    EXPECT_UNICODE(driver, 0x2328); // ⌨
1239+    ucis_start();
1240+
1241+    EXPECT_EQ(ucis_active(), true);
1242+    EXPECT_EQ(ucis_count(), 0);
1243+
1244+    EXPECT_REPORT(driver, (KC_Q));
1245+    EXPECT_EMPTY_REPORT(driver);
1246+    tap_key(key_q);
1247+    EXPECT_EQ(ucis_count(), 1);
1248+
1249+    EXPECT_REPORT(driver, (KC_M));
1250+    EXPECT_EMPTY_REPORT(driver);
1251+    tap_key(key_m);
1252+    EXPECT_EQ(ucis_count(), 2);
1253+
1254+    EXPECT_REPORT(driver, (KC_K));
1255+    EXPECT_EMPTY_REPORT(driver);
1256+    tap_key(key_k);
1257+    EXPECT_EQ(ucis_count(), 3);
1258+
1259+    EXPECT_REPORT(driver, (KC_BACKSPACE)).Times(4);
1260+    EXPECT_EMPTY_REPORT(driver).Times(4);
1261+    EXPECT_UNICODE(driver, 0x03A8);
1262+    tap_key(key_enter);
1263+
1264+    EXPECT_EQ(ucis_active(), false);
1265+
1266+    VERIFY_AND_CLEAR(driver);
1267+}
1268+
1269+TEST_F(UnicodeUCIS, cancels_sequence) {
1270+    TestDriver driver;
1271+
1272+    auto key_q      = KeymapKey(0, 0, 0, KC_Q);
1273+    auto key_m      = KeymapKey(0, 1, 0, KC_M);
1274+    auto key_k      = KeymapKey(0, 2, 0, KC_K);
1275+    auto key_escape = KeymapKey(0, 3, 0, KC_ESCAPE);
1276+
1277+    set_keymap({key_q, key_m, key_k, key_escape});
1278+
1279+    EXPECT_UNICODE(driver, 0x2328); // ⌨
1280+    ucis_start();
1281+
1282+    EXPECT_EQ(ucis_active(), true);
1283+    EXPECT_EQ(ucis_count(), 0);
1284+
1285+    EXPECT_REPORT(driver, (KC_Q));
1286+    EXPECT_EMPTY_REPORT(driver);
1287+    tap_key(key_q);
1288+    EXPECT_EQ(ucis_count(), 1);
1289+
1290+    EXPECT_REPORT(driver, (KC_M));
1291+    EXPECT_EMPTY_REPORT(driver);
1292+    tap_key(key_m);
1293+    EXPECT_EQ(ucis_count(), 2);
1294+
1295+    EXPECT_REPORT(driver, (KC_K));
1296+    EXPECT_EMPTY_REPORT(driver);
1297+    tap_key(key_k);
1298+    EXPECT_EQ(ucis_count(), 3);
1299+
1300+    EXPECT_NO_REPORT(driver);
1301+    tap_key(key_escape);
1302+
1303+    EXPECT_EQ(ucis_active(), false);
1304+
1305+    VERIFY_AND_CLEAR(driver);
1306+}
1307+
1308+TEST_F(UnicodeUCIS, matches_sequence_with_corrected_typo) {