Simplicity is the ultimate sophistication, Anonymous

This is the next entry in my series of Rofi modules written in Python to extend its functionality. While the previous module focused on searching the web and the terminal, this script, myrofi_snippets.py, transforms Rofi into a powerful text expander.
It solves the repetitive typing problem by allowing you to define short “triggers” for long pieces of text (emails, URLs, code boilerplate). When invoked, it presents a searchable menu of these triggers. Selecting one instantly copies the full text to your Wayland clipboard using wl-copy. It also supports dynamic placeholders, allowing you to insert the current date or time automatically.
#!/usr/bin/env python3
# Shebang: tells the kernel to execute this file with the `python3` interpreter
# found in the user's PATH. This allows the script to be run directly as an
# executable (e.g., `./myrofi_snippets.py`) without explicitly typing `python3`.
"""
myrofi_snippets.py — Text snippet expander for Niri / Arch / Wayland
─────────────────────────────────────────────────────────────────────
Reads ~/dotfiles/myArch/config/snippets/mysnippets.json, shows triggers
in rofi, copies the selected expansion to the Wayland clipboard with
wl-copy, and sends a desktop notification.
Snippet file format:
{
"snippets": [
{ "trigger": "mywebsite", "text": "https://justtothepoint.com" },
{ "trigger": "myemail", "text": "nmaximo7@gmail.com" }
]
}
Required packages:
sudo pacman -S wl-clipboard (provides wl-copy)
Usage from rofi-launcher.sh:
N) python3 ~/dotfiles/scripts/myrofi_snippets.py ;;
"""
# The module docstring serves as both a user guide and a developer reference.
# It explains the script's purpose, the expected JSON format, dependencies,
# and how to integrate it into a rofi‑based launcher.
# ── Imports ───────────────────────────────────────────────────────────────────
# Standard library imports are grouped together for clarity.
import json # Parses the snippet JSON file.
import os # Expands ~ to the user's home directory.
import subprocess # Launches external commands: rofi, wl‑copy, notify‑send.
import sys # Provides exit() and stderr for error reporting.
import shutil # Contains shutil.which() to check for installed commands.
import datetime # Used for dynamic date/time snippet expansion.
# Note: All imports are from Python's standard library. The script has no
# external Python dependencies beyond what is packaged with Python itself.
# External tools (rofi, wl‑copy, notify‑send) are checked at runtime.
# ── Configuration ─────────────────────────────────────────────────────────────
# All configurable paths and constants are defined here, using os.path.expanduser
# to resolve the tilde (~) to the actual home directory. This ensures the script
# works correctly regardless of the current working directory.
SNIPPET_FILE = os.path.expanduser(
"~/dotfiles/myArch/config/snippets/mysnippets.json"
)
# Path to a custom rofi theme file. Rofi will use this theme to style the menu.
# If the file is missing, rofi falls back to its default theme silently.
ROFI_THEME = os.path.expanduser("~/dotfiles/myArch/config/rofi/rounded-dark")
# Font specification passed to rofi's `-font` flag.
# The format is "font family size", e.g., "hack 20" for Hack at 20pt.
ROFI_FONT = "hack 20"
# In the rofi menu, each snippet shows a preview of its expanded text.
# To prevent the menu from becoming unwieldy, previews longer than this
# number of characters are truncated with an ellipsis (…).
# The full text is still copied to the clipboard when selected.
PREVIEW_MAX = 55
# Special values that expand dynamically at selection time
DYNAMIC_DATE = "__DATE__" # → YYYY-MM-DD
DYNAMIC_DATETIME = "__DATETIME__" # → YYYY-MM-DD HH:MM:SS
# These magic strings, when placed in a snippet's "text" field, are replaced
# at runtime with the current date or date+time. This allows snippets like
# "Today's date is __DATE__" to produce dynamic output without modifying the
# JSON file. The strings are chosen to be unlikely to clash with normal text.
# ── Snippet loading ───────────────────────────────────────────────────────────
def load_snippets(path: str) -> list[dict]:
"""
Load the JSON file and return a list of snippet dicts.
Each dict has at minimum: trigger (str) and text (str).
Entries missing either key are skipped with a warning.
The JSON file is expected to have a top‑level object with a "snippets"
key containing an array of snippet objects. Example:
{
"snippets": [
{"trigger": "hello", "text": "Hello, world!"}
]
}
This function performs basic validation and silently ignores malformed
entries, printing a warning to stderr. This allows the rest of the
snippets to remain usable even if one entry is broken.
Parameters:
path : Absolute path to the JSON snippet file.
Returns:
A list of dictionaries, each guaranteed to contain non‑empty
'trigger' and 'text' keys. Returns an empty list if the file
contains no valid snippets.
"""
# Open the file with explicit UTF‑8 encoding to ensure cross‑platform
# compatibility (especially important for emojis or special characters).
with open(path, encoding="utf-8") as f:
data = json.load(f) # Parse the entire JSON file into a Python dict.
snippets = [] # Accumulator for valid snippet dicts.
# Iterate over the entries in the "snippets" list, using enumerate to
# capture the index for error reporting.
for i, entry in enumerate(data.get("snippets", [])):
# Use .get() with defaults to avoid KeyError if keys are missing.
trigger = entry.get("trigger", "").strip()
text = entry.get("text", "")
# Validation: trigger must be non‑empty after stripping, and text
# must be a string (even an empty string is acceptable, though it
# would copy nothing). If validation fails, print a warning to stderr
# use the entry number (i) as reference, and skip this entry.
if not trigger or not isinstance(text, str):
print(f" skipping entry #{i} — missing trigger or text", file=sys.stderr)
continue
# Append a clean dict containing only the valid keys.
snippets.append({"trigger": trigger, "text": text})
return snippets
# ── Dynamic text expansion ────────────────────────────────────────────────────
def expand(text: str) -> str:
"""
Resolve any dynamic placeholder in the text field.
Returns the plain text unchanged for normal snippets.
This function is called immediately before copying the snippet to the
clipboard. It checks if the text exactly matches one of the predefined
dynamic placeholders (__DATE__ or __DATETIME__) and, if so, returns the
current date/time formatted accordingly. All other text is returned as‑is.
Parameters:
text : The raw snippet text (may contain a placeholder).
Returns:
The expanded string. For static snippets, this is identical to the input.
"""
# Check for exact match with the dynamic date placeholder.
# Using exact match (rather than substring replacement) keeps the behaviour
# simple and predictable. Only whole‑field replacement is supported.
if text == DYNAMIC_DATE:
# datetime.date.today() returns a date object.
# .isoformat() produces a string like "2026-04-19".
return datetime.date.today().isoformat()
if text == DYNAMIC_DATETIME:
# datetime.datetime.now() returns the current local date and time.
# strftime("%Y-%m-%d %H:%M:%S") formats it as "2026-04-19 14:30:45".
return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# For all other text, return unchanged.
return text
# ── Rofi menu ─────────────────────────────────────────────────────────────────
def show_menu(snippets: list[dict]) -> dict | None:
"""
Display all snippets in rofi and return the chosen snippet dict.
Returns None if the user pressed Escape.
Each row shows: trigger preview of text
We use -format i (index) to identify the selection so the lookup
is always exact — no string matching needed, no whitespace issues.
"""
col = min(max(len(s["trigger"]) for s in snippets) + 2, 22)
lines = []
for s in snippets:
preview = expand(s["text"])
if len(preview) > PREVIEW_MAX:
preview = preview[:PREVIEW_MAX - 1] + "…"
lines.append(f"{s['trigger'].ljust(col)} {preview}")
# `subprocess.run` is used to invoke rofi synchronously.
# Arguments explained:
# -dmenu : Act as a dmenu replacement (reads stdin, outputs selected line).
# -i : Case‑insensitive filtering.
# -p " Snippets" : Prompt text displayed in the entry box.
# -no-custom : Prevent the user from typing arbitrary text; only existing
# entries can be selected. This ensures we always get a valid snippet.
# -format i : Output the zero‑based index of the selected line instead of the
# line text. This eliminates any ambiguity caused by special
# characters or duplicate display text.
# -lines N : Maximum number of visible rows (up to 20, or fewer if we have
# fewer snippets). Prevents rofi from taking the whole screen.
# -theme FILE : Path to a custom rofi theme file.
# -font FONT : Font specification (family and size).
result = subprocess.run(
[
"rofi", "-dmenu", "-i",
"-p", " Snippets",
"-no-custom",
"-format", "i", # return 0-based index — no string matching needed
"-lines", str(min(len(lines), 20)),
"-theme", ROFI_THEME,
"-font", ROFI_FONT,
],
input="\n".join(lines), # The menu entries, one per line.
capture_output=True, # Capture stdout and stderr.
text=True, # Work with strings, not bytes.
)
# A non‑zero return code usually means the user pressed Escape or closed
# the window. In that case, we return None to signal cancellation.
if result.returncode != 0:
return None
# The stdout should contain a single integer (the chosen index).
# We strip whitespace and verify it's composed only of digits.
idx_str = result.stdout.strip()
if not idx_str.isdigit():
return None
# Convert to int and perform a bounds check before accessing the list.
idx = int(idx_str)
return snippets[idx] if 0 <= idx < len(snippets) else None
# ── Clipboard ─────────────────────────────────────────────────────────────────
def copy_to_clipboard(text: str) -> None:
"""
Copy the given text to the Wayland clipboard using `wl-copy`.
`wl-copy` is part of `wl-clipboard`, the standard Wayland equivalent
of `xclip`. It writes the input to the clipboard selection, making it
available for pasting in other applications (e.g., with Ctrl+V).
WHY Popen and NOT subprocess.run:
wl-copy stays alive in the background to serve clipboard paste
requests from other applications. It never exits on its own.
subprocess.run() waits for the child to finish — it would hang
forever. Popen() starts wl-copy and returns immediately.
WHY start_new_session=True:
Detaches wl-copy from our process group so it keeps running
after this script exits. Without this, wl-copy receives SIGHUP
when the script's session ends and the clipboard is lost.
Returns True on success, False if the command fails or is not installed.
"""
# Check if wl-copy exists in PATH. If not, there's no point in trying
# to run it, and we can provide a clear error later.
if not shutil.which("wl-copy"):
return False
# Launch wl-copy as a detached background process.
# - stdin=subprocess.PIPE : We'll write the text to wl-copy's stdin.
# - stdout/stderr=DEVNULL : Discard any output (wl-copy is silent).
# - start_new_session=True : Put wl-copy in its own process session.
# This ensures it survives the termination of this Python script.
proc = subprocess.Popen(
["wl-copy"],
stdin=subprocess.PIPE,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True,
)
# Write the expanded text to wl-copy's stdin as UTF‑8 encoded bytes.
# We must encode because stdin expects bytes when using PIPE.
proc.stdin.write(text.encode("utf-8"))
# Close the stdin pipe. This signals to wl-copy that no more data is coming
# and it can take ownership of the clipboard content.
proc.stdin.close()
# At this point wl-copy is running in the background.
# We intentionally do NOT call proc.wait() — that would block forever.
# The function returns immediately, and wl-copy continues to serve
# clipboard requests until the user copies something else.
# ── Notification ──────────────────────────────────────────────────────────────
def notify(title: str, body: str = "", error: bool = False) -> None:
"""
Send a desktop notification. Silent if notify-send is absent.
This function is a simplified version of the one in myrofi_search.py.
It launches notify-send as a detached background process so the script
does not wait for the notification to disappear.
Parameters:
title : The notification summary (e.g., "Snippets").
body : Optional additional text (e.g., the trigger name).
error : If True, uses an error icon; otherwise a copy icon.
"""
# Bail out early if notify-send isn't installed.
if not shutil.which("notify-send"):
return
# Choose an appropriate icon from the freedesktop.org naming spec.
# - "dialog-error" for error messages.
# - "edit-copy" for successful clipboard operations.
icon = "dialog-error" if error else "edit-copy"
# Launch notify-send in the background. We don't wait for it to finish.
subprocess.Popen(
["notify-send", "-i", icon, "-t", "3000", title, body],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
# ══════════════════════════════════════════════════════════════════════════════
# ENTRY POINT
# ══════════════════════════════════════════════════════════════════════════════
def main() -> None:
"""
Main orchestration function: load snippets, show rofi menu, copy selection,
and notify the user.
"""
# ── Step 1: Check for wl-copy availability ──────────────────────────────
# The script cannot function without wl-copy. If it's missing, we notify
# the user with installation instructions and exit with an error code.
if not shutil.which("wl-copy"):
notify("Snippets", "wl-copy not found.\nsudo pacman -S wl-clipboard", error=True)
sys.exit(1)
# ── Step 2: Load snippets from the JSON file ─────────────────────────────
# First, verify that the snippet file actually exists. If not, notify and exit.
if not os.path.exists(SNIPPET_FILE):
notify("Snippets", f"File not found:\n{SNIPPET_FILE}", error=True)
sys.exit(1)
# Attempt to parse the JSON file. Any JSONDecodeError is caught and
# reported to the user.
try:
snippets = load_snippets(SNIPPET_FILE)
except json.JSONDecodeError as e:
notify("Snippets", f"JSON error:\n{e}", error=True)
sys.exit(1)
# If the file loaded successfully but contains zero valid snippets,
# we notify the user and exit gracefully. This is not considered an error.
if not snippets:
notify("Snippets", "No snippets found in file.", error=True)
sys.exit(0)
# ── Step 3: Present the rofi menu ────────────────────────────────────────
# show_menu returns the chosen snippet dict or None if the user pressed
# Escape or otherwise cancelled the selection.
chosen = show_menu(snippets)
if chosen is None:
sys.exit(0)
# ── Step 4: Expand dynamic placeholders and copy to clipboard ────────────
# Apply any dynamic expansions (__DATE__, __DATETIME__) to the snippet text.
final_text = expand(chosen["text"])
# Copy the expanded text to the Wayland clipboard.
# Note: copy_to_clipboard() returns immediately; wl-copy runs detached.
copy_to_clipboard(final_text)
# ── Step 5: Notify the user of success ───────────────────────────────────
# Build a preview of the copied text, truncated to PREVIEW_MAX characters
# with an ellipsis if it's too long.
preview = final_text if len(final_text) <= PREVIEW_MAX else final_text[:PREVIEW_MAX - 1] + "…"
# Send a desktop notification with the trigger name and the preview.
# The 📋 emoji adds a visual cue that something was copied.
notify(f"📋 {chosen['trigger']}", f"{preview}\n\nCopied to clipboard")
# ══════════════════════════════════════════════════════════════════════════════
# SCRIPT EXECUTION GUARD
# ══════════════════════════════════════════════════════════════════════════════
if __name__ == "__main__":
main()