diff --git a/.gitignore b/.gitignore index 76a8fb48b0..360df729cb 100644 --- a/.gitignore +++ b/.gitignore @@ -120,3 +120,402 @@ via*.json !keyboards/keychron/*/firmware/*.bin /.remember/tmp/save-session.pid /.direnv/CACHEDIR.TAG + +# ──────────────── Added by agents-init ────────────────── +# agents-init --agents +AGENTS/ +/AGENTS.md +/CLAUDE.md +# ──────────────────────────────────────────────────────── + +# ──────────────── Added by agents-init ────────────────── +# agents-init --plugins +docs/superpowers +docs/plans +docs/specs +docs/devlogs +# ──────────────────────────────────────────────────────── + +# id: gitig-boilerplate-e76d1ca7170bfb8385be6293da3f57bc +# ╭──────────────────────────────────────────────────────────╮ +# │ GitIgnore Boilerplate Template │ +# ╰──────────────────────────────────────────────────────────╯ +# +# ──────────────────── OS-Generated Files ──────────────────── +# automatic backup files created by some editors (e.g., Vim, Emacs) +*~ + +# temporary files created if a process still has a handle to a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# MacOS junk +.DS_Store +Thumbs.db + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# files created when an open file is removed but is still being accessed +.nfs* + +# ─────────────────── Debug/Temporary/Testing ──────────────── +# Matches OLD / .OLD +[Oo][Ll][Dd]/ +.[Oo][Ll][Dd]/ + +# Matches DISABLE / .DISABLE +[Dd][Ii][Ss][Aa][Bb][Ll][Ee]/ +.[Dd][Ii][Ss][Aa][Bb][Ll][Ee]/ + +# Matches DISABLED / .DISABLED +[Dd][Ii][Ss][Aa][Bb][Ll][Ee][Dd]/ +.[Dd][Ii][Ss][Aa][Bb][Ll][Ee][Dd]/ + +# Matches DEBUG / .DEBUG +[Dd][Ee][Bb][Uu][Gg]/ +.[Dd][Ee][Bb][Uu][Gg]/ + +# Matches TMP / .TMP +[Tt][Mm][Pp]/ +.[Tt][Mm][Pp]/ + +# Matches TEMP / .TEMP +[Tt][Ee][Mm][Pp]/ +.[Tt][Ee][Mm][Pp]/ + +# Matches TEMPORARY / .TEMPORARY +[Tt][Ee][Mm][Pp][Oo][Rr][Aa][Rr][Yy]/ +.[Tt][Ee][Mm][Pp][Oo][Rr][Aa][Rr][Yy]/ + +# Matches TESTING / .TESTING +[Tt][Ee][Ss][Tt][Ii][Nn][Gg]/ +.[Tt][Ee][Ss][Tt][Ii][Nn][Gg]/ + +# ─────────────────── AI Sessions and Rules ────────────────── +# Matches CLAUDE.md, .claud*, etc. +[Cc][Ll][Aa][Uu][Dd][Ee].[Mm][Dd] +.[Cc][Ll][Aa][Uu][Dd]* + +# Matches GEMINI.md, .gemin*, etc. +[Gg][Ee][Mm][Ii][Nn][Ii].[Mm][Dd] +.[Gg][Ee][Mm][Ii][Nn]* + +# Matches ANTIGRAVITY.md, .antigrav*, etc. +[Aa][nN][Tt][Ii][Gg][Rr][Aa][Vv][Ii][Tt][Yy].[Mm][Dd] +.[Aa][Nn][Tt][Ii][Gg][Rr][Aa][Vv]* + +# Matches AGENTS.md, .agents, .remember, etc. +[Aa][Gg][Ee][Nn][Tt][Ss].[Mm][Dd] +.[Aa][Gg][Ee][Nn][Tt][Ss] +.[Rr][Ee][Mm][Ee][Mm][Bb][Ee][Rr] + +# ──────────────────── Planning Artifacts ─────────────────── +# Catalog files generated by pre-implementation analysis passes +.superpowers +docs/superpowers +docs/specs +docs/devlogs + +# ────────────────────────────────────────────────────────────── + +# id: gi-patterns-54143085ec229f3cf907a5aa2a8fcade +# Created by https://www.toptal.com/developers/gitignore/api/linux +# Edit at https://www.toptal.com/developers/gitignore?templates=linux + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# End of https://www.toptal.com/developers/gitignore/api/linux + +# id: gi-patterns-0f32034d72ab434db92ec99a29f317cd +# Created by https://www.toptal.com/developers/gitignore/api/c +# Edit at https://www.toptal.com/developers/gitignore?templates=c + +### C ### +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf + +# End of https://www.toptal.com/developers/gitignore/api/c + +# id: gi-patterns-8d9b9f4f5eba6df00650424b9b029769 +# Created by https://www.toptal.com/developers/gitignore/api/c++ +# Edit at https://www.toptal.com/developers/gitignore?templates=c++ + +### C++ ### +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +# End of https://www.toptal.com/developers/gitignore/api/c++ + +# id: gi-patterns-e390e9c720b3dda36906a2b4cb76ffdd +# Created by https://www.toptal.com/developers/gitignore/api/python +# Edit at https://www.toptal.com/developers/gitignore?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +# End of https://www.toptal.com/developers/gitignore/api/python diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 510441ca3b..0000000000 --- a/CLAUDE.md +++ /dev/null @@ -1,107 +0,0 @@ -# CLAUDE.md - QMK Development (Keychron Q5 Max / K17 Max) - -Guidelines and commands for the customized Keychron 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) -* **Secondary Keyboard:** Keychron K17 Max (occasionally) -* **Development Branches:** - * `dev/q5` — Q5 Max work-in-progress, merges to `main` - * `dev/k17` — K17 Max work-in-progress, merges 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 - -# Build the K17 Max firmware (rgb variant; separate 'white' LED variant exists) -qmk compile -kb keychron/k17_max/ansi_encoder/rgb -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 - -# Flash the K17 Max (requires the board to be in bootloader mode) -qmk flash -kb keychron/k17_max/ansi_encoder/rgb -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 `dev/q5` (Q5 Max) or `dev/k17` (K17 Max). -2. Implement features in the relevant keymap directory: - * Q5 Max: `keyboards/keychron/q5_max/ansi_encoder/keymaps/via/` - * K17 Max: `keyboards/keychron/k17_max/ansi_encoder/rgb/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):`, `fix(k17_max):`). -* Do **not** include `Co-Authored-By: Claude` trailers in commit messages. - -### Chained / Stacked PRs - -When merging a chain of PRs (e.g. `A → main`, `B → A`, `C → B`), always **delete the branch after each merge**. Gitea (and GitHub) will automatically retarget any open PRs pointing at the deleted branch to the branch it was merged into. This keeps the chain collapsing cleanly into `main` without manual retargeting or cleanup PRs. - -## EEPROM Layout Notes - -The Q5 Max uses wear-leveling EEPROM (STM32F401). Key layout facts: - -* `EECONFIG_RGB_MATRIX` is at bytes 24–31; byte 0 packs `mode[7:2] | enable[1:0]`. -* Keychron custom RGB data (effect list, regions, per-key colours, retail demo flag) lives in `EECONFIG_KB_DATABLOCK` immediately after `EECONFIG_BASE_SIZE` (37 bytes). -* `VIA_EEPROM_MAGIC_ADDR` is pinned to **544** in `ansi_encoder/config.h`. Do not lower this value — it must stay above `EECONFIG_BASE_SIZE + EECONFIG_KB_DATA_SIZE`. If Keychron EEPROM grows, raise 544 accordingly and clear EEPROM on the board. -* `EECONFIG_KB_DATA_SIZE` is computed in `eeconfig_kb.h` and requires an `#undef` before the `#define` to suppress QMK's default-zero value. - -## Keychron RGB (`KEYCHRON_RGB_ENABLE`) - -Enabled via `KEYCHRON_RGB_ENABLE = yes` in `rules.mk`. Key behavioural notes: - -* `eeconfig_init_custom_rgb()` **loads** Keychron RGB state from EEPROM into RAM. It must be called in `keyboard_post_init_kb()` and in `wireless_enter_connected_kb()`; without it the arrays are zero-initialised and Launcher settings are lost on every boot or transport change. -* `eeconfig_reset_custom_rgb()` **writes** defaults to EEPROM and stamps the version. The version stamp (`eeprom_update_dword(EECONFIG_KEYBOARD, ...)`) belongs here only — not in the load path. -* `kc_rgb_save()` must call `eeconfig_update_rgb_matrix()` to persist the QMK RGB mode alongside the Keychron custom data; otherwise `rgb_matrix_init()` (triggered on every transport change by `REINIT_LED_DRIVER = 1`) reloads the compile-time default `RGB_MATRIX_TYPING_HEATMAP`. -* `retail_demo_enable` is a single byte. A bug in the original Keychron code used `eeprom_read_block` instead of `eeprom_update_block` in `eeconfig_reset_custom_rgb()`, leaving `0xFF` on freshly-flashed boards. `retail_demo_task()` treats any non-zero value as "demo active" and forces `CUSTOM_MIXED_RGB` every scan. The load path now clamps `> 1` to `0` and re-writes the byte as a one-time recovery. -* `default_per_key_led[]` and `default_region[]` must be defined in board-specific code (e.g. `ansi_encoder.c`) — `keychron_rgb.c` declares them `extern`. - -## DIP Switch (Win/Mac) - -The Win-side dip switch uses a **frame overlay** rather than calling `rgb_matrix_mode()`. A `dip_win_active` flag is set on switch change; `rgb_matrix_indicators_advanced_user()` paints all LEDs white each frame when the flag is set. This avoids writing to EEPROM and preserves the Launcher-configured effect, which would otherwise be overwritten by the direct mode call. diff --git a/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/cheatsheet.html b/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/cheatsheet.html new file mode 100644 index 0000000000..e65bfd4881 --- /dev/null +++ b/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/cheatsheet.html @@ -0,0 +1,481 @@ + +Q5 Max Cheatsheet + + + + + + +
+
+
Keychron Q5 Max · ANSI encoder · via keymap
+

Cheatsheet

+ +
+ +
+

Layer maps

+
+
+
+ +
+
+
+
Hover or focus a key for details.
+
+ +
+

Key tricks

+
+
+ +
+

Autocorrect

+

+ +
+
+ +
+

Symbol chords

+
    +
    +
    + + +
    +
    +
    +
    +
    + + +
    + + + diff --git a/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/cheatsheet.template.html b/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/cheatsheet.template.html new file mode 100644 index 0000000000..98c2775647 --- /dev/null +++ b/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/cheatsheet.template.html @@ -0,0 +1,482 @@ + + +Q5 Max Cheatsheet + + + + + + +
    +
    +
    Keychron Q5 Max · ANSI encoder · via keymap
    +

    Cheatsheet

    + +
    + +
    +

    Layer maps

    +
    +
    +
    + +
    +
    +
    +
    Hover or focus a key for details.
    +
    + +
    +

    Key tricks

    +
    +
    + +
    +

    Autocorrect

    +

    + +
    +
    + +
    +

    Symbol chords

    +
      +
      +
      + + +
      +
      +
      +
      +
      + + +
      + + + 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 index b1806557bc..31d6f78404 100644 --- a/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/chord_unicode.c +++ b/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/chord_unicode.c @@ -29,6 +29,8 @@ static const chord_entry_t chord_table[] = { // ---- Math / Science ---------------------------------------------------- {"deg", "°"}, // degree sign U+00B0 + {"cels", "℃"}, // degree Celsius U+2103 (not "degc": would delay "deg") + {"fahr", "℉"}, // degree Fahrenheit U+2109 {"sqrt", "√"}, // square root U+221A {"sqrd", "²"}, // superscript 2 U+00B2 {"cubd", "³"}, // superscript 3 U+00B3 @@ -177,6 +179,31 @@ static const chord_entry_t chord_table[] = { {"wow", "😮"}, // face with open mouth U+1F62E {"zip", "🤐"}, // zipper-mouth face U+1F910 {"nerdy", "🤓"}, // nerd face U+1F913 + {"love", "😍"}, // heart eyes U+1F60D + {"kiss", "😘"}, // face blowing a kiss U+1F618 + {"blush", "😊"}, // smiling, smiling eyes U+1F60A + {"smirk", "😏"}, // smirking face U+1F60F + {"eyeroll","🙄"}, // face with rolling eyes U+1F644 + {"sweat", "😅"}, // grinning face w/ sweat U+1F605 + {"rofl", "🤣"}, // rolling on the floor U+1F923 + {"flip", "🙃"}, // upside-down face U+1F643 (not "upside": would delay "ups") + {"plead", "🥺"}, // pleading face U+1F97A + {"melt", "🫠"}, // melting face U+1FAE0 + {"salute", "🫡"}, // saluting face U+1FAE1 + {"hug", "🤗"}, // hugging face U+1F917 + {"shush", "🤫"}, // shushing face U+1F92B + {"mind", "🤯"}, // exploding head U+1F92F + {"mad", "😠"}, // angry face U+1F620 (not "angry": would delay "ang") + {"hot", "🥵"}, // hot face U+1F975 + {"cold", "🥶"}, // cold face U+1F976 + {"puke", "🤮"}, // face vomiting U+1F92E + {"party", "🥳"}, // partying face U+1F973 + {"cowboy", "🤠"}, // cowboy hat face U+1F920 + {"halo", "😇"}, // smiling face with halo U+1F607 (not "angel": would delay "ang") + {"devil", "😈"}, // smiling face w/ horns U+1F608 + {"horny", "😈"}, // slang alias + {"clown", "🤡"}, // clown face U+1F921 + {"ghost", "👻"}, // ghost U+1F47B // ---- Emoji : Gestures -------------------------------------------------- {"thup", "👍"}, // thumbs up U+1F44D @@ -187,6 +214,12 @@ static const chord_entry_t chord_table[] = { {"pray", "🙏"}, // folded hands U+1F64F {"ok", "👌"}, // ok hand U+1F44C {"point", "👉"}, // backhand index pointing right U+1F449 + {"facepalm","🤦"}, // person facepalming U+1F926 + {"flex", "💪"}, // flexed biceps U+1F4AA (not "muscle": would delay "mus") + {"praise", "🙌"}, // raising hands U+1F64C + {"shake", "🤝"}, // handshake U+1F91D + {"luck", "🤞"}, // crossed fingers U+1F91E + {"nail", "💅"}, // nail polish U+1F485 // ---- Emoji : Symbols --------------------------------------------------- // Variation selectors (U+FE0F) are omitted for cross-app compatibility; @@ -218,6 +251,13 @@ static const chord_entry_t chord_table[] = { {"lock", "🔒"}, // lock U+1F512 {"robot", "🤖"}, // robot U+1F916 {"alien", "👽"}, // alien U+1F47D + {"broken", "💔"}, // broken heart U+1F494 + {"sparkle","✨"}, // sparkles U+2728 + {"drops", "💦"}, // sweat droplets U+1F4A6 + {"cum", "💦"}, // slang alias + {"brain", "🧠"}, // brain U+1F9E0 + {"trophy", "🏆"}, // trophy U+1F3C6 + {"moai", "🗿"}, // moai U+1F5FF // ---- Emoji : Nature ---------------------------------------------------- {"sun", "☀"}, // black sun U+2600 @@ -229,6 +269,9 @@ static const chord_entry_t chord_table[] = { {"dog", "🐶"}, // dog face U+1F436 {"fox", "🦊"}, // fox face U+1F98A {"bear", "🐻"}, // bear face U+1F43B + {"goat", "🐐"}, // goat U+1F410 + {"unicorn","🦄"}, // unicorn U+1F984 + {"rnbw", "🌈"}, // rainbow U+1F308 (not "rainbow": would delay "rain") // ---- Emoji : Food & Objects -------------------------------------------- {"coffee","☕"}, // hot beverage U+2615 @@ -251,6 +294,20 @@ static const chord_entry_t chord_table[] = { {"trash", "🗑"}, // wastebasket U+1F5D1 {"clock", "🕐"}, // one o'clock U+1F550 {"hour", "⏳"}, // hourglass with flowing sand U+23F3 + {"eggplant","🍆"}, // eggplant U+1F346 + {"cock", "🍆"}, // slang alias + {"dick", "🍆"}, // slang alias + {"peach", "🍑"}, // peach U+1F351 + {"butt", "🍑"}, // slang alias + {"ass", "🍑"}, // slang alias + {"melon", "🍈"}, // melon U+1F348 + {"boobs", "🍈"}, // slang alias + {"taco", "🌮"}, // taco U+1F32E + {"pussy", "🌮"}, // slang alias + {"vag", "🌮"}, // slang alias + {"wine", "🍷"}, // wine glass U+1F377 + {"chair", "🪑"}, // chair U+1FA91 + {"lmao", "🪑"}, // slang alias (TikTok "I'm dead" laugh) }; // clang-format on @@ -275,7 +332,7 @@ static uint16_t chord_timer = 0; // last activity timestamp // whose output differs. static bool chord_disambig = false; static uint16_t chord_disambig_t = 0; -static uint8_t chord_disambig_i = 0; +static uint16_t chord_disambig_i = 0; // ============================================================================ // Internal helpers @@ -289,7 +346,7 @@ static void chord_reset(void) { chord_disambig = false; } -static void chord_output(uint8_t idx) { +static void chord_output(uint16_t idx) { send_unicode_string(chord_table[idx].out); chord_reset(); } @@ -308,7 +365,7 @@ static int chord_check(bool *is_prefix) { int exact_idx = -1; bool any_match = false; - for (uint8_t i = 0; i < CHORD_TABLE_LEN; i++) { + for (uint16_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 @@ -346,7 +403,7 @@ static int chord_check(bool *is_prefix) { // Walk again only for the is_prefix refinement. if (*is_prefix) { *is_prefix = false; - for (uint8_t i = 0; i < CHORD_TABLE_LEN; i++) { + for (uint16_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; @@ -492,7 +549,7 @@ bool process_chord(uint16_t keycode, keyrecord_t *record) { // Start the disambiguation timer; output fires when it expires. chord_disambig = true; chord_disambig_t = timer_read(); - chord_disambig_i = (uint8_t)idx; + chord_disambig_i = (uint16_t)idx; } // idx == -1: only prefix match(es) so far → keep collecting. diff --git a/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/gen_cheatsheet.py b/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/gen_cheatsheet.py new file mode 100644 index 0000000000..33dfbea3eb --- /dev/null +++ b/keyboards/keychron/q5_max/ansi_encoder/keymaps/via/gen_cheatsheet.py @@ -0,0 +1,248 @@ +#!/usr/bin/env python3 +"""Generate cheatsheet.html from chord_unicode.c, keymap.c, autocorrect_dictionary.txt and info.json. + +Run from anywhere: python3 gen_cheatsheet.py +The output is a single self-contained HTML page (layer maps, key tricks, autocorrect and chord lookup). +""" + +import json +import re +from pathlib import Path + +HERE = Path(__file__).resolve().parent +OUT = HERE / "cheatsheet.html" + +# --------------------------------------------------------------------------- +# Chord table +# --------------------------------------------------------------------------- + + +def parse_chords(): + src = (HERE / "chord_unicode.c").read_text() + table = src[src.index("chord_table[] = {"):src.index("// clang-format on")] + entries, cat = [], "Other" + for line in table.splitlines(): + m = re.match(r"\s*// ---- (.+?) -+\s*$", line) + if m: + cat = m.group(1).replace("Emoji : ", "Emoji · ") + continue + m = re.match(r'\s*\{"([^"]+)",\s*"([^"]+)"\},\s*(?://\s*(.*))?', line) + if m: + seq, out, comment = m.group(1), m.group(2), (m.group(3) or "") + name = re.sub(r"\s*U\+[0-9A-Fa-f]+.*$", "", comment).strip() + entries.append({"seq": seq, "out": out, "cat": cat, "name": name}) + return entries + + +def simulate(seq, entries): + """Replay process_chord()/chord_check() for one typed sequence. + + Returns (status, fired_at): 'ok' fires on the last letter, 'wait' needs the + CHORD_DISAMBIG_MS pause (or Enter), 'early' fires before the sequence ends. + """ + for k in range(1, len(seq) + 1): + buf = seq[:k] + exact = next((e for e in entries if e["seq"] == buf), None) + if exact is None: + continue + conflict = any(e["seq"].startswith(buf) and len(e["seq"]) > k and e["out"] != exact["out"] for e in entries) + if not conflict: + return ("ok" if k == len(seq) else "early"), buf + if k == len(seq): + return "wait", buf + return "ok", seq + + +def build_chords(): + entries = parse_chords() + groups = {} + for e in entries: + g = groups.setdefault(e["out"], {"out": e["out"], "cat": e["cat"], "name": "", "seqs": []}) + if e["name"] and "alias" not in e["name"] and not g["name"]: + g["name"] = re.sub(r"\s*\(.*\)$", "", e["name"]) + status, at = simulate(e["seq"], entries) + g["seqs"].append({"s": e["seq"], "st": status, "at": at}) + for g in groups.values(): + g["cp"] = ["U+%04X" % ord(c) for c in g["out"]] + g["seqs"].sort(key=lambda s: (s["st"] == "early", len(s["s"]))) + return list(groups.values()) + + +# --------------------------------------------------------------------------- +# Keymap +# --------------------------------------------------------------------------- + +LAYER_INFO = { + "BASE": ("Base", "#8a94a0", "ESC LED off"), + "FN1": ("Fn1", "#0080ff", "ESC LED blue"), + "FN2": ("Fn2", "#00dc50", "ESC LED green"), + "FN3": ("Fn3", "#ff7800", "ESC LED orange"), + "FN4": ("Fn4", "#b400ff", "ESC LED purple"), + "KEEB_CTL": ("Control", "#ff2020", "ESC LED red"), +} + +SHIFTED = { + "KC_GRV": ("~", "`"), "KC_1": ("!", "1"), "KC_2": ("@", "2"), "KC_3": ("#", "3"), "KC_4": ("$", "4"), + "KC_5": ("%", "5"), "KC_6": ("^", "6"), "KC_7": ("&", "7"), "KC_8": ("*", "8"), "KC_9": ("(", "9"), + "KC_0": (")", "0"), "KC_MINS": ("_", "-"), "KC_EQL": ("+", "="), "KC_LBRC": ("{", "["), + "KC_RBRC": ("}", "]"), "KC_BSLS": ("|", "\\"), "KC_SCLN": (":", ";"), "KC_QUOT": ('"', "'"), + "KC_COMM": ("<", ","), "KC_DOT": (">", "."), "KC_SLSH": ("?", "/"), +} + +# keycode: (label, category, description) +LABELS = { + "KC_ESC": ("Esc", "mod", "Escape"), "KC_TAB": ("Tab", "mod", "Tab"), "KC_BSPC": ("Backspace", "mod", "Backspace"), + "KC_ENT": ("Enter", "mod", "Enter"), "KC_SPC": ("", "mod", "Space"), "KC_DEL": ("Del", "mod", "Delete"), + "KC_LSFT": ("Shift", "mod", "Left Shift"), "KC_RSFT": ("Shift", "mod", "Right Shift"), + "KC_LCTL": ("Ctrl", "mod", "Left Control"), "KC_RCTL": ("Ctrl", "mod", "Right Control"), + "KC_LGUI": ("Super", "mod", "Left GUI / Super"), "KC_LALT": ("Alt", "mod", "Left Alt"), + "KC_PSCR": ("PrtSc", "mod", "Print Screen"), "KC_CALC": ("Calc", "media", "Calculator"), + "KC_FIND": ("Find", "media", "Find"), "KC_PGUP": ("PgUp", "mod", "Page Up"), "KC_PGDN": ("PgDn", "mod", "Page Down"), + "KC_HOME": ("Home", "mod", "Home"), "KC_END": ("End", "mod", "End"), + "KC_UP": ("↑", "mod", "Up"), "KC_DOWN": ("↓", "mod", "Down"), "KC_LEFT": ("←", "mod", "Left"), "KC_RGHT": ("→", "mod", "Right"), + "KC_NUM": ("Num", "mod", "Num Lock"), "KC_PSLS": ("/", "", "Keypad /"), "KC_PAST": ("*", "", "Keypad *"), + "KC_PMNS": ("−", "", "Keypad −"), "KC_PPLS": ("+", "", "Keypad +"), "KC_PDOT": (".", "", "Keypad ."), + "KC_PENT": ("Enter", "mod", "Keypad Enter"), + "KC_MPLY": ("⏯", "media", "Play / pause"), "KC_MPRV": ("⏮", "media", "Previous track"), + "KC_MNXT": ("⏭", "media", "Next track"), "KC_MUTE": ("Mute", "media", "Mute"), + "KC_VOLD": ("Vol −", "media", "Volume down"), "KC_VOLU": ("Vol +", "media", "Volume up"), + "KC_MRWD": ("Rewind", "media", "Rewind"), "KC_MFFD": ("Fast fwd", "media", "Fast forward"), + "KC_BRID": ("Bright −", "media", "Screen brightness down"), "KC_BRIU": ("Bright +", "media", "Screen brightness up"), + "KC_MCTRL": ("Mission", "media", "Mission Control (macOS)"), "KC_LNPAD": ("Launch", "media", "Launchpad (macOS)"), + "KC_TASK": ("Tasks", "media", "Task view"), "KC_FILE": ("Files", "media", "File explorer"), + "KC_SLEP": ("Sleep", "sys", "System sleep"), "KC_PWR": ("Power", "sys", "System power"), + "RGB_TOG": ("LED ⏻", "rgb", "Backlight on/off"), "RGB_MOD": ("Effect →", "rgb", "Next backlight effect"), + "RGB_RMOD": ("Effect ←", "rgb", "Previous backlight effect"), "RGB_VAI": ("LED +", "rgb", "Backlight brighter"), + "RGB_VAD": ("LED −", "rgb", "Backlight dimmer"), "RGB_HUI": ("Hue +", "rgb", "Backlight hue up"), + "RGB_HUD": ("Hue −", "rgb", "Backlight hue down"), "RGB_SAI": ("Sat +", "rgb", "Backlight saturation up"), + "RGB_SAD": ("Sat −", "rgb", "Backlight saturation down"), "RGB_SPI": ("Speed +", "rgb", "Effect speed up"), + "RGB_SPD": ("Speed −", "rgb", "Effect speed down"), + "BT_HST1": ("BT 1", "sys", "Bluetooth host 1"), "BT_HST2": ("BT 2", "sys", "Bluetooth host 2"), + "BT_HST3": ("BT 3", "sys", "Bluetooth host 3"), "P2P4G": ("2.4 GHz", "sys", "2.4 GHz dongle"), + "BAT_LVL": ("Battery", "sys", "Show battery level on LEDs"), "NK_TOGG": ("NKRO", "sys", "Toggle N-key rollover"), + "QK_CLEAR_EEPROM": ("Clear EE", "danger", "Clear EEPROM — wipes all saved settings"), + "KC_MS_WH_UP": ("Wheel ↑", "mouse", "Scroll up"), "KC_MS_WH_DOWN": ("Wheel ↓", "mouse", "Scroll down"), + "KC_MS_UP": ("Ms ↑", "mouse", "Mouse up"), "KC_MS_DOWN": ("Ms ↓", "mouse", "Mouse down"), + "KC_MS_LEFT": ("Ms ←", "mouse", "Mouse left"), "KC_MS_RIGHT": ("Ms →", "mouse", "Mouse right"), + "KC_MS_BTN1": ("Click L", "mouse", "Left click"), "KC_MS_BTN2": ("Click R", "mouse", "Right click"), + "KC_MS_BTN3": ("Click M", "mouse", "Middle click"), + "CAPS_MOD": ("Esc ⁄ Ctrl", "custom", "Tap Esc · hold Ctrl · Shift+tap Caps Lock · Alt+tap Caps Word · Super+tap toggle Autocorrect"), + "TD(TD_HOME_END)": ("Home ⁄ End", "custom", "Tap Home · double-tap End"), + "CHORD_KEY": ("Chord", "chord", "Chord entry — type a sequence to output a symbol"), + "LCK_BASE": ("Unlock", "custom", "Clear layer locks, return to Base"), + "ALT_TAB_FWD": ("Alt-Tab →", "custom", "Cycle windows forward"), "ALT_TAB_BWD": ("Alt-Tab ←", "custom", "Cycle windows back"), +} +LAYER_OPS = { + "TT": ("", "Tap-toggle {n}: hold for momentary, tap 3× to toggle"), + "TG": ("⇄ ", "Toggle {n} on/off"), + "TO": ("→ ", "Switch to {n} (clears others)"), + "OSL": ("¹ ", "One-shot {n}: applies to the next key only"), + "MO": ("", "Hold for {n}"), +} + + +def describe(kc): + if kc == "_______": + return {"t": 1} + m = re.fullmatch(r"(TT|TG|TO|OSL|MO)\((\w+)\)", kc) + if m: + op, layer = m.groups() + name = LAYER_INFO[layer][0] + prefix, desc = LAYER_OPS[op] + return {"l": prefix + name, "c": "layer", "to": layer, "d": desc.format(n=name)} + m = re.fullmatch(r"LCK_(FN\d|CTL)", kc) + if m: + layer = "KEEB_CTL" if m.group(1) == "CTL" else m.group(1) + name = LAYER_INFO[layer][0] + return {"l": "🔒 " + name, "c": "layer", "to": layer, "d": f"Lock {name} on (press again to unlock)"} + if kc in SHIFTED: + top, bottom = SHIFTED[kc] + return {"l": bottom, "s": top, "c": "", "d": kc} + if kc in LABELS: + label, cat, desc = LABELS[kc] + return {"l": label, "c": cat, "d": desc} + m = re.fullmatch(r"KC_([A-Z])", kc) + if m: + return {"l": m.group(1), "c": "", "d": kc} + m = re.fullmatch(r"KC_(\d)", kc) + if m: + return {"l": m.group(1), "c": "", "d": kc} + m = re.fullmatch(r"KC_P(\d)", kc) + if m: + return {"l": m.group(1), "c": "", "d": "Keypad " + m.group(1)} + m = re.fullmatch(r"KC_F(\d+)", kc) + if m: + return {"l": "F" + m.group(1), "c": "", "d": "F" + m.group(1)} + return {"l": kc.replace("KC_", ""), "c": "", "d": kc} + + +def split_args(body): + args, depth, cur = [], 0, "" + for ch in body: + if ch == "(": + depth += 1 + elif ch == ")": + depth -= 1 + if ch == "," and depth == 0: + args.append(cur.strip()) + cur = "" + else: + cur += ch + if cur.strip(): + args.append(cur.strip()) + return args + + +def build_layers(): + src = (HERE / "keymap.c").read_text() + info = json.loads((HERE / "../../info.json").read_text()) + geom = info["layouts"]["LAYOUT_ansi_101"]["layout"] + layers = [] + for m in re.finditer(r"\[(\w+)\]\s*=\s*LAYOUT_ansi_101\(", src): + i, depth = m.end(), 1 + while depth: + depth += {"(": 1, ")": -1}.get(src[i], 0) + i += 1 + codes = split_args(src[m.end():i - 1]) + assert len(codes) == len(geom), f"{m.group(1)}: {len(codes)} keycodes vs {len(geom)} positions" + keys = [] + for kc, g in zip(codes, geom): + k = describe(kc) + k.update(kc=kc, x=g["x"], y=g["y"], w=g.get("w", 1), h=g.get("h", 1)) + keys.append(k) + name = m.group(1) + layers.append({"id": name, "name": LAYER_INFO[name][0], "led": LAYER_INFO[name][1], "ledNote": LAYER_INFO[name][2], "keys": keys}) + + enc = dict((m.group(1), [describe(m.group(2))["l"], describe(m.group(3))["l"]]) + for m in re.finditer(r"\[(\w+)\]\s*=\s*\{ENCODER_CCW_CW\((\w+),\s*(\w+)\)\}", src)) + for layer in layers: + layer["enc"] = enc.get(layer["id"]) + # Every key on another layer that leads here. + layer["via"] = [{"from": other["name"], "l": k["l"], "d": k["d"]} + for other in layers if other is not layer + for k in other["keys"] if k.get("to") == layer["id"]] + return layers + + +def main(): + header = (HERE / "chord_unicode.h").read_text() + consts = {n: int(re.search(rf"#define {n} (\d+)", header).group(1)) + for n in ("CHORD_MAX_LEN", "CHORD_TIMEOUT_MS", "CHORD_DISAMBIG_MS")} + keymap = (HERE / "keymap.c").read_text() + config = (HERE / "config.h").read_text() + (HERE / "../../config.h").read_text() + keys = { + "TAPPING_TERM": int(re.search(r"#define TAPPING_TERM (\d+)", config).group(1)), + "TAPPING_TOGGLE": int(re.search(r"#define TAPPING_TOGGLE (\d+)", config).group(1)), + "ALT_TAB_TIMEOUT": int(re.search(r"#define ALT_TAB_TIMEOUT (\d+)", keymap).group(1)), + "HOME_END_MS": int(re.search(r"case TD\(TD_HOME_END\):\s*return (\d+)", keymap).group(1)), + } + autocorrect = re.findall(r"^(\S+)\s*->\s*(.+?)\s*$", (HERE / "autocorrect_dictionary.txt").read_text(), re.M) + data = {"chords": build_chords(), "layers": build_layers(), "consts": consts, "keys": keys, "autocorrect": autocorrect} + payload = json.dumps(data, ensure_ascii=False, separators=(",", ":")).replace("