diff --git a/content/posts/vim-like-layer-for-xorg-wayland.md b/content/posts/vim-like-layer-for-xorg-wayland.md new file mode 100644 index 0000000..f22af65 --- /dev/null +++ b/content/posts/vim-like-layer-for-xorg-wayland.md @@ -0,0 +1,312 @@ ++++ +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`. diff --git a/static/images/insert_mode.jpg b/static/images/insert_mode.jpg new file mode 100644 index 0000000..6b139e9 Binary files /dev/null and b/static/images/insert_mode.jpg differ diff --git a/static/images/normal_mode.jpg b/static/images/normal_mode.jpg new file mode 100644 index 0000000..d286f05 Binary files /dev/null and b/static/images/normal_mode.jpg differ diff --git a/static/images/normal_mode_unbound.jpg b/static/images/normal_mode_unbound.jpg new file mode 100644 index 0000000..712dec9 Binary files /dev/null and b/static/images/normal_mode_unbound.jpg differ