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}