313 lines
8.2 KiB
Markdown
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`.
|