+++ title = "Vim-like Layer for Xorg and Wayland" date = "2020-08-25" author = "Ceda EI" tags = ["xorg", "wayland", "vim"] keywords = ["xorg", "wayland", "vim"] description = "Create a layer of shortcuts with xkb for Xorg and Wayland" showFullContent = false +++ # The Goal ## Insert Mode ![Insert Mode: A keyboard layout similar to normal QWERTY layout](/images/insert_mode.jpg) ## Normal Mode ![Normal Mode: A keyboard layout with the alphabet keys replaced with shortcut key](/images/normal_mode.jpg) Inspired by vim, I wanted to create a layer on top of my keyboard which worked like a shortcut layer. So, to start off, I found out about XKB[^1]. XKB is the Xorg Keyboard Extension which tells Xorg on how to react to input from keyboard. After reading through some source code, I found out that Xorg has support for function keys F1 - F35[^2]. The general idea here was: - Create an insert mode layout for text input. - Replace keys with relevant keys in normal mode (e.g. replace j with Down) and for keys that require executing a command, replace then with a function key above F12 (e.g. replace q with F13). - Bind all the function keys above F12 to the respective functions. To start off, I began a fresh Xorg session with nothing modifying the keys (removed `xmodmap` from startup) and first dumped the current layout into a file. ```bash xkbcomp $DISPLAY ~/.xkb/insert.xkb ``` This was my starting point. I made changes to this file which were common to both Insert and Normal mode. e.g. replaced `Caps Lock` with `Ctrl` and made `Shift+Caps Lock` `Caps Lock`. Also, I unbound `Alt_R` as a modifier so that I could use that as a switch between Normal and Insert Mode. Here is a diff between the original layout and Insert mode. ``` 1323c1321 < key { [ Caps_Lock ] }; --- > key { [ Control_L, Caps_Lock ] }; 1551c1549 < modifier_map Lock { }; --- > modifier_map Control { }; 1555d1552 < modifier_map Mod1 { }; ``` Next, I copied `~/.xkb/insert.xkb` to `~/.xkb/normal.xkb`. I replaced keys as per the plan. Here is a diff between Insert mode and Normal mode. ```diff 1200c1200 < symbols[Group1]= [ q, Q ] --- > symbols[Group1]= [F13] 1204c1204 < symbols[Group1]= [ w, W ] --- > symbols[Group1]= [F14] 1208c1208 < symbols[Group1]= [ e, E ] --- > symbols[Group1]= [F15] 1212c1212 < symbols[Group1]= [ r, R ] --- > symbols[Group1]= [F16] 1216c1216 < symbols[Group1]= [ t, T ] --- > symbols[Group1]= [F17] 1220c1220 < symbols[Group1]= [ y, Y ] --- > symbols[Group1]= [F18] 1224c1224 < symbols[Group1]= [ u, U ] --- > symbols[Group1]= [F19] 1228c1228 < symbols[Group1]= [ i, I ] --- > symbols[Group1]= [Alt_R] 1232c1232 < symbols[Group1]= [ o, O ] --- > symbols[Group1]= [F20] 1236c1236 < symbols[Group1]= [ p, P ] --- > symbols[Group1]= [F21] 1244c1244 < symbols[Group1]= [ a, A ] --- > symbols[Group1]= [F22] 1248c1248 < symbols[Group1]= [ s, S ] --- > symbols[Group1]= [Delete] 1252c1252 < symbols[Group1]= [ d, D ] --- > symbols[Group1]= [BackSpace] 1256c1256 < symbols[Group1]= [ f, F ] --- > symbols[Group1]= [Home] 1260c1260 < symbols[Group1]= [ g, G ] --- > symbols[Group1]= [End] 1264c1264 < symbols[Group1]= [ h, H ] --- > symbols[Group1]= [Left] 1268c1268 < symbols[Group1]= [ j, J ] --- > symbols[Group1]= [Down] 1272c1272 < symbols[Group1]= [ k, K ] --- > symbols[Group1]= [Up] 1276c1276 < symbols[Group1]= [ l, L ] --- > symbols[Group1]= [Right] 1285c1285 < symbols[Group1]= [ z, Z ] --- > symbols[Group1]= [F23] 1289c1289 < symbols[Group1]= [ x, X ] --- > symbols[Group1]= [F24] 1293c1293 < symbols[Group1]= [ c, C ] --- > symbols[Group1]= [F25] 1297c1297 < symbols[Group1]= [ v, V ] --- > symbols[Group1]= [F26] 1301c1301 < symbols[Group1]= [ b, B ] --- > symbols[Group1]= [F27] 1305c1305 < symbols[Group1]= [ n, N ] --- > symbols[Group1]= [Next] 1309c1309 < symbols[Group1]= [ m, M ] --- > symbols[Group1]= [Prior] ``` At this point, `normal.xkb` file defines the following layout. ![Normal Mode: A keyboard ](/images/normal_mode_unbound.jpg) Now, we need a script that switches between layouts. To load an layout in Xorg, we use ```bash xkbcomp ~/.xkb/normal.xkb "$DISPLAY" ``` Sway supports this via the input command in the following form. ```bash swaymsg input '*' xkb_file ~/.xkb/normal.xkb ``` The following script cycles through the layouts when it is called. It also allows to add more layouts later (just add them to layouts array and it will cycle in the order of the array). ```bash #!/usr/bin/env bash # Usage: xkb_swapper.sh [layout_name] function set_layout() { echo "Setting layout to $1" if [[ -v WAYLAND_DISPLAY ]]; then swaymsg input '*' xkb_file ~/.xkb/"$1".xkb else xkbcomp ~/.xkb/"$1".xkb "$DISPLAY" fi echo "$1" > ~/.cache/xkb-curr-"$DISPLAY" } layouts=(insert normal) current_layout=$(cat ~/.cache/xkb-curr-"$DISPLAY" || echo "") if [[ $1 != "" ]]; then set_layout "$1" exit fi if [[ $current_layout == "" ]]; then echo "No current layout found!" set_layout "${layouts[0]}" fi i=0 while [[ $i -lt ${#layouts[@]} ]]; do if [[ $current_layout == "${layouts[$i]}" ]]; then new_idx="$((i+1))" if [[ $new_idx -eq ${#layouts[@]} ]]; then set_layout "${layouts[0]}" else set_layout "${layouts[$new_idx]}" fi exit fi ((i++)) done echo "Current Layout doesn't exist!" set_layout "${layouts[0]}" ``` The above script works with all Xorg based DE/WMs as well as Sway (wayland compositor). I saved it as `xkb_swapper.sh` in my `PATH`. Calling the script without any argument cycles through the layouts. If arguments are passed, the first argument is taken as layout name and layout is changed to that. The last step is binding the function keys and `Alt_R` to commands to execute. Here are some of the parts of my i3 config that bind the function keys. ``` bindsym Alt_R exec xkb_swapper.sh bindsym 0xffca kill bindsym 0xffcf exec volchange -5 bindsym 0xffd0 exec volchange +5 bindsym 0xffd1 exec brightness -200 bindsym 0xffd2 exec brightness +200 bindsym 0xffcb exec mpc prev bindsym 0xffcc exec mpc toggle bindsym 0xffcd exec mpc next ``` `i3` doesn't seem to accept `F13` - `F35` as keynames however it accepts the keycodes[^2]. Here is a small list for easy access. ``` 0xffbe F1 0xffbf F2 0xffc0 F3 0xffc1 F4 0xffc2 F5 0xffc3 F6 0xffc4 F7 0xffc5 F8 0xffc6 F9 0xffc7 F10 0xffc8 F11 0xffc9 F12 0xffca F13 0xffcb F14 0xffcc F15 0xffcd F16 0xffce F17 0xffcf F18 0xffd0 F19 0xffd1 F20 0xffd2 F21 0xffd3 F22 0xffd4 F23 0xffd5 F24 0xffd6 F25 0xffd7 F26 0xffd8 F27 0xffd9 F28 0xffda F29 0xffdb F30 0xffdc F31 0xffdd F32 0xffde F33 0xffdf F34 0xffe0 F35 ``` # Bonus: Displaying the current mode in your bar The script stores the mode in `~/.cache/xkb-curr-$DISPLAY`. `cat` that and wrap in your bar's config. Here is my config for [i3status-rust](https://github.com/greshake/i3status-rust). ```toml [[block]] block = "custom" command = "echo -en '\\uf11c '; cat ~/.cache/xkb-curr-$DISPLAY" interval = 0.5 ``` [^1]: As always, the [Arch Wiki page on XKB](https://wiki.archlinux.org/index.php/X_keyboard_extension) is a nice place to start. [^2]: You can find all the defined keys in `/usr/include/X11/keysymdef.h`.