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 }