Lightweight Wayland text expander. Reads raw keyboard events via evdev, types replacements directly via uinput. Works on any Wayland compositor (KDE, GNOME, Hyprland, Sway, etc.).
Single static binary. YAML config (espanso-compatible format). Zero runtime dependencies (optional: wtype for Unicode, wl-clipboard as last-resort fallback).
Warning: This was vibe coded. It works, but don't expect anything from it xD.
[Keyboard] ββevdevβββ texpand ββuinputβββ [Any App]
- Monitors
/dev/input/event*devices via evdev (non-exclusive) - Maintains a rolling buffer of recent keystrokes
- On match: backspaces the trigger, types the replacement via uinput (falls back to
wtypefor Unicode, then clipboard paste as last resort)
texpand watches /dev/input while it runs. If a keyboard disappears and
reappears, for example when a monitor with an attached USB hub changes input or
powers off and on, texpand rescans devices and starts monitoring the new event
node without requiring a service restart.
Two trigger modes (set globally in config.yml):
- Space (default): fires when space is pressed after the trigger
- Immediate: fires as soon as the trigger is typed
Config changes are picked up automatically β no restart needed.
go install github.com/andresousadotpt/texpand@latesttexpand initCreates ~/.config/texpand/match/ with default YAML trigger files.
texpand reads from /dev/input/ and writes to /dev/uinput.
# Add your user to the input group
sudo usermod -aG input $USER
# Ensure the uinput module loads at boot
echo uinput | sudo tee /etc/modules-load.d/uinput.conf
sudo modprobe uinput
# Allow input group to write to /dev/uinput
sudo cp 99-uinput.rules /etc/udev/rules.d/99-uinput.rules
sudo udevadm control --reload-rules && sudo udevadm trigger
# Log out and back in for group change to take effectcp texpand.service ~/.config/systemd/user/texpand.service
systemctl --user daemon-reload
systemctl --user enable --now texpand.servicego install github.com/andresousadotpt/texpand@latest
systemctl --user restart texpand.serviceAfter updating, run the migration command to update your config files to the latest format:
texpand migrateThis safely removes deprecated fields, creates .bak backups of modified files, and is idempotent (safe to run multiple times).
To pick up new default config files (without overwriting your existing ones):
texpand initYAML files in ~/.config/texpand/match/*.yml. Espanso-compatible subset.
~/.config/texpand/config.yml controls global behavior:
# "space" (default) - triggers fire on space
# "immediate" - triggers fire as soon as typed
trigger_mode: spacematches:
- trigger: "'date"
replace: "{{_date}}"matches:
- triggers: ["'binsh", "'#!"]
replace: "#!/bin/sh"global_vars:
- name: _date
type: date
params:
format: "%d/%m/%Y"
matches:
- trigger: "'date"
replace: "{{_date}}"matches:
- trigger: "'tdate"
replace: "{{tomorrow}}"
vars:
- name: tomorrow
type: date
params:
format: "%a %m/%d/%Y"
offset: 86400Use $|$ to mark where the cursor should land after expansion:
matches:
- trigger: "'11"
replace: "{{time_with_ampm}} - 1:1 with [$|$]"| Token | Meaning | Example |
|---|---|---|
%Y |
4-digit year | 2026 |
%m |
Month (zero-padded) | 02 |
%d |
Day (zero-padded) | 23 |
%H |
Hour 24h | 14 |
%I |
Hour 12h | 02 |
%M |
Minute | 30 |
%S |
Second | 05 |
%p |
AM/PM | PM |
%a |
Short weekday | Mon |
%A |
Full weekday | Monday |
%b |
Short month | Jan |
%B |
Full month | January |
| Trigger | Output | Trigger | Output |
|---|---|---|---|
]a |
Γ‘ | ]A |
Γ |
}a |
Γ | }A |
Γ |
~a |
Γ£ | ~o |
Γ΅ |
]e |
Γ© | ]E |
Γ |
}e |
Γ¨ | }E |
Γ |
]i |
Γ | ]I |
Γ |
}i |
Γ¬ | }I |
Γ |
]o |
Γ³ | ]O |
Γ |
}o |
Γ² | }O |
Γ |
]u |
ΓΊ | ]U |
Γ |
}u |
ΓΉ | }U |
Γ |
'c, |
Γ§ |
| Trigger | Output |
|---|---|
'deg |
ΒΊ |
'... |
... |
euros |
β¬ |
| Trigger | Output |
|---|---|
'binsh / '#! |
#!/bin/sh |
'gsm |
git switch main && git pull origin main |
'gpomr |
git pull origin main --rebase |
texpand [--debug] [init|version|migrate]
| Command | Description |
|---|---|
| (none) | Run texpand (monitor keyboards, expand triggers) |
init |
Create default config in ~/.config/texpand/ |
version |
Print version |
migrate |
Migrate config files to the latest format |
| Trigger | Example output |
|---|---|
'n |
10:56 AM - |
'date |
23/02/2026 |
'ddate |
Mon 23/02/2026 |
'nn |
Mon 23/02/2026 - 10:56 AM - |
'st |
Mon 23/02/2026 - 10:56 AM - meeting start |
'end |
Mon 23/02/2026 - 10:56 AM - meeting end |
'11 |
10:56 AM - 1:1 with [cursor] |
'tdate |
Tomorrow's date |
'ydate |
Yesterday's date |
Edit or create YAML files in ~/.config/texpand/match/. Changes are picked up automatically β no restart needed.
systemctl --user status texpand.service # Check status
journalctl --user -u texpand.service -f # View logs
systemctl --user restart texpand.service # Restart after config changes
systemctl --user stop texpand.service # Stop
systemctl --user disable texpand.service # Disable auto-startRun texpand directly in a terminal (not via systemd) to see diagnostic output:
# Stop the service first to avoid conflicts
systemctl --user stop texpand.service
# Run in foreground β shows detected keyboards and trigger count
./texpandYou'll see output like:
texpand: monitoring 2 keyboard(s) β 35 triggers loaded
AT Translated Set 2 keyboard
Logitech USB Receiver
Use --debug (or -d) for verbose output on stderr β shows config loading, trigger mode, loaded triggers, buffer state, and match decisions:
./texpand --debugRun texpand init to see the config directory, then inspect the YAML files:
texpand init # Shows config path, skips existing files
ls ~/.config/texpand/match/To see raw kernel input events (useful for verifying your keyboard is detected):
# List all input devices
ls -la /dev/input/event*
# Watch events from a specific device (Ctrl+C to stop)
# Requires: sudo pacman -S evtest
sudo evtest /dev/input/event0If triggers fire but paste wrong text, verify wl-clipboard works:
echo "test" | wl-copy
wl-paste -n # Should print "test"# Live logs
journalctl --user -u texpand.service -f
# Last 50 lines
journalctl --user -u texpand.service -n 50
# Since last boot
journalctl --user -u texpand.service -bgroups # Should include 'input'
sudo usermod -aG input $USER
# Log out and back intexpand automatically rescans /dev/input when keyboard event devices are
created, removed, renamed, or have permissions updated. It also performs a
periodic rescan as a fallback.
Run with debug logging if expansions stop after changing monitor input or power cycling a monitor:
./texpand --debugThe logs include keyboard disconnect, reconnect, and rescan messages. If no
keyboard is reconnected, check that the new /dev/input/event* device is
readable by your user and that your user is still in the input group.
The most common cause is the uinput kernel module not being loaded at boot:
# Ensure the module loads at boot and load it now
echo uinput | sudo tee /etc/modules-load.d/uinput.conf
sudo modprobe uinput
# Install the udev rule and reload
sudo cp 99-uinput.rules /etc/udev/rules.d/99-uinput.rules
sudo udevadm control --reload-rules && sudo udevadm trigger
# If udevadm trigger fails with "No such device", fix permissions manually:
sudo chgrp input /dev/uinput && sudo chmod 0660 /dev/uinput
ls -la /dev/uinput # Should show crw-rw---- root inputtexpand auto-detects the Wayland socket at startup. If it fails:
systemctl --user import-environment WAYLAND_DISPLAY
systemctl --user restart texpand.serviceThe keymap assumes US/International layout. Letters and numbers work across layouts, but symbol keys (], }, ~, ') may differ.
MIT