QMK and Cyrillic

Eugene Bogorad
4 min readApr 15, 2024

--

Updated on 2024–04–26: added info about HRM mitigation.

While setting up my new keyboard, I had some considerations for the Cyrillic layout. I’m using a modified Miryoku layout.

First, I was done with the need to remember which keyboard layout was active and constantly typing using the wrong one. So, here’s my hack for stateless language switching: put the Cyrillic layer on a timeout! Now I always start typing in Russian with a D+H combo (in Colemak-DH it only takes a slight movement of both index fingers), and after 5 seconds of inactivity I’m back to English!

Here we go:

enum custom_keycodes {
LANG_ENG = SAFE_RANGE, // Ensure these don't conflict with existing keycodes
LANG_RUS,
};
const uint16_t PROGMEM switch_to_rus[] = {KC_D, KC_H, COMBO_END};

combo_t key_combos[COMBO_COUNT] = {
COMBO(switch_to_rus, LANG_RUS),
};

//////////////// here we switch languages
//
// define fallback first
void switch_to_english(void) {
// Save mods.
uint8_t mod_state = get_mods();
// Clear mods so Windows doesn't get confused.
clear_mods();
// Shift+Alt+0 sent to host
SEND_STRING(SS_LSFT(SS_LALT("0")));
// Switch to default layer.
layer_move(U_BASE);
// debug
dprint("Switched to English!\n");
// Restore mods.
set_mods(mod_state);
};

#define RUS_LAYER_TIMEOUT 5000 // timeout in milliseconds

void switch_to_russian(void) {
// switch back to English if already in Russian mode
if (get_highest_layer(layer_state) == U_EXTRA) {
dprint("Russian active! Switching back to English\n");
switch_to_english();
}
else
{
// Save mods.
uint8_t mod_state = get_mods();
// Clear mods so Windows doesn't get confused.
clear_mods();
// Shift+Alt+1 sent to host
SEND_STRING(SS_LSFT(SS_LALT("1")));
// Switch to Cyrillic-friendly layer.
layer_move(U_EXTRA);
// debug
dprint("Switched to Russian!\n");
// Restore mods.
set_mods(mod_state);
};
};

void matrix_scan_user(void) {
// achordion
achordion_task();
//
// this resets current language to English after <timeout> if Russian was active
if (U_EXTRA == get_highest_layer(layer_state)) {
if (RUS_LAYER_TIMEOUT < last_input_activity_elapsed()) {
switch_to_english();
}
}
}

Second, I was done with the sad fact of life that symbols are in different places for US-English and Russian — now that I can finally fix it.

At first, I tried creating a separate symbols layer for Cyrillic, so that the correct keys are sent to Windows. Didn’t work — not all keys are mapped in Cyrillic.

Then I decided to use Unicode in this new layer. Big mistake! Unicode input is not standardized, QMK suggests two ways of doing it in Windows, and they both suck.

The EnableHexNumpad way causes some apps to go haywire because of Numpad e.g., Slack loses focus.

The WinCompose way is even worse — there is no version for Win/ARM64 (so it won’t work on my Lenovo X13s), and it uses <master>U as prefix. But KC_U produces a different keycode in Cyrillic (duh)! Yes, there is an issue about it on GitHub, with a lively discussion, but no one clearly gives a crap.

Then I thought about creating my own Cyrillic layout in Windows, using MS Keyboard Layout Creator. But it’s all but dead now, and there is no support for ARM64. I even considered using MS Power Toys, but it makes no sense.

So, I decided to modify QMK’s LT() function to switch to English prior to entering symbols. Then I decided to generalize my approach and use it for my numbers layer as well. And it works!

Here we go:

enum custom_keycodes {
LANG_ENG = SAFE_RANGE, // Ensure these don't conflict with existing keycodes
LANG_RUS,
LT_SYM_ENT,
LT_NUM_BSPC,
};

////////////////////////////////////////////////
// Define a structure to hold layer-tap information
typedef struct {
bool is_active;
bool command_sent;
uint16_t timer;
} layer_tap_t;

// Initialize structure instance for holding state
layer_tap_t my_lt = {false, false, 0};
////////////////////////////////////////////////////////////////////
// This switches to English but only when LT-key is pressed and another key it tapped.
//
// Function to handle layer-tap logic
void process_layer_tap(uint16_t keycode, uint16_t layer, keyrecord_t *record) {
if (record->event.pressed) {
// Key was pressed - start timer.
my_lt.timer = timer_read();
my_lt.is_active = true;
layer_on(layer);
dprintf("lt: pressed!\n");
} else { // Key was released
// Was it a tap?
if (my_lt.is_active && timer_elapsed(my_lt.timer) < TAPPING_TERM) {
tap_code(keycode);
dprintf("lt: tapped!\n");
}
layer_off(layer);
// Only retore win kbd layout if it was switched.
if (my_lt.command_sent) {
// Tell Windows we're still in Cyrillic
SEND_STRING(SS_LSFT(SS_LALT("1")));
// Clear flag.
my_lt.command_sent = false;
dprintf("lt: sent SA(1) to windows!\n");
}
my_lt.is_active = false;
dprintf("lt: released!\n");
}
}

bool process_record_user(uint16_t keycode, keyrecord_t *record) {
// check for specific keycodes
switch (keycode) {
//
case LT_SYM_ENT:
process_layer_tap(KC_ENT, U_SYM, record);
return false; // Skip all further processing of this key
//
case LT_NUM_BSPC:
process_layer_tap(KC_BSPC, U_NUM, record);
return false; // Skip all further processing of this key
//
default:
if (my_lt.is_active && record->event.pressed) {
// Only switch win kbd layout if it has not been switched.
if (!my_lt.command_sent) {
// Custom function called when any key is pressed while MY_LT is active
my_lt.command_sent = true;
SEND_STRING(SS_LSFT(SS_LALT("0")));
dprintf("lt: sent SA(0) to windows!\n");
}
return true; // Allow the key press to be handled normally by QMK
}
break;
} // done with specific keys
//
return true;
}

Finally, I sometimes switch tabs/windows and I never need Cyrillic in a new one. And so, I switch to English whenever modified TAB or ESC are tapped (alt+esc, win+tab etc).

Here we go:

        case LT(U_MOUSE,KC_TAB):
case LT(U_MEDIA,KC_ESC):
// only operate if pressed
if (record->event.pressed &&
// Ignore if held
record->tap.count &&
// Check if the current layer is U_EXTRA
layer_state_cmp(layer_state, U_EXTRA) &&
// And any of the modifiers are active
(mod_state & (MOD_BIT(KC_LALT) |
MOD_BIT(KC_LSFT) |
MOD_BIT(KC_LCTL) |
MOD_BIT(KC_LGUI)
)))
{
switch_to_english();
};
return true; // Continue normal tab processing

New:

Ran into a problem — when Cyrillic is active on QWERTY, and met/alt/ctrl ir held, wrong keys are sent since my muscle memory is trained on Colemak-DH. To fix this I implemented a quick hack: while in QWERTY and HRMs are active, temporarily switch to Colemak:

bool need_to_restore_layer = false;


/////////////////////////////////////////////////
// macro processor etc
bool process_record_user(uint16_t keycode, keyrecord_t *record)
{
// Check if the current layer is U_EXTRA
// and key is pressed
// and HRMs are active
if (layer_state_cmp(layer_state, U_EXTRA) &&
record->event.pressed &&
(mod_state & (MOD_BIT(KC_LALT) |
MOD_BIT(KC_LCTL) |
MOD_BIT(KC_LGUI))))
{
{
dprintf("HRMs active, switching to U_BASE\n");
layer_move(U_BASE);
need_to_restore_layer = true;
return true; // Continue normal tab processing
}
}

if (!record->event.pressed &&
need_to_restore_layer)
{
dprintf("Restoring U_EXTRA\n");
layer_move(U_EXTRA);
need_to_restore_layer = false;
return true; // Continue normal tab processing
}
///////////////////// more here
}

--

--