qmk-config
qmk configs for my open-source keyboards
git clone https://9o.is/git/qmk-config.git
achordion.c
(14462B)
1 // Copyright 2022-2024 Google LLC
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // https://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 /**
16 * @file achordion.c
17 * @brief Achordion implementation
18 *
19 * For full documentation, see
20 * <https://getreuer.info/posts/keyboards/achordion>
21 */
22
23 #include "achordion.h"
24
25 #if !defined(IS_QK_MOD_TAP)
26 // Attempt to detect out-of-date QMK installation, which would fail with
27 // implicit-function-declaration errors in the code below.
28 #error "achordion: QMK version is too old to build. Please update QMK."
29 #else
30
31 // Copy of the `record` and `keycode` args for the current active tap-hold key.
32 static keyrecord_t tap_hold_record;
33 static uint16_t tap_hold_keycode = KC_NO;
34 // Timeout timer. When it expires, the key is considered held.
35 static uint16_t hold_timer = 0;
36 // Eagerly applied mods, if any.
37 static uint8_t eager_mods = 0;
38 // Flag to determine whether another key is pressed within the timeout.
39 static bool pressed_another_key_before_release = false;
40
41 #ifdef ACHORDION_STREAK
42 // Timer for typing streak
43 static uint16_t streak_timer = 0;
44 #else
45 // When disabled, is_streak is never true
46 #define is_streak false
47 #endif
48
49 // Achordion's current state.
50 enum {
51 // A tap-hold key is pressed, but hasn't yet been settled as tapped or held.
52 STATE_UNSETTLED,
53 // Achordion is inactive.
54 STATE_RELEASED,
55 // Active tap-hold key has been settled as tapped.
56 STATE_TAPPING,
57 // Active tap-hold key has been settled as held.
58 STATE_HOLDING,
59 // This state is set while calling `process_record()`, which will recursively
60 // call `process_achordion()`. This state is checked so that we don't process
61 // events generated by Achordion and potentially create an infinite loop.
62 STATE_RECURSING,
63 };
64 static uint8_t achordion_state = STATE_RELEASED;
65
66 #ifdef ACHORDION_STREAK
67 static void update_streak_timer(uint16_t keycode, keyrecord_t* record) {
68 if (achordion_streak_continue(keycode)) {
69 // We use 0 to represent an unset timer, so `| 1` to force a nonzero value.
70 streak_timer = record->event.time | 1;
71 } else {
72 streak_timer = 0;
73 }
74 }
75 #endif
76
77 // Presses or releases eager_mods through process_action(), which skips the
78 // usual event handling pipeline. The action is considered as a mod-tap hold or
79 // release, with Retro Tapping if enabled.
80 static void process_eager_mods_action(void) {
81 action_t action;
82 action.code = ACTION_MODS_TAP_KEY(
83 eager_mods, QK_MOD_TAP_GET_TAP_KEYCODE(tap_hold_keycode));
84 process_action(&tap_hold_record, action);
85 }
86
87 // Calls `process_record()` with state set to RECURSING.
88 static void recursively_process_record(keyrecord_t* record, uint8_t state) {
89 achordion_state = STATE_RECURSING;
90 #if defined(POINTING_DEVICE_ENABLE) && defined(POINTING_DEVICE_AUTO_MOUSE_ENABLE)
91 int8_t mouse_key_tracker = get_auto_mouse_key_tracker();
92 #endif
93 process_record(record);
94 #if defined(POINTING_DEVICE_ENABLE) && defined(POINTING_DEVICE_AUTO_MOUSE_ENABLE)
95 set_auto_mouse_key_tracker(mouse_key_tracker);
96 #endif
97 achordion_state = state;
98 }
99
100 // Sends hold press event and settles the active tap-hold key as held.
101 static void settle_as_hold(void) {
102 if (eager_mods) {
103 // If eager mods are being applied, nothing needs to be done besides
104 // updating the state.
105 dprintln("Achordion: Settled eager mod as hold.");
106 achordion_state = STATE_HOLDING;
107 } else {
108 // Create hold press event.
109 dprintln("Achordion: Plumbing hold press.");
110 recursively_process_record(&tap_hold_record, STATE_HOLDING);
111 }
112 }
113
114 // Sends tap press and release and settles the active tap-hold key as tapped.
115 static void settle_as_tap(void) {
116 if (eager_mods) { // Clear eager mods if set.
117 #if defined(RETRO_TAPPING) || defined(RETRO_TAPPING_PER_KEY)
118 #ifdef DUMMY_MOD_NEUTRALIZER_KEYCODE
119 neutralize_flashing_modifiers(get_mods());
120 #endif // DUMMY_MOD_NEUTRALIZER_KEYCODE
121 #endif // defined(RETRO_TAPPING) || defined(RETRO_TAPPING_PER_KEY)
122 tap_hold_record.event.pressed = false;
123 // To avoid falsely triggering Retro Tapping, process eager mods release as
124 // a regular mods release rather than a mod-tap release.
125 action_t action;
126 action.code = ACTION_MODS(eager_mods);
127 process_action(&tap_hold_record, action);
128 eager_mods = 0;
129 }
130
131 dprintln("Achordion: Plumbing tap press.");
132 tap_hold_record.event.pressed = true;
133 tap_hold_record.tap.count = 1; // Revise event as a tap.
134 tap_hold_record.tap.interrupted = true;
135 // Plumb tap press event.
136 recursively_process_record(&tap_hold_record, STATE_TAPPING);
137
138 send_keyboard_report();
139 #if TAP_CODE_DELAY > 0
140 wait_ms(TAP_CODE_DELAY);
141 #endif // TAP_CODE_DELAY > 0
142
143 dprintln("Achordion: Plumbing tap release.");
144 tap_hold_record.event.pressed = false;
145 // Plumb tap release event.
146 recursively_process_record(&tap_hold_record, STATE_TAPPING);
147 }
148
149 bool process_achordion(uint16_t keycode, keyrecord_t* record) {
150 // Don't process events that Achordion generated.
151 if (achordion_state == STATE_RECURSING) {
152 return true;
153 }
154
155 // Determine whether the current event is for a mod-tap or layer-tap key.
156 const bool is_mt = IS_QK_MOD_TAP(keycode);
157 const bool is_tap_hold = is_mt || IS_QK_LAYER_TAP(keycode);
158 // Check that this is a normal key event, don't act on combos.
159 const bool is_key_event = IS_KEYEVENT(record->event);
160
161 // Event while no tap-hold key is active.
162 if (achordion_state == STATE_RELEASED) {
163 if (is_tap_hold && record->tap.count == 0 && record->event.pressed &&
164 is_key_event) {
165 // A tap-hold key is pressed and considered by QMK as "held".
166 const uint16_t timeout = achordion_timeout(keycode);
167 if (timeout > 0) {
168 achordion_state = STATE_UNSETTLED;
169 // Save info about this key.
170 tap_hold_keycode = keycode;
171 tap_hold_record = *record;
172 hold_timer = record->event.time + timeout;
173 pressed_another_key_before_release = false;
174 eager_mods = 0;
175
176 if (is_mt) { // Apply mods immediately if they are "eager."
177 const uint8_t mod = mod_config(QK_MOD_TAP_GET_MODS(keycode));
178 if (
179 #if defined(CAPS_WORD_ENABLE) && defined(CAPS_WORD_INVERT_ON_SHIFT)
180 // Since eager mods bypass normal event handling, eager Shift does
181 // not work with CAPS_WORD_INVERT_ON_SHIFT. So if this option is
182 // enabled, we don't apply Shift eagerly when Caps Word is on.
183 !(is_caps_word_on() && (mod & MOD_LSFT) != 0) &&
184 #endif // defined(CAPS_WORD_ENABLE) && defined(CAPS_WORD_INVERT_ON_SHIFT)
185 achordion_eager_mod(mod)) {
186 eager_mods = mod;
187 process_eager_mods_action();
188 }
189 }
190
191 dprintf("Achordion: Key 0x%04X pressed.%s\n", keycode,
192 eager_mods ? " Set eager mods." : "");
193 return false; // Skip default handling.
194 }
195 }
196
197 #ifdef ACHORDION_STREAK
198 update_streak_timer(keycode, record);
199 #endif
200 return true; // Otherwise, continue with default handling.
201 } else if (record->event.pressed && tap_hold_keycode != keycode) {
202 // Track whether another key was pressed while using a tap-hold key.
203 pressed_another_key_before_release = true;
204 }
205
206 // Release of the active tap-hold key.
207 if (keycode == tap_hold_keycode && !record->event.pressed) {
208 if (eager_mods) {
209 dprintln("Achordion: Key released. Clearing eager mods.");
210 tap_hold_record.event.pressed = false;
211 process_eager_mods_action();
212 } else if (achordion_state == STATE_HOLDING) {
213 dprintln("Achordion: Key released. Plumbing hold release.");
214 tap_hold_record.event.pressed = false;
215 // Plumb hold release event.
216 recursively_process_record(&tap_hold_record, STATE_RELEASED);
217 } else if (!pressed_another_key_before_release) {
218 // No other key was pressed between the press and release of the tap-hold
219 // key, plumb a hold press and then a release.
220 dprintln("Achordion: Key released. Plumbing hold press and release.");
221 recursively_process_record(&tap_hold_record, STATE_HOLDING);
222 tap_hold_record.event.pressed = false;
223 recursively_process_record(&tap_hold_record, STATE_RELEASED);
224 } else {
225 dprintln("Achordion: Key released.");
226 }
227
228 achordion_state = STATE_RELEASED;
229 tap_hold_keycode = KC_NO;
230 return false;
231 }
232
233 if (achordion_state == STATE_UNSETTLED && record->event.pressed) {
234 #ifdef ACHORDION_STREAK
235 const uint16_t s_timeout =
236 achordion_streak_chord_timeout(tap_hold_keycode, keycode);
237 const bool is_streak =
238 streak_timer && s_timeout &&
239 !timer_expired(record->event.time, (streak_timer + s_timeout));
240 #endif
241
242 // Press event occurred on a key other than the active tap-hold key.
243
244 // If the other key is *also* a tap-hold key and considered by QMK to be
245 // held, then we settle the active key as held. This way, things like
246 // chording multiple home row modifiers will work, but let's our logic
247 // consider simply a single tap-hold key as "active" at a time.
248 //
249 // Otherwise, we call `achordion_chord()` to determine whether to settle the
250 // tap-hold key as tapped vs. held. We implement the tap or hold by plumbing
251 // events back into the handling pipeline so that QMK features and other
252 // user code can see them. This is done by calling `process_record()`, which
253 // in turn calls most handlers including `process_record_user()`.
254 if (!is_streak &&
255 (!is_key_event || (is_tap_hold && record->tap.count == 0) ||
256 achordion_chord(tap_hold_keycode, &tap_hold_record, keycode,
257 record))) {
258 settle_as_hold();
259
260 #ifdef REPEAT_KEY_ENABLE
261 // Edge case involving LT + Repeat Key: in a sequence of "LT down, other
262 // down" where "other" is on the other layer in the same position as
263 // Repeat or Alternate Repeat, the repeated keycode is set instead of the
264 // the one on the switched-to layer. Here we correct that.
265 if (get_repeat_key_count() != 0 && IS_QK_LAYER_TAP(tap_hold_keycode)) {
266 record->keycode = KC_NO; // Forget the repeated keycode.
267 clear_weak_mods();
268 }
269 #endif // REPEAT_KEY_ENABLE
270 } else {
271 settle_as_tap();
272
273 #ifdef ACHORDION_STREAK
274 update_streak_timer(keycode, record);
275 if (is_streak && is_key_event && is_tap_hold && record->tap.count == 0) {
276 // If we are in a streak and resolved the current tap-hold key as a tap
277 // consider the next tap-hold key as active to be resolved next.
278 update_streak_timer(tap_hold_keycode, &tap_hold_record);
279 const uint16_t timeout = achordion_timeout(keycode);
280 tap_hold_keycode = keycode;
281 tap_hold_record = *record;
282 hold_timer = record->event.time + timeout;
283 achordion_state = STATE_UNSETTLED;
284 pressed_another_key_before_release = false;
285 return false;
286 }
287 #endif
288 }
289
290 recursively_process_record(record, achordion_state); // Re-process event.
291 return false; // Block the original event.
292 }
293
294 #ifdef ACHORDION_STREAK
295 // update idle timer on regular keys event
296 update_streak_timer(keycode, record);
297 #endif
298 return true;
299 }
300
301 void achordion_task(void) {
302 if (achordion_state == STATE_UNSETTLED &&
303 timer_expired(timer_read(), hold_timer)) {
304 settle_as_hold(); // Timeout expired, settle the key as held.
305 }
306
307 #ifdef ACHORDION_STREAK
308 #define MAX_STREAK_TIMEOUT 800
309 if (streak_timer &&
310 timer_expired(timer_read(), (streak_timer + MAX_STREAK_TIMEOUT))) {
311 streak_timer = 0; // Expired.
312 }
313 #endif
314 }
315
316 // Returns true if `pos` on the left hand of the keyboard, false if right.
317 static bool on_left_hand(keypos_t pos) {
318 #ifdef SPLIT_KEYBOARD
319 return pos.row < MATRIX_ROWS / 2;
320 #else
321 return (MATRIX_COLS > MATRIX_ROWS) ? pos.col < MATRIX_COLS / 2
322 : pos.row < MATRIX_ROWS / 2;
323 #endif
324 }
325
326 bool achordion_opposite_hands(const keyrecord_t* tap_hold_record,
327 const keyrecord_t* other_record) {
328 return on_left_hand(tap_hold_record->event.key) !=
329 on_left_hand(other_record->event.key);
330 }
331
332 // By default, use the BILATERAL_COMBINATIONS rule to consider the tap-hold key
333 // "held" only when it and the other key are on opposite hands.
334 __attribute__((weak)) bool achordion_chord(uint16_t tap_hold_keycode,
335 keyrecord_t* tap_hold_record,
336 uint16_t other_keycode,
337 keyrecord_t* other_record) {
338 return achordion_opposite_hands(tap_hold_record, other_record);
339 }
340
341 // By default, the timeout is 1000 ms for all keys.
342 __attribute__((weak)) uint16_t achordion_timeout(uint16_t tap_hold_keycode) {
343 return 1000;
344 }
345
346 // By default, Shift and Ctrl mods are eager, and Alt and GUI are not.
347 __attribute__((weak)) bool achordion_eager_mod(uint8_t mod) {
348 return (mod & (MOD_LALT | MOD_LGUI)) == 0;
349 }
350
351 #ifdef ACHORDION_STREAK
352 __attribute__((weak)) bool achordion_streak_continue(uint16_t keycode) {
353 // If any mods other than shift or AltGr are held, don't continue the streak
354 if (get_mods() & (MOD_MASK_CG | MOD_BIT_LALT)) return false;
355 // This function doesn't get called for holds, so convert to tap version of
356 // keycodes
357 if (IS_QK_MOD_TAP(keycode)) keycode = QK_MOD_TAP_GET_TAP_KEYCODE(keycode);
358 if (IS_QK_LAYER_TAP(keycode)) keycode = QK_LAYER_TAP_GET_TAP_KEYCODE(keycode);
359 // Regular letters and punctuation continue the streak.
360 if (keycode >= KC_A && keycode <= KC_Z) return true;
361 switch (keycode) {
362 case KC_DOT:
363 case KC_COMMA:
364 case KC_QUOTE:
365 case KC_SPACE:
366 return true;
367 }
368 // All other keys end the streak
369 return false;
370 }
371
372 __attribute__((weak)) uint16_t achordion_streak_chord_timeout(
373 uint16_t tap_hold_keycode, uint16_t next_keycode) {
374 return achordion_streak_timeout(tap_hold_keycode);
375 }
376
377 __attribute__((weak)) uint16_t
378 achordion_streak_timeout(uint16_t tap_hold_keycode) {
379 return 200;
380 }
381 #endif
382
383 #endif // version check
384