Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions Unicode.md
Original file line number Diff line number Diff line change
@@ -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)
104 changes: 104 additions & 0 deletions src/utils/advanced-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
};
Expand All @@ -172,6 +186,26 @@ export const advancedKeycodeToString = (
basicKeyToByte: Record<string, number>,
byteToKey: Record<number, string>,
): 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]);
Expand Down Expand Up @@ -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<string, number>,
Expand Down