Merge pull request 'feat(q5_max): expand chord table and add cheatsheet generator' (#25) from feat/q5-chord-expansion-cheatsheet into main
Reviewed-on: #25
This commit was merged in pull request #25.
This commit is contained in:
+399
@@ -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
|
||||
|
||||
@@ -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.
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,482 @@
|
||||
<!-- Template for gen_cheatsheet.py — edit here, then regenerate cheatsheet.html -->
|
||||
<meta charset="utf-8">
|
||||
<title>Q5 Max Cheatsheet</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible+Next:wght@400;600;700&family=JetBrains+Mono:wght@500;700&family=Noto+Sans+Math&family=Noto+Sans+Symbols+2&display=swap">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #e8ebee;
|
||||
--panel: #f5f6f8;
|
||||
--ink: #15191e;
|
||||
--muted: #5b6570;
|
||||
--line: #d1d6dc;
|
||||
--cap: #ffffff;
|
||||
--cap-edge: #bfc6ce;
|
||||
--accent: #0a66e8;
|
||||
--accent-ink: #ffffff;
|
||||
--warn: #a35a00;
|
||||
--warn-bg: #fdf0dc;
|
||||
--danger: #c62828;
|
||||
--c-media: #0f8a7e;
|
||||
--c-rgb: #b0369c;
|
||||
--c-mouse: #3d8b2f;
|
||||
--c-sys: #6450c8;
|
||||
--c-custom: #d6336c;
|
||||
--c-chord: #b8860b;
|
||||
--c-mod: #5b6570;
|
||||
--sans: "Atkinson Hyperlegible Next", "Atkinson Hyperlegible", system-ui, sans-serif;
|
||||
--mono: "JetBrains Mono", ui-monospace, "SFMono-Regular", Menlo, monospace;
|
||||
--glyph: "Noto Sans Math", "Noto Sans Symbols 2", "Noto Color Emoji", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", system-ui, sans-serif;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) {
|
||||
color-scheme: dark;
|
||||
--bg: #111418; --panel: #191d22; --ink: #e6e9ed; --muted: #8e97a2; --line: #2a3038;
|
||||
--cap: #252b32; --cap-edge: #0a0c0f; --accent: #4f9bff; --accent-ink: #0b1220;
|
||||
--warn: #f0b35c; --warn-bg: #33270f; --danger: #ff6b6b;
|
||||
--c-media: #3cc7b8; --c-rgb: #e070cf; --c-mouse: #7ccc6c; --c-sys: #a497ff; --c-custom: #ff5c8f; --c-chord: #e0b040; --c-mod: #8e97a2;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
--bg: #111418; --panel: #191d22; --ink: #e6e9ed; --muted: #8e97a2; --line: #2a3038;
|
||||
--cap: #252b32; --cap-edge: #0a0c0f; --accent: #4f9bff; --accent-ink: #0b1220;
|
||||
--warn: #f0b35c; --warn-bg: #33270f; --danger: #ff6b6b;
|
||||
--c-media: #3cc7b8; --c-rgb: #e070cf; --c-mouse: #7ccc6c; --c-sys: #a497ff; --c-custom: #ff5c8f; --c-chord: #e0b040; --c-mod: #8e97a2;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
body { background: var(--bg); color: var(--ink); font: 16px/1.5 var(--sans); }
|
||||
.wrap { max-width: 1240px; margin: 0 auto; padding: 32px 20px 64px; display: grid; gap: 40px; }
|
||||
h1, h2 { font-family: var(--mono); letter-spacing: -0.02em; margin: 0; text-wrap: balance; }
|
||||
h1 { font-size: clamp(1.75rem, 4vw, 2.5rem); line-height: 1.1; }
|
||||
h2 { font-size: 1.35rem; }
|
||||
p { margin: 0; }
|
||||
.muted { color: var(--muted); }
|
||||
kbd {
|
||||
font: 700 0.8em/1 var(--mono); display: inline-block; min-width: 1.7em; text-align: center;
|
||||
padding: 0.35em 0.45em 0.3em; border-radius: 5px; background: var(--cap); color: var(--ink);
|
||||
border: 1px solid var(--line); border-bottom: 3px solid var(--cap-edge);
|
||||
}
|
||||
button { font: inherit; color: inherit; }
|
||||
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||
|
||||
/* ---- Header ---------------------------------------------------------- */
|
||||
header { display: grid; gap: 18px; }
|
||||
.eyebrow { font: 600 0.75rem/1 var(--mono); letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); }
|
||||
nav { display: flex; gap: 16px; flex-wrap: wrap; font-size: 0.95rem; }
|
||||
nav a { color: var(--accent); text-decoration: none; font-weight: 600; }
|
||||
nav a:hover { text-decoration: underline; }
|
||||
.how { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 12px; list-style: none; margin: 0; padding: 0; }
|
||||
.how li { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 14px 16px; display: grid; gap: 6px; align-content: start; }
|
||||
.how b { font-family: var(--mono); font-size: 0.8rem; color: var(--muted); letter-spacing: 0.06em; text-transform: uppercase; }
|
||||
.how p { font-size: 0.95rem; }
|
||||
|
||||
/* ---- Search ---------------------------------------------------------- */
|
||||
.finder { position: sticky; top: env(safe-area-inset-top, 0px); z-index: 5; background: var(--bg); padding-block: 12px; display: grid; gap: 10px; border-bottom: 1px solid var(--line); }
|
||||
.searchrow { display: flex; gap: 12px; align-items: center; }
|
||||
.search { flex: 1; display: flex; align-items: center; gap: 10px; background: var(--cap); border: 1.5px solid var(--line); border-radius: 10px; padding: 0 14px; }
|
||||
.search:focus-within { border-color: var(--accent); }
|
||||
.search input { flex: 1; min-width: 0; border: 0; outline: 0; background: transparent; color: var(--ink); font: 500 1.15rem var(--mono); padding: 12px 0; }
|
||||
.search input::placeholder { color: var(--muted); font-family: var(--sans); font-weight: 400; }
|
||||
.count { font: 500 0.85rem var(--mono); color: var(--muted); white-space: nowrap; font-variant-numeric: tabular-nums; }
|
||||
.chips { display: flex; gap: 6px; overflow-x: auto; padding-bottom: 2px; scrollbar-width: thin; }
|
||||
.chip { flex: none; border: 1px solid var(--line); background: var(--panel); border-radius: 99px; padding: 4px 11px; font-size: 0.85rem; cursor: pointer; white-space: nowrap; }
|
||||
.chip[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }
|
||||
.chip span { opacity: 0.65; font-variant-numeric: tabular-nums; margin-left: 4px; }
|
||||
|
||||
/* ---- Results --------------------------------------------------------- */
|
||||
.results { display: grid; grid-template-columns: repeat(auto-fill, minmax(168px, 1fr)); gap: 10px; }
|
||||
.tile {
|
||||
text-align: left; cursor: pointer; background: var(--panel); border: 1px solid var(--line); border-radius: 10px;
|
||||
padding: 12px 14px 12px; display: grid; grid-template-rows: auto auto 1fr auto; gap: 6px; min-height: 100%;
|
||||
}
|
||||
.tile:hover { border-color: var(--accent); }
|
||||
.tile .top { display: flex; justify-content: space-between; align-items: start; gap: 6px; }
|
||||
.glyph { font: 2.3rem/1.15 var(--glyph); }
|
||||
.cp { font: 500 0.7rem var(--mono); color: var(--muted); padding-top: 4px; }
|
||||
.seq { display: flex; gap: 3px; flex-wrap: wrap; }
|
||||
.seq kbd { font-size: 0.85rem; }
|
||||
.name { font-size: 0.85rem; color: var(--muted); line-height: 1.3; }
|
||||
.alts { font: 500 0.78rem var(--mono); color: var(--muted); display: flex; flex-wrap: wrap; gap: 4px 10px; }
|
||||
.alts s { opacity: 0.7; }
|
||||
.flag { justify-self: start; font: 600 0.72rem/1.2 var(--sans); color: var(--warn); background: var(--warn-bg); padding: 3px 7px; border-radius: 5px; }
|
||||
.empty { grid-column: 1 / -1; padding: 40px 0; text-align: center; color: var(--muted); }
|
||||
.toast {
|
||||
position: fixed; left: 50%; bottom: calc(24px + env(safe-area-inset-bottom, 0px)); transform: translateX(-50%);
|
||||
background: var(--ink); color: var(--bg); padding: 10px 18px; border-radius: 99px; font-weight: 600; z-index: 10;
|
||||
}
|
||||
|
||||
/* ---- Keyboard -------------------------------------------------------- */
|
||||
.layers { display: grid; gap: 16px; }
|
||||
.tabs { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.tab { display: inline-flex; align-items: center; gap: 8px; border: 1px solid var(--line); background: var(--panel); border-radius: 8px; padding: 7px 14px; cursor: pointer; font-weight: 600; }
|
||||
.tab i { width: 10px; height: 10px; border-radius: 50%; background: var(--led); box-shadow: 0 0 8px var(--led); }
|
||||
.tab[aria-selected="true"] { border-color: var(--led); background: color-mix(in oklab, var(--led) 14%, var(--panel)); }
|
||||
.layer-meta { display: grid; gap: 4px; font-size: 0.95rem; }
|
||||
.layer-meta b { font-weight: 600; }
|
||||
.opts { display: flex; flex-wrap: wrap; gap: 8px 20px; align-items: center; font-size: 0.9rem; }
|
||||
.opts label { display: inline-flex; gap: 6px; align-items: center; cursor: pointer; }
|
||||
.legend { display: flex; flex-wrap: wrap; gap: 6px 14px; }
|
||||
.legend span { display: inline-flex; align-items: center; gap: 6px; color: var(--muted); }
|
||||
.legend span::before { content: ""; width: 12px; height: 12px; border-radius: 3px; background: color-mix(in oklab, var(--cat) 40%, var(--cap)); border: 1px solid var(--cat); }
|
||||
.legend span.solid::before { background: var(--cat); }
|
||||
.legend span.multi::before { background: conic-gradient(var(--stops)); border-color: var(--line); }
|
||||
|
||||
.board { overflow-x: auto; padding-bottom: 6px; }
|
||||
.case {
|
||||
/* White Q5 Max with eggshell white keycaps: fixed physical colours, same in both themes. */
|
||||
--egg: #f1f0ea; --egg-edge: #c9c7bd; --grey: #a6a898; --grey-edge: #7b7d6f; --green: #5f9a78; --green-edge: #3f6e53; --legend: #34383a;
|
||||
min-width: 780px; background: linear-gradient(170deg, #f4f4f1, #d8d9d5); border-radius: 18px; padding: 1.4%;
|
||||
box-shadow: inset 0 1px 0 #fff, inset 0 -2px 0 rgba(0,0,0,.08), 0 12px 30px -16px rgba(0,0,0,.45);
|
||||
}
|
||||
.kb { position: relative; container-type: inline-size; aspect-ratio: 20.5 / 6.25; background: #e4e5e3; border-radius: 8px; box-shadow: inset 0 1px 3px rgba(0,0,0,.12); }
|
||||
.key { position: absolute; padding: 0.14%; }
|
||||
.key > div {
|
||||
--cat: var(--c-mod); --kcap: var(--egg); --kedge: var(--egg-edge);
|
||||
width: 100%; height: 100%; border-radius: 0.55cqw; background: var(--kcap); color: var(--legend);
|
||||
border: 1px solid rgba(0,0,0,.1); border-bottom-width: 0.35cqw; border-bottom-color: var(--kedge);
|
||||
display: flex; flex-direction: column; justify-content: space-between; padding: 0.35cqw 0.45cqw;
|
||||
font: 600 1.02cqw/1.1 var(--sans); overflow: hidden; transition: opacity .15s;
|
||||
}
|
||||
.key .s { opacity: 0.6; }
|
||||
.key .l { word-break: break-word; }
|
||||
/* The chord key is the one key the whole page revolves around: a solid fill instead of a tint. */
|
||||
.key.tint.solid > div { background: var(--cat); color: #241c05; border-bottom-color: color-mix(in oklab, var(--cat) 60%, #000); }
|
||||
.key.grey > div { --kcap: var(--grey); --kedge: var(--grey-edge); }
|
||||
.key.green > div { --kcap: var(--green); --kedge: var(--green-edge); color: #111; }
|
||||
.key.knob > div { border-radius: 50%; align-items: center; justify-content: center; text-align: center; background: radial-gradient(circle at 35% 30%, #ffffff, #c4c6c4); color: var(--legend); border-color: #a4a7a5; }
|
||||
.key.tint > div { background: color-mix(in oklab, var(--cat) 26%, var(--kcap)); color: var(--legend); border-color: color-mix(in oklab, var(--cat) 65%, transparent); border-bottom-color: color-mix(in oklab, var(--cat) 55%, var(--kedge)); }
|
||||
.key.trans > div { background: transparent; border: 1px dashed #9ba09d; color: #858b88; font-weight: 400; }
|
||||
.key.same > div { opacity: 0.32; }
|
||||
.key:hover > div, .key:focus-visible > div { opacity: 1; outline: 2px solid var(--accent); outline-offset: -1px; }
|
||||
.key:focus-visible { outline: none; }
|
||||
.keyinfo { min-height: 3em; font-size: 0.95rem; display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap; }
|
||||
.keyinfo code { font: 500 0.85rem var(--mono); background: var(--panel); border: 1px solid var(--line); padding: 2px 6px; border-radius: 5px; }
|
||||
.notes { display: grid; grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); gap: 22px 28px; font-size: 0.92rem; }
|
||||
.notes > div { border-top: 1px solid var(--line); padding-top: 10px; display: grid; gap: 6px; align-content: start; }
|
||||
.notes b { font-family: var(--mono); font-size: 0.8rem; letter-spacing: 0.05em; text-transform: uppercase; color: var(--muted); }
|
||||
|
||||
.stack { display: grid; gap: 14px; }
|
||||
.stack > .search { max-width: 520px; }
|
||||
.acres { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 4px 20px; font: 500 0.9rem/1.6 var(--mono); }
|
||||
.acres div { display: flex; gap: 8px; min-width: 0; }
|
||||
.acres .typo { color: var(--danger); text-decoration: line-through; text-decoration-thickness: 1px; }
|
||||
.acres .b { color: var(--muted); opacity: 0.6; }
|
||||
.acres p { grid-column: 1 / -1; font-family: var(--sans); color: var(--muted); }
|
||||
.notes p { color: var(--muted); font-size: 0.85rem; }
|
||||
.notes p.lead { color: var(--ink); font-size: inherit; }
|
||||
.notes dl { display: grid; grid-template-columns: auto 1fr; gap: 5px 14px; margin: 2px 0 4px; align-items: center; }
|
||||
.notes dt { white-space: nowrap; display: flex; align-items: center; gap: 4px; }
|
||||
.notes dd { margin: 0; }
|
||||
.dot { width: 10px; height: 10px; border-radius: 50%; background: var(--led); border: 1px solid var(--line); margin-right: 4px; }
|
||||
footer { font-size: 0.85rem; color: var(--muted); }
|
||||
@media (prefers-reduced-motion: reduce) { * { transition: none !important; } }
|
||||
</style>
|
||||
|
||||
<div class="wrap">
|
||||
<header>
|
||||
<div class="eyebrow">Keychron Q5 Max · ANSI encoder · via keymap</div>
|
||||
<h1>Cheatsheet</h1>
|
||||
<nav><a href="#layers">Layer maps</a><a href="#tricks">Key tricks</a><a href="#autocorrect">Autocorrect</a><a href="#symbols">Symbol chords</a></nav>
|
||||
</header>
|
||||
|
||||
<section id="layers" class="layers">
|
||||
<h2>Layer maps</h2>
|
||||
<div class="tabs" id="tabs" role="tablist" aria-label="Layers"></div>
|
||||
<div class="layer-meta" id="layermeta"></div>
|
||||
<div class="opts">
|
||||
<label for="dim"><input type="checkbox" id="dim" checked> Fade keys identical to Base</label>
|
||||
<div class="legend" id="legend"></div>
|
||||
</div>
|
||||
<div class="board"><div class="case"><div class="kb" id="kb"></div></div></div>
|
||||
<div class="keyinfo" id="keyinfo"><span class="muted">Hover or focus a key for details.</span></div>
|
||||
</section>
|
||||
|
||||
<section id="tricks" class="stack">
|
||||
<h2>Key tricks</h2>
|
||||
<div class="notes" id="tricklist"></div>
|
||||
</section>
|
||||
|
||||
<section id="autocorrect" class="stack">
|
||||
<h2>Autocorrect</h2>
|
||||
<p class="muted" id="acintro"></p>
|
||||
<label class="search" for="ac">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-4-4"/></svg>
|
||||
<input id="ac" type="search" autocomplete="off" spellcheck="false" placeholder="Check a word — e.g. teh, recieve, definately" aria-label="Search autocorrect dictionary">
|
||||
</label>
|
||||
<div class="acres" id="acres" aria-live="polite"></div>
|
||||
</section>
|
||||
|
||||
<section id="symbols" class="stack">
|
||||
<h2>Symbol chords</h2>
|
||||
<ol class="how" id="how"></ol>
|
||||
<div class="finder">
|
||||
<div class="searchrow">
|
||||
<label class="search" for="q">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-4-4"/></svg>
|
||||
<input id="q" type="search" autocomplete="off" spellcheck="false" placeholder="Search by name, sequence, or symbol — e.g. arrow, ≤, gree…" aria-label="Search symbols">
|
||||
<kbd title="Press / to focus">/</kbd>
|
||||
</label>
|
||||
<span class="count" id="count"></span>
|
||||
</div>
|
||||
<div class="chips" id="chips" role="group" aria-label="Filter by category"></div>
|
||||
</div>
|
||||
<div class="results" id="results" aria-live="polite"></div>
|
||||
</section>
|
||||
|
||||
<footer>Generated from <code>chord_unicode.c</code>, <code>keymap.c</code>, <code>autocorrect_dictionary.txt</code> and <code>info.json</code> by <code>gen_cheatsheet.py</code>. Click a symbol to copy it.</footer>
|
||||
</div>
|
||||
<div class="toast" id="toast" hidden></div>
|
||||
|
||||
<script>
|
||||
const DATA = /*DATA*/null;
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
||||
const C = DATA.consts;
|
||||
|
||||
// ---- How-to ---------------------------------------------------------------
|
||||
$("how").innerHTML = `
|
||||
<li><b>1 · Start</b><p>Hold <kbd>Fn1</kbd> and press <kbd>Alt</kbd> (left). Keep holding, or let go — you then have ${C.CHORD_TIMEOUT_MS / 1000} s to begin.</p></li>
|
||||
<li><b>2 · Type</b><p>Type the letters. The symbol appears as soon as the sequence is unique. Up to ${C.CHORD_MAX_LEN} letters.</p></li>
|
||||
<li><b>3 · Adjust</b><p><kbd>Enter</kbd> confirms now, <kbd>Backspace</kbd> removes a letter, <kbd>Esc</kbd> cancels. Output uses Linux <kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>U</kbd> entry.</p></li>`;
|
||||
|
||||
// ---- Key tricks -------------------------------------------------------------
|
||||
const K = DATA.keys;
|
||||
// Each card: title, rows of [what you do, what happens], optional footnote.
|
||||
const dot = (c) => `<i class="dot" style="--led:${c}"></i>`;
|
||||
const LEDS = Object.fromEntries(DATA.layers.map((l) => [l.name, l.led]));
|
||||
const tricks = [
|
||||
["Caps key", [
|
||||
["Tap", "<kbd>Esc</kbd>"],
|
||||
["Hold", "<kbd>Ctrl</kbd>"],
|
||||
["<kbd>Shift</kbd> + tap", "Caps Lock"],
|
||||
["<kbd>Alt</kbd> + tap", "Caps Word"],
|
||||
["<kbd>Super</kbd> + tap", "Autocorrect on / off"],
|
||||
], "In Caps Word, holding Shift types lowercase."],
|
||||
["Caps key light", [
|
||||
[dot("#00c800") + "Green", "Caps Word on"],
|
||||
[dot("#9600ff") + "Purple", "Autocorrect off"],
|
||||
[dot("#ffffff") + "White", "Caps Lock on"],
|
||||
]],
|
||||
["Home / End", [
|
||||
["Tap", "<kbd>Home</kbd>"],
|
||||
["Double-tap", "<kbd>End</kbd>"],
|
||||
], `The key right of Enter. Double-tap within ${K.HOME_END_MS} ms. On Fn1 it is a plain End.`],
|
||||
["Fn keys", [
|
||||
["Hold <kbd>Fn1</kbd> / <kbd>Fn2</kbd>", "Layer while held"],
|
||||
[`Tap ${K.TAPPING_TOGGLE}×`, "Layer stays on"],
|
||||
], `Hold time for dual-role keys: ${K.TAPPING_TERM} ms.`],
|
||||
["Layer locks", [
|
||||
["<kbd>1</kbd> – <kbd>4</kbd>", "Lock Fn1 – Fn4"],
|
||||
["<kbd>5</kbd>", "Lock Control"],
|
||||
["<kbd>0</kbd>", "Unlock, back to Base"],
|
||||
["Locked number again", "Unlock, back to Base"],
|
||||
], "Once locked on Fn1 – Fn4 the number row does this directly — no Fn hold needed. From Control, press 0.",
|
||||
"From Base, hold <kbd>Fn1</kbd> or <kbd>Fn2</kbd>, then press:"],
|
||||
["Escape hatch", [
|
||||
["<kbd>,</kbd> <kbd>.</kbd> <kbd>/</kbd> together", "Clear locks, back to Base"],
|
||||
], "Works on every layer, even where those keys are remapped."],
|
||||
["Knob", [
|
||||
["Base", "Volume"],
|
||||
["Fn2", "Window switcher (Alt-Tab)"],
|
||||
["Fn1 · Fn3 · Fn4", "Rewind / fast-forward"],
|
||||
["Control", "Backlight brightness"],
|
||||
], `Window switcher: Alt releases ${K.ALT_TAB_TIMEOUT} ms after the last click.`],
|
||||
["Esc light", [
|
||||
[dot("transparent") + "Off", "Base"],
|
||||
...["Fn1", "Fn2", "Fn3", "Fn4", "Control"].map((n) => [dot(LEDS[n]) + ({ Fn1: "Blue", Fn2: "Green", Fn3: "Orange", Fn4: "Purple", Control: "Red" })[n], n]),
|
||||
]],
|
||||
["Win / Mac switch", [
|
||||
["Win side", "Whole backlight white"],
|
||||
["Mac side", "Effect chosen in Launcher"],
|
||||
], "The chosen effect is kept; flipping back restores it."],
|
||||
];
|
||||
$("tricklist").innerHTML = tricks.map(([title, rows, note, lead]) => `<div><b>${title}</b>${lead ? `<p class="lead">${lead}</p>` : ""}<dl>${
|
||||
rows.map(([k, v]) => `<dt>${k}</dt><dd>${v}</dd>`).join("")}</dl>${note ? `<p>${note}</p>` : ""}</div>`).join("");
|
||||
|
||||
// ---- Autocorrect --------------------------------------------------------------
|
||||
const AC = DATA.autocorrect;
|
||||
$("acintro").innerHTML = `${AC.length.toLocaleString()} typos are fixed as you type. Toggle with <kbd>Super</kbd>+tap Caps. <span class="b">|</span> marks a word edge — the typo only counts at the start or end of a word.`;
|
||||
const edge = (w) => esc(w).replace(/^:/, '<span class="b">|</span>').replace(/:$/, '<span class="b">|</span>');
|
||||
function renderAC() {
|
||||
const q = $("ac").value.trim().toLowerCase();
|
||||
if (!q) { $("acres").innerHTML = ""; return; }
|
||||
const hits = AC.filter(([t, f]) => t.replace(/:/g, "").includes(q) || f.toLowerCase().includes(q));
|
||||
const LIMIT = 60;
|
||||
$("acres").innerHTML = hits.slice(0, LIMIT).map(([t, f]) =>
|
||||
`<div><span class="typo">${edge(t)}</span><span class="b">→</span><span>${esc(f)}</span></div>`).join("")
|
||||
+ (hits.length > LIMIT ? `<p>${hits.length - LIMIT} more — keep typing to narrow down.</p>` : "")
|
||||
+ (hits.length ? "" : `<p>“${esc(q)}” is not in the dictionary, so autocorrect leaves it alone.</p>`);
|
||||
}
|
||||
$("ac").addEventListener("input", renderAC);
|
||||
$("ac").value = "teh";
|
||||
renderAC();
|
||||
|
||||
// ---- Symbols ----------------------------------------------------------------
|
||||
const chords = DATA.chords.map((c) => ({
|
||||
...c,
|
||||
hay: [c.out, c.name, c.cat, ...c.cp, ...c.seqs.map((s) => s.s)].join(" ").toLowerCase(),
|
||||
}));
|
||||
const cats = [...new Set(chords.map((c) => c.cat))];
|
||||
let activeCat = "All";
|
||||
|
||||
$("chips").innerHTML = ["All", ...cats].map((cat) => {
|
||||
const n = cat === "All" ? chords.length : chords.filter((c) => c.cat === cat).length;
|
||||
return `<button class="chip" data-cat="${esc(cat)}" aria-pressed="${cat === activeCat}">${esc(cat)}<span>${n}</span></button>`;
|
||||
}).join("");
|
||||
$("chips").addEventListener("click", (e) => {
|
||||
const b = e.target.closest(".chip");
|
||||
if (!b) return;
|
||||
activeCat = b.dataset.cat;
|
||||
for (const c of $("chips").children) c.setAttribute("aria-pressed", c === b);
|
||||
render();
|
||||
});
|
||||
|
||||
function score(c, q) {
|
||||
if (!q) return 1;
|
||||
const words = q.split(/\s+/).filter(Boolean);
|
||||
if (!words.every((w) => c.hay.includes(w))) return 0;
|
||||
let s = 1;
|
||||
for (const { s: seq } of c.seqs) {
|
||||
if (seq === q) s = Math.max(s, 100);
|
||||
else if (seq.startsWith(q)) s = Math.max(s, 50);
|
||||
}
|
||||
if (c.out === q) s = Math.max(s, 100);
|
||||
if (c.name.toLowerCase().startsWith(q)) s = Math.max(s, 20);
|
||||
return s;
|
||||
}
|
||||
|
||||
const caps = (seq) => [...seq].map((ch) => `<kbd>${esc(ch)}</kbd>`).join("");
|
||||
let shown = [];
|
||||
|
||||
function tile(c) {
|
||||
const [primary, ...rest] = c.seqs;
|
||||
const alts = rest.map((s) => s.st === "early"
|
||||
? `<s title="Fires as soon as “${esc(s.at)}” is typed; the rest is typed as plain text">${esc(s.s)}</s>`
|
||||
: `<span>${esc(s.s)}</span>`).join("");
|
||||
const wait = c.seqs.some((s) => s.st === "wait")
|
||||
? `<span class="flag" title="A longer sequence starts with the same letters">Pause ${C.CHORD_DISAMBIG_MS} ms or press Enter</span>` : "";
|
||||
return `<button class="tile" data-out="${esc(c.out)}" title="Copy ${esc(c.out)}">
|
||||
<div class="top"><span class="glyph">${esc(c.out)}</span><span class="cp">${c.cp.join(" ")}</span></div>
|
||||
<div class="seq" aria-label="Sequence ${esc(primary.s)}">${caps(primary.s)}</div>
|
||||
<div class="name">${esc(c.name || c.cat)}${alts ? `<div class="alts">also ${alts}</div>` : ""}</div>
|
||||
${wait}
|
||||
</button>`;
|
||||
}
|
||||
|
||||
function render() {
|
||||
const q = $("q").value.trim().toLowerCase();
|
||||
shown = chords
|
||||
.filter((c) => activeCat === "All" || c.cat === activeCat)
|
||||
.map((c) => [score(c, q), c]).filter(([s]) => s > 0)
|
||||
.sort((a, b) => b[0] - a[0]).map(([, c]) => c);
|
||||
$("count").textContent = `${shown.length} / ${chords.length}`;
|
||||
$("results").innerHTML = shown.length ? shown.map(tile).join("")
|
||||
: `<p class="empty">No symbol matches “${esc(q)}”. Try a name like “arrow” or “integral”.</p>`;
|
||||
}
|
||||
$("q").addEventListener("input", render);
|
||||
$("q").addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter" && shown[0]) copy(shown[0].out);
|
||||
if (e.key === "Escape") { $("q").value = ""; render(); }
|
||||
});
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "/" && document.activeElement !== $("q")) { e.preventDefault(); $("q").focus(); }
|
||||
});
|
||||
$("results").addEventListener("click", (e) => {
|
||||
const t = e.target.closest(".tile");
|
||||
if (t) copy(t.dataset.out);
|
||||
});
|
||||
|
||||
let toastTimer;
|
||||
async function copy(text) {
|
||||
let msg = `Copied ${text}`;
|
||||
try { await navigator.clipboard.writeText(text); } catch { msg = "Copy blocked by the browser — select the symbol instead"; }
|
||||
$("toast").textContent = msg;
|
||||
$("toast").hidden = false;
|
||||
clearTimeout(toastTimer);
|
||||
toastTimer = setTimeout(() => ($("toast").hidden = true), 1400);
|
||||
}
|
||||
render();
|
||||
|
||||
// ---- Layers -----------------------------------------------------------------
|
||||
const CAT = {
|
||||
mod: ["Modifier / nav", "--c-mod"], media: ["Media & system", "--c-media"], rgb: ["Backlight", "--c-rgb"],
|
||||
mouse: ["Mouse", "--c-mouse"], sys: ["Power & wireless", "--c-sys"], custom: ["Custom", "--c-custom"],
|
||||
chord: ["Chord key", "--c-chord"], danger: ["Destructive", "--danger"],
|
||||
};
|
||||
const layers = DATA.layers;
|
||||
const base = layers[0];
|
||||
const W = 20.5, H = 6.25;
|
||||
const LED = Object.fromEntries(layers.map((l) => [l.id, l.led]));
|
||||
let current = 0;
|
||||
const KNOB = base.keys.findIndex((k) => k.x === 19.5 && k.y === 0); // encoder push sits top-right
|
||||
|
||||
$("legend").innerHTML = Object.entries(CAT).filter(([k]) => k !== "mod")
|
||||
.map(([k, [label, v]]) => `<span class="${k === "chord" ? "solid" : ""}" style="--cat:var(${v})">${label}</span>`).join("")
|
||||
+ `<span class="multi" style="--stops:${layers.slice(1).map((l) => l.led).join(",")}">Layer key (colour of the layer it opens)</span>`;
|
||||
|
||||
$("tabs").innerHTML = layers.map((l, i) =>
|
||||
`<button class="tab" role="tab" id="tab-${l.id}" style="--led:${l.led}" aria-selected="${i === current}" data-i="${i}"><i></i>${esc(l.name)}</button>`).join("");
|
||||
$("tabs").addEventListener("click", (e) => {
|
||||
const b = e.target.closest(".tab");
|
||||
if (b) { current = +b.dataset.i; drawLayer(); }
|
||||
});
|
||||
$("dim").addEventListener("change", drawLayer);
|
||||
|
||||
// Physical keycap colour follows the Base keycode at that position (the cap never changes with the layer).
|
||||
const GREEN_CAPS = new Set(["KC_ESC", "KC_ENT"]);
|
||||
const GREY_CAPS = new Set([
|
||||
"KC_F5", "KC_F6", "KC_F7", "KC_F8", "KC_DEL", "KC_GRV", "KC_BSPC", "KC_TAB", "KC_BSLS", "CAPS_MOD",
|
||||
"KC_LSFT", "KC_RSFT", "KC_LCTL", "KC_LGUI", "KC_LALT", "TT(FN2)", "TT(FN1)", "KC_RCTL",
|
||||
"KC_PGUP", "KC_PGDN", "TD(TD_HOME_END)",
|
||||
]);
|
||||
const capColour = (b) => GREEN_CAPS.has(b.kc) ? "green" : GREY_CAPS.has(b.kc) ? "grey" : "white";
|
||||
|
||||
function keyHTML(k, i, layer) {
|
||||
const b = base.keys[i];
|
||||
const shownKey = k.t ? b : k;
|
||||
const knob = i === KNOB;
|
||||
const cls = ["key"];
|
||||
let cat = "";
|
||||
if (k.t) cls.push("trans");
|
||||
else if (k.to) { cls.push("tint"); cat = LED[k.to]; }
|
||||
else if (k.c && k.c !== "mod") { cls.push("tint"); cat = `var(${CAT[k.c][1]})`; }
|
||||
if (k.c === "chord") cls.push("solid");
|
||||
if (knob) cls.push("knob");
|
||||
else cls.push(capColour(b));
|
||||
if (layer !== base && $("dim").checked && (k.t || k.kc === b.kc)) cls.push("same");
|
||||
const style = `left:${(k.x / W) * 100}%;top:${(k.y / H) * 100}%;width:${(k.w / W) * 100}%;height:${(k.h / H) * 100}%`;
|
||||
return `<div class="${cls.join(" ")}" style="${style}" tabindex="0" data-i="${i}">
|
||||
<div style="${cat ? `--cat:${cat}` : ""}"><span class="s">${esc(shownKey.s || "")}</span><span class="l">${esc(shownKey.l || "")}</span></div></div>`;
|
||||
}
|
||||
|
||||
function drawLayer() {
|
||||
const l = layers[current];
|
||||
for (const t of $("tabs").children) t.setAttribute("aria-selected", +t.dataset.i === current);
|
||||
$("kb").innerHTML = l.keys.map((k, i) => keyHTML(k, i, l)).join("");
|
||||
const via = l.via.length
|
||||
? l.via.map((v) => `<b>${esc(v.l)}</b> on ${esc(v.from)} <span class="muted">(${esc(v.d)})</span>`).join(" · ")
|
||||
: "Default layer";
|
||||
$("layermeta").innerHTML = `
|
||||
<p><span class="muted">Reached with:</span> ${via}</p>
|
||||
<p><span class="muted">Knob:</span> turn left <b>${esc(l.enc[0])}</b> · turn right <b>${esc(l.enc[1])}</b> · press <b>${esc((l.keys[KNOB].t ? base.keys[KNOB] : l.keys[KNOB]).d)}</b>
|
||||
<span class="muted">· ${esc(l.ledNote)}</span></p>`;
|
||||
}
|
||||
|
||||
function showInfo(e) {
|
||||
const el = e.target.closest(".key");
|
||||
if (!el) return;
|
||||
const i = +el.dataset.i;
|
||||
const k = layers[current].keys[i];
|
||||
const eff = k.t ? base.keys[i] : k;
|
||||
$("keyinfo").innerHTML = k.t
|
||||
? `<code>_______</code> Transparent — falls through to the layer below (Base: <b>${esc(eff.d)}</b>)`
|
||||
: `<code>${esc(k.kc)}</code> <span>${esc(k.d)}</span>`;
|
||||
}
|
||||
$("kb").addEventListener("mouseover", showInfo);
|
||||
$("kb").addEventListener("focusin", showInfo);
|
||||
drawLayer();
|
||||
</script>
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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("</", "<\\/")
|
||||
template = (HERE / "cheatsheet.template.html").read_text().split("\n", 1)[1] # drop the template note
|
||||
OUT.write_text(template.replace("/*DATA*/null", payload))
|
||||
print(f"wrote {OUT.name}: {len(data['chords'])} symbols, {len(data['layers'])} layers, {len(autocorrect)} autocorrections, {keys}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user