# SnipExpand
Fast, config-based text expansion for Linux and Wayland. **First-class support for [Omarchy](https://omarchy.org) and [Hyprland](https://hypr.land).**
[](https://github.com/silouanwright/snipexpand/actions/workflows/ci.yml)
[](https://github.com/silouanwright/snipexpand/actions/workflows/release.yml)
[](LICENSE)
<p>
<a href="docs/assets/hero-demo.mp4">
<img src="docs/assets/hero-demo.gif" width="640" alt="SnipExpand replacing short triggers with an email address, emoji, Unicode text, a multiline signature, and code">
</a>
</p>
## Features
- System-wide, clipboard-free expansion
- Immediate or terminator-based expansion
- Plain text and multiline replacements
- Cursor placement with `$|$`
- Recursive YAML configuration with automatic reload
- Multiple triggers for one replacement
- Regex triggers with named captures
- Reusable nested snippets and date variables
- Configurable word boundaries and case propagation
- Search labels and terms for picker integrations
- Immediate Backspace undo for simple expansions
- Application exclusions and per-application profiles
- Pause and resume controls through the CLI, IPC, and Omarchy plugin
- Deliberate duplicate-trigger selection by source
- Git-published snippet packs with pinned revisions and explicit updates
- Direct installation of strictly compatible Espanso packs
- Persistent Wayland injection with a `uinput` fallback
- Strict validation and diagnostics
## Why SnipExpand?
Text expansion on Omarchy and Hyprland is still unreliable or awkward. Compare
SnipExpand with the alternatives below.
## Alternatives
| [Espanso](https://espanso.org) | Cross-platform automation, forms, scripts, and packages | Built for Omarchy and Hyprland; Espanso has documented Linux and Wayland issues with [application compatibility](https://github.com/espanso/espanso/issues/2162), [startup reliability](https://github.com/espanso/espanso/issues/2223), and [expansions stopping over time](https://github.com/espanso/espanso/issues/2423) |
| [Taurine](https://github.com/ereinaimer/taurine) | Cross-platform Rust automation with scripts, conversions, and optional AI | A local-only core, YAML configuration, persistent Wayland injection, and GPL licensing |
| [FlitKey](https://github.com/swarajnandedkar/FlitKey) | A graphical picker with hotkeys, imports, and expansion packs | Typed Wayland expansion instead of copy and paste, with no Python GUI runtime |
| [AutoKey for Wayland](https://github.com/dlk3/autokey-wayland) | GUI automation and Python scripting | Hyprland support and a native Rust daemon; AutoKey's Wayland fork targets GNOME |
| [Texpand](https://github.com/andresousadotpt/texpand) | Lightweight Go, YAML, and cursor placement | Rust, persistent Wayland injection, validation, exclusions, and diagnostics |
| [text-expander-wayland](https://github.com/quantavil/text-expander-wayland) | Rust, Espanso-style YAML, variables, and optional AI | Persistent injection instead of launching `wtype` or `ydotool` for each expansion |
| [SRKT](https://github.com/aaaorg/srkt) | A small Rust foundation for Wayland expansion | YAML, multiline matches, cursor placement, reloads, exclusions, and runtime tooling |
## Requirements
Run SnipExpand from a local Wayland desktop session with:
- `libxkbcommon` and Wayland client libraries
- Read access to keyboard devices under `/dev/input/` (see setup below)
- `wtype` for the Unicode fallback path
## Install
Install from crates.io:
```bash
cargo install snipexpand
```
Prebuilt x86_64 and aarch64 binaries are available from
[GitHub Releases](https://github.com/silouanwright/snipexpand/releases).
## Set up
```bash
snipexpand install --keyboard-access
snipexpand doctor
```
Run these commands as your desktop user. The `--keyboard-access` option requests
administrator authentication to install a keyboard-only udev rule. The rule
grants keyboard access to the active local session and applies again after a
reboot or keyboard reconnect. It does not add your user to the `input` group.
Setup creates missing starter files without overwriting your config, then
starts and enables the user service. It checks that a keyboard can actually be read before
reporting success. If your system already provides keyboard access, you can
use `snipexpand install` without the flag.
### Manual keyboard permissions
On a system using systemd-logind and udev, an administrator can install the same
rule manually. This also works with older SnipExpand releases that do not have
`--keyboard-access`:
```bash
sudo tee /etc/udev/rules.d/71-snipexpand-keyboard.rules >/dev/null <<'EOF'
SUBSYSTEM=="input", KERNEL=="event*", ENV{ID_INPUT_KEYBOARD}=="1", TAG+="uaccess"
EOF
sudo udevadm control --reload-rules
sudo udevadm trigger --action=change --subsystem-match=input --property-match=ID_INPUT_KEYBOARD=1 --settle
systemctl --user restart snipexpand
```
Package maintainers can ship `contrib/71-snipexpand-keyboard.rules` in their
distribution's udev rules directory. The CLI embeds the rule, so binaries and
crates.io installs can configure it through `--keyboard-access` too. The automatic
setup refuses to overwrite a different existing rule at its destination.
If your system does not support active-session udev access, follow its input
device permission policy. Membership in the `input` group is another option,
but grants access to additional input devices and requires a new login.
### Service runs, but typed triggers do not expand
Run `snipexpand doctor`. A running service or a successful paste from the Omarchy
panel only confirms that SnipExpand can send text. Automatic expansion also
requires keyboard read access. Restarting the service cannot repair a missing
permission. Current diagnostics test actual keyboard access; older releases
check only `input` group membership and may report failure even with a working
udev rule.
## AI agents
SnipExpand installs an AI skill at `~/.config/snipexpand/SKILL.md`. Point your
coding agent at it and describe what you want:
> Read `~/.config/snipexpand/SKILL.md`, then set up my SnipExpand snippets.
The skill teaches agents how to scaffold and edit match files, change settings,
use the CLI, validate changes, and check the running service. You can manage
your entire setup this way without learning the commands below.
## Add your first snippet
Add an expansion from the command line:
```bash
snipexpand add --label 'Email address' --search-term contact ';mail' 'user@example.com'
```
## Match files
For more control, create or edit a YAML file below
`~/.config/snipexpand/match/`. SnipExpand offers
[best-effort compatibility](docs/compatibility.md) with Espanso's YAML match
format:
```yaml
# Match files reload when saved.
global_vars:
- name: today
type: date
params:
format: "%Y-%m-%d"
matches:
# Multiple triggers, one replacement
- triggers: [";mail", ";email"]
label: "Email address"
search_terms: [email, contact]
replace: "user@example.com"
# Whole-word matching and multiline text
- trigger: ";sig"
label: "Email signature"
word: true
replace: |
Best regards,
Your Name
# $|$ marks the cursor position after expansion.
- trigger: ";function"
replace: |
fn example() {
$|$
}
# Insert a formatted date
- trigger: ";today"
replace: "{{today}}"
# Named regex captures become replacement variables.
- regex: "issue-(?P<number>\\d{3})"
replace: "Issue #{{number}}"
# Reuse another snippet without running commands.
- trigger: ";name"
replace: "Your Name"
- trigger: ";greeting"
replace: "Hello from {{name}}"
vars:
- name: name
type: match
params:
trigger: ";name"
```
## Settings
Edit `~/.config/snipexpand/config.yml`:
```yaml
# Choose when expansion happens.
trigger_mode: space # immediate | space
terminators: [space] # any of: space, enter, tab
# Optional. Override which characters delimit word-boundary matches.
# word_separators: [" ", ".", ",", "!", "?"]
# Maximum characters retained while evaluating regex triggers.
regex_max_buffer: 256
# Prefer native Wayland injection and fall back to uinput.
injection_backend: auto # auto | wayland | uinput
# Tune these only if an application drops or reorders characters.
injection_delay_ms: 1
wayland_injection_delay_ms: 0
uinput_injection_delay_ms: 1
injection_settle_ms: 10
# Backspace immediately after a simple expansion to restore its trigger.
undo_enabled: true # true | false
# Optional. Disable expansion in matching applications. Default: []
app_exclusions:
- class: "^1Password$"
- class: "^org\\.keepassxc\\.KeePassXC$"
# Optional. The first matching profile overrides behavior for that application.
app_profiles:
- name: Browser
filter:
class: "^(firefox|chromium)$"
include_match_files: [browser.yml]
trigger_mode: space
injection_delay_ms: 1
```
Run `snipexpand detect` while an application is focused to find the title,
class, and executable values needed for an exclusion or profile.
## Snippet packs
Install a native SnipExpand pack or a compatible Espanso pack directly from a
Git repository:
```bash
snipexpand pack inspect espanso:arrows
snipexpand pack install espanso:arrows
snipexpand pack inspect https://github.com/example/useful-symbols
snipexpand pack install https://github.com/example/useful-symbols
```
Use `--path DIR` when a repository contains several packs and `--ref REF` to
select a tag, branch, or commit. SnipExpand records the exact resolved commit.
Installed packs are read-only, independently enableable collections. Updates
are always explicit:
```bash
snipexpand pack list
snipexpand pack disable useful-symbols
snipexpand pack enable useful-symbols
snipexpand pack update useful-symbols
snipexpand pack remove useful-symbols
```
SnipExpand validates every pack before enabling it. Espanso packs using
unsupported fields, scripts, forms, or variables are rejected rather than
partially installed.
The `espanso:NAME` shorthand selects the latest stable version of a compatible
package from the official Espanso Hub.
## Commands
```text
snipexpand [COMMAND]
sxp [COMMAND] Short form
(no command) Run the daemon in the foreground
init Explicitly create starter configuration
add TRIGGER TEXT Add or replace a generated expansion
remove TRIGGER Remove a generated expansion
list List triggers and source files
check Validate configuration
detect Inspect the focused application
reload Reload the running daemon
enable Enable automatic expansion
disable Pause automatic expansion
toggle Toggle automatic expansion
paste [--source PATH] [--delay-ms N] TRIGGER
Insert a configured expansion
status [--json] Show daemon and configuration status
doctor Diagnose setup and runtime requirements
install Install and start the user service
uninstall Remove the service; preserve configuration
pack inspect SOURCE Validate a Git-published pack
pack install SOURCE Install and enable a pack
pack list List installed packs
pack update NAME Update a pack explicitly
pack enable NAME Enable an installed pack
pack disable NAME Disable an installed pack
pack remove NAME Remove an installed pack
```
Duplicate triggers may coexist for picker use. Automatic typing expands only
when the active app profile leaves one matching snippet. A picker can select an
exact duplicate with `paste --source PATH TRIGGER`, using the `source` returned
by `list --json`.
## Limitations
- Hyprland is the only supported and tested compositor. Other Wayland
compositors may work but are not yet part of the test matrix.
- Undo works only immediately after a plain, single-line expansion. Multiline
and cursor-positioned expansions cannot be undone back to their trigger.
- SnipExpand does not run scripts or shell commands, display forms, insert rich
text or images, or provide a package registry.
- Regex triggers use a bounded rolling buffer and support named captures, but
do not implement Espanso's full regex behavior.
- Variables are limited to formatted dates, regex captures, and safe nested
snippet references. Shell, script, and form variables are intentionally
unsupported.
- Application exclusions operate at the application level. Wayland does not
expose a browser's focused field type, so SnipExpand cannot automatically
identify password fields inside an allowed browser.
- SnipExpand reads global keyboard events, including sensitive input.
Application exclusions stop expansion but do not stop the daemon from
receiving those events. Install only binaries you trust.
SnipExpand does not execute snippets, access the clipboard, or contact online
services.
See the [compatibility matrix](docs/compatibility.md) for the complete supported
configuration surface.
## Documentation
- [Prioritized tasks](TASKS.md)
- [Espanso compatibility](docs/compatibility.md)
- [Espanso-informed design notes](docs/espanso-roadmap.md)
- [Publishing and managing snippet packs](docs/packs.md)
## Development
```bash
cargo fmt --check
cargo clippy --all-targets -- -D warnings
cargo test
```
## License
[GNU General Public License v3.0 or later](LICENSE).
Other platforms offer polished text expansion built in or through expensive
software. Linux users should not have to settle for less or pay a costly
subscription for basic infrastructure. SnipExpand is free and open source so
anyone can use it, study it, improve it, and share it.
Anyone who distributes a modified version must make its source available under
compatible terms. The project cannot be repackaged and distributed as
closed-source software. Private use and private modifications remain private.