From c819c83b801dfb3483505c4b50271e0fbae7c555 Mon Sep 17 00:00:00 2001 From: rootiest Date: Mon, 6 Apr 2026 12:18:08 -0400 Subject: [PATCH 1/3] feat(q5_max): add chord-based unicode/emoji input system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement a chord-mode unicode entry system activated by Fn1+LeftAlt (CHORD_KEY). Supports two activation styles: tap CHORD_KEY then type the sequence within a 2-second window, or hold CHORD_KEY, type the sequence, and release to commit. - Add chord_unicode.c/h with a ~110-entry table covering math symbols (°²³√≈≠≤≥±÷×∞π), Greek letters, currency, fractions, arrows, typography, and a broad emoji set - Prefix-aware matching with a 300ms disambiguation timer handles same-prefix alias pairs (e.g. lte/lteq→≤, inf/infty→∞) cleanly - Backspace deletes, Enter confirms, Escape cancels while in chord mode - Modifier and layer key-up events pass through so TT(FN1) release correctly deactivates the layer while chord mode is active - Enable UNICODE_ENABLE and wire chord_unicode.c into the build --- .../ansi_encoder/keymaps/via/chord_unicode.c | 440 ++++++++++++++++++ .../ansi_encoder/keymaps/via/chord_unicode.h | 60 +++ .../q5_max/ansi_encoder/keymaps/via/keymap.c | 28 +- .../q5_max/ansi_encoder/keymaps/via/rules.mk | 2 + 4 files changed, 529 insertions(+), 1 deletion(-) create mode 100644 keyboards/keychron/q5_max/ansi_encoder/keymaps/via/chord_unicode.c create mode 100644 keyboards/keychron/q5_max/ansi_encoder/keymaps/via/chord_unicode.h diff --git a/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/chord_unicode.c b/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/chord_unicode.c new file mode 100644 index 0000000000..b6bfa3a44d --- /dev/null +++ b/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/chord_unicode.c @@ -0,0 +1,440 @@ +// Copyright 2024 rootiest +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "chord_unicode.h" +#include + +// ============================================================================ +// Chord table +// ============================================================================ +// Rules: +// • Sequences are lowercase ASCII letters and digits only. +// • Two entries whose sequences share a prefix are fine as long as they +// produce the SAME output (aliases). If they produce DIFFERENT output, +// they must differ before the shorter one ends — otherwise the +// disambiguation timer handles it (CHORD_DISAMBIG_MS). +// • Max sequence length is CHORD_MAX_LEN (8). +// +// NOTE: ARM targets (this keyboard) store const data in flash regardless of +// PROGMEM; no pgm_read_* helpers are needed here. +// ============================================================================ + +typedef struct { + const char *seq; // ASCII sequence to type after CHORD_KEY + const char *out; // UTF-8 string to output +} chord_entry_t; + +// clang-format off +static const chord_entry_t chord_table[] = { + + // ---- Math / Science ---------------------------------------------------- + {"deg", "°"}, // degree sign U+00B0 + {"sqrt", "√"}, // square root U+221A + {"sqrd", "²"}, // superscript 2 U+00B2 + {"cubd", "³"}, // superscript 3 U+00B3 + {"aprx", "≈"}, // almost equal U+2248 + {"apx", "≈"}, // alias + {"neq", "≠"}, // not equal U+2260 + {"lteq", "≤"}, // less-or-equal U+2264 + {"lte", "≤"}, // alias + {"gteq", "≥"}, // greater-or-equal U+2265 + {"gte", "≥"}, // alias + {"pm", "±"}, // plus-minus U+00B1 + {"div", "÷"}, // division sign U+00F7 + {"times", "×"}, // multiplication sign U+00D7 + {"mult", "×"}, // alias + {"infty", "∞"}, // infinity U+221E + {"inf", "∞"}, // alias (prefix of "infty" → same output, OK) + {"pi", "π"}, // pi U+03C0 + {"micro", "µ"}, // micro sign U+00B5 + {"sigma", "Σ"}, // capital sigma U+03A3 + {"delta", "Δ"}, // capital delta U+0394 + {"theta", "θ"}, // small theta U+03B8 + {"omega", "Ω"}, // capital omega U+03A9 + {"alpha", "α"}, // small alpha U+03B1 + {"beta", "β"}, // small beta U+03B2 + + // ---- Currency ---------------------------------------------------------- + {"euro", "€"}, // euro U+20AC + {"pound", "£"}, // pound sterling U+00A3 + {"yen", "¥"}, // yen / yuan U+00A5 + {"cent", "¢"}, // cent sign U+00A2 + + // ---- Fractions --------------------------------------------------------- + {"half", "½"}, // 1/2 U+00BD + {"qtr", "¼"}, // 1/4 U+00BC + {"3qtr", "¾"}, // 3/4 U+00BE + + // ---- Legal / IP -------------------------------------------------------- + {"copy", "©"}, // copyright U+00A9 + {"reg", "®"}, // registered U+00AE + {"tm", "™"}, // trademark U+2122 + + // ---- Arrows ------------------------------------------------------------ + // Use "arr{l,r,u,d}" for the four cardinal arrows and + // "arr{h,v}" for the bidirectional pair. This avoids "arrl" being a + // prefix of a different-output entry. + {"arrl", "←"}, // left U+2190 + {"arrr", "→"}, // right U+2192 + {"arru", "↑"}, // up U+2191 + {"arrd", "↓"}, // down U+2193 + {"arrh", "↔"}, // left↔right U+2194 + {"arrv", "↕"}, // up↕down U+2195 + {"dbl", "⇒"}, // double right arrow U+21D2 + + // ---- Typography -------------------------------------------------------- + {"bull", "•"}, // bullet U+2022 + {"mdash", "—"}, // em dash U+2014 + {"ndash", "–"}, // en dash U+2013 + {"ellip", "…"}, // horizontal ellipsis U+2026 + + // ---- Emoji : Faces ----------------------------------------------------- + {"smile", "🙂"}, // slightly smiling face U+1F642 + {"grin", "😁"}, // beaming face U+1F601 + {"lol", "😂"}, // face with tears of joy U+1F602 + {"cry", "😭"}, // loudly crying face U+1F62D + {"sad", "🙁"}, // slightly frowning face U+1F641 + {"wink", "😉"}, // winking face U+1F609 + {"cool", "😎"}, // smiling face w/ sunglasses U+1F60E + {"think", "🤔"}, // thinking face U+1F914 + {"shrug", "🤷"}, // person shrugging U+1F937 + {"ugh", "😤"}, // face with steam U+1F624 + {"wow", "😮"}, // face with open mouth U+1F62E + {"zip", "🤐"}, // zipper-mouth face U+1F910 + {"nerdy", "🤓"}, // nerd face U+1F913 + + // ---- Emoji : Gestures -------------------------------------------------- + {"thup", "👍"}, // thumbs up U+1F44D + {"thdn", "👎"}, // thumbs down U+1F44E + {"wave", "👋"}, // waving hand U+1F44B + {"clap", "👏"}, // clapping U+1F44F + {"fist", "✊"}, // raised fist U+270A + {"pray", "🙏"}, // folded hands U+1F64F + {"ok", "👌"}, // ok hand U+1F44C + {"point", "👉"}, // backhand index pointing right U+1F449 + + // ---- Emoji : Symbols --------------------------------------------------- + // Variation selectors (U+FE0F) are omitted for cross-app compatibility; + // modern renderers apply emoji presentation automatically. + {"heart", "❤"}, // heavy black heart U+2764 + {"check", "✓"}, // check mark U+2713 + {"cross", "✗"}, // ballot X U+2717 + {"warn", "⚠"}, // warning sign U+26A0 + {"stop", "🛑"}, // stop sign U+1F6D1 + {"yes", "✅"}, // white heavy check mark U+2705 + {"nope", "❌"}, // cross mark U+274C + {"ques", "❓"}, // question mark U+2753 + {"excl", "❗"}, // exclamation mark U+2757 + {"help", "ℹ"}, // information source U+2139 + {"fire", "🔥"}, // fire U+1F525 + {"star", "⭐"}, // star U+2B50 + {"tada", "🎉"}, // party popper U+1F389 + {"100", "💯"}, // hundred points U+1F4AF + {"zzz", "💤"}, // zzz U+1F4A4 + {"skull", "💀"}, // skull U+1F480 + {"poop", "💩"}, // pile of poo U+1F4A9 + {"eyes", "👀"}, // eyes U+1F440 + {"bell", "🔔"}, // bell U+1F514 + {"mute", "🔇"}, // muted speaker U+1F507 + {"loud", "🔊"}, // loud speaker U+1F50A + {"bulb", "💡"}, // light bulb U+1F4A1 + {"tack", "📌"}, // pushpin / thumbtack U+1F4CC + {"key", "🔑"}, // key U+1F511 + {"lock", "🔒"}, // lock U+1F512 + {"robot", "🤖"}, // robot U+1F916 + {"alien", "👽"}, // alien U+1F47D + + // ---- Emoji : Nature ---------------------------------------------------- + {"sun", "☀"}, // black sun U+2600 + {"moon", "🌙"}, // crescent moon U+1F319 + {"snow", "❄"}, // snowflake U+2744 + {"rain", "🌧"}, // cloud with rain U+1F327 + {"bolt", "⚡"}, // high voltage / lightning U+26A1 + {"cat", "🐱"}, // cat face U+1F431 + {"dog", "🐶"}, // dog face U+1F436 + {"fox", "🦊"}, // fox face U+1F98A + {"bear", "🐻"}, // bear face U+1F43B + + // ---- Emoji : Food & Objects -------------------------------------------- + {"coffee","☕"}, // hot beverage U+2615 + {"beer", "🍺"}, // beer mug U+1F37A + {"pizza", "🍕"}, // pizza U+1F355 + {"cake", "🎂"}, // birthday cake U+1F382 + {"gift", "🎁"}, // wrapped gift U+1F381 + {"mike", "🎤"}, // microphone U+1F3A4 + {"mus", "🎵"}, // musical note U+1F3B5 + {"phone", "📱"}, // mobile phone U+1F4F1 + {"pc", "💻"}, // laptop U+1F4BB + {"book", "📖"}, // open book U+1F4D6 + {"mail", "📧"}, // e-mail U+1F4E7 + {"money", "💰"}, // money bag U+1F4B0 + {"gem", "💎"}, // gem stone U+1F48E + {"sword", "⚔"}, // crossed swords U+2694 + {"shield","🛡"}, // shield U+1F6E1 + {"rocket","🚀"}, // rocket U+1F680 + {"tools", "🔧"}, // wrench / tools U+1F527 + {"trash", "🗑"}, // wastebasket U+1F5D1 + {"clock", "🕐"}, // one o'clock U+1F550 + {"hour", "⏳"}, // hourglass with flowing sand U+23F3 +}; +// clang-format on + +#define CHORD_TABLE_LEN (sizeof(chord_table) / sizeof(chord_table[0])) + +// ============================================================================ +// State machine +// ============================================================================ + +typedef enum { + CS_IDLE, + CS_ACTIVE, +} chord_state_t; + +static chord_state_t chord_state = CS_IDLE; +static bool chord_held = false; // true while CHORD_KEY is still held +static char chord_buf[CHORD_MAX_LEN + 1]; +static uint8_t chord_buf_len = 0; +static uint16_t chord_timer = 0; // last activity timestamp + +// Disambiguation: pending exact match that is also a prefix of a longer entry +// whose output differs. +static bool chord_disambig = false; +static uint16_t chord_disambig_t = 0; +static uint8_t chord_disambig_i = 0; + +// ============================================================================ +// Internal helpers +// ============================================================================ + +static void chord_reset(void) { + chord_state = CS_IDLE; + chord_held = false; + chord_buf_len = 0; + chord_buf[0] = '\0'; + chord_disambig = false; +} + +static void chord_output(uint8_t idx) { + send_unicode_string(chord_table[idx].out); + chord_reset(); +} + +/* + * Scan the chord table against the current buffer. + * + * Returns: + * -2 — no match at all (not even a prefix) → cancel chord mode + * -1 — buffer is a prefix of ≥1 entry but no exact match yet → keep going + * ≥ 0 — index of the first exact match; *is_prefix is set if the buffer is + * also a prefix of a DIFFERENT-output longer entry + */ +static int chord_check(bool *is_prefix) { + *is_prefix = false; + int exact_idx = -1; + bool any_match = false; + + for (uint8_t i = 0; i < CHORD_TABLE_LEN; i++) { + uint8_t slen = (uint8_t)strlen(chord_table[i].seq); + + // Entry shorter than current buffer → can't match + if (slen < chord_buf_len) continue; + + // Must be a prefix match at minimum + if (memcmp(chord_table[i].seq, chord_buf, chord_buf_len) != 0) continue; + + any_match = true; + + if (slen == chord_buf_len) { + // Exact match — take the first one found + if (exact_idx < 0) exact_idx = (int)i; + } else { + // Buffer is a strict prefix of this longer entry + // Flag as a prefix conflict only when output differs from the + // already-found exact match (same-output aliases are harmless). + if (exact_idx >= 0) { + if (strcmp(chord_table[i].out, chord_table[exact_idx].out) != 0) { + *is_prefix = true; + } + } else { + // No exact match found yet; mark the prefix tentatively. + // We'll re-evaluate the flag after finding an exact match, but + // we need any_match = true to avoid returning -2. + *is_prefix = true; // tentative; may be cleared below + } + } + } + + if (!any_match) return -2; + if (exact_idx < 0) return -1; // prefix(es) only + + // Re-verify is_prefix: we may have set it before seeing the exact match. + // Walk again only for the is_prefix refinement. + if (*is_prefix) { + *is_prefix = false; + for (uint8_t i = 0; i < CHORD_TABLE_LEN; i++) { + uint8_t slen = (uint8_t)strlen(chord_table[i].seq); + if (slen <= chord_buf_len) continue; + if (memcmp(chord_table[i].seq, chord_buf, chord_buf_len) != 0) continue; + // Longer entry that shares our prefix + if (strcmp(chord_table[i].out, chord_table[exact_idx].out) != 0) { + *is_prefix = true; + break; + } + } + } + + return exact_idx; +} + +// ============================================================================ +// Public API +// ============================================================================ + +void chord_init(void) { + chord_reset(); +} + +void chord_activate(void) { + chord_reset(); + chord_state = CS_ACTIVE; + chord_held = true; + chord_timer = timer_read(); +} + +void chord_key_released(void) { + if (chord_state != CS_ACTIVE) return; + + if (chord_buf_len == 0) { + // Chord key tapped with nothing typed yet → switch to tap mode. + // Keep collecting; idle timeout will cancel if nothing arrives. + chord_held = false; + chord_timer = timer_read(); + return; + } + + // Hold mode: key released with a partial or complete sequence. + if (chord_disambig) { + chord_output(chord_disambig_i); + return; + } + + bool is_prefix; + int idx = chord_check(&is_prefix); + if (idx >= 0) { + chord_output(idx); + } else { + chord_reset(); + } +} + +bool process_chord(uint16_t keycode, keyrecord_t *record) { + if (chord_state == CS_IDLE) return true; + + // Pass through key-up events while chord mode is active. + // Basic key-ups (a-z, 0-9) are no-ops on the host since we never sent + // the corresponding key-down. Layer/quantum key-ups (TT, MO, LT, etc.) + // must reach QMK's layer system so layers deactivate correctly. + if (!record->event.pressed) return true; + + // Any keypress resets the idle timer. + chord_timer = timer_read(); + + // --- Special keys ------------------------------------------------------- + + if (keycode == KC_ESC) { + chord_reset(); + return false; + } + + if (keycode == KC_BSPC) { + if (chord_disambig) { + chord_disambig = false; + } else if (chord_buf_len > 0) { + chord_buf[--chord_buf_len] = '\0'; + } + return false; + } + + if (keycode == KC_ENT) { + // Enter explicitly confirms the current buffer. + if (chord_disambig) { + chord_output(chord_disambig_i); + } else { + bool is_prefix; + int idx = chord_check(&is_prefix); + if (idx >= 0) { + chord_output(idx); + } else { + chord_reset(); + } + } + return false; + } + + // Modifier keys (Ctrl, Shift, Alt, GUI) pass through without disturbing + // chord mode — the user may have them held for unrelated reasons. + if (keycode >= KC_LCTL && keycode <= KC_RGUI) { + return true; + } + + // --- Build the sequence buffer ------------------------------------------ + + char c = 0; + if (keycode >= KC_A && keycode <= KC_Z) { + c = 'a' + (keycode - KC_A); + } else if (keycode >= KC_1 && keycode <= KC_9) { + c = '1' + (keycode - KC_1); + } else if (keycode == KC_0) { + c = '0'; + } else { + // Unrecognised key: cancel chord mode silently. + chord_reset(); + return false; + } + + if (chord_buf_len >= CHORD_MAX_LEN) { + chord_reset(); + return false; + } + + chord_buf[chord_buf_len++] = c; + chord_buf[chord_buf_len] = '\0'; + chord_disambig = false; + + // --- Check the table ---------------------------------------------------- + + bool is_prefix; + int idx = chord_check(&is_prefix); + + if (idx == -2) { + // Dead end — no entry can ever match this buffer. + chord_reset(); + } else if (idx >= 0 && !is_prefix) { + // Clean exact match: output immediately. + chord_output(idx); + } else if (idx >= 0 && is_prefix) { + // Exact match, but a longer different-output entry shares the prefix. + // Start the disambiguation timer; output fires when it expires. + chord_disambig = true; + chord_disambig_t = timer_read(); + chord_disambig_i = (uint8_t)idx; + } + // idx == -1: only prefix match(es) so far → keep collecting. + + return false; +} + +void chord_scan(void) { + if (chord_state == CS_IDLE) return; + + // Disambiguation timeout: no longer input arrived → commit exact match. + if (chord_disambig && timer_elapsed(chord_disambig_t) > CHORD_DISAMBIG_MS) { + chord_output(chord_disambig_i); + return; + } + + // Idle timeout (tap mode only): cancel if nothing typed for too long. + if (!chord_held && timer_elapsed(chord_timer) > CHORD_TIMEOUT_MS) { + chord_reset(); + } +} diff --git a/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/chord_unicode.h b/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/chord_unicode.h new file mode 100644 index 0000000000..81ee3624ea --- /dev/null +++ b/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/chord_unicode.h @@ -0,0 +1,60 @@ +// Copyright 2024 rootiest +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include QMK_KEYBOARD_H + +/* + * Chord Unicode System + * ==================== + * Activate chord mode with CHORD_KEY (Fn1 + Left Alt). + * + * Two activation styles: + * Tap - tap CHORD_KEY, release it, then type the sequence within + * CHORD_TIMEOUT_MS. Output fires on an unambiguous exact match. + * Hold - hold CHORD_KEY, type the sequence, then release CHORD_KEY. + * Output fires on exact match or on key release if buffer matches. + * + * While collecting: + * - Alpha / digit keys append to the sequence buffer (always lowercase). + * - Backspace deletes the last character. + * - Enter confirms the current buffer (useful when a shorter sequence is + * a prefix of a longer one with different output). + * - Escape cancels chord mode. + * - Any other key cancels chord mode silently. + * + * Prefix disambiguation: + * If the current buffer is an exact match AND a prefix of a longer entry + * that maps to a DIFFERENT character, a CHORD_DISAMBIG_MS timer fires + * and commits the shorter match if no more keys arrive in time. + * Aliases (different sequences → same output) coexist without conflict. + */ + +/* Maximum sequence length (characters). */ +#define CHORD_MAX_LEN 8 + +/* Milliseconds of idle time before tap-mode chord is cancelled. */ +#define CHORD_TIMEOUT_MS 2000 + +/* Milliseconds to wait for more input when an exact match is also a prefix + * of a longer entry with different output. */ +#define CHORD_DISAMBIG_MS 300 + +/* Called once at startup to prepare the chord subsystem. + * The caller (keyboard_post_init_user in keymap.c) is responsible for + * setting the unicode input mode via set_unicode_input_mode(). */ +void chord_init(void); + +/* Call from process_record_user when CHORD_KEY is pressed. */ +void chord_activate(void); + +/* Call from process_record_user when CHORD_KEY is released. */ +void chord_key_released(void); + +/* Call from process_record_user for every other key event. + * Returns false when the key was consumed by the chord subsystem (do not + * pass through to the host), true otherwise. */ +bool process_chord(uint16_t keycode, keyrecord_t *record); + +/* Call from matrix_scan_user to drive timeouts. */ +void chord_scan(void); diff --git a/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/keymap.c b/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/keymap.c index a8069c8201..c343a075ce 100644 --- a/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/keymap.c +++ b/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/keymap.c @@ -16,6 +16,7 @@ #include QMK_KEYBOARD_H #include "keychron_common.h" +#include "chord_unicode.h" // Tap Dance declarations enum { @@ -26,6 +27,7 @@ enum { enum custom_keycodes { ALT_TAB_FWD = SAFE_RANGE, // Alt+Tab (forward) ALT_TAB_BWD, // Alt+Shift+Tab (backward) + CHORD_KEY, // Fn1+LeftAlt → chord/unicode entry mode }; // Alt-Tab cycling state @@ -58,7 +60,7 @@ const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = { KC_TAB, KC_Q, KC_W, KC_E, KC_R, KC_T, KC_Y, KC_U, KC_I, KC_O, KC_P, KC_LBRC, KC_RBRC, KC_BSLS, KC_PGDN, KC_P7, KC_P8, KC_P9, KC_CAPS, KC_A, KC_S, KC_D, KC_F, KC_G, KC_H, KC_J, KC_K, KC_L, KC_SCLN, KC_QUOT, KC_ENT, KC_END, KC_P4, KC_P5, KC_P6, KC_PPLS, KC_LSFT, KC_Z, KC_X, KC_C, KC_V, KC_B, KC_N, KC_M, KC_COMM, KC_DOT, KC_SLSH, KC_RSFT, KC_UP, KC_P1, KC_P2, KC_P3, - KC_LCTL, KC_LGUI, KC_LALT, KC_SPC, TT(FN3), TG(FN1), OSL(KEEB_CTL), KC_HOME, KC_DOWN, KC_END, KC_P0, KC_PDOT, KC_PENT), + KC_LCTL, KC_LGUI, CHORD_KEY, KC_SPC, TT(FN3), TG(FN1), OSL(KEEB_CTL), KC_HOME, KC_DOWN, KC_END, KC_P0, KC_PDOT, KC_PENT), [FN2] = LAYOUT_ansi_101( KC_PWR, KC_F13, KC_F14, KC_F15, KC_F16, KC_F17, KC_F18, KC_F19, KC_F20, KC_F21, KC_F22, KC_F23, KC_F24, KC_DEL, KC_PSCR, KC_CALC, KC_FIND, KC_MPLY, @@ -105,10 +107,33 @@ const uint16_t PROGMEM encoder_map[][NUM_ENCODERS][2] = { #endif // ENCODER_MAP_ENABLE // clang-format on + +void keyboard_post_init_user(void) { + chord_init(); + // Use the Linux unicode input method (Ctrl+Shift+U → hex → Enter). + set_unicode_input_mode(UNICODE_MODE_LINUX); +} + bool process_record_user(uint16_t keycode, keyrecord_t *record) { if (!process_record_keychron_common(keycode, record)) { return false; } + + // Chord key: activate/deactivate chord unicode mode. + if (keycode == CHORD_KEY) { + if (record->event.pressed) { + chord_activate(); + } else { + chord_key_released(); + } + return false; + } + + // While chord mode is active, let it consume the key event. + if (!process_chord(keycode, record)) { + return false; + } + switch (keycode) { case ALT_TAB_FWD: if (record->event.pressed) { @@ -141,6 +166,7 @@ void matrix_scan_user(void) { unregister_code(KC_LALT); alt_tab_active = false; } + chord_scan(); } // Tap Dance definitions diff --git a/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/rules.mk b/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/rules.mk index 791d5ab502..d41444a388 100644 --- a/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/rules.mk +++ b/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/rules.mk @@ -1,2 +1,4 @@ VIA_ENABLE = yes TAP_DANCE_ENABLE = yes +UNICODE_ENABLE = yes +SRC += chord_unicode.c -- 2.52.0 From e1bed8ec91fe75df17427386ad709e41813886ad Mon Sep 17 00:00:00 2001 From: rootiest Date: Mon, 6 Apr 2026 12:21:39 -0400 Subject: [PATCH 2/3] docs: add CLAUDE.md with project guidelines and venv requirement Document build commands, code style, and development workflow for the Q5 Max via keymap. Emphasize that all qmk commands must be run inside the project Python venv (.venv/bin/activate) rather than the system qmk. --- CLAUDE.md | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..64cf2bfb73 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,64 @@ +# CLAUDE.md - QMK Development (Keychron Q5 Max) + +Guidelines and commands for the customized Keychron Q5 Max firmware project based on the `wireless_playground` fork. + +## Project Scope + +* **Origin:** [git.rootiest.dev/rootiest/qmk_firmware](https://git.rootiest.dev/rootiest/qmk_firmware) +* **Upstream:** [github.com/Keychron/qmk_firmware](https://github.com/Keychron/qmk_firmware) (branch: `wireless_playground`) +* **Primary Keyboard:** Keychron Q5 Max (ANSI Encoder) +* **Development Workflow:** Work is performed on the `q5_dev` branch before merging to `main`. +* **Feature Goals:** Tap-Dance, expanded layers, advanced Chording, Unicode support, and Auto-correct. + +## Build and Flash Commands + +All commands must be run from the root of the repository **inside the project +Python virtual environment**. The system `qmk` is not used — activate the venv +first: + +```bash +source .venv/bin/activate +``` + +Every `qmk` command below assumes the venv is active (or prefix each with +`source .venv/bin/activate &&`). + +### Compilation + +```bash +# Build the Q5 Max ANSI Encoder firmware +qmk compile -kb keychron/q5_max/ansi_encoder -km via +``` + +### Flashing + +```bash +# Flash the Q5 Max (requires the board to be in bootloader mode) +qmk flash -kb keychron/q5_max/ansi_encoder -km via +``` + +### Environment Setup + +```bash +# Ensure the submodules are up to date (critical for the wireless_playground branch) +git submodule update --init --recursive + +# Set the default keyboard/keymap +qmk setup +qmk config user.keyboard=keychron/q5_max/ansi_encoder +qmk config user.keymap=via +``` + +## Code Style and Patterns + +* **Keymap Structure:** Keep the `keymap.c` organized by layers. Use descriptive defines for layer names (e.g., `_BASE`, `_FN`, `_CHORD`). +* **Feature Modules:** For advanced features like Chording or Tap-Dance, prefer creating separate headers/source files in the keymap folder to keep `keymap.c` readable. +* **Firmware Size:** Monitor the compiled `.bin` size, as wireless features and large feature sets (like Auto-correct) can quickly fill up flash memory. +* **Documentation:** Comment any complex chording logic or non-standard Tap-Dance implementations to ensure maintainability. + +## Development Workflow + +1. Verify the current branch is `q5_dev`. +2. Implement features in `keyboards/keychron/q5_max/ansi_encoder/keymaps/via/`. +3. Test compilation locally before committing. +4. Ensure `rules.mk` has the necessary flags enabled (e.g., `TAP_DANCE_ENABLE = yes`, `UNICODE_ENABLE = yes`). -- 2.52.0 From bdcec0fd7ee5a3cc755aa208857bc271a844503c Mon Sep 17 00:00:00 2001 From: rootiest Date: Mon, 6 Apr 2026 12:23:13 -0400 Subject: [PATCH 3/3] docs(CLAUDE.md): add git conventions section --- CLAUDE.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 64cf2bfb73..4cf77ed536 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -62,3 +62,8 @@ qmk config user.keymap=via 2. Implement features in `keyboards/keychron/q5_max/ansi_encoder/keymaps/via/`. 3. Test compilation locally before committing. 4. Ensure `rules.mk` has the necessary flags enabled (e.g., `TAP_DANCE_ENABLE = yes`, `UNICODE_ENABLE = yes`). + +## Git Conventions + +* Use conventional commits (`feat:`, `fix:`, `docs:`, `chore:`, etc.) scoped to the keyboard where relevant (e.g. `feat(q5_max):`). +* Do **not** include `Co-Authored-By: Claude` trailers in commit messages. -- 2.52.0