cedaei.com/content/posts/vim-like-layer-for-xorg-way...

313 lines
8.2 KiB
Markdown

+++
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> { [ Caps_Lock ] };
---
> key <CAPS> { [ Control_L, Caps_Lock ] };
1551c1549
< modifier_map Lock { <CAPS> };
---
> modifier_map Control { <CAPS> };
1555d1552
< modifier_map Mod1 { <RALT> };
```
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`.