diff --git a/keyboards/yowkees/keyball39/config.h b/keyboards/yowkees/keyball39/config.h
new file mode 100644
index 00000000000..0c96642e6ae
--- /dev/null
+++ b/keyboards/yowkees/keyball39/config.h
@@ -0,0 +1,73 @@
+/*
+Copyright 2022 @Yowkees
+Copyright 2022 MURAOKA Taro (aka KoRoN, @kaoriya)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+#pragma once
+
+// Key matrix parameters
+#define MATRIX_MASKED
+
+// Split parameters
+#define SPLIT_HAND_MATRIX_GRID_LOW_IS_LEFT
+#ifdef OLED_ENABLE
+# define SPLIT_OLED_ENABLE
+#endif
+
+// If your PC does not recognize Keyball, try setting this macro. This macro
+// increases the firmware size by 200 bytes, so it is disabled by default, but
+// it has been reported to work well in such cases.
+//#define SPLIT_WATCHDOG_ENABLE
+
+// PMW3360 configuration
+#define PMW33XX_CS_PIN B6
+
+#define SPLIT_TRANSACTION_IDS_KB KEYBALL_GET_INFO, KEYBALL_GET_MOTION, KEYBALL_SET_CPI
+
+// RGB LED settings
+#ifdef RGBLIGHT_ENABLE
+# define RGBLED_NUM 48
+# define RGBLED_SPLIT { 24, 24 } // (24 + 22)
+# ifndef RGBLIGHT_LIMIT_VAL
+# define RGBLIGHT_LIMIT_VAL 150 // limitated for power consumption
+# endif
+# ifndef RGBLIGHT_VAL_STEP
+# define RGBLIGHT_VAL_STEP 15
+# endif
+# ifndef RGBLIGHT_HUE_STEP
+# define RGBLIGHT_HUE_STEP 17
+# endif
+# ifndef RGBLIGHT_SAT_STEP
+# define RGBLIGHT_SAT_STEP 17
+# endif
+#endif
+#ifdef RGB_MATRIX_ENABLE
+# define RGB_MATRIX_SPLIT { 24, 24 }
+#endif
+
+#ifndef OLED_FONT_H
+# define OLED_FONT_H "../lib/logofont/logofont.c"
+# define OLED_FONT_START 32
+# define OLED_FONT_END 195
+#endif
+
+#if !defined(LAYER_STATE_8BIT) && !defined(LAYER_STATE_16BIT) && !defined(LAYER_STATE_32BIT)
+# define LAYER_STATE_8BIT
+#endif
+
+// To squeeze firmware size
+#undef LOCKING_SUPPORT_ENABLE
+#undef LOCKING_RESYNC_ENABLE
diff --git a/keyboards/yowkees/keyball39/keyball39.c b/keyboards/yowkees/keyball39/keyball39.c
new file mode 100644
index 00000000000..5472a0d48ff
--- /dev/null
+++ b/keyboards/yowkees/keyball39/keyball39.c
@@ -0,0 +1,46 @@
+/*
+Copyright 2021 @Yowkees
+Copyright 2022 MURAOKA Taro (aka KoRoN, @kaoriya)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+#include QMK_KEYBOARD_H
+
+#include "lib/keyball/keyball.h"
+
+//////////////////////////////////////////////////////////////////////////////
+
+// clang-format off
+matrix_row_t matrix_mask[MATRIX_ROWS] = {
+ 0b00011111,
+ 0b00011111,
+ 0b00011111,
+ 0b00111111,
+ 0b00011111,
+ 0b00011111,
+ 0b00011111,
+ 0b00111111,
+};
+// clang-format on
+
+void keyball_on_adjust_layout(keyball_adjust_t v) {
+#ifdef RGBLIGHT_ENABLE
+ // adjust RGBLIGHT's clipping and effect ranges
+ uint8_t lednum_this = keyball.this_have_ball ? 22 : 24;
+ uint8_t lednum_that = !keyball.that_enable ? 0 : keyball.that_have_ball ? 22 : 24;
+ rgblight_set_clipping_range(is_keyboard_left() ? 0 : lednum_that, lednum_this);
+ rgblight_set_effect_range(0, lednum_this + lednum_that);
+#endif
+}
diff --git a/keyboards/yowkees/keyball39/keyball39.h b/keyboards/yowkees/keyball39/keyball39.h
new file mode 100644
index 00000000000..171f2083ed3
--- /dev/null
+++ b/keyboards/yowkees/keyball39/keyball39.h
@@ -0,0 +1,22 @@
+/*
+Copyright 2022 @Yowkees
+Copyright 2022 MURAOKA Taro (aka KoRoN, @kaoriya)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+#pragma once
+
+#include "quantum.h"
+#include "lib/keyball/keyball.h"
diff --git a/keyboards/yowkees/keyball39/keyboard.json b/keyboards/yowkees/keyball39/keyboard.json
new file mode 100644
index 00000000000..3aedd9c0c42
--- /dev/null
+++ b/keyboards/yowkees/keyball39/keyboard.json
@@ -0,0 +1,239 @@
+{
+ "keyboard_name": "Keyball39",
+ "manufacturer": "Yowkees",
+ "url": "https://github.com/Yowkees/keyball",
+ "maintainer": "qmk",
+ "usb": {
+ "vid": "0x5957",
+ "pid": "0x0200",
+ "device_version": "1.0.0"
+ },
+ "development_board": "promicro",
+ "matrix_pins": {
+ "rows": ["F4", "F5", "F6", "F7"],
+ "cols": ["D4", "C6", "D7", "E6", "B4", "B5"]
+ },
+ "diode_direction": "COL2ROW",
+ "debounce": 5,
+ "features": {
+ "bootmagic": false,
+ "mousekey": false,
+ "extrakey": false,
+ "console": false,
+ "command": false,
+ "nkro": false,
+ "backlight": false,
+ "rgblight": true,
+ "rgb_matrix": false,
+ "oled": true,
+ "pointing_device": true,
+ "audio": false,
+ "sleep_led": false,
+ "grave_esc": false,
+ "magic": false,
+ "space_cadet": false
+ },
+ "build": {
+ "lto": true
+ },
+ "split": {
+ "enabled": true,
+ "soft_serial_pin": "D2",
+ "transport": {
+ "protocol": "serial"
+ },
+ "usb_detect": {
+ "enabled": true
+ },
+ "handedness": {
+ "matrix_grid": ["F6", "B5"]
+ }
+ },
+ "rgblight": {
+ "driver": "ws2812"
+ },
+ "rgb_matrix": {
+ "driver": "ws2812"
+ },
+ "ws2812": {
+ "pin": "D3"
+ },
+ "layout_aliases": {
+ "LAYOUT": "LAYOUT_right_ball",
+ "LAYOUT_universal": "LAYOUT_no_ball"
+ },
+ "layouts": {
+ "LAYOUT_no_ball": {
+ "layout": [
+ {"matrix": [0, 0], "x": 0, "y": 0.6},
+ {"matrix": [0, 1], "x": 1, "y": 0.25},
+ {"matrix": [0, 2], "x": 2, "y": 0},
+ {"matrix": [0, 3], "x": 3, "y": 0.125},
+ {"matrix": [0, 4], "x": 4, "y": 0.25},
+ {"matrix": [4, 4], "x": 9, "y": 0.25},
+ {"matrix": [4, 3], "x": 10, "y": 0.125},
+ {"matrix": [4, 2], "x": 11, "y": 0},
+ {"matrix": [4, 1], "x": 12, "y": 0.25},
+ {"matrix": [4, 0], "x": 13, "y": 0.6},
+ {"matrix": [1, 0], "x": 0, "y": 1.6},
+ {"matrix": [1, 1], "x": 1, "y": 1.25},
+ {"matrix": [1, 2], "x": 2, "y": 1.0},
+ {"matrix": [1, 3], "x": 3, "y": 1.125},
+ {"matrix": [1, 4], "x": 4, "y": 1.25},
+ {"matrix": [5, 4], "x": 9, "y": 1.25},
+ {"matrix": [5, 3], "x": 10, "y": 1.125},
+ {"matrix": [5, 2], "x": 11, "y": 1.0},
+ {"matrix": [5, 1], "x": 12, "y": 1.25},
+ {"matrix": [5, 0], "x": 13, "y": 1.6},
+ {"matrix": [2, 0], "x": 0, "y": 2.6},
+ {"matrix": [2, 1], "x": 1, "y": 2.25},
+ {"matrix": [2, 2], "x": 2, "y": 2.0},
+ {"matrix": [2, 3], "x": 3, "y": 2.125},
+ {"matrix": [2, 4], "x": 4, "y": 2.25},
+ {"matrix": [6, 4], "x": 9, "y": 2.25},
+ {"matrix": [6, 3], "x": 10, "y": 2.125},
+ {"matrix": [6, 2], "x": 11, "y": 2.0},
+ {"matrix": [6, 1], "x": 12, "y": 2.25},
+ {"matrix": [6, 0], "x": 13, "y": 2.6},
+ {"matrix": [3, 0], "x": 0, "y": 3.6},
+ {"matrix": [3, 1], "x": 1, "y": 3.25},
+ {"matrix": [3, 2], "x": 2, "y": 3.0},
+ {"matrix": [3, 3], "x": 3.15, "y": 3.5},
+ {"matrix": [3, 4], "x": 4.25, "y": 3.61},
+ {"matrix": [3, 5], "x": 5.38, "y": 3.86},
+ {"matrix": [7, 5], "x": 7.62, "y": 3.86},
+ {"matrix": [7, 4], "x": 8.75, "y": 3.61},
+ {"matrix": [7, 3], "x": 9.85, "y": 3.5},
+ {"matrix": [7, 2], "x": 11, "y": 3.0},
+ {"matrix": [7, 1], "x": 12, "y": 3.25},
+ {"matrix": [7, 0], "x": 13, "y": 3.6}
+ ]
+ },
+ "LAYOUT_left_ball": {
+ "layout": [
+ {"matrix": [0, 0], "x": 0, "y": 0.6},
+ {"matrix": [0, 1], "x": 1, "y": 0.25},
+ {"matrix": [0, 2], "x": 2, "y": 0},
+ {"matrix": [0, 3], "x": 3, "y": 0.125},
+ {"matrix": [0, 4], "x": 4, "y": 0.25},
+ {"matrix": [4, 4], "x": 9, "y": 0.25},
+ {"matrix": [4, 3], "x": 10, "y": 0.125},
+ {"matrix": [4, 2], "x": 11, "y": 0},
+ {"matrix": [4, 1], "x": 12, "y": 0.25},
+ {"matrix": [4, 0], "x": 13, "y": 0.6},
+ {"matrix": [1, 0], "x": 0, "y": 1.6},
+ {"matrix": [1, 1], "x": 1, "y": 1.25},
+ {"matrix": [1, 2], "x": 2, "y": 1.0},
+ {"matrix": [1, 3], "x": 3, "y": 1.125},
+ {"matrix": [1, 4], "x": 4, "y": 1.25},
+ {"matrix": [5, 4], "x": 9, "y": 1.25},
+ {"matrix": [5, 3], "x": 10, "y": 1.125},
+ {"matrix": [5, 2], "x": 11, "y": 1.0},
+ {"matrix": [5, 1], "x": 12, "y": 1.25},
+ {"matrix": [5, 0], "x": 13, "y": 1.6},
+ {"matrix": [2, 0], "x": 0, "y": 2.6},
+ {"matrix": [2, 1], "x": 1, "y": 2.25},
+ {"matrix": [2, 2], "x": 2, "y": 2.0},
+ {"matrix": [2, 3], "x": 3, "y": 2.125},
+ {"matrix": [2, 4], "x": 4, "y": 2.25},
+ {"matrix": [6, 4], "x": 9, "y": 2.25},
+ {"matrix": [6, 3], "x": 10, "y": 2.125},
+ {"matrix": [6, 2], "x": 11, "y": 2.0},
+ {"matrix": [6, 1], "x": 12, "y": 2.25},
+ {"matrix": [6, 0], "x": 13, "y": 2.6},
+ {"matrix": [3, 0], "x": 0, "y": 3.6},
+ {"matrix": [3, 4], "x": 4.25, "y": 3.61},
+ {"matrix": [3, 5], "x": 5.38, "y": 3.86},
+ {"matrix": [7, 5], "x": 7.62, "y": 3.86},
+ {"matrix": [7, 4], "x": 8.75, "y": 3.61},
+ {"matrix": [7, 3], "x": 9.85, "y": 3.5},
+ {"matrix": [7, 2], "x": 11, "y": 3.0},
+ {"matrix": [7, 1], "x": 12, "y": 3.25},
+ {"matrix": [7, 0], "x": 13, "y": 3.6}
+ ]
+ },
+ "LAYOUT_right_ball": {
+ "layout": [
+ {"matrix": [0, 0], "x": 0, "y": 0.6},
+ {"matrix": [0, 1], "x": 1, "y": 0.25},
+ {"matrix": [0, 2], "x": 2, "y": 0},
+ {"matrix": [0, 3], "x": 3, "y": 0.125},
+ {"matrix": [0, 4], "x": 4, "y": 0.25},
+ {"matrix": [4, 4], "x": 9, "y": 0.25},
+ {"matrix": [4, 3], "x": 10, "y": 0.125},
+ {"matrix": [4, 2], "x": 11, "y": 0},
+ {"matrix": [4, 1], "x": 12, "y": 0.25},
+ {"matrix": [4, 0], "x": 13, "y": 0.6},
+ {"matrix": [1, 0], "x": 0, "y": 1.6},
+ {"matrix": [1, 1], "x": 1, "y": 1.25},
+ {"matrix": [1, 2], "x": 2, "y": 1.0},
+ {"matrix": [1, 3], "x": 3, "y": 1.125},
+ {"matrix": [1, 4], "x": 4, "y": 1.25},
+ {"matrix": [5, 4], "x": 9, "y": 1.25},
+ {"matrix": [5, 3], "x": 10, "y": 1.125},
+ {"matrix": [5, 2], "x": 11, "y": 1.0},
+ {"matrix": [5, 1], "x": 12, "y": 1.25},
+ {"matrix": [5, 0], "x": 13, "y": 1.6},
+ {"matrix": [2, 0], "x": 0, "y": 2.6},
+ {"matrix": [2, 1], "x": 1, "y": 2.25},
+ {"matrix": [2, 2], "x": 2, "y": 2.0},
+ {"matrix": [2, 3], "x": 3, "y": 2.125},
+ {"matrix": [2, 4], "x": 4, "y": 2.25},
+ {"matrix": [6, 4], "x": 9, "y": 2.25},
+ {"matrix": [6, 3], "x": 10, "y": 2.125},
+ {"matrix": [6, 2], "x": 11, "y": 2.0},
+ {"matrix": [6, 1], "x": 12, "y": 2.25},
+ {"matrix": [6, 0], "x": 13, "y": 2.6},
+ {"matrix": [3, 0], "x": 0, "y": 3.6},
+ {"matrix": [3, 1], "x": 1, "y": 3.25},
+ {"matrix": [3, 2], "x": 2, "y": 3.0},
+ {"matrix": [3, 3], "x": 3.15, "y": 3.5},
+ {"matrix": [3, 4], "x": 4.25, "y": 3.61},
+ {"matrix": [3, 5], "x": 5.38, "y": 3.86},
+ {"matrix": [7, 5], "x": 7.62, "y": 3.86},
+ {"matrix": [7, 4], "x": 8.75, "y": 3.61},
+ {"matrix": [7, 0], "x": 13, "y": 3.6}
+ ]
+ },
+ "LAYOUT_dual_ball": {
+ "layout": [
+ {"matrix": [0, 0], "x": 0, "y": 0.6},
+ {"matrix": [0, 1], "x": 1, "y": 0.25},
+ {"matrix": [0, 2], "x": 2, "y": 0},
+ {"matrix": [0, 3], "x": 3, "y": 0.125},
+ {"matrix": [0, 4], "x": 4, "y": 0.25},
+ {"matrix": [4, 4], "x": 9, "y": 0.25},
+ {"matrix": [4, 3], "x": 10, "y": 0.125},
+ {"matrix": [4, 2], "x": 11, "y": 0},
+ {"matrix": [4, 1], "x": 12, "y": 0.25},
+ {"matrix": [4, 0], "x": 13, "y": 0.6},
+ {"matrix": [1, 0], "x": 0, "y": 1.6},
+ {"matrix": [1, 1], "x": 1, "y": 1.25},
+ {"matrix": [1, 2], "x": 2, "y": 1.0},
+ {"matrix": [1, 3], "x": 3, "y": 1.125},
+ {"matrix": [1, 4], "x": 4, "y": 1.25},
+ {"matrix": [5, 4], "x": 9, "y": 1.25},
+ {"matrix": [5, 3], "x": 10, "y": 1.125},
+ {"matrix": [5, 2], "x": 11, "y": 1.0},
+ {"matrix": [5, 1], "x": 12, "y": 1.25},
+ {"matrix": [5, 0], "x": 13, "y": 1.6},
+ {"matrix": [2, 0], "x": 0, "y": 2.6},
+ {"matrix": [2, 1], "x": 1, "y": 2.25},
+ {"matrix": [2, 2], "x": 2, "y": 2.0},
+ {"matrix": [2, 3], "x": 3, "y": 2.125},
+ {"matrix": [2, 4], "x": 4, "y": 2.25},
+ {"matrix": [6, 4], "x": 9, "y": 2.25},
+ {"matrix": [6, 3], "x": 10, "y": 2.125},
+ {"matrix": [6, 2], "x": 11, "y": 2.0},
+ {"matrix": [6, 1], "x": 12, "y": 2.25},
+ {"matrix": [6, 0], "x": 13, "y": 2.6},
+ {"matrix": [3, 0], "x": 0, "y": 3.6},
+ {"matrix": [3, 4], "x": 4.25, "y": 3.61},
+ {"matrix": [3, 5], "x": 5.38, "y": 3.86},
+ {"matrix": [7, 5], "x": 7.62, "y": 3.86},
+ {"matrix": [7, 4], "x": 8.75, "y": 3.61},
+ {"matrix": [7, 0], "x": 13, "y": 3.6}
+ ]
+ }
+ }
+}
diff --git a/keyboards/yowkees/keyball39/keymaps/default/config.h b/keyboards/yowkees/keyball39/keymaps/default/config.h
new file mode 100644
index 00000000000..2e4971f0f5c
--- /dev/null
+++ b/keyboards/yowkees/keyball39/keymaps/default/config.h
@@ -0,0 +1,41 @@
+/*
+This is the c configuration file for the keymap
+
+Copyright 2022 @Yowkees
+Copyright 2022 MURAOKA Taro (aka KoRoN, @kaoriya)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+#pragma once
+
+#define PMW33XX_UPLOAD_SROM
+
+#ifdef RGBLIGHT_ENABLE
+//# define RGBLIGHT_EFFECT_BREATHING
+//# define RGBLIGHT_EFFECT_RAINBOW_MOOD
+//# define RGBLIGHT_EFFECT_RAINBOW_SWIRL
+//# define RGBLIGHT_EFFECT_SNAKE
+//# define RGBLIGHT_EFFECT_KNIGHT
+//# define RGBLIGHT_EFFECT_CHRISTMAS
+//# define RGBLIGHT_EFFECT_STATIC_GRADIENT
+//# define RGBLIGHT_EFFECT_RGB_TEST
+//# define RGBLIGHT_EFFECT_ALTERNATING
+//# define RGBLIGHT_EFFECT_TWINKLE
+#endif
+
+#define TAP_CODE_DELAY 5
+
+#define POINTING_DEVICE_AUTO_MOUSE_ENABLE
+#define AUTO_MOUSE_DEFAULT_LAYER 1
diff --git a/keyboards/yowkees/keyball39/keymaps/default/keymap.c b/keyboards/yowkees/keyball39/keymaps/default/keymap.c
new file mode 100644
index 00000000000..4ba6da4d49a
--- /dev/null
+++ b/keyboards/yowkees/keyball39/keymaps/default/keymap.c
@@ -0,0 +1,69 @@
+/*
+Copyright 2022 @Yowkees
+Copyright 2022 MURAOKA Taro (aka KoRoN, @kaoriya)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+#include QMK_KEYBOARD_H
+
+// clang-format off
+const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
+ // keymap for default
+ [0] = LAYOUT_no_ball(
+ KC_Q, KC_W, KC_E, KC_R, KC_T, KC_Y, KC_U, KC_I, KC_O, KC_P,
+ KC_A, KC_S, KC_D, KC_F, KC_G, KC_H, KC_J, KC_K, KC_L, KC_MINS,
+ KC_Z, KC_X, KC_C, KC_V, KC_B, KC_N, KC_M, KC_COMM, KC_DOT, KC_SLSH,
+ KC_LCTL, KC_LGUI, KC_LALT, LSFT_T(KC_LNG2), LT(1,KC_SPC), LT(3,KC_LNG1), KC_BSPC, LT(2,KC_ENT), LSFT_T(KC_LNG2), KC_RALT, KC_RGUI, KC_RSFT
+ ),
+
+ [1] = LAYOUT_no_ball(
+ KC_F1, KC_F2, KC_F3, KC_F4, KC_RBRC, KC_F6, KC_F7, KC_F8, KC_F9, KC_F10,
+ KC_F5, KC_EXLM, S(KC_6), S(KC_INT3), S(KC_8), S(KC_INT1), KC_BTN1, KC_PGUP, KC_BTN2, KC_SCLN,
+ S(KC_EQL), S(KC_LBRC), S(KC_7), S(KC_2), S(KC_RBRC), KC_LBRC, KC_DLR, KC_PGDN, KC_BTN3, KC_F11,
+ KC_INT1, KC_EQL, S(KC_3), _______, _______, _______, TO(2), TO(0), _______, KC_RALT, KC_RGUI, KC_F12
+ ),
+
+ [2] = LAYOUT_no_ball(
+ KC_TAB, KC_7, KC_8, KC_9, KC_MINS, KC_NUHS, _______, KC_BTN3, _______, KC_BSPC,
+ S(KC_QUOT), KC_4, KC_5, KC_6, S(KC_SCLN), S(KC_9), KC_BTN1, KC_UP, KC_BTN2, KC_QUOT,
+ KC_SLSH, KC_1, KC_2, KC_3, S(KC_MINS), S(KC_NUHS), KC_LEFT, KC_DOWN, KC_RGHT, _______,
+ KC_ESC, KC_0, KC_DOT, KC_DEL, KC_ENT, KC_BSPC, _______, _______, _______, _______, _______, _______
+ ),
+
+ [3] = LAYOUT_no_ball(
+ RGB_TOG, AML_TO, AML_I50, AML_D50, _______, _______, _______, SSNP_HOR, SSNP_VRT, SSNP_FRE,
+ RGB_MOD, RGB_HUI, RGB_SAI, RGB_VAI, SCRL_DVI, _______, _______, _______, _______, _______,
+ RGB_RMOD, RGB_HUD, RGB_SAD, RGB_VAD, SCRL_DVD, CPI_D1K, CPI_D100, CPI_I100, CPI_I1K, KBC_SAVE,
+ QK_BOOT, KBC_RST, _______, _______, _______, _______, _______, _______, _______, _______, KBC_RST, QK_BOOT
+ ),
+};
+// clang-format on
+
+layer_state_t layer_state_set_user(layer_state_t state) {
+ // Auto enable scroll mode when the highest layer is 3
+ keyball_set_scroll_mode(get_highest_layer(state) == 3);
+ return state;
+}
+
+#ifdef OLED_ENABLE
+
+# include "lib/oledkit/oledkit.h"
+
+void oledkit_render_info_user(void) {
+ keyball_oled_render_keyinfo();
+ keyball_oled_render_ballinfo();
+ keyball_oled_render_layerinfo();
+}
+#endif
diff --git a/keyboards/yowkees/keyball39/keymaps/develop/config.h b/keyboards/yowkees/keyball39/keymaps/develop/config.h
new file mode 100644
index 00000000000..7c18d0969f7
--- /dev/null
+++ b/keyboards/yowkees/keyball39/keymaps/develop/config.h
@@ -0,0 +1,33 @@
+/*
+This is the c configuration file for the keymap
+
+Copyright 2022 MURAOKA Taro (aka KoRoN, @kaoriya)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+#pragma once
+
+#ifdef RGBLIGHT_ENABLE
+# define RGBLIGHT_EFFECT_BREATHING
+# define RGBLIGHT_EFFECT_RAINBOW_MOOD
+# define RGBLIGHT_EFFECT_RAINBOW_SWIRL
+# define RGBLIGHT_EFFECT_SNAKE
+# define RGBLIGHT_EFFECT_KNIGHT
+# define RGBLIGHT_EFFECT_CHRISTMAS
+# define RGBLIGHT_EFFECT_STATIC_GRADIENT
+# define RGBLIGHT_EFFECT_RGB_TEST
+# define RGBLIGHT_EFFECT_ALTERNATING
+# define RGBLIGHT_EFFECT_TWINKLE
+#endif
diff --git a/keyboards/yowkees/keyball39/keymaps/develop/keymap.c b/keyboards/yowkees/keyball39/keymaps/develop/keymap.c
new file mode 100644
index 00000000000..680f8cc5bd9
--- /dev/null
+++ b/keyboards/yowkees/keyball39/keymaps/develop/keymap.c
@@ -0,0 +1,49 @@
+/*
+Copyright 2022 @Yowkees
+Copyright 2022 MURAOKA Taro (aka KoRoN, @kaoriya)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+#include QMK_KEYBOARD_H
+
+// clang-format off
+const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
+ // keymap for development
+ [0] = LAYOUT_no_ball(
+ KC_Q, KC_W, KC_E, KC_R, KC_T, KC_Y, KC_U, KC_I, KC_O, KC_P,
+ KC_A, KC_S, KC_D, KC_F, KC_G, KC_H, KC_J, KC_K, KC_L, KC_SCLN,
+ KC_Z, KC_X, KC_C, KC_V, KC_B, KC_N, KC_M, KC_COMM, KC_DOT, KC_SLSH,
+ KC_LCTL, KC_LGUI, KC_LALT, SCRL_MO, KC_SPC, LT(1,KC_TAB), LT(1,KC_BSPC), KC_ENT, SCRL_MO, KC_RALT, KC_RGUI, KC_RSFT
+ ),
+
+ [1] = LAYOUT_no_ball(
+ RGB_TOG, _______, _______, _______, _______, RGB_M_P, RGB_M_B, RGB_M_R, RGB_M_SW, RGB_M_SN,
+ RGB_MOD, RGB_HUI, RGB_SAI, RGB_VAI, SCRL_DVI, RGB_M_K, RGB_M_X, RGB_M_G, RGB_M_T, RGB_M_TW,
+ RGB_RMOD, RGB_HUD, RGB_SAD, RGB_VAD, SCRL_DVD, CPI_D1K, CPI_D100, CPI_I100, CPI_I1K, KBC_SAVE,
+ QK_BOOT, KBC_RST, _______, _______, _______, _______, _______, _______, _______, _______, KBC_RST, QK_BOOT
+ ),
+};
+// clang-format on
+
+#ifdef OLED_ENABLE
+
+# include "lib/oledkit/oledkit.h"
+
+void oledkit_render_info_user(void) {
+ keyball_oled_render_keyinfo();
+ keyball_oled_render_ballinfo();
+ keyball_oled_render_layerinfo();
+}
+#endif
diff --git a/keyboards/yowkees/keyball39/keymaps/develop/rules.mk b/keyboards/yowkees/keyball39/keymaps/develop/rules.mk
new file mode 100644
index 00000000000..a84a68c3b78
--- /dev/null
+++ b/keyboards/yowkees/keyball39/keymaps/develop/rules.mk
@@ -0,0 +1,2 @@
+# for debug
+#CONSOLE_ENABLE = yes
diff --git a/keyboards/yowkees/keyball39/keymaps/test/config.h b/keyboards/yowkees/keyball39/keymaps/test/config.h
new file mode 100644
index 00000000000..b8849eed0cc
--- /dev/null
+++ b/keyboards/yowkees/keyball39/keymaps/test/config.h
@@ -0,0 +1,25 @@
+/*
+This is the c configuration file for the keymap
+
+Copyright 2022 @Yowkees
+Copyright 2022 MURAOKA Taro (aka KoRoN, @kaoriya)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+#pragma once
+
+#ifdef RGBLIGHT_ENABLE
+# define RGBLIGHT_EFFECT_RGB_TEST // required for LED test
+#endif
diff --git a/keyboards/yowkees/keyball39/keymaps/test/keymap.c b/keyboards/yowkees/keyball39/keymaps/test/keymap.c
new file mode 100644
index 00000000000..4a7306480e0
--- /dev/null
+++ b/keyboards/yowkees/keyball39/keymaps/test/keymap.c
@@ -0,0 +1,49 @@
+/*
+Copyright 2022 @Yowkees
+Copyright 2022 MURAOKA Taro (aka KoRoN, @kaoriya)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+#include QMK_KEYBOARD_H
+
+// clang-format off
+const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
+ [0] = LAYOUT_no_ball(
+ KC_Q, KC_W, KC_E, KC_R, KC_T, KC_Y, KC_U, KC_I, KC_O, KC_P,
+ KC_A, KC_S, KC_D, KC_F, KC_G, KC_H, KC_J, KC_K, KC_L, KC_SCLN,
+ KC_Z, KC_X, KC_C, KC_V, KC_B, KC_N, KC_M, KC_COMM, KC_DOT, KC_SLSH,
+ KC_LCTL, KC_LGUI, KC_LALT, KC_ESC, KC_SPC, KC_TAB, KC_BSPC, KC_ENT, KC_ESC, KC_RALT, KC_RGUI, KC_RSFT
+ ),
+};
+// clang-format on
+
+void keyboard_post_init_user() {
+#ifdef RGBLIGHT_ENABLE
+ // Force RGB lights to show test animation without writing EEPROM.
+ rgblight_enable_noeeprom();
+ rgblight_mode_noeeprom(RGBLIGHT_MODE_RGB_TEST);
+#endif
+}
+
+#ifdef OLED_ENABLE
+
+# include "lib/oledkit/oledkit.h"
+
+void oledkit_render_info_user(void) {
+ keyball_oled_render_keyinfo();
+ keyball_oled_render_ballinfo();
+ keyball_oled_render_layerinfo();
+}
+#endif
diff --git a/keyboards/yowkees/keyball39/keymaps/test/rules.mk b/keyboards/yowkees/keyball39/keymaps/test/rules.mk
new file mode 100644
index 00000000000..a84a68c3b78
--- /dev/null
+++ b/keyboards/yowkees/keyball39/keymaps/test/rules.mk
@@ -0,0 +1,2 @@
+# for debug
+#CONSOLE_ENABLE = yes
diff --git a/keyboards/yowkees/keyball39/readme.md b/keyboards/yowkees/keyball39/readme.md
new file mode 100644
index 00000000000..3d52cc8bc71
--- /dev/null
+++ b/keyboards/yowkees/keyball39/readme.md
@@ -0,0 +1,21 @@
+# Keyball39
+
+![Keyball39](https://raw.githubusercontent.com/Yowkees/keyball/main/keyball39/doc/rev1/images/kb39_001.jpg)
+
+A split keyboard with 39 vertically staggered keys and 34mm track ball.
+
+* Keyboard Maintainer: [@Yowkees](https://twitter.com/Yowkees)
+* Hardware Supported: Keyball39 PCB, ProMicro
+* Hardware Availability:
+ *
+ *
+
+Make example for this keyboard (after setting up your build environment):
+
+ make yowkees/keyball39:default
+
+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).
+
+## Special keycodes
+
+See [Special Keycode](../lib/keyball/keycodes.md) file.
diff --git a/keyboards/yowkees/keyball39/rules.mk b/keyboards/yowkees/keyball39/rules.mk
new file mode 100644
index 00000000000..906354aea0d
--- /dev/null
+++ b/keyboards/yowkees/keyball39/rules.mk
@@ -0,0 +1,7 @@
+# Optical sensor driver for trackball.
+POINTING_DEVICE_DRIVER = pmw3360
+
+SRC += lib/oledkit/oledkit.c # OLED utility for Keyball series.
+
+# Include common library
+SRC += lib/keyball/keyball.c
diff --git a/keyboards/yowkees/lib/duplexmatrix/duplexmatrix.c b/keyboards/yowkees/lib/duplexmatrix/duplexmatrix.c
new file mode 100644
index 00000000000..97fa4217532
--- /dev/null
+++ b/keyboards/yowkees/lib/duplexmatrix/duplexmatrix.c
@@ -0,0 +1,177 @@
+/*
+Copyright 2022 MURAOKA Taro (aka KoRoN, @kaoriya)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+#include
+#include "quantum.h"
+#include "matrix.h"
+#include "debounce.h"
+
+#ifdef SPLIT_KEYBOARD
+# include "split_common/split_util.h"
+# include "split_common/transactions.h"
+#endif
+
+#ifdef SPLIT_KEYBOARD
+# define PINNUM_ROW (MATRIX_ROWS / 2)
+# define ROWS_PER_HAND (MATRIX_ROWS / 2)
+#else
+# define PINNUM_ROW (MATRIX_ROWS)
+# define ROWS_PER_HAND (MATRIX_ROWS)
+#endif
+#define PINNUM_COL (MATRIX_COLS / 2)
+
+#define MATRIXSIZE_PER_HAND (ROWS_PER_HAND * sizeof(matrix_row_t))
+
+static pin_t row_pins[PINNUM_ROW] = MATRIX_ROW_PINS;
+static pin_t col_pins[PINNUM_COL] = MATRIX_COL_PINS;
+
+static inline void set_pin_input(pin_t pin) {
+ setPinInputHigh(pin);
+}
+
+static void set_pins_input(pin_t* pins, uint8_t n) {
+ for (uint8_t i = 0; i < n; i++) {
+ set_pin_input(pins[i]);
+ }
+}
+
+static inline void set_pin_output(pin_t pin) {
+ setPinOutput(pin);
+ writePinLow(pin);
+}
+
+static inline bool get_pin(pin_t pin) {
+ return readPin(pin);
+}
+
+__attribute__((weak)) void duplex_scan_raw_post_kb(matrix_row_t out_matrix[]) {}
+
+static void duplex_scan_raw(matrix_row_t out_matrix[]) {
+ // scan column to row
+ for (uint8_t row = 0; row < PINNUM_ROW; row++) {
+ set_pin_output(row_pins[row]);
+ matrix_output_select_delay();
+ for (uint8_t col = 0; col < PINNUM_COL; col++) {
+ if (!get_pin(col_pins[col])) {
+ out_matrix[row] |= 1 << col;
+ }
+ }
+ set_pin_input(row_pins[row]);
+ matrix_output_unselect_delay(row, false);
+ }
+
+ // scan row to column.
+ for (uint8_t col = 0; col < PINNUM_COL; col++) {
+ set_pin_output(col_pins[col]);
+ matrix_output_select_delay();
+ matrix_row_t shifter = ((matrix_row_t)1) << (col + PINNUM_COL);
+ for (uint8_t row = 0; row < PINNUM_ROW; row++) {
+ if (!get_pin(row_pins[row])) {
+ out_matrix[row] |= shifter;
+ }
+ }
+ set_pin_input(col_pins[col]);
+ matrix_output_unselect_delay(col, false);
+ }
+
+ duplex_scan_raw_post_kb(out_matrix);
+}
+
+static bool duplex_scan(matrix_row_t current_matrix[]) {
+ bool changed = false;
+ matrix_row_t tmp[MATRIX_ROWS] = {0};
+
+ duplex_scan_raw(tmp);
+ for (uint8_t row = 0; row < PINNUM_ROW; row++) {
+ if (tmp[row] != current_matrix[row]) {
+ changed = true;
+ current_matrix[row] = tmp[row];
+ }
+ }
+ return changed;
+}
+
+#ifdef SPLIT_KEYBOARD
+static uint8_t thisHand, thatHand;
+#else
+# define thisHand 0
+#endif
+
+void matrix_init_custom(void) {
+#ifdef SPLIT_KEYBOARD
+ split_pre_init();
+#endif
+
+ set_pins_input(col_pins, PINNUM_COL);
+ set_pins_input(row_pins, PINNUM_ROW);
+
+#ifdef SPLIT_KEYBOARD
+ thisHand = isLeftHand ? 0 : ROWS_PER_HAND;
+ thatHand = ROWS_PER_HAND - thisHand;
+
+ split_post_init();
+#endif
+}
+
+#ifdef SPLIT_KEYBOARD
+
+// user-defined overridable functions
+__attribute__((weak)) void matrix_slave_scan_kb(void) {
+ matrix_slave_scan_user();
+}
+
+__attribute__((weak)) void matrix_slave_scan_user(void) {}
+
+#endif
+
+// declare matrix buffers which defined in quantum/matrix_common.c
+extern matrix_row_t raw_matrix[MATRIX_ROWS];
+extern matrix_row_t matrix[MATRIX_ROWS];
+
+uint8_t matrix_scan(void) {
+ bool changed = duplex_scan(raw_matrix);
+
+ debounce(raw_matrix, matrix + thisHand, ROWS_PER_HAND, changed);
+
+#ifdef SPLIT_KEYBOARD
+ if (!is_keyboard_master()) {
+ // send to primary.
+ transport_slave(matrix + thatHand, matrix + thisHand);
+ matrix_slave_scan_kb();
+ return changed;
+ }
+
+ // receive from secondary.
+ static bool last_connected = false;
+ matrix_row_t* that_raw = raw_matrix + ROWS_PER_HAND;
+ memset(that_raw, 0, MATRIXSIZE_PER_HAND);
+ if (transport_master_if_connected(matrix + thisHand, that_raw)) {
+ last_connected = true;
+ if (memcmp(matrix + thatHand, that_raw, MATRIXSIZE_PER_HAND) != 0) {
+ memcpy(matrix + thatHand, that_raw, MATRIXSIZE_PER_HAND);
+ changed = true;
+ }
+ } else if (last_connected) {
+ last_connected = false;
+ memset(matrix + thatHand, 0, MATRIXSIZE_PER_HAND);
+ changed = true;
+ }
+#endif
+
+ matrix_scan_kb();
+ return changed;
+}
diff --git a/keyboards/yowkees/lib/duplexmatrix/duplexmatrix.h b/keyboards/yowkees/lib/duplexmatrix/duplexmatrix.h
new file mode 100644
index 00000000000..44f57c629aa
--- /dev/null
+++ b/keyboards/yowkees/lib/duplexmatrix/duplexmatrix.h
@@ -0,0 +1,20 @@
+/*
+Copyright 2022 MURAOKA Taro (aka KoRoN, @kaoriya)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+#pragma once
+
+void duplex_scan_raw_post_kb(matrix_row_t out_matrix[]);
diff --git a/keyboards/yowkees/lib/glcdfont.c b/keyboards/yowkees/lib/glcdfont.c
new file mode 100644
index 00000000000..c634898aed1
--- /dev/null
+++ b/keyboards/yowkees/lib/glcdfont.c
@@ -0,0 +1,234 @@
+// This is the 'classic' fixed-space bitmap font for Adafruit_GFX since 1.0.
+// See gfxfont.h for newer custom bitmap font info.
+
+#include "progmem.h"
+
+// Standard ASCII 5x7 font
+// clang-format off
+const unsigned char font[] PROGMEM = {
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x3E, 0x5B, 0x4F, 0x5B, 0x3E, 0x00,
+ 0x3E, 0x6B, 0x4F, 0x6B, 0x3E, 0x00,
+ 0x1C, 0x3E, 0x7C, 0x3E, 0x1C, 0x00,
+ 0x18, 0x3C, 0x7E, 0x3C, 0x18, 0x00,
+ 0x1C, 0x57, 0x7D, 0x57, 0x1C, 0x00,
+ 0x1C, 0x5E, 0x7F, 0x5E, 0x1C, 0x00,
+ 0x00, 0x18, 0x3C, 0x18, 0x00, 0x00,
+ 0xFF, 0xE7, 0xC3, 0xE7, 0xFF, 0x00,
+ 0x00, 0x18, 0x24, 0x18, 0x00, 0x00,
+ 0xFF, 0xE7, 0xDB, 0xE7, 0xFF, 0x00,
+ 0x30, 0x48, 0x3A, 0x06, 0x0E, 0x00,
+ 0x26, 0x29, 0x79, 0x29, 0x26, 0x00,
+ 0x40, 0x7F, 0x05, 0x05, 0x07, 0x00,
+ 0x40, 0x7F, 0x05, 0x25, 0x3F, 0x00,
+ 0x5A, 0x3C, 0xE7, 0x3C, 0x5A, 0x00,
+ 0x7F, 0x3E, 0x1C, 0x1C, 0x08, 0x00,
+ 0x08, 0x1C, 0x1C, 0x3E, 0x7F, 0x00,
+ 0x14, 0x22, 0x7F, 0x22, 0x14, 0x00,
+ 0x5F, 0x5F, 0x00, 0x5F, 0x5F, 0x00,
+ 0x06, 0x09, 0x7F, 0x01, 0x7F, 0x00,
+ 0x00, 0x66, 0x89, 0x95, 0x6A, 0x00,
+ 0x60, 0x60, 0x60, 0x60, 0x60, 0x00,
+ 0x94, 0xA2, 0xFF, 0xA2, 0x94, 0x00,
+ 0x08, 0x04, 0x7E, 0x04, 0x08, 0x00,
+ 0x10, 0x20, 0x7E, 0x20, 0x10, 0x00,
+ 0x08, 0x08, 0x2A, 0x1C, 0x08, 0x00,
+ 0x08, 0x1C, 0x2A, 0x08, 0x08, 0x00,
+ 0x1E, 0x10, 0x10, 0x10, 0x10, 0x00,
+ 0x0C, 0x1E, 0x0C, 0x1E, 0x0C, 0x00,
+ 0x30, 0x38, 0x3E, 0x38, 0x30, 0x00,
+ 0x06, 0x0E, 0x3E, 0x0E, 0x06, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x5F, 0x00, 0x00, 0x00,
+ 0x00, 0x07, 0x00, 0x07, 0x00, 0x00,
+ 0x14, 0x7F, 0x14, 0x7F, 0x14, 0x00,
+ 0x24, 0x2A, 0x7F, 0x2A, 0x12, 0x00,
+ 0x23, 0x13, 0x08, 0x64, 0x62, 0x00,
+ 0x36, 0x49, 0x56, 0x20, 0x50, 0x00,
+ 0x00, 0x08, 0x07, 0x03, 0x00, 0x00,
+ 0x00, 0x1C, 0x22, 0x41, 0x00, 0x00,
+ 0x00, 0x41, 0x22, 0x1C, 0x00, 0x00,
+ 0x2A, 0x1C, 0x7F, 0x1C, 0x2A, 0x00,
+ 0x08, 0x08, 0x3E, 0x08, 0x08, 0x00,
+ 0x00, 0x80, 0x70, 0x30, 0x00, 0x00,
+ 0x08, 0x08, 0x08, 0x08, 0x08, 0x00,
+ 0x00, 0x00, 0x60, 0x60, 0x00, 0x00,
+ 0x20, 0x10, 0x08, 0x04, 0x02, 0x00,
+ 0x3E, 0x51, 0x49, 0x45, 0x3E, 0x00,
+ 0x00, 0x42, 0x7F, 0x40, 0x00, 0x00,
+ 0x72, 0x49, 0x49, 0x49, 0x46, 0x00,
+ 0x21, 0x41, 0x49, 0x4D, 0x33, 0x00,
+ 0x18, 0x14, 0x12, 0x7F, 0x10, 0x00,
+ 0x27, 0x45, 0x45, 0x45, 0x39, 0x00,
+ 0x3C, 0x4A, 0x49, 0x49, 0x31, 0x00,
+ 0x41, 0x21, 0x11, 0x09, 0x07, 0x00,
+ 0x36, 0x49, 0x49, 0x49, 0x36, 0x00,
+ 0x46, 0x49, 0x49, 0x29, 0x1E, 0x00,
+ 0x00, 0x00, 0x14, 0x00, 0x00, 0x00,
+ 0x00, 0x40, 0x34, 0x00, 0x00, 0x00,
+ 0x00, 0x08, 0x14, 0x22, 0x41, 0x00,
+ 0x14, 0x14, 0x14, 0x14, 0x14, 0x00,
+ 0x00, 0x41, 0x22, 0x14, 0x08, 0x00,
+ 0x02, 0x01, 0x59, 0x09, 0x06, 0x00,
+ 0x3E, 0x41, 0x5D, 0x59, 0x4E, 0x00,
+ 0x7C, 0x12, 0x11, 0x12, 0x7C, 0x00,
+ 0x7F, 0x49, 0x49, 0x49, 0x36, 0x00,
+ 0x3E, 0x41, 0x41, 0x41, 0x22, 0x00,
+ 0x7F, 0x41, 0x41, 0x41, 0x3E, 0x00,
+ 0x7F, 0x49, 0x49, 0x49, 0x41, 0x00,
+ 0x7F, 0x09, 0x09, 0x09, 0x01, 0x00,
+ 0x3E, 0x41, 0x41, 0x51, 0x73, 0x00,
+ 0x7F, 0x08, 0x08, 0x08, 0x7F, 0x00,
+ 0x00, 0x41, 0x7F, 0x41, 0x00, 0x00,
+ 0x20, 0x40, 0x41, 0x3F, 0x01, 0x00,
+ 0x7F, 0x08, 0x14, 0x22, 0x41, 0x00,
+ 0x7F, 0x40, 0x40, 0x40, 0x40, 0x00,
+ 0x7F, 0x02, 0x1C, 0x02, 0x7F, 0x00,
+ 0x7F, 0x04, 0x08, 0x10, 0x7F, 0x00,
+ 0x3E, 0x41, 0x41, 0x41, 0x3E, 0x00,
+ 0x7F, 0x09, 0x09, 0x09, 0x06, 0x00,
+ 0x3E, 0x41, 0x51, 0x21, 0x5E, 0x00,
+ 0x7F, 0x09, 0x19, 0x29, 0x46, 0x00,
+ 0x26, 0x49, 0x49, 0x49, 0x32, 0x00,
+ 0x03, 0x01, 0x7F, 0x01, 0x03, 0x00,
+ 0x3F, 0x40, 0x40, 0x40, 0x3F, 0x00,
+ 0x1F, 0x20, 0x40, 0x20, 0x1F, 0x00,
+ 0x3F, 0x40, 0x38, 0x40, 0x3F, 0x00,
+ 0x63, 0x14, 0x08, 0x14, 0x63, 0x00,
+ 0x03, 0x04, 0x78, 0x04, 0x03, 0x00,
+ 0x61, 0x59, 0x49, 0x4D, 0x43, 0x00,
+ 0x00, 0x7F, 0x41, 0x41, 0x41, 0x00,
+ 0x02, 0x04, 0x08, 0x10, 0x20, 0x00,
+ 0x00, 0x41, 0x41, 0x41, 0x7F, 0x00,
+ 0x04, 0x02, 0x01, 0x02, 0x04, 0x00,
+ 0x40, 0x40, 0x40, 0x40, 0x40, 0x00,
+ 0x00, 0x03, 0x07, 0x08, 0x00, 0x00,
+ 0x20, 0x54, 0x54, 0x78, 0x40, 0x00,
+ 0x7F, 0x28, 0x44, 0x44, 0x38, 0x00,
+ 0x38, 0x44, 0x44, 0x44, 0x28, 0x00,
+ 0x38, 0x44, 0x44, 0x28, 0x7F, 0x00,
+ 0x38, 0x54, 0x54, 0x54, 0x18, 0x00,
+ 0x00, 0x08, 0x7E, 0x09, 0x02, 0x00,
+ 0x18, 0x24, 0x24, 0x1C, 0x78, 0x00,
+ 0x7F, 0x08, 0x04, 0x04, 0x78, 0x00,
+ 0x00, 0x44, 0x7D, 0x40, 0x00, 0x00,
+ 0x20, 0x40, 0x40, 0x3D, 0x00, 0x00,
+ 0x7F, 0x10, 0x28, 0x44, 0x00, 0x00,
+ 0x00, 0x41, 0x7F, 0x40, 0x00, 0x00,
+ 0x7C, 0x04, 0x78, 0x04, 0x78, 0x00,
+ 0x7C, 0x08, 0x04, 0x04, 0x78, 0x00,
+ 0x38, 0x44, 0x44, 0x44, 0x38, 0x00,
+ 0x7C, 0x18, 0x24, 0x24, 0x18, 0x00,
+ 0x18, 0x24, 0x24, 0x18, 0x7C, 0x00,
+ 0x7C, 0x08, 0x04, 0x04, 0x08, 0x00,
+ 0x48, 0x54, 0x54, 0x54, 0x24, 0x00,
+ 0x04, 0x04, 0x3F, 0x44, 0x24, 0x00,
+ 0x3C, 0x40, 0x40, 0x20, 0x7C, 0x00,
+ 0x1C, 0x20, 0x40, 0x20, 0x1C, 0x00,
+ 0x3C, 0x40, 0x30, 0x40, 0x3C, 0x00,
+ 0x44, 0x28, 0x10, 0x28, 0x44, 0x00,
+ 0x4C, 0x90, 0x90, 0x90, 0x7C, 0x00,
+ 0x44, 0x64, 0x54, 0x4C, 0x44, 0x00,
+ 0x00, 0x08, 0x36, 0x41, 0x00, 0x00,
+ 0x00, 0x00, 0x77, 0x00, 0x00, 0x00,
+ 0x00, 0x41, 0x36, 0x08, 0x00, 0x00,
+ 0x02, 0x01, 0x02, 0x04, 0x02, 0x00,
+ 0x3C, 0x26, 0x23, 0x26, 0x3C, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xC0, 0xF0, 0xF8, 0x8C, 0x86, 0xC6,
+ 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ 0xFF, 0xFE, 0xFC, 0xFC, 0xF8, 0xE0,
+ 0x80, 0x00, 0x00, 0x00, 0xF0, 0xF0,
+ 0xF0, 0x00, 0x00, 0xC0, 0xE0, 0xF0,
+ 0x70, 0x70, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xF8, 0xF8, 0xF8, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0xF8, 0xF8,
+ 0xF8, 0x00, 0x00, 0xF8, 0xF8, 0xF8,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xE0, 0xF0, 0xF0, 0xF0, 0xE0, 0xEC,
+ 0xEE, 0xF7, 0xF3, 0x70, 0x20, 0x00,
+ 0x7C, 0x7C, 0x7C, 0x7E, 0x00, 0x7E,
+ 0x7E, 0x7E, 0x7F, 0x7F, 0x7F, 0x00,
+ 0x00, 0x80, 0xC0, 0xE0, 0x7E, 0x5B,
+ 0x4F, 0x5B, 0xFE, 0xC0, 0x00, 0x00,
+ 0xC0, 0x00, 0xDC, 0xD7, 0xDE, 0xDE,
+ 0xDE, 0xD7, 0xDC, 0x00, 0xC0, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x80,
+ 0xE0, 0x70, 0x78, 0x68, 0x6C, 0x64,
+ 0xC7, 0xC7, 0xC7, 0xC7, 0xC7, 0xC7,
+ 0xE7, 0x3F, 0x1F, 0x0F, 0x3F, 0xFF,
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F,
+ 0x0F, 0x00, 0x00, 0x00, 0xFF, 0xFF,
+ 0xFF, 0x1E, 0x3F, 0xFF, 0xF3, 0xE1,
+ 0x80, 0x00, 0x00, 0xF8, 0xFC, 0xFE,
+ 0x36, 0x36, 0x36, 0x3E, 0xBC, 0xB8,
+ 0x00, 0x0E, 0x3E, 0xFE, 0xF0, 0x80,
+ 0xF0, 0xFE, 0x3E, 0x0E, 0x00, 0x00,
+ 0xFF, 0xFF, 0xFF, 0x06, 0x06, 0x06,
+ 0xFE, 0xFE, 0xFC, 0x70, 0x00, 0xE6,
+ 0xF6, 0xF6, 0x36, 0x36, 0xFE, 0xFE,
+ 0xFC, 0x00, 0x00, 0x00, 0xFF, 0xFF,
+ 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x0F, 0x1F, 0x3F, 0x7F, 0x7F, 0x7F,
+ 0x7F, 0x7F, 0x3F, 0x1E, 0x0C, 0x00,
+ 0x1F, 0x1F, 0x1F, 0x3F, 0x00, 0x3F,
+ 0x3F, 0x3F, 0x7F, 0x7F, 0x7F, 0x00,
+ 0x30, 0x7B, 0x7F, 0x78, 0x30, 0x20,
+ 0x20, 0x30, 0x78, 0x7F, 0x3B, 0x00,
+ 0x03, 0x00, 0x0F, 0x7F, 0x0F, 0x0F,
+ 0x0F, 0x7F, 0x0F, 0x00, 0x03, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x3C, 0x7F,
+ 0x63, 0x60, 0xE0, 0xC0, 0xC0, 0xC0,
+ 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFF,
+ 0xFF, 0xC0, 0x60, 0x30, 0x18, 0x0F,
+ 0x03, 0x03, 0x03, 0x01, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x03, 0x03,
+ 0x03, 0x00, 0x00, 0x00, 0x03, 0x03,
+ 0x03, 0x03, 0x00, 0x01, 0x03, 0x03,
+ 0x03, 0x03, 0x03, 0x03, 0x03, 0x01,
+ 0x00, 0x70, 0x70, 0x39, 0x3F, 0x1F,
+ 0x07, 0x01, 0x00, 0x00, 0x00, 0x00,
+ 0x01, 0x03, 0x03, 0x03, 0x03, 0x03,
+ 0x03, 0x03, 0x01, 0x00, 0x00, 0x01,
+ 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+ 0x01, 0x00, 0x00, 0x00, 0x03, 0x03,
+ 0x03, 0x00, 0x00, 0x03, 0x03, 0x03,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+};
+// clang-format on
diff --git a/keyboards/yowkees/lib/keyball/README.md b/keyboards/yowkees/lib/keyball/README.md
new file mode 100644
index 00000000000..0c54427a9b5
--- /dev/null
+++ b/keyboards/yowkees/lib/keyball/README.md
@@ -0,0 +1,128 @@
+# Keyball Core Function Library
+
+## Scroll snap mode
+
+When scrolling with the trackball, the scroll direction is restricted.
+This restriction is called "scroll snap".
+
+The direction of restriction can be changed using special key codes.
+You can also release this restriction.
+It is called as "scroll snap mode"
+The current mode is displayed on the OLED.
+
+There are 3 modes for scroll snap.
+
+1. Vertical (default): key code is `SSNP_VRT`, indicated as `VT`.
+2. Horizontal: key code is `SSNP_HOR`, indicated as `HO`.
+3. Free: key code is `SSNP_FRE`, indicated as `SCR`.
+
+The scroll snap mode at startup is vertical,
+but you can change it by saving the current mode with `KBC_SAVE`
+
+## MEMO
+
+This section contains notes regarding the specifications of this library.
+Since the purpose is to keep a record in whatever form it takes,
+a lot of Japanese is included.
+If you would like to read it in English, please request a translation via issue or discussion.
+Of course you can translate it for us. If you translate it,
+please make pull requests to share it us.
+
+### Scroll Snap Spec
+
+この機能は config.h に `#define KEYBALL_SCROLLSNAP_ENABLE 0` を書き加えることで無効化できる。
+
+トラックボールによるスクロールの方向を制限するのがスクロールスナップ。
+現在のスクロールスナップには3つのモードがある。
+
+* 垂直方向にスナップ (デフォルト)
+* 水平方向にスナップ
+* スナップしない自由スクロール
+
+以上を `SSNP_VRT`, `SSNP_HOR`, `SSNP_FRE` の独自キーコードを用いて手動で切り替える。
+
+#### up to 1.3.2
+
+初期状態でトラックボールによるスクロールを垂直方向に制限(スナップ)している。
+この振る舞いは config.h に `#define KEYBALL_SCROLLSNAP_ENABLE 1` を書き加えることで有効化できる。
+
+この機能はスナップモードとフリーモードから構成される。
+初期状態はスナップモードで、このモードではスクロール方向は垂直に制限される。
+スナップモードで水平の一定方向に一定カウント(デフォルトは12)以上スクロールするとフリーモードに遷移する。
+なおこのカウントはスクロール除数適用後のカウントである。
+フリーモードでは制限が取り払われ、水平と垂直どちらにも自由にスクロールできる。
+フリーモードで一定時間(デフォルトは 100 ミリ秒)、スクロール操作を行わないとスナップモードに遷移する。
+
+フリーモードに遷移するためのカウント数を変更するには `KEYBALL_SCROLLSNAP_TENSION_THRESHOLD` を、
+スナップモードに遷移するためのインターバル(ミリ秒)を変更するには `KEYBALL_SCROLLSNAP_RESET_TIMER` を、それぞれ config.h で設定できる。
+
+以下はカウント数を 5 に、インターバルを 200 ミリ秒に変更する例:
+
+```c
+#define KEYBALL_SCROLLSNAP_TENSION_THRESHOLD 5
+#define KEYBALL_SCROLLSNAP_RESET_TIMER 200
+```
+
+#### History of Scroll Snap
+
+もともとは自由にスクロールできるようにしていた。
+しかし思ったよりもボールの感度が高く一定方向だけに動かすのが難しく、
+誤操作を誘発していた。
+そのためなんらかのスナップ機能が必要だと判断した。
+
+最初のスナップ機能は垂直・水平のどちらかの軸から一定角度以内で収まってるうちはそちらへスナップするとした。
+しかし回転開始初期にはその移動量が極小かつセンサーの感度が高いので、
+垂直に動かしたい時に水平にも極小量の移動が発生しておりスナップ方向が定まらない、
+という問題が発生した。
+人間は自分が思うほどには指を正確に動かせていなかった。
+
+そこで一定方向に一定以上のカウントを検出するまでは一切スクロールしないようにした。
+これは回転開始初期のスクロール量を読み捨てるに等しい。
+
+しかしWebブラウザを思い浮かべてもらえればすぐにわかるように、
+一般のユースケースでは垂直方向のスクロールを頻繁に利用する。
+先の読み捨てにより、垂直方向のスクロールがワンテンポ遅れ、体験を大幅に損なうことが明らかになった。
+この解決のためモード: 初期は垂直のみ、後に自由スクロールする、を導入した。
+
+### Scroll Divider / スクロール除数
+
+Keyballのセンサーは感度がとても高い。
+そのため生の値をスクロール量としてしまうとスクロール操作がとても難しくなった。
+そこで生の値をある数で割ってスクロールに適用する方式を採用した。
+この時の割る数をスクロール除数と言っている。
+
+スクロール除数は、体感として小さく制御する意味がなかったので、
+1, 2, 4, 8, 16, 32, 64 というように2の乗数とした。
+2の乗数であるのならば値の表現として $2 ^ n$ の $n$ で表せる。
+またEEPROMに設定値を保存できるようにするために、
+ビット数を節約する目的で $n$ が取りうる値は 1~7 の範囲とした。
+結果実際の割る数は以下の式で計算できる。
+
+$$ 2 ^ {(n - 1)} $$
+
+$n$ の初期値は 4 で 1/8 になることを意味する。
+この値は config.h で `KEYBALL_SCROLL_DIV_DEFAULT` マクロを定義することで変更できる。
+これを0にすることは考慮していないので設定しないこと。
+
+### Scroll Inhivitor
+
+トラックボールの移動量をポインタに適用するかスクロールに適用するか、
+Keyballは内部にスクロールモードという名のモードで管理している。
+スクロールモードはキーコードやAPI呼び出しの任意のタイミングで切り替えられる。
+デフォルトのキーマップでは特定のレイヤーの状態を
+スクロールモードのオンオフに適用している。
+
+当初はスクロールモードの切り替え直後に、
+トラックボールの移動が意図しない適用先に適用されることが頻発した。
+ポインタをブラウザまで動かした後にスクロールモードに変更すると、
+意図していない方向にスクロールするといった体験になる。
+
+そこでスクロールモード切替直後の一定時間は
+一切のトラックボール操作を読み捨てることにした。
+この読み捨てる時間のことを Scroll Inhivitor と名付けた。
+この Scroll Inhivitor のデフォルト値は 50 ミリ秒である。
+短い時間ではあるが結構効いている。
+
+Scroll Inhivitor は config.h で `KEYBALL_SCROLLBALL_INHIVITOR` マクロを定義することで変更できる。
+無効化したい場合は値として `0` を設定する。
+興味があれば無効にしてみるのも面白いかもしれない。
diff --git a/keyboards/yowkees/lib/keyball/keyball.c b/keyboards/yowkees/lib/keyball/keyball.c
new file mode 100644
index 00000000000..2c89e2e4d41
--- /dev/null
+++ b/keyboards/yowkees/lib/keyball/keyball.c
@@ -0,0 +1,736 @@
+/*
+Copyright 2022 MURAOKA Taro (aka KoRoN, @kaoriya)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+#include "quantum.h"
+#ifdef SPLIT_KEYBOARD
+# include "transactions.h"
+#endif
+
+#include "keyball.h"
+
+#include
+
+const uint8_t CPI_DEFAULT = KEYBALL_CPI_DEFAULT / 100;
+/**
+ * The Keyball library uses a range of 0 <= cpi <= 119 internally, and the true
+ * CPI value is ( cpi + 1 ) * 100.
+ */
+const uint8_t CPI_MAX = KEYBALL_PMW3360_MAXCPI + 1; // 119 + 1 = 12000 CPI actual
+const uint8_t SCROLL_DIV_MAX = 7;
+
+const uint16_t AML_TIMEOUT_MIN = 100;
+const uint16_t AML_TIMEOUT_MAX = 1000;
+const uint16_t AML_TIMEOUT_QU = 50; // Quantization Unit
+
+static const char BL = '\xB0'; // Blank indicator character
+static const char LFSTR_ON[] PROGMEM = "\xB2\xB3";
+static const char LFSTR_OFF[] PROGMEM = "\xB4\xB5";
+
+keyball_t keyball = {
+ .this_have_ball = false,
+ .that_enable = false,
+ .that_have_ball = false,
+
+ .this_motion = {0},
+ .that_motion = {0},
+
+ .cpi_value = 0,
+ .cpi_changed = false,
+
+ .scroll_mode = false,
+ .scroll_div = 0,
+
+ .pressing_keys = { '\xB0', '\xB0', '\xB0', '\xB0', '\xB0', '\xB0', 0 },
+};
+
+//////////////////////////////////////////////////////////////////////////////
+// Hook points
+
+__attribute__((weak)) void keyball_on_adjust_layout(keyball_adjust_t v) {}
+
+//////////////////////////////////////////////////////////////////////////////
+// Static utilities
+
+// add16 adds two int16_t with clipping.
+static int16_t add16(int16_t a, int16_t b) {
+ int16_t r = a + b;
+ if (a >= 0 && b >= 0 && r < 0) {
+ r = 32767;
+ } else if (a < 0 && b < 0 && r >= 0) {
+ r = -32768;
+ }
+ return r;
+}
+
+// divmod16 divides *v by div, returns the quotient, and assigns the remainder
+// to *v.
+static int16_t divmod16(int16_t *v, int16_t div) {
+ int16_t r = *v / div;
+ *v -= r * div;
+ return r;
+}
+
+// clip2int8 clips an integer fit into int8_t.
+static inline int8_t clip2int8(int16_t v) {
+ return (v) < -127 ? -127 : (v) > 127 ? 127 : (int8_t)v;
+}
+
+#ifdef OLED_ENABLE
+static const char *format_4d(int8_t d) {
+ static char buf[5] = {0}; // max width (4) + NUL (1)
+ char lead = ' ';
+ if (d < 0) {
+ d = -d;
+ lead = '-';
+ }
+ buf[3] = (d % 10) + '0';
+ d /= 10;
+ if (d == 0) {
+ buf[2] = lead;
+ lead = ' ';
+ } else {
+ buf[2] = (d % 10) + '0';
+ d /= 10;
+ }
+ if (d == 0) {
+ buf[1] = lead;
+ lead = ' ';
+ } else {
+ buf[1] = (d % 10) + '0';
+ d /= 10;
+ }
+ buf[0] = lead;
+ return buf;
+}
+
+static char to_1x(uint8_t x) {
+ x &= 0x0f;
+ return x < 10 ? x + '0' : x + 'a' - 10;
+}
+#endif
+
+static void add_cpi(int8_t delta) {
+ int16_t v = keyball_get_cpi() + delta;
+ keyball_set_cpi(v < 1 ? 1 : v);
+}
+
+static void add_scroll_div(int8_t delta) {
+ int8_t v = keyball_get_scroll_div() + delta;
+ keyball_set_scroll_div(v < 1 ? 1 : v);
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Pointing device driver
+
+#if KEYBALL_MODEL == 46
+void keyboard_pre_init_kb(void) {
+ keyball.this_have_ball = pmw3360_init();
+ keyboard_pre_init_user();
+}
+#endif
+
+__attribute__((weak)) void keyball_on_apply_motion_to_mouse_move(keyball_motion_t *m, report_mouse_t *r, bool is_left) {
+#if KEYBALL_MODEL == 61 || KEYBALL_MODEL == 39 || KEYBALL_MODEL == 147 || KEYBALL_MODEL == 44
+ r->x = clip2int8(m->y);
+ r->y = clip2int8(m->x);
+ if (is_left) {
+ r->x = -r->x;
+ r->y = -r->y;
+ }
+#elif KEYBALL_MODEL == 46
+ r->x = clip2int8(m->x);
+ r->y = -clip2int8(m->y);
+#else
+# error("unknown Keyball model")
+#endif
+ // clear motion
+ m->x = 0;
+ m->y = 0;
+}
+
+__attribute__((weak)) void keyball_on_apply_motion_to_mouse_scroll(keyball_motion_t *m, report_mouse_t *r, bool is_left) {
+ // consume motion of trackball.
+ int16_t div = 1 << (keyball_get_scroll_div() - 1);
+ int16_t x = divmod16(&m->x, div);
+ int16_t y = divmod16(&m->y, div);
+
+ // apply to mouse report.
+#if KEYBALL_MODEL == 61 || KEYBALL_MODEL == 39 || KEYBALL_MODEL == 147 || KEYBALL_MODEL == 44
+ r->h = clip2int8(y);
+ r->v = -clip2int8(x);
+ if (is_left) {
+ r->h = -r->h;
+ r->v = -r->v;
+ }
+#elif KEYBALL_MODEL == 46
+ r->h = clip2int8(x);
+ r->v = clip2int8(y);
+#else
+# error("unknown Keyball model")
+#endif
+
+ // Scroll snapping
+#if KEYBALL_SCROLLSNAP_ENABLE == 1
+ // Old behavior up to 1.3.2)
+ uint32_t now = timer_read32();
+ if (r->h != 0 || r->v != 0) {
+ keyball.scroll_snap_last = now;
+ } else if (TIMER_DIFF_32(now, keyball.scroll_snap_last) >= KEYBALL_SCROLLSNAP_RESET_TIMER) {
+ keyball.scroll_snap_tension_h = 0;
+ }
+ if (abs(keyball.scroll_snap_tension_h) < KEYBALL_SCROLLSNAP_TENSION_THRESHOLD) {
+ keyball.scroll_snap_tension_h += y;
+ r->h = 0;
+ }
+#elif KEYBALL_SCROLLSNAP_ENABLE == 2
+ // New behavior
+ switch (keyball_get_scrollsnap_mode()) {
+ case KEYBALL_SCROLLSNAP_MODE_VERTICAL:
+ r->h = 0;
+ break;
+ case KEYBALL_SCROLLSNAP_MODE_HORIZONTAL:
+ r->v = 0;
+ break;
+ default:
+ // pass by without doing anything
+ break;
+ }
+#endif
+}
+
+static inline bool should_report(void) {
+ uint32_t now = timer_read32();
+#if defined(KEYBALL_REPORTMOUSE_INTERVAL) && KEYBALL_REPORTMOUSE_INTERVAL > 0
+ // throttling mouse report rate.
+ static uint32_t last = 0;
+ if (TIMER_DIFF_32(now, last) < KEYBALL_REPORTMOUSE_INTERVAL) {
+ return false;
+ }
+ last = now;
+#endif
+#if defined(KEYBALL_SCROLLBALL_INHIVITOR) && KEYBALL_SCROLLBALL_INHIVITOR > 0
+ if (TIMER_DIFF_32(now, keyball.scroll_mode_changed) < KEYBALL_SCROLLBALL_INHIVITOR) {
+ keyball.this_motion.x = 0;
+ keyball.this_motion.y = 0;
+ keyball.that_motion.x = 0;
+ keyball.that_motion.y = 0;
+ }
+#endif
+ return true;
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Split RPC
+
+#ifdef SPLIT_KEYBOARD
+
+static void rpc_get_info_handler(uint8_t in_buflen, const void *in_data, uint8_t out_buflen, void *out_data) {
+ keyball_info_t info = {
+ .ballcnt = keyball.this_have_ball ? 1 : 0,
+ };
+ *(keyball_info_t *)out_data = info;
+ keyball_on_adjust_layout(KEYBALL_ADJUST_SECONDARY);
+}
+
+static void rpc_get_info_invoke(void) {
+ static bool negotiated = false;
+ static uint32_t last_sync = 0;
+ static int round = 0;
+ uint32_t now = timer_read32();
+ if (negotiated || TIMER_DIFF_32(now, last_sync) < KEYBALL_TX_GETINFO_INTERVAL) {
+ return;
+ }
+ last_sync = now;
+ round++;
+ keyball_info_t recv = {0};
+ if (!transaction_rpc_exec(KEYBALL_GET_INFO, 0, NULL, sizeof(recv), &recv)) {
+ if (round < KEYBALL_TX_GETINFO_MAXTRY) {
+ dprintf("keyball:rpc_get_info_invoke: missed #%d\n", round);
+ return;
+ }
+ }
+ negotiated = true;
+ keyball.that_enable = true;
+ keyball.that_have_ball = recv.ballcnt > 0;
+ dprintf("keyball:rpc_get_info_invoke: negotiated #%d %d\n", round, keyball.that_have_ball);
+
+ // split keyboard negotiation completed.
+
+# ifdef VIA_ENABLE
+ // adjust VIA layout options according to current combination.
+ uint8_t layouts = (keyball.this_have_ball ? (is_keyboard_left() ? 0x02 : 0x01) : 0x00) | (keyball.that_have_ball ? (is_keyboard_left() ? 0x01 : 0x02) : 0x00);
+ uint32_t curr = via_get_layout_options();
+ uint32_t next = (curr & ~0x3) | layouts;
+ if (next != curr) {
+ via_set_layout_options(next);
+ }
+# endif
+
+ keyball_on_adjust_layout(KEYBALL_ADJUST_PRIMARY);
+}
+
+static void rpc_get_motion_handler(uint8_t in_buflen, const void *in_data, uint8_t out_buflen, void *out_data) {
+ *(keyball_motion_t *)out_data = keyball.this_motion;
+ // clear motion
+ keyball.this_motion.x = 0;
+ keyball.this_motion.y = 0;
+}
+
+static void rpc_get_motion_invoke(void) {
+ static uint32_t last_sync = 0;
+ uint32_t now = timer_read32();
+ if (TIMER_DIFF_32(now, last_sync) < KEYBALL_TX_GETMOTION_INTERVAL) {
+ return;
+ }
+ keyball_motion_t recv = {0};
+ if (transaction_rpc_exec(KEYBALL_GET_MOTION, 0, NULL, sizeof(recv), &recv)) {
+ keyball.that_motion.x = add16(keyball.that_motion.x, recv.x);
+ keyball.that_motion.y = add16(keyball.that_motion.y, recv.y);
+ }
+ last_sync = now;
+ return;
+}
+
+static void rpc_set_cpi_handler(uint8_t in_buflen, const void *in_data, uint8_t out_buflen, void *out_data) {
+ keyball_set_cpi(*(keyball_cpi_t *)in_data);
+}
+
+static void rpc_set_cpi_invoke(void) {
+ if (!keyball.cpi_changed) {
+ return;
+ }
+ keyball_cpi_t req = keyball.cpi_value;
+ if (!transaction_rpc_send(KEYBALL_SET_CPI, sizeof(req), &req)) {
+ return;
+ }
+ keyball.cpi_changed = false;
+}
+
+#endif
+
+//////////////////////////////////////////////////////////////////////////////
+// OLED utility
+
+#ifdef OLED_ENABLE
+// clang-format off
+const char PROGMEM code_to_name[] = {
+ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
+ 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
+ 'u', 'v', 'w', 'x', 'y', 'z', '1', '2', '3', '4',
+ '5', '6', '7', '8', '9', '0', 'R', 'E', 'B', 'T',
+ '_', '-', '=', '[', ']', '\\', '#', ';', '\'', '`',
+ ',', '.', '/',
+};
+// clang-format on
+#endif
+
+void keyball_oled_render_ballinfo(void) {
+#ifdef OLED_ENABLE
+ // Format: `Ball:{mouse x}{mouse y}{mouse h}{mouse v}`
+ //
+ // Output example:
+ //
+ // Ball: -12 34 0 0
+
+ // 1st line, "Ball" label, mouse x, y, h, and v.
+ oled_write_P(PSTR("Ball\xB1"), false);
+ oled_write(format_4d(keyball.last_mouse.x), false);
+ oled_write(format_4d(keyball.last_mouse.y), false);
+ oled_write(format_4d(keyball.last_mouse.h), false);
+ oled_write(format_4d(keyball.last_mouse.v), false);
+
+ // 2nd line, empty label and CPI
+ oled_write_P(PSTR(" \xB1\xBC\xBD"), false);
+ oled_write(format_4d(keyball_get_cpi()) + 1, false);
+ oled_write_P(PSTR("00 "), false);
+
+ // indicate scroll snap mode: "VT" (vertical), "HN" (horiozntal), and "SCR" (free)
+#if 1 && KEYBALL_SCROLLSNAP_ENABLE == 2
+ switch (keyball_get_scrollsnap_mode()) {
+ case KEYBALL_SCROLLSNAP_MODE_VERTICAL:
+ oled_write_P(PSTR("VT"), false);
+ break;
+ case KEYBALL_SCROLLSNAP_MODE_HORIZONTAL:
+ oled_write_P(PSTR("HO"), false);
+ break;
+ default:
+ oled_write_P(PSTR("\xBE\xBF"), false);
+ break;
+ }
+#else
+ oled_write_P(PSTR("\xBE\xBF"), false);
+#endif
+ // indicate scroll mode: on/off
+ if (keyball.scroll_mode) {
+ oled_write_P(LFSTR_ON, false);
+ } else {
+ oled_write_P(LFSTR_OFF, false);
+ }
+
+ // indicate scroll divider:
+ oled_write_P(PSTR(" \xC0\xC1"), false);
+ oled_write_char('0' + keyball_get_scroll_div(), false);
+#endif
+}
+
+void keyball_oled_render_ballsubinfo(void) {
+#ifdef OLED_ENABLE
+#endif
+}
+
+void keyball_oled_render_keyinfo(void) {
+#ifdef OLED_ENABLE
+ // Format: `Key : R{row} C{col} K{kc} {name}{name}{name}`
+ //
+ // Where `kc` is lower 8 bit of keycode.
+ // Where `name`s are readable labels for pressing keys, valid between 4 and 56.
+ //
+ // `row`, `col`, and `kc` indicates the last processed key,
+ // but `name`s indicate unreleased keys in best effort.
+ //
+ // It is aligned to fit with output of keyball_oled_render_ballinfo().
+ // For example:
+ //
+ // Key : R2 C3 K06 abc
+ // Ball: 0 0 0 0
+
+ // "Key" Label
+ oled_write_P(PSTR("Key \xB1"), false);
+
+ // Row and column
+ oled_write_char('\xB8', false);
+ oled_write_char(to_1x(keyball.last_pos.row), false);
+ oled_write_char('\xB9', false);
+ oled_write_char(to_1x(keyball.last_pos.col), false);
+
+ // Keycode
+ oled_write_P(PSTR("\xBA\xBB"), false);
+ oled_write_char(to_1x(keyball.last_kc >> 4), false);
+ oled_write_char(to_1x(keyball.last_kc), false);
+
+ // Pressing keys
+ oled_write_P(PSTR(" "), false);
+ oled_write(keyball.pressing_keys, false);
+#endif
+}
+
+void keyball_oled_render_layerinfo(void) {
+#ifdef OLED_ENABLE
+ // Format: `Layer:{layer state}`
+ //
+ // Output example:
+ //
+ // Layer:-23------------
+ //
+ oled_write_P(PSTR("L\xB6\xB7r\xB1"), false);
+ for (uint8_t i = 1; i < 8; i++) {
+ oled_write_char((layer_state_is(i) ? to_1x(i) : BL), false);
+ }
+ oled_write_char(' ', false);
+
+# ifdef POINTING_DEVICE_AUTO_MOUSE_ENABLE
+ oled_write_P(PSTR("\xC2\xC3"), false);
+ if (get_auto_mouse_enable()) {
+ oled_write_P(LFSTR_ON, false);
+ } else {
+ oled_write_P(LFSTR_OFF, false);
+ }
+
+ oled_write(format_4d(get_auto_mouse_timeout() / 10) + 1, false);
+ oled_write_char('0', false);
+# else
+ oled_write_P(PSTR("\xC2\xC3\xB4\xB5 ---"), false);
+# endif
+#endif
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Public API functions
+
+bool keyball_get_scroll_mode(void) {
+ return keyball.scroll_mode;
+}
+
+void keyball_set_scroll_mode(bool mode) {
+ if (mode != keyball.scroll_mode) {
+ keyball.scroll_mode_changed = timer_read32();
+ }
+ keyball.scroll_mode = mode;
+}
+
+keyball_scrollsnap_mode_t keyball_get_scrollsnap_mode(void) {
+#if KEYBALL_SCROLLSNAP_ENABLE == 2
+ return keyball.scrollsnap_mode;
+#else
+ return 0;
+#endif
+}
+
+void keyball_set_scrollsnap_mode(keyball_scrollsnap_mode_t mode) {
+#if KEYBALL_SCROLLSNAP_ENABLE == 2
+ keyball.scrollsnap_mode = mode;
+#endif
+}
+
+uint8_t keyball_get_scroll_div(void) {
+ return keyball.scroll_div == 0 ? KEYBALL_SCROLL_DIV_DEFAULT : keyball.scroll_div;
+}
+
+void keyball_set_scroll_div(uint8_t div) {
+ keyball.scroll_div = div > SCROLL_DIV_MAX ? SCROLL_DIV_MAX : div;
+}
+
+uint8_t keyball_get_cpi(void) {
+ return keyball.cpi_value == 0 ? CPI_DEFAULT : keyball.cpi_value;
+}
+
+void keyball_set_cpi(uint8_t cpi) {
+ if (cpi > CPI_MAX) {
+ cpi = CPI_MAX;
+ }
+ keyball.cpi_value = cpi;
+ keyball.cpi_changed = true;
+ if (keyball.this_have_ball) {
+ /**
+ * QMK's core PMW3360 driver uses the true CPI value internally, so we
+ * have to translate the Keyball library's value to QMK's expected
+ * range.
+ *
+ * QMK's core driver also caps the range internally to a valid CPI
+ * value, so we don't need to do it here.
+ */
+ pmw33xx_set_cpi(0, (cpi + 1) * 100);
+ }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Keyboard hooks
+
+void keyboard_post_init_kb(void) {
+#ifdef SPLIT_KEYBOARD
+ // register transaction handlers on secondary.
+ if (!is_keyboard_master()) {
+ transaction_register_rpc(KEYBALL_GET_INFO, rpc_get_info_handler);
+ transaction_register_rpc(KEYBALL_GET_MOTION, rpc_get_motion_handler);
+ transaction_register_rpc(KEYBALL_SET_CPI, rpc_set_cpi_handler);
+ }
+#endif
+
+ // read keyball configuration from EEPROM
+ if (eeconfig_is_enabled()) {
+ keyball_config_t c = {.raw = eeconfig_read_kb()};
+ keyball_set_cpi(c.cpi);
+ keyball_set_scroll_div(c.sdiv);
+#ifdef POINTING_DEVICE_AUTO_MOUSE_ENABLE
+ set_auto_mouse_enable(c.amle);
+ set_auto_mouse_timeout(c.amlto == 0 ? AUTO_MOUSE_TIME : (c.amlto + 1) * AML_TIMEOUT_QU);
+#endif
+#if KEYBALL_SCROLLSNAP_ENABLE == 2
+ keyball_set_scrollsnap_mode(c.ssnap);
+#endif
+ }
+
+ keyball_on_adjust_layout(KEYBALL_ADJUST_PENDING);
+ keyboard_post_init_user();
+}
+
+#if SPLIT_KEYBOARD
+void housekeeping_task_kb(void) {
+ if (is_keyboard_master()) {
+ rpc_get_info_invoke();
+ if (keyball.that_have_ball) {
+ rpc_get_motion_invoke();
+ rpc_set_cpi_invoke();
+ }
+ }
+}
+#endif
+
+static void pressing_keys_update(uint16_t keycode, keyrecord_t *record) {
+ // Process only valid keycodes.
+ if (keycode >= 4 && keycode < 57) {
+ char value = pgm_read_byte(code_to_name + keycode - 4);
+ char where = BL;
+ if (!record->event.pressed) {
+ // Swap `value` and `where` when releasing.
+ where = value;
+ value = BL;
+ }
+ // Rewrite the last `where` of pressing_keys to `value` .
+ for (int i = 0; i < KEYBALL_OLED_MAX_PRESSING_KEYCODES; i++) {
+ if (keyball.pressing_keys[i] == where) {
+ keyball.pressing_keys[i] = value;
+ break;
+ }
+ }
+ }
+}
+
+#ifdef POINTING_DEVICE_AUTO_MOUSE_ENABLE
+bool is_mouse_record_kb(uint16_t keycode, keyrecord_t* record) {
+ switch (keycode) {
+ case SCRL_MO:
+ return true;
+ }
+ return is_mouse_record_user(keycode, record);
+}
+#endif
+
+bool process_record_kb(uint16_t keycode, keyrecord_t *record) {
+ // store last keycode, row, and col for OLED
+ keyball.last_kc = keycode;
+ keyball.last_pos = record->event.key;
+
+ pressing_keys_update(keycode, record);
+
+ if (!process_record_user(keycode, record)) {
+ return false;
+ }
+
+ // strip QK_MODS part.
+ if (keycode >= QK_MODS && keycode <= QK_MODS_MAX) {
+ keycode &= 0xff;
+ }
+
+ switch (keycode) {
+#ifndef MOUSEKEY_ENABLE
+ // process KC_MS_BTN1~8 by myself
+ // See process_action() in quantum/action.c for details.
+ case KC_MS_BTN1 ... KC_MS_BTN8: {
+ extern void register_mouse(uint8_t mouse_keycode, bool pressed);
+ register_mouse(keycode, record->event.pressed);
+ // to apply QK_MODS actions, allow to process others.
+ return true;
+ }
+#endif
+
+ case SCRL_MO:
+ keyball_set_scroll_mode(record->event.pressed);
+ // process_auto_mouse may use this in future, if changed order of
+ // processes.
+ return true;
+ }
+
+ // process events which works on pressed only.
+ if (record->event.pressed) {
+ switch (keycode) {
+ case KBC_RST:
+ keyball_set_cpi(0);
+ keyball_set_scroll_div(0);
+#ifdef POINTING_DEVICE_AUTO_MOUSE_ENABLE
+ set_auto_mouse_enable(false);
+ set_auto_mouse_timeout(AUTO_MOUSE_TIME);
+#endif
+ break;
+ case KBC_SAVE: {
+ keyball_config_t c = {
+ .cpi = keyball.cpi_value,
+ .sdiv = keyball.scroll_div,
+#ifdef POINTING_DEVICE_AUTO_MOUSE_ENABLE
+ .amle = get_auto_mouse_enable(),
+ .amlto = (get_auto_mouse_timeout() / AML_TIMEOUT_QU) - 1,
+#endif
+#if KEYBALL_SCROLLSNAP_ENABLE == 2
+ .ssnap = keyball_get_scrollsnap_mode(),
+#endif
+ };
+ eeconfig_update_kb(c.raw);
+ } break;
+
+ case CPI_I100:
+ add_cpi(1);
+ break;
+ case CPI_D100:
+ add_cpi(-1);
+ break;
+ case CPI_I1K:
+ add_cpi(10);
+ break;
+ case CPI_D1K:
+ add_cpi(-10);
+ break;
+
+ case SCRL_TO:
+ keyball_set_scroll_mode(!keyball.scroll_mode);
+ break;
+ case SCRL_DVI:
+ add_scroll_div(1);
+ break;
+ case SCRL_DVD:
+ add_scroll_div(-1);
+ break;
+
+#if KEYBALL_SCROLLSNAP_ENABLE == 2
+ case SSNP_HOR:
+ keyball_set_scrollsnap_mode(KEYBALL_SCROLLSNAP_MODE_HORIZONTAL);
+ break;
+ case SSNP_VRT:
+ keyball_set_scrollsnap_mode(KEYBALL_SCROLLSNAP_MODE_VERTICAL);
+ break;
+ case SSNP_FRE:
+ keyball_set_scrollsnap_mode(KEYBALL_SCROLLSNAP_MODE_FREE);
+ break;
+#endif
+
+#ifdef POINTING_DEVICE_AUTO_MOUSE_ENABLE
+ case AML_TO:
+ set_auto_mouse_enable(!get_auto_mouse_enable());
+ break;
+ case AML_I50:
+ {
+ uint16_t v = get_auto_mouse_timeout() + 50;
+ set_auto_mouse_timeout(MIN(v, AML_TIMEOUT_MAX));
+ }
+ break;
+ case AML_D50:
+ {
+ uint16_t v = get_auto_mouse_timeout() - 50;
+ set_auto_mouse_timeout(MAX(v, AML_TIMEOUT_MIN));
+ }
+ break;
+#endif
+
+ default:
+ return true;
+ }
+ return false;
+ }
+
+ return true;
+}
+
+// Disable functions keycode_config() and mod_config() in keycode_config.c to
+// reduce size. These functions are provided for customizing magic keycode.
+// These two functions are mostly unnecessary if `MAGIC_KEYCODE_ENABLE = no` is
+// set.
+//
+// If `MAGIC_KEYCODE_ENABLE = no` and you want to keep these two functions as
+// they are, define the macro KEYBALL_KEEP_MAGIC_FUNCTIONS.
+//
+// See: https://docs.qmk.fm/#/squeezing_avr?id=magic-functions
+//
+#if !defined(MAGIC_KEYCODE_ENABLE) && !defined(KEYBALL_KEEP_MAGIC_FUNCTIONS)
+
+uint16_t keycode_config(uint16_t keycode) {
+ return keycode;
+}
+
+uint8_t mod_config(uint8_t mod) {
+ return mod;
+}
+
+#endif
diff --git a/keyboards/yowkees/lib/keyball/keyball.h b/keyboards/yowkees/lib/keyball/keyball.h
new file mode 100644
index 00000000000..cb54241f46c
--- /dev/null
+++ b/keyboards/yowkees/lib/keyball/keyball.h
@@ -0,0 +1,276 @@
+/*
+Copyright 2022 MURAOKA Taro (aka KoRoN, @kaoriya)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+#pragma once
+
+//////////////////////////////////////////////////////////////////////////////
+// Configurations
+
+#ifndef KEYBALL_CPI_DEFAULT
+# define KEYBALL_CPI_DEFAULT 500
+#endif
+
+#ifndef KEYBALL_SCROLL_DIV_DEFAULT
+# define KEYBALL_SCROLL_DIV_DEFAULT 4 // 4: 1/8 (1/2^(n-1))
+#endif
+
+#ifndef KEYBALL_REPORTMOUSE_INTERVAL
+# define KEYBALL_REPORTMOUSE_INTERVAL 8 // mouse report rate: 125Hz
+#endif
+
+#ifndef KEYBALL_SCROLLBALL_INHIVITOR
+# define KEYBALL_SCROLLBALL_INHIVITOR 50
+#endif
+
+/// To disable scroll snap feature, define 0 in your config.h
+#ifndef KEYBALL_SCROLLSNAP_ENABLE
+# define KEYBALL_SCROLLSNAP_ENABLE 2
+#endif
+
+#ifndef KEYBALL_SCROLLSNAP_RESET_TIMER
+# define KEYBALL_SCROLLSNAP_RESET_TIMER 100
+#endif
+
+#ifndef KEYBALL_SCROLLSNAP_TENSION_THRESHOLD
+# define KEYBALL_SCROLLSNAP_TENSION_THRESHOLD 12
+#endif
+
+#ifndef KEYBALL_PMW3360_MAXCPI
+# define KEYBALL_PMW3360_MAXCPI 119 // 12000 CPI actual
+#endif
+
+/// Specify SROM ID to be uploaded PMW3360DW (optical sensor). It will be
+/// enabled high CPI setting or so. Valid valus are 0x04 or 0x81. Define this
+/// in your config.h to be enable. Please note that using this option will
+/// increase the firmware size by more than 4KB.
+//#define KEYBALL_PMW3360_UPLOAD_SROM_ID 0x04
+//#define KEYBALL_PMW3360_UPLOAD_SROM_ID 0x81
+
+/// Defining this macro keeps two functions intact: keycode_config() and
+/// mod_config() in keycode_config.c.
+///
+/// These functions customize the magic key code and are useless if the magic
+/// key code is disabled. Therefore, Keyball automatically disables it.
+/// However, there may be cases where you still need these functions even after
+/// disabling the magic key code. In that case, define this macro.
+//#define KEYBALL_KEEP_MAGIC_FUNCTIONS
+
+//////////////////////////////////////////////////////////////////////////////
+// Constants
+
+#define KEYBALL_TX_GETINFO_INTERVAL 500
+#define KEYBALL_TX_GETINFO_MAXTRY 10
+#define KEYBALL_TX_GETMOTION_INTERVAL 4
+
+#if (PRODUCT_ID & 0xff00) == 0x0000
+# define KEYBALL_MODEL 46
+#elif (PRODUCT_ID & 0xff00) == 0x0100
+# define KEYBALL_MODEL 61
+#elif (PRODUCT_ID & 0xff00) == 0x0200
+# define KEYBALL_MODEL 39
+#elif (PRODUCT_ID & 0xff00) == 0x0300
+# define KEYBALL_MODEL 147
+#elif (PRODUCT_ID & 0xff00) == 0x0400
+# define KEYBALL_MODEL 44
+#endif
+
+#define KEYBALL_OLED_MAX_PRESSING_KEYCODES 6
+
+//////////////////////////////////////////////////////////////////////////////
+// Types
+
+enum keyball_keycodes {
+ KBC_RST = QK_KB_0, // Keyball configuration: reset to default
+ KBC_SAVE = QK_KB_1, // Keyball configuration: save to EEPROM
+
+ CPI_I100 = QK_KB_2, // CPI +100 CPI
+ CPI_D100 = QK_KB_3, // CPI -100 CPI
+ CPI_I1K = QK_KB_4, // CPI +1000 CPI
+ CPI_D1K = QK_KB_5, // CPI -1000 CPI
+
+ // In scroll mode, motion from primary trackball is treated as scroll
+ // wheel.
+ SCRL_TO = QK_KB_6, // Toggle scroll mode
+ SCRL_MO = QK_KB_7, // Momentary scroll mode
+ SCRL_DVI = QK_KB_8, // Increment scroll divider
+ SCRL_DVD = QK_KB_9, // Decrement scroll divider
+
+ SSNP_VRT = QK_KB_13, // Set scroll snap mode as vertical
+ SSNP_HOR = QK_KB_14, // Set scroll snap mode as horizontal
+ SSNP_FRE = QK_KB_15, // Set scroll snap mode as disable (free scroll)
+
+ // Auto mouse layer control keycodes.
+ // Only works when POINTING_DEVICE_AUTO_MOUSE_ENABLE is defined.
+ AML_TO = QK_KB_10, // Toggle automatic mouse layer
+ AML_I50 = QK_KB_11, // Increment automatic mouse layer timeout
+ AML_D50 = QK_KB_12, // Decrement automatic mouse layer timeout
+
+ // User customizable 32 keycodes.
+ KEYBALL_SAFE_RANGE = QK_USER_0,
+};
+
+typedef union {
+ uint32_t raw;
+ struct {
+ uint8_t cpi : 7;
+ uint8_t sdiv : 3; // scroll divider
+#ifdef POINTING_DEVICE_AUTO_MOUSE_ENABLE
+ uint8_t amle : 1; // automatic mouse layer enabled
+ uint16_t amlto : 5; // automatic mouse layer timeout
+#endif
+#if KEYBALL_SCROLLSNAP_ENABLE == 2
+ uint8_t ssnap : 2; // scroll snap mode
+#endif
+ };
+} keyball_config_t;
+
+typedef struct {
+ uint8_t ballcnt; // count of balls: support only 0 or 1, for now
+} keyball_info_t;
+
+typedef struct {
+ int16_t x;
+ int16_t y;
+} keyball_motion_t;
+
+typedef uint8_t keyball_cpi_t;
+
+typedef enum {
+ KEYBALL_SCROLLSNAP_MODE_VERTICAL = 0,
+ KEYBALL_SCROLLSNAP_MODE_HORIZONTAL = 1,
+ KEYBALL_SCROLLSNAP_MODE_FREE = 2,
+} keyball_scrollsnap_mode_t;
+
+typedef struct {
+ bool this_have_ball;
+ bool that_enable;
+ bool that_have_ball;
+
+ keyball_motion_t this_motion;
+ keyball_motion_t that_motion;
+
+ uint8_t cpi_value;
+ bool cpi_changed;
+
+ bool scroll_mode;
+ uint32_t scroll_mode_changed;
+ uint8_t scroll_div;
+
+#if KEYBALL_SCROLLSNAP_ENABLE == 1
+ uint32_t scroll_snap_last;
+ int8_t scroll_snap_tension_h;
+#elif KEYBALL_SCROLLSNAP_ENABLE == 2
+ keyball_scrollsnap_mode_t scrollsnap_mode;
+#endif
+
+ uint16_t last_kc;
+ keypos_t last_pos;
+ report_mouse_t last_mouse;
+
+ // Buffer to indicate pressing keys.
+ char pressing_keys[KEYBALL_OLED_MAX_PRESSING_KEYCODES + 1];
+} keyball_t;
+
+typedef enum {
+ KEYBALL_ADJUST_PENDING = 0,
+ KEYBALL_ADJUST_PRIMARY = 1,
+ KEYBALL_ADJUST_SECONDARY = 2,
+} keyball_adjust_t;
+
+//////////////////////////////////////////////////////////////////////////////
+// Exported values (touch carefully)
+
+extern keyball_t keyball;
+
+//////////////////////////////////////////////////////////////////////////////
+// Hook points
+
+/// keyball_on_adjust_layout is called when the keyboard layout adjustted
+void keyball_on_adjust_layout(keyball_adjust_t v);
+
+/// keyball_on_apply_motion_to_mouse_move applies trackball's motion m to r as
+/// mouse movement.
+/// You can change the default algorithm by override this function.
+void keyball_on_apply_motion_to_mouse_move(keyball_motion_t *m, report_mouse_t *r, bool is_left);
+
+/// keyball_on_apply_motion_to_mouse_scroll applies trackball's motion m to r
+/// as mouse scroll.
+/// You can change the default algorithm by override this function.
+void keyball_on_apply_motion_to_mouse_scroll(keyball_motion_t *m, report_mouse_t *r, bool is_left);
+
+//////////////////////////////////////////////////////////////////////////////
+// Public API functions
+
+/// keyball_oled_render_ballinfo renders ball information to OLED.
+/// It uses just 21 columns to show the info.
+void keyball_oled_render_ballinfo(void);
+
+/// keyball_oled_render_keyinfo renders last processed key information to OLED.
+/// It shows column, row, key code, and key name (if available).
+void keyball_oled_render_keyinfo(void);
+
+/// keyball_oled_render_layerinfo renders current layer status information to
+/// OLED. It shows layer mask with number (1~f) for active layers and '_' for
+/// inactive layers.
+void keyball_oled_render_layerinfo(void);
+
+/// keyball_get_scroll_mode gets current scroll mode.
+bool keyball_get_scroll_mode(void);
+
+/// keyball_set_scroll_mode modify scroll mode.
+void keyball_set_scroll_mode(bool mode);
+
+/// keyball_get_scrollsnap_mode gets current scroll snap mode.
+keyball_scrollsnap_mode_t keyball_get_scrollsnap_mode(void);
+
+/// keyball_set_scrollsnap_mode change scroll snap mode.
+void keyball_set_scrollsnap_mode(keyball_scrollsnap_mode_t mode);
+
+/// keyball_get_scroll_div gets current scroll divider.
+/// See also keyball_set_scroll_div for the scroll divider's detail.
+uint8_t keyball_get_scroll_div(void);
+
+/// keyball_set_scroll_div changes scroll divider.
+///
+/// The scroll divider is the number that divides the raw value when applying
+/// trackball motion to scrolling. The CPI value of the trackball is very
+/// high, so if you apply it to scrolling as is, it will scroll too much.
+/// In order to adjust the scroll amount to be appropriate, it is applied after
+/// dividing it by a scroll divider. The actual denominator is determined by
+/// the following formula:
+///
+/// denominator = 2 ^ (div - 1) ^2
+///
+/// Valid values are between 1 and 7, KEYBALL_SCROLL_DIV_DEFAULT is used when 0
+/// is specified.
+void keyball_set_scroll_div(uint8_t div);
+
+/// keyball_get_cpi gets current CPI of trackball.
+/// The actual CPI value is the returned value +1 and multiplied by 100:
+///
+/// CPI = (v + 1) * 100
+uint8_t keyball_get_cpi(void);
+
+/// keyball_set_cpi changes CPI of trackball.
+/// Valid values are between 0 to 119, and the actual CPI value is the set
+/// value +1 and multiplied by 100:
+///
+/// CPI = (v + 1) * 100
+///
+/// In addition, if you do not upload SROM, the maximum value will be limited
+/// to 34 (3500CPI).
+void keyball_set_cpi(uint8_t cpi);
diff --git a/keyboards/yowkees/lib/keyball/keycodes.md b/keyboards/yowkees/lib/keyball/keycodes.md
new file mode 100644
index 00000000000..ed1d15c33d1
--- /dev/null
+++ b/keyboards/yowkees/lib/keyball/keycodes.md
@@ -0,0 +1,52 @@
+# Keyball: Special Keycodes
+
+* [English/英語](#english)
+* [日本語/Japanese](#japanese)
+
+
+## Special Keycodes
+
+| Keycode | Value on Remap | Hex | Description |
+|:-----------|:----------------|:---------|:------------------------------------------------------------------|
+| `KBC_RST` | `Kb 0` | `0x7e00` | Reset Keyball configuration[^1] |
+| `KBC_SAVE` | `Kb 1` | `0x7e01` | Persist Keyball configuration[^1] to EEPROM |
+| `CPI_I100` | `Kb 2` | `0x7e02` | Increase 100 CPI (max 12000) |
+| `CPI_D100` | `Kb 3` | `0x7e03` | Decrease 100 CPI (min 100) |
+| `CPI_I1K` | `Kb 4` | `0x7e04` | Increase 1000 CPI (max 12000) |
+| `CPI_D1K` | `Kb 5` | `0x7e05` | Decrease 1000 CPI (min 100) |
+| `SCRL_TO` | `Kb 6` | `0x7e06` | Toggle scroll mode |
+| `SCRL_MO` | `Kb 7` | `0x7e07` | Enable scroll mode when pressing |
+| `SCRL_DVI` | `Kb 8` | `0x7e08` | Increase scroll divider (max D7 = 1/128) <- Most Scroll slow |
+| `SCRL_DVD` | `Kb 9` | `0x7e09` | Decrease scroll divider (min 0 = 1/1) <- Most Scroll fast |
+| `AML_TO` | `Kb 10` | `0x7e0a` | Toggle automatic mouse layer |
+| `AML_I50` | `Kb 11` | `0x7e0b` | Increase 50ms automatic mouse layer timeout (max 1000ms) |
+| `AML_D50` | `Kb 12` | `0x7e0c` | Decrease 50ms automatic mouse layer timeout (min 100ms) |
+| `SSNP_VRT` | `Kb 13` | `0x7e0d` | Set scroll snap mode as vertical |
+| `SSNP_HOR` | `Kb 14` | `0x7e0e` | Set scroll snap mode as horizontal |
+| `SSNP_FRE` | `Kb 15` | `0x7e0f` | Set scroll snap mode as disable (free scroll) |
+
+[^1]: CPI, scroll divider, automatic mouse layer's enable/disable, and automatic mouse layer's timeout.
+
+
+## 特殊キーコード
+
+| キーコード | Remap上での表記 | 値 | 説明 |
+|:-----------|:----------------|:---------|:------------------------------------------------------------------|
+| `KBC_RST` | `Kb 0` | `0x7e00` | Keyball設定[^2]のリセット |
+| `KBC_SAVE` | `Kb 1` | `0x7e01` | 現在のKeyball設定[^2]をEEPROMに保存します |
+| `CPI_I100` | `Kb 2` | `0x7e02` | CPIを100増加させます(最大:12000) |
+| `CPI_D100` | `Kb 3` | `0x7e03` | CPIを100減少させます(最小:100) |
+| `CPI_I1K` | `Kb 4` | `0x7e04` | CPIを1000増加させます(最大:12000) |
+| `CPI_D1K` | `Kb 5` | `0x7e05` | CPIを1000減少させます(最小:100) |
+| `SCRL_TO` | `Kb 6` | `0x7e06` | タップごとにスクロールモードのON/OFFを切り替えます |
+| `SCRL_MO` | `Kb 7` | `0x7e07` | キーを押している間、スクロールモードになります |
+| `SCRL_DVI` | `Kb 8` | `0x7e08` | スクロール除数を1つ上げます(max D7 = 1/128)←最もスクロール遅い |
+| `SCRL_DVD` | `Kb 9` | `0x7e09` | スクロール除数を1つ下げます(min D0 = 1/1)←最もスクロール速い |
+| `AML_TO` | `Kb 10` | `0x7e0a` | 自動マウスレイヤーをトグルします。 |
+| `AML_I50` | `Kb 11` | `0x7e0b` | 自動マウスレイヤーのタイムアウトを50msec増やします (max 1000ms) |
+| `AML_D50` | `Kb 12` | `0x7e0c` | 自動マウスレイヤーのタイムアウトを50msec減らします (min 100ms) |
+| `SSNP_VRT` | `Kb 13` | `0x7e0d` | スクロールスナップモードを垂直にする |
+| `SSNP_HOR` | `Kb 14` | `0x7e0e` | スクロールスナップモードを水平にする |
+| `SSNP_FRE` | `Kb 15` | `0x7e0f` | スクロールスナップモードを無効にする(自由スクロール) |
+
+[^2]: CPI、スクロール除数、自動マウスレイヤーのON/OFF状態、及び自動マウスレイヤのタイムアウト
diff --git a/keyboards/yowkees/lib/logofont/font.md b/keyboards/yowkees/lib/logofont/font.md
new file mode 100644
index 00000000000..5c963c75ca6
--- /dev/null
+++ b/keyboards/yowkees/lib/logofont/font.md
@@ -0,0 +1,212 @@
+# Special character patterns
+
+## blank
+
+```
+00000000
+00000000
+00000000
+00000000
+00000000
+00000000
+```
+
+## blank indicator
+
+```
+00000000
+00010000
+00010000
+00010000
+00000000
+00000000
+00000000
+```
+
+## vertical label separator
+
+```
+00000000
+00000000
+01010101
+00000000
+00000000
+00000000
+```
+
+## "ON" in 2 chars with light background
+
+```
+00111110
+01100011
+01011101
+01011101
+01100011
+01111111
+01000001
+01111011
+01110111
+01000001
+00111110
+00000000
+```
+
+## "OFF" in 2 chars
+
+```
+00000000
+00011100
+00100010
+00100010
+00011100
+00000000
+00111110
+00001010
+00000000
+00111110
+00001010
+00000000
+```
+
+## "aye" in 2 chars for "Layer"
+
+```
+00110010
+00101010
+00111100
+00000000
+00100110
+00101000
+00011110
+00000000
+00011100
+00101010
+00101100
+00000000
+```
+
+## right half 'R'
+
+```
+00000000
+00000000
+00111111
+00001001
+00110110
+00000000
+```
+
+## right half 'C'
+
+```
+00000000
+00000000
+00011110
+00100001
+00100001
+00000000
+```
+
+## "KC" in 2 chars, indicate "keycode"
+
+```
+00000000
+00000000
+00000000
+00000000
+00111111
+00001000
+00110111
+00000000
+00011110
+00100001
+00100001
+00000000
+```
+
+## "CPI" in 2 chars
+
+```
+00011110
+00100001
+00100001
+00000000
+00111111
+00001001
+00000110
+00000000
+00100001
+00111111
+00100001
+00000000
+```
+
+## "SCR" in 2 chars
+
+```
+00100010
+00100101
+00011001
+00000000
+00011110
+00100001
+00100001
+00000000
+00111111
+00001001
+00110110
+00000000
+```
+
+## "DIV" in 2 chars
+
+```
+00111111
+00100001
+00011110
+00000000
+00100001
+00111111
+00100001
+00000000
+00001111
+00110000
+00001111
+00000000
+```
+
+## "AML" in 2 chars
+
+```
+00111110
+00001001
+00111110
+00000000
+00111111
+00000110
+00111111
+00000000
+00111111
+00100000
+00100000
+00000000
+```
+
+## inactive "ON" in 2 chars
+
+(not use)
+
+```
+00000000
+00011100
+00100010
+00100010
+00011100
+00000000
+00111110
+00000100
+00001000
+00111110
+00000000
+00000000
+```
diff --git a/keyboards/yowkees/lib/logofont/logofont.c b/keyboards/yowkees/lib/logofont/logofont.c
new file mode 100644
index 00000000000..29ce4888891
--- /dev/null
+++ b/keyboards/yowkees/lib/logofont/logofont.c
@@ -0,0 +1,199 @@
+// Copyright 2021 @Yowkees: Keyball logo
+//
+// - Keyball logo (0x80 ~ 0xAF)
+//
+// Copyright 2024 MURAOKA Taro (aka KoRoN, @kaoriya): Other glyphs
+//
+// - ASCII characters (0x20 ~ 0x7E)
+// - Special characters (0xB0 ~ 0xC3)
+
+#include "progmem.h"
+
+// clang-format off
+const unsigned char font[] PROGMEM = {
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Standard ASCII characters, most of which fit within 5x6 area.
+
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x20 ' '
+ 0x00, 0x00, 0x2F, 0x00, 0x00, 0x00, // 0x21 '!'
+ 0x00, 0x07, 0x00, 0x07, 0x00, 0x00, // 0x22 '"'
+ 0x12, 0x3F, 0x12, 0x3F, 0x12, 0x00, // 0x23 '#'
+ 0x24, 0x2A, 0x7F, 0x2A, 0x12, 0x00, // 0x24 '$'
+ 0x36, 0x09, 0x1E, 0x24, 0x1B, 0x00, // 0x25 '%'
+ 0x18, 0x26, 0x29, 0x12, 0x28, 0x00, // 0x26 '&'
+ 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, // 0x27 '''
+ 0x00, 0x1E, 0x21, 0x40, 0x00, 0x00, // 0x28 '('
+ 0x00, 0x40, 0x21, 0x1E, 0x00, 0x00, // 0x29 ')'
+ 0x14, 0x08, 0x3E, 0x08, 0x14, 0x00, // 0x2A '*'
+ 0x08, 0x08, 0x3E, 0x08, 0x08, 0x00, // 0x2B '+'
+ 0x00, 0x00, 0x30, 0x70, 0x00, 0x00, // 0x2C ','
+ 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, // 0x2D '-'
+ 0x00, 0x00, 0x30, 0x30, 0x00, 0x00, // 0x2E '.'
+ 0x20, 0x10, 0x0C, 0x02, 0x01, 0x00, // 0x2F '/'
+ 0x1E, 0x29, 0x25, 0x23, 0x1E, 0x00, // 0x30 '0'
+ 0x00, 0x22, 0x3F, 0x20, 0x00, 0x00, // 0x31 '1'
+ 0x22, 0x31, 0x29, 0x29, 0x26, 0x00, // 0x32 '2'
+ 0x12, 0x21, 0x25, 0x25, 0x1A, 0x00, // 0x33 '3'
+ 0x18, 0x14, 0x12, 0x3F, 0x10, 0x00, // 0x34 '4'
+ 0x17, 0x25, 0x25, 0x25, 0x18, 0x00, // 0x35 '5'
+ 0x1E, 0x25, 0x25, 0x25, 0x18, 0x00, // 0x36 '6'
+ 0x01, 0x31, 0x09, 0x05, 0x03, 0x00, // 0x37 '7'
+ 0x1A, 0x25, 0x25, 0x25, 0x1A, 0x00, // 0x38 '8'
+ 0x06, 0x29, 0x29, 0x29, 0x1E, 0x00, // 0x39 '9'
+ 0x00, 0x00, 0x33, 0x33, 0x00, 0x00, // 0x3A ':'
+ 0x00, 0x00, 0x33, 0x73, 0x00, 0x00, // 0x3B ';'
+ 0x08, 0x14, 0x14, 0x22, 0x22, 0x00, // 0x3C '<'
+ 0x14, 0x14, 0x14, 0x14, 0x14, 0x00, // 0x3D '='
+ 0x22, 0x22, 0x14, 0x14, 0x08, 0x00, // 0x3E '>'
+ 0x02, 0x01, 0x29, 0x05, 0x02, 0x00, // 0x3F '?'
+ 0x3E, 0x59, 0x55, 0x5D, 0x3E, 0x00, // 0x40 '@'
+ 0x38, 0x16, 0x11, 0x16, 0x38, 0x00, // 0x41 'A'
+ 0x3F, 0x25, 0x25, 0x25, 0x1A, 0x00, // 0x42 'B'
+ 0x1E, 0x21, 0x21, 0x21, 0x12, 0x00, // 0x43 'C'
+ 0x3F, 0x21, 0x21, 0x21, 0x1E, 0x00, // 0x44 'D'
+ 0x3F, 0x25, 0x25, 0x25, 0x21, 0x00, // 0x45 'E'
+ 0x3F, 0x09, 0x09, 0x09, 0x01, 0x00, // 0x46 'F'
+ 0x1E, 0x21, 0x21, 0x29, 0x1A, 0x00, // 0x47 'G'
+ 0x3F, 0x04, 0x04, 0x04, 0x3F, 0x00, // 0x48 'H'
+ 0x00, 0x21, 0x3F, 0x21, 0x00, 0x00, // 0x49 'I'
+ 0x10, 0x20, 0x20, 0x20, 0x1F, 0x00, // 0x4A 'J'
+ 0x3F, 0x04, 0x0C, 0x12, 0x21, 0x00, // 0x4B 'K'
+ 0x3F, 0x20, 0x20, 0x20, 0x20, 0x00, // 0x4C 'L'
+ 0x3F, 0x02, 0x04, 0x02, 0x3F, 0x00, // 0x4D 'M'
+ 0x3F, 0x02, 0x0C, 0x10, 0x3F, 0x00, // 0x4E 'N'
+ 0x1E, 0x21, 0x21, 0x21, 0x1E, 0x00, // 0x4F 'O'
+ 0x3F, 0x09, 0x09, 0x09, 0x06, 0x00, // 0x50 'P'
+ 0x1E, 0x21, 0x29, 0x11, 0x2E, 0x00, // 0x51 'Q'
+ 0x3F, 0x09, 0x09, 0x19, 0x26, 0x00, // 0x52 'R'
+ 0x12, 0x25, 0x25, 0x29, 0x12, 0x00, // 0x53 'S'
+ 0x01, 0x01, 0x3F, 0x01, 0x01, 0x00, // 0x54 'T'
+ 0x1F, 0x20, 0x20, 0x20, 0x1F, 0x00, // 0x55 'U'
+ 0x07, 0x18, 0x20, 0x18, 0x07, 0x00, // 0x56 'V'
+ 0x0F, 0x30, 0x0C, 0x30, 0x0F, 0x00, // 0x57 'W'
+ 0x21, 0x12, 0x0C, 0x12, 0x21, 0x00, // 0x58 'X'
+ 0x03, 0x04, 0x38, 0x04, 0x03, 0x00, // 0x59 'Y'
+ 0x31, 0x29, 0x25, 0x23, 0x21, 0x00, // 0x5A 'Z'
+ 0x00, 0x7F, 0x41, 0x41, 0x00, 0x00, // 0x5B '['
+ 0x01, 0x02, 0x0C, 0x10, 0x20, 0x00, // 0x5C '\'
+ 0x00, 0x41, 0x41, 0x7F, 0x00, 0x00, // 0x5D ']'
+ 0x04, 0x02, 0x01, 0x02, 0x04, 0x00, // 0x5E '^'
+ 0x40, 0x40, 0x40, 0x40, 0x40, 0x00, // 0x5F '_'
+ 0x00, 0x01, 0x03, 0x04, 0x00, 0x00, // 0x60 '`'
+ 0x10, 0x2A, 0x2A, 0x2A, 0x3C, 0x00, // 0x61 'a'
+ 0x3F, 0x24, 0x22, 0x22, 0x1C, 0x00, // 0x62 'b'
+ 0x1C, 0x22, 0x22, 0x22, 0x24, 0x00, // 0x63 'c'
+ 0x1C, 0x22, 0x22, 0x24, 0x3F, 0x00, // 0x64 'd'
+ 0x1C, 0x2A, 0x2A, 0x2A, 0x0C, 0x00, // 0x65 'e'
+ 0x00, 0x04, 0x3E, 0x05, 0x05, 0x00, // 0x66 'f'
+ 0x0C, 0x52, 0x52, 0x54, 0x3E, 0x00, // 0x67 'g'
+ 0x3F, 0x08, 0x04, 0x04, 0x38, 0x00, // 0x68 'h'
+ 0x00, 0x00, 0x3D, 0x00, 0x00, 0x00, // 0x69 'i'
+ 0x20, 0x40, 0x40, 0x3D, 0x00, 0x00, // 0x6A 'j'
+ 0x3F, 0x08, 0x08, 0x14, 0x22, 0x00, // 0x6B 'k'
+ 0x00, 0x01, 0x3F, 0x20, 0x00, 0x00, // 0x6C 'l'
+ 0x3E, 0x02, 0x3C, 0x02, 0x3C, 0x00, // 0x6D 'm'
+ 0x3E, 0x04, 0x02, 0x02, 0x3C, 0x00, // 0x6E 'n'
+ 0x1C, 0x22, 0x22, 0x22, 0x1C, 0x00, // 0x6F 'o'
+ 0x7E, 0x14, 0x22, 0x22, 0x1C, 0x00, // 0x70 'p'
+ 0x1C, 0x22, 0x22, 0x14, 0x7E, 0x00, // 0x71 'q'
+ 0x3E, 0x04, 0x02, 0x02, 0x04, 0x00, // 0x72 'r'
+ 0x24, 0x2A, 0x2A, 0x2A, 0x12, 0x00, // 0x73 's'
+ 0x00, 0x02, 0x1F, 0x22, 0x00, 0x00, // 0x74 't'
+ 0x1E, 0x20, 0x20, 0x10, 0x3E, 0x00, // 0x75 'u'
+ 0x06, 0x18, 0x20, 0x18, 0x06, 0x00, // 0x76 'v'
+ 0x0E, 0x30, 0x0C, 0x30, 0x0E, 0x00, // 0x77 'w'
+ 0x22, 0x14, 0x08, 0x14, 0x22, 0x00, // 0x78 'x'
+ 0x0E, 0x50, 0x50, 0x48, 0x3E, 0x00, // 0x79 'y'
+ 0x22, 0x32, 0x2A, 0x26, 0x22, 0x00, // 0x7A 'z'
+ 0x08, 0x08, 0x77, 0x41, 0x41, 0x00, // 0x7B '{'
+ 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, // 0x7C '|'
+ 0x41, 0x41, 0x77, 0x08, 0x08, 0x00, // 0x7D '}'
+ 0x18, 0x04, 0x08, 0x10, 0x0C, 0x00, // 0x7E '~'
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x7F
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Keyball logo
+
+ // Logo 1/3 (0x80 ~ 0x8F)
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xC0, 0xF0, 0xF8, 0x8C, 0x86, 0xC6,
+ 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ 0xFF, 0xFE, 0xFC, 0xFC, 0xF8, 0xE0,
+ 0x80, 0x00, 0x00, 0x00, 0xF0, 0xF0,
+ 0xF0, 0x00, 0x00, 0xC0, 0xE0, 0xF0,
+ 0x70, 0x70, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xF8, 0xF8, 0xF8, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0xF8, 0xF8,
+ 0xF8, 0x00, 0x00, 0xF8, 0xF8, 0xF8,
+
+ // Logo 2/3 (0x90 ~ 0x9F)
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x80,
+ 0xE0, 0x70, 0x78, 0x68, 0x6C, 0x64,
+ 0xC7, 0xC7, 0xC7, 0xC7, 0xC7, 0xC7,
+ 0xE7, 0x3F, 0x1F, 0x0F, 0x3F, 0xFF,
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F,
+ 0x0F, 0x00, 0x00, 0x00, 0xFF, 0xFF,
+ 0xFF, 0x1E, 0x3F, 0xFF, 0xF3, 0xE1,
+ 0x80, 0x00, 0x00, 0xF8, 0xFC, 0xFE,
+ 0x36, 0x36, 0x36, 0x3E, 0xBC, 0xB8,
+ 0x00, 0x0E, 0x3E, 0xFE, 0xF0, 0x80,
+ 0xF0, 0xFE, 0x3E, 0x0E, 0x00, 0x00,
+ 0xFF, 0xFF, 0xFF, 0x06, 0x06, 0x06,
+ 0xFE, 0xFE, 0xFC, 0x70, 0x00, 0xE6,
+ 0xF6, 0xF6, 0x36, 0x36, 0xFE, 0xFE,
+ 0xFC, 0x00, 0x00, 0x00, 0xFF, 0xFF,
+ 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF,
+
+ // Logo 3/3 (0xA0 ~ 0xAF)
+ 0x00, 0x00, 0x00, 0x00, 0x3C, 0x7F,
+ 0x63, 0x60, 0xE0, 0xC0, 0xC0, 0xC0,
+ 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFF,
+ 0xFF, 0xC0, 0x60, 0x30, 0x18, 0x0F,
+ 0x03, 0x03, 0x03, 0x01, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x03, 0x03,
+ 0x03, 0x00, 0x00, 0x00, 0x03, 0x03,
+ 0x03, 0x03, 0x00, 0x01, 0x03, 0x03,
+ 0x03, 0x03, 0x03, 0x03, 0x03, 0x01,
+ 0x00, 0x70, 0x70, 0x39, 0x3F, 0x1F,
+ 0x07, 0x01, 0x00, 0x00, 0x00, 0x00,
+ 0x01, 0x03, 0x03, 0x03, 0x03, 0x03,
+ 0x03, 0x03, 0x01, 0x00, 0x00, 0x01,
+ 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+ 0x01, 0x00, 0x00, 0x00, 0x03, 0x03,
+ 0x03, 0x00, 0x00, 0x03, 0x03, 0x03,
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Special characters for Keyball
+
+ // 0xB0
+ 0x00, 0x10, 0x10, 0x10, 0x00, 0x00, // B0: blank indicator (BL)
+ 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, // B1: vertical label separator
+ 0x3E, 0x63, 0x5D, 0x5D, 0x63, 0x7F, // B2, B3:
+ 0x41, 0x7B, 0x77, 0x41, 0x3E, 0x00, // "ON" in 2 chars with light bg.
+ 0x00, 0x1C, 0x22, 0x22, 0x1C, 0x00, // B4, B5:
+ 0x3E, 0x0A, 0x00, 0x3E, 0x0A, 0x00, // "OFF" in 2 chars
+ 0x32, 0x2A, 0x3C, 0x00, 0x26, 0x28, // B6, B7:
+ 0x1E, 0x00, 0x1C, 0x2A, 0x2C, 0x00, // "aye" in 2 chars
+ 0x00, 0x00, 0x3F, 0x09, 0x36, 0x00, // B8: right half "R" indicate "row"
+ 0x00, 0x00, 0x1E, 0x21, 0x21, 0x00, // B9: right halc "C" indicate "column"
+ 0x00, 0x00, 0x00, 0x00, 0x3F, 0x08, // BA, BB:
+ 0x37, 0x00, 0x1E, 0x21, 0x21, 0x00, // "KC" in 2 chars right aligned
+ 0x1E, 0x21, 0x21, 0x00, 0x3F, 0x09, // BC, BD:
+ 0x06, 0x00, 0x21, 0x3F, 0x21, 0x00, // "CPI" in 2 chars
+ 0x22, 0x25, 0x19, 0x00, 0x1E, 0x21, // BE, BF:
+ 0x21, 0x00, 0x3F, 0x09, 0x36, 0x00, // "SCR" in 2 chars indicate "scroll"
+
+ // 0xC0
+ 0x3F, 0x21, 0x1E, 0x00, 0x21, 0x3F, // C0, C1:
+ 0x21, 0x00, 0x0F, 0x30, 0x0F, 0x00, // "DIV" in 2 chars indicate "divider"
+ 0x3E, 0x09, 0x3E, 0x00, 0x3F, 0x06, // C2, C3:
+ 0x3F, 0x00, 0x3F, 0x20, 0x20, 0x00, // "AML" in 2 chars
+};
+// clang-format on
diff --git a/keyboards/yowkees/lib/oledkit/oledkit.c b/keyboards/yowkees/lib/oledkit/oledkit.c
new file mode 100644
index 00000000000..04e67517a9e
--- /dev/null
+++ b/keyboards/yowkees/lib/oledkit/oledkit.c
@@ -0,0 +1,61 @@
+/*
+Copyright 2021 @Yowkees
+Copyright 2021 MURAOKA Taro (aka KoRoN, @kaoriya)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+#include "quantum.h"
+
+#if defined(OLED_ENABLE) && !defined(OLEDKIT_DISABLE)
+
+__attribute__((weak)) void oledkit_render_logo_user(void) {
+ // Require `OLED_FONT_H "keyboards/keyball/lib/logofont/logofont.c"`
+ char ch = 0x80;
+ for (int y = 0; y < 3; y++) {
+ oled_write_P(PSTR(" "), false);
+ for (int x = 0; x < 16; x++) {
+ oled_write_char(ch++, false);
+ }
+ oled_advance_page(false);
+ }
+}
+
+__attribute__((weak)) void oledkit_render_info_user(void) {
+ oledkit_render_logo_user();
+}
+
+__attribute__((weak)) bool oled_task_user(void) {
+ if (is_keyboard_master()) {
+ oledkit_render_info_user();
+ } else {
+ oledkit_render_logo_user();
+ }
+ return true;
+}
+
+__attribute__((weak)) oled_rotation_t oled_init_user(oled_rotation_t rotation) {
+ // Logo needs to be rotated 180 degrees.
+ //
+ // A typical OLED has a narrow margin on the left side near the origin, and
+ // a wide margin on the right side. The Keyball logo consists of three
+ // lines. If the logo is displayed on an OLED consisting of four lines, the
+ // margin on the right side will be too large and the balance is not good.
+ //
+ // Additionally, by rotating it, the left side of the logo will be above
+ // the OLED screen, giving it a natural look.
+ return !is_keyboard_master() ? OLED_ROTATION_180 : rotation;
+}
+
+#endif // OLED_ENABLE
diff --git a/keyboards/yowkees/lib/oledkit/oledkit.h b/keyboards/yowkees/lib/oledkit/oledkit.h
new file mode 100644
index 00000000000..c8865eb4525
--- /dev/null
+++ b/keyboards/yowkees/lib/oledkit/oledkit.h
@@ -0,0 +1,34 @@
+/*
+Copyright 2021 @Yowkees
+Copyright 2021 MURAOKA Taro (aka KoRoN, @kaoriya)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+#pragma once
+
+#if defined(OLED_ENABLE) && !defined(OLEDKIT_DISABLE)
+
+// oledkit_render_info_user renders keyboard's internal state information to
+// primary board. A keymap can override this by defining a function with same
+// signature.
+//
+// It render a logo as default.
+void oledkit_render_info_user(void);
+
+// oledkit_render_logo_user renders a logo of keyboard to secondary board.
+// A keymap can override this by defining a function with same signature.
+void oledkit_render_logo_user(void);
+
+#endif // OLED_ENABLE