summaryrefslogtreecommitdiff
path: root/keyboards/orbekk_ferris_sweep
diff options
context:
space:
mode:
authorKjetil Orbekk <kj@orbekk.com>2024-11-25 19:27:42 -0500
committerKjetil Orbekk <kj@orbekk.com>2024-11-25 19:27:42 -0500
commit2664e41cf37822a8432993381412e54e77eedbbb (patch)
tree09145289407a3e75634bd038a5a93eb46ad669ac /keyboards/orbekk_ferris_sweep
parent67998a52f662227751180196dc9142e9270ef911 (diff)
Add ferris sweep RP2040 keyboard
Diffstat (limited to 'keyboards/orbekk_ferris_sweep')
-rw-r--r--keyboards/orbekk_ferris_sweep/config.h55
-rw-r--r--keyboards/orbekk_ferris_sweep/info.json81
-rw-r--r--keyboards/orbekk_ferris_sweep/keymaps/default/features/achordion.c282
-rw-r--r--keyboards/orbekk_ferris_sweep/keymaps/default/features/achordion.h185
-rw-r--r--keyboards/orbekk_ferris_sweep/keymaps/default/features/custom_shift_keys.c87
-rw-r--r--keyboards/orbekk_ferris_sweep/keymaps/default/features/custom_shift_keys.h99
-rw-r--r--keyboards/orbekk_ferris_sweep/keymaps/default/keymap.c201
-rw-r--r--keyboards/orbekk_ferris_sweep/keymaps/default/layout.h30
-rw-r--r--keyboards/orbekk_ferris_sweep/keymaps/orbekk/keymap.c0
-rw-r--r--keyboards/orbekk_ferris_sweep/readme.md29
-rw-r--r--keyboards/orbekk_ferris_sweep/rules.mk1
11 files changed, 1050 insertions, 0 deletions
diff --git a/keyboards/orbekk_ferris_sweep/config.h b/keyboards/orbekk_ferris_sweep/config.h
new file mode 100644
index 0000000000..0f82d6793e
--- /dev/null
+++ b/keyboards/orbekk_ferris_sweep/config.h
@@ -0,0 +1,55 @@
+// Copyright 2023 KJ Orbekk (@orbekk)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+/*
+ * Feature disable options
+ * These options are also useful to firmware size reduction.
+ */
+
+/* disable debug print */
+//#define NO_DEBUG
+
+/* disable print */
+//#define NO_PRINT
+
+/* disable action features */
+//#define NO_ACTION_LAYER
+//#define NO_ACTION_TAPPING
+//#define NO_ACTION_ONESHOT
+//#define SERIAL_USART_FULL_DUPLEX //Enable full duplex operation mode.
+#define SERIAL_USART_TX_PIN GP1
+//#define SERIAL_USART_RX_PIN GP1
+
+#define RP2040_BOOTLOADER_DOUBLE_TAP_RESET
+#define RP2040_BOOTLOADER_DOUBLE_TAP_RESET_TIMEOUT 500U
+
+#define MASTER_LEFT
+
+#define LAYOUT(k0A, k0B, k0C, k0D, k0E, k4E, k4D, k4C, k4B, k4A, k1A, k1B, k1C, k1D, k1E, k5E, k5D, k5C, k5B, k5A, k2A, k2B, k2C, k2D, k2E, k6E, k6D, k6C, k6B, k6A, k3B, k3D, k3E, k7E, k7D, k7B) { \
+ {k0A, k0B, k0C, k0D, k0E}, \
+ {k1A, k1B, k1C, k1D, k1E}, \
+ {k2A, k2B, k2C, k2D, k2E}, \
+ {KC_NO, k3B, KC_NO, k3D, k3E}, \
+ {k4A, k4B, k4C, k4D, k4E}, \
+ {k5A, k5B, k5C, k5D, k5E}, \
+ {k6A, k6B, k6C, k6D, k6E}, \
+ {KC_NO, k7B, KC_NO, k7D, k7E} \
+}
+
+#define LAYOUT_LR( \
+LA0, LA1, LA2, LA3, LA4, \
+LB0, LB1, LB2, LB3, LB4, \
+LC0, LC1, LC2, LC3, LC4, \
+LT0, LT1, \
+RA0, RA1, RA2, RA3, RA4, \
+RB0, RB1, RB2, RB3, RB4, \
+RC0, RC1, RC2, RC3, RC4, \
+RT0, RT1 \
+) LAYOUT( \
+LA0, LA1, LA2, LA3, LA4, RA0, RA1, RA2, RA3, RA4, \
+LB0, LB1, LB2, LB3, LB4, RB0, RB1, RB2, RB3, RB4, \
+LC0, LC1, LC2, LC3, LC4, RC0, RC1, RC2, RC3, RC4, \
+LT0, LT1, KC_NO, KC_NO, RT0, RT1 \
+)
diff --git a/keyboards/orbekk_ferris_sweep/info.json b/keyboards/orbekk_ferris_sweep/info.json
new file mode 100644
index 0000000000..811c8fe3d1
--- /dev/null
+++ b/keyboards/orbekk_ferris_sweep/info.json
@@ -0,0 +1,81 @@
+{
+ "manufacturer": "DPB",
+ "keyboard_name": "Ferris sweep orbekk",
+ "maintainer": "orbekk",
+ "bootloader": "rp2040",
+ "features": {
+ "bootmagic": true,
+ "command": false,
+ "console": false,
+ "extrakey": true,
+ "mousekey": true,
+ "nkro": true
+ },
+ "matrix_pins": {
+ "direct": [
+ ["GP7", "GP26", "GP27", "GP28", "GP29"],
+ ["GP22", "GP20", "GP23", "GP21", "GP0"],
+ ["GP2", "GP3", "GP4", "GP5", "GP6"],
+ ["GP8", "GP9", null, null, null],
+ ]
+ },
+ "split": {
+ "enabled": true
+ }
+ "processor": "RP2040",
+ "url": "",
+ "usb": {
+ "device_version": "1.0.0",
+ "pid": "0xC2AB",
+ "vid": "0x3939"
+ },
+ "layouts": {
+ "LAYOUT": {
+ "layout": [
+ {"matrix": [0, 0], "x": 0, "y": 0},
+ {"matrix": [0, 1], "x": 1, "y": 0},
+ {"matrix": [0, 2], "x": 2, "y": 0},
+ {"matrix": [0, 3], "x": 3, "y": 0},
+ {"matrix": [0, 4], "x": 4, "y": 0},
+
+ {"matrix": [4, 4], "x": 5, "y": 0},
+ {"matrix": [4, 3], "x": 6, "y": 0},
+ {"matrix": [4, 2], "x": 7, "y": 0},
+ {"matrix": [4, 1], "x": 8, "y": 0},
+ {"matrix": [4, 0], "x": 9, "y": 0},
+
+ {"matrix": [1, 0], "x": 0, "y": 1},
+ {"matrix": [1, 1], "x": 1, "y": 1},
+ {"matrix": [1, 2], "x": 2, "y": 1},
+ {"matrix": [1, 3], "x": 3, "y": 1},
+ {"matrix": [1, 4], "x": 4, "y": 1},
+
+ {"matrix": [5, 4], "x": 5, "y": 1},
+ {"matrix": [5, 3], "x": 6, "y": 1},
+ {"matrix": [5, 2], "x": 7, "y": 1},
+ {"matrix": [5, 1], "x": 8, "y": 1},
+ {"matrix": [5, 0], "x": 9, "y": 1},
+
+ {"matrix": [2, 0], "x": 0, "y": 2},
+ {"matrix": [2, 1], "x": 1, "y": 2},
+ {"matrix": [2, 2], "x": 2, "y": 2},
+ {"matrix": [2, 3], "x": 3, "y": 2},
+ {"matrix": [2, 4], "x": 4, "y": 2},
+
+ {"matrix": [6, 4], "x": 5, "y": 2},
+ {"matrix": [6, 3], "x": 6, "y": 2},
+ {"matrix": [6, 2], "x": 7, "y": 2},
+ {"matrix": [6, 1], "x": 8, "y": 2},
+ {"matrix": [6, 0], "x": 9, "y": 2},
+
+ {"matrix": [3, 1], "x": 0, "y": 3},
+ {"matrix": [3, 3], "x": 1, "y": 3},
+ {"matrix": [3, 4], "x": 2, "y": 3}
+
+ {"matrix": [7, 4], "x": 3, "y": 3},
+ {"matrix": [7, 3], "x": 4, "y": 3},
+ {"matrix": [7, 1], "x": 4, "y": 3}
+ ]
+ }
+ }
+}
diff --git a/keyboards/orbekk_ferris_sweep/keymaps/default/features/achordion.c b/keyboards/orbekk_ferris_sweep/keymaps/default/features/achordion.c
new file mode 100644
index 0000000000..97bc9beacb
--- /dev/null
+++ b/keyboards/orbekk_ferris_sweep/keymaps/default/features/achordion.c
@@ -0,0 +1,282 @@
+// Copyright 2022-2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @file achordion.c
+ * @brief Achordion implementation
+ *
+ * For full documentation, see
+ * <https://getreuer.info/posts/keyboards/achordion>
+ */
+
+#include "achordion.h"
+
+#if !defined(IS_QK_MOD_TAP)
+// Attempt to detect out-of-date QMK installation, which would fail with
+// implicit-function-declaration errors in the code below.
+#error "achordion: QMK version is too old to build. Please update QMK."
+#else
+
+// Copy of the `record` and `keycode` args for the current active tap-hold key.
+static keyrecord_t tap_hold_record;
+static uint16_t tap_hold_keycode = KC_NO;
+// Timeout timer. When it expires, the key is considered held.
+static uint16_t hold_timer = 0;
+// Eagerly applied mods, if any.
+static uint8_t eager_mods = 0;
+// Flag to determine whether another key is pressed within the timeout.
+static bool pressed_another_key_before_release = false;
+
+#ifdef ACHORDION_STREAK
+// Timer for typing streak
+static uint16_t streak_timer = 0;
+#else
+// When disabled, is_streak is never true
+#define is_streak false
+#endif
+
+// Achordion's current state.
+enum {
+ // A tap-hold key is pressed, but hasn't yet been settled as tapped or held.
+ STATE_UNSETTLED,
+ // Achordion is inactive.
+ STATE_RELEASED,
+ // Active tap-hold key has been settled as tapped.
+ STATE_TAPPING,
+ // Active tap-hold key has been settled as held.
+ STATE_HOLDING,
+ // This state is set while calling `process_record()`, which will recursively
+ // call `process_achordion()`. This state is checked so that we don't process
+ // events generated by Achordion and potentially create an infinite loop.
+ STATE_RECURSING,
+};
+static uint8_t achordion_state = STATE_RELEASED;
+
+// Calls `process_record()` with state set to RECURSING.
+static void recursively_process_record(keyrecord_t* record, uint8_t state) {
+ achordion_state = STATE_RECURSING;
+ process_record(record);
+ achordion_state = state;
+}
+
+// Clears eagerly-applied mods.
+static void clear_eager_mods(void) {
+ unregister_mods(eager_mods);
+ eager_mods = 0;
+}
+
+// Sends hold press event and settles the active tap-hold key as held.
+static void settle_as_hold(void) {
+ clear_eager_mods();
+ // Create hold press event.
+ recursively_process_record(&tap_hold_record, STATE_HOLDING);
+}
+
+bool process_achordion(uint16_t keycode, keyrecord_t* record) {
+ // Don't process events that Achordion generated.
+ if (achordion_state == STATE_RECURSING) {
+ return true;
+ }
+
+ // If this is a keypress and if the key is different than the tap-hold key,
+ // this information is saved to a flag to be processed later when the tap-hold
+ // key is released.
+ if (!pressed_another_key_before_release && record->event.pressed &&
+ tap_hold_keycode != KC_NO && tap_hold_keycode != keycode) {
+ pressed_another_key_before_release = true;
+ }
+
+ // Determine whether the current event is for a mod-tap or layer-tap key.
+ const bool is_mt = IS_QK_MOD_TAP(keycode);
+ const bool is_tap_hold = is_mt || IS_QK_LAYER_TAP(keycode);
+ // Check that this is a normal key event, don't act on combos.
+#ifdef IS_KEYEVENT
+ const bool is_key_event = IS_KEYEVENT(record->event);
+#else
+ const bool is_key_event =
+ (record->event.key.row < 254 && record->event.key.col < 254);
+#endif
+
+ if (achordion_state == STATE_RELEASED) {
+ if (is_tap_hold && record->tap.count == 0 && record->event.pressed &&
+ is_key_event) {
+ // A tap-hold key is pressed and considered by QMK as "held".
+ const uint16_t timeout = achordion_timeout(keycode);
+ if (timeout > 0) {
+ achordion_state = STATE_UNSETTLED;
+ // Save info about this key.
+ tap_hold_keycode = keycode;
+ tap_hold_record = *record;
+ hold_timer = record->event.time + timeout;
+
+ if (is_mt) { // Apply mods immediately if they are "eager."
+ uint8_t mod = mod_config(QK_MOD_TAP_GET_MODS(tap_hold_keycode));
+ if (achordion_eager_mod(mod)) {
+ eager_mods = ((mod & 0x10) == 0) ? mod : (mod << 4);
+ register_mods(eager_mods);
+ }
+ }
+
+ dprintf("Achordion: Key 0x%04X pressed.%s\n", keycode,
+ eager_mods ? " Set eager mods." : "");
+ return false; // Skip default handling.
+ }
+ }
+
+#ifdef ACHORDION_STREAK
+ streak_timer = (timer_read() + achordion_streak_timeout(keycode)) | 1;
+#endif
+ return true; // Otherwise, continue with default handling.
+ }
+
+ if (keycode == tap_hold_keycode && !record->event.pressed) {
+ // The active tap-hold key is being released.
+ if (achordion_state == STATE_HOLDING) {
+ dprintln("Achordion: Key released. Plumbing hold release.");
+ tap_hold_record.event.pressed = false;
+ // Plumb hold release event.
+ recursively_process_record(&tap_hold_record, STATE_RELEASED);
+ } else if (!pressed_another_key_before_release) {
+ // No other key was pressed between the press and release of the tap-hold
+ // key, simulate a hold and then a release without waiting for Achordion
+ // timeout to end.
+ dprintln("Achordion: Key released. Simulating hold and release.");
+ settle_as_hold();
+ tap_hold_record.event.pressed = false;
+ // Plumb hold release event.
+ recursively_process_record(&tap_hold_record, STATE_RELEASED);
+ } else {
+ dprintf("Achordion: Key released.%s\n",
+ eager_mods ? " Clearing eager mods." : "");
+ if (is_mt) {
+ clear_eager_mods();
+ }
+ }
+
+ achordion_state = STATE_RELEASED;
+ // The tap-hold key is released, clear the related keycode and the flag.
+ tap_hold_keycode = KC_NO;
+ pressed_another_key_before_release = false;
+ return false;
+ }
+
+ if (achordion_state == STATE_UNSETTLED && record->event.pressed) {
+#ifdef ACHORDION_STREAK
+ const bool is_streak = (streak_timer != 0);
+ streak_timer = (timer_read() + achordion_streak_timeout(keycode)) | 1;
+#endif
+
+ // Press event occurred on a key other than the active tap-hold key.
+
+ // If the other key is *also* a tap-hold key and considered by QMK to be
+ // held, then we settle the active key as held. This way, things like
+ // chording multiple home row modifiers will work, but let's our logic
+ // consider simply a single tap-hold key as "active" at a time.
+ //
+ // Otherwise, we call `achordion_chord()` to determine whether to settle the
+ // tap-hold key as tapped vs. held. We implement the tap or hold by plumbing
+ // events back into the handling pipeline so that QMK features and other
+ // user code can see them. This is done by calling `process_record()`, which
+ // in turn calls most handlers including `process_record_user()`.
+ if (!is_streak && (!is_key_event || (is_tap_hold && record->tap.count == 0) ||
+ achordion_chord(tap_hold_keycode, &tap_hold_record, keycode, record))) {
+ dprintln("Achordion: Plumbing hold press.");
+ settle_as_hold();
+ } else {
+ clear_eager_mods(); // Clear in case eager mods were set.
+
+ dprintln("Achordion: Plumbing tap press.");
+ tap_hold_record.tap.count = 1; // Revise event as a tap.
+ tap_hold_record.tap.interrupted = true;
+ // Plumb tap press event.
+ recursively_process_record(&tap_hold_record, STATE_TAPPING);
+
+ send_keyboard_report();
+#if TAP_CODE_DELAY > 0
+ wait_ms(TAP_CODE_DELAY);
+#endif // TAP_CODE_DELAY > 0
+
+ dprintln("Achordion: Plumbing tap release.");
+ tap_hold_record.event.pressed = false;
+ // Plumb tap release event.
+ recursively_process_record(&tap_hold_record, STATE_TAPPING);
+ }
+
+ recursively_process_record(record, achordion_state); // Re-process event.
+ return false; // Block the original event.
+ }
+
+#ifdef ACHORDION_STREAK
+ // update idle timer on regular keys event
+ streak_timer = (timer_read() + achordion_streak_timeout(keycode)) | 1;
+#endif
+ return true;
+}
+
+void achordion_task(void) {
+ if (achordion_state == STATE_UNSETTLED &&
+ timer_expired(timer_read(), hold_timer)) {
+ dprintln("Achordion: Timeout. Plumbing hold press.");
+ settle_as_hold(); // Timeout expired, settle the key as held.
+ }
+
+#ifdef ACHORDION_STREAK
+ if (streak_timer && timer_expired(timer_read(), streak_timer)) {
+ streak_timer = 0; // Expired.
+ }
+#endif
+}
+
+// Returns true if `pos` on the left hand of the keyboard, false if right.
+static bool on_left_hand(keypos_t pos) {
+#ifdef SPLIT_KEYBOARD
+ return pos.row < MATRIX_ROWS / 2;
+#else
+ return (MATRIX_COLS > MATRIX_ROWS) ? pos.col < MATRIX_COLS / 2
+ : pos.row < MATRIX_ROWS / 2;
+#endif
+}
+
+bool achordion_opposite_hands(const keyrecord_t* tap_hold_record,
+ const keyrecord_t* other_record) {
+ return on_left_hand(tap_hold_record->event.key) !=
+ on_left_hand(other_record->event.key);
+}
+
+// By default, use the BILATERAL_COMBINATIONS rule to consider the tap-hold key
+// "held" only when it and the other key are on opposite hands.
+__attribute__((weak)) bool achordion_chord(uint16_t tap_hold_keycode,
+ keyrecord_t* tap_hold_record,
+ uint16_t other_keycode,
+ keyrecord_t* other_record) {
+ return achordion_opposite_hands(tap_hold_record, other_record);
+}
+
+// By default, the timeout is 1000 ms for all keys.
+__attribute__((weak)) uint16_t achordion_timeout(uint16_t tap_hold_keycode) {
+ return 1000;
+}
+
+// By default, Shift and Ctrl mods are eager, and Alt and GUI are not.
+__attribute__((weak)) bool achordion_eager_mod(uint8_t mod) {
+ return (mod & (MOD_LALT | MOD_LGUI)) == 0;
+}
+
+#ifdef ACHORDION_STREAK
+__attribute__((weak)) uint16_t achordion_streak_timeout(uint16_t tap_hold_keycode) {
+ return 100; // Default of 100 ms.
+}
+#endif
+
+#endif // version check
diff --git a/keyboards/orbekk_ferris_sweep/keymaps/default/features/achordion.h b/keyboards/orbekk_ferris_sweep/keymaps/default/features/achordion.h
new file mode 100644
index 0000000000..dfa8dc232c
--- /dev/null
+++ b/keyboards/orbekk_ferris_sweep/keymaps/default/features/achordion.h
@@ -0,0 +1,185 @@
+// Copyright 2022-2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @file achordion.h
+ * @brief Achordion: Customizing the tap-hold decision.
+ *
+ * Overview
+ * --------
+ *
+ * This library customizes when tap-hold keys are considered held vs. tapped
+ * based on the next pressed key, like Manna Harbour's Bilateral Combinations or
+ * ZMK's positional hold. The library works on top of QMK's existing tap-hold
+ * implementation. You define mod-tap and layer-tap keys as usual and use
+ * Achordion to fine-tune the behavior.
+ *
+ * When QMK settles a tap-hold key as held, Achordion intercepts the event.
+ * Achordion then revises the event as a tap or passes it along as a hold:
+ *
+ * * Chord condition: On the next key press, a customizable `achordion_chord()`
+ * function is called, which takes the tap-hold key and the next key pressed
+ * as args. When the function returns true, the tap-hold key is settled as
+ * held, and otherwise as tapped.
+ *
+ * * Timeout: If no other key press occurs within a timeout, the tap-hold key
+ * is settled as held. This is customizable with `achordion_timeout()`.
+ *
+ * Achordion only changes the behavior when QMK considered the key held. It
+ * changes some would-be holds to taps, but no taps to holds.
+ *
+ * @note Some QMK features handle events before the point where Achordion can
+ * intercept them, particularly: Combos, Key Lock, and Dynamic Macros. It's
+ * still possible to use these features and Achordion in your keymap, but beware
+ * they might behave poorly when used simultaneously with tap-hold keys.
+ *
+ *
+ * For full documentation, see
+ * <https://getreuer.info/posts/keyboards/achordion>
+ */
+
+#pragma once
+
+#include "quantum.h"
+
+/**
+ * Suppress tap-hold mods within a *typing streak* by defining
+ * ACHORDION_STREAK. This can help preventing accidental mod
+ * activation when performing a fast tapping sequence.
+ * This is inspired by https://sunaku.github.io/home-row-mods.html#typing-streaks
+ *
+ * Enable with:
+ *
+ * #define ACHORDION_STREAK
+ *
+ * Adjust the maximum time between key events before modifiers can be enabled
+ * by defining the following callback in your keymap.c:
+ *
+ * uint16_t achordion_streak_timeout(uint16_t tap_hold_keycode) {
+ * return 100; // Default of 100 ms.
+ * }
+ */
+#ifdef ACHORDION_STREAK
+uint16_t achordion_streak_timeout(uint16_t tap_hold_keycode);
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Handler function for Achordion.
+ *
+ * Call this function from `process_record_user()` as
+ *
+ * #include "features/achordion.h"
+ *
+ * bool process_record_user(uint16_t keycode, keyrecord_t* record) {
+ * if (!process_achordion(keycode, record)) { return false; }
+ * // Your macros...
+ * return true;
+ * }
+ */
+bool process_achordion(uint16_t keycode, keyrecord_t* record);
+
+/**
+ * Matrix task function for Achordion.
+ *
+ * Call this function from `matrix_scan_user()` as
+ *
+ * void matrix_scan_user(void) {
+ * achordion_task();
+ * }
+ */
+void achordion_task(void);
+
+/**
+ * Optional callback to customize which key chords are considered "held".
+ *
+ * In your keymap.c, define the callback
+ *
+ * bool achordion_chord(uint16_t tap_hold_keycode,
+ * keyrecord_t* tap_hold_record,
+ * uint16_t other_keycode,
+ * keyrecord_t* other_record) {
+ * // Conditions...
+ * }
+ *
+ * This callback is called if while `tap_hold_keycode` is pressed,
+ * `other_keycode` is pressed. Return true if the tap-hold key should be
+ * considered held, or false to consider it tapped.
+ *
+ * @param tap_hold_keycode Keycode of the tap-hold key.
+ * @param tap_hold_record keyrecord_t from the tap-hold press event.
+ * @param other_keycode Keycode of the other key.
+ * @param other_record keyrecord_t from the other key's press event.
+ * @return True if the tap-hold key should be considered held.
+ */
+bool achordion_chord(uint16_t tap_hold_keycode, keyrecord_t* tap_hold_record,
+ uint16_t other_keycode, keyrecord_t* other_record);
+
+/**
+ * Optional callback to define a timeout duration per keycode.
+ *
+ * In your keymap.c, define the callback
+ *
+ * uint16_t achordion_timeout(uint16_t tap_hold_keycode) {
+ * // ...
+ * }
+ *
+ * The callback determines Achordion's timeout duration for `tap_hold_keycode`
+ * in units of milliseconds. The timeout be in the range 0 to 32767 ms (upper
+ * bound is due to 16-bit timer limitations). Use a timeout of 0 to bypass
+ * Achordion.
+ *
+ * @param tap_hold_keycode Keycode of the tap-hold key.
+ * @return Timeout duration in milliseconds in the range 0 to 32767.
+ */
+uint16_t achordion_timeout(uint16_t tap_hold_keycode);
+
+/**
+ * Optional callback defining which mods are "eagerly" applied.
+ *
+ * This callback defines which mods are "eagerly" applied while a mod-tap
+ * key is still being settled. This is helpful to reduce delay particularly when
+ * using mod-tap keys with an external mouse.
+ *
+ * Define this callback in your keymap.c. The default callback is eager for
+ * Shift and Ctrl, and not for Alt and GUI:
+ *
+ * bool achordion_eager_mod(uint8_t mod) {
+ * return (mod & (MOD_LALT | MOD_LGUI)) == 0;
+ * }
+ *
+ * @note `mod` should be compared with `MOD_` prefixed codes, not `KC_` codes,
+ * described at <https://docs.qmk.fm/#/mod_tap>.
+ *
+ * @param mod Modifier `MOD_` code.
+ * @return True if the modifier should be eagerly applied.
+ */
+bool achordion_eager_mod(uint8_t mod);
+
+/**
+ * Returns true if the args come from keys on opposite hands.
+ *
+ * @param tap_hold_record keyrecord_t from the tap-hold key's event.
+ * @param other_record keyrecord_t from the other key's event.
+ * @return True if the keys are on opposite hands.
+ */
+bool achordion_opposite_hands(const keyrecord_t* tap_hold_record,
+ const keyrecord_t* other_record);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/keyboards/orbekk_ferris_sweep/keymaps/default/features/custom_shift_keys.c b/keyboards/orbekk_ferris_sweep/keymaps/default/features/custom_shift_keys.c
new file mode 100644
index 0000000000..cb371ae339
--- /dev/null
+++ b/keyboards/orbekk_ferris_sweep/keymaps/default/features/custom_shift_keys.c
@@ -0,0 +1,87 @@
+// Copyright 2021-2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @file custom_shift_keys.c
+ * @brief Custom Shift Keys implementation
+ *
+ * For full documentation, see
+ * <https://getreuer.info/posts/keyboards/custom-shift-keys>
+ */
+
+#include "custom_shift_keys.h"
+
+#if !defined(IS_QK_MOD_TAP)
+// Attempt to detect out-of-date QMK installation, which would fail with
+// implicit-function-declaration errors in the code below.
+#error "custom_shift_keys: QMK version is too old to build. Please update QMK."
+#else
+
+bool process_custom_shift_keys(uint16_t keycode, keyrecord_t *record) {
+ static uint16_t registered_keycode = KC_NO;
+
+ // If a custom shift key is registered, then this event is either releasing
+ // it or manipulating another key at the same time. Either way, we release
+ // the currently registered key.
+ if (registered_keycode != KC_NO) {
+ unregister_code16(registered_keycode);
+ registered_keycode = KC_NO;
+ }
+
+ if (record->event.pressed) { // Press event.
+ const uint8_t saved_mods = get_mods();
+#ifndef NO_ACTION_ONESHOT
+ const uint8_t mods = saved_mods | get_weak_mods() | get_oneshot_mods();
+#else
+ const uint8_t mods = saved_mods | get_weak_mods();
+#endif // NO_ACTION_ONESHOT
+ if ((mods & MOD_MASK_SHIFT) != 0 // Shift is held.
+#if CUSTOM_SHIFT_KEYS_NEGMODS != 0
+ // Nothing in CUSTOM_SHIFT_KEYS_NEGMODS is held.
+ && (mods & (CUSTOM_SHIFT_KEYS_NEGMODS)) == 0
+#endif // CUSTOM_SHIFT_KEYS_NEGMODS != 0
+ ) {
+ // Continue default handling if this is a tap-hold key being held.
+ if ((IS_QK_MOD_TAP(keycode) || IS_QK_LAYER_TAP(keycode)) &&
+ record->tap.count == 0) {
+ return true;
+ }
+
+ // Search for a custom shift key whose keycode is `keycode`.
+ for (int i = 0; i < NUM_CUSTOM_SHIFT_KEYS; ++i) {
+ if (keycode == custom_shift_keys[i].keycode) {
+ registered_keycode = custom_shift_keys[i].shifted_keycode;
+ if (IS_QK_MODS(registered_keycode) && // Should keycode be shifted?
+ (QK_MODS_GET_MODS(registered_keycode) & MOD_LSFT) != 0) {
+ register_code16(registered_keycode); // If so, press it directly.
+ } else {
+ // Otherwise cancel shift mods, press the key, and restore mods.
+ del_weak_mods(MOD_MASK_SHIFT);
+#ifndef NO_ACTION_ONESHOT
+ del_oneshot_mods(MOD_MASK_SHIFT);
+#endif // NO_ACTION_ONESHOT
+ unregister_mods(MOD_MASK_SHIFT);
+ register_code16(registered_keycode);
+ set_mods(saved_mods);
+ }
+ return false;
+ }
+ }
+ }
+ }
+
+ return true; // Continue with default handling.
+}
+
+#endif // version check
diff --git a/keyboards/orbekk_ferris_sweep/keymaps/default/features/custom_shift_keys.h b/keyboards/orbekk_ferris_sweep/keymaps/default/features/custom_shift_keys.h
new file mode 100644
index 0000000000..d6cb7a9fdd
--- /dev/null
+++ b/keyboards/orbekk_ferris_sweep/keymaps/default/features/custom_shift_keys.h
@@ -0,0 +1,99 @@
+// Copyright 2021-2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @file custom_shift_keys.h
+ * @brief Custom shift keys: customize what keycode is produced when shifted.
+ *
+ * Overview
+ * --------
+ *
+ * This library implements custom shift keys, keys where you can customize
+ * what keycode is produced when shifted.
+ *
+ * Step 1: In your keymap.c, define a table of custom shift keys like
+ *
+ * #include "features/custom_shift_keys.h"
+ *
+ * const custom_shift_key_t custom_shift_keys[] = {
+ * {KC_DOT , KC_QUES}, // Shift . is ?
+ * {KC_COMM, KC_EXLM}, // Shift , is !
+ * {KC_MINS, KC_EQL }, // Shift - is =
+ * {KC_COLN, KC_SCLN}, // Shift : is ;
+ * };
+ *
+ * Each row defines one key. The first field is the keycode as it appears in
+ * your layout and determines what is typed normally. The second entry is what
+ * you want the key to type when shifted.
+ *
+ * Step 2: Handle custom shift keys from your `process_record_user` function as
+ *
+ * bool process_record_user(uint16_t keycode, keyrecord_t* record) {
+ * if (!process_custom_shift_keys(keycode, record)) { return false; }
+ * // Your macros ...
+ *
+ * return true;
+ * }
+ *
+ * Step 3: add `features/custom_shift_keys.c` to your rules.mk as
+ *
+ * SRC += features/custom_shift_keys.c
+ *
+ *
+ * For full documentation, see
+ * <https://getreuer.info/posts/keyboards/custom-shift-keys>
+ */
+
+#pragma once
+
+#include "quantum.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Custom shift key entry. The `keycode` field is the keycode as it appears in
+ * your layout and determines what is typed normally. The `shifted_keycode` is
+ * what you want the key to type when shifted.
+ */
+typedef struct {
+ uint16_t keycode;
+ uint16_t shifted_keycode;
+} custom_shift_key_t;
+
+/** Table of custom shift keys. */
+extern const custom_shift_key_t custom_shift_keys[];
+/** Number of entries in the `custom_shift_keys` table. */
+extern uint8_t NUM_CUSTOM_SHIFT_KEYS;
+
+/**
+ * Handler function for custom shift keys.
+ *
+ * In keymap.c, call this function from your `process_record_user` function as
+ *
+ * #include "features/custom_shift_keys.h"
+ *
+ * bool process_record_user(uint16_t keycode, keyrecord_t* record) {
+ * if (!process_custom_shift_keys(keycode, record)) { return false; }
+ * // Your macros ...
+ *
+ * return true;
+ * }
+ */
+bool process_custom_shift_keys(uint16_t keycode, keyrecord_t *record);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/keyboards/orbekk_ferris_sweep/keymaps/default/keymap.c b/keyboards/orbekk_ferris_sweep/keymaps/default/keymap.c
new file mode 100644
index 0000000000..ac97a604d2
--- /dev/null
+++ b/keyboards/orbekk_ferris_sweep/keymaps/default/keymap.c
@@ -0,0 +1,201 @@
+#include QMK_KEYBOARD_H
+#include "layout.h"
+#include "features/custom_shift_keys.h"
+#include "features/achordion.h"
+
+// Based on:
+// https://github.com/getreuer/qmk-keymap/blob/main/keymap.c
+
+enum layers {
+ GR, // Graphite
+ DV, // Dvorak
+ QW, // Qwerty (intended for typing in dvorak mode)
+ GAMING,
+ LOW,
+ RAISE,
+ NAV,
+ FN,
+ GAMELOW,
+};
+
+#define HL_1(kc) LGUI_T(kc)
+#define HL_2(kc) LALT_T(kc)
+#define HL_3(kc) LCTL_T(kc)
+#define HL_4(kc) LSFT_T(kc)
+#define HR_1(kc) LGUI_T(kc)
+#define HR_2(kc) LALT_T(kc)
+#define HR_3(kc) LCTL_T(kc)
+#define HR_4(kc) LSFT_T(kc)
+
+const uint16_t PROGMEM combo_z[] = {KC_X, KC_M, COMBO_END};
+combo_t key_combos[] = {
+ COMBO(combo_z, KC_Z),
+};
+
+const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
+ [GR] = LAYOUT_LR(
+ KC_B, KC_L, KC_D, KC_W, KC_COMMA,
+ HL_1(KC_N), HL_2(KC_R), HL_3(KC_T), HL_4(KC_S), KC_G,
+ KC_Q, KC_X, KC_M, KC_C, KC_V,
+ TL_LOWR, LT(NAV, KC_SPC),
+
+ KC_QUOT, KC_F, KC_O, KC_U, KC_J,
+ KC_Y, HR_4(KC_H), HR_3(KC_A), HR_2(KC_E), HR_1(KC_I),
+ KC_K, KC_P, KC_DOT, KC_MINUS, KC_SLASH,
+ KC_LSFT, TL_UPPR
+ ),
+
+ [DV] = LAYOUT_LR(
+ KC_QUOT, KC_COMM, KC_DOT, KC_P, KC_Y,
+ HL_1(KC_A), HL_2(KC_O), HL_3(KC_E), HL_4(KC_U), KC_I,
+ KC_SCLN, KC_Q, KC_J, KC_K, KC_X,
+ KC_TRNS, KC_TRNS,
+
+ KC_F, KC_G, KC_C, KC_R, KC_L,
+ KC_D, HR_4(KC_H), HR_3(KC_T), HR_2(KC_N), HR_1(KC_S),
+ KC_B, KC_M, KC_W, KC_V, KC_Z,
+ KC_TRNS, KC_TRNS
+ ),
+
+ [QW] = LAYOUT_LR(
+ KC_Q, KC_W, KC_E, KC_R, KC_T,
+ HL_1(KC_A), HL_2(KC_S), HL_3(KC_D), HL_4(KC_F), KC_G,
+ KC_Z, KC_X, KC_C, KC_V, KC_B,
+ KC_TRNS, KC_TRNS,
+
+ KC_Y, KC_U, KC_I, KC_O, KC_P,
+ KC_H, HR_4(KC_J), HR_3(KC_K), HR_2(KC_L), HR_1(KC_SCLN),
+ KC_N, KC_M, KC_COMMA, KC_DOT, KC_SLASH,
+ KC_TRNS, KC_TRNS
+ ),
+
+ [GAMING] = LAYOUT_LR(
+ KC_TAB, KC_Q, KC_W, KC_E, KC_R,
+ KC_LCTL, KC_A, KC_S, KC_D, KC_F,
+ KC_LSFT, KC_LALT, KC_X, KC_C, KC_V,
+ MO(GAMELOW), KC_SPC,
+
+ KC_Y, KC_U, KC_I, KC_O, KC_P,
+ KC_H, KC_J, KC_K, KC_L, KC_SCLN,
+ KC_N, KC_M, KC_COMMA, KC_DOT, KC_SLASH,
+ KC_TRNS, TO(GR)
+ ),
+
+ [LOW] = LAYOUT_LR(
+ KC_EXLM, KC_AT, KC_HASH, KC_DOLLAR, KC_PERCENT,
+ KC_LGUI, KC_LALT, KC_LCTL, KC_LSFT, KC_BACKSLASH,
+ KC_GRAVE, KC_TILDE, KC_TAB, KC_ESC, KC_PIPE,
+ KC_TRNS, KC_TRNS,
+
+ KC_CIRC, KC_AMPERSAND, KC_ASTERISK, KC_LPRN, KC_RPRN,
+ KC_SLASH, KC_UNDERSCORE, KC_PLUS, KC_LCBR, KC_RCBR,
+ KC_QUESTION, KC_MINUS, KC_EQUAL, KC_LBRC, KC_RBRC,
+ KC_TRNS, KC_TRNS
+ ),
+
+ [RAISE] = LAYOUT_LR(
+ KC_1, KC_2, KC_3, KC_4, KC_5,
+ KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS,
+ KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS,
+ KC_TRNS, KC_TRNS,
+
+ KC_6, KC_7, KC_8, KC_9, KC_0,
+ KC_BSPC, KC_LSFT, KC_LCTL, KC_LALT, KC_LGUI,
+ KC_DEL, KC_ENTER, KC_COMMA, KC_SEMICOLON, KC_COLON,
+ KC_TRNS, KC_TRNS
+ ),
+
+ [FN] = LAYOUT_LR(
+ KC_F1, KC_F2, KC_F3, KC_F4, KC_F5,
+ KC_F11, KC_F12, KC_TRNS, KC_TRNS, KC_TRNS,
+ KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS,
+ KC_TRNS, KC_TRNS,
+
+ KC_F6, KC_F7, KC_F8, KC_F9, KC_F10,
+ KC_TRNS, TO(GR), TO(DV), TO(QW), TO(GAMING),
+ KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, QK_BOOT,
+ KC_TRNS, KC_TRNS
+ ),
+
+ [NAV] = LAYOUT_LR(
+ KC_BTN4, KC_BTN3, KC_UP, KC_BTN1, KC_BTN2,
+ KC_TRNS, KC_LEFT, KC_DOWN, KC_RIGHT, KC_PGUP,
+ KC_HOME, KC_WH_D, KC_WH_U, KC_END, KC_PGDN,
+ KC_TRNS, KC_TRNS,
+
+ KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS,
+ KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS,
+ KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS,
+ KC_TRNS, KC_TRNS
+ ),
+
+ [GAMELOW] = LAYOUT_LR(
+ KC_ESC, KC_7, KC_8, KC_9, KC_TRNS,
+ KC_TRNS, KC_4, KC_5, KC_6, KC_TRNS,
+ KC_TRNS, KC_1, KC_2, KC_3, KC_0,
+ KC_TRNS, KC_TRNS,
+
+ KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS,
+ KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS,
+ KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS,
+ KC_TRNS, KC_TRNS
+ ),
+
+};
+
+const custom_shift_key_t custom_shift_keys[] = {
+ {KC_DOT , KC_GT},
+ {KC_SLASH, KC_LT},
+ {KC_MINS, KC_DOUBLE_QUOTE},
+ {KC_QUOTE, KC_UNDERSCORE},
+ {KC_COMMA, KC_QUESTION},
+};
+uint8_t NUM_CUSTOM_SHIFT_KEYS =
+ sizeof(custom_shift_keys) / sizeof(custom_shift_key_t);
+
+bool process_record_user(uint16_t keycode, keyrecord_t* record) {
+ if (!process_achordion(keycode, record)) { return false; }
+
+ const bool use_custom_shift = !IS_LAYER_ON(DV) &&
+ !IS_LAYER_ON(QW) && !IS_LAYER_ON(GAMING);
+ if (use_custom_shift &&
+ !process_custom_shift_keys(keycode, record)) {
+ return false;
+ }
+
+ return true;
+}
+
+void matrix_scan_user(void) {
+ achordion_task();
+}
+
+bool achordion_chord(uint16_t tap_hold_keycode,
+ keyrecord_t* tap_hold_record,
+ uint16_t other_keycode,
+ keyrecord_t* other_record) {
+ // Also allow same-hand holds when the other key is in the rows below the
+ // alphas. I need the `% (MATRIX_ROWS / 2)` because my keyboard is split.
+ if (other_record->event.key.row % (MATRIX_ROWS / 2) >= 2) { return true; }
+ if (tap_hold_record->event.key.row % (MATRIX_ROWS / 2) >= 2) { return true; }
+
+ // Otherwise, follow the opposite hands rule.
+ return achordion_opposite_hands(tap_hold_record, other_record);
+}
+
+bool achordion_eager_mod(uint8_t mod) {
+ switch (mod) {
+ case MOD_LSFT:
+ case MOD_RSFT:
+ case MOD_LCTL:
+ case MOD_RCTL:
+ case MOD_LALT:
+ case MOD_RALT:
+ case MOD_LGUI:
+ case MOD_RGUI:
+ return true; // Eagerly apply mods.
+
+ default:
+ return false;
+ }
+}
diff --git a/keyboards/orbekk_ferris_sweep/keymaps/default/layout.h b/keyboards/orbekk_ferris_sweep/keymaps/default/layout.h
new file mode 100644
index 0000000000..b992f3c9f8
--- /dev/null
+++ b/keyboards/orbekk_ferris_sweep/keymaps/default/layout.h
@@ -0,0 +1,30 @@
+#pragma once
+
+#include QMK_KEYBOARD_H
+
+#define LAYOUT(k0A, k0B, k0C, k0D, k0E, k4E, k4D, k4C, k4B, k4A, k1A, k1B, k1C, k1D, k1E, k5E, k5D, k5C, k5B, k5A, k2A, k2B, k2C, k2D, k2E, k6E, k6D, k6C, k6B, k6A, k3B, k3D, k3E, k7E, k7D, k7B) { \
+ {k0A, k0B, k0C, k0D, k0E}, \
+ {k1A, k1B, k1C, k1D, k1E}, \
+ {k2A, k2B, k2C, k2D, k2E}, \
+ {KC_NO, k3B, KC_NO, k3D, k3E}, \
+ {k4A, k4B, k4C, k4D, k4E}, \
+ {k5A, k5B, k5C, k5D, k5E}, \
+ {k6A, k6B, k6C, k6D, k6E}, \
+ {KC_NO, k7B, KC_NO, k7D, k7E} \
+}
+
+#define LAYOUT_LR( \
+LA0, LA1, LA2, LA3, LA4, \
+LB0, LB1, LB2, LB3, LB4, \
+LC0, LC1, LC2, LC3, LC4, \
+LT0, LT1, \
+RA0, RA1, RA2, RA3, RA4, \
+RB0, RB1, RB2, RB3, RB4, \
+RC0, RC1, RC2, RC3, RC4, \
+RT0, RT1 \
+) LAYOUT( \
+LA0, LA1, LA2, LA3, LA4, RA0, RA1, RA2, RA3, RA4, \
+LB0, LB1, LB2, LB3, LB4, RB0, RB1, RB2, RB3, RB4, \
+LC0, LC1, LC2, LC3, LC4, RC0, RC1, RC2, RC3, RC4, \
+LT0, LT1, KC_NO, KC_NO, RT0, RT1 \
+)
diff --git a/keyboards/orbekk_ferris_sweep/keymaps/orbekk/keymap.c b/keyboards/orbekk_ferris_sweep/keymaps/orbekk/keymap.c
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/keyboards/orbekk_ferris_sweep/keymaps/orbekk/keymap.c
diff --git a/keyboards/orbekk_ferris_sweep/readme.md b/keyboards/orbekk_ferris_sweep/readme.md
new file mode 100644
index 0000000000..9a72c9bc5b
--- /dev/null
+++ b/keyboards/orbekk_ferris_sweep/readme.md
@@ -0,0 +1,29 @@
+# orbekk_dactyl
+
+https://docs.qmk.fm/#/porting_your_keyboard_to_qmk
+
+![orbekk_dactyl](imgur.com image replace me!)
+
+*A short description of the keyboard/project*
+
+* Keyboard Maintainer: [KJ Orbekk](https://github.com/orbekk)
+* Hardware Supported: *The PCBs, controllers supported*
+* Hardware Availability: *Links to where you can find this hardware*
+
+Make example for this keyboard (after setting up your build environment):
+
+ make orbekk_dactyl:default
+
+Flashing example for this keyboard:
+
+ make orbekk_dactyl:default:flash
+
+See the [build environment setup](https://docs.qmk.fm/#/getting_started_build_tools) and the [make instructions](https://docs.qmk.fm/#/getting_started_make_guide) for more information. Brand new to QMK? Start with our [Complete Newbs Guide](https://docs.qmk.fm/#/newbs).
+
+## Bootloader
+
+Enter the bootloader in 3 ways:
+
+* **Bootmagic reset**: Hold down the key at (0,0) in the matrix (usually the top left key or Escape) and plug in the keyboard
+* **Physical reset button**: Briefly press the button on the back of the PCB - some may have pads you must short instead
+* **Keycode in layout**: Press the key mapped to `QK_BOOT` if it is available
diff --git a/keyboards/orbekk_ferris_sweep/rules.mk b/keyboards/orbekk_ferris_sweep/rules.mk
new file mode 100644
index 0000000000..161ec22b16
--- /dev/null
+++ b/keyboards/orbekk_ferris_sweep/rules.mk
@@ -0,0 +1 @@
+SERIAL_DRIVER = vendor