diff --git a/Unicode.md b/Unicode.md new file mode 100644 index 000000000..9259297e1 --- /dev/null +++ b/Unicode.md @@ -0,0 +1,186 @@ +# Unicode Support in VIA + +This guide explains how to use Unicode keycodes in VIA and configure your QMK firmware to support them. + +## Overview + +VIA now supports three types of Unicode keycodes through the "Any" keycode feature: + +- **UC(hex)** - Basic Unicode (up to U+7FFF) +- **UM(index)** - Unicode Map (full Unicode range via array index) +- **UP(i,j)** - Unicode Pair (lowercase/uppercase switching) + +## Prerequisites + +### Operating System Configuration + +Before using Unicode features, ensure your OS is configured for Unicode input: + +- **Windows**: WinCompose or the built-in hex input method +- **macOS**: Unicode Hex Input keyboard layout +- **Linux**: IBus or the Ctrl+Shift+U method + +### Firmware Requirements + +**Important**: You can only enable ONE of these options at a time. Enabling both `UNICODE_ENABLE` and `UNICODEMAP_ENABLE` will result in compilation errors. + +#### For UC() keycodes: +Add to your `rules.mk`: +```makefile +UNICODE_ENABLE = yes +# UNICODEMAP_ENABLE = no # Must be disabled or omitted +``` + +Add to your `config.h`: +```c +#define UNICODE_SELECTED_MODES UC_WIN // or UC_MAC, UC_LNX +``` + +#### For UM() and UP() keycodes: +Add to your `rules.mk`: +```makefile +UNICODEMAP_ENABLE = yes +# UNICODE_ENABLE = no # Must be disabled or omitted +``` + +## Using Unicode in VIA + +### Basic Unicode - UC() + +1. Click on a key in VIA +2. Click the "Any" button +3. Enter a Unicode keycode: + - `UC(0x00E4)` - ä (with 0x prefix) + - `UC(00E4)` - ä (without 0x prefix) + +Common examples: +- `UC(0x00A9)` - © (Copyright) +- `UC(0x2764)` - ❤ (Heart) +- `UC(0x03B1)` - α (Greek alpha) +- `UC(0x2318)` - ⌘ (Command) + +**Limitation**: UC() only supports characters up to U+7FFF. + +### Unicode Map - UM() + +For full Unicode support (including emoji), use Unicode Map: + +1. Define a `unicode_map` array in your keymap.c: +```c +enum unicode_names { + EYES, + FIRE, + ROCKET, + HEART, + SMILE, + THINK, + PARTY, + COOL +}; + +const uint32_t PROGMEM unicode_map[] = { + [EYES] = 0x1F440, // 👀 + [FIRE] = 0x1F525, // 🔥 + [ROCKET] = 0x1F680, // 🚀 + [HEART] = 0x2764, // ❤️ + [SMILE] = 0x1F604, // 😄 + [THINK] = 0x1F914, // 🤔 + [PARTY] = 0x1F389, // 🎉 + [COOL] = 0x1F60E // 😎 +}; +``` + +2. In VIA, use the index: + - `UM(0)` - 👀 (eyes) + - `UM(1)` - 🔥 (fire) + - `UM(2)` - 🚀 (rocket) + +### Unicode Pair - UP() + +For characters that have lowercase/uppercase variants: + +1. Define pairs in your unicode_map: +```c +enum unicode_names { + GREEK_LOWER_ALPHA, + GREEK_UPPER_ALPHA, + GREEK_LOWER_BETA, + GREEK_UPPER_BETA +}; + +const uint32_t PROGMEM unicode_map[] = { + [GREEK_LOWER_ALPHA] = 0x03B1, // α + [GREEK_UPPER_ALPHA] = 0x0391, // Α + [GREEK_LOWER_BETA] = 0x03B2, // β + [GREEK_UPPER_BETA] = 0x0392 // Β +}; +``` + +2. In VIA, use: + - `UP(0,1)` - α/Α (switches with Shift/Caps Lock) + - `UP(2,3)` - β/Β + +## Complete Example + +Here's a complete example for a keymap with emoji support: + +```c +// In keymap.c +enum unicode_names { + // Emoji + EYES, FIRE, ROCKET, HEART, SMILE, THINK, PARTY, COOL, + // Greek letters (lowercase, uppercase) + GR_ALPHA_L, GR_ALPHA_U, + GR_BETA_L, GR_BETA_U +}; + +const uint32_t PROGMEM unicode_map[] = { + // Emoji + [EYES] = 0x1F440, // 👀 + [FIRE] = 0x1F525, // 🔥 + [ROCKET] = 0x1F680, // 🚀 + [HEART] = 0x2764, // ❤️ + [SMILE] = 0x1F604, // 😄 + [THINK] = 0x1F914, // 🤔 + [PARTY] = 0x1F389, // 🎉 + [COOL] = 0x1F60E, // 😎 + // Greek pairs + [GR_ALPHA_L] = 0x03B1, // α + [GR_ALPHA_U] = 0x0391, // Α + [GR_BETA_L] = 0x03B2, // β + [GR_BETA_U] = 0x0392 // Β +}; +``` + +Then in VIA: +- `UM(0)` through `UM(7)` for emoji +- `UP(8,9)` for α/Α +- `UP(10,11)` for β/Β + +## Troubleshooting + +### Characters not appearing +1. Check your OS Unicode input method is enabled +2. Verify firmware has UNICODE_ENABLE or UNICODEMAP_ENABLE +3. Ensure the correct input mode (UC_WIN/UC_MAC/UC_LNX) + +### Invalid keycode in VIA +1. For UC(), ensure hex value is ≤ 0x7FFF +2. For UM(), ensure index exists in your unicode_map +3. For UP(), ensure both indices are ≤ 127 + +### Emoji showing as boxes +- Your application/font may not support emoji +- Try in a modern browser or text editor + +## Legacy Aliases + +VIA also supports legacy aliases: +- `X()` is equivalent to `UM()` +- `XP()` is equivalent to `UP()` + +## Resources + +- [QMK Unicode Documentation](https://docs.qmk.fm/#/feature_unicode) +- [Unicode Character Table](https://unicode-table.com/) +- [Emoji Unicode List](https://unicode.org/emoji/charts/full-emoji-list.html) \ No newline at end of file diff --git a/src/utils/advanced-keys.ts b/src/utils/advanced-keys.ts index 6ddf2cc4c..4ad6e6539 100644 --- a/src/utils/advanced-keys.ts +++ b/src/utils/advanced-keys.ts @@ -79,6 +79,14 @@ const topLevelMacroToValue = { MACRO: '_QK_MACRO', // MACRO(n) }; +// QMK Unicode constants +const QK_UNICODE = 0x8000; +const QK_UNICODE_MAX = 0xFFFF; +const QK_UNICODEMAP = 0x8000; +const QK_UNICODEMAP_MAX = 0xBFFF; +const QK_UNICODEMAP_PAIR = 0xC000; +const QK_UNICODEMAP_PAIR_MAX = 0xFFFF; + const modifierKeyToValue = { LCTL: modCodes.QK_LCTL, C: modCodes.QK_LCTL, @@ -163,6 +171,12 @@ export const advancedStringToKeycode = ( return parseTopLevelMacro(parts, basicKeyToByte); } else if (Object.keys(modifierKeyToValue).includes(parts[0])) { return parseModifierCode(parts, basicKeyToByte); + } else if (parts[0] === 'UC') { + return parseUnicodeCode(parts); + } else if (parts[0] === 'UM') { + return parseUnicodeMapCode(parts); + } else if (parts[0] === 'UP') { + return parseUnicodePairCode(parts); } return 0; }; @@ -172,6 +186,26 @@ export const advancedKeycodeToString = ( basicKeyToByte: Record, byteToKey: Record, ): string | null => { + // Check if it's a Unicode keycode first + if (inputKeycode >= QK_UNICODE && inputKeycode <= QK_UNICODE_MAX) { + // Unicode Map (UM) + if (inputKeycode >= QK_UNICODEMAP && inputKeycode <= QK_UNICODEMAP_MAX) { + const index = inputKeycode & 0x3FFF; + return `UM(${index})`; + } + // Unicode Pair (UP) + else if (inputKeycode >= QK_UNICODEMAP_PAIR && inputKeycode <= QK_UNICODEMAP_PAIR_MAX) { + const lowerIndex = inputKeycode & 0x7F; + const upperIndex = (inputKeycode >> 7) & 0x7F; + return `UP(${lowerIndex}, ${upperIndex})`; + } + // Basic Unicode (UC) + else { + const codePoint = inputKeycode & 0x7FFF; + return `UC(0x${codePoint.toString(16).toUpperCase().padStart(4, '0')})`; + } + } + let valueToRange: [number, string][] = Object.entries( quantumRanges(basicKeyToByte), ).map(([key, value]) => [value, key]); @@ -400,6 +434,76 @@ const parseModifierCode = ( return bytes.reduce((acc, byte) => acc | byte, 0); }; +const parseUnicodeCode = (inputParts: string[]): number => { + if (inputParts.length < 2 || !inputParts[1]) { + return 0; + } + + const parameter = inputParts[1].trim(); + let codePoint = 0; + + // Parse hex value (with or without 0x prefix) + if (parameter.startsWith('0X')) { + codePoint = parseInt(parameter, 16); + } else if (/^[0-9A-F]+$/i.test(parameter)) { + // Plain hex without 0x prefix + codePoint = parseInt(parameter, 16); + } else { + return 0; + } + + // Validate code point is within QMK's supported range (0 to 0x7FFF) + if (codePoint < 0 || codePoint > 0x7FFF) { + return 0; + } + + // Return the QMK Unicode keycode (QK_UNICODE | codePoint) + return QK_UNICODE | codePoint; +}; + +const parseUnicodeMapCode = (inputParts: string[]): number => { + if (inputParts.length < 2 || !inputParts[1]) { + return 0; + } + + const parameter = inputParts[1].trim(); + const index = parseInt(parameter, 10); + + // Validate index is within valid range (0 to 0x3FFF = 16383) + if (isNaN(index) || index < 0 || index > 0x3FFF) { + return 0; + } + + // Return the QMK Unicode Map keycode (QK_UNICODEMAP | index) + return QK_UNICODEMAP | index; +}; + +const parseUnicodePairCode = (inputParts: string[]): number => { + if (inputParts.length < 2 || !inputParts[1]) { + return 0; + } + + const parameter = inputParts[1].trim(); + const indices = parameter.split(',').map(s => s.trim()); + + if (indices.length !== 2) { + return 0; + } + + const lowerIndex = parseInt(indices[0], 10); + const upperIndex = parseInt(indices[1], 10); + + // Validate indices are within valid range (0 to 0x7F = 127) + if (isNaN(lowerIndex) || lowerIndex < 0 || lowerIndex > 0x7F || + isNaN(upperIndex) || upperIndex < 0 || upperIndex > 0x7F) { + return 0; + } + + // Return the QMK Unicode Pair keycode + // UP(i, j) = QK_UNICODEMAP_PAIR | (i & 0x7F) | ((j & 0x7F) << 7) + return QK_UNICODEMAP_PAIR | (lowerIndex & 0x7F) | ((upperIndex & 0x7F) << 7); +}; + export const anyKeycodeToString = ( input: number, basicKeyToByte: Record,