Krei.se

23 min read Original article ↗

XKB Input Flow

You can use xkb with just 5 concepts thus reducing the complexity from ~20 to 5:

  • scancodes (evdev) -> keycodes
  • keycodes --(keytype)--> keysymbol (actual character).
  • modifiers actions -> keytypes

Useful but not needed: Keycode-aliases, indicators

Scrapped: All .lst, .xml, Rules, Models, Layouts, Variants, Options, virtual modifiers, explicit, implicit, indirect.

It's possible to write a full X11 compatible map with 8 modifiers, 4 groups, 8 levels getting (108) 4 8 = 3456 characters.

Then you only send this map and actually know whats going on - never needing strange overrides, etc. - which feels great.

It took me 4 days, you can likely do it in 1-2.

Just start

apt install libxkbcommon-tools for xkbcli

  • wayland only, although X11 compatible map is done here and doable

Read this, do not read blogs: https://xkbcommon.org/doc/current/keymap-text-format-v1-v2.html

0 Hardware stage (output USB HID)

1 Kernel-stage (input USB HID - output Kernel keycode, Kernel key)

  • The operating system uses its input system to read the RAW keycode.

  • The operating system uses its EVent DEVice (evdev) framework to translate the keycode into a key of event type EV_KEY.

    • Debug as root: evtest - sometimes keyboards expose multiple events like:

      • /dev/input/event1: BFO-9000 BFO-9000:

        type 4 (EV_MSC), code 4 (MSC_SCAN), value 70004
        type 1 (EV_KEY), code 30 (KEY_A), value 1
      • /dev/input/event4: BFO-9000 BFO-9000 Consumer Control:

        type 4 (EV_MSC), code 4 (MSC_SCAN), value c00e9
        type 1 (EV_KEY), code 115 (KEY_VOLUMEUP), value 1

      Notice the 70000 / c0000 - "USB HID Usage Page" 7 - keyboard (normal keys), c - consumer device (multimedia keys)

      value 0 - released key. value 1 - pressed key. value 2 - key repeat

HID Usage Pages:

https://github.com/torvalds/linux/blob/075b74841bd0065a3bda3440873c747938e69b68/include/linux/hid.h

#define HID_UP_KEYBOARD     0x00070000
#define HID_UP_CONSUMER     0x000c0000

Mapping of USB HID Input to kernel keycode is done via: https://github.com/torvalds/linux/blob/master/drivers/hid/hid-input.c

# line 27
static const unsigned char hid_keyboard[256] = {
    0,  0,  0,  0, 30, 48, 46, 32, 18, 33, 34, 35, 23, 36, 37, 38, # 0x0004 USB == KEY_A 30
    50, 49, 24, 25, 16, 19, 31, 20, 22, 47, 17, 45, 21, 44,  2,  3,
    4,  5,  6,  7,  8,  9, 10, 11, 28,  1, 14, 15, 57, 12, 13, 26, # 0x0029 USB == KEY_ESC 1
    27, 43, 43, 39, 40, 41, 51, 52, 53, 58, 59, 60, 61, 62, 63, 64,
    65, 66, 67, 68, 87, 88, 99, 70,119,110,102,104,111,107,109,106,
    105,108,103, 69, 98, 55, 74, 78, 96, 79, 80, 81, 75, 76, 77, 71,
    72, 73, 82, 83, 86,127,116,117,183,184,185,186,187,188,189,190,
    191,192,193,194,134,138,130,132,128,129,131,137,133,135,136,113,
    115,114,unk,unk,unk,121,unk, 89, 93,124, 92, 94, 95,unk,unk,unk,
    122,123, 90, 91, 85,unk,unk,unk,unk,unk,unk,unk,111,unk,unk,unk,
    unk,unk,unk,unk,unk,unk,unk,unk,unk,unk,unk,unk,unk,unk,unk,unk,
    unk,unk,unk,unk,unk,unk,179,180,unk,unk,unk,unk,unk,unk,unk,unk,
    unk,unk,unk,unk,unk,unk,unk,unk,unk,unk,unk,unk,unk,unk,unk,unk,
    unk,unk,unk,unk,unk,unk,unk,unk,111,unk,unk,unk,unk,unk,unk,unk,
    29, 42, 56,125, 97, 54,100,126,164,166,165,163,161,115,114,113,
    150,158,159,128,136,177,178,176,142,152,173,140,unk,unk,unk,unk
};

# line 772
case HID_UP_KEYBOARD:
    set_bit(EV_REP, input->evbit);

    if ((usage->hid & HID_USAGE) < 256) {
        if (!hid_keyboard[usage->hid & HID_USAGE]) goto ignore;
        map_key_clear(hid_keyboard[usage->hid & HID_USAGE]);
    } else
        map_key(KEY_UNKNOWN);

    break;

# line 1116

    case HID_UP_CONSUMER:   /* USB HUT v1.12, pages 75-84 */
        switch (usage->hid & HID_USAGE) {
        case 0x000: goto ignore;
        case 0x030: map_key_clear(KEY_POWER);       break;
        case 0x031: map_key_clear(KEY_RESTART);     break;

        ...

        case 0x22e: map_key_clear(KEY_ZOOMOUT);     break;
        case 0x22f: map_key_clear(KEY_ZOOMRESET);   break;
        case 0x232: map_key_clear(KEY_FULL_SCREEN); break;

So you get: The USB Page + hidinput code mapped to a kernel keycode and kernel key

  • for < 256 lookup-table is used (hid_keyboard)
  • >= 256 KEY_ constants (from kernel key - kernel keycode map - see below)

kernel key - kernel keycode map:

https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h

or

/usr/include/linux/input.h

#define KEY_RESERVED        0
#define KEY_ESC         1
#define KEY_1           2
#define KEY_2           3
#define KEY_3           4
#define KEY_4           5
#define KEY_5           6
#define KEY_6           7

2 libinput-stage (wayland, input Kernel Keycode - output evdev scancode)

https://en.wikipedia.org/wiki/Wayland_(protocol)#libinput

This is the userspace library used by your compositor (Wayland) to talk to evdev devices. So if you set your input devices in sway et. here is the interface.

Small History Lesson

  • Bits 0-7 in X11 are reserved. See: https://tronche.com/gui/x/xlib/input/keyboard-encoding.html

    • In the X11 Protocol Specification, a keycode was defined as a single 8-bit unsigned byte (CARD8), giving a possible range of 0 to 255.

    • First bit (0) means Anykey which hints the GrabKey() function to grab any key. 1-7 are undefined - no explanation.

      #define AnyKey 0L

      Keycode 0 was hardcoded as a wildcard value for functions like XGrabKey(). If a program tells the X server, "Grab Keycode 0 with the Alt modifier," it means "Grab ALL keys as long as Alt is held."

      So using 0 a Program can grab all keys and using 8-255 grabs a specific one. This being the 80s i assume they planned on allowing lists of keys (grab all f-keys, modifiers, etc.) or allow special function keys - but never actually agreed on these lists or functions. Look no further fearless traveller as this is the end.

    • A decade later, Linux created its own input subsystem called evdev.

      The Linux kernel developers didn't care about X11 protocol rules; they just wanted a clean array starting at the beginning of memory. So Linux assigned its keycodes starting at 1:

      KEY_RESERVED = 0
      KEY_ESC = 1
  • Thus evdev adds 8

  • XKB receives this scancode, "keycodes" translate that to <keycodes>

  • The display system (e.g., X11, Wayland) receives a keymap from libxkbcommon to translate the key into a character.

    • This is what we build manually
  • The application uses the character however it sees fit.

!! SKIP this to keep sane and go to KcCGST !!

How xkb builds this map from a magic cookbook called RMLVO

!! SKIP this to keep sane and go to KcCGST !!

2 Layers of maps:

  • RMLVO (rules, models, layouts, variants, options)
  • KcCGST (keycodes, compat, geometry, symbols, types)

translation of RMLVO to KcCGST:

setxkbmap -layout us -variant -print

RMLVO

Rules define how to compile maps. So this is the first entry - and usually the evdev-ruleset

  • use your own rules/evdev to extend that:

    // Include the system (%S is /usr/share/X11/xkb/rules) `evdev` file.
    ! include %S/evdev
    
    // Configure custom symbols. This is configured after including the system
    // `evdev` file so the changes aren't overridden by the system configuration.
    // however this won't matter when you use your own identifiers
    
    // map ! MLVO identifier to KcCGST
    // MLVO can be multiple like: ! model layout = symbols  
    // multiple matches combine to +
    // See https://www.charvolant.org/doug/xkb/html/node5.html
    
    ! option = symbols
    bfo9000:function_keys = +bfo9000(function_keys)

3 XKBs KcCGST (input evdev scancode, output keysym)

keycodes, compat, (depr. geometry), symbols, types

Build your own

Take a template:

xkbcli compile-keymap --layout us > full_us_layout.xkb

[xkb_keycodes]
    - A translation of the hardware/evdev **scancodes** from the keyboard into XKB symbolic **keycodes**. 
        - <KeyC> = 30; 
    - keycode aliases
        - <AliC> = <KeyC>
    - Indicator names
[xkb_types]
    - Define what a keycode-type (1, 2, or 4 symbols) behaves like when a modifier is used 
[xkb_compat]
    - implicit modifier definitions and actions (preferred)
    - Indicator rules
[xkb_symbols]
    - explicit modifier definitions and actions
    - A translation of symbolic key codes into actual symbols and actions. 

XKB Keycodes

Easy, just record the scancode with evtest (add 8) or wev. You can name XKB keycodes however you like (need be 4 chars though)!

xkb_keycodes "yourcodes" {

    // X11 compatible, Wayland can do more - i didnt need to.
    minimum = 8;
    maximum = 255;

//  <keycode> = scancode;

    <LatA> = 38;
    <LatB> = 56;
    <LatC> = 54;
    <LatD> = 40;
    .
    .
    .
    // all fine with ,.+= etc as chars
    <KP.> = 91;
    <KP,> = 129;

    <KP/> = 106;
    <KP*> = 63;

    <KP->  = 82;
    <KP+>  = 86;

    <KP=>  = 125;
    <KPRT> = 104;

};

XKB Types

Each keycode will be mapped to one or more keysym.

xkb_symbols "(unnamed)" {
    ...
    // four levels, FOUR_LEVEL_SEMIALPHABETIC
    key <LatE> { [ e , E, EuroSign, cent ] };
    ...
};

But how?

XKB autoguesses rules:

See: https://github.com/xkbcommon/libxkbcommon/blob/master/doc/keymap-text-format-v1-v2.md#type-key-type-setting

  • 1 keysym: ONE_LEVEL
  • 2 keysyms:
    • if the two keysyms are letter and the first is lower case and the other upper case, then ALPHABETIC;
    • if one of the keysyms is numpad, then KEYPAD else TWO_LEVEL.
  • 3 or 4 keysyms (a missing 4th keysym is set to NoSymbol):
    • if the first two keysyms are letters and the first is lower case and the other upper case:
      • if the last two keysyms are letters and the first is lower case and the other upper case then FOUR_LEVEL_ALPHABETIC;
      • else FOUR_LEVEL_SEMIALPHABETIC.
    • if one of the first two keysyms is numpad, then FOUR_LEVEL_KEYPAD;
    • else FOUR_LEVEL.

Means if you skip Numpad and use key <KP1> { [ KP_1 ] }; etc. you can stay with only 3-5 types.

You can also set types yourselt and assign them to a key (never needed that)

Modifiers

These alter what "level" keysym you get when pressing a key.

See: https://github.com/xkbcommon/libxkbcommon/blob/master/doc/keymap-text-format-v1-v2.md#real-and-virtual-modifiers

Real Modifiers

these are sent to the OS as 8-bit bitmask

$ xmodmap -pm
xmodmap:  up to 4 keys per modifier, (keycodes in parentheses):

shift       Shift_L (0x32),  Shift_R (0x3e)
lock        Caps_Lock (0x42)
control     Control_L (0x25),  Control_R (0x69)
mod1        Alt_L (0x40),  Alt_L (0xcc),  Meta_L (0xcd)
mod2        Num_Lock (0x4d)
mod3        ISO_Level5_Shift (0xcb) <LVL5>
mod4        Super_L (0x85),  Super_R (0x86),  Super_L (0xce),  Hyper_L (0xcf)
mod5        ISO_Level3_Shift (0x5c)

Virtual Modifiers

These stay in XKB.

https://xkbcommon.org/doc/current/keymap-text-format-v1-v2.html#modifiers-declaration-and-binding

I recommend to start with only real modifiers - super easy:

Modifiers super easy


xkb_compatibility "(unnamed)" {

    // These are all keysyms we map below from keycodes

    interpret Shift_L { action = SetMods(modifiers=Shift); };
    interpret Shift_R { action = SetMods(modifiers=Shift); };

    interpret Caps_Lock { action = LockMods(modifiers=Lock); };

    interpret Control_L { action = SetMods(modifiers=Control); };
    interpret Control_R { action = SetMods(modifiers=Control); };

    interpret Alt_L { action = SetMods(modifiers=Mod1); };

    interpret Num_Lock { action = LockMods(modifiers=Mod2); };

    interpret Scroll_Lock { action = LockMods(modifiers=Mod3); };

    interpret Super_L { action = SetMods(modifiers=Mod4); };
    interpret Super_R { action = SetMods(modifiers=Mod4); };

    interpret ISO_Level3_Shift { action = SetMods(modifiers=Mod5); };

}

xkb_symbols "(unnamed)" {

    // "Real" modifiers only, available systemwide

    modifier_map Shift { <LFSH>, <RTSH> };
    modifier_map Lock { <CAPS> };
    modifier_map Control { <LCTL>, <RCTL> };
    modifier_map Mod1 { <LALT> };
    modifier_map Mod2 { <NMLK> };
    modifier_map Mod3 { <SCLK> };
    modifier_map Mod4 { <LWIN>, <RWIN> };
    modifier_map Mod5 { <RALT> };

    .
    .
    .
    // Modifiers keysym                                     //

    key <LFSH> { [ Shift_L ] };
    key <RTSH> { [ Shift_R ] };

    // not on my BFO
    key <CAPS> { [ Caps_Lock ] };

    key <LCTL> { [ Control_L ] };
    key <RCTL> { [ Control_R ] };

    //  mod1
    key <LALT> { [ Alt_L ] };
    //  mod5       This symbol does nothing in the OS
    key <RALT> { [ ISO_Level3_Shift ] };

    //  mod2
    key <NMLK> { [ Num_Lock ] };

    //  mod3
    key <SCLK> { [ Scroll_Lock ] };

    //  mod4
    key <LWIN> { [ Super_L ] };
    key <RWIN> { [ Super_R ] };

}

And thats it! Most keysyms get their type detected automagically:


    key <LatD> { [ d , D ] };                       // TWO_LEVEL
    key <LatE> { [ e , E, EuroSign, cent ] };       // FOUR_LEVEL_ALPHABETIC

Now define their behaviour:


xkb_types "(unnamed)" {

    type "ONE_LEVEL" {
        modifiers= none;

        level_name[1]= "Any";
    };

    type "TWO_LEVEL" {
        modifiers= Shift;

        map[Shift]= 2;

        level_name[1]= "Base";
        level_name[2]= "Shift";
    };
    .
    .
    .
    type "FOUR_LEVEL_ALPHABETIC" {
        modifiers= Shift+Lock+Mod5;

        map[Shift]= 2;
        map[Lock]= 2;
        map[Mod5]= 3;
        map[Shift+Mod5]= 4;
        map[Lock+Mod5]= 4;
        map[Shift+Lock+Mod5]= 3; // <-- we all remember this one! /s

        level_name[1]= "Base";
        level_name[2]= "Shift";
        level_name[3]= "AltGr";
        level_name[4]= "Shift AltGr";
    };
    .
    .
    .
};

Actions

easier to not get wrong when using interpret in compat

https://xkbcommon.org/doc/current/keymap-text-format-v1-v2.html#modifiers-bindings

Symbols

https://github.com/xkbcommon/libxkbcommon/blob/master/include/xkbcommon/xkbcommon-keysyms.h

Test your map

xkbcli compile-keymap --from-xkb < bfo.xkb

My Keymap

Im still playing with groups. Note a switch to Group 2 still maps keys from Group 1 if not defined so experiments are not leaving you with an unusable keyboard.

xkb_keymap {
xkb_keycodes "ortholinear" {
    minimum = 8;
    maximum = 255;

// Indicators                       //

    indicator 1 = "Caps Lock";
    indicator 2 = "Num Lock";
    indicator 3 = "Scroll Lock";
    indicator 4 = "Compose";
    indicator 5 = "Kana";
    indicator 6 = "Sleep";
    indicator 7 = "Suspend";
    indicator 8 = "Mute";
    indicator 9 = "Misc";
    indicator 10 = "Mail";
    indicator 11 = "Charging";
    indicator 12 = "Shift Lock";
    indicator 13 = "Group 2";
    indicator 14 = "Mouse Keys";

// Keycodes                         //

// Modifiers

    //  shift
    <LFSH> = 50;
    <RTSH> = 62;

    //  lock
    <CAPS> = 66;

    //  control
    <LCTL> = 37;
    <RCTL> = 105;

    //  mod1
    <LALT> = 64;
    //  mod5
    <RALT> = 108;

    //  mod2
    <NMLK> = 77;

    //  mod3 
    <SCLK> = 78;

    //  mod4
    <LWIN> = 133;
    <RWIN> = 134;

    // not a modN - Context-Menu-key
    <COMP> = 135;   

// Function Keys
    <FK01> = 67;
    <FK02> = 68;
    <FK03> = 69;
    <FK04> = 70;
    <FK05> = 71;
    <FK06> = 72;
    <FK07> = 73;
    <FK08> = 74;
    <FK09> = 75;
    <FK10> = 76;
    <FK11> = 95;
    <FK12> = 96;
    <FK13> = 191;
    <FK14> = 192;
    <FK15> = 193;
    <FK16> = 194;
    <FK17> = 195;
    <FK18> = 196;
    <FK19> = 197;
    <FK20> = 198;
    <FK21> = 199;
    <FK22> = 200;
    <FK23> = 201;
    <FK24> = 202;

// Numbers
    <Ara1> = 10;
    <Ara2> = 11;
    <Ara3> = 12;
    <Ara4> = 13;
    <Ara5> = 14;
    <Ara6> = 15;
    <Ara7> = 16;
    <Ara8> = 17;
    <Ara9> = 18;
    <Ara0> = 19;

// Latin
    <LatA> = 38;
    <LatB> = 56;
    <LatC> = 54;
    <LatD> = 40;
    <LatE> = 26;
    <LatF> = 41;
    <LatG> = 42;
    <LatH> = 43;
    <LatI> = 31;
    <LatJ> = 44;
    <LatK> = 45;
    <LatL> = 46;
    <LatM> = 58;
    <LatN> = 57;
    <LatO> = 32;
    <LatP> = 33;
    <LatQ> = 24;
    <LatR> = 27;
    <LatS> = 39;
    <LatT> = 28;
    <LatU> = 30;
    <LatV> = 55;
    <LatW> = 25;
    <LatX> = 53;
    <LatY> = 29;
    <LatZ> = 52;

// Stuff
    <ESC> = 9;
    <BKSP> = 22;
    <TAB> = 23;
    <RTRN> = 36;
    <SPCE> = 65;

// ,./\;
    <COMM> = 59;
    <PERI> = 60;
    <SLSH> = 61;
    <BSLS> = 51;
    <SEMI> = 47;
// []-=`'
    <BRAL> = 34;
    <BRAR> = 35;
    <MINU> = 20;
    <EQUA> = 21;
    <GRAV> = 49;
    <APOS> = 48;

// Arrows and so

    <PRSC> = 107;
    <PAUS> = 127;

    <HOME> = 110;
    <END>  = 115;

    <PGUP> = 112;
    <PGDN> = 117;

    <INS>  = 118;
    <DELE> = 119;

    <UP>   = 111;
    <LEFT> = 113;
    <RGHT> = 114;
    <DOWN> = 116;

// Numpad

    <KP1>  = 87;
    <KP2>  = 88;
    <KP3>  = 89;
    <KP4>  = 83;
    <KP5>  = 84;
    <KP6>  = 85;
    <KP7>  = 79;
    <KP8>  = 80;
    <KP9>  = 81;
    <KP0>  = 90;

    <KP.> = 91;
    <KP,> = 129;

    <KP/> = 106;
    <KP*> = 63;

    <KP->  = 82;
    <KP+>  = 86;

    <KP=>  = 125;
    <KPRT> = 104;

// Multimedia
    <VOL-> = 122;
    <VOL+> = 123;
    <BRI-> = 232;
    <BRI+> = 233;
    <PLPA> = 172;

// Aliases                          //

// Row / Column Aliases Lefthand

    alias <LH24> = <LatQ>;
    alias <LH25> = <LatW>;
    alias <LH26> = <LatF>;
    alias <LH27> = <LatP>;
    alias <LH28> = <LatB>;
    alias <LH29> = <GRAV>;

    alias <LH34> = <LatA>;
    alias <LH35> = <LatR>;
    alias <LH36> = <LatS>;
    alias <LH37> = <LatT>;
    alias <LH38> = <LatG>;
    alias <LH39> = <MINU>;

    alias <LH44> = <LatZ>;
    alias <LH45> = <LatX>;
    alias <LH46> = <LatC>;
    alias <LH47> = <LatD>;
    alias <LH48> = <LatV>;
    alias <LH49> = <BRAL>;

// Row / Column Aliases  Righthand

    alias <RH22> = <LatJ>;
    alias <RH23> = <LatL>;
    alias <RH24> = <LatU>;
    alias <RH25> = <LatY>;
    alias <RH26> = <SEMI>;
    alias <RH27> = <BKSP>;

    alias <RH32> = <LatM>;
    alias <RH33> = <LatN>;
    alias <RH34> = <LatE>;
    alias <RH35> = <LatI>;
    alias <RH36> = <LatO>;
    alias <RH37> = <RTRN>;

    alias <RH42> = <LatK>;
    alias <RH43> = <LatH>;
    alias <RH44> = <COMM>;
    alias <RH45> = <PERI>;
    alias <RH46> = <SLSH>;
    alias <RH47> = <BSLS>;

};

xkb_types "(unnamed)" {

    type "ONE_LEVEL" {
        modifiers= none;

        level_name[1]= "Any";
    };
    type "TWO_LEVEL" {
        modifiers= Shift;

        map[Shift]= 2;

        level_name[1]= "Base";
        level_name[2]= "Shift";
    };
    type "ALPHABETIC" {
        modifiers= Shift+Lock;

        map[Shift]= 2;
        map[Lock]= 2;

        level_name[1]= "Base";
        level_name[2]= "Caps";
    };
    type "FOUR_LEVEL" {
        modifiers= Shift+Mod5;

        map[Shift]= 2;
        map[Mod5]= 3;
        map[Shift+Mod5]= 4;

        level_name[1]= "Base";
        level_name[2]= "Shift";
        level_name[3]= "AltGr";
        level_name[4]= "Shift AltGr";
    };
    type "FOUR_LEVEL_ALPHABETIC" {
        modifiers= Shift+Lock+Mod5;

        map[Shift]= 2;
        map[Lock]= 2;
        map[Mod5]= 3;
        map[Shift+Mod5]= 4;
        map[Lock+Mod5]= 4;
        map[Shift+Lock+Mod5]= 3;

        level_name[1]= "Base";
        level_name[2]= "Shift";
        level_name[3]= "AltGr";
        level_name[4]= "Shift AltGr";
    };
    type "FOUR_LEVEL_SEMIALPHABETIC" {
        modifiers= Shift+Lock+Mod5;

        map[Shift]= 2;
        map[Lock]= 2;
        map[Mod5]= 3;
        map[Shift+Mod5]= 4;
        map[Lock+Mod5]= 3;

        preserve[Lock+Mod5]= Lock;
        map[Shift+Lock+Mod5]= 4;
        preserve[Shift+Lock+Mod5]= Lock;

        level_name[1]= "Base";
        level_name[2]= "Shift";
        level_name[3]= "AltGr";
        level_name[4]= "Shift AltGr";
    };
    type "FOUR_LEVEL_KEYPAD" {
        modifiers= Shift+Mod2+Mod5;

        map[Shift]= 2;
        map[Mod2]= 2;
        map[Mod5]= 3;
        map[Shift+Mod5]= 4;
        map[Mod2+Mod5]= 4;
        map[Shift+Mod2+Mod5]= 3;

        level_name[1]= "Base";
        level_name[2]= "Shift/Numlock";
        level_name[3]= "AltGr";
        level_name[4]= "Shift/Numlock AltGr";
    };
};

xkb_compatibility "(unnamed)" {

    // (indirect) virtual Modifier Map                          //

    // i only use indirect because else you need to use explicit actions - no interpret
    virtual_modifiers ModSwitchGroup = 0x1000; // 32-bit
    // Example to make it use Mod2+Mod3 so you can derive it from real-modifiers
    // virtual_modifiers SwitchGroup = Mod2+Mod3;

    // Modifier actions                                         //

    // interpret (a keysym) is easier not to get wrong

    //  Alternative on keysym:
    // key <LALT> {
    //    // Explicit virtual modifier map
    //    virtualModifiers = Alt,
    //    ...
    // };
    interpret F17 {
        //  Bind the virtual modifier…
        virtualModifier = ModSwitchGroup;
        //  … independently of the group and level (default)
        useModMap = AnyLevel;
        action = LockGroup(group=+1); // cycle groups while on any group - SAFE

        //  BEWARE this will lock the Group and then you cannot switch back:
        // action[Group1] = LockGroup(group=2)

        //  instead if you want this still, use:
        // action[Group1] = LockGroup(group=2)
        // action[Group2] = LockGroup(group=1)

        //  SetGroup also possible (Shift-like)
        // action = SetGroup(group=2);

    };

    //  You could map these as explicit actions on the keysym like:
    // key <RTSH> { [ Shift_R ], actions[Group1] = [ SetMods(modifiers=Shift) ] };
    // but DontRepeatYourself
    interpret Shift_L { action = SetMods(modifiers=Shift); };
    interpret Shift_R { action = SetMods(modifiers=Shift); };

    interpret Caps_Lock { action = LockMods(modifiers=Lock); };

    interpret Control_L { action = SetMods(modifiers=Control); };
    interpret Control_R { action = SetMods(modifiers=Control); };

    interpret Alt_L { action = SetMods(modifiers=Mod1); };

    interpret Num_Lock { action = LockMods(modifiers=Mod2); };

    interpret Scroll_Lock { action = LockMods(modifiers=Mod3); };

    interpret Super_L { action = SetMods(modifiers=Mod4); };
    interpret Super_R { action = SetMods(modifiers=Mod4); };

    interpret ISO_Level3_Shift { action = SetMods(modifiers=Mod5); };

    // Indicators                                                   //

    indicator "Shift Lock" {
        whichModState= locked;
        modifiers= Shift;
    };
    indicator "Caps Lock" {
        whichModState= locked;
        modifiers= Lock;
    };
    indicator "Num Lock" {
        whichModState= locked;
        modifiers= Mod2;
    };
    indicator "Scroll Lock" {
        whichModState= locked;
        modifiers= Mod3;
    };

    indicator "Group 2" {
        groups= 0xfe;
    };
    indicator "Mouse Keys" {
        controls= MouseKeys;
    };

};

xkb_symbols "(unnamed)" {

    name[Group1]="Colemak US Lvl 3 German Umlauts + Math";
    name[Group1]="Mooore Math";

    // Real Modifiers Map                                   //
    // always direct (never indirect in compat)

    // i only use direct keycodes
    modifier_map Shift { <LFSH>, <RTSH> };
    modifier_map Lock { <CAPS> };
    modifier_map Control { <LCTL>, <RCTL> };
    modifier_map Mod1 { <LALT> };
    modifier_map Mod2 { <NMLK> };
    modifier_map Mod3 { <SCLK> };
    modifier_map Mod4 { <LWIN>, <RWIN> };
    modifier_map Mod5 { <RALT> };

    // Example to bind indirectly to a keysym not directly to a keycode:
    // BUT this involves special rules so i dont do that
    // See "Setting the real modifier map" in https://xkbcommon.org/doc/current/keymap-text-format-v1-v2.html#modifiers-bindings
    // modifier_map Shift { Shift_L, Shift_R };

    // (direct) Virtual Modifiers Map                       //
    // use this if you only want explicit actions

    // virtual_modifiers ModSwitchGroup = 0x1000; // 32-bit
    // Example to make it use Mod2+Mod3 so you can derive it from real-modifiers
    // virtual_modifiers ModSwitchGroup = Mod2+Mod3;

    // Modifiers keysym                                     //

    key <LFSH> { [ Shift_L ] };
    key <RTSH> { [ Shift_R ] };

    // not on my BFO
    key <CAPS> { [ Caps_Lock ] };

    key <LCTL> { [ Control_L ] };
    key <RCTL> { [ Control_R ] };

    //  mod1
    key <LALT> { [ Alt_L ] };
    //  mod5       This symbol does nothing in the OS
    key <RALT> { [ ISO_Level3_Shift ] };

    //  mod2
    key <NMLK> { [ Num_Lock ] };

    //  mod3
    key <SCLK> { [ Scroll_Lock ] };

    //  mod4
    key <LWIN> { [ Super_L ] };
    key <RWIN> { [ Super_R ] };

    key <COMP> { [ Menu ] };

    // Symbols                                              //

    // Function Keys
    //          type FOUR_LEVEL
    key <FK01> { [ F1, F1, F1, F1 ] };
    key <FK02> { [ F2, F2, F2, F2 ] };
    key <FK03> { [ F3, F3, F3, F3 ] };
    key <FK04> { [ F4, F4, F4, F4 ] };
    key <FK05> { [ F5, F5, F5, F5 ] };
    key <FK06> { [ F6, F6, F6, F6 ] };

    key <FK07> { [ F7, F7, F7, F7 ] };
    key <FK08> { [ F8, F8, F8, F8 ] };
    key <FK09> { [ F9, F9, F9, F9 ] };
    key <FK10> { [ F10, F10, F10, F10 ] };
    key <FK11> { [ F11, F11, F11, F11 ] };

    key <FK12> { [ F12, F12, F12, F12 ] };
    key <FK13> { [ F13, F13, F13, F13 ] };
    key <FK14> { [ F14, F14, F14, F14 ] };
    key <FK15> { [ F15, F15, F15, F15 ] };
    key <FK16> { [ F16, F16, F16, F16 ] };
    key <FK17> { [ F17, F17, F17, F17 ] };

    // Example for explicit definition of a virtual modifier
    // i only use implicit in _compat
    // key <FK17> { virtualModifiers = ModSwitchGroup, [ F17, F17, F17, F17 ] };

    key <FK18> { [ F18, F18, F18, F18 ] };
    key <FK19> { [ F19, F19, F19, F19 ] };
    key <FK20> { [ F20, F20, F20, F20 ] };
    key <FK21> { [ F21, F21, F21, F21 ] };
    key <FK22> { [ F22, F22, F22, F22 ] };
    key <FK23> { [ F23, F23, F23, F23 ] };
    key <FK24> { [ F24, F24, F24, F24 ] };

    // Numbers
    //          type FOUR_LEVEL
    key <Ara1> { [ 1,     exclam,     onesuperior,      exclamdown ] };
    key <Ara2> { [ 2,     at,         twosuperior,      squareroot ] };
    key <Ara3> { [ 3,     numbersign, threesuperior,    cuberoot ] };
    key <Ara4> { [ 4,     dollar,     foursuperior,     fourthroot ] };
    //          type TWO_LEVEL
    key <Ara5> { [ 5,         percent ] };
    key <Ara6> { [ 6,     asciicircum ] };
    key <Ara7> { [ 7,       ampersand ] };
    key <Ara8> { [ 8,        asterisk ] };
    key <Ara9> { [ 9,       parenleft ] };
    key <Ara0> { [ 0,      parenright ] };

    // Latin
    //          type TWO_LEVEL - math gets extended via Row/Column LH/RH
    key <LatA> { [ a , A, adiaeresis, Adiaeresis ], [ b, B ] };
    key <LatB> { [ b , B ] };
    key <LatC> { [ c , C ] };
    key <LatD> { [ d , D ] };
    key <LatE> { [ e , E, EuroSign, cent ] };
    key <LatF> { [ f , F ] };
    key <LatG> { [ g , G ] };
    key <LatH> { [ h , H ] };
    key <LatI> { [ i , I ] };
    key <LatJ> { [ j , J ] };

    key <LatK> { [ k , K ] };
    key <LatL> { [ l , L ] };
    key <LatM> { [ m , M ] };
    key <LatN> { [ n , N ] };
    key <LatO> { [ o , O, odiaeresis, Odiaeresis ] };
    key <LatP> { [ p , P ] };
    key <LatQ> { [ q , Q ] };
    key <LatR> { [ r , R ] };
    key <LatS> { [ s , S, ssharp ] };
    key <LatT> { [ t , T ] };

    key <LatU> { [ u , U, udiaeresis, Udiaeresis ] };
    key <LatV> { [ v , V ] };
    key <LatW> { [ w , W ] };
    key <LatX> { [ x , X ] };
    key <LatY> { [ y , Y ] };
    key <LatZ> { [ z , Z ] };

    // Type ONE_LEVEL
    key <ESC>  { [ Escape ] };
    //                          maybe?
    key <BKSP> { [ BackSpace,   BackSpace ] };
    key <TAB>  { [ Tab,         ISO_Left_Tab ] };
    key <RTRN> { [ Return ] };
    key <SPCE> { [ space ] };

    // ,./\;    
    key <COMM> { [ comma,       less ] };
    key <PERI> { [ period,      greater ] };
    key <SLSH> { [ slash,       question ] };
    key <BSLS> { [ backslash,   bar ] };
    key <SEMI> { [ semicolon,   colon ] };

    // []-=`'
    key <BRAL> { [ bracketleft,     braceleft ] };
    key <BRAR> { [ bracketright,    braceright] };
    key <MINU> { [ minus,           underscore] };
    key <EQUA> { [ equal,           plus] };
    key <GRAV> { [ grave,           asciitilde] };
    key <APOS> { [ apostrophe,      quotedbl] };

    // Arrows and so
    key <PRSC> { [ Print,   Sys_Req ] };
    key <PAUS> { [ Pause,   Break ] };

    key <HOME> { [ Home ] };
    key <END>  { [ End ] };

    key <PGUP> { [ Prior ] };
    key <PGDN> { [ Next ] };

    key <INS>  { [ Insert ] };
    key <DELE> { [ Delete ] };

    key <UP>   { [ Up ] };
    key <LEFT> { [ Left ] };
    key <RGHT> { [ Right ] };
    key <DOWN> { [ Down ] };

    // Numpad

    key <KP1>  { [ KP_1 ] };
    key <KP2>  { [ KP_2 ] };
    key <KP3>  { [ KP_3 ] };
    key <KP4>  { [ KP_4 ] };
    key <KP5>  { [ KP_5 ] };
    key <KP6>  { [ KP_6 ] };
    key <KP7>  { [ KP_7 ] };
    key <KP8>  { [ KP_8 ] };
    key <KP9>  { [ KP_9 ] };
    key <KP0>  { [ KP_0 ] };

    key <KP.>  { [ KP_Decimal ] };
    key <KP,>  { [ KP_Separator ] };

    key <KP/>  { [ KP_Divide ] };
    key <KP*>  { [ KP_Multiply ] };

    key <KP->  { [ KP_Subtract ] };
    key <KP+>  { [ KP_Add ] };

    key <KP=>  { [ KP_Equal ] };
    key <KPRT> { [ KP_Enter ] };

    // Multimedia
    key <VOL-> { [ XF86AudioLowerVolume ] };
    key <VOL+> { [ XF86AudioRaiseVolume ] };
    key <BRI-> { [ XF86MonBrightnessDown ] };
    key <BRI+> { [ XF86MonBrightnessUp ] };
    key <PLPA> { [ XF86AudioPlay,           XF86AudioPause ] };

    // Maths 

};

};

Actually use it:

sway config, use output of swaymsg -t get_inputs

# Keebio BFO-9000
input "51984:4457:Keebio_BFO-9000" {
    xkb_file ~/.config/xkb/bfo.xkb
    repeat_delay 200
    repeat_rate 60
}

input "51984:4457:Keebio_BFO-9000_Consumer_Control" {
    xkb_file ~/.config/xkb/bfo.xkb
    repeat_delay 200
    repeat_rate 60
}

Further reads:

Shift Latch Lock

https://xkbcommon.org/doc/current/faq.html#how-do-i-break-a-latch-before-triggering-another-latch-or-lock

Indicators:

Gemini

1. Kernel sends EV_KEY to XKB
2. XKB toggles its internal "Lock" modifier state
3. XKB triggers its internal "indicator"
4. Compositor tells kernel to send EV_LED (Type 17) to /dev/input/eventX
5. Kernel sends a USB HID Output Report packet back down the USB cable (value 0 is ON, 1 is OFF)

http://cgit.freedesktop.org/xkeyboard-config

https://xkbcommon.org/doc/current/custom-configuration.html


https://matthewsanabria.com/posts/functional-keys-with-x-keyboard-extension/

https://who-t.blogspot.com/2020/02/user-specific-xkb-configuration-part-1.html


https://www.charvolant.org/doug/xkb/html/xkb.html

https://www.charvolant.org/doug/xkb/html/node5.html