diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000..d9bf0d44 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,5 @@ +* @zmkfirmware/core + +/app/boards @zmkfirmware/boards-shields + +/docs @zmkfirmware/docs \ No newline at end of file diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 5fb3827c..b217a1a1 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -52,6 +52,7 @@ if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_BLE_ROLE_CENTRAL) target_sources(app PRIVATE src/behaviors/behavior_none.c) target_sources(app PRIVATE src/behaviors/behavior_sensor_rotate_key_press.c) target_sources_ifdef(CONFIG_ZMK_EXT_POWER app PRIVATE src/behaviors/behavior_ext_power.c) + target_sources(app PRIVATE src/combo.c) target_sources(app PRIVATE src/keymap.c) endif() target_sources_ifdef(CONFIG_ZMK_RGB_UNDERGLOW app PRIVATE src/behaviors/behavior_rgb_underglow.c) diff --git a/app/Kconfig b/app/Kconfig index f5d92a88..0aa291d6 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -251,6 +251,23 @@ config ZMK_EXT_POWER #Power Management endmenu +menu "Combo options" + +config ZMK_COMBO_MAX_PRESSED_COMBOS + int "Maximum number of currently pressed combos" + default 4 + +config ZMK_COMBO_MAX_COMBOS_PER_KEY + int "Maximum number of combos per key" + default 5 + +config ZMK_COMBO_MAX_KEYS_PER_COMBO + int "Maximum number of keys per combo" + default 4 + +#Display/LED Options +endmenu + menu "Advanced" menu "Initialization Priorities" diff --git a/app/dts/bindings/zmk,combos.yaml b/app/dts/bindings/zmk,combos.yaml new file mode 100644 index 00000000..75eaa3e1 --- /dev/null +++ b/app/dts/bindings/zmk,combos.yaml @@ -0,0 +1,22 @@ +# Copyright (c) 2020, The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Combos container + +compatible: "zmk,combos" + +child-binding: + description: "A combo" + + properties: + bindings: + type: phandle-array + required: true + key-positions: + type: array + required: true + timeout-ms: + type: int + default: 50 + slow-release: + type: boolean diff --git a/app/src/combo.c b/app/src/combo.c new file mode 100644 index 00000000..49638703 --- /dev/null +++ b/app/src/combo.c @@ -0,0 +1,466 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_combos + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) + +struct combo_cfg { + int32_t key_positions[CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO]; + int32_t key_position_len; + struct zmk_behavior_binding behavior; + int32_t timeout_ms; + // if slow release is set, the combo releases when the last key is released. + // otherwise, the combo releases when the first key is released. + bool slow_release; + // the virtual key position is a key position outside the range used by the keyboard. + // it is necessary so hold-taps can uniquely identify a behavior. + int32_t virtual_key_position; +}; + +struct active_combo { + struct combo_cfg *combo; + // key_positions_pressed is filled with key_positions when the combo is pressed. + // The keys are removed from this array when they are released. + // Once this array is empty, the behavior is released. + struct position_state_changed *key_positions_pressed[CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO]; +}; + +struct combo_candidate { + struct combo_cfg *combo; + // the time after which this behavior should be removed from candidates. + // by keeping track of when the candidate should be cleared there is no + // possibility of accidental releases. + int64_t timeout_at; +}; + +// set of keys pressed +struct position_state_changed *pressed_keys[CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO] = {NULL}; +// the set of candidate combos based on the currently pressed_keys +struct combo_candidate candidates[CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY]; +// the last candidate that was completely pressed +struct combo_cfg *fully_pressed_combo = NULL; +// a lookup dict that maps a key position to all combos on that position +struct combo_cfg *combo_lookup[ZMK_KEYMAP_LEN][CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY] = {NULL}; +// combos that have been activated and still have (some) keys pressed +// this array is always contiguous from 0. +struct active_combo active_combos[CONFIG_ZMK_COMBO_MAX_PRESSED_COMBOS] = {NULL}; +int active_combo_count = 0; + +struct k_delayed_work timeout_task; +int64_t timeout_task_timeout_at; + +// Store the combo key pointer in the combos array, one pointer for each key position +// The combos are sorted shortest-first, then by virtual-key-position. +static int initialize_combo(struct combo_cfg *new_combo) { + for (int i = 0; i < new_combo->key_position_len; i++) { + int32_t position = new_combo->key_positions[i]; + if (position >= ZMK_KEYMAP_LEN) { + LOG_ERR("Unable to initialize combo, key position %d does not exist", position); + return -EINVAL; + } + + struct combo_cfg *insert_combo = new_combo; + bool set = false; + for (int j = 0; j < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; j++) { + struct combo_cfg *combo_at_j = combo_lookup[position][j]; + if (combo_at_j == NULL) { + combo_lookup[position][j] = insert_combo; + set = true; + break; + } + if (combo_at_j->key_position_len < insert_combo->key_position_len || + (combo_at_j->key_position_len == insert_combo->key_position_len && + combo_at_j->virtual_key_position < insert_combo->virtual_key_position)) { + continue; + } + // put insert_combo in this spot, move all other combos up. + combo_lookup[position][j] = insert_combo; + insert_combo = combo_at_j; + } + if (!set) { + LOG_ERR("Too many combos for key position %d, CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY %d.", + position, CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY); + return -ENOMEM; + } + } + return 0; +} + +static int setup_candidates_for_first_keypress(int32_t position, int64_t timestamp) { + for (int i = 0; i < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; i++) { + struct combo_cfg *combo = combo_lookup[position][i]; + if (combo == NULL) { + return i; + } + candidates[i].combo = combo; + candidates[i].timeout_at = timestamp + combo->timeout_ms; + // LOG_DBG("combo timeout %d %d %d", position, i, candidates[i].timeout_at); + } + return CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; +} + +static int filter_candidates(int32_t position) { + // this code iterates over candidates and the lookup together to filter in O(n) + // assuming they are both sorted on key_position_len, virtal_key_position + int matches = 0, lookup_idx = 0, candidate_idx = 0; + while (lookup_idx < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY && + candidate_idx < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY) { + struct combo_cfg *candidate = candidates[candidate_idx].combo; + struct combo_cfg *lookup = combo_lookup[position][lookup_idx]; + if (candidate == NULL || lookup == NULL) { + break; + } + if (candidate->virtual_key_position == lookup->virtual_key_position) { + candidates[matches] = candidates[candidate_idx]; + matches++; + candidate_idx++; + lookup_idx++; + } else if (candidate->key_position_len > lookup->key_position_len) { + lookup_idx++; + } else if (candidate->key_position_len < lookup->key_position_len) { + candidate_idx++; + } else if (candidate->virtual_key_position > lookup->virtual_key_position) { + lookup_idx++; + } else if (candidate->virtual_key_position < lookup->virtual_key_position) { + candidate_idx++; + } + } + // clear unmatched candidates + for (int i = matches; i < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; i++) { + candidates[i].combo = NULL; + } + // LOG_DBG("combo matches after filter %d", matches); + return matches; +} + +static int64_t first_candidate_timeout() { + int64_t first_timeout = LONG_MAX; + for (int i = 0; i < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; i++) { + if (candidates[i].combo == NULL) { + break; + } + if (candidates[i].timeout_at < first_timeout) { + first_timeout = candidates[i].timeout_at; + } + } + return first_timeout; +} + +static inline bool candidate_is_completely_pressed(struct combo_cfg *candidate) { + // this code assumes set(pressed_keys) <= set(candidate->key_positions) + // this invariant is enforced by filter_candidates + // the only thing we need to do is check if len(pressed_keys) == len(combo->key_positions) + return pressed_keys[candidate->key_position_len - 1] != NULL; +} + +static void cleanup(); + +static int filter_timed_out_candidates(int64_t timestamp) { + int num_candidates = 0; + for (int i = 0; i < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; i++) { + struct combo_candidate *candidate = &candidates[i]; + if (candidate->combo == NULL) { + break; + } + if (candidate->timeout_at > timestamp) { + // reorder candidates so they're contiguous + candidates[num_candidates].combo = candidate->combo; + candidates[num_candidates].timeout_at = candidates->timeout_at; + num_candidates++; + } else { + candidate->combo = NULL; + } + } + return num_candidates; +} + +static int clear_candidates() { + for (int i = 0; i < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; i++) { + if (candidates[i].combo == NULL) { + return i; + } + candidates[i].combo = NULL; + } + return CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; +} + +static int capture_pressed_key(struct position_state_changed *ev) { + for (int i = 0; i < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; i++) { + if (pressed_keys[i] != NULL) { + continue; + } + pressed_keys[i] = ev; + return ZMK_EV_EVENT_CAPTURED; + } + return 0; +} + +const struct zmk_listener zmk_listener_combo; + +static void release_pressed_keys() { + // release the first key that was pressed + if (pressed_keys[0] == NULL) { + return; + } + ZMK_EVENT_RELEASE(pressed_keys[0]) + pressed_keys[0] = NULL; + + // reprocess events (see tests/combo/fully-overlapping-combos-3 for why this is needed) + for (int i = 1; i < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; i++) { + if (pressed_keys[i] == NULL) { + return; + } + struct position_state_changed *captured_event = pressed_keys[i]; + pressed_keys[i] = NULL; + ZMK_EVENT_RAISE(captured_event); + } +} + +static inline int press_combo_behavior(struct combo_cfg *combo, int32_t timestamp) { + struct zmk_behavior_binding_event event = { + .position = combo->virtual_key_position, + .timestamp = timestamp, + }; + + return behavior_keymap_binding_pressed(&combo->behavior, event); +} + +static inline int release_combo_behavior(struct combo_cfg *combo, int32_t timestamp) { + struct zmk_behavior_binding_event event = { + .position = combo->virtual_key_position, + .timestamp = timestamp, + }; + + return behavior_keymap_binding_released(&combo->behavior, event); +} + +static void move_pressed_keys_to_active_combo(struct active_combo *active_combo) { + int combo_length = active_combo->combo->key_position_len; + for (int i = 0; i < combo_length; i++) { + active_combo->key_positions_pressed[i] = pressed_keys[i]; + pressed_keys[i] = NULL; + } + // move any other pressed keys up + for (int i = 0; i + combo_length < CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO; i++) { + if (pressed_keys[i + combo_length] == NULL) { + return; + } + pressed_keys[i] = pressed_keys[i + combo_length]; + pressed_keys[i + combo_length] = NULL; + } +} + +static struct active_combo *store_active_combo(struct combo_cfg *combo) { + for (int i = 0; i < CONFIG_ZMK_COMBO_MAX_PRESSED_COMBOS; i++) { + if (active_combos[i].combo == NULL) { + active_combos[i].combo = combo; + active_combo_count++; + return &active_combos[i]; + } + } + LOG_ERR("Unable to store combo; already %d active. Increase " + "CONFIG_ZMK_COMBO_MAX_PRESSED_COMBOS", + CONFIG_ZMK_COMBO_MAX_PRESSED_COMBOS); + return NULL; +} + +static void activate_combo(struct combo_cfg *combo) { + struct active_combo *active_combo = store_active_combo(combo); + if (active_combo == NULL) { + // unable to store combo + release_pressed_keys(); + return; + } + move_pressed_keys_to_active_combo(active_combo); + press_combo_behavior(combo, active_combo->key_positions_pressed[0]->timestamp); +} + +static void deactivate_combo(int active_combo_index) { + active_combo_count--; + if (active_combo_index != active_combo_count) { + memcpy(&active_combos[active_combo_index], &active_combos[active_combo_count], + sizeof(struct active_combo)); + } + active_combos[active_combo_count].combo = NULL; + active_combos[active_combo_count] = (struct active_combo){0}; +} + +/* returns true if a key was released. */ +static bool release_combo_key(int32_t position, int64_t timestamp) { + for (int combo_idx = 0; combo_idx < active_combo_count; combo_idx++) { + struct active_combo *active_combo = &active_combos[combo_idx]; + + bool key_released = false; + bool all_keys_pressed = true; + bool all_keys_released = true; + for (int i = 0; i < active_combo->combo->key_position_len; i++) { + if (active_combo->key_positions_pressed[i] == NULL) { + all_keys_pressed = false; + } else if (active_combo->key_positions_pressed[i]->position != position) { + all_keys_released = false; + } else { // not null and position matches + k_free(active_combo->key_positions_pressed[i]); + active_combo->key_positions_pressed[i] = NULL; + key_released = true; + } + } + + if (key_released) { + if ((active_combo->combo->slow_release && all_keys_released) || + (!active_combo->combo->slow_release && all_keys_pressed)) { + release_combo_behavior(active_combo->combo, timestamp); + } + if (all_keys_released) { + deactivate_combo(combo_idx); + } + return true; + } + } + return false; +} + +static void cleanup() { + k_delayed_work_cancel(&timeout_task); + clear_candidates(); + if (fully_pressed_combo != NULL) { + activate_combo(fully_pressed_combo); + fully_pressed_combo = NULL; + } + release_pressed_keys(); +} + +static void update_timeout_task() { + int64_t first_timeout = first_candidate_timeout(); + if (timeout_task_timeout_at == first_timeout) { + return; + } + if (first_timeout == LLONG_MAX) { + timeout_task_timeout_at = 0; + k_delayed_work_cancel(&timeout_task); + return; + } + if (k_delayed_work_submit(&timeout_task, K_MSEC(first_timeout - k_uptime_get())) == 0) { + timeout_task_timeout_at = first_timeout; + } +} + +static int position_state_down(struct position_state_changed *ev) { + int num_candidates; + if (candidates[0].combo == NULL) { + num_candidates = setup_candidates_for_first_keypress(ev->position, ev->timestamp); + if (num_candidates == 0) { + return 0; + } + } else { + filter_timed_out_candidates(ev->timestamp); + num_candidates = filter_candidates(ev->position); + } + update_timeout_task(); + + struct combo_cfg *candidate_combo = candidates[0].combo; + int ret = capture_pressed_key(ev); + switch (num_candidates) { + case 0: + cleanup(); + return ret; + case 1: + if (candidate_is_completely_pressed(candidate_combo)) { + fully_pressed_combo = candidate_combo; + cleanup(); + } + return ret; + default: + if (candidate_is_completely_pressed(candidate_combo)) { + fully_pressed_combo = candidate_combo; + } + return ret; + } +} + +static int position_state_up(struct position_state_changed *ev) { + cleanup(); + if (release_combo_key(ev->position, ev->timestamp)) { + return ZMK_EV_EVENT_HANDLED; + } else { + return 0; + } +} + +static void combo_timeout_handler(struct k_work *item) { + if (timeout_task_timeout_at == 0 || k_uptime_get() < timeout_task_timeout_at) { + // timer was cancelled or rescheduled. + return; + } + if (filter_timed_out_candidates(timeout_task_timeout_at) < 2) { + cleanup(); + } + update_timeout_task(); +} + +static int position_state_changed_listener(const struct zmk_event_header *eh) { + if (!is_position_state_changed(eh)) { + return 0; + } + + struct position_state_changed *ev = cast_position_state_changed(eh); + if (ev->state) { // keydown + return position_state_down(ev); + } else { // keyup + return position_state_up(ev); + } +} + +ZMK_LISTENER(combo, position_state_changed_listener); +ZMK_SUBSCRIPTION(combo, position_state_changed); + +// todo: remove this once #506 is merged and #include +#define KEY_BINDING_TO_STRUCT(idx, drv_inst) \ + { \ + .behavior_dev = DT_LABEL(DT_PHANDLE_BY_IDX(drv_inst, bindings, idx)), \ + .param1 = COND_CODE_0(DT_PHA_HAS_CELL_AT_IDX(drv_inst, bindings, idx, param1), (0), \ + (DT_PHA_BY_IDX(drv_inst, bindings, idx, param1))), \ + .param2 = COND_CODE_0(DT_PHA_HAS_CELL_AT_IDX(drv_inst, bindings, idx, param2), (0), \ + (DT_PHA_BY_IDX(drv_inst, bindings, idx, param2))), \ + } + +#define COMBO_INST(n) \ + static struct combo_cfg combo_config_##n = { \ + .timeout_ms = DT_PROP(n, timeout_ms), \ + .key_positions = DT_PROP(n, key_positions), \ + .key_position_len = DT_PROP_LEN(n, key_positions), \ + .behavior = KEY_BINDING_TO_STRUCT(0, n), \ + .virtual_key_position = ZMK_KEYMAP_LEN + __COUNTER__, \ + .slow_release = DT_PROP(n, slow_release), \ + }; + +#define INITIALIZE_COMBO(n) initialize_combo(&combo_config_##n); + +DT_INST_FOREACH_CHILD(0, COMBO_INST) + +static int combo_init() { + k_delayed_work_init(&timeout_task, combo_timeout_handler); + DT_INST_FOREACH_CHILD(0, INITIALIZE_COMBO); + return 0; +} + +SYS_INIT(combo_init, APPLICATION, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT); + +#endif \ No newline at end of file diff --git a/app/tests/combo/combos-and-holdtaps-0/events.patterns b/app/tests/combo/combos-and-holdtaps-0/events.patterns new file mode 100644 index 00000000..b90d7863 --- /dev/null +++ b/app/tests/combo/combos-and-holdtaps-0/events.patterns @@ -0,0 +1,2 @@ +s/.*hid_listener_keycode_//p +s/.*combo//p \ No newline at end of file diff --git a/app/tests/combo/combos-and-holdtaps-0/keycode_events.snapshot b/app/tests/combo/combos-and-holdtaps-0/keycode_events.snapshot new file mode 100644 index 00000000..ad86b269 --- /dev/null +++ b/app/tests/combo/combos-and-holdtaps-0/keycode_events.snapshot @@ -0,0 +1,4 @@ +pressed: usage_page 0x07 keycode 0xe0 mods 0x00 +pressed: usage_page 0x07 keycode 0x1c mods 0x00 +released: usage_page 0x07 keycode 0xe0 mods 0x00 +released: usage_page 0x07 keycode 0x1c mods 0x00 diff --git a/app/tests/combo/combos-and-holdtaps-0/native_posix.keymap b/app/tests/combo/combos-and-holdtaps-0/native_posix.keymap new file mode 100644 index 00000000..d35c7277 --- /dev/null +++ b/app/tests/combo/combos-and-holdtaps-0/native_posix.keymap @@ -0,0 +1,47 @@ +#include +#include +#include + +&mt { + flavor = "hold-preferred"; +}; + +/* +This test fails if the order of event handlers for hold-taps +and combos is wrong. Hold-taps need to process key position events +first so the decision to hold or tap can be made. +*/ +/ { + combos { + compatible = "zmk,combos"; + + combo_two { + timeout-ms = <100>; + key-positions = <1 2>; + bindings = <&kp Y>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &mt LEFT_CONTROL A &kp B + &kp C &none + >; + }; + }; +}; + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(0,2,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(0,2,10) + >; +}; \ No newline at end of file diff --git a/app/tests/combo/combos-and-holdtaps-1/events.patterns b/app/tests/combo/combos-and-holdtaps-1/events.patterns new file mode 100644 index 00000000..b90d7863 --- /dev/null +++ b/app/tests/combo/combos-and-holdtaps-1/events.patterns @@ -0,0 +1,2 @@ +s/.*hid_listener_keycode_//p +s/.*combo//p \ No newline at end of file diff --git a/app/tests/combo/combos-and-holdtaps-1/keycode_events.snapshot b/app/tests/combo/combos-and-holdtaps-1/keycode_events.snapshot new file mode 100644 index 00000000..dc4dbb49 --- /dev/null +++ b/app/tests/combo/combos-and-holdtaps-1/keycode_events.snapshot @@ -0,0 +1,4 @@ +pressed: usage_page 0x07 keycode 0x1c mods 0x00 +pressed: usage_page 0x07 keycode 0x06 mods 0x00 +released: usage_page 0x07 keycode 0x1c mods 0x00 +released: usage_page 0x07 keycode 0x06 mods 0x00 diff --git a/app/tests/combo/combos-and-holdtaps-1/native_posix.keymap b/app/tests/combo/combos-and-holdtaps-1/native_posix.keymap new file mode 100644 index 00000000..a99c15d9 --- /dev/null +++ b/app/tests/combo/combos-and-holdtaps-1/native_posix.keymap @@ -0,0 +1,42 @@ +#include +#include +#include + +&mt { + flavor = "hold-preferred"; +}; + +/* this test checks if hold-taps can be part of a combo */ +/ { + combos { + compatible = "zmk,combos"; + combo_two { + timeout-ms = <100>; + key-positions = <0 1>; + bindings = <&kp Y>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &mt LEFT_CONTROL A &kp B + &kp C &none + >; + }; + }; +}; + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(0,2,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(0,2,10) + >; +}; \ No newline at end of file diff --git a/app/tests/combo/combos-and-holdtaps-2/events.patterns b/app/tests/combo/combos-and-holdtaps-2/events.patterns new file mode 100644 index 00000000..b90d7863 --- /dev/null +++ b/app/tests/combo/combos-and-holdtaps-2/events.patterns @@ -0,0 +1,2 @@ +s/.*hid_listener_keycode_//p +s/.*combo//p \ No newline at end of file diff --git a/app/tests/combo/combos-and-holdtaps-2/keycode_events.snapshot b/app/tests/combo/combos-and-holdtaps-2/keycode_events.snapshot new file mode 100644 index 00000000..a6508804 --- /dev/null +++ b/app/tests/combo/combos-and-holdtaps-2/keycode_events.snapshot @@ -0,0 +1,2 @@ +pressed: usage_page 0x07 keycode 0xe0 mods 0x00 +pressed: usage_page 0x07 keycode 0xe4 mods 0x00 diff --git a/app/tests/combo/combos-and-holdtaps-2/native_posix.keymap b/app/tests/combo/combos-and-holdtaps-2/native_posix.keymap new file mode 100644 index 00000000..f8dbe450 --- /dev/null +++ b/app/tests/combo/combos-and-holdtaps-2/native_posix.keymap @@ -0,0 +1,45 @@ +#include +#include +#include + +&mt { + flavor = "hold-preferred"; +}; + +/* This test verifies that hold-tap keys can observe + * events which were released from combos. + */ +/ { + combos { + compatible = "zmk,combos"; + combo_one { + timeout-ms = <100>; + key-positions = <0 2>; + bindings = <&kp Y>; + }; + combo_two { + timeout-ms = <100>; + key-positions = <1 3>; + bindings = <&kp Z>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &mt LEFT_CONTROL A &mt RIGHT_CONTROL B + &none &none + >; + }; + }; +}; + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,0) + ZMK_MOCK_PRESS(0,1,300) + >; +}; \ No newline at end of file diff --git a/app/tests/combo/multiple-timeouts/events.patterns b/app/tests/combo/multiple-timeouts/events.patterns new file mode 100644 index 00000000..b1342af4 --- /dev/null +++ b/app/tests/combo/multiple-timeouts/events.patterns @@ -0,0 +1 @@ +s/.*hid_listener_keycode_//p diff --git a/app/tests/combo/multiple-timeouts/keycode_events.snapshot b/app/tests/combo/multiple-timeouts/keycode_events.snapshot new file mode 100644 index 00000000..c5bdd6e0 --- /dev/null +++ b/app/tests/combo/multiple-timeouts/keycode_events.snapshot @@ -0,0 +1,4 @@ +pressed: usage_page 0x07 keycode 0x04 mods 0x00 +pressed: usage_page 0x07 keycode 0x05 mods 0x00 +released: usage_page 0x07 keycode 0x04 mods 0x00 +released: usage_page 0x07 keycode 0x05 mods 0x00 diff --git a/app/tests/combo/multiple-timeouts/native_posix.keymap b/app/tests/combo/multiple-timeouts/native_posix.keymap new file mode 100644 index 00000000..91bf5235 --- /dev/null +++ b/app/tests/combo/multiple-timeouts/native_posix.keymap @@ -0,0 +1,40 @@ +#include +#include +#include + +/ { + combos { + compatible = "zmk,combos"; + combo_one { + timeout-ms = <30>; + key-positions = <0 1>; + bindings = <&kp C>; + }; + combo_two { + timeout-ms = <120>; + key-positions = <0 1 2>; + bindings = <&kp C>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &kp A &kp B + &none &none + >; + }; + }; +}; + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,100) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + >; +}; \ No newline at end of file diff --git a/app/tests/combo/overlapping-combos-0/events.patterns b/app/tests/combo/overlapping-combos-0/events.patterns new file mode 100644 index 00000000..b90d7863 --- /dev/null +++ b/app/tests/combo/overlapping-combos-0/events.patterns @@ -0,0 +1,2 @@ +s/.*hid_listener_keycode_//p +s/.*combo//p \ No newline at end of file diff --git a/app/tests/combo/overlapping-combos-0/keycode_events.snapshot b/app/tests/combo/overlapping-combos-0/keycode_events.snapshot new file mode 100644 index 00000000..ec63b77f --- /dev/null +++ b/app/tests/combo/overlapping-combos-0/keycode_events.snapshot @@ -0,0 +1,20 @@ +pressed: usage_page 0x07 keycode 0x1b mods 0x00 +released: usage_page 0x07 keycode 0x1b mods 0x00 +pressed: usage_page 0x07 keycode 0x1b mods 0x00 +released: usage_page 0x07 keycode 0x1b mods 0x00 +pressed: usage_page 0x07 keycode 0x1b mods 0x00 +released: usage_page 0x07 keycode 0x1b mods 0x00 +pressed: usage_page 0x07 keycode 0x1b mods 0x00 +released: usage_page 0x07 keycode 0x1b mods 0x00 +pressed: usage_page 0x07 keycode 0x1b mods 0x00 +released: usage_page 0x07 keycode 0x1b mods 0x00 +pressed: usage_page 0x07 keycode 0x1b mods 0x00 +released: usage_page 0x07 keycode 0x1b mods 0x00 +pressed: usage_page 0x07 keycode 0x1c mods 0x00 +released: usage_page 0x07 keycode 0x1c mods 0x00 +pressed: usage_page 0x07 keycode 0x1c mods 0x00 +released: usage_page 0x07 keycode 0x1c mods 0x00 +pressed: usage_page 0x07 keycode 0x1c mods 0x00 +released: usage_page 0x07 keycode 0x1c mods 0x00 +pressed: usage_page 0x07 keycode 0x1c mods 0x00 +released: usage_page 0x07 keycode 0x1c mods 0x00 diff --git a/app/tests/combo/overlapping-combos-0/native_posix.keymap b/app/tests/combo/overlapping-combos-0/native_posix.keymap new file mode 100644 index 00000000..e3cbf437 --- /dev/null +++ b/app/tests/combo/overlapping-combos-0/native_posix.keymap @@ -0,0 +1,117 @@ +#include +#include +#include + +/* + combo 0 timeout inf + combo 01 timeout inf + combo 0123 timeout inf + press 012 in any combination, release any of those keys + expected: combo 012 on key-release + */ + +/* it is useful to set timeout to a large value when attaching a debugger. */ +#define TIMEOUT (60*60*1000) + +/ { + combos { + compatible = "zmk,combos"; + combo_one { + timeout-ms = ; + key-positions = <0 1 2>; + bindings = <&kp X>; + }; + + combo_two { + timeout-ms = ; + key-positions = <0 2>; + bindings = <&kp Y>; + }; + + combo_three { + timeout-ms = ; + key-positions = <1>; + bindings = <&kp Z>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &kp A &kp B + &kp C &none + >; + }; + }; +}; +&kscan { + events = < + /* all permutations of combo one press, combo triggered by release */ + /* while debugging these, you may want to set the release_timer to a high number */ + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(0,2,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(0,2,10) + + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,2,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,2,10) + ZMK_MOCK_RELEASE(0,1,10) + + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,2,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,2,10) + + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(0,2,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(0,2,10) + ZMK_MOCK_RELEASE(0,0,10) + + ZMK_MOCK_PRESS(0,2,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,2,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + + ZMK_MOCK_PRESS(0,2,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,2,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + + /* all permutations of combo two press and release, combo triggered by release */ + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,2,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,2,10) + + ZMK_MOCK_PRESS(0,2,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,2,10) + ZMK_MOCK_RELEASE(0,0,10) + + ZMK_MOCK_PRESS(0,2,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,2,10) + + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,2,10) + ZMK_MOCK_RELEASE(0,2,10) + ZMK_MOCK_RELEASE(0,0,10) + >; +}; \ No newline at end of file diff --git a/app/tests/combo/overlapping-combos-1/events.patterns b/app/tests/combo/overlapping-combos-1/events.patterns new file mode 100644 index 00000000..b90d7863 --- /dev/null +++ b/app/tests/combo/overlapping-combos-1/events.patterns @@ -0,0 +1,2 @@ +s/.*hid_listener_keycode_//p +s/.*combo//p \ No newline at end of file diff --git a/app/tests/combo/overlapping-combos-1/keycode_events.snapshot b/app/tests/combo/overlapping-combos-1/keycode_events.snapshot new file mode 100644 index 00000000..daf72478 --- /dev/null +++ b/app/tests/combo/overlapping-combos-1/keycode_events.snapshot @@ -0,0 +1,8 @@ +pressed: usage_page 0x07 keycode 0x1c mods 0x00 +released: usage_page 0x07 keycode 0x1c mods 0x00 +pressed: usage_page 0x07 keycode 0x1c mods 0x00 +released: usage_page 0x07 keycode 0x1c mods 0x00 +pressed: usage_page 0x07 keycode 0x1c mods 0x00 +released: usage_page 0x07 keycode 0x1c mods 0x00 +pressed: usage_page 0x07 keycode 0x1c mods 0x00 +released: usage_page 0x07 keycode 0x1c mods 0x00 diff --git a/app/tests/combo/overlapping-combos-1/native_posix.keymap b/app/tests/combo/overlapping-combos-1/native_posix.keymap new file mode 100644 index 00000000..c228c475 --- /dev/null +++ b/app/tests/combo/overlapping-combos-1/native_posix.keymap @@ -0,0 +1,65 @@ +#include +#include +#include + +/* + combo 01 timeout 50 + combo 012 timeout 100 + AB is pressed within 50ms, C is never pressed. + expected outcome: AB after 100ms +*/ +/ { + combos { + compatible = "zmk,combos"; + combo_two { + timeout-ms = <50>; + key-positions = <0 1>; + bindings = <&kp Y>; + }; + + combo_three { + timeout-ms = <100>; + key-positions = <0 1 2>; + bindings = <&kp X>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &kp A &kp B + &kp C &none + >; + }; + }; +}; + +&kscan { + events = < + /* if you're debugging these, remember that the timer can be triggered between + events while stepping through code. */ + /* all permutations of combo two press and release, combo triggered by timeout */ + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,100) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(0,0,100) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(0,0,100) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,100) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + >; +}; \ No newline at end of file diff --git a/app/tests/combo/overlapping-combos-2/events.patterns b/app/tests/combo/overlapping-combos-2/events.patterns new file mode 100644 index 00000000..b90d7863 --- /dev/null +++ b/app/tests/combo/overlapping-combos-2/events.patterns @@ -0,0 +1,2 @@ +s/.*hid_listener_keycode_//p +s/.*combo//p \ No newline at end of file diff --git a/app/tests/combo/overlapping-combos-2/keycode_events.snapshot b/app/tests/combo/overlapping-combos-2/keycode_events.snapshot new file mode 100644 index 00000000..dc4dbb49 --- /dev/null +++ b/app/tests/combo/overlapping-combos-2/keycode_events.snapshot @@ -0,0 +1,4 @@ +pressed: usage_page 0x07 keycode 0x1c mods 0x00 +pressed: usage_page 0x07 keycode 0x06 mods 0x00 +released: usage_page 0x07 keycode 0x1c mods 0x00 +released: usage_page 0x07 keycode 0x06 mods 0x00 diff --git a/app/tests/combo/overlapping-combos-2/native_posix.keymap b/app/tests/combo/overlapping-combos-2/native_posix.keymap new file mode 100644 index 00000000..3d364213 --- /dev/null +++ b/app/tests/combo/overlapping-combos-2/native_posix.keymap @@ -0,0 +1,52 @@ +#include +#include +#include + +/* + combo 01 timeout 100 + combo 0123 timeout 100 + press 012, wait until timeout runs out + expected: combo 01 after 100ms, immediately followed by key 2. + */ +/ { + combos { + compatible = "zmk,combos"; + combo_two { + timeout-ms = <100>; + key-positions = <0 1>; + bindings = <&kp Y>; + }; + + combo_four { + timeout-ms = <100>; + key-positions = <0 1 2 3>; + bindings = <&kp W>; + }; + + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &kp A &kp B + &kp C &none + >; + }; + }; +}; + +&kscan { + events = < + /* if you're debugging these, remember that the timer can be triggered between + events while stepping through code. */ + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(0,2,100) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(0,2,100) + >; +}; \ No newline at end of file diff --git a/app/tests/combo/overlapping-combos-3/events.patterns b/app/tests/combo/overlapping-combos-3/events.patterns new file mode 100644 index 00000000..b90d7863 --- /dev/null +++ b/app/tests/combo/overlapping-combos-3/events.patterns @@ -0,0 +1,2 @@ +s/.*hid_listener_keycode_//p +s/.*combo//p \ No newline at end of file diff --git a/app/tests/combo/overlapping-combos-3/keycode_events.snapshot b/app/tests/combo/overlapping-combos-3/keycode_events.snapshot new file mode 100644 index 00000000..e0cb655e --- /dev/null +++ b/app/tests/combo/overlapping-combos-3/keycode_events.snapshot @@ -0,0 +1,4 @@ +pressed: usage_page 0x07 keycode 0x04 mods 0x00 +pressed: usage_page 0x07 keycode 0x1c mods 0x00 +released: usage_page 0x07 keycode 0x04 mods 0x00 +released: usage_page 0x07 keycode 0x1c mods 0x00 diff --git a/app/tests/combo/overlapping-combos-3/native_posix.keymap b/app/tests/combo/overlapping-combos-3/native_posix.keymap new file mode 100644 index 00000000..0622dcd0 --- /dev/null +++ b/app/tests/combo/overlapping-combos-3/native_posix.keymap @@ -0,0 +1,53 @@ +#include +#include +#include + +/* + combo 12 timeout 100 + combo 0123 timeout 100 + press 012, release 2 + expected: key pos 0 followed by combo 12 + */ +/ { + combos { + compatible = "zmk,combos"; + combo_two { + timeout-ms = <100>; + key-positions = <1 2>; + bindings = <&kp Y>; + }; + + + combo_four { + timeout-ms = <100>; + key-positions = <0 1 2 3>; + bindings = <&kp W>; + }; + + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &kp A &kp B + &kp C &none + >; + }; + }; +}; + +&kscan { + events = < + /* if you're debugging these, remember that the timer can be triggered between + events while stepping through code. */ + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(0,2,100) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(0,2,100) + >; +}; \ No newline at end of file diff --git a/app/tests/combo/partially-overlapping-combos/events.patterns b/app/tests/combo/partially-overlapping-combos/events.patterns new file mode 100644 index 00000000..b1342af4 --- /dev/null +++ b/app/tests/combo/partially-overlapping-combos/events.patterns @@ -0,0 +1 @@ +s/.*hid_listener_keycode_//p diff --git a/app/tests/combo/partially-overlapping-combos/keycode_events.snapshot b/app/tests/combo/partially-overlapping-combos/keycode_events.snapshot new file mode 100644 index 00000000..adaa64bc --- /dev/null +++ b/app/tests/combo/partially-overlapping-combos/keycode_events.snapshot @@ -0,0 +1,16 @@ +pressed: usage_page 0x07 keycode 0x1b mods 0x00 +released: usage_page 0x07 keycode 0x1b mods 0x00 +pressed: usage_page 0x07 keycode 0x1b mods 0x00 +released: usage_page 0x07 keycode 0x1b mods 0x00 +pressed: usage_page 0x07 keycode 0x1b mods 0x00 +released: usage_page 0x07 keycode 0x1b mods 0x00 +pressed: usage_page 0x07 keycode 0x1b mods 0x00 +released: usage_page 0x07 keycode 0x1b mods 0x00 +pressed: usage_page 0x07 keycode 0x1c mods 0x00 +released: usage_page 0x07 keycode 0x1c mods 0x00 +pressed: usage_page 0x07 keycode 0x1c mods 0x00 +released: usage_page 0x07 keycode 0x1c mods 0x00 +pressed: usage_page 0x07 keycode 0x1c mods 0x00 +released: usage_page 0x07 keycode 0x1c mods 0x00 +pressed: usage_page 0x07 keycode 0x1c mods 0x00 +released: usage_page 0x07 keycode 0x1c mods 0x00 diff --git a/app/tests/combo/partially-overlapping-combos/native_posix.keymap b/app/tests/combo/partially-overlapping-combos/native_posix.keymap new file mode 100644 index 00000000..4e68105f --- /dev/null +++ b/app/tests/combo/partially-overlapping-combos/native_posix.keymap @@ -0,0 +1,84 @@ +#include +#include +#include + +/ { + combos { + compatible = "zmk,combos"; + combo_one { + timeout-ms = <30>; + key-positions = <0 1>; + bindings = <&kp X>; + }; + + combo_two { + timeout-ms = <30>; + key-positions = <0 2>; + bindings = <&kp Y>; + }; + + combo_three { + timeout-ms = <30>; + key-positions = <3>; + bindings = <&kp Z>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &kp A &kp B + &kp C &none + >; + }; + }; +}; + +&kscan { + events = < + /* all permutations of combo one press and release */ + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + + /* all permutations of combo two press and release */ + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,2,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,2,10) + + ZMK_MOCK_PRESS(0,2,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,2,10) + ZMK_MOCK_RELEASE(0,0,10) + + ZMK_MOCK_PRESS(0,2,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,2,10) + + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,2,10) + ZMK_MOCK_RELEASE(0,2,10) + ZMK_MOCK_RELEASE(0,0,10) + >; +}; diff --git a/app/tests/combo/press-release/events.patterns b/app/tests/combo/press-release/events.patterns new file mode 100644 index 00000000..b1342af4 --- /dev/null +++ b/app/tests/combo/press-release/events.patterns @@ -0,0 +1 @@ +s/.*hid_listener_keycode_//p diff --git a/app/tests/combo/press-release/keycode_events.snapshot b/app/tests/combo/press-release/keycode_events.snapshot new file mode 100644 index 00000000..01718e71 --- /dev/null +++ b/app/tests/combo/press-release/keycode_events.snapshot @@ -0,0 +1,8 @@ +pressed: usage_page 0x07 keycode 0x06 mods 0x00 +released: usage_page 0x07 keycode 0x06 mods 0x00 +pressed: usage_page 0x07 keycode 0x06 mods 0x00 +released: usage_page 0x07 keycode 0x06 mods 0x00 +pressed: usage_page 0x07 keycode 0x06 mods 0x00 +released: usage_page 0x07 keycode 0x06 mods 0x00 +pressed: usage_page 0x07 keycode 0x06 mods 0x00 +released: usage_page 0x07 keycode 0x06 mods 0x00 diff --git a/app/tests/combo/press-release/native_posix.keymap b/app/tests/combo/press-release/native_posix.keymap new file mode 100644 index 00000000..0f45792d --- /dev/null +++ b/app/tests/combo/press-release/native_posix.keymap @@ -0,0 +1,51 @@ +#include +#include +#include + +/ { + combos { + compatible = "zmk,combos"; + combo_one { + timeout-ms = <30>; + key-positions = <0 1>; + bindings = <&kp C>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &kp A &kp B + &none &none + >; + }; + }; +}; + +&kscan { + events = < + /* all different combinations of press and release order */ + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + >; +}; \ No newline at end of file diff --git a/app/tests/combo/press-timeout/events.patterns b/app/tests/combo/press-timeout/events.patterns new file mode 100644 index 00000000..b1342af4 --- /dev/null +++ b/app/tests/combo/press-timeout/events.patterns @@ -0,0 +1 @@ +s/.*hid_listener_keycode_//p diff --git a/app/tests/combo/press-timeout/keycode_events.snapshot b/app/tests/combo/press-timeout/keycode_events.snapshot new file mode 100644 index 00000000..c5bdd6e0 --- /dev/null +++ b/app/tests/combo/press-timeout/keycode_events.snapshot @@ -0,0 +1,4 @@ +pressed: usage_page 0x07 keycode 0x04 mods 0x00 +pressed: usage_page 0x07 keycode 0x05 mods 0x00 +released: usage_page 0x07 keycode 0x04 mods 0x00 +released: usage_page 0x07 keycode 0x05 mods 0x00 diff --git a/app/tests/combo/press-timeout/native_posix.keymap b/app/tests/combo/press-timeout/native_posix.keymap new file mode 100644 index 00000000..ff0b7493 --- /dev/null +++ b/app/tests/combo/press-timeout/native_posix.keymap @@ -0,0 +1,35 @@ +#include +#include +#include + +/ { + combos { + compatible = "zmk,combos"; + combo_one { + timeout-ms = <30>; + key-positions = <0 1>; + bindings = <&kp C>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &kp A &kp B + &none &none + >; + }; + }; +}; + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,100) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + >; +}; \ No newline at end of file diff --git a/app/tests/combo/press1-press2-release1-release2/events.patterns b/app/tests/combo/press1-press2-release1-release2/events.patterns new file mode 100644 index 00000000..5f3e4cf7 --- /dev/null +++ b/app/tests/combo/press1-press2-release1-release2/events.patterns @@ -0,0 +1,2 @@ +s/.*hid_listener_keycode_//p +s/.*combo/combo/p \ No newline at end of file diff --git a/app/tests/combo/press1-press2-release1-release2/keycode_events.snapshot b/app/tests/combo/press1-press2-release1-release2/keycode_events.snapshot new file mode 100644 index 00000000..cfa02de2 --- /dev/null +++ b/app/tests/combo/press1-press2-release1-release2/keycode_events.snapshot @@ -0,0 +1,4 @@ +pressed: usage_page 0x07 keycode 0x06 mods 0x00 +pressed: usage_page 0x07 keycode 0x07 mods 0x00 +released: usage_page 0x07 keycode 0x06 mods 0x00 +released: usage_page 0x07 keycode 0x07 mods 0x00 diff --git a/app/tests/combo/press1-press2-release1-release2/native_posix.keymap b/app/tests/combo/press1-press2-release1-release2/native_posix.keymap new file mode 100644 index 00000000..2518bbc9 --- /dev/null +++ b/app/tests/combo/press1-press2-release1-release2/native_posix.keymap @@ -0,0 +1,45 @@ +#include +#include +#include + +/ { + combos { + compatible = "zmk,combos"; + combo_one { + timeout-ms = <30>; + key-positions = <0 1>; + bindings = <&kp C>; + }; + + combo_two { + timeout-ms = <30>; + key-positions = <2 3>; + bindings = <&kp D>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &kp A &kp B + &kp Z &kp Y + >; + }; + }; +}; + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(1,0,10) + ZMK_MOCK_RELEASE(1,1,10) + >; +}; \ No newline at end of file diff --git a/app/tests/combo/press1-press2-release2-release1/events.patterns b/app/tests/combo/press1-press2-release2-release1/events.patterns new file mode 100644 index 00000000..b54b66b6 --- /dev/null +++ b/app/tests/combo/press1-press2-release2-release1/events.patterns @@ -0,0 +1,2 @@ +s/.*hid_listener_keycode_//p +s/.*combo/combo/p diff --git a/app/tests/combo/press1-press2-release2-release1/keycode_events.snapshot b/app/tests/combo/press1-press2-release2-release1/keycode_events.snapshot new file mode 100644 index 00000000..b55f09ba --- /dev/null +++ b/app/tests/combo/press1-press2-release2-release1/keycode_events.snapshot @@ -0,0 +1,4 @@ +pressed: usage_page 0x07 keycode 0x06 mods 0x00 +pressed: usage_page 0x07 keycode 0x07 mods 0x00 +released: usage_page 0x07 keycode 0x07 mods 0x00 +released: usage_page 0x07 keycode 0x06 mods 0x00 diff --git a/app/tests/combo/press1-press2-release2-release1/native_posix.keymap b/app/tests/combo/press1-press2-release2-release1/native_posix.keymap new file mode 100644 index 00000000..4895636e --- /dev/null +++ b/app/tests/combo/press1-press2-release2-release1/native_posix.keymap @@ -0,0 +1,46 @@ +#include +#include +#include + +/ { + combos { + compatible = "zmk,combos"; + combo_one { + timeout-ms = <30>; + key-positions = <0 1>; + bindings = <&kp C>; + }; + + combo_two { + timeout-ms = <30>; + key-positions = <2 3>; + bindings = <&kp D>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &kp A &kp B + &kp Z &kp Y + >; + }; + }; +}; + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_PRESS(1,1,10) + + ZMK_MOCK_RELEASE(1,0,10) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + >; +}; \ No newline at end of file diff --git a/app/tests/combo/press1-release1-press2-release2/events.patterns b/app/tests/combo/press1-release1-press2-release2/events.patterns new file mode 100644 index 00000000..5f3e4cf7 --- /dev/null +++ b/app/tests/combo/press1-release1-press2-release2/events.patterns @@ -0,0 +1,2 @@ +s/.*hid_listener_keycode_//p +s/.*combo/combo/p \ No newline at end of file diff --git a/app/tests/combo/press1-release1-press2-release2/keycode_events.snapshot b/app/tests/combo/press1-release1-press2-release2/keycode_events.snapshot new file mode 100644 index 00000000..c41dee8c --- /dev/null +++ b/app/tests/combo/press1-release1-press2-release2/keycode_events.snapshot @@ -0,0 +1,4 @@ +pressed: usage_page 0x07 keycode 0x06 mods 0x00 +released: usage_page 0x07 keycode 0x06 mods 0x00 +pressed: usage_page 0x07 keycode 0x07 mods 0x00 +released: usage_page 0x07 keycode 0x07 mods 0x00 diff --git a/app/tests/combo/press1-release1-press2-release2/native_posix.keymap b/app/tests/combo/press1-release1-press2-release2/native_posix.keymap new file mode 100644 index 00000000..0c4a698c --- /dev/null +++ b/app/tests/combo/press1-release1-press2-release2/native_posix.keymap @@ -0,0 +1,46 @@ +#include +#include +#include + +/ { + combos { + compatible = "zmk,combos"; + combo_one { + timeout-ms = <30>; + key-positions = <0 1>; + bindings = <&kp C>; + }; + + combo_two { + timeout-ms = <30>; + key-positions = <2 3>; + bindings = <&kp D>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &kp A &kp B + &kp Z &kp Y + >; + }; + }; +}; + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,0,10) + ZMK_MOCK_RELEASE(1,1,10) + >; +}; \ No newline at end of file diff --git a/app/tests/combo/slowrelease-disabled/events.patterns b/app/tests/combo/slowrelease-disabled/events.patterns new file mode 100644 index 00000000..b1342af4 --- /dev/null +++ b/app/tests/combo/slowrelease-disabled/events.patterns @@ -0,0 +1 @@ +s/.*hid_listener_keycode_//p diff --git a/app/tests/combo/slowrelease-disabled/keycode_events.snapshot b/app/tests/combo/slowrelease-disabled/keycode_events.snapshot new file mode 100644 index 00000000..c41dee8c --- /dev/null +++ b/app/tests/combo/slowrelease-disabled/keycode_events.snapshot @@ -0,0 +1,4 @@ +pressed: usage_page 0x07 keycode 0x06 mods 0x00 +released: usage_page 0x07 keycode 0x06 mods 0x00 +pressed: usage_page 0x07 keycode 0x07 mods 0x00 +released: usage_page 0x07 keycode 0x07 mods 0x00 diff --git a/app/tests/combo/slowrelease-disabled/native_posix.keymap b/app/tests/combo/slowrelease-disabled/native_posix.keymap new file mode 100644 index 00000000..3bacb886 --- /dev/null +++ b/app/tests/combo/slowrelease-disabled/native_posix.keymap @@ -0,0 +1,38 @@ +#include +#include +#include + +/ { + combos { + compatible = "zmk,combos"; + combo_one { + timeout-ms = <30>; + key-positions = <0 1>; + bindings = <&kp C>; + /* no slow-release! */ + }; + }; + + keymap { + compatible = "zmk,keymap"; + label = "Default keymap"; + + default_layer { + bindings = < + &kp A &kp B + &kp D &none + >; + }; + }; +}; + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) /* this should release the combo */ + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(1,0,10) + >; +}; \ No newline at end of file diff --git a/app/tests/combo/slowrelease-enabled/events.patterns b/app/tests/combo/slowrelease-enabled/events.patterns new file mode 100644 index 00000000..b1342af4 --- /dev/null +++ b/app/tests/combo/slowrelease-enabled/events.patterns @@ -0,0 +1 @@ +s/.*hid_listener_keycode_//p diff --git a/app/tests/combo/slowrelease-enabled/keycode_events.snapshot b/app/tests/combo/slowrelease-enabled/keycode_events.snapshot new file mode 100644 index 00000000..cfa02de2 --- /dev/null +++ b/app/tests/combo/slowrelease-enabled/keycode_events.snapshot @@ -0,0 +1,4 @@ +pressed: usage_page 0x07 keycode 0x06 mods 0x00 +pressed: usage_page 0x07 keycode 0x07 mods 0x00 +released: usage_page 0x07 keycode 0x06 mods 0x00 +released: usage_page 0x07 keycode 0x07 mods 0x00 diff --git a/app/tests/combo/slowrelease-enabled/native_posix.keymap b/app/tests/combo/slowrelease-enabled/native_posix.keymap new file mode 100644 index 00000000..8ac8316b --- /dev/null +++ b/app/tests/combo/slowrelease-enabled/native_posix.keymap @@ -0,0 +1,38 @@ +#include +#include +#include + +/ { + combos { + compatible = "zmk,combos"; + combo_one { + timeout-ms = <30>; + key-positions = <0 1>; + bindings = <&kp C>; + slow-release; + }; + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &kp A &kp B + &kp D &none + >; + }; + }; +}; + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) /* this should not release the combo yet */ + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(1,0,10) + >; +}; \ No newline at end of file diff --git a/docs/docs/behaviors/combos.md b/docs/docs/behaviors/combos.md new file mode 100644 index 00000000..e9b01761 --- /dev/null +++ b/docs/docs/behaviors/combos.md @@ -0,0 +1,52 @@ +--- +title: Combo Behavior +sidebar_label: Combos +--- + +## Summary + +Combo keys are a way to combine multiple keypresses to output a different key. For example, you can hit the Q and W keys on your keyboard to output escape. + +### Configuration + +Combos are specified like this: + +``` +/ { + combos { + compatible = "zmk,combos"; + combo_esc { + timeout-ms = <50>; + key-positions = <0 1>; + bindings = <&kp ESC>; + }; + }; +}; +``` + +- The name of the combo doesn't really matter, but convention is to start the node name with `combo_`. +- The `compatible` property should always be `"zmk,combos"` for combos. +- `timeout-ms` is the number of milliseconds that all keys of the combo must be pressed. +- `key-positions` is an array of key positions. See the info section below about how to figure out the positions on your board. +- `bindings` is the behavior that is activated when the behavior is pressed. +- (advanced) you can specify `slow-release` if you want the combo binding to be released when all key-positions are released. The default is to release the combo as soon as any of the keys in the combo is released. + +:::info + +Key positions are numbered like the keys in your keymap, starting at 0. So, if the first key in your keymap is `Q`, this key is in position `0`. The next key (possibly `W`) will have position 1, etcetera. + +::: + +### Advanced usage + +- Partially overlapping combos like `0 1` and `0 2` are supported. +- Fully overlapping combos like `0 1` and `0 1 2` are supported. +- You are not limited to `&kp` bindings. You can use all ZMK behaviors there, like `&mo`, `&bt`, `&mt`, `<` etc. + +### Advanced configuration + +There are three global combo parameters which are set through KConfig. You can set them in the `.conf` file in the same directory as your keymap file. + +- `CONFIG_ZMK_COMBO_MAX_PRESSED_COMBOS` is the number of combos that can be active at the same time. Default 4. +- `CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY` is the maximum number of combos that can be active on a key position. Defaults to 5. (So you can have 5 separate combos that use position `3` for example) +- `CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO` is the maximum number of keys that need to be pressed to activate a combo. Default 4. If you want a combo that triggers when pressing 5 keys, you'd set this to 5 for example. diff --git a/docs/docs/intro.md b/docs/docs/intro.md index 57670ea0..057eea57 100644 --- a/docs/docs/intro.md +++ b/docs/docs/intro.md @@ -26,7 +26,11 @@ ZMK is currently missing some features found in other popular firmware. This tab | [Display Support](features/displays)[^2] | 🚧 | 🚧 | ✅ | | [RGB Underglow](features/underglow) | ✅ | ✅ | ✅ | | One Shot Keys | ✅ | ✅ | ✅ | +<<<<<<< HEAD | [Combo Keys](https://github.com/zmkfirmware/zmk/pull/504) | 🚧 | | ✅ | +======= +| [Combo Keys](behaviors/combos) | ✅ | | ✅ | +>>>>>>> feb0d5b90cbbb1a1026bf356afd788c860824ccf | Macros | 🚧 | ✅ | ✅ | | Mouse Keys | 💡 | ✅ | ✅ | | Low Active Power Usage | ✅ | | | diff --git a/docs/docs/intro.md.orig b/docs/docs/intro.md.orig new file mode 100644 index 00000000..057eea57 --- /dev/null +++ b/docs/docs/intro.md.orig @@ -0,0 +1,52 @@ +--- +title: Introduction to ZMK +sidebar_label: Introduction +slug: / +--- + +ZMK Firmware is an open source (MIT) keyboard +firmware built on the [Zephyr™ Project](https://zephyrproject.org/) Real Time Operating System (RTOS). ZMK's goal is to provide a modern, wireless, and powerful firmware free of licensing issues. + +## Features + +ZMK is currently missing some features found in other popular firmware. This table compares the features supported by ZMK, BlueMicro and QMK: + +| **Feature** | ZMK | BlueMicro | QMK | +| ------------------------------------------------------------------------------------------------------------------------- | :-: | :-------: | :-: | +| Low Latency BLE Support | ✅ | ✅ | | +| Multi-Device BLE Support | ✅ | | | +| [USB Connectivity](behaviors/outputs) | ✅ | | ✅ | +| User Configuration Repositories | ✅ | | | +| Split Keyboard Support | ✅ | ✅ | ✅ | +| [Keymaps and Layers](behaviors/layers) | ✅ | ✅ | ✅ | +| [Hold-Tap](behaviors/hold-tap) (which includes [Mod-Tap](behaviors/mod-tap) and [Layer-Tap](behaviors/layers/#layer-tap)) | ✅ | ✅ | ✅ | +| [Keyboard Codes](codes/#keyboard) | ✅ | ✅ | ✅ | +| [Media](codes/#media-controls) & [Consumer](codes/#consumer-controls) Codes | ✅ | ✅ | ✅ | +| [Encoders](features/encoders)[^1] | ✅ | | ✅ | +| [Display Support](features/displays)[^2] | 🚧 | 🚧 | ✅ | +| [RGB Underglow](features/underglow) | ✅ | ✅ | ✅ | +| One Shot Keys | ✅ | ✅ | ✅ | +<<<<<<< HEAD +| [Combo Keys](https://github.com/zmkfirmware/zmk/pull/504) | 🚧 | | ✅ | +======= +| [Combo Keys](behaviors/combos) | ✅ | | ✅ | +>>>>>>> feb0d5b90cbbb1a1026bf356afd788c860824ccf +| Macros | 🚧 | ✅ | ✅ | +| Mouse Keys | 💡 | ✅ | ✅ | +| Low Active Power Usage | ✅ | | | +| Low Power Sleep States | ✅ | ✅ | | +| [Low Power Mode (VCC Shutoff)](behaviors/power) | ✅ | | | +| Battery Reporting | ✅ | ✅ | | +| Shell over BLE | 💡 | | | +| Realtime Keymap Updating | 💡 | | ✅ | +| AVR/8 Bit | | | ✅ | +| [Wide Range of ARM Chips Supported](https://docs.zephyrproject.org/latest/boards/index.html) | ✅ | | | + +[^2]: Encoders are not currently supported on peripheral side splits. +[^1]: OLEDs are currently proof of concept in ZMK. + +## Code Of Conduct + +Please note that this project is released with a +[Contributor Code of Conduct](https://www.contributor-covenant.org/version/2/0/code_of_conduct/). +By participating in this project you agree to abide by its terms. diff --git a/docs/sidebars.js b/docs/sidebars.js index 8fc1dc54..d095a47e 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -20,6 +20,7 @@ module.exports = { "behaviors/misc", "behaviors/hold-tap", "behaviors/mod-tap", + "behaviors/combos", "behaviors/reset", "behaviors/bluetooth", "behaviors/outputs", diff --git a/docs/static/setup.sh b/docs/static/setup.sh index 8ac39435..34f0f054 100644 --- a/docs/static/setup.sh +++ b/docs/static/setup.sh @@ -91,7 +91,7 @@ echo "" echo "Keyboard Shield Selection:" prompt="Pick an keyboard:" -options=("Kyria" "Lily58" "Corne" "Splitreus62" "Sofle" "Iris" "Reviung41" "RoMac" "RoMac+" "makerdiary M60" "Microdox" "TG4X" "QAZ" "NIBBLE" "Jorne" "Jian" "CRBN" "Tidbit" "Eek!" "BF0-9000" "Helix") +options=("Kyria" "Lily58" "Corne" "Splitreus62" "Sofle" "Iris" "Reviung41" "RoMac" "RoMac+" "makerdiary M60" "Microdox" "TG4X" "QAZ" "NIBBLE" "Jorne" "Jian" "CRBN" "Tidbit" "Eek!" "BFO-9000" "Helix") PS3="$prompt " # TODO: Add support for "Other" and linking to docs on adding custom shields in user config repos. diff --git a/docs/static/setup_BACKUP_1333.sh b/docs/static/setup_BACKUP_1333.sh new file mode 100644 index 00000000..34f0f054 --- /dev/null +++ b/docs/static/setup_BACKUP_1333.sh @@ -0,0 +1,228 @@ +#!/bin/bash + +# Copyright (c) 2020 The ZMK Contributors +# SPDX-License-Identifier: MIT + +set -e + +check_exists() { + command_to_run=$1 + error_message=$2 + local __resultvar=$3 + + if ! eval "$command_to_run" &> /dev/null; then + if [[ "$__resultvar" != "" ]]; then + eval $__resultvar="'false'" + else + printf "%s\n" "$error_message" + exit 1 + fi + else + if [[ "$__resultvar" != "" ]]; then + eval $__resultvar="'true'" + fi + fi +} + +check_exists "command -v git" "git is not installed, and is required for this script!" +check_exists "command -v curl" "curl is not installed, and is required for this script!" curl_exists +check_exists "command -v wget" "wget is not installed, and is required for this script!" wget_exists + +check_exists "git config user.name" "Git username not set!\nRun: git config --global user.name 'My Name'" +check_exists "git config user.email" "Git email not set!\nRun: git config --global user.email 'example@myemail.com'" + +# Check to see if the user has write permissions in this directory to prevent a cryptic error later on +if [ ! -w `pwd` ]; then + echo 'Sorry, you do not have write permissions in this directory.'; + echo 'Please try running this script again from a directory that you do have write permissions for.'; + exit 1 +fi + +# Parse all commandline options +while [[ "$#" -gt 0 ]]; do + case $1 in + -w|--wget) force_wget="true"; break;; + *) echo "Unknown parameter: $1"; exit 1;; + esac + shift +done + +if [[ $curl_exists == "true" && $wget_exists == "true" ]]; then + if [[ $force_wget == "true" ]]; then + download_command="wget " + else + download_command="curl -O " + fi +elif [[ $curl_exists == "true" ]]; then + download_command="curl -O " +elif [[ $wget_exists == "true" ]]; then + download_command="wget " +else + echo 'Neither curl nor wget are installed. One of the two is required for this script!' + exit 1 +fi + +repo_path="https://github.com/zmkfirmware/zmk-config-split-template.git" +title="ZMK Config Setup:" + +prompt="Pick an MCU board:" +options=("nice!nano" "QMK Proton-C" "BlueMicro840 (v1)" "makerdiary nRF52840 M.2") + +echo "$title" +echo "" +echo "MCU Board Selection:" +PS3="$prompt " +select opt in "${options[@]}" "Quit"; do + + case "$REPLY" in + + 1 ) board="nice_nano"; break;; + 2 ) board="proton_c"; break;; + 3 ) board="bluemicro840_v1"; break;; + 3 ) board="nrf52840_m2"; break;; + + $(( ${#options[@]}+1 )) ) echo "Goodbye!"; exit 1;; + *) echo "Invalid option. Try another one."; continue;; + + esac +done + +echo "" +echo "Keyboard Shield Selection:" + +prompt="Pick an keyboard:" +options=("Kyria" "Lily58" "Corne" "Splitreus62" "Sofle" "Iris" "Reviung41" "RoMac" "RoMac+" "makerdiary M60" "Microdox" "TG4X" "QAZ" "NIBBLE" "Jorne" "Jian" "CRBN" "Tidbit" "Eek!" "BFO-9000" "Helix") + +PS3="$prompt " +# TODO: Add support for "Other" and linking to docs on adding custom shields in user config repos. +# select opt in "${options[@]}" "Other" "Quit"; do +select opt in "${options[@]}" "Quit"; do + + case "$REPLY" in + + 1 ) shield_title="Kyria" shield="kyria"; split="y"; break;; + 2 ) shield_title="Lily58" shield="lily58"; split="y"; break;; + 3 ) shield_title="Corne" shield="corne"; split="y"; break;; + 4 ) shield_title="Splitreus62" shield="splitreus62"; split="y"; break;; + 5 ) shield_title="Sofle" shield="sofle"; split="y"; break;; + 6 ) shield_title="Iris" shield="iris"; split="y"; break;; + 7 ) shield_title="Reviung41" shield="reviung41"; split="n"; break;; + 8 ) shield_title="RoMac" shield="romac"; split="n"; break;; + 9 ) shield_title="RoMac+" shield="romac_plus"; split="n"; break;; + 10 ) shield_title="M60" shield="m60"; split="n"; break;; + 11 ) shield_title="Microdox" shield="microdox"; split="y"; break;; + 12 ) shield_title="TG4X" shield="tg4x"; split="n"; break;; + 13 ) shield_title="QAZ" shield="qaz"; split="n"; break;; + 14 ) shield_title="NIBBLE" shield="nibble"; split="n"; break;; + 15 ) shield_title="Jorne" shield="jorne"; split="y"; break;; + 16 ) shield_title="Jian" shield="jian"; split="y"; break;; + 17 ) shield_title="CRBN" shield="crbn"; split="n"; break;; + 18 ) shield_title="Tidbit" shield="tidbit"; split="n" break;; + 19 ) shield_title="Eek!" shield="eek"; split="n" break;; + 20 ) shield_title="BFO-9000" shield="bfo9000"; split="y"; break;; + 21 ) shield_title="Helix" shield="helix"; split="y"; break;; + + # Add link to docs on adding your own custom shield in your ZMK config! + # $(( ${#options[@]}+1 )) ) echo "Other!"; break;; + $(( ${#options[@]}+1 )) ) echo "Goodbye!"; exit 1;; + *) echo "Invalid option. Try another one.";continue;; + + esac +done + +if [ "$split" == "n" ]; then + repo_path="https://github.com/zmkfirmware/zmk-config-template.git" +fi + +read -r -e -p "Copy in the stock keymap for customization? [Yn]: " copy_keymap + +if [ -z "$copy_keymap" ] || [ "$copy_keymap" == "Y" ] || [ "$copy_keymap" == "y" ]; then copy_keymap="yes"; fi + +read -r -e -p "GitHub Username (leave empty to skip GitHub repo creation): " github_user +if [ -n "$github_user" ]; then + read -r -p "GitHub Repo Name [zmk-config]: " repo_name + if [ -z "$repo_name" ]; then repo_name="zmk-config"; fi + + read -r -p "GitHub Repo [https://github.com/${github_user}/${repo_name}.git]: " github_repo + + if [ -z "$github_repo" ]; then github_repo="https://github.com/${github_user}/${repo_name}.git"; fi +else + repo_name="zmk-config" +fi + +echo "" +echo "Preparing a user config for:" +echo "* MCU Board: ${board}" +echo "* Shield: ${shield}" + +if [ "$copy_keymap" == "yes" ]; then + echo "* Copy Keymap?: ✓" +else + echo "* Copy Keymap?: ❌" +fi + +if [ -n "$github_repo" ]; then + echo "* GitHub Repo To Push (please create this in GH first!): ${github_repo}" +fi + +echo "" +read -r -p "Continue? [Yn]: " do_it + +if [ -n "$do_it" ] && [ "$do_it" != "y" ] && [ "$do_it" != "Y" ]; then + echo "Aborting..." + exit 1 +fi + +git clone --single-branch $repo_path ${repo_name} +cd ${repo_name} + +pushd config + +$download_command "https://raw.githubusercontent.com/zmkfirmware/zmk/main/app/boards/shields/${shield}/${shield}.conf" + +if [ "$copy_keymap" == "yes" ]; then + $download_command "https://raw.githubusercontent.com/zmkfirmware/zmk/main/app/boards/shields/${shield}/${shield}.keymap" +fi + +popd + +sed -i'.orig' \ + -e "s/BOARD_NAME/$board/" \ + -e "s/SHIELD_NAME/$shield/" \ + -e "s/KEYBOARD_TITLE/$shield_title/" \ + .github/workflows/build.yml + +if [ "$board" == "proton_c" ]; then + # Proton-C board still fa + sed -i'.orig' -e "s/uf2/hex/g" .github/workflows/build.yml +fi + +rm .github/workflows/*.yml.orig + +rm -rf .git +git init . +git add . +git commit -m "Initial User Config." + +if [ -n "$github_repo" ]; then + git remote add origin "$github_repo" + git push --set-upstream origin "$(git symbolic-ref --short HEAD)" + push_return_code=$? + + # If push failed, assume that the origin was incorrect and give instructions on fixing. + if [ ${push_return_code} -ne 0 ]; then + echo "Remote repository $github_repo not found..." + echo "Check GitHub URL, and try adding again." + echo "Run the following: " + echo " git remote rm origin" + echo " git remote add origin FIXED_URL" + echo " git push --set-upstream origin $(git symbolic-ref --short HEAD)" + echo "Once pushed, your firmware should be availalbe from GitHub Actions at: ${github_repo%.git}/actions" + exit 1 + fi + + # TODO: Support determing the actions URL when non-https:// repo URL is used. + if [ "${github_repo}" != "${github_repo#https://}" ]; then + echo "Your firmware should be available from GitHub Actions shortly: ${github_repo%.git}/actions" + fi +fi diff --git a/docs/static/setup_BASE_1333.sh b/docs/static/setup_BASE_1333.sh new file mode 100644 index 00000000..5768fa6b --- /dev/null +++ b/docs/static/setup_BASE_1333.sh @@ -0,0 +1,226 @@ +#!/bin/bash + +# Copyright (c) 2020 The ZMK Contributors +# +# SPDX-License-Identifier: MIT + +set -e + +check_exists() { + command_to_run=$1 + error_message=$2 + local __resultvar=$3 + + if ! eval "$command_to_run" &> /dev/null; then + if [[ "$__resultvar" != "" ]]; then + eval $__resultvar="'false'" + else + printf "%s\n" "$error_message" + exit 1 + fi + else + if [[ "$__resultvar" != "" ]]; then + eval $__resultvar="'true'" + fi + fi +} + +check_exists "command -v git" "git is not installed, and is required for this script!" +check_exists "command -v curl" "curl is not installed, and is required for this script!" curl_exists +check_exists "command -v wget" "wget is not installed, and is required for this script!" wget_exists + +check_exists "git config user.name" "Git username not set!\nRun: git config --global user.name 'My Name'" +check_exists "git config user.email" "Git email not set!\nRun: git config --global user.email 'example@myemail.com'" + +# Check to see if the user has write permissions in this directory to prevent a cryptic error later on +if [ ! -w `pwd` ]; then + echo 'Sorry, you do not have write permissions in this directory.'; + echo 'Please try running this script again from a directory that you do have write permissions for.'; + exit 1 +fi + +# Parse all commandline options +while [[ "$#" -gt 0 ]]; do + case $1 in + -w|--wget) force_wget="true"; break;; + *) echo "Unknown parameter: $1"; exit 1;; + esac + shift +done + +if [[ $curl_exists == "true" && $wget_exists == "true" ]]; then + if [[ $force_wget == "true" ]]; then + download_command="wget " + else + download_command="curl -O " + fi +elif [[ $curl_exists == "true" ]]; then + download_command="curl -O " +elif [[ $wget_exists == "true" ]]; then + download_command="wget " +else + echo 'Neither curl nor wget are installed. One of the two is required for this script!' + exit 1 +fi + +repo_path="https://github.com/zmkfirmware/zmk-config-split-template.git" +title="ZMK Config Setup:" + +prompt="Pick an MCU board:" +options=("nice!nano" "QMK Proton-C" "BlueMicro840 (v1)" "makerdiary nRF52840 M.2") + +echo "$title" +echo "" +echo "MCU Board Selection:" +PS3="$prompt " +select opt in "${options[@]}" "Quit"; do + + case "$REPLY" in + + 1 ) board="nice_nano"; break;; + 2 ) board="proton_c"; break;; + 3 ) board="bluemicro840_v1"; break;; + 3 ) board="nrf52840_m2"; break;; + + $(( ${#options[@]}+1 )) ) echo "Goodbye!"; exit 1;; + *) echo "Invalid option. Try another one."; continue;; + + esac +done + +echo "" +echo "Keyboard Shield Selection:" + +prompt="Pick an keyboard:" +options=("Kyria" "Lily58" "Corne" "Splitreus62" "Sofle" "Iris" "Reviung41" "RoMac" "RoMac+" "makerdiary M60" "Microdox" "TG4X" "QAZ" "Jorne" "Jian" "CRBN" "Tidbit") + +PS3="$prompt " +# TODO: Add support for "Other" and linking to docs on adding custom shields in user config repos. +# select opt in "${options[@]}" "Other" "Quit"; do +select opt in "${options[@]}" "Quit"; do + + case "$REPLY" in + + 1 ) shield_title="Kyria" shield="kyria"; split="y"; break;; + 2 ) shield_title="Lily58" shield="lily58"; split="y"; break;; + 3 ) shield_title="Corne" shield="corne"; split="y"; break;; + 4 ) shield_title="Splitreus62" shield="splitreus62"; split="y"; break;; + 5 ) shield_title="Sofle" shield="sofle"; split="y"; break;; + 6 ) shield_title="Iris" shield="iris"; split="y"; break;; + 7 ) shield_title="Reviung41" shield="reviung41"; split="n"; break;; + 8 ) shield_title="RoMac" shield="romac"; split="n"; break;; + 9 ) shield_title="RoMac+" shield="romac_plus"; split="n"; break;; + 10 ) shield_title="M60" shield="m60"; split="n"; break;; + 11 ) shield_title="Microdox" shield="microdox"; split="y"; break;; + 12 ) shield_title="TG4X" shield="tg4x"; split="n"; break;; + 13 ) shield_title="QAZ" shield="qaz"; split="n"; break;; + 14 ) shield_title="NIBBLE" shield="nibble"; split="n"; break;; + 15 ) shield_title="Jorne" shield="jorne"; split="y"; break;; + 16 ) shield_title="Jian" shield="jian"; split="y"; break;; + 17 ) shield_title="CRBN" shield="crbn"; split="n"; break;; + 18 ) shield_title="Tidbit" shield="tidbit"; split="n" break;; + + # Add link to docs on adding your own custom shield in your ZMK config! + # $(( ${#options[@]}+1 )) ) echo "Other!"; break;; + $(( ${#options[@]}+1 )) ) echo "Goodbye!"; exit 1;; + *) echo "Invalid option. Try another one.";continue;; + + esac +done + +if [ "$split" == "n" ]; then + repo_path="https://github.com/zmkfirmware/zmk-config-template.git" +fi + +read -r -e -p "Copy in the stock keymap for customization? [Yn]: " copy_keymap + +if [ -z "$copy_keymap" ] || [ "$copy_keymap" == "Y" ] || [ "$copy_keymap" == "y" ]; then copy_keymap="yes"; fi + +read -r -e -p "GitHub Username (leave empty to skip GitHub repo creation): " github_user +if [ -n "$github_user" ]; then + read -r -p "GitHub Repo Name [zmk-config]: " repo_name + if [ -z "$repo_name" ]; then repo_name="zmk-config"; fi + + read -r -p "GitHub Repo [https://github.com/${github_user}/${repo_name}.git]: " github_repo + + if [ -z "$github_repo" ]; then github_repo="https://github.com/${github_user}/${repo_name}.git"; fi +else + repo_name="zmk-config" +fi + +echo "" +echo "Preparing a user config for:" +echo "* MCU Board: ${board}" +echo "* Shield: ${shield}" + +if [ "$copy_keymap" == "yes" ]; then + echo "* Copy Keymap?: ✓" +else + echo "* Copy Keymap?: ❌" +fi + +if [ -n "$github_repo" ]; then + echo "* GitHub Repo To Push (please create this in GH first!): ${github_repo}" +fi + +echo "" +read -r -p "Continue? [Yn]: " do_it + +if [ -n "$do_it" ] && [ "$do_it" != "y" ] && [ "$do_it" != "Y" ]; then + echo "Aborting..." + exit 1 +fi + +git clone --single-branch $repo_path ${repo_name} +cd ${repo_name} + +pushd config + +$download_command "https://raw.githubusercontent.com/zmkfirmware/zmk/main/app/boards/shields/${shield}/${shield}.conf" + +if [ "$copy_keymap" == "yes" ]; then + $download_command "https://raw.githubusercontent.com/zmkfirmware/zmk/main/app/boards/shields/${shield}/${shield}.keymap" +fi + +popd + +sed -i'.orig' \ + -e "s/BOARD_NAME/$board/" \ + -e "s/SHIELD_NAME/$shield/" \ + -e "s/KEYBOARD_TITLE/$shield_title/" \ + .github/workflows/build.yml + +if [ "$board" == "proton_c" ]; then + # Proton-C board still fa + sed -i'.orig' -e "s/uf2/hex/g" .github/workflows/build.yml +fi + +rm .github/workflows/*.yml.orig + +rm -rf .git +git init . +git add . +git commit -m "Initial User Config." + +if [ -n "$github_repo" ]; then + git remote add origin "$github_repo" + git push --set-upstream origin "$(git symbolic-ref --short HEAD)" + push_return_code=$? + + # If push failed, assume that the origin was incorrect and give instructions on fixing. + if [ ${push_return_code} -ne 0 ]; then + echo "Remote repository $github_repo not found..." + echo "Check GitHub URL, and try adding again." + echo "Run the following: " + echo " git remote rm origin" + echo " git remote add origin FIXED_URL" + echo " git push --set-upstream origin $(git symbolic-ref --short HEAD)" + echo "Once pushed, your firmware should be availalbe from GitHub Actions at: ${github_repo%.git}/actions" + exit 1 + fi + + # TODO: Support determing the actions URL when non-https:// repo URL is used. + if [ "${github_repo}" != "${github_repo#https://}" ]; then + echo "Your firmware should be available from GitHub Actions shortly: ${github_repo%.git}/actions" + fi +fi diff --git a/docs/static/setup_LOCAL_1333.sh b/docs/static/setup_LOCAL_1333.sh new file mode 100644 index 00000000..8ac39435 --- /dev/null +++ b/docs/static/setup_LOCAL_1333.sh @@ -0,0 +1,228 @@ +#!/bin/bash + +# Copyright (c) 2020 The ZMK Contributors +# SPDX-License-Identifier: MIT + +set -e + +check_exists() { + command_to_run=$1 + error_message=$2 + local __resultvar=$3 + + if ! eval "$command_to_run" &> /dev/null; then + if [[ "$__resultvar" != "" ]]; then + eval $__resultvar="'false'" + else + printf "%s\n" "$error_message" + exit 1 + fi + else + if [[ "$__resultvar" != "" ]]; then + eval $__resultvar="'true'" + fi + fi +} + +check_exists "command -v git" "git is not installed, and is required for this script!" +check_exists "command -v curl" "curl is not installed, and is required for this script!" curl_exists +check_exists "command -v wget" "wget is not installed, and is required for this script!" wget_exists + +check_exists "git config user.name" "Git username not set!\nRun: git config --global user.name 'My Name'" +check_exists "git config user.email" "Git email not set!\nRun: git config --global user.email 'example@myemail.com'" + +# Check to see if the user has write permissions in this directory to prevent a cryptic error later on +if [ ! -w `pwd` ]; then + echo 'Sorry, you do not have write permissions in this directory.'; + echo 'Please try running this script again from a directory that you do have write permissions for.'; + exit 1 +fi + +# Parse all commandline options +while [[ "$#" -gt 0 ]]; do + case $1 in + -w|--wget) force_wget="true"; break;; + *) echo "Unknown parameter: $1"; exit 1;; + esac + shift +done + +if [[ $curl_exists == "true" && $wget_exists == "true" ]]; then + if [[ $force_wget == "true" ]]; then + download_command="wget " + else + download_command="curl -O " + fi +elif [[ $curl_exists == "true" ]]; then + download_command="curl -O " +elif [[ $wget_exists == "true" ]]; then + download_command="wget " +else + echo 'Neither curl nor wget are installed. One of the two is required for this script!' + exit 1 +fi + +repo_path="https://github.com/zmkfirmware/zmk-config-split-template.git" +title="ZMK Config Setup:" + +prompt="Pick an MCU board:" +options=("nice!nano" "QMK Proton-C" "BlueMicro840 (v1)" "makerdiary nRF52840 M.2") + +echo "$title" +echo "" +echo "MCU Board Selection:" +PS3="$prompt " +select opt in "${options[@]}" "Quit"; do + + case "$REPLY" in + + 1 ) board="nice_nano"; break;; + 2 ) board="proton_c"; break;; + 3 ) board="bluemicro840_v1"; break;; + 3 ) board="nrf52840_m2"; break;; + + $(( ${#options[@]}+1 )) ) echo "Goodbye!"; exit 1;; + *) echo "Invalid option. Try another one."; continue;; + + esac +done + +echo "" +echo "Keyboard Shield Selection:" + +prompt="Pick an keyboard:" +options=("Kyria" "Lily58" "Corne" "Splitreus62" "Sofle" "Iris" "Reviung41" "RoMac" "RoMac+" "makerdiary M60" "Microdox" "TG4X" "QAZ" "NIBBLE" "Jorne" "Jian" "CRBN" "Tidbit" "Eek!" "BF0-9000" "Helix") + +PS3="$prompt " +# TODO: Add support for "Other" and linking to docs on adding custom shields in user config repos. +# select opt in "${options[@]}" "Other" "Quit"; do +select opt in "${options[@]}" "Quit"; do + + case "$REPLY" in + + 1 ) shield_title="Kyria" shield="kyria"; split="y"; break;; + 2 ) shield_title="Lily58" shield="lily58"; split="y"; break;; + 3 ) shield_title="Corne" shield="corne"; split="y"; break;; + 4 ) shield_title="Splitreus62" shield="splitreus62"; split="y"; break;; + 5 ) shield_title="Sofle" shield="sofle"; split="y"; break;; + 6 ) shield_title="Iris" shield="iris"; split="y"; break;; + 7 ) shield_title="Reviung41" shield="reviung41"; split="n"; break;; + 8 ) shield_title="RoMac" shield="romac"; split="n"; break;; + 9 ) shield_title="RoMac+" shield="romac_plus"; split="n"; break;; + 10 ) shield_title="M60" shield="m60"; split="n"; break;; + 11 ) shield_title="Microdox" shield="microdox"; split="y"; break;; + 12 ) shield_title="TG4X" shield="tg4x"; split="n"; break;; + 13 ) shield_title="QAZ" shield="qaz"; split="n"; break;; + 14 ) shield_title="NIBBLE" shield="nibble"; split="n"; break;; + 15 ) shield_title="Jorne" shield="jorne"; split="y"; break;; + 16 ) shield_title="Jian" shield="jian"; split="y"; break;; + 17 ) shield_title="CRBN" shield="crbn"; split="n"; break;; + 18 ) shield_title="Tidbit" shield="tidbit"; split="n" break;; + 19 ) shield_title="Eek!" shield="eek"; split="n" break;; + 20 ) shield_title="BFO-9000" shield="bfo9000"; split="y"; break;; + 21 ) shield_title="Helix" shield="helix"; split="y"; break;; + + # Add link to docs on adding your own custom shield in your ZMK config! + # $(( ${#options[@]}+1 )) ) echo "Other!"; break;; + $(( ${#options[@]}+1 )) ) echo "Goodbye!"; exit 1;; + *) echo "Invalid option. Try another one.";continue;; + + esac +done + +if [ "$split" == "n" ]; then + repo_path="https://github.com/zmkfirmware/zmk-config-template.git" +fi + +read -r -e -p "Copy in the stock keymap for customization? [Yn]: " copy_keymap + +if [ -z "$copy_keymap" ] || [ "$copy_keymap" == "Y" ] || [ "$copy_keymap" == "y" ]; then copy_keymap="yes"; fi + +read -r -e -p "GitHub Username (leave empty to skip GitHub repo creation): " github_user +if [ -n "$github_user" ]; then + read -r -p "GitHub Repo Name [zmk-config]: " repo_name + if [ -z "$repo_name" ]; then repo_name="zmk-config"; fi + + read -r -p "GitHub Repo [https://github.com/${github_user}/${repo_name}.git]: " github_repo + + if [ -z "$github_repo" ]; then github_repo="https://github.com/${github_user}/${repo_name}.git"; fi +else + repo_name="zmk-config" +fi + +echo "" +echo "Preparing a user config for:" +echo "* MCU Board: ${board}" +echo "* Shield: ${shield}" + +if [ "$copy_keymap" == "yes" ]; then + echo "* Copy Keymap?: ✓" +else + echo "* Copy Keymap?: ❌" +fi + +if [ -n "$github_repo" ]; then + echo "* GitHub Repo To Push (please create this in GH first!): ${github_repo}" +fi + +echo "" +read -r -p "Continue? [Yn]: " do_it + +if [ -n "$do_it" ] && [ "$do_it" != "y" ] && [ "$do_it" != "Y" ]; then + echo "Aborting..." + exit 1 +fi + +git clone --single-branch $repo_path ${repo_name} +cd ${repo_name} + +pushd config + +$download_command "https://raw.githubusercontent.com/zmkfirmware/zmk/main/app/boards/shields/${shield}/${shield}.conf" + +if [ "$copy_keymap" == "yes" ]; then + $download_command "https://raw.githubusercontent.com/zmkfirmware/zmk/main/app/boards/shields/${shield}/${shield}.keymap" +fi + +popd + +sed -i'.orig' \ + -e "s/BOARD_NAME/$board/" \ + -e "s/SHIELD_NAME/$shield/" \ + -e "s/KEYBOARD_TITLE/$shield_title/" \ + .github/workflows/build.yml + +if [ "$board" == "proton_c" ]; then + # Proton-C board still fa + sed -i'.orig' -e "s/uf2/hex/g" .github/workflows/build.yml +fi + +rm .github/workflows/*.yml.orig + +rm -rf .git +git init . +git add . +git commit -m "Initial User Config." + +if [ -n "$github_repo" ]; then + git remote add origin "$github_repo" + git push --set-upstream origin "$(git symbolic-ref --short HEAD)" + push_return_code=$? + + # If push failed, assume that the origin was incorrect and give instructions on fixing. + if [ ${push_return_code} -ne 0 ]; then + echo "Remote repository $github_repo not found..." + echo "Check GitHub URL, and try adding again." + echo "Run the following: " + echo " git remote rm origin" + echo " git remote add origin FIXED_URL" + echo " git push --set-upstream origin $(git symbolic-ref --short HEAD)" + echo "Once pushed, your firmware should be availalbe from GitHub Actions at: ${github_repo%.git}/actions" + exit 1 + fi + + # TODO: Support determing the actions URL when non-https:// repo URL is used. + if [ "${github_repo}" != "${github_repo#https://}" ]; then + echo "Your firmware should be available from GitHub Actions shortly: ${github_repo%.git}/actions" + fi +fi diff --git a/docs/static/setup_REMOTE_1333.sh b/docs/static/setup_REMOTE_1333.sh new file mode 100644 index 00000000..34f0f054 --- /dev/null +++ b/docs/static/setup_REMOTE_1333.sh @@ -0,0 +1,228 @@ +#!/bin/bash + +# Copyright (c) 2020 The ZMK Contributors +# SPDX-License-Identifier: MIT + +set -e + +check_exists() { + command_to_run=$1 + error_message=$2 + local __resultvar=$3 + + if ! eval "$command_to_run" &> /dev/null; then + if [[ "$__resultvar" != "" ]]; then + eval $__resultvar="'false'" + else + printf "%s\n" "$error_message" + exit 1 + fi + else + if [[ "$__resultvar" != "" ]]; then + eval $__resultvar="'true'" + fi + fi +} + +check_exists "command -v git" "git is not installed, and is required for this script!" +check_exists "command -v curl" "curl is not installed, and is required for this script!" curl_exists +check_exists "command -v wget" "wget is not installed, and is required for this script!" wget_exists + +check_exists "git config user.name" "Git username not set!\nRun: git config --global user.name 'My Name'" +check_exists "git config user.email" "Git email not set!\nRun: git config --global user.email 'example@myemail.com'" + +# Check to see if the user has write permissions in this directory to prevent a cryptic error later on +if [ ! -w `pwd` ]; then + echo 'Sorry, you do not have write permissions in this directory.'; + echo 'Please try running this script again from a directory that you do have write permissions for.'; + exit 1 +fi + +# Parse all commandline options +while [[ "$#" -gt 0 ]]; do + case $1 in + -w|--wget) force_wget="true"; break;; + *) echo "Unknown parameter: $1"; exit 1;; + esac + shift +done + +if [[ $curl_exists == "true" && $wget_exists == "true" ]]; then + if [[ $force_wget == "true" ]]; then + download_command="wget " + else + download_command="curl -O " + fi +elif [[ $curl_exists == "true" ]]; then + download_command="curl -O " +elif [[ $wget_exists == "true" ]]; then + download_command="wget " +else + echo 'Neither curl nor wget are installed. One of the two is required for this script!' + exit 1 +fi + +repo_path="https://github.com/zmkfirmware/zmk-config-split-template.git" +title="ZMK Config Setup:" + +prompt="Pick an MCU board:" +options=("nice!nano" "QMK Proton-C" "BlueMicro840 (v1)" "makerdiary nRF52840 M.2") + +echo "$title" +echo "" +echo "MCU Board Selection:" +PS3="$prompt " +select opt in "${options[@]}" "Quit"; do + + case "$REPLY" in + + 1 ) board="nice_nano"; break;; + 2 ) board="proton_c"; break;; + 3 ) board="bluemicro840_v1"; break;; + 3 ) board="nrf52840_m2"; break;; + + $(( ${#options[@]}+1 )) ) echo "Goodbye!"; exit 1;; + *) echo "Invalid option. Try another one."; continue;; + + esac +done + +echo "" +echo "Keyboard Shield Selection:" + +prompt="Pick an keyboard:" +options=("Kyria" "Lily58" "Corne" "Splitreus62" "Sofle" "Iris" "Reviung41" "RoMac" "RoMac+" "makerdiary M60" "Microdox" "TG4X" "QAZ" "NIBBLE" "Jorne" "Jian" "CRBN" "Tidbit" "Eek!" "BFO-9000" "Helix") + +PS3="$prompt " +# TODO: Add support for "Other" and linking to docs on adding custom shields in user config repos. +# select opt in "${options[@]}" "Other" "Quit"; do +select opt in "${options[@]}" "Quit"; do + + case "$REPLY" in + + 1 ) shield_title="Kyria" shield="kyria"; split="y"; break;; + 2 ) shield_title="Lily58" shield="lily58"; split="y"; break;; + 3 ) shield_title="Corne" shield="corne"; split="y"; break;; + 4 ) shield_title="Splitreus62" shield="splitreus62"; split="y"; break;; + 5 ) shield_title="Sofle" shield="sofle"; split="y"; break;; + 6 ) shield_title="Iris" shield="iris"; split="y"; break;; + 7 ) shield_title="Reviung41" shield="reviung41"; split="n"; break;; + 8 ) shield_title="RoMac" shield="romac"; split="n"; break;; + 9 ) shield_title="RoMac+" shield="romac_plus"; split="n"; break;; + 10 ) shield_title="M60" shield="m60"; split="n"; break;; + 11 ) shield_title="Microdox" shield="microdox"; split="y"; break;; + 12 ) shield_title="TG4X" shield="tg4x"; split="n"; break;; + 13 ) shield_title="QAZ" shield="qaz"; split="n"; break;; + 14 ) shield_title="NIBBLE" shield="nibble"; split="n"; break;; + 15 ) shield_title="Jorne" shield="jorne"; split="y"; break;; + 16 ) shield_title="Jian" shield="jian"; split="y"; break;; + 17 ) shield_title="CRBN" shield="crbn"; split="n"; break;; + 18 ) shield_title="Tidbit" shield="tidbit"; split="n" break;; + 19 ) shield_title="Eek!" shield="eek"; split="n" break;; + 20 ) shield_title="BFO-9000" shield="bfo9000"; split="y"; break;; + 21 ) shield_title="Helix" shield="helix"; split="y"; break;; + + # Add link to docs on adding your own custom shield in your ZMK config! + # $(( ${#options[@]}+1 )) ) echo "Other!"; break;; + $(( ${#options[@]}+1 )) ) echo "Goodbye!"; exit 1;; + *) echo "Invalid option. Try another one.";continue;; + + esac +done + +if [ "$split" == "n" ]; then + repo_path="https://github.com/zmkfirmware/zmk-config-template.git" +fi + +read -r -e -p "Copy in the stock keymap for customization? [Yn]: " copy_keymap + +if [ -z "$copy_keymap" ] || [ "$copy_keymap" == "Y" ] || [ "$copy_keymap" == "y" ]; then copy_keymap="yes"; fi + +read -r -e -p "GitHub Username (leave empty to skip GitHub repo creation): " github_user +if [ -n "$github_user" ]; then + read -r -p "GitHub Repo Name [zmk-config]: " repo_name + if [ -z "$repo_name" ]; then repo_name="zmk-config"; fi + + read -r -p "GitHub Repo [https://github.com/${github_user}/${repo_name}.git]: " github_repo + + if [ -z "$github_repo" ]; then github_repo="https://github.com/${github_user}/${repo_name}.git"; fi +else + repo_name="zmk-config" +fi + +echo "" +echo "Preparing a user config for:" +echo "* MCU Board: ${board}" +echo "* Shield: ${shield}" + +if [ "$copy_keymap" == "yes" ]; then + echo "* Copy Keymap?: ✓" +else + echo "* Copy Keymap?: ❌" +fi + +if [ -n "$github_repo" ]; then + echo "* GitHub Repo To Push (please create this in GH first!): ${github_repo}" +fi + +echo "" +read -r -p "Continue? [Yn]: " do_it + +if [ -n "$do_it" ] && [ "$do_it" != "y" ] && [ "$do_it" != "Y" ]; then + echo "Aborting..." + exit 1 +fi + +git clone --single-branch $repo_path ${repo_name} +cd ${repo_name} + +pushd config + +$download_command "https://raw.githubusercontent.com/zmkfirmware/zmk/main/app/boards/shields/${shield}/${shield}.conf" + +if [ "$copy_keymap" == "yes" ]; then + $download_command "https://raw.githubusercontent.com/zmkfirmware/zmk/main/app/boards/shields/${shield}/${shield}.keymap" +fi + +popd + +sed -i'.orig' \ + -e "s/BOARD_NAME/$board/" \ + -e "s/SHIELD_NAME/$shield/" \ + -e "s/KEYBOARD_TITLE/$shield_title/" \ + .github/workflows/build.yml + +if [ "$board" == "proton_c" ]; then + # Proton-C board still fa + sed -i'.orig' -e "s/uf2/hex/g" .github/workflows/build.yml +fi + +rm .github/workflows/*.yml.orig + +rm -rf .git +git init . +git add . +git commit -m "Initial User Config." + +if [ -n "$github_repo" ]; then + git remote add origin "$github_repo" + git push --set-upstream origin "$(git symbolic-ref --short HEAD)" + push_return_code=$? + + # If push failed, assume that the origin was incorrect and give instructions on fixing. + if [ ${push_return_code} -ne 0 ]; then + echo "Remote repository $github_repo not found..." + echo "Check GitHub URL, and try adding again." + echo "Run the following: " + echo " git remote rm origin" + echo " git remote add origin FIXED_URL" + echo " git push --set-upstream origin $(git symbolic-ref --short HEAD)" + echo "Once pushed, your firmware should be availalbe from GitHub Actions at: ${github_repo%.git}/actions" + exit 1 + fi + + # TODO: Support determing the actions URL when non-https:// repo URL is used. + if [ "${github_repo}" != "${github_repo#https://}" ]; then + echo "Your firmware should be available from GitHub Actions shortly: ${github_repo%.git}/actions" + fi +fi