Custom UI
Introduction
The built-in menus of VIA (Keymap
, Layouts
, Macros
, Save + Load
) will be displayed depending on keyboard definition and firmware.
The menus
element is used to define more menus in VIA. It can contain one or more of the following built-in UI definitions:
and/or a definition of custom UI, i.e. explicitly defining all the UI controls required.
For example, a definition enabling the built-in UI for QMK RGB Matrix could be done like so:
"menus": ["qmk_rgb_matrix" ]
or alternatively defined explicitly using custom UI definitions, like so:
...
"menus": [
{
"label": "Lighting",
"content": [
{
"label": "Backlight",
"content": [
{
"label": "Brightness",
"type": "range",
"options": [0, 255],
"content": ["id_qmk_rgb_matrix_brightness", 3, 1]
},
...
]
}
]
}
]
The "qmk_backlight"
, "qmk_rgblight"
, "qmk_rgb_matrix"
, "qmk_backlight_rgblight"
and "qmk_audio"
strings enable the built-in UI definitions that match the default implementation of VIA protocol handlers in QMK, with respect to channel_id
, value_id
, etc.
These built-in UI definitions are defined the same way as custom UI definitions (i.e. JSON format). They can be used as examples to create custom UI definitions.
Definitions containing multiple top level menus with the same name (e.g. Lighting
) will not merge into one top level menu. Thus, combining "qmk_backlight"
and "qmk_rgblight"
will result in two top-level Lighting
menus. Use "qmk_backlight_rgblight"
instead.
Similarly, combining built-in UI definitions with custom UI that has the same top-level menu name will result in multiple top-level menus, not a merged one. If you want to combine a built-in UI like qmk_rgb_matrix
with extra lighting sub-menus under a single Lighting
top level menu, explicitly define all the UI controls using the built-in UI definition as the base.
Structure
A custom UI definition (as a child element of menu
) consists of a single top-level menu element with label
and content
elements.
A top-level menu element must contain one or more sub-menu elements as an array in content
.
Each sub-menu element consists of a label
and content
element, and must contain one or more UI control elements as an array in the content
element.
Each UI control element must have label
, type
and content
elements.
Thus the "tree" has a maximum depth of three: top-level menu, sub-menu and UI control, and the type of entity is inferred from the depth.
Top Level Menus and Sub-Menus
Each element in the menus
element defines a top-level menu, either a built-in one or a custom defined one. When custom defined, label
defines the name displayed in the top left menu of the VIA app, and content
is an array of sub-menu elements.
The following example defines:
- a top level menu
Lighting
with sub-menusUnderglow
andIndicators
- a top level menu
Display
with sub-menusGeneral
andAdvanced
"menus": [
{
"label": "Lighting",
"content": [
{
"label": "Underglow",
...
},
{
"label": "Indicators",
...
}
]
},
{
"label": "Display",
"content": [
{
"label": "General",
...
},
{
"label": "Advanced",
...
}
]
}
]
VIA will then display Lighting
and Display
in the top left menu, and then display the sub-menus in the bottom left, like this:
Note: Top-level menus should be consistent across all keyboards in VIA.
For example, all custom UI defined for any kind of lighting should use a top-level menu called Lighting
, and then the sub-menus should be named with the specifics of that lighting, such as Underglow
, Buttglow®
, Backlight
, Indicators
.
Other top-level menus should be similarly generalized, such as Audio
, Display
, Knobs
, etc.
Top-level menus will be displayed with an icon based on the label
. The current list is:
"Lighting"
"Audio"
"Display"
Top level menus with a label
not in this list will be displayed with a generic icon.
In the case where a built-in definition requires some changes to match the firmware, the built-in definition can be inserted in place in the menus
element and modified. In most cases, the default handlers in the firmware can be used.
Examples of this are:
- Replacing the sub-menu name
Underglow
with a different name - Configuring the set of lighting effects (e.g. changing the array of string/number pairs)
- Adding a new sub-menu and controls to match keyboard-level code which overrides the default behaviour (e.g. customized indicators)
This is facilitated by the use of channel_id
in the set/get custom value commands. See #Channels
UI Controls
Definition
The sub-menu element has a content
element, which is an array of UI control elements. Each UI control element must have label
, type
and content
elements.
Example:
"menus": [
{
"label": "Lighting",
"content": [
{
"label": "Underglow",
"content": [
{
"label": "Brightness",
"type": "range",
"options": [0, 255],
"content": ["id_qmk_rgblight_brightness", 2, 1]
},
{
"label": "Effect",
"type": "dropdown",
"content": ["id_qmk_rgblight_effect", 2, 2],
"options": [
"All Off",
"Solid Color",
"Breathing 1",
...
]
}
]
}
]
}
]
label
defines the text label on the left of the control
type
defines the type of control. Valid values are:
button
toggle
range
dropdown
color
keycode
options
defines the possible values of the control (the numerical range, or string/integer values)
content
defines the binding of the control to the channel_id
and value_id
used in the VIA protocol. These values are preceeded by a value_key
string that is used by VIA. The value_key
should match the name of the enum value in the firmware for uniqueness and readability. It must be unique for all UI controls in a UI definition.
For example:
{
"label": "Brightness",
"type": "range",
"options": [0, 255],
"content": ["id_qmk_rgblight_brightness", 2, 1]
}
... defines a range (slider) UI control, labelled Brightness
, with a range of 0 to 255. It will be using channel_id
of 2
and value_id
of 1
in the VIA protocol to set/get the value in the firmware. It has a value_key
of "id_qmk_rgblight_brightness"
because the value_id
of 1
matches the integer value of id_qmk_rgblight_brightness
in enum id_qmk_rgblight
in the firmware code.
Button Control
The button control is a clickable that sends a numeric value.
{
"label": "Test button",
"type": "button",
"options": [10],
"content": ["id_test_button", 0, 3]
}
The options
element defines value to be sent, e.g. "options": [10],
. The value data is one byte (i.e. <=255), otherwise it is two bytes. See Handling 16-bit Values for handling two byte values.
Toggle Control
The toggle control is a toggle switch that controls a boolean (on/off) value.
{
"label": "God Mode",
"type": "toggle",
"content": ["id_god_mode", 0, 1]
}
The default value data is one byte, either 0
or 1
. The options
element can be used to define the two values if required, using an array of two values.
Range Control
The range control is a slider that controls a numeric value. The options
element defines the range limits, e.g. "options": [0, 255],
. The value data is one byte if the range maximum can fit in one byte (i.e. <=255), otherwise it is two bytes. See Handling 16-bit Values for handling two byte values.
{
"label": "Audacity",
"type": "range",
"options": [0, 255],
"content": ["id_audacity", 0, 2]
}
Dropdown Control
The dropdown control is a drop-down menu of strings, which maps to an integer value. The options
element defines the item labels and associated number.
{
"label": "Date Format",
"type": "dropdown",
"options": [
["yyyy-mm-dd", 0],
["dd/mm/yyyy", 1],
["mm/dd/yyyy", 2]
],
"content": ["id_date_format", 0, 5]
}
The numbers can be omitted from options
, in which case, the array is of strings and the numbers implicitly assigned starting at zero, for example:
"options": [
"yyyy-mm-dd",
"dd/mm/yyyy",
"mm/dd/yyyy"
]
This allows easier definition of things like lighting effects, which can be conditionally enabled. Firmware authors can copy the full set of strings from the built-in UI definition and edit it to match the firmware, without needing to define the numbers.
Color Control
The color control is a swatch of color showing the current color value, which pops up a color picker when clicked. It allows selection of hue and saturation values only, since brightness is usually controlled separately in lighting code.
{
"label": "Color",
"type": "color",
"content": ["id_qmk_rgblight_color", 2, 4]
}
Keycode Control
The keycode control allows display and editing of a QMK keycode. Firmware writers can use this for custom rotary encoder handling or any other way of triggering keycode events.
{
"label": "Head Slam",
"type": "keycode",
"content": ["id_head_slam", 0, 6]
}
Keycodes are 16-bit values, so the value data is two bytes. See Handling 16-bit Values
Showing/Hiding Controls
Sometimes good UI design requires only showing a control when another control is in a given state. For example, a dropdown control for a "mode" and only showing controls which are used in that "mode".
The showIf
element can be used to show one (or more) UI controls only if the expression evaluates true.
The built-in UI for qmk_rgblight
shows an example of showIf
. The Effect Speed
and Color
are only shown for some Effect
values. Note how the value of a dropdown control evaluates to the number value of the string/number pair.
Example:
{
"label": "Effect",
"type": "dropdown",
"content": ["id_qmk_rgblight_effect", 2, 2],
"options": [
"All Off",
"Solid Color",
"Breathing 1",
"Breathing 2",
"Breathing 3",
"Breathing 4",
...
]
},
{
"showIf": "{id_qmk_rgblight_effect} >= 2",
"label": "Effect Speed",
"type": "range",
"options": [0, 3],
"content": ["id_qmk_rgblight_effect_speed", 2, 3]
},
{
"showIf": "{id_qmk_rgblight_effect} > 0",
"label": "Color",
"type": "color",
"content": ["id_qmk_rgblight_color", 2, 4]
}
Alternatively, the showIf
element can "contain" one or more UI controls, which are only shown if the expression evaluates true.
For example, the following will only display the Audacity
and Tenacity
range controls if the God Mode
toggle is on.
{
"label": "God Mode",
"type": "toggle",
"content": ["id_god_mode", 0, 1]
},
{
"showIf": "{id_god_mode} == 1",
"content": [
{
"label": "Audacity",
"type": "range",
"options": [0, 255],
"content": ["id_audacity", 0, 2]
},
{
"label": "Tenacity",
"type": "range",
"options": [0, 255],
"content": ["id_tenacity", 0, 3]
}
]
}
...
The expression evaluator uses operators such as ==
, !=
, <
, <=
, >
, >=
, ||
, &&
, !
, (
, )
. Values are referenced by enclosing their key string in {
and }
.
Array Values
Sometimes firmware has a set of the same type of values, for which enumerating each in an enum, and using a range of value_id
values, is not elegant.
In this situation, one or more numbers can be added after the value_id
in the content
element, These can be used for any purpose in the firmware, such as an index to an array.
Example:
"content": ["id_some_value_array[0]", 9, 99, 0]
For example, to control 3 color values, rather than defining 3 enum values in firmware, a single enum value id_buttglow_color
can be used, a single integer value used for value_id
, and a value index appended in the UI control definition, as follows:
{
"label": "Color 1",
"type": "color",
"content": ["id_buttglow_color[0]", 0, 4, 0]
},
{
"label": "Color 2",
"type": "color",
"content": ["id_buttglow_color[1]", 0, 4, 1]
},
{
"label": "Color 3",
"type": "color",
"content": ["id_buttglow_color[2]", 0, 4, 2]
}
Note that the value_key
(the string in content
) must be unique per UI control, so for array values, include the array index in the string.
Firmware Implementation
Custom UI is handled in firmware by handling three commands of the VIA protocol:
id_custom_set_value
id_custom_get_value
id_custom_save
When enabling VIA and a feature (like lighting) in QMK Core, by default, the command handlers that match the built-in UI definitions will be compiled. Firmware authors who do not add or extend a feature do not need to write handlers of the above commands.
The following describes how to implement handlers for the above commands, in keyboard level code, to match the custom UI definitions in VIA.
Channel ID
id_custom_set_value
and id_custom_get_value
commands use a channel_id
for routing commands to handlers for that feature, and a value_id
to identify the value.
channel_id
is a qualifier to value_id
, allowing multiple enums with overlapping integer ranges to be used, rather than a single enum that must be the superset of all values for all features.
The built-in UI definitions use channel_id
values that match the default values in firmware. When using custom UI definitions, the channel_id
values can be defined to anything on both sides (QMK and VIA).
id_custom_save
command uses a channel_id
to signal saving the current state of values associated with that channel. See Saving Values
Value ID
When implementing a new feature, either in QMK Core or at the keyboard level, define a new enum for the possible value_id
values, explicitly assigning integer values starting with 1
.
For example:
enum via_buttglow_value {
id_buttglow_brightness = 1,
id_buttglow_effect = 2,
id_buttglow_effect_speed = 3,
id_buttglow_color = 4
};
Command Handlers
via_custom_value_command()
(in via.c
) has the default handling of custom value commands for QMK Core modules like lighting. It will use the channel_id
to route the commands to handlers specific to that feature. If the channel_id
in the command does not match the channels it is enabled to handle, it will call via_custom_value_command_kb()
, allowing keyboard level code to handle the commands for that channel_id
.
Thus, overriding via_custom_value_command_kb()
in the keyboard level code allows firmware authors to write a command handler to set/get the values defined by the custom UI definition in VIA.
In the simple case of implementing a few keyboard specific custom values, it is recommended to use a channel_id
of id_custom_channel = 0
, which won't conflict with the channels being handled in via.c
, as these start with 1
. However, firmware authors could choose to use multiple channel_id
values, to support multiple features.
It is possible to combine the default command handlers for a QMK feature (using the default channel_id
) with a command handler for a keyboard specific feature. For example, a firmware author could use the built-in UI for RGB Matrix on channel id_qmk_rgb_matrix_channel = 4
, and implement via_custom_value_command_kb()
to only handle commands on channel id_custom_channel = 0
.
In the rare case of needing to subvert/extend/replace the default custom value handling of a QMK feature, via_custom_value_command()
itself can be overridden and reimplemented in keyboard level code, handling the channel_id
for QMK features.
The following is an example of implementing via_custom_value_command_kb()
. It routes the three commands to functions to handle each command, this avoids nested switch/case statements and improves readability.
void via_custom_value_command_kb(uint8_t *data, uint8_t length) {
// data = [ command_id, channel_id, value_id, value_data ]
uint8_t *command_id = &(data[0]);
uint8_t *channel_id = &(data[1]);
uint8_t *value_id_and_data = &(data[2]);
if ( *channel_id == id_custom_channel ) {
switch ( *command_id )
{
case id_custom_set_value:
{
buttglow_config_set_value(value_id_and_data);
break;
}
case id_custom_get_value:
{
buttglow_config_get_value(value_id_and_data);
break;
}
case id_custom_save:
{
buttglow_config_save();
break;
}
default:
{
// Unhandled message.
*command_id = id_unhandled;
break;
}
}
return;
}
// Return the unhandled state
*command_id = id_unhandled;
// DO NOT call raw_hid_send(data,length) here, let caller do this
}
The following is an example of implementing the set/get command handlers specific to a feature - switch on possible values and set/get the values to/from a global struct which stores the state being used by the feature. Triggering update functions on state change can be optionally added.
void buttglow_config_set_value( uint8_t *data )
{
// data = [ value_id, value_data ]
uint8_t *value_id = &(data[0]);
uint8_t *value_data = &(data[1]);
switch ( *value_id )
{
case id_buttglow_brightness:
{
g_buttglow_config.brightness = *value_data;
break;
}
...
}
}
void buttglow_config_get_value( uint8_t *data )
{
// data = [ value_id, value_data ]
uint8_t *value_id = &(data[0]);
uint8_t *value_data = &(data[1]);
switch ( *value_id )
{
case id_buttglow_brightness:
{
*value_data = g_buttglow_config.brightness;
break;
}
...
}
}
void buttglow_config_save(void)
{
eeprom_update_block( &g_buttglow_config,
((void*)BUTTGLOW_CONFIG_EEPROM_ADDR),
sizeof(buttglow_config) );
}
Saving Values
The id_custom_save
command is sent after one or more id_custom_set_value
commands have been sent, and after a small delay. It is used to allow the firmware to defer writing to EEPROM and respond to set value commands quickly. This also reduces the number of redundant writes to EEPROM, especially when users are changing values quickly in VIA, like when using a color picker. The channel_id
of the id_custom_save
command will be the same value as used in the id_custom_set_value
commands which preceeded it, thus a handler to write all current values to EEPROM for a given feature can be called and bound per channel.
Handling 16-bit Values
The keycode control will send/receive 16-bit values. Likewise, the range control will send/receive 16-bit values if the range maximum is greater than 255. The order of bytes is [high byte, low byte]
.
In the following example, g_buttglow_config.audacity
is uint16_t
and the range control is configured to use a range of 0 to 1023.
void buttglow_config_set_value( uint8_t *data )
{
// data = [ value_id, value_data ]
uint8_t *value_id = &(data[0]);
uint8_t *value_data = &(data[1]);
switch ( *value_id )
{
case id_buttglow_audacity:
{
g_buttglow_config.audacity = value_data[0] << 8 | value_data[1];
break;
}
...
}
}
void buttglow_config_get_value( uint8_t *data )
{
// data = [ value_id, value_data ]
uint8_t *value_id = &(data[0]);
uint8_t *value_data = &(data[1]);
switch ( *value_id )
{
case id_buttglow_audacity:
{
value_data[0] = g_buttglow_config.audacity >> 8;
value_data[1] = g_buttglow_config.audacity & 0xFF;
break;
}
...
}
}
Handling Array Values
The following is an example of a custom UI control using an array value, the array index is after the value_id
, thus it is in value_data[0]
and the actual value starts at value_data[1]
, whereas for non-array values, the value starts at value_data[0]
.
void buttglow_config_set_value( uint8_t *data )
{
// data = [ value_id, value_data ]
uint8_t *value_id = &(data[0]);
uint8_t *value_data = &(data[1]);
switch ( *value_id )
{
case id_buttglow_color: // == 4
{
uint8_t index = value_data[0]; // == 0,1,2
if ( index >= 0 && index < 3 )
{
g_buttglow_config.color[index].h = value_data[1];
g_buttglow_config.color[index].s = value_data[2];
}
}
...
}
}
See Array Values