From 7e5c99eb037b80ed4a44129bcda2de38d5e25bb5 Mon Sep 17 00:00:00 2001 From: Joel Spadin Date: Sat, 5 Aug 2023 23:29:48 -0500 Subject: [PATCH] feat(behaviors): Add a send string behavior This adds the following: - A character map driver API, which maps Unicode code points to behavior bindings. - A zmk,character-map driver which implements this API. This driver is designed for ROM efficiency, so it sends every value defined in the map to one behavior and passes any code point not in the map through to another. (A more flexible implementation that allows for a unique behavior binding per character could be added later if necessary.) - A zmk,send-string behavior, which users can define and bind to their keymaps to send strings. - A zmk_send_string() function, which queues the necessary behaviors to type a UTF-8 string. This is separated from the send string behavior since it could be used for other features such as Unicode key sequences, behaviors that print dynamic messages, etc. --- app/CMakeLists.txt | 4 + app/Kconfig | 21 +- app/Kconfig.behaviors | 6 + app/dts/behaviors.dtsi | 2 + app/dts/behaviors/character_map.dtsi | 116 +++++++ app/dts/behaviors/send_string.dtsi | 13 + .../behaviors/zmk,behavior-send-string.yaml | 26 ++ app/dts/bindings/zmk,character-map.yaml | 28 ++ app/include/drivers/character_map.h | 66 ++++ app/include/zmk/send_string.h | 89 ++++++ app/src/behaviors/behavior_send_string.c | 57 ++++ app/src/character_map.c | 103 ++++++ app/src/send_string.c | 56 ++++ app/tests/send-string/ascii/events.patterns | 1 + .../send-string/ascii/keycode_events.snapshot | 26 ++ .../send-string/ascii/native_posix_64.keymap | 16 + app/tests/send-string/behavior_keymap.dtsi | 29 ++ docs/docs/config/behaviors.md | 65 +++- docs/docs/keymaps/behaviors/macros.md | 14 +- docs/docs/keymaps/behaviors/send-string.md | 299 ++++++++++++++++++ docs/sidebars.js | 1 + 21 files changed, 1028 insertions(+), 10 deletions(-) create mode 100644 app/dts/behaviors/character_map.dtsi create mode 100644 app/dts/behaviors/send_string.dtsi create mode 100644 app/dts/bindings/behaviors/zmk,behavior-send-string.yaml create mode 100644 app/dts/bindings/zmk,character-map.yaml create mode 100644 app/include/drivers/character_map.h create mode 100644 app/include/zmk/send_string.h create mode 100644 app/src/behaviors/behavior_send_string.c create mode 100644 app/src/character_map.c create mode 100644 app/src/send_string.c create mode 100644 app/tests/send-string/ascii/events.patterns create mode 100644 app/tests/send-string/ascii/keycode_events.snapshot create mode 100644 app/tests/send-string/ascii/native_posix_64.keymap create mode 100644 app/tests/send-string/behavior_keymap.dtsi create mode 100644 docs/docs/keymaps/behaviors/send-string.md diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index fd4b7ab5519..1d3ca4c99d5 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -16,6 +16,7 @@ if(CONFIG_ZMK_BEHAVIOR_LOCAL_IDS) endif() zephyr_syscall_header(${APPLICATION_SOURCE_DIR}/include/drivers/behavior.h) +zephyr_syscall_header(${APPLICATION_SOURCE_DIR}/include/drivers/character_map.h) zephyr_syscall_header(${APPLICATION_SOURCE_DIR}/include/drivers/ext_power.h) # Add your source file to the "app" target. This must come after @@ -24,10 +25,12 @@ target_include_directories(app PRIVATE include) target_sources(app PRIVATE src/stdlib.c) target_sources(app PRIVATE src/activity.c) target_sources(app PRIVATE src/behavior.c) +target_sources_ifdef(CONFIG_ZMK_CHARACTER_MAP app PRIVATE src/character_map.c) target_sources_ifdef(CONFIG_ZMK_KSCAN_SIDEBAND_BEHAVIORS app PRIVATE src/kscan_sideband_behaviors.c) target_sources(app PRIVATE src/matrix_transform.c) target_sources(app PRIVATE src/physical_layouts.c) target_sources(app PRIVATE src/sensors.c) +target_sources_ifdef(CONFIG_ZMK_SEND_STRING app PRIVATE src/send_string.c) target_sources_ifdef(CONFIG_ZMK_WPM app PRIVATE src/wpm.c) target_sources(app PRIVATE src/event_manager.c) target_sources_ifdef(CONFIG_ZMK_PM app PRIVATE src/pm.c) @@ -59,6 +62,7 @@ if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL) target_sources(app PRIVATE src/behaviors/behavior_to_layer.c) target_sources(app PRIVATE src/behaviors/behavior_transparent.c) target_sources(app PRIVATE src/behaviors/behavior_none.c) + target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_SEND_STRING app PRIVATE src/behaviors/behavior_send_string.c) target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_SENSOR_ROTATE app PRIVATE src/behaviors/behavior_sensor_rotate.c) target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_SENSOR_ROTATE_VAR app PRIVATE src/behaviors/behavior_sensor_rotate_var.c) target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_SENSOR_ROTATE_COMMON app PRIVATE src/behaviors/behavior_sensor_rotate_common.c) diff --git a/app/Kconfig b/app/Kconfig index b0ffc72ac02..a6f4ab188df 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -495,12 +495,13 @@ endmenu menu "Behavior Options" +rsource "Kconfig.behaviors" + config ZMK_BEHAVIORS_QUEUE_SIZE int "Maximum number of behaviors to allow queueing from a macro or other complex behavior" + default 256 if ZMK_BEHAVIOR_SEND_STRING default 64 -rsource "Kconfig.behaviors" - config ZMK_MACRO_DEFAULT_WAIT_MS int "Default time to wait (in milliseconds) before triggering the next behavior in macros" default 15 @@ -509,6 +510,14 @@ config ZMK_MACRO_DEFAULT_TAP_MS int "Default time to wait (in milliseconds) between the press and release events of a tapped behavior in macros" default 30 +config ZMK_SEND_STRING_DEFAULT_WAIT_MS + int "Default time to wait (in milliseconds) before pressing the next key in the text" + default 0 + +config ZMK_SEND_STRING_DEFAULT_TAP_MS + int "Default time to wait (in milliseconds) between the press and release for each key in the text" + default 5 + endmenu menu "Advanced" @@ -709,6 +718,14 @@ config USB_DEVICE_STACK config FPU default CPU_HAS_FPU +config ZMK_SEND_STRING + bool "Enable send string function" + +config ZMK_CHARACTER_MAP + bool + default y + depends on DT_HAS_ZMK_CHARACTER_MAP_ENABLED + config ZMK_WPM bool "Calculate WPM" diff --git a/app/Kconfig.behaviors b/app/Kconfig.behaviors index 69419a2f17e..b4e466147e1 100644 --- a/app/Kconfig.behaviors +++ b/app/Kconfig.behaviors @@ -118,3 +118,9 @@ config ZMK_BEHAVIOR_MACRO bool default y depends on DT_HAS_ZMK_BEHAVIOR_MACRO_ENABLED || DT_HAS_ZMK_BEHAVIOR_MACRO_ONE_PARAM_ENABLED || DT_HAS_ZMK_BEHAVIOR_MACRO_TWO_PARAM_ENABLED + +config ZMK_BEHAVIOR_SEND_STRING + bool + default y + depends on DT_HAS_ZMK_BEHAVIOR_SEND_STRING_ENABLED + select ZMK_SEND_STRING diff --git a/app/dts/behaviors.dtsi b/app/dts/behaviors.dtsi index fcb4a63d450..9215f0ef5a8 100644 --- a/app/dts/behaviors.dtsi +++ b/app/dts/behaviors.dtsi @@ -22,3 +22,5 @@ #include #include #include +#include +#include diff --git a/app/dts/behaviors/character_map.dtsi b/app/dts/behaviors/character_map.dtsi new file mode 100644 index 00000000000..d6a9788159f --- /dev/null +++ b/app/dts/behaviors/character_map.dtsi @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2023 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include + +/ { + // Codepoint to keycode mapping for a US keyboard layout. + /omit-if-no-ref/ charmap_us: character_map_us { + compatible = "zmk,character-map"; + behavior = <&kp>; + map = <0x08 BACKSPACE> + , <0x0A RETURN> + , <0x0B TAB> + , <0x20 SPACE> + , <0x21 EXCLAMATION> + , <0x22 DOUBLE_QUOTES> + , <0x23 HASH> + , <0x24 DOLLAR> + , <0x25 PERCENT> + , <0x26 AMPERSAND> + , <0x27 APOSTROPHE> + , <0x28 LEFT_PARENTHESIS> + , <0x29 RIGHT_PARENTHESIS> + , <0x2A ASTERISK> + , <0x2B PLUS> + , <0x2C COMMA> + , <0x2D MINUS> + , <0x2E PERIOD> + , <0x2F SLASH> + , <0x30 N0> + , <0x31 N1> + , <0x32 N2> + , <0x33 N3> + , <0x34 N4> + , <0x35 N5> + , <0x36 N6> + , <0x37 N7> + , <0x38 N8> + , <0x39 N9> + , <0x3A COLON> + , <0x3B SEMICOLON> + , <0x3C LESS_THAN> + , <0x3D EQUAL> + , <0x3E GREATER_THAN> + , <0x3F QUESTION> + , <0x40 AT_SIGN> + , <0x41 LS(A)> + , <0x42 LS(B)> + , <0x43 LS(C)> + , <0x44 LS(D)> + , <0x45 LS(E)> + , <0x46 LS(F)> + , <0x47 LS(G)> + , <0x48 LS(H)> + , <0x49 LS(I)> + , <0x4A LS(J)> + , <0x4B LS(K)> + , <0x4C LS(L)> + , <0x4D LS(M)> + , <0x4E LS(N)> + , <0x4F LS(O)> + , <0x50 LS(P)> + , <0x51 LS(Q)> + , <0x52 LS(R)> + , <0x53 LS(S)> + , <0x54 LS(T)> + , <0x55 LS(U)> + , <0x56 LS(V)> + , <0x57 LS(W)> + , <0x58 LS(X)> + , <0x59 LS(Y)> + , <0x5A LS(Z)> + , <0x5B LEFT_BRACKET> + , <0x5C BACKSLASH> + , <0x5D RIGHT_BRACKET> + , <0x5E CARET> + , <0x5F UNDERSCORE> + , <0x60 GRAVE> + , <0x61 A> + , <0x62 B> + , <0x63 C> + , <0x64 D> + , <0x65 E> + , <0x66 F> + , <0x67 G> + , <0x68 H> + , <0x69 I> + , <0x6A J> + , <0x6B K> + , <0x6C L> + , <0x6D M> + , <0x6E N> + , <0x6F O> + , <0x70 P> + , <0x71 Q> + , <0x72 R> + , <0x73 S> + , <0x74 T> + , <0x75 U> + , <0x76 V> + , <0x77 W> + , <0x78 X> + , <0x79 Y> + , <0x7A Z> + , <0x7B LEFT_BRACE> + , <0x7C PIPE> + , <0x7D RIGHT_BRACE> + , <0x7E TILDE> + , <0x7F DELETE> + ; + }; +}; + diff --git a/app/dts/behaviors/send_string.dtsi b/app/dts/behaviors/send_string.dtsi new file mode 100644 index 00000000000..4f1ff59a94c --- /dev/null +++ b/app/dts/behaviors/send_string.dtsi @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2023 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define ZMK_SEND_STRING(name, string, ...) \ +name: name { \ + compatible = "zmk,behavior-send-string"; \ + #binding-cells = <0>; \ + text = string; \ + __VA_ARGS__ \ +}; diff --git a/app/dts/bindings/behaviors/zmk,behavior-send-string.yaml b/app/dts/bindings/behaviors/zmk,behavior-send-string.yaml new file mode 100644 index 00000000000..840b6c4f179 --- /dev/null +++ b/app/dts/bindings/behaviors/zmk,behavior-send-string.yaml @@ -0,0 +1,26 @@ +# Copyright (c) 2023 The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Send String Behavior + +compatible: "zmk,behavior-send-string" + +include: zero_param.yaml + +properties: + text: + type: string + required: true + description: The text to send. + + wait-ms: + type: int + description: The time to wait (in milliseconds) before pressing the next key in the text. + + tap-ms: + type: int + description: The time to wait (in milliseconds) between the press and release of each key in the text. + + charmap: + type: phandle + description: A zmk,character-map instance to use. If omitted, the zmk,charmap chosen node is used. diff --git a/app/dts/bindings/zmk,character-map.yaml b/app/dts/bindings/zmk,character-map.yaml new file mode 100644 index 00000000000..a880a56a917 --- /dev/null +++ b/app/dts/bindings/zmk,character-map.yaml @@ -0,0 +1,28 @@ +# Copyright (c) 2023, The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Unicode codepoint to behavior binding mapping + +compatible: "zmk,character-map" + +properties: + behavior: + type: phandle + required: true + description: | + Behavior to use for a code point in the mapping (typically <&kp>). + The behavior is given one parameter which is the value for the code point + from the "map" property. + + unicode-behavior: + type: phandle + description: | + Optional behavior to use for a code point not in the mapping. + The behavior is given one parameter which is the code point. + + map: + type: array + required: true + description: | + List of pairs. Each pair maps a codepoint to a parameter + given to "behavior" to type that code point. diff --git a/app/include/drivers/character_map.h b/app/include/drivers/character_map.h new file mode 100644 index 00000000000..07bf66dba08 --- /dev/null +++ b/app/include/drivers/character_map.h @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2023 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @cond INTERNAL_HIDDEN + * + * Character map driver API definition and system call entry points. + * + * (Internal use only.) + */ + +typedef int (*character_map_codepoint_to_binding_t)(const struct device *device, uint32_t codepoint, + struct zmk_behavior_binding *binding); + +__subsystem struct character_map_driver_api { + character_map_codepoint_to_binding_t codepoint_to_binding; +}; +/** + * @endcond + */ + +/** + * @brief Map a Unicode codepoint to a behavior binding. + * @param charmap Pointer to the device structure for the driver instance. + * @param codepoint Unicode codepoint to map. + * @param binding Corresponding behavior binding is written here if successful. + * + * @retval 0 If successful. + * @retval Negative errno code if failure. + */ +__syscall int character_map_codepoint_to_binding(const struct device *charmap, uint32_t codepoint, + struct zmk_behavior_binding *binding); + +static inline int z_impl_character_map_codepoint_to_binding(const struct device *charmap, + uint32_t codepoint, + struct zmk_behavior_binding *binding) { + const struct character_map_driver_api *api = + (const struct character_map_driver_api *)charmap->api; + + if (api->codepoint_to_binding == NULL) { + return -ENOTSUP; + } + + return api->codepoint_to_binding(charmap, codepoint, binding); +} + +#ifdef __cplusplus +} +#endif + +#include diff --git a/app/include/zmk/send_string.h b/app/include/zmk/send_string.h new file mode 100644 index 00000000000..e107d6e836e --- /dev/null +++ b/app/include/zmk/send_string.h @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2023 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include +#include +#include + +struct zmk_send_string_config { + /// zmk,character-map driver instance to use + const struct device *character_map; + /// Time in milliseconds to wait between key presses + uint32_t wait_ms; + /// Time in milliseconds to wait between the press and release of each key + uint32_t tap_ms; +}; + +/** + * Assert at compile time that a zmk,charmap chosen node is set. + */ +#define ZMK_BUILD_ASSERT_HAS_CHOSEN_CHARMAP() \ + BUILD_ASSERT( \ + DT_HAS_CHOSEN(zmk_charmap), \ + "A zmk,charmap chosen node must be set to use send string functions. See " \ + "https://zmk.dev/docs/keymaps/behaviors/send-string#character-maps for more information.") + +/** + * Get a struct zmk_send_string_config which uses the zmk,charmap chosen node + * and Kconfig options for timing. + * + * Use ZMK_BUILD_ASSERT_HAS_CHOSEN_CHARMAP() somewhere in the file before using + * this macro to provide a nice error message if a character map hasn't been set. + */ +#define ZMK_SEND_STRING_CONFIG_DEFAULT \ + ((struct zmk_send_string_config){ \ + .character_map = DEVICE_DT_GET(DT_CHOSEN(zmk_charmap)), \ + .wait_ms = CONFIG_ZMK_SEND_STRING_DEFAULT_WAIT_MS, \ + .tap_ms = CONFIG_ZMK_SEND_STRING_DEFAULT_TAP_MS, \ + }) + +/** + * Assert at compile time that a DT_DRV_INST(n) has a charmap property or a + * zmk,character-map chosen node is set. + */ +#define ZMK_BUILD_ASSERT_DT_INST_HAS_CHARMAP(n) \ + BUILD_ASSERT(DT_INST_NODE_HAS_PROP(n, charmap) || DT_HAS_CHOSEN(zmk_charmap), \ + "Node " DT_NODE_PATH(DT_DRV_INST( \ + n)) " requires a charmap property or a zmk,charmap chosen node. " \ + "See https://zmk.dev/docs/keymaps/behaviors/send-string#character-maps " \ + "for more information.") + +/** + * Get a struct zmk_send_string_config from properties on DT_DRV_INST(n) with + * fallbacks to the values from ZMK_SEND_STRING_CONFIG_DEFAULT. + * + * The driver should have the following properties defined in its YAML file: + * + * charmap: + * type: phandle + * wait-ms: + * type: int + * tap-ms: + * type: int + * + * Use ZMK_BUILD_ASSERT_CHARACTER_MAP_DT_INST_PROP(n) somewhere in the file before using + * this macro to provide a nice error message if a character map hasn't been set. + */ +#define ZMK_SEND_STRING_CONFIG_DT_INST_PROP(n) \ + ((struct zmk_send_string_config){ \ + .character_map = DEVICE_DT_GET(DT_INST_PROP_OR(n, charmap, DT_CHOSEN(zmk_charmap))), \ + .wait_ms = DT_INST_PROP_OR(n, wait_ms, CONFIG_ZMK_SEND_STRING_DEFAULT_WAIT_MS), \ + .tap_ms = DT_INST_PROP_OR(n, tap_ms, CONFIG_ZMK_SEND_STRING_DEFAULT_TAP_MS), \ + }) + +/** + * Queues behaviors to type a string. + * + * @param config Character map and other configuration to use. + * Pass &ZMK_SEND_STRING_CONFIG_DEFAULT to use default values. + * @param position Key position to use for the key presses/releases + * @param text UTF-8 encoded string + */ +void zmk_send_string(const struct zmk_send_string_config *config, + const struct zmk_behavior_binding_event *event, const char *text); diff --git a/app/src/behaviors/behavior_send_string.c b/app/src/behaviors/behavior_send_string.c new file mode 100644 index 00000000000..34ba0f570f7 --- /dev/null +++ b/app/src/behaviors/behavior_send_string.c @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_behavior_send_string + +#include +#include +#include +#include +#include +#include + +struct behavior_send_string_config { + const char *text; + struct zmk_send_string_config config; +}; + +static int on_send_string_binding_pressed(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + const struct device *dev = device_get_binding(binding->behavior_dev); + const struct behavior_send_string_config *config = dev->config; + + zmk_send_string(&config->config, &event, config->text); + + return ZMK_BEHAVIOR_OPAQUE; +} + +static int on_send_string_binding_released(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + return ZMK_BEHAVIOR_OPAQUE; +} + +static const struct behavior_driver_api behavior_send_string_driver_api = { + .binding_pressed = on_send_string_binding_pressed, + .binding_released = on_send_string_binding_released, +#if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) + .get_parameter_metadata = zmk_behavior_get_empty_param_metadata, +#endif +}; + +static int behavior_send_string_init(const struct device *dev) { return 0; } + +#define SEND_STRING_INST(n) \ + ZMK_BUILD_ASSERT_DT_INST_HAS_CHARMAP(n); \ + \ + static const struct behavior_send_string_config behavior_send_string_config_##n = { \ + .text = DT_INST_PROP(n, text), \ + .config = ZMK_SEND_STRING_CONFIG_DT_INST_PROP(n), \ + }; \ + BEHAVIOR_DT_INST_DEFINE(n, behavior_send_string_init, NULL, NULL, \ + &behavior_send_string_config_##n, POST_KERNEL, \ + CONFIG_APPLICATION_INIT_PRIORITY, &behavior_send_string_driver_api); + +DT_INST_FOREACH_STATUS_OKAY(SEND_STRING_INST); diff --git a/app/src/character_map.c b/app/src/character_map.c new file mode 100644 index 00000000000..828a8490179 --- /dev/null +++ b/app/src/character_map.c @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2023 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_character_map + +#include +#include +#include +#include +#include +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +struct codepoint_param { + uint32_t codepoint; + uint32_t param; +}; + +struct character_map_config { + const char *behavior_dev; + const char *fallback_behavior_dev; + struct codepoint_param *map; + size_t map_size; +}; + +static int compare_codepoints(const void *lhs, const void *rhs) { + const struct codepoint_param *lhs_item = (const struct codepoint_param *)lhs; + const struct codepoint_param *rhs_item = (const struct codepoint_param *)rhs; + + return (int64_t)lhs_item->codepoint - (int64_t)rhs_item->codepoint; +} + +static int codepoint_to_binding(const struct device *dev, uint32_t codepoint, + struct zmk_behavior_binding *binding) { + const struct character_map_config *config = dev->config; + + const struct codepoint_param key = {.codepoint = codepoint}; + const struct codepoint_param *result = + bsearch(&key, config->map, config->map_size, sizeof(config->map[0]), compare_codepoints); + + if (result) { + *binding = (struct zmk_behavior_binding){.behavior_dev = config->behavior_dev, + .param1 = result->param}; + return 0; + } + + if (config->fallback_behavior_dev) { + *binding = (struct zmk_behavior_binding){.behavior_dev = config->fallback_behavior_dev, + .param1 = codepoint}; + return 0; + } + + return -ENOTSUP; +} + +static const struct character_map_driver_api character_map_driver_api = { + .codepoint_to_binding = codepoint_to_binding, +}; + +static int character_map_init(const struct device *dev) { + const struct character_map_config *config = dev->config; + + // Sort the character map by codepoint for faster lookup + qsort(config->map, config->map_size, sizeof(config->map[0]), compare_codepoints); + + return 0; +} + +#define MAP_LEN(n) DT_INST_PROP_LEN(n, map) +#define MAP_INIT(node_id, prop, idx) DT_PROP_BY_IDX(node_id, prop, idx), + +#define CHARMAP_INST(n) \ + BUILD_ASSERT(MAP_LEN(n) > 0, "'map' property must not be an empty array."); \ + BUILD_ASSERT(MAP_LEN(n) % 2 == 0, \ + "'map' property must be an array of pairs."); \ + \ + /* Since we can't iterate over the "map" elements pairwise, we write all the values to a flat \ + * array and then reinterpret it as an array of struct codepoint_param. */ \ + static uint32_t character_map_array_##n[] = {DT_INST_FOREACH_PROP_ELEM(n, map, MAP_INIT)}; \ + \ + BUILD_ASSERT(sizeof(character_map_array_##n) % sizeof(struct codepoint_param) == 0, \ + "sizeof(struct codepoint_param) must evenly divide array size"); \ + \ + static const struct character_map_config character_map_config_##n = { \ + .behavior_dev = DEVICE_DT_NAME(DT_INST_PROP(n, behavior)), \ + .fallback_behavior_dev = \ + COND_CODE_1(DT_INST_NODE_HAS_PROP(n, fallback_behavior), \ + (DT_DEVICE_NAME(DT_INST_PROP(n, fallback_behavior))), (NULL)), \ + .map = (struct codepoint_param *)character_map_array_##n, \ + .map_size = ARRAY_SIZE(character_map_array_##n) / 2, \ + }; \ + \ + DEVICE_DT_INST_DEFINE(n, character_map_init, NULL, NULL, &character_map_config_##n, \ + POST_KERNEL, CONFIG_APPLICATION_INIT_PRIORITY, \ + &character_map_driver_api); + +DT_INST_FOREACH_STATUS_OKAY(CHARMAP_INST); diff --git a/app/src/send_string.c b/app/src/send_string.c new file mode 100644 index 00000000000..f8137a0b6fd --- /dev/null +++ b/app/src/send_string.c @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +static bool is_utf8_continuation(const char c) { return (c & 0xc0) == 0x80; } + +/** + * Reads a code point from the UTF-8 string at **str and advances *str to point + * to the start of the next code point. + * + * The string is expected to be valid UTF-8. No validation is performed. + * + * Based on public domain code at https://gist.github.com/tylerneylon/9773800 + */ +static uint32_t decode_utf8(const char **str) { + int size = **str ? u32_count_leading_zeros(~(**str << 24)) : 0; // Number of leading 1 bits + uint32_t mask = (1 << (8 - size)) - 1; // All 1s with "size" leading 0s. + uint32_t codepoint = **str & mask; + + for (++(*str), --size; size > 0 && is_utf8_continuation(**str); --size, ++(*str)) { + codepoint <<= 6; + codepoint += (**str & 0x3F); + } + + return codepoint; +} + +void zmk_send_string(const struct zmk_send_string_config *config, + const struct zmk_behavior_binding_event *event, const char *text) { + const char *current = text; + + uint32_t codepoint; + while ((codepoint = decode_utf8(¤t))) { + struct zmk_behavior_binding binding; + int ret = character_map_codepoint_to_binding(config->character_map, codepoint, &binding); + + if (ret != 0) { + LOG_WRN("Failed to map codepoint 0x%04x to a behavior binding: %d", codepoint, ret); + continue; + } + + zmk_behavior_queue_add(event, binding, true, config->tap_ms); + zmk_behavior_queue_add(event, binding, false, config->wait_ms); + } +} \ No newline at end of file diff --git a/app/tests/send-string/ascii/events.patterns b/app/tests/send-string/ascii/events.patterns new file mode 100644 index 00000000000..f058db1c1ac --- /dev/null +++ b/app/tests/send-string/ascii/events.patterns @@ -0,0 +1 @@ +s/.*hid_listener_keycode/kp/p diff --git a/app/tests/send-string/ascii/keycode_events.snapshot b/app/tests/send-string/ascii/keycode_events.snapshot new file mode 100644 index 00000000000..7bbd09dd6c9 --- /dev/null +++ b/app/tests/send-string/ascii/keycode_events.snapshot @@ -0,0 +1,26 @@ +kp_pressed: usage_page 0x07 keycode 0x0B implicit_mods 0x02 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x0B implicit_mods 0x02 explicit_mods 0x00 +kp_pressed: usage_page 0x07 keycode 0x08 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x08 implicit_mods 0x00 explicit_mods 0x00 +kp_pressed: usage_page 0x07 keycode 0x0F implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x0F implicit_mods 0x00 explicit_mods 0x00 +kp_pressed: usage_page 0x07 keycode 0x0F implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x0F implicit_mods 0x00 explicit_mods 0x00 +kp_pressed: usage_page 0x07 keycode 0x12 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x12 implicit_mods 0x00 explicit_mods 0x00 +kp_pressed: usage_page 0x07 keycode 0x36 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x36 implicit_mods 0x00 explicit_mods 0x00 +kp_pressed: usage_page 0x07 keycode 0x2C implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x2C implicit_mods 0x00 explicit_mods 0x00 +kp_pressed: usage_page 0x07 keycode 0x1A implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x1A implicit_mods 0x00 explicit_mods 0x00 +kp_pressed: usage_page 0x07 keycode 0x12 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x12 implicit_mods 0x00 explicit_mods 0x00 +kp_pressed: usage_page 0x07 keycode 0x15 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x15 implicit_mods 0x00 explicit_mods 0x00 +kp_pressed: usage_page 0x07 keycode 0x0F implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x0F implicit_mods 0x00 explicit_mods 0x00 +kp_pressed: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00 +kp_pressed: usage_page 0x07 keycode 0x1E implicit_mods 0x02 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x1E implicit_mods 0x02 explicit_mods 0x00 diff --git a/app/tests/send-string/ascii/native_posix_64.keymap b/app/tests/send-string/ascii/native_posix_64.keymap new file mode 100644 index 00000000000..2c1757a16ed --- /dev/null +++ b/app/tests/send-string/ascii/native_posix_64.keymap @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2022 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include + +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,500) + >; +}; diff --git a/app/tests/send-string/behavior_keymap.dtsi b/app/tests/send-string/behavior_keymap.dtsi new file mode 100644 index 00000000000..0bb682c4f2a --- /dev/null +++ b/app/tests/send-string/behavior_keymap.dtsi @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include + +/ { + chosen { + zmk,charmap = &charmap_us; + }; + + behaviors { + ZMK_SEND_STRING(hello_world, "Hello, world!") + }; + + keymap { + compatible = "zmk,keymap"; + + default_layer { + bindings = < + &hello_world &none + &none &none + >; + }; + }; +}; \ No newline at end of file diff --git a/docs/docs/config/behaviors.md b/docs/docs/config/behaviors.md index 6914495fa41..598e61b5905 100644 --- a/docs/docs/config/behaviors.md +++ b/docs/docs/config/behaviors.md @@ -13,9 +13,17 @@ See the [zmk/app/dts/behaviors/](https://github.com/zmkfirmware/zmk/tree/main/ap ### Kconfig -| Config | Type | Description | Default | -| --------------------------------- | ---- | ------------------------------------------------------------------------------------ | ------- | -| `CONFIG_ZMK_BEHAVIORS_QUEUE_SIZE` | int | Maximum number of behaviors to allow queueing from a macro or other complex behavior | 64 | +| Config | Type | Description | Default | +| --------------------------------- | ---- | ------------------------------------------------------------------------------------ | --------------------------------------------------- | +| `CONFIG_ZMK_BEHAVIORS_QUEUE_SIZE` | int | Maximum number of behaviors to allow queueing from a macro or other complex behavior | 256 if [send string](#send-string) is used, else 64 | + +### Devicetree + +Applies to: [`/chosen` node](https://docs.zephyrproject.org/latest/guides/dts/intro.html#aliases-and-chosen-nodes) + +| Property | Type | Description | +| ------------- | ---- | -------------------------------------------------------------------------------------------- | +| `zmk,charmap` | path | The default [character map](#character-map) to use for [send string](#send-string) behaviors | ## Caps Word @@ -196,6 +204,57 @@ You can use the following nodes to tweak the default behaviors: | -------- | ------------------------------------------------- | | `&gresc` | [Grave escape](../keymaps/behaviors/mod-morph.md) | +## Send String + +Creates a custom behavior that types a text string. + +See the [send string behavior](../keymaps/behaviors/send-string.md) documentation for more details and examples. + +### Kconfig + +| Config | Type | Description | Default | +| ---------------------------------------- | ---- | ----------------------------------------------------- | ------- | +| `CONFIG_ZMK_SEND_STRING_DEFAULT_WAIT_MS` | int | Default value for `wait-ms` in send string behaviors. | 0 | +| `CONFIG_ZMK_SEND_STRING_DEFAULT_TAP_MS` | int | Default value for `tap-ms` in send string behaviors. | 5 | + +### Devicetree + +Definition file: [zmk/app/dts/bindings/behaviors/zmk,behavior-send-string.yaml](https://github.com/zmkfirmware/zmk/blob/main/app/dts/bindings/behaviors/zmk%2Cbehavior-send-string.yaml) + +Applies to: `compatible = "zmk,send-string"` + +| Property | Type | Description | Default | +| ---------------- | ------- | ---------------------------------------------------------------------------------------- | ---------------------------------------- | +| `#binding-cells` | int | Must be `<0>` | | +| `text` | string | The text to send | | +| `charmap` | phandle | The [character map](#character-map) to use | `zmk,charmap` chosen node. | +| `wait-ms` | int | The time to wait (in milliseconds) before pressing the next key in the text | `CONFIG_ZMK_SEND_STRING_DEFAULT_WAIT_MS` | +| `tap-ms` | int | The time to wait (in milliseconds) between the press and release of each key in the text | `CONFIG_ZMK_SEND_STRING_DEFAULT_TAP_MS` | + +### Character Map + +Maps Unicode [code points](https://en.wikipedia.org/wiki/List_of_Unicode_characters) to key codes for [send string behaviors](#send-string). + +See the [send string behavior](../keymaps/behaviors/send-string.md#character-maps) documentation for more details and examples. + +#### Devicetree + +Definition file: [zmk/app/drivers/zephyr/dts/bindings/character_map/zmk,character-map.yaml](https://github.com/zmkfirmware/zmk/blob/main/app/dts/bindings/character_map/zmk%2Ccharacter-map.yaml) + +Applies to: `compatible = "zmk,character-map"` + +| Property | Type | Description | +| ------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------- | +| `behavior` | phandle | Behavior to use for a code point in the map (typically should be `<&kp>`) | +| `fallback-behavior` | phandle | Optional behavior which will be sent any code points not in the map | +| `map` | array | List of `` pairs which give the [key code](../keymaps/list-of-keycodes.mdx) to use for each Unicode code point | + +You can use the following nodes to tweak the default behaviors: + +| Node | Description | +| ------------- | ------------------------------------ | +| `&charmap_us` | Character map for US keyboard layout | + ## Sensor Rotation Creates a custom behavior which sends a tap of other behaviors when a sensor is rotated. diff --git a/docs/docs/keymaps/behaviors/macros.md b/docs/docs/keymaps/behaviors/macros.md index a06efaf3760..400d5fde972 100644 --- a/docs/docs/keymaps/behaviors/macros.md +++ b/docs/docs/keymaps/behaviors/macros.md @@ -8,6 +8,15 @@ sidebar_label: Macros The macro behavior allows configuring a list of other behaviors to invoke when the macro is pressed and/or released. +:::note +Some things that you can do with macros can be done more easily with other behaviors. + +To send a single keycode with modifiers, for instance ctrl+tab, you can use the [key press behavior](key-press.md) +with [modifier functions](../modifiers.mdx#modifier-functions). + +To send a string of text, you can use the [send string behavior](send-string.md). +::: + ## Macro Definition Each macro you want to use in your keymap gets defined first, then bound in your keymap. @@ -43,11 +52,6 @@ The macro can then be bound in your keymap by referencing it by the label `&zed_ }; ``` -:::note -For use cases involving sending a single keycode with modifiers, for instance ctrl+tab, the [key press behavior](key-press.md) -with [modifier functions](../modifiers.mdx#modifier-functions) can be used instead of a macro. -::: - ### Bindings Like [hold-taps](hold-tap.mdx), macros are created by composing other behaviors, and any of those behaviors can diff --git a/docs/docs/keymaps/behaviors/send-string.md b/docs/docs/keymaps/behaviors/send-string.md new file mode 100644 index 00000000000..f60107e8acb --- /dev/null +++ b/docs/docs/keymaps/behaviors/send-string.md @@ -0,0 +1,299 @@ +--- +title: Send String Behavior +sidebar_label: Send String +--- + +## Summary + +The send string behavior types a string of text when pressed. + +## Behavior Definition + +Each string you want to send must be defined as a new behavior in your keymap, then bound to a key. Each behavior must have the following properties: + +```dts +compatible = "zmk,behavior-send-string"; +#binding-cells = <0>; +text = "..."; +``` + +For example, the following defines a `&hello_world` behavior that types `Hello, world!` when triggered: + +```dts +/ { + chosen { + zmk,charmap = &charmap_us; + }; + + behaviors { + hello_world: hello_world { + compatible = "zmk,behavior-send-string"; + #binding-cells = <0>; + text = "Hello, world!"; + }; + }; + + keymap { + compatible = "zmk,keymap"; + + default_layer { + bindings = <&hello_world>; + }; + }; +}; +``` + +:::caution[Character Maps] +You must also select a [character map](#character-maps) so ZMK knows which key to press for each character in the string. +::: + +### Configuration + +#### `text` + +This sets the text to type. It can contain almost any text, however some characters require special escape sequences: + +- Double quotes must be written as `\"`. + - e.g. `text = "This is \"quoted\" text"` types `This is "quoted" text`. +- Backslashes must be written as `\\`. + - e.g. `text = "C:\\Windows\\System32"` types `C:\Windows\System32`. +- `\t` will press tab. +- `\n` will press enter. +- `\x08` will press backspace. +- `\x7F` will press delete. + +#### `charmap` + +Selects the [character map](#character-maps) to use for mapping string characters to keys. + +If this is not set, it defaults to the `zmk,charmap` chosen node. + +#### `tap-ms` and `wait-ms` + +The `tap-ms` property controls how long each key is held. The `wait-ms` property controls how long of a delay there is between key presses. These default to 5 and 0 ms respectively, but they can be increased if strings aren't being typed correctly (at the cost of typing them more slowly). + +```dts +hello_world: hello_world { + compatible = "zmk,behavior-send-string"; + #binding-cells = <0>; + text = "Hello, world!"; + tap-ms = <15>; + wait-ms = <5>; +}; +``` + +You can also change the default values in [your `.conf` file](../../config/index.md) with the `CONFIG_ZMK_SEND_STRING_DEFAULT_TAP_MS` and `CONFIG_ZMK_SEND_STRING_DEFAULT_WAIT_MS` settings, e.g.: + +```kconfig +CONFIG_ZMK_SEND_STRING_DEFAULT_TAP_MS=15 +CONFIG_ZMK_SEND_STRING_DEFAULT_WAIT_MS=5 +``` + +### Convenience C Macro + +You can use the `ZMK_SEND_STRING(name, text)` macro to reduce the boilerplate when defining a new string. + +For example... + +```dts +/ { + behaviors { + ZMK_SEND_STRING(hello_world, "Hello, world!") + }; +}; +``` + +...creates this behavior: + +```dts +/ { + behaviors { + hello_world: hello_world { + compatible = "zmk,behavior-send-string"; + #binding-cells = <0>; + text = "Hello, world!"; + }; + }; +}; +``` + +You can also add a third parameter with extra properties such as [timing configuration](#tap-ms-and-wait-ms) and [character map selection](#character-maps): + +```dts +/ { + behaviors { + ZMK_SEND_STRING(hello_world, "Hello, world!", + tap-ms = <15>; + wait-ms = <5>; + ) + }; +}; +``` + +### Behavior Queue Limit + +Send string behaviors use an internal queue to handle each key press and release. Adding a send string behavior to your keymap will increase the default size of the queue to 256. Each character queues one key press and one release, so this allows 128 characters to be queued. + +If you need to send longer strings, you can change the size of this queue via the `CONFIG_ZMK_BEHAVIORS_QUEUE_SIZE` setting in your [`.conf` file](../../config/index.md). For example, `CONFIG_ZMK_BEHAVIORS_QUEUE_SIZE=512` would allow a string of 256 characters. + +## Character Maps + +You must select a character map for ZMK to know which key to press to type each character. ZMK provides one character map, `&charmap_us`, which is designed to work if your operating system is set to a US keyboard layout. If your OS is set to a different layout, you can [create a new character map](#creating-character-maps). + +To set the character map to use by default, set the `zmk,charmap` chosen node: + +```dts +/ { + chosen { + zmk,charmap = &charmap_us; + }; +}; +``` + +You can also override this for individual send string behaviors with the `charmap` property: + +```dts +/ { + behaviors { + ZMK_SEND_STRING(hello_world, "Hello, world!", + charmap = <&charmap_us>; + ) + }; +}; +``` + +:::note +Properties with a `-map` suffix have a special meaning in Zephyr, so the property is named `charmap` instead of `character-map`. +::: + +### Creating Character Maps + +If your OS is set to a non-US keyboard layout, you will need to create a matching character map. + +Add a node to your keymap with the following properties: + +```dts +/ { + charmap_name: charmap_name { + compatible = "zmk,character-map"; + behavior = <&kp>; + map = + , + ... + ; + }; +}; +``` + +The `behavior` property selects the behavior that the key codes will be sent to. This will typically be `&kp`. + +The `map` property is a list of pairs of values. The first value in each pair is the Unicode [code point](https://en.wikipedia.org/wiki/List_of_Unicode_characters) of a character, and the second value is the key code to send to `&kp` to type that character. Add a pair for every character that you want to use. + +A character map for German might look like this (many characters are omitted from this example for brevity): + +```dts +#include +#include + +#define DE_A A +... +#define DE_Y Z +#define DE_Z Y +#define DE_A_UMLAUT SQT +#define DE_O_UMLAUT SEMI +#define DE_U_UMLAUT LBKT +... + +/ { + charmap_de: charmap_de { + compatible = "zmk,character-map"; + behavior = <&kp>; + map = <0x08 BACKSPACE> + ... + , <0x20 SPACE> + , <0x21 DE_EXCL> // ! + , <0x22 DE_DQT> // " + ... + , <0x41 LS(DE_A)> // A + , <0x42 LS(DE_B)> // B + ... + , <0x59 LS(DE_Y)> // Y + , <0x5A LS(DE_Z)> // Z + ... + , <0x61 DE_A> // a + , <0x62 DE_B> // b + ... + , <0x79 DE_Y> // y + , <0x7A DE_Z> // z + , <0x7B DE_LBRC> // [ + , <0x7C DE_PIPE> // | + , <0x7D DE_RBRC> // ] + , <0x7E DE_TILDE> // ~ + , <0x7F DELETE> + , <0xC4 LS(DE_A_UMLAUT)> // Ä + , <0xD6 LS(DE_O_UMLAUT)> // Ö + , <0xDC LS(DE_U_UMLAUT)> // Ü + , <0xE4 DE_A_UMLAUT> // ä + , <0xF6 DE_O_UMLAUT> // ö + , <0xFC DE_U_UMLAUT> // ü + ... + ; + }; +}; +``` + +Every character map should typically have the following mappings: + +| Code point | Key code | +| ---------- | ----------- | +| `0x08` | `BACKSPACE` | +| `0x0A` | `RETURN` | +| `0x0B` | `TAB` | +| `0x20` | `SPACE` | +| `0x7F` | `DELETE` | + +You can then select this character map by setting the chosen node to its node label: + +```dts +/ { + chosen { + zmk,charmap = &charmap_de; + }; +}; +``` + +If you want different strings to use different character maps (for example if you have different layers for different OS keyboard layouts), you can set a different `charmap` property on each send string behavior: + +```dts +/ { + behaviors { + ZMK_SEND_STRING(qwerty_us, "qwerty", + charmap = <&charmap_us>; + ) + ZMK_SEND_STRING(qwerty_de, "qwerty", + charmap = <&charmap_de>; + ) + }; +}; +``` + +### Unmapped Characters + +By default, if a string contains a character whose code point is not in the character map, that character will be ignored. If you add a `fallback-behavior` property which refers to a one-parameter behavior, then that behavior will be invoked with the unmapped code point as its parameter instead. + +ZMK does not yet have a behavior for sending arbitrary Unicode characters, but once this is added, it could be used as follows: + +```dts +&uni { + // Unicode behavior configuration... +}; + +/ { + charmap_de: charmap_de { + compatible = "zmk,character-map"; + behavior = <&kp>; + fallback-behavior = <&uni>; + ... + }; +}; +``` diff --git a/docs/sidebars.js b/docs/sidebars.js index 8825746f451..c63c0ed769e 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -69,6 +69,7 @@ module.exports = { "keymaps/behaviors/mod-tap", "keymaps/behaviors/mod-morph", "keymaps/behaviors/macros", + "keymaps/behaviors/send-string", "keymaps/behaviors/key-toggle", "keymaps/behaviors/sticky-key", "keymaps/behaviors/sticky-layer",