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:
- (
combo
in the example) This gives a name to the listener, for the event manager to refer back to it. - (
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 eventstruct
to the next listener.ZMK_EV_EVENT_HANDLED
: Stop propagating the eventstruct
to the next listener. The event manager still owns thestruct
's memory, so it will befree
d automatically. Do not free the memory in this function.ZMK_EV_EVENT_CAPTURED
: Stop propagating the eventstruct
to the next listener. The eventstruct
's memory is now owned by your code, so the event manager will not free the eventstruct
memory. Make sure your code will release or free the event at some point in the future. (Use theZMK_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);