← More about keyboards

Overview

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.

Obligatory 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.

License

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

Copyright 2021-2022 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 a couple simple macros. Add the following to your keymap.c file.

Step 1: Define keycodes for your macros:

enum custom_keycodes {
  UPDIR = SAFE_RANGE,
  EQ3X,
};

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

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

Step 3: 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;

    case EQ3X:  // Types triple equal ===
      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.

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 email. Macros could be used to type commands, say SEND_STRING("git status"), but for that I suggest shell aliases are a better solution. For instance in your .bashrc or .zshrc, add alias gs="git status".

You can also use tap_code(keycode) to tap an individual key. Or for even finer grained control, register_code(keycode) and unregister_code(keycode) to respectively hold 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, an event is generated. The event 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 to the event. Additionally, handlers may return false to block later handlers from receiving the event. The process_record_user() function is one such handler in the pipeline, enabling user code to customize what happens. See also Understanding QMK.

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, you can do a different action when the macro button is pressed with a mod. For instance, the following implements an ARROW button so that ARROW + shift 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?
        del_mods(MOD_MASK_SHIFT);  // Temporarily delete shift.
        del_oneshot_mods(MOD_MASK_SHIFT);
        SEND_STRING("=>");
        set_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. So without treatment, the shift would turn the intended arrow => into +>. We get around this by temporarily deleting shift from the mod state. Alternatively, we can turn off all mods temporarily with

clear_mods();
clear_oneshot_mods();
// Do stuff...
set_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_mods();  // Temporarily disable mods.
      clear_oneshot_mods();
      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.
      set_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.

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.

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.

Emojis

Julia Ebert’s blog notes that the way to type a multi-codepoint emoji is to implement it as a macro. The following types a US flag emoji 🇺🇸:

case US_FLAG:  // Types US flag emoji.
  if (record->event.pressed) {
    // UTF-8 encoding of U+1F1FA, U+1F1F8.
    send_unicode_string("\xf0\x9f\x87\xba\xf0\x9f\x87\xb8");
  }
  return false;

Note: the above assumes UNICODEMAP_ENABLE = yes is set in the rules.mk file. You’ll also need to define UNICODE_SELECTED_MODES in config.h according to your OS.

Triggers

See my following post about triggers: react to layer changes, tap vs. long press, previously typed keys, and more.

← More about keyboards