Skip to content

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.

Text snippets list with a selected snippet and expanded preview

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.

KeyAction
EnterPrimary action (copy, or hypr_paste on Hyprland)
Ctrl+EnterSecondary action (insert)
↑ / ↓Move through the snippets
Ctrl+RReload a file or directory source
EscClose Raffi

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.

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.

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), hyprctl and wl-clipboard
  • Raffi started inside the Hyprland session, so it inherits HYPRLAND_INSTANCE_SIGNATURE and XDG_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.

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".

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"

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:

Terminal window
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" || break
done

Keep 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: true

Use 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.

SearchCopy before opening RaffiResult
sn !bugError output or a failing test logMarkdown bug report with the error and report date
sn !reviewA pull request URLReview request with testing prompts, a deadline, and your sign-off
sn !meetingA meeting topic on one lineNotes with decisions, action owners, and next week’s date
sn !handoverThe latest incident summaryHandover with impact, next actions, and an update time in 30 minutes
sn !followupA ticket, link, or earlier messageFollow-up email with a reply date and your sign-off
sn !signoffNothingThe shared sign-off used by the email templates
bug-report.snippet
Bug report from clipboard
keyword: !bug
icon: 🐛
---
## Summary
[What failed, and where?]
## Steps to reproduce
1. [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}

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.

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__":
pass
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}"

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: true

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.

Snippets listed under snippets in raffi.yaml, as in the Quick start.

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 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 Signature
keyword: !sig
icon: ✍️
---
Best regards,
John Doe
john@example.com

Files without the --- separator, with an unknown header line, or with no content are skipped. Snippets are listed by name until usage reorders them.

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.

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}() {}"
PlaceholderResult
{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
Snippet preview showing an expanded dynamic value and its raw template

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 +1M is 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 with insert. 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: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.

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.