qmk_firmware

QMK firmware for my keyboards (Corne, Sweep Ferris) and trackball (Ploopy Adept)
Log | Files | Refs | Submodules | LICENSE

process_autocorrect.c (13177B)


      1 // Copyright 2021 Google LLC
      2 // Copyright 2021 @filterpaper
      3 // Copyright 2023 Pablo Martinez (@elpekenin) <elpekenin@elpekenin.dev>
      4 // SPDX-License-Identifier: Apache-2.0
      5 // Original source: https://getreuer.info/posts/keyboards/autocorrection
      6 
      7 #include "process_autocorrect.h"
      8 #include <string.h>
      9 #include "keycodes.h"
     10 #include "quantum_keycodes.h"
     11 #include "keycode_config.h"
     12 #include "send_string.h"
     13 #include "action_util.h"
     14 
     15 #if __has_include("autocorrect_data.h")
     16 #    include "autocorrect_data.h"
     17 #else
     18 #    pragma message "Autocorrect is using the default library."
     19 #    include "autocorrect_data_default.h"
     20 #endif
     21 
     22 static uint8_t typo_buffer[AUTOCORRECT_MAX_LENGTH] = {KC_SPC};
     23 static uint8_t typo_buffer_size                    = 1;
     24 
     25 /**
     26  * @brief function for querying the enabled state of autocorrect
     27  *
     28  * @return true if enabled
     29  * @return false if disabled
     30  */
     31 bool autocorrect_is_enabled(void) {
     32     return keymap_config.autocorrect_enable;
     33 }
     34 
     35 /**
     36  * @brief Enables autocorrect and saves state to eeprom
     37  *
     38  */
     39 void autocorrect_enable(void) {
     40     keymap_config.autocorrect_enable = true;
     41     eeconfig_update_keymap(&keymap_config);
     42 }
     43 
     44 /**
     45  * @brief Disables autocorrect and saves state to eeprom
     46  *
     47  */
     48 void autocorrect_disable(void) {
     49     keymap_config.autocorrect_enable = false;
     50     typo_buffer_size                 = 0;
     51     eeconfig_update_keymap(&keymap_config);
     52 }
     53 
     54 /**
     55  * @brief Toggles autocorrect's status and save state to eeprom
     56  *
     57  */
     58 void autocorrect_toggle(void) {
     59     keymap_config.autocorrect_enable = !keymap_config.autocorrect_enable;
     60     typo_buffer_size                 = 0;
     61     eeconfig_update_keymap(&keymap_config);
     62 }
     63 
     64 /**
     65  * @brief handler for user to override whether autocorrect should process this keypress
     66  *
     67  * @param keycode Keycode registered by matrix press, per keymap
     68  * @param record keyrecord_t structure
     69  * @param typo_buffer_size passed along to allow resetting of autocorrect buffer
     70  * @param mods allow processing of mod status
     71  * @return true Allow autocorection
     72  * @return false Stop processing and escape from autocorrect.
     73  */
     74 __attribute__((weak)) bool process_autocorrect_user(uint16_t *keycode, keyrecord_t *record, uint8_t *typo_buffer_size, uint8_t *mods) {
     75     return process_autocorrect_default_handler(keycode, record, typo_buffer_size, mods);
     76 }
     77 
     78 /**
     79  * @brief fallback handler for determining if autocorrect should process this keypress
     80  *        can be used by user callback to get the basic keycode being "wrapped"
     81  *
     82  * NOTE: These values may have been edited by user callback before getting here
     83  *
     84  * @param keycode Keycode registered by matrix press, per keymap
     85  * @param record keyrecord_t structure
     86  * @param typo_buffer_size passed along to allow resetting of autocorrect buffer
     87  * @param mods allow processing of mod status
     88  * @return true Allow autocorection
     89  * @return false Stop processing and escape from autocorrect.
     90  */
     91 bool process_autocorrect_default_handler(uint16_t *keycode, keyrecord_t *record, uint8_t *typo_buffer_size, uint8_t *mods) {
     92     // See quantum_keycodes.h for reference on these matched ranges.
     93     switch (*keycode) {
     94         // Exclude these keycodes from processing.
     95         case KC_LSFT:
     96         case KC_RSFT:
     97         case KC_CAPS:
     98         case QK_TO ... QK_TO_MAX:
     99         case QK_MOMENTARY ... QK_MOMENTARY_MAX:
    100         case QK_DEF_LAYER ... QK_DEF_LAYER_MAX:
    101         case QK_PERSISTENT_DEF_LAYER ... QK_PERSISTENT_DEF_LAYER_MAX:
    102         case QK_TOGGLE_LAYER ... QK_TOGGLE_LAYER_MAX:
    103         case QK_ONE_SHOT_LAYER ... QK_ONE_SHOT_LAYER_MAX:
    104         case QK_LAYER_TAP_TOGGLE ... QK_LAYER_TAP_TOGGLE_MAX:
    105         case QK_LAYER_MOD ... QK_LAYER_MOD_MAX:
    106         case QK_ONE_SHOT_MOD ... QK_ONE_SHOT_MOD_MAX:
    107             return false;
    108 
    109         // Mask for base keycode from shifted keys.
    110         case QK_LSFT ... QK_LSFT + 255:
    111         case QK_RSFT ... QK_RSFT + 255:
    112             if (*keycode >= QK_LSFT && *keycode <= (QK_LSFT + 255)) {
    113                 *mods |= MOD_LSFT;
    114             } else {
    115                 *mods |= MOD_RSFT;
    116             }
    117             *keycode = QK_MODS_GET_BASIC_KEYCODE(*keycode); // Get the basic keycode.
    118             return true;
    119 #ifndef NO_ACTION_TAPPING
    120         // Exclude tap-hold keys when they are held down
    121         // and mask for base keycode when they are tapped.
    122         case QK_LAYER_TAP ... QK_LAYER_TAP_MAX:
    123 #    ifdef NO_ACTION_LAYER
    124             // Exclude Layer Tap, if layers are disabled
    125             // but action tapping is still enabled.
    126             return false;
    127 #    else
    128             // Exclude hold keycode
    129             if (!record->tap.count) {
    130                 return false;
    131             }
    132             *keycode = QK_LAYER_TAP_GET_TAP_KEYCODE(*keycode);
    133             break;
    134 #    endif
    135         case QK_MOD_TAP ... QK_MOD_TAP_MAX:
    136             // Exclude hold keycode
    137             if (!record->tap.count) {
    138                 return false;
    139             }
    140             *keycode = QK_MOD_TAP_GET_TAP_KEYCODE(*keycode);
    141             break;
    142 #else
    143         case QK_MOD_TAP ... QK_MOD_TAP_MAX:
    144         case QK_LAYER_TAP ... QK_LAYER_TAP_MAX:
    145             // Exclude if disabled
    146             return false;
    147 #endif
    148         // Exclude swap hands keys when they are held down
    149         // and mask for base keycode when they are tapped.
    150         case QK_SWAP_HANDS ... QK_SWAP_HANDS_MAX:
    151 #ifdef SWAP_HANDS_ENABLE
    152             // Note: IS_SWAP_HANDS_KEYCODE() actually tests for the special action keycodes like SH_TOGG, SH_TT, ...,
    153             // which currently overlap the SH_T(kc) range.
    154             if (IS_SWAP_HANDS_KEYCODE(*keycode)
    155 #    ifndef NO_ACTION_TAPPING
    156                 || !record->tap.count
    157 #    endif // NO_ACTION_TAPPING
    158             ) {
    159                 return false;
    160             }
    161             *keycode = QK_SWAP_HANDS_GET_TAP_KEYCODE(*keycode);
    162             break;
    163 #else
    164             // Exclude if disabled
    165             return false;
    166 #endif
    167     }
    168 
    169     // Disable autocorrect while a mod other than shift is active.
    170     if ((*mods & ~MOD_MASK_SHIFT) != 0) {
    171         *typo_buffer_size = 0;
    172         return false;
    173     }
    174 
    175     return true;
    176 }
    177 
    178 /**
    179  * @brief handling for when autocorrection has been triggered
    180  *
    181  * @param backspaces number of characters to remove
    182  * @param str pointer to PROGMEM string to replace mistyped seletion with
    183  * @param typo the wrong string that triggered a correction
    184  * @param correct what it would become after the changes
    185  * @return true apply correction
    186  * @return false user handled replacement
    187  */
    188 __attribute__((weak)) bool apply_autocorrect(uint8_t backspaces, const char *str, char *typo, char *correct) {
    189     return true;
    190 }
    191 
    192 /**
    193  * @brief Process handler for autocorrect feature
    194  *
    195  * @param keycode Keycode registered by matrix press, per keymap
    196  * @param record keyrecord_t structure
    197  * @return true Continue processing keycodes, and send to host
    198  * @return false Stop processing keycodes, and don't send to host
    199  */
    200 bool process_autocorrect(uint16_t keycode, keyrecord_t *record) {
    201     uint8_t mods = get_mods();
    202 #ifndef NO_ACTION_ONESHOT
    203     mods |= get_oneshot_mods();
    204 #endif
    205 
    206     if ((keycode >= QK_AUTOCORRECT_ON && keycode <= QK_AUTOCORRECT_TOGGLE) && record->event.pressed) {
    207         if (keycode == QK_AUTOCORRECT_ON) {
    208             autocorrect_enable();
    209         } else if (keycode == QK_AUTOCORRECT_OFF) {
    210             autocorrect_disable();
    211         } else if (keycode == QK_AUTOCORRECT_TOGGLE) {
    212             autocorrect_toggle();
    213         } else {
    214             return true;
    215         }
    216 
    217         return false;
    218     }
    219 
    220     if (!keymap_config.autocorrect_enable) {
    221         typo_buffer_size = 0;
    222         return true;
    223     }
    224 
    225     if (!record->event.pressed) {
    226         return true;
    227     }
    228 
    229     // autocorrect keycode verification and extraction
    230     if (!process_autocorrect_user(&keycode, record, &typo_buffer_size, &mods)) {
    231         return true;
    232     }
    233 
    234     // keycode buffer check
    235     switch (keycode) {
    236         case KC_A ... KC_Z:
    237             // process normally
    238             break;
    239         case KC_1 ... KC_0:
    240         case KC_TAB ... KC_SEMICOLON:
    241         case KC_GRAVE ... KC_SLASH:
    242             // Set a word boundary if space, period, digit, etc. is pressed.
    243             keycode = KC_SPC;
    244             break;
    245         case KC_ENTER:
    246             // Behave more conservatively for the enter key. Reset, so that enter
    247             // can't be used on a word ending.
    248             typo_buffer_size = 0;
    249             keycode          = KC_SPC;
    250             break;
    251         case KC_BSPC:
    252             // Remove last character from the buffer.
    253             if (typo_buffer_size > 0) {
    254                 --typo_buffer_size;
    255             }
    256             return true;
    257         case KC_QUOTE:
    258             // Treat " (shifted ') as a word boundary.
    259             if ((mods & MOD_MASK_SHIFT) != 0) {
    260                 keycode = KC_SPC;
    261             }
    262             break;
    263         default:
    264             // Clear state if some other non-alpha key is pressed.
    265             typo_buffer_size = 0;
    266             return true;
    267     }
    268 
    269     // Rotate oldest character if buffer is full.
    270     if (typo_buffer_size >= AUTOCORRECT_MAX_LENGTH) {
    271         memmove(typo_buffer, typo_buffer + 1, AUTOCORRECT_MAX_LENGTH - 1);
    272         typo_buffer_size = AUTOCORRECT_MAX_LENGTH - 1;
    273     }
    274 
    275     // Append `keycode` to buffer.
    276     typo_buffer[typo_buffer_size++] = keycode;
    277     // Return if buffer is smaller than the shortest word.
    278     if (typo_buffer_size < AUTOCORRECT_MIN_LENGTH) {
    279         return true;
    280     }
    281 
    282     // Check for typo in buffer using a trie stored in `autocorrect_data`.
    283     uint16_t state = 0;
    284     uint8_t  code  = pgm_read_byte(autocorrect_data + state);
    285     for (int8_t i = typo_buffer_size - 1; i >= 0; --i) {
    286         uint8_t const key_i = typo_buffer[i];
    287 
    288         if (code & 64) { // Check for match in node with multiple children.
    289             code &= 63;
    290             for (; code != key_i; code = pgm_read_byte(autocorrect_data + (state += 3))) {
    291                 if (!code) return true;
    292             }
    293             // Follow link to child node.
    294             state = (pgm_read_byte(autocorrect_data + state + 1) | pgm_read_byte(autocorrect_data + state + 2) << 8);
    295             // Check for match in node with single child.
    296         } else if (code != key_i) {
    297             return true;
    298         } else if (!(code = pgm_read_byte(autocorrect_data + (++state)))) {
    299             ++state;
    300         }
    301 
    302         // Stop if `state` becomes an invalid index. This should not normally
    303         // happen, it is a safeguard in case of a bug, data corruption, etc.
    304         if (state >= DICTIONARY_SIZE) {
    305             return true;
    306         }
    307 
    308         code = pgm_read_byte(autocorrect_data + state);
    309 
    310         if (code & 128) { // A typo was found! Apply autocorrect.
    311             const uint8_t backspaces = (code & 63) + !record->event.pressed;
    312             const char   *changes    = (const char *)(autocorrect_data + state + 1);
    313 
    314             /* Gather info about the typo'd word
    315              *
    316              * Since buffer may contain several words, delimited by spaces, we
    317              * iterate from the end to find the start and length of the typo
    318              */
    319             char typo[AUTOCORRECT_MAX_LENGTH + 1] = {0}; // extra char for null terminator
    320 
    321             uint8_t typo_len   = 0;
    322             uint8_t typo_start = 0;
    323             bool    space_last = typo_buffer[typo_buffer_size - 1] == KC_SPC;
    324             for (uint8_t i = typo_buffer_size; i > 0; --i) {
    325                 // stop counting after finding space (unless it is the last thing)
    326                 if (typo_buffer[i - 1] == KC_SPC && i != typo_buffer_size) {
    327                     typo_start = i;
    328                     break;
    329                 }
    330 
    331                 ++typo_len;
    332             }
    333 
    334             // when detecting 'typo:', reduce the length of the string by one
    335             if (space_last) {
    336                 --typo_len;
    337             }
    338 
    339             // convert buffer of keycodes into a string
    340             for (uint8_t i = 0; i < typo_len; ++i) {
    341                 typo[i] = typo_buffer[typo_start + i] - KC_A + 'a';
    342             }
    343 
    344             /* Gather the corrected word
    345              *
    346              * A) Correction of 'typo:' -- Code takes into account
    347              * an extra backspace to delete the space (which we dont copy)
    348              * for this reason the offset is correct to "skip" the null terminator
    349              *
    350              * B) When correcting 'typo' -- Need extra offset for terminator
    351              */
    352             char correct[AUTOCORRECT_MAX_LENGTH + 10] = {0}; // let's hope this is big enough
    353 
    354             uint8_t offset = space_last ? backspaces : backspaces + 1;
    355             strcpy(correct, typo);
    356             strcpy_P(correct + typo_len - offset, changes);
    357 
    358             if (apply_autocorrect(backspaces, changes, typo, correct)) {
    359                 for (uint8_t i = 0; i < backspaces; ++i) {
    360                     tap_code(KC_BSPC);
    361                 }
    362                 send_string_P(changes);
    363             }
    364 
    365             if (keycode == KC_SPC) {
    366                 typo_buffer[0]   = KC_SPC;
    367                 typo_buffer_size = 1;
    368                 return true;
    369             } else {
    370                 typo_buffer_size = 0;
    371                 return false;
    372             }
    373         }
    374     }
    375     return true;
    376 }