← More about keyboards

⚠  Warning

Don’t use macros to type passwords or credit card numbers. Otherwise if someone has physical access to your keyboard, they can access this info.

Overview

This post is first in a series on interesting effects in QMK:

  1. QMK macros 1: intro and assortment of practical examples ← this post
  2. QMK macros 2: triggers, reacting to interesting events
  3. QMK macros 3: advanced effects

One of QMK’s most empowering features is the ability to define macro buttons: a button, that when pressed, types a sequence of keys. This page is an assortment of practical QMK macros, collected from real users’ keymaps.

License

Code snippets in this post are shared under Apache 2 license.

Copyright 2021-2024 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.

Simple macros

First, to establish the basic pattern, here are instructions for implementing an example simple macro.

Step 1: In your keymap.c file above the keymap definition, add the following to define a custom keycode “UPDIR” with the value SAFE_RANGE:

enum custom_keycodes {
  UPDIR = SAFE_RANGE,
};

Step 2: Use these keycodes in your layout, just as you would any other keycode:

[BASE] = LAYOUT(
  UPDIR  , KC_1   , KC_2   , KC_3   , KC_4   , KC_5   , ...

Step 3: Also in keymap.c, create (or add to) your process_record_user function to define the behavior for these buttons:

bool process_record_user(uint16_t keycode, keyrecord_t* record) {
  switch (keycode) {
    case UPDIR:  // Types ../ to go up a directory on the shell.
      if (record->event.pressed) {
        SEND_STRING("../");
      }
      return false;
  }
  return true;
}

See also the QMK macros documentation.

With the above implementation, pressing the UPDIR button on the keyboard has the effect of typing ../, for “up directory” when in the terminal. This is my favorite macro! I can mash it a few times (../../../file) to refer to files several levels up. It might sound small, but from experience I can say macros like this add up when it’s something you use a lot or awkward to type manually.

Additional macros

To implement additional macros, extend the custom_keycodes enum to define additional keycodes:

enum custom_keycodes {
  UPDIR = SAFE_RANGE,
  EQ3X,
  // Other keycodes...
};

This defines a keycode called “EQ3X” with value (SAFE_RANGE + 1). Add handlers for the new keycodes in process_record_user():

bool process_record_user(uint16_t keycode, keyrecord_t* record) {
  switch (keycode) {
    case UPDIR:  // Types ../ to go up a directory on the shell.
      if (record->event.pressed) {
        SEND_STRING("../");
      }
      return false;

    case EQ3X:  // Types triple equal ===
      if (record->event.pressed) {
        SEND_STRING("===");
      }
      return false;

    // Other macros...
  }
  return true;
}

SEND_STRING

Use SEND_STRING to type a sequence of keys when the macro button is pressed. You can use this to blast out programming syntax like -> or === or patterns in the languages you use. For instance, typing the C++ scope operator :: in most keyboard layouts is an uncomfortable shifted double tap on a pinky key. So a well-used feature in my QMK keymap is a macro that types ::.

Macros are also useful for typing common strings, like your name or email. Macros could be used to type commands, say SEND_STRING("git status"), but for that I suggest shell aliases are a better solution. In your .bashrc or .zshrc, add alias gs="git status".

Again, don’t use macros to type passwords or credit card numbers. Otherwise if someone has physical access to your keyboard, they can access this info.

SEND_STRING has some gotchas when it comes to special characters. See SEND_STRING doesn’t work for troubleshooting tips.

You can also use tap_code(kc) to tap an individual key, where kc is a basic keycode. Or for even finer grained control, register_code(kc) and unregister_code(kc) to respectively press down and release a key.

process_record_user() in depth

process record user in depth

Before we dive into more interesting macros, let’s go over what the process_record_user() function is.

bool process_record_user(uint16_t keycode, keyrecord_t* record) {
  // Macros...
  return true;
}

The big picture: QMK’s main loop scans the key matrix to watch for changes. When a button is pressed or released, a “keyrecord” representing the event is generated. The keyrecord enters a pipeline of handlers. A handler function runs for each QMK feature (Mod-Tap, Combos, Auto Shift, Tap Dance, …), one after another, so that each feature a chance to react. Additionally, handlers may return false to block later handlers from running. The process_record_user() function is one such handler in the pipeline, enabling user code to customize what happens. See also the Understanding QMK documentation page.

Function arguments: The keycode arg is the keycode as in your keymap that was pressed or released, like UPDIR or KC_A. The record arg contains additional information about the event:

Return value: The return value from process_record_user() tells QMK whether to continue handling the event. Returning false from process_record_user() means the remaining handlers in the pipeline should be skipped. We return false for events on our macro buttons, since as they are custom keycodes, QMK’s default handling will ignore them anyway. Otherwise, we return true so that QMK handles the event.

Other interesting state:

Macros that respond to mods

Arrow macro, types -> or =>

For extra functionality on one key, you can perform a different action when the macro button is pressed with a modifier. For instance, the following implements an ARROW button so that Shift + ARROW types => and ARROW by itself types ->.

// Get current mod and one-shot mod states.
const uint8_t mods = get_mods();
const uint8_t oneshot_mods = get_oneshot_mods();

switch (keycode) {
  case ARROW:  // Arrow macro, types -> or =>.
    if (record->event.pressed) {
      if ((mods | oneshot_mods) & MOD_MASK_SHIFT) {  // Is shift held?
        // Temporarily delete shift.
        del_oneshot_mods(MOD_MASK_SHIFT);
        unregister_mods(MOD_MASK_SHIFT);  
        SEND_STRING("=>");
        register_mods(mods);            // Restore mods.
      } else {
        SEND_STRING("->");
      }
    }
    return false;
}

The tricky part is that Shift or any other active mods will apply to the keys sent by SEND_STRING. Without treatment, the Shift would turn the intended arrow => into +>. We get around this by temporarily deleting Shift from the mod state. Or, we could turn off all mods temporarily with

clear_oneshot_mods();
unregister_mods(MOD_MASK_CSAG);
// Do stuff...
register_mods(mods);

Braces macro, types [], {}, or <>

From draevin’s user directory, the following types [, then ], and taps the left arrow to position the cursor between the []. Or if shift is held, it does the same but with curly braces {}. Or with Ctrl, it does angle brackets <>.

const uint8_t mods = get_mods();
const uint8_t oneshot_mods = get_oneshot_mods();

switch (keycode) {
  case BRACES:  // Types [], {}, or <> and puts cursor between braces.
    if (record->event.pressed) {
      clear_oneshot_mods();  // Temporarily disable mods.
      unregister_mods(MOD_MASK_CSAG);
      if ((mods | oneshot_mods) & MOD_MASK_SHIFT) {
        SEND_STRING("{}");
      } else if ((mods | oneshot_mods) & MOD_MASK_CTRL) {
        SEND_STRING("<>");
      } else {
        SEND_STRING("[]");
      }
      tap_code(KC_LEFT);  // Move cursor between braces.
      register_mods(mods);  // Restore mods.
    }
    return false;
}

Custom shift keys

An especially handy use of get_mods() is to change what key is typed when it is shifted. For instance, a key with “inverted” shifting such that it types : when pressed normally and ; when pressed shifted, or implementing “programmer” layouts that type symbols normally and digits when shifted. See Custom shift keys for an implementation.

Macros with special keys

SEND_STRING is not limited to printable characters. To tap the left arrow key, for instance, do SEND_STRING(SS_TAP(X_LEFT)). Within SEND_STRING, the key name begins with X_ instead of KC_, otherwise keys have the same names as elsewhere.

Modifiers can be applied:

A few examples are described next.

Mouse double click

Enable Mouse Keys for this macro with MOUSEKEY_ENABLE = yes in rules.mk.

This macro double clicks the left mouse button:

case DBLCLK:  // Double click the left mouse button.
  if (record->event.pressed) {
    SEND_STRING(SS_TAP(X_BTN1) SS_DELAY(50) SS_TAP(X_BTN1));
  }
  return false;

The OS might debounce and ignore a double click that is too fast. To prevent this, the SS_DELAY(50) defines a delay in milliseconds between the clicks.

Select word macro

Most text editors support Ctrl+left / Ctrl+right to move the cursor by words. From ajp10304’s user directory, this macro selects the current word. It types Ctrl+right to move to the end of the word, then Ctrl+Shift+left to select to the beginning of the word:

case SELWORD:  // Selects the current word under the cursor.
  if (record->event.pressed) {
    SEND_STRING(SS_LCTL(SS_TAP(X_RGHT) SS_LSFT(SS_TAP(X_LEFT))));
    // Mac users, change LCTL to LALT:
    // SEND_STRING(SS_LALT(SS_TAP(X_RGHT) SS_LSFT(SS_TAP(X_LEFT))));
  }
  return false;

See also my Word Selection post for an elaborated version of this macro.

Select line macro

This macro selects the current line, by tapping the home key and then Shift+End:

case SELLINE:  // Selects the current line.
  if (record->event.pressed) {
    SEND_STRING(SS_TAP(X_HOME) SS_LSFT(SS_TAP(X_END)));
    // Mac users, use:
    // SEND_STRING(SS_LCTL("a" SS_LSFT("e")));
  }
  return false;

Web search for current selection

Suppose you are in a web browser and have selected some text. This macro copies the text and does a web search for that text in a new tab.

case SRCHSEL:  // Searches the current selection in a new tab.
  if (record->event.pressed) {
    // Mac users, change LCTL to LGUI.
    SEND_STRING(SS_LCTL("ct") SS_DELAY(100) SS_LCTL("v") SS_TAP(X_ENTER));
  }
  return false;

In detail, it copies the selection (Ctrl+C), opens a new tab (Ctrl+T), pastes the selection into the omnibar (Ctrl+V), and finally taps enter to do the search. Since the browser might need a moment to open the new tab, SS_DELAY is used to wait briefly before pasting.

Join lines

The following works in most text editors to join the current line with the following line, similar to Vim’s “J” command. It presses End, Delete, and replaces intermediate whitespace with a space:

case JOINLN:  // Join lines like Vim's `J` command.
  if (record->event.pressed) {
    SEND_STRING( // Go to the end of the line and tap delete.
        SS_TAP(X_END) SS_TAP(X_DEL)
        // In case this has joined two words together, insert one space.
        SS_TAP(X_SPC)
        SS_LCTL(
          // Go to the beginning of the next word.
          SS_TAP(X_RGHT) SS_TAP(X_LEFT)
          // Select back to the end of the previous word. This should select
          // all spaces and tabs between the joined lines from indentation
          // or trailing whitespace, including the space inserted earlier.
          SS_LSFT(SS_TAP(X_LEFT) SS_TAP(X_RGHT)))
        // Replace the selection with a single space.
        SS_TAP(X_SPC));
  }
  return false;

Note: I’ve noticed slight differences between text editors in how hotkeys work at the end of the line. This macro might produce incorrect results in some editors.

Other cool ideas

Next sentence macro

Inspired by precondition’s keymap, here is a “next sentence” macro. This is useful to flow between sentences. Having tried it, I admit it takes some practice to incorporate into your typing habits, but it packs a satisfying amount of action into a single button press. The macro types a period, space, then sets a one-shot mod so that the next key typed is shifted, to capitalize the first letter of the next sentence:

case NEXTSEN:  // Next sentence macro.
  if (record->event.pressed) {
    SEND_STRING(". ");
    add_oneshot_mods(MOD_BIT(KC_LSFT));  // Set one-shot mod for shift.
  }
  return false;

See also the approach taken in precondition’s keymap: there, the next sentence is triggered by double tapping the period button through a QMK tap dance, rather than using a macro.

And see also Sentence Case, which detects and capitalizes the first letter of sentences automatically.

Multi-codepoint emoji

Enable a Unicode input method for this macro, either Basic Unicode, Unicode Map, or UCIS; see Unicode Support. See also Typing non-English letters.

Julia Ebert’s blog notes that the only way to type a multi-codepoint emoji is to implement it as a macro. The following types a US flag emoji 🇺🇸 (U+1F1FA, U+1F1F8):

case US_FLAG:  // Types US flag emoji.
  if (record->event.pressed) {
    send_unicode_string("🇺🇸");
    // Or if you prefer ASCII-only source files, use escape codes to
    // write the UTF-8 encoding of U+1F1FA, U+1F1F8:
    // send_unicode_string("\xf0\x9f\x87\xba\xf0\x9f\x87\xb8");
  }
  return false;

More macros

See the following posts in this series:

← More about keyboards