qmk/modules/getreuer/select_word/select_word.c

301 lines
8.3 KiB
C

// Copyright 2021-2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @file select_word.c
* @brief Select Word community module implementation
*
* For full documentation, see
* <https://getreuer.info/posts/keyboards/select-word>
*/
#include "select_word.h"
ASSERT_COMMUNITY_MODULES_MIN_API_VERSION(1, 0, 0);
// Default to a timeout of 5 seconds.
#ifndef SELECT_WORD_TIMEOUT
# define SELECT_WORD_TIMEOUT 5000
#endif // SELECT_WORD_TIMEOUT
static int8_t selection_dir = 0;
static bool reset_before_next_event = false;
static uint8_t registered_hotkey = KC_NO;
// Macro `IS_MAC` determines whether to use Mac vs. Windows/Linux hotkeys:
//
// * OS Detection is used if it is enabled.
//
// * With SELECT_WORD_OS_DYNAMIC, the user may define callback
// select_word_host_is_mac().
//
// * Otherwise, the assumed OS is set at compile time, to Window/Linux by
// default, or to Mac with SELECT_WORD_OS_MAC.
#if defined(SELECT_WORD_OS_DYNAMIC) || defined(OS_DETECTION_ENABLE)
__attribute__((weak)) bool select_word_host_is_mac(void) {
# ifdef OS_DETECTION_ENABLE // Use OS Detection if enabled.
switch (detected_host_os()) {
case OS_LINUX:
case OS_WINDOWS:
return false;
case OS_MACOS:
case OS_IOS:
return true;
default:
break;
}
# endif // OS_DETECTION_ENABLE
# ifdef SELECT_WORD_OS_MAC
return true;
# else
return false;
# endif // SELECT_WORD_OS_MAC
}
# define IS_MAC select_word_host_is_mac()
#else
# ifdef SELECT_WORD_OS_MAC
# define IS_MAC true
# else
# define IS_MAC false
# endif // SELECT_WORD_OS_MAC
#endif // defined(SELECT_WORD_OS_DYNAMIC) || defined(OS_DETECTION_ENABLE)
// Idle timeout timer to reset Select Word after a period of inactivity.
#if SELECT_WORD_TIMEOUT > 0
# if SELECT_WORD_TIMEOUT < 100 || SELECT_WORD_TIMEOUT > 30000
// Constrain timeout to a sensible range. With the 16-bit timer, the longest
// representable timeout is 32768 ms, rounded here to 30000 ms = half a minute.
# error "select_word: SELECT_WORD_TIMEOUT must be between 100 and 30000 ms"
# endif
static uint16_t idle_timer = 0;
static void restart_idle_timer(void) {
idle_timer = (timer_read() + SELECT_WORD_TIMEOUT) | 1;
}
void housekeeping_task_select_word(void) {
if (idle_timer && timer_expired(timer_read(), idle_timer)) {
idle_timer = 0;
selection_dir = 0;
}
}
#endif // SELECT_WORD_TIMEOUT > 0
static void clear_all_mods(void) {
clear_mods();
clear_weak_mods();
#ifndef NO_ACTION_ONESHOT
clear_oneshot_mods();
#endif // NO_ACTION_ONESHOT
}
static void select_word_in_dir(int8_t dir) {
// With Windows and Linux (non-Mac) systems:
// dir < 0: Backward word selection: Ctrl+Left, Ctrl+Right, Ctrl+Shift+Left.
// dir > 0: Forward word selection: Ctrl+Right, Ctrl+Left, Ctrl+Shift+Right.
// Or to extend an existing selection:
// dir < 0: Backward word selection: Ctrl+Shift+Left.
// dir > 0: Forward word selection: Ctrl+Shift+Right.
//
// With Mac OS, use Alt (Opt) instead of Ctrl:
// dir < 0: Backward word selection: Alt+Left, Alt+Right, Alt+Shift+Left.
// dir > 0: Forward word selection: Alt+Right, Alt+Left, Alt+Shift+Right.
// Or to extend an existing selection:
// dir < 0: Backward word selection: Alt+Shift+Left.
// dir > 0: Forward word selection: Alt+Shift+Right.
reset_before_next_event = false;
const uint8_t saved_mods = get_mods();
clear_all_mods();
if (selection_dir && (selection_dir < 0) != (dir < 0)) { // Reversal.
send_keyboard_report();
tap_code_delay((dir < 0) ? KC_RGHT : KC_LEFT, TAP_CODE_DELAY);
}
add_mods(IS_MAC ? MOD_BIT_LALT : MOD_BIT_LCTRL);
if (selection_dir == 0) { // Initial selection.
send_keyboard_report();
send_string_with_delay_P(
(dir < 0) ? PSTR(SS_TAP(X_LEFT) SS_TAP(X_RGHT))
: PSTR(SS_TAP(X_RGHT) SS_TAP(X_LEFT)),
TAP_CODE_DELAY);
}
register_mods(MOD_BIT_LSHIFT);
registered_hotkey = (dir < 0) ? KC_LEFT : KC_RGHT;
register_code(registered_hotkey);
set_mods(saved_mods);
selection_dir = dir;
}
static void select_line(void) {
// With Windows and Linux (non-Mac) systems:
// Home, Shift+End.
// Or to extend an existing selection:
// Shift+Down.
//
// With Mac OS, use GUI (Command) + arrows:
// GUI+Left, Shift+GUI+Right.
// Or to extend an existing selection:
// Shift+Down.
reset_before_next_event = false;
const uint8_t saved_mods = get_mods();
clear_all_mods();
if (selection_dir != 2) {
send_keyboard_report();
send_string_with_delay_P(
IS_MAC ? PSTR(SS_LGUI(SS_TAP(X_LEFT) SS_LSFT(SS_TAP(X_RGHT))))
: PSTR(SS_TAP(X_HOME) SS_LSFT(SS_TAP(X_END))),
TAP_CODE_DELAY);
} else {
register_mods(MOD_BIT_LSHIFT);
registered_hotkey = KC_DOWN;
register_code(KC_DOWN);
}
set_mods(saved_mods);
selection_dir = 2;
}
void select_word_register(char action) {
if (registered_hotkey) {
select_word_unregister();
}
switch (action) {
case 'W':
select_word_in_dir(1);
break;
case 'B':
select_word_in_dir(-1);
break;
case 'L':
select_line();
break;
}
#if SELECT_WORD_TIMEOUT > 0
idle_timer = 0;
#endif // SELECT_WORD_TIMEOUT > 0
}
void select_word_unregister(void) {
reset_before_next_event = false;
unregister_code(registered_hotkey);
if (registered_hotkey == KC_DOWN) {
// When using line selection to select multiple lines, tap Shift+End (or on
// Mac, GUI+Shift+Right) on release to ensure the selection extends to the
// end of the current line.
const uint8_t saved_mods = get_mods();
clear_all_mods();
send_keyboard_report();
send_string_with_delay_P(
IS_MAC ? PSTR(SS_LGUI(SS_LSFT(SS_TAP(X_RGHT))))
: PSTR(SS_LSFT(SS_TAP(X_END))),
TAP_CODE_DELAY);
set_mods(saved_mods);
}
registered_hotkey = KC_NO;
#if SELECT_WORD_TIMEOUT > 0
restart_idle_timer();
#endif // SELECT_WORD_TIMEOUT > 0
}
bool process_record_select_word(uint16_t keycode, keyrecord_t* record) {
if (!process_record_select_word_kb(keycode, record)) {
return false;
}
if (selection_dir) {
if (reset_before_next_event) {
selection_dir = 0;
}
// Ignore most modifier and layer switch keys.
switch (keycode) {
case MODIFIER_KEYCODE_RANGE:
case QK_MOMENTARY ... QK_MOMENTARY_MAX:
case QK_LAYER_MOD ... QK_LAYER_MOD_MAX:
case QK_LAYER_TAP_TOGGLE ... QK_LAYER_TAP_TOGGLE_MAX:
case QK_TO ... QK_TO_MAX:
case QK_TOGGLE_LAYER ... QK_TOGGLE_LAYER_MAX:
#ifndef NO_ACTION_ONESHOT
case QK_ONE_SHOT_LAYER ... QK_ONE_SHOT_LAYER_MAX:
case QK_ONE_SHOT_MOD ... QK_ONE_SHOT_MOD_MAX:
#endif // NO_ACTION_ONESHOT
#ifdef LAYER_LOCK_ENABLE
case QK_LLCK:
#endif // LAYER_LOCK_ENABLE
return true;
#ifndef NO_ACTION_TAPPING
// Ignore hold events on mod-tap and layer-tap keys.
case QK_MOD_TAP ... QK_MOD_TAP_MAX:
case QK_LAYER_TAP ... QK_LAYER_TAP_MAX:
if (record->tap.count == 0) {
return true;
}
break;
#endif // NO_ACTION_TAPPING
}
reset_before_next_event = true;
}
#if SELECT_WORD_TIMEOUT > 0
if (idle_timer) {
restart_idle_timer();
}
#endif // SELECT_WORD_TIMEOUT > 0
const bool shifted = MOD_MASK_SHIFT & (get_mods() | get_weak_mods()
#ifndef NO_ACTION_ONESHOT
| get_oneshot_mods()
#endif // NO_ACTION_ONESHOT
);
switch (keycode) {
case SELECT_WORD:
if (record->event.pressed) {
select_word_register(shifted ? 'L' : 'W');
} else {
select_word_unregister();
}
return false;
case SELECT_WORD_BACK:
if (record->event.pressed) {
select_word_register('B');
} else {
select_word_unregister();
}
return false;
case SELECT_LINE:
if (record->event.pressed) {
select_word_register('L');
} else {
select_word_unregister();
}
return false;
}
return true;
}