Skip to main content

ZMK Events

ZMK makes use of events to decouple individual components such as behaviors and peripherals from the core functionality. For this purpose, ZMK has implemented its own event manager. This page is a (brief) overview of the functionality and methods exposed by the event manager, documenting its API. Its purpose is to aid module developers and contributors, such as for the development of new behaviors or new features. There is no value in reading this page as an end-user.

To see what events exist and what data they contain, it is best to view the corresponding event header files directly. Including the event_manager header via #include <zmk/event_manager.h> is required for any interaction with the event system.

Generic Events

The generic event type is struct zmk_event_t. This struct looks like this:

typedef struct {
const struct zmk_event_type *event;
uint8_t last_listener_index;
} zmk_event_t;

In memory, the struct for a specific raised event struct zmk_specific_thing_happened_event always consists of a zmk_event_t struct followed immediately afterwards by the struct containing the data for the actual event.

struct zmk_specific_thing_happened_event {
zmk_event_t header;
struct zmk_specific_thing_happened data;
};

The contents of header.event allows us to identify which type a particular event actually is, so that we may safely access its data in data. This is handled by the following function, which allows us to obtain the underlying data from a generic event:

struct zmk_specific_thing_happened *as_specific_thing_happened(const zmk_event_t *eh);

This method takes in a pointer to a zmk_event_t (which is actually a pointer to a specific event, such as zmk_specific_thing_happened_event), and will return the underlying zmk_specific_thing_happened data struct if the zmk_event_t header indicates that the generic event pointer is indeed a pointer to a zmk_specific_thing_happened_event. If the type of the event does not match the function, then the function will return NULL. By convention, zmk_event_t pointer arguments are named eh, short for "event header".

This method will exist for every type of event, so for zmk_layer_state_changed we have as_zmk_layer_state_changed, etc. It is generated by a macro as part of the event declaration.

Subscribing To Events

Subscription and Listener

To subscribe to any events, you will first need to inform the event manager that you wish to add a new listener. This is done by calling the ZMK_LISTENER macro:

ZMK_LISTENER(combo, behavior_combo_listener);

This macro takes two parameters:

  1. (combo in the example) This gives a name to the listener, for the event manager to refer back to it.
  2. (behavior_combo_listener in the example) This is a callback that will be called whenever any event that the listener subscribes to occurs (if it is not handled by another listener with a higher priority). By convention, the callback should have the suffix _listener.

Once you have a listener set up, you can subscribe to individual events by calling the ZMK_SUBSCRIPTION macro:

ZMK_SUBSCRIPTION(combo, zmk_keycode_state_changed);

The first parameter is the name of the listener created with ZMK_LISTENER, while the second is the name of the struct that defines the event's data, which was declared in the corresponding header file. By convention the header file for an event will be named specific_thing_happened, with the struct named zmk_specific_thing_happened.

Of course, you will also need to import the corresponding event header at the top of your file.

Listener Callback

The listener will be passed a raised zmk_event_t pointer (as described previously) as an argument, and should have int as its return type.

The listener should return one of three values (which are of type int) back to the event manager:

  • ZMK_EV_EVENT_BUBBLE: Keep propagating the event struct to the next listener.
  • ZMK_EV_EVENT_HANDLED: Stop propagating the event struct to the next listener. The event manager still owns the struct's memory, so it will be freed automatically. Do not free the memory in this function.
  • ZMK_EV_EVENT_CAPTURED: Stop propagating the event struct to the next listener. The event struct's memory is now owned by your code, so the event manager will not free the event struct memory. Make sure your code will release or free the event at some point in the future. (Use the ZMK_EVENT_* macros described below.)

If an error occurs during the listener call, it should return a negative value indicating the appropriate error code.

As mentioned previously, the same callback will be called when any event that is subscribed to occurs. To obtain the underlying event from the generic event passed to the listener, the previously described as_zmk_specific_thing_happened function should be used:

int behavior_hold_tap_listener(const zmk_event_t *eh) {
if (as_zmk_position_state_changed(eh) != NULL) {
// it is a position_state_changed event, handle it with my_position_state_handler
return my_position_state_handler(eh);
} else if (as_zmk_keycode_state_changed(eh) != NULL) {
// it is a keycode_state_changed event, handle it with my_keycode_state_handler
return my_keycode_state_handler(eh);
}
return ZMK_EV_EVENT_BUBBLE;
}

The priority of the listeners is determined by the order in which the linker links the files. Within ZMK, this is the order of the corresponding files in CMakeLists.txt. External modules targeting app are linked prior to any files within ZMK itself, making them the highest priority. It is thus the module maintainer's responsibility to both ensure that their module does not cause issues by being first in the listener queue. For example, hold-tap is the first listener to position_state_changed, and may behave inconsistently if a behavior defined in a module listens to position_state_changed and invokes a hold-tap (e.g. by calling zmk_behavior_invoke_event with a hold-tap as the binding).

In addition, because modules listen to the events first, they should never capture/handle an event defined in ZMK without releasing it later. Unless it is unavoidable, it is recommended to bubble events whenever possible.

When considering multiple modules, priority is determined by the order in which the modules are present in the user's west.yml. Hence there should be no order dependencies between modules, only within a module.

Raising Events

There are several different ways to raise events, with slight differences between them.

  • int raise_zmk_specific_thing_happened(struct zmk_specific_thing_happened event): This function will take an event data structure, add a header to it, and then start handling the event with the first registered event listener.

The following macros can also be used for advanced use cases. These will each take in an event ev which already consists of the header & data combination, i.e. ev has the type struct zmk_specific_thing_happened_event.

  • ZMK_EVENT_RAISE(ev): Start handling this event (ev) with the first registered event listener.
  • ZMK_EVENT_RAISE_AFTER(ev, mod): Start handling this event (ev) after the event is captured by the named event listener (mod). The named event listener will be skipped as well.
  • ZMK_EVENT_RAISE_AT(ev, mod): Start handling this event (ev) at the named event listener (mod). The named event listener is the first handler to be invoked.
  • ZMK_EVENT_RELEASE(ev): Continue handling this event (ev) at the next registered event listener.
  • ZMK_EVENT_FREE(ev): Free the memory associated with the event (ev).

Optionally, some events may also declare an extra function similar to raise_zmk_specific_thing_happened named raise_specific_thing_happened. This function will take in some or all of the components of the zmk_specific_thing_happened struct, and then create the struct (perhaps with some additional data obtained from elsewhere) before calling raise_zmk_specific_thing_happened. For example:

static inline int raise_layer_state_changed(uint8_t layer, bool state) {
return raise_zmk_layer_state_changed(
(struct zmk_layer_state_changed){
.layer = layer,
.state = state,
.timestamp = k_uptime_get()
}
);
}

Creating New Events

Header File

Your event's header file should have four things:

  • A copyright comment
  • Any required header includes (along with #pragma once)
  • The event's data struct
  • The macro ZMK_EVENT_DECLARE, called with the name of your event's data struct.

For example:

/*
- Copyright (c) 2021 The ZMK Contributors
-
- SPDX-License-Identifier: MIT
*/

#pragma once

#include <zephyr/kernel.h>

#include <zmk/endpoints_types.h>
#include <zmk/event_manager.h>

struct zmk_endpoint_changed {
struct zmk_endpoint_instance endpoint;
};

ZMK_EVENT_DECLARE(zmk_endpoint_changed);

Code File

Your event's code file merely needs three things:

  • A copyright comment
  • Any required header files (including that of your event)
  • The macro ZMK_EVENT_IMPL, called with the name of your event's data struct.
/*
* Copyright (c) 2021 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/

#include <zephyr/kernel.h>
#include <zmk/events/endpoint_changed.h>

ZMK_EVENT_IMPL(zmk_endpoint_changed);