This commit is contained in:
Christoph Cullmann 2025-03-18 22:27:19 +01:00
parent 4dc5811f00
commit ee0a3c3be0
No known key found for this signature in database
61 changed files with 5743 additions and 0 deletions

View file

@ -0,0 +1,68 @@
# Select Word
<table>
<tr><td><b>Module</b></td><td><tt>getreuer/select_word</tt></td></tr>
<tr><td><b>Version</b></td><td>2025-03-07</td></tr>
<tr><td><b>Maintainer</b></td><td>Pascal Getreuer (@getreuer)</td></tr>
<tr><td><b>License</b></td><td><a href="../LICENSE.txt">Apache 2.0</a></td></tr>
<tr><td><b>Documentation</b></td><td>
<a href="https://getreuer.info/posts/keyboards/select-word">https://getreuer.info/posts/keyboards/select-word</a>
</td></tr>
</table>
This is a community module adaptation of [Select
Word](https://getreuer.info/posts/keyboards/select-word) for selecting words and
lines, assuming conventional text editing hotkeys.
## Use
Add the following to your `keymap.json`:
```json
{
"modules": ["getreuer/select_word"]
}
```
Then use one or more of the following keycodes in your layout:
| Keycode | Short alias | Description |
|--------------------|-------------|--------------------------------------------------------|
| `SELECT_WORD` | `SELWORD` | Forward word selection. Or with Shift, line selection. |
| `SELECT_WORD_BACK` | `SELWBAK` | Backward word selection. |
| `SELECT_WORD_LINE` | `SELLINE` | Line selection. |
Press `SELWORD` to select the current word. Press it again to extend the
selection to the following word. The effect is similar to word selection (`W`)
in the [Kakoune editor](https://kakoune.org). Or for line selection, press
`SELWORD` with shift to select the current line, and press it again to extend
the selection to the following line.
## Mac hotkeys
Different hotkeys are needed to perform word and line selection on Mac OS. There
are several ways that Select Word can be configured to send the appropriate
hotkeys:
* To hardcode to Mac hotkeys, define in your `config.h` file:
~~~{.c}
#define SELECT_WORD_OS_MAC
~~~
* If [OS Detection](https://docs.qmk.fm/features/os_detection) is enabled,
Select Word uses it determine which kind of hotkeys to send.
* For direct control, define in `config.h`:
~~~{.c}
#define SELECT_WORD_OS_DYNAMIC
~~~
Then in `keymap.c`, define the callback `select_word_host_is_mac()`. Return
true for Mac hotkeys, false for Windows/Linux.
See the [Select Word
documentation](https://getreuer.info/posts/keyboards/select-word) for further
configuration options and details.

View file

@ -0,0 +1,17 @@
// Copyright 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.
#pragma once
#include "select_word.h"

View file

@ -0,0 +1,24 @@
{
"module_name": "Select Word",
"maintainer": "getreuer",
"keycodes": [
{
"key": "SELECT_WORD",
"aliases": [
"SELWORD"
]
},
{
"key": "SELECT_WORD_BACK",
"aliases": [
"SELWBAK"
]
},
{
"key": "SELECT_LINE",
"aliases": [
"SELLINE"
]
}
]
}

View file

@ -0,0 +1,301 @@
// 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;
}

View file

@ -0,0 +1,91 @@
// 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.h
* @brief Select Word community module: select words and lines.
*
* Overview
* --------
*
* Implements a button that selects the current word, assuming conventional text
* editor hotkeys. Pressing it again extends the selection to the following
* word. The effect is similar to word selection (W) in the Kakoune editor.
*
* Pressing the button with shift selects the current line, and pressing the
* button again extends the selection to the following line.
*
* For full documentation, see
* <https://getreuer.info/posts/keyboards/select-word>
*/
#pragma once
#include "quantum.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Registers (presses) selection `action`.
*
* The `action` argument in these functions specifies the type of selection:
*
* 'W' = word selection
* 'B' = backward word selection, left of the cursor
* 'L' = line selection
*
* A selection is first registered with `select_word_register(action)`. This
* should be followed by a call to `select_word_unregister()` to unregister the
* hotkeys. The point of these separate register and unregister calls is to
* enable holding the hotkey as a means to extend the selection range.
*
* @warning Forgetting to unregister results in stuck keys:
* `select_word_register()` must be followed by `select_word_unregister()`.
*
* @param action Type of selection to perform.
*/
void select_word_register(char action);
/** Unregisters (releases) selection hotkey. */
void select_word_unregister(void);
/** Registers and unregisters ("taps") selection `action.` */
static inline void select_word_tap(char action) {
select_word_register(action);
wait_ms(TAP_CODE_DELAY);
select_word_unregister();
}
#if defined(SELECT_WORD_OS_DYNAMIC) || defined(OS_DETECTION_ENABLE)
/**
* @brief Callback for whether the host uses Mac vs. Windows/Linux hotkeys.
*
* Optionally, this callback may be defined to indicate dynamically whether the
* keyboard is being used with a Mac or non-Mac system.
*
* For instance suppose layer 0 is your base layer for Windows and layer 1 is
* your base layer for Mac. Indicate this by adding in keymap.c:
*
* bool select_word_host_is_mac(void) {
* return IS_LAYER_ON(1); // Supposing layer 1 = base layer for Mac.
* }
*/
bool select_word_host_is_mac(void);
#endif // defined(SELECT_WORD_OS_DYNAMIC) || defined(OS_DETECTION_ENABLE)
#ifdef __cplusplus
}
#endif