Text Snippets
Text snippets are pieces of text you search by name and then copy, type or
paste into the window you came from. A snippet source groups them under a
keyword. The snippets can be written in your config, read from a YAML file or
a directory of .snippet files, or produced by a command, and their values
can fill in placeholders such as today’s date or the
clipboard.
Quick start
Section titled “Quick start”Add a source to raffi.yaml:
addons: text_snippets: - name: "Emails" keyword: "em" icon: "mail" snippets: - name: "Personal Email" icon: "📨" value: "user@example.com" - name: "Work Email" value: "user@company.com"Open Raffi and type em pers. “Personal Email” is selected; press Enter to
copy user@example.com, or Ctrl+Enter to type it into the focused window.
While you are still typing the keyword, Raffi shows a row describing the source. Press Enter or Tab on that row to jump straight into it.
| Key | Action |
|---|---|
| Enter | Primary action (copy, or hypr_paste on Hyprland) |
| Ctrl+Enter | Secondary action (insert) |
| ↑ / ↓ | Move through the snippets |
| Ctrl+R | Reload a file or directory source |
| Esc | Close Raffi |
Snippet Keywords and Search
Section titled “Snippet Keywords and Search”Each snippet can have its own keyword, a short abbreviation that finds it
straight away, like Alfred’s snippet keywords:
snippets: - name: "Email signature" keyword: "!sig" value: "Best regards, John"Typing sn !sig puts that snippet first. Results are ranked as exact keyword,
then keyword prefix, then fuzzy match on the name, then fuzzy match on the raw
snippet value (before placeholders are filled in). The keyword is shown next
to the snippet name.
Raffi remembers which snippets you pick and scores them with the same
sort_mode as your apps
and entries (hybrid by default). With nothing typed after the source
keyword, the snippets you use most come first. While typing, usage only
decides between equally good matches. A query starting with ! is a keyword
lookup, so usage is ignored there and ties keep the source order. Usage is
stored in ~/.cache/raffi/snippets.cache, per source keyword and snippet
name.
To search a source without its keyword, add it to the
main list as
text_snippets."<name>". Its best matches then show in the main list. A
command source runs once when Raffi opens on the main list, rather than for
each search.
Actions
Section titled “Actions”Default Actions
Section titled “Default Actions”action sets what Enter does and secondary_action what Ctrl+Enter does.
Both accept copy, insert, hypr_paste or a shell command.
action: "copy"Copies the snippet to the clipboard with wl-copy, xclip or xsel.
This is the default for Enter outside Hyprland.
action: "insert"Types the snippet into the focused window with wtype or ydotool. This
is the default for Ctrl+Enter.
action: "hypr_paste"secondary_action: "copy"restore_clipboard: trueCopies the snippet with wl-copy, closes Raffi, waits for the window you
came from to get focus back, and sends it the paste shortcut through
Hyprland: Ctrl+V, or Ctrl+Shift+V in terminals. Long snippets arrive at
once instead of being typed character by character.
When Raffi runs inside Hyprland and a source has no action, Enter uses
hypr_paste. If Raffi finds no window to paste into when it opens, or
Hyprland is older than 0.55, Enter keeps the copy default. Set
action: "copy" to keep copying on Hyprland.
hypr_paste works as either action or secondary_action and needs:
- Hyprland 0.55 or later (for its Lua API),
hyprctlandwl-clipboard - Raffi started inside the Hyprland session, so it inherits
HYPRLAND_INSTANCE_SIGNATUREandXDG_RUNTIME_DIR - a receiving app that accepts Ctrl+V, or Ctrl+Shift+V for terminals
Raffi recognises common terminals by their window class. Add others with
general.terminal_classes.
Snippets copied for pasting are marked sensitive, so clipboard managers that
honour this hint, including CopyQ with its default settings, keep them out of
history. The hint needs wl-clipboard 2.3 or later; older versions paste
without it. Ordinary copy actions and restored clipboard text are not
marked, and existing history entries are not removed.
By default the snippet stays on the clipboard. With restore_clipboard: true,
Raffi puts the previous plain text back one second after sending the paste
shortcut, as long as the clipboard still holds the snippet Raffi copied. A
newer copy skips restoring, even one with identical text. If the previous
clipboard is empty, unavailable, not text, invalid UTF-8 or larger than
100 KiB, Raffi skips restoring and still pastes. Rich text comes back as plain
text; images and other formats are lost. A slow application may not have
read the snippet before the second is up.
{cursor} is removed from the value but does not move the caret with
hypr_paste. If the clipboard cannot be prepared, Raffi shows the error and
stays open. If the original window does not get focus back within two
seconds, or Hyprland rejects the shortcut, Raffi reports an error and does
not paste anywhere else. A shortcut that was sent does not guarantee the app
inserted the text.
Custom actions
Section titled “Custom actions”Any other value is run with sh -c. The snippet value is exported as
RAFFI_VALUE:
text_snippets: - name: "Commands" keyword: "cmd" action: 'kitty sh -c "$RAFFI_VALUE"' snippets: - name: "System Update" value: "sudo pacman -Syu"The command can also use {value}, which is pasted in as raw text, so quotes
or $(...) in the snippet are interpreted by the shell. For snippets that
pull in the clipboard, shell output or anything else you do not control, quote
the variable instead: notify-send "Snippet" "$RAFFI_VALUE".
Enter and Ctrl+Enter
Section titled “Enter and Ctrl+Enter”text_snippets: - name: "Emails" keyword: "em" icon: "mail" action: "insert" # Enter: type into the app secondary_action: "copy" # Ctrl+Enter: copy to the clipboard snippets: - name: "Personal Email" value: "user@example.com"Examples
Section titled “Examples”Work templates
Section titled “Work templates”These templates turn copied context into bug reports, review requests, notes
and emails. Browse the snippet files on GitHub,
or download all six with curl. This command skips files you already have:
snippet_dir="$HOME/.local/share/raffi/snippets"base_url="https://raw.githubusercontent.com/chmouel/raffi/main/examples/snippets"mkdir -p "$snippet_dir"
for name in bug-report review-request meeting-notes incident-handover follow-up sign-off; do [ -e "$snippet_dir/$name.snippet" ] && continue curl --fail --location --remove-on-error \ "$base_url/$name.snippet" \ --output "$snippet_dir/$name.snippet" || breakdoneKeep the six files together: the review request and follow-up email reuse the
sign-off by its !signoff keyword. Edit that file to use your preferred name;
by default it uses your login name with an initial capital.
Add a directory source to your addons.text_snippets list:
addons: text_snippets: - name: "Work templates" keyword: "sn" directory: "~/.local/share/raffi/snippets" action: "hypr_paste" secondary_action: "copy" restore_clipboard: trueUse action: "copy" on other compositors. Copy the context described below,
focus a browser text field, open Raffi, and search for the template’s
keyword. Dates use local time; +2d means two calendar days. Replace the
bracketed prompts after pasting.
| Search | Copy before opening Raffi | Result |
|---|---|---|
sn !bug | Error output or a failing test log | Markdown bug report with the error and report date |
sn !review | A pull request URL | Review request with testing prompts, a deadline, and your sign-off |
sn !meeting | A meeting topic on one line | Notes with decisions, action owners, and next week’s date |
sn !handover | The latest incident summary | Handover with impact, next actions, and an update time in 30 minutes |
sn !followup | A ticket, link, or earlier message | Follow-up email with a reply date and your sign-off |
sn !signoff | Nothing | The shared sign-off used by the email templates |
Bug report from clipboardkeyword: !bugicon: 🐛---## Summary[What failed, and where?]
## Steps to reproduce1. [First step]2. [Action that triggers the failure]
## Expected result[What should have happened?]
## Actual result```text{clipboard|trim}```
## Environment- Version: [version or commit]- Reported: {date} at {time}- Reporter: {env:USER}PR review requestkeyword: !reviewicon: 🔍---Ready for review: {clipboard|trim}
What changed:- [Main change and why it was needed]
How I tested it:- [Command and result]
Please focus on:- [A risk, edge case, or design question]
Could you take a look by {date +2d:%A %d %B}?
{snippet:!signoff}Meeting noteskeyword: !meetingicon: 🗓️---# {clipboard|trim}{date:%A %d %B %Y} at {time}
## Decisions- [Decision and reason]
## Actions- [ ] [Task] | Owner: [name] | Due: {date +1w}
## Open questions- [Question to resolve before the next meeting]
## Next meeting{date +1w:%A %d %B} at [time]Incident handoverkeyword: !handovericon: 🚨---## Handover | {datetime}Next update: {datetime +30m:%Y-%m-%d %H:%M}
## Current context{clipboard|trim}
## Impact- [Affected users or services]- Started: [time and timezone]
## Checked so far- [Evidence, logs, or dashboards]
## Next action- [Action] | Owner: [name]
## Rollback[Runbook or rollback instructions]
Handing over from: {env:USER}Taking over: [name]Follow-up emailkeyword: !followupicon: 📬---Hi,
Following up on:
{clipboard|trim}
Could you send an update by {date +2d:%A %d %B}?If that timing doesn't work, please suggest another date.
{snippet:!signoff}Short sign-offkeyword: !signofficon: ✍️---Thanks,{env:USER|capitals}To check clipboard restoring, copy a recognisable piece of text, paste a template with Enter, wait a second, then paste manually again. The second paste should contain the original text. With CopyQ’s default settings, the generated template stays out of history. Ctrl+Enter uses the ordinary copy action, so you can keep a generated template on the clipboard and in history when you want to reuse it.
Code that you type in
Section titled “Code that you type in”text_snippets: - name: "Code Templates" keyword: "code" icon: "code" action: "insert" snippets: - name: "Rust Main" value: | fn main() { println!("Hello, world!"); } - name: "Python Main" value: | if __name__ == "__main__": passGit commands from a file
Section titled “Git commands from a file”text_snippets: - name: "Git Commands" keyword: "git" file: "~/.config/raffi/git-snippets.yaml"- name: "Amend Commit" value: "git commit --amend --no-edit"- name: "Push Force" value: "git push --force-with-lease"- name: "Interactive Rebase" value: "git rebase -i HEAD~"text_snippets: - name: "Dates" keyword: "date" icon: "calendar" snippets: - name: "Today" keyword: "td" value: "{date}" - name: "Yesterday" value: "{date -1d}" - name: "Tomorrow" value: "{date +1d}" - name: "ISO Timestamp" value: "{datetime:%Y-%m-%dT%H:%M:%S%:z}"Configuration
Section titled “Configuration”Snippet sources are a list under addons.text_snippets (default: []).
Each source needs a name, a keyword and one of snippets, file,
directory or command. If you set more than one, Raffi uses the first in
that order and ignores the rest.
name string (required)
Name of the source, shown on the keyword row (“Browse Emails”) and while a command source is loading.
keyword string (required)
Text that opens the source. Type it followed by a space: em enters the
source, while em alone keeps searching the main list. A
script filter with the same keyword takes precedence.
icon string
Default icon for every snippet in the source: an emoji, an icon name or an
image path. A snippet’s own icon overrides it, and a snippet icon that
cannot be found falls back to it. general.no_icons hides both. A leading
~/ and ${VAR} references are expanded.
snippets list
Snippets written in the config. Each has a name and a value, and
optionally a keyword and an icon (emoji, icon name or image path). See
Inline snippets.
file string
Path to a YAML file of snippets, in the same format as snippets. See
File source.
directory string
Path to a directory of .snippet files. See
Directory source.
command string
Program that prints its snippets as Alfred Script Filter JSON. See Command source.
args list of strings (default: [])
Arguments passed to command.
action string (default: copy, or hypr_paste on Hyprland)
What Enter does: copy, insert, hypr_paste or a shell command. See
Actions. Inside Hyprland, an unset action becomes hypr_paste
when Raffi finds a window to paste into, and copy otherwise.
secondary_action string (default: insert)
What Ctrl+Enter does. It accepts the same values as action.
restore_clipboard boolean (default: false)
With hypr_paste, put the previous plain-text clipboard back one second
after pasting. Needs wl-paste. It has no effect on other actions. See
Default actions for the limits.
placeholders boolean (default: true)
Fill in dynamic placeholders in snippet values. Set
it to false to use values exactly as written.
close_on_action boolean (default: true)
Close Raffi after copying a snippet or running a custom action. When unset,
general.close_on_action applies. insert and hypr_paste always close
Raffi, since the text has to go to another window.
addons: text_snippets: - name: "Emails" keyword: "em" icon: "mail" action: "insert" secondary_action: "copy" close_on_action: false snippets: - name: "Personal" value: "user@example.com" - name: "Templates" keyword: "tpl" icon: "document" file: "~/.config/raffi/templates.yaml" placeholders: false - name: "Projects" keyword: "proj" icon: "folder" command: "project-snippets" args: ["--json"] - name: "Work templates" keyword: "sn" directory: "~/.local/share/raffi/snippets" action: "hypr_paste" secondary_action: "copy" restore_clipboard: trueSnippet sources
Section titled “Snippet sources”Paths in file, directory and command expand a leading ~/ and
${VAR} references. File and directory sources are read once per Raffi
session; press Ctrl+R to read them again after editing.
Inline snippets
Section titled “Inline snippets”Snippets listed under snippets in raffi.yaml, as in the
Quick start.
File source
Section titled “File source”file points to a YAML list with the same fields as inline snippets:
addons: text_snippets: - name: "Templates" keyword: "tpl" icon: "document" file: "~/.config/raffi/snippets.yaml"- name: "Greeting" value: "Hello, world!"
- name: "Signature" keyword: "!sig" icon: "✍️" value: "Best regards, User"
- name: "Meeting Template" value: | Meeting Notes Date: {date} Attendees: [NAMES]Directory source
Section titled “Directory source”directory points to a folder of .snippet files, one snippet per file:
addons: text_snippets: - name: "Snippets" keyword: "sn" icon: "snippets" directory: "~/.local/share/raffi/snippets"The first line of a file is the snippet name. Optional keyword: and icon:
lines follow, in either order. A --- line separates them from the content,
which is everything after it:
Professional Signaturekeyword: !sigicon: ✍️---Best regards,John Doejohn@example.comFiles without the --- separator, with an unknown header line, or with no
content are skipped. Snippets are listed by name until usage reorders them.
Command source
Section titled “Command source”command runs a program and lists what it prints. Raffi passes no query to
it and filters the results itself, running it again each time the search
changes, so keep it quick. The program prints JSON in the
Alfred Script Filter format:
addons: text_snippets: - name: "Dynamic" keyword: "dyn" icon: "terminal" command: "my-snippet-gen" args: ["-j"]{ "items": [ { "title": "Current Date", "arg": "2024-03-15", "keyword": "!d" }, { "title": "Mail client", "arg": "thunderbird", "icon": { "path": "mail-client" } } ]}title becomes the snippet name and arg its value. The optional keyword
sets the snippet keyword, and icon.path the
icon. {shell:...} placeholders in command output are not run.
Dynamic Placeholders
Section titled “Dynamic Placeholders”Snippet values can contain placeholders that are filled in when you pick the snippet. The subtitle under each snippet previews the result.
snippets: - name: "Quote clipboard" value: "> {clipboard}" - name: "Meeting header" value: "## Meeting {date:%A %d %B}, follow-up on {date +1w}" - name: "Branch" value: "{shell:git -C ~/src/project branch --show-current}" - name: "Function" value: "fn {cursor}() {}"| Placeholder | Result |
|---|---|
{clipboard} | Current clipboard text |
{clipboard:1} | An earlier clipboard entry, up to {clipboard:100}. See Clipboard history |
{date} {time} {datetime} | Now, formatted as %Y-%m-%d, %H:%M and %Y-%m-%d %H:%M |
{date:%A %d %B} | Custom strftime format, also for time and datetime |
{date +1d} {datetime -2h +30m:%H:%M} | Date arithmetic. Units: s m h d w M (months) y |
{random:uuid} | A random UUID (v4) |
{random:1-100} | A random whole number between both values, inclusive |
{env:USER} | An environment variable, empty when unset |
{snippet:NAME} | Another snippet from the same source, by name or keyword |
{shell:COMMAND} | Output of sh -c COMMAND |
{cursor} | Where the caret ends up after insert |
Add transforms after a |, chained left to right: uppercase, lowercase,
capitals (first letter of each word) and trim. For example
{clipboard|trim|lowercase} or {env:USER|capitals}. Transforms work on
every placeholder except shell and cursor.
Only the names above are placeholders. Other braces, such as code or a
{value} in an action, are left alone. Write \{date} to get a literal
{date}. YAML double-quoted strings reject \{ and \} as invalid escapes,
so put such values in single quotes or a block scalar, for example
value: '\{date} is {date}'.
More rules:
- A placeholder with a mistake in it (unknown transform, bad date format, reversed range, missing snippet) shows an error row and leaves Raffi open instead of copying the wrong text.
- Month and year offsets stop at the end of the month: January 31
+1Mis the last day of February. - Text that comes from the clipboard, the environment, a shell command or
another snippet’s placeholders is inserted as is and not scanned for more
placeholders.
{snippet:...}references can nest up to 5 levels and expand at most 1000 times in total. {shell:...}ends at the first}. Write\}for a literal}inside the command. It times out after 5 seconds, which stops the command and its child processes. It only runs when you select the snippet, never for the preview.- Previews leave
{shell:...}and{random:...}untouched. The preview reads the clipboard once when the source opens; the final value reads it again. The preview pane shows at most the first 8 KiB or 100 lines of a snippet and says when it cut the rest. - Only the first
{cursor}counts, and it only moves the caret withinsert. The caret moves one Left key per character, which can be off by a few for emoji sequences or combining accents. - Expanded values are limited to 100 KiB. With
insert,{cursor}can be at most 1000 characters from the end of the value.
Clipboard History
Section titled “Clipboard History”{clipboard:1} is the entry copied before the current one, {clipboard:2}
the one before that, and so on. {clipboard:0} is the same as {clipboard}.
Raffi keeps no history of its own, so these read from the clipboard manager
you already run: cliphist,
CopyQ or
clipman.
snippets: - name: "Previous copy" value: "{clipboard:1|trim}" - name: "Name and email" value: "{clipboard:1} <{clipboard}>"Raffi picks the manager running under your user. It looks for
cliphist store or clipman store (usually run from wl-paste --watch) and
for the CopyQ server, and prefers them in that order when several run. A
custom -db-path for cliphist or --histpath for clipman on that command
line is used too, including paths containing spaces; quote those paths when
you start the watcher through a shell. To pick one yourself or turn history
off, set clipboard_history in
general:.
With CopyQ, Raffi reads the tab CopyQ saves the clipboard to (its
clipboard_tab setting), even when that is not the first tab.
Numbering assumes the newest entry in the manager is the current clipboard, as Alfred does. If the manager skipped what you just copied (an image, a password it ignores, text below its minimum length), the numbers are off by one. Entries that are not text, such as images, show an error instead of being inserted.
Requirements
Section titled “Requirements”Copying needs wl-copy (wl-clipboard), xclip or xsel. Typing with
insert needs wtype or ydotool. {clipboard:N} needs cliphist, CopyQ or
clipman running. hypr_paste needs Hyprland 0.55 or later, hyprctl and
wl-clipboard.