sdiol
(pronounced: "style") extends standard IO devices, like keyboards and
mice, with sophisticated features like multi-layered keymaps and network
serving. Possible use-cases include:
-
Converting caps lock into a control key.
-
Configuring left shift key to be a shift key when held or an escape key when tapped.
-
Configuring the f key to be an f when tapped or to modify the h/j/k/l keys to behave like arrow keys while f is held.
-
Configure multiple keyboards with different keybindings.
-
Share a keyboard and/or mouse between multiple desktop/laptop computers over the network so you don't have to constantly plug and unplug devices or keep multiple keyboards and/or mice on your desk.
sdiol
operates at the device level, so it works equally well in tty, X
windows, or Wayland environments.
See section on Building
(below) if you want to follow along locally.
Here is the output of sdiol --help
:
usage: sdiol local # modify local IO
usage: sdiol serve unix_socket # serve IO over a unix socket
usage: sdiol read # read IO from STDIN
# insecure, experimental features:
usage: sdiol serve-tcp [host] port # serve IO over the network
usage: sdiol connect host port # read IO from the network
general options:
-h, --help print this help text
-c, --config FILE set config file (default /etc/sdiol/conf.lua)
-v, --verbose print useful info while running
--timeout N exit after N seconds (for testing)
--systemd run as systemd Type=notify service
options specific to sdiol serve:
--chown-socket USER:GROUP set user and group of unix socket
--chmod-socket MODE set mode of unix socket, e.g. 600
sdiol
requires a Lua configuration file to run (see Configuration Reference
,
below, for details). By default it looks for /etc/sdiol/conf.lua
, but we
can also specify a file with the --config
option.
So let's start by creating a file called conf.lua
with the following content:
-- the f key will expose an alternate layer of the keymap when held
f_keymap = {
-- when f is held, convert h/j/k/l to arrow keys
KEY_H = KEY_LEFT,
KEY_J = KEY_DOWN,
KEY_K = KEY_UP,
KEY_L = KEY_RIGHT,
-- also convert u and i to left and right parentheses
KEY_U = shift(KEY_9),
KEY_I = shift(KEY_0),
}
root_keymap = {
-- caps lock is way too useless of a key for your home row
KEY_CAPSLOCK = KEY_LEFTCTRL,
-- assign f to have different behavior on tap vs hold
KEY_F = dual_key(KEY_F, f_keymap),
}
-- use a case-insensitive regex to identify the keyboard device(s) to grab
grab_keyboard(".*keyboard.*", root_keymap)
Now we will do a test run:
sudo ./sdiol local --config conf.lua --timeout 20 --verbose
The argument meanings are as follows:
-
sudo
is required because we are trying to claim hardware devices -
local
means we are intercepting a local keyboard -
--timeout 20
means "exit after 20 seconds", so if we lose the ability to typectrl-c
, we don't have to reboot the computer -
--verbose
will help us debug our config by printing key names after each key press and by printing device names at startup or after a new device is plugged in
If you don't see a line like grabbing <your keyboard name>
you may have to
edit the regex in conf.lua
before continuing.
Now, while the sdiol
process is running, you can open a text editor and
verify the following behaviors:
-
The caps lock key has become a control key
-
Tapping f types an f (but not until the key is released)
-
Holding f does not type an f, but instead turns h/j/k/l into arrow keys and u/i into left/right parentheses for as long as you hold f
-
Quickly double tapping f (but holding the second press) will auto-repeat the f key for as long as you hold the second press
-
All other keys behave normally
sdiol
is configured in Lua. A config is required, and can either be at the
default location (/etc/sdiol/conf.lua
) or at a location given by the
--config
option.
The Lua functions available for configuration are as follows:
Grab any keyboard with a name matching REGEX and assign it a keymap of MAP.
REGEX should be a string and MAP should be a Lua table with string keys. The
keys of the table should be the names of Linux inputs, as you might find either
through the use of the --verbose
flag or by looking them up in
/usr/include/linux/input-event-codes.h
. The values in MAP indicate what
action should be taken when the corresponding key is pressed.
There are four types of actions:
-
a key name, such as
KEY_A
-
a
dual_key()
, to indicate a key which can one of two actions based on whether it is tapped or held -
a
macro()
,shift()
,ctrl()
,alt()
, ormeta()
to indicate a sequence of keys -
another keymap like MAP, to indicate that a key exposes an alternate keymapping on the keyboard
Don't grab any keyboard with a name matching REGEX. Multiple grab_keyboard()
and ignore_keyboard()
functions can be in the configuration, and the first
REGEX to match a keyboard name determines the behavior for that keyboard.
A key which takes one of two actions based on how long it is held. Both TAP
and HOLD should be actions. Note that TAP cannot be a keymap action, and
neither TAP nor HOLD can be nested dual_key()
actions.
Dual-mode keys are very flexible. They are configured by passing a lua table of configurations as the optional third argument. The allowable keys in the dictionary are as follows:
-
HOLD_MS
: a number of milliseconds after which a pressed dual-mode key will take on its HOLD behavior (default:200
). -
DOUBLE_TAP_MS
: the maximum number of milliseconds between when a key is released and when it is re-pressed for it to qualify as a possible double tap (default:300
). Set to0
to allow for infinite time, or to-1
to disable double tap behavior. A double tap is when you press a dual-mode key twice in quick succession, holding it the second time. This would be desirable if f were a dual-mode key, but you wanted to get the automatic key repeat behavior of holding a normal (non-dual-mode) f key, such as to page though a very long file inless
. -
MODE
: By default, a key is considered "held" if it is pressed for longer than itsHOLD_TIMEOUT
(200ms by default) or if some second key is both pressed and released before the first key is pressed (press A, press B, release B, release A). The in-between case (press A, press B, release A, release B) is called "rollover", and its behavior is configurable using the optional MODE argument. MODE can be one of:-
TAP_ON_ROLLOVER
(the default) should be used for making dual-mode keys out of normal keys, like f. When you press f, press another key, release f, and release the other key, (a "rollover" situation in typing) you will get the behavior specified by the TAP argument (probably a normal f) -
HOLD_ON_ROLLOVER
should generally be used for making dual-mode keys out of modifier keys, like shift. When you press shift, press another key, release shift, and release the other key, you will get the behavior specified by the HOLD argument (probably apply shift to the second key). -
TIMEOUT_ONLY
means that the only way to trigger the HOLD behavior is with the 200ms timeout.
-
So a customized dual-mode key action for shift might be created like this:
root_keymap = {
-- Modifiers like shift work better with HOLD_ON_ROLLOVER, and my
-- pinky is slow, so increase the limits for holds and double taps.
KEY_LEFTSHIFT = dual_key(KEY_SHIFT, KEY_ESCAPE, {
MODE = HOLD_ON_ROLLOVER,
HOLD_MS = 300,
DOUBLE_TAP_MS = 400,
}),
}
A macro is series of key names which should be pressed/released in sequence.
shift()
and ctrl()
are just macros where a modifier key will be held
throughout. Key macros are composable, meaning that the key action defined by:
macro(KEY_A, KEY_B, KEY_C, shift(KEY_A, KEY_B, KEY_C))
would type abcABC
when triggered.
A note on terminology: Here, the "server" refers to the machine with a keyboard and/or mouse plugged in to it, and the "client" refers to the machine on which you want to receive keyboard and/or mouse events.
Using sdiol serve
effectively has some additional requirements:
-
Have working
ssh
access from the client to the server. -
Install the openbsd variant of netcat installed on the server (or an appropriate substitute).
-
A working knowledge of file permissions so other users with access to the server are not able to log all your keystrokes.
A simple /etc/sdiol/conf.lua
for grabbing a keyboard and mouse might look
like this:
grab_keyboard(".*keyboard.*", {})
-- future mouse support will include mouse-specific features,
-- but until then mice are grabbed with grab_keyboard()
grab_keyboard(".*mouse.*", {})
Then, assuming your user name was bob
and you wanted to make sure that only
your user could access the sdiol
server, you could start it like this:
sudo sdiol serve /run/sdiol.sock --chown-socket bob:bob
Be sure to check out the --chown-socket
and --chmod-socket
options and make
sure you get the correct permissions on your socket for your environment.
sdiol
does not do secure networking at all, so instead we will use ssh
to
encrypt the connection to the server, then on the server we will use nc
to
connect to the socket. This leaves sdiol
to simply read events from stdin:
ssh bob@myserver nc -U /run/sdiol.sock </dev/null | sudo sdiol read
And you're in business!
(The </dev/null
is to make sure that the command works ok with background
execution, where trying to read from stdin may behave oddly)
If a new client connects to the sdiol
server, the first client will be
disconnected. In the future, sdiol
will be able to maintain multiple client
connections and will support cycling through them using a keybinding on the
shared keyboard.
First install dependencies:
-
Archlinux:
sudo pacman -S libsystemd liblua cmake
-
Debian/Ubuntu:
sudo apt install libsystemd-dev liblua5.3-dev cmake
Then run the following build steps from the sdiol
directory:
mkdir build
cd build
cmake ..
make
From the build directory, just run:
sudo make install
When you are satisfied with your sdiol
configuration at /etc/sdiol/conf.lua
,
you can start sdiol
as a background service:
sudo systemctl start sdiol.service
# Then make sure it's working:
sudo systemctl status sdiol.service
And if you want sdiol
to start automatically on boot:
sudo systemctl enable sdiol.service
sdiol
began as a fork of dzhu/keytap. Many
thanks to dzhu for innovating such a flexible
architecture.
khash.h
was taken from
attractivechaos/klib. Many thanks
to attractivechaos for maintaining a
freely-available generics library for C.
The file khash.h
is from attractivechaos's
klib project.
All other source files are in the public domain, under the conditions of the Unlicense.