Parent directory

process_autocorrect.c

13186 bytes
  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
 22static uint8_t typo_buffer[AUTOCORRECT_MAX_LENGTH] = {KC_SPC};
 23static 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 */
 31bool autocorrect_is_enabled(void) {
 32    return keymap_config.autocorrect_enable;
 33}
 34
 35/**
 36 * @brief Enables autocorrect and saves state to eeprom
 37 *
 38 */
 39void autocorrect_enable(void) {
 40    keymap_config.autocorrect_enable = true;
 41    eeconfig_update_keymap(keymap_config.raw);
 42}
 43
 44/**
 45 * @brief Disables autocorrect and saves state to eeprom
 46 *
 47 */
 48void autocorrect_disable(void) {
 49    keymap_config.autocorrect_enable = false;
 50    typo_buffer_size                 = 0;
 51    eeconfig_update_keymap(keymap_config.raw);
 52}
 53
 54/**
 55 * @brief Toggles autocorrect's status and save state to eeprom
 56 *
 57 */
 58void autocorrect_toggle(void) {
 59    keymap_config.autocorrect_enable = !keymap_config.autocorrect_enable;
 60    typo_buffer_size                 = 0;
 61    eeconfig_update_keymap(keymap_config.raw);
 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 */
 91bool 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 */
200bool 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}