← More about keyboards

Overview

This post describes a QMK macro for a button that selects the current word, assuming conventional text editing hotkeys. Pressing it again extends the selection to the following word. The effect is similar to word selection (W) in the Kakoune editor.

Effect of pressing the macro repeatedly.

Similarly, pressing the button with shift selects the current line, and pressing the button again extends the selection to the following line.

Add it to your keymap

If you are new to QMK macros, see my macro buttons post for an intro.

Step 1: In your keymap.c, add a custom keycode for activating the macro and use the new keycode somewhere in your layout. I’ll name it SELWORD, but you can call it anything you like.

enum custom_keycodes {
  SELWORD = SAFE_RANGE,
  // Other custom keys...
};

Step 2: Handle the macro from your process_record_user function by calling process_select_word, passing your custom keycode as the third argument:

#include "features/select_word.h"

bool process_record_user(uint16_t keycode, keyrecord_t* record) {
  if (!process_select_word(keycode, record, SELWORD)) { return false; }
  // Your macros ...

  return true;
}

Step 3: In your rules.mk file, add SRC += select_word.c

Step 4: In the directory containing your keymap.c, create a features subdirectory and copy select_word.h and select_word.c there. This is the meat of the implementation.

Note for Mac users: The implementation assumes Windows/Linux editing hotkeys by default. Uncomment the #define MAC_HOTKEYS line in select_word.c for Mac hotkeys. The Mac implementation is untested, let me know if it has problems.

select_word.h

// Copyright 2021 Google LLC.
// SPDX-License-Identifier: Apache-2.0

#pragma once

#include QMK_KEYBOARD_H

bool process_select_word(uint16_t keycode, keyrecord_t* record,
                         uint16_t sel_keycode);

select_word.c

// Copyright 2021 Google LLC.
// SPDX-License-Identifier: Apache-2.0

#include "select_word.h"

// Mac users, uncomment this line:
// #define MAC_HOTKEYS

enum {
  STATE_NONE, STATE_SELECTED, STATE_WORD, STATE_FIRST_LINE, STATE_LINE
};

bool process_select_word(uint16_t keycode, keyrecord_t* record,
                         uint16_t sel_keycode) {
  static uint8_t state = STATE_NONE;

  if (keycode == KC_LSFT || keycode == KC_RSFT) { return true; }

  if (keycode == sel_keycode && record->event.pressed) {  // On key press.
    const uint8_t mods = get_mods();
    if (!((mods | get_oneshot_mods()) & MOD_MASK_SHIFT)) {  // Select word.
#ifdef MAC_HOTKEYS
      register_code(KC_LALT);
#else
      register_code(KC_LCTL);
#endif
      if (state == STATE_NONE) {
        SEND_STRING(SS_TAP(X_RGHT) SS_TAP(X_LEFT));
      }
      register_code(KC_LSFT);
      register_code(KC_RGHT);
      state = STATE_WORD;
    } else {  // Select line.
      if (state == STATE_NONE) {
        clear_mods();
        clear_oneshot_mods();
#ifdef MAC_HOTKEYS
        SEND_STRING(SS_LCTL("a" SS_LSFT("e")));
#else
        SEND_STRING(SS_TAP(X_HOME) SS_LSFT(SS_TAP(X_END)));
#endif
        set_mods(mods);
        state = STATE_FIRST_LINE;
      } else {
        register_code(KC_DOWN);
        state = STATE_LINE;
      }
    }
    return false;
  }

  // `sel_keycode` was released, or another key was pressed.
  switch (state) {
    case STATE_WORD:
      unregister_code(KC_RGHT);
      unregister_code(KC_LSFT);
#ifdef MAC_HOTKEYS
      unregister_code(KC_LALT);
#else
      unregister_code(KC_LCTL);
#endif
      state = STATE_SELECTED;
      break;

    case STATE_FIRST_LINE:
      state = STATE_SELECTED;
      break;

    case STATE_LINE:
      unregister_code(KC_DOWN);
      state = STATE_SELECTED;
      break;

    case STATE_SELECTED:
      if (keycode == KC_ESC) {
        tap_code(KC_RGHT);
        state = STATE_NONE;
        return false;
      }
      // Fallthrough.
    default:
      state = STATE_NONE;
  }

  return true;
}

Explanation

The macro checks for events involving sel_keycode. For word selection, the first press of the macro sends the keys Ctrl+→, Ctrl+← to move the cursor to the beginning of the word, then holds Ctrl+Shift+→ to select to the end of the word. On subsequent presses, Ctrl+Shift+→ is pressed again to extend the selection to the next word.

For line selection, the macro sends Home, Shift+End on the first press, then on subsequent presses.

The state variable keeps track of whether the macro has done the initial press and whether it is making a word vs. line selection.

← More about keyboards