QMK macros 1: intro and assortment of practical examples
Pascal Getreuer, 2021-10-30 (updated 2024-10-16)
⚠ 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:
- QMK macros 1: intro and assortment of practical examples ← this post
- QMK macros 2: triggers, reacting to interesting events
- 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 {
= SAFE_RANGE,
UPDIR };
Step 2: Use these keycodes in your layout, just as you would any other keycode:
[BASE] = LAYOUT(
, KC_1 , KC_2 , KC_3 , KC_4 , KC_5 , ... UPDIR
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 {
= SAFE_RANGE,
UPDIR ,
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:
record->event.pressed
is true if the event is a press (key down), and false if it is a release (key up). In the example above, we only take action on the press event.record->event.time
is a 16-bit timestamp in units of milliseconds of when the event occurred (see also Software Timers and Deferred Execution).record->event.key.row
andrecord->event.key.col
are the matrix position of the key, counting from 0.- If the event comes from a mod-tap (
MT
) or layer-tap (LT
) key, thenrecord->tap.count
is zero if the key is considered held, otherwise it is considered tapped (see also Intercepting Mod-Taps and Achordion).
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:
get_mods()
returns a bitfield of which modifiers are currently active, and similarlyget_oneshot_mods()
for one-shot modifiers (see also Checking Modifier State and the next section, Macros that respond to mods).- Get the layer associated with the key event:
uint8_t layer = read_source_layers_cache(record->event.key);
IS_LAYER_ON(layer)
returns whetherlayer
is currently on, andget_highest_layer(layer_state)
returns the highest layer that is currently on (see also Working with Layers).get_repeat_key_count()
indicates whether the key is being invoked through Repeat Key or Alternate Repeat Key (see also Repeat Key functions).
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.
(MOD_MASK_SHIFT);
del_oneshot_mods(MOD_MASK_SHIFT);
unregister_mods("=>");
SEND_STRING(mods); // Restore mods.
register_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(MOD_MASK_CSAG);
unregister_mods// Do stuff...
(mods); register_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) {
(); // Temporarily disable mods.
clear_oneshot_mods(MOD_MASK_CSAG);
unregister_modsif ((mods | oneshot_mods) & MOD_MASK_SHIFT) {
("{}");
SEND_STRING} else if ((mods | oneshot_mods) & MOD_MASK_CTRL) {
("<>");
SEND_STRING} else {
("[]");
SEND_STRING}
(KC_LEFT); // Move cursor between braces.
tap_code(mods); // Restore mods.
register_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:
SS_LCTL(string)
= left CtrlSS_LSFT(string)
= left ShiftSS_LALT(string)
= left Alt (Option key ⌥ on Mac)SS_LGUI(string)
= left GUI (Win key on Windows, Cmd key ⌘ on Mac)
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) {
(SS_TAP(X_BTN1) SS_DELAY(50) SS_TAP(X_BTN1));
SEND_STRING}
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) {
(SS_LCTL(SS_TAP(X_RGHT) SS_LSFT(SS_TAP(X_LEFT))));
SEND_STRING// 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) {
(SS_TAP(X_HOME) SS_LSFT(SS_TAP(X_END)));
SEND_STRING// 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.
(SS_LCTL("ct") SS_DELAY(100) SS_LCTL("v") SS_TAP(X_ENTER));
SEND_STRING}
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) {
( // Go to the end of the line and tap delete.
SEND_STRING(X_END) SS_TAP(X_DEL)
SS_TAP// In case this has joined two words together, insert one space.
(X_SPC)
SS_TAP(
SS_LCTL// Go to the beginning of the next word.
(X_RGHT) SS_TAP(X_LEFT)
SS_TAP// 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_TAP(X_LEFT) SS_TAP(X_RGHT)))
SS_LSFT// Replace the selection with a single space.
(X_SPC));
SS_TAP}
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(MOD_BIT(KC_LSFT)); // Set one-shot mod for shift.
add_oneshot_mods}
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:
QMK macros 2: triggers, reacting to interesting events: react to layer changes, tap vs. long press, previously typed keys.
QMK macros 3: advanced effects: timing effects, random emojis, and more.