This document describes the current design of Raffi as implemented in the repository today. It is intended as an engineering map for contributors, not a product roadmap.
Raffi is a YAML-driven launcher with two runtime frontends:
fuzzelmode: a lightweight external launcher integration- native mode: an iced-based Wayland UI with built-in addons
At a high level, Raffi:
- parses CLI arguments
- loads and normalizes launcher configuration from YAML
- selects a UI backend
- returns the chosen item description/binary name from the UI
- executes the matching command or inline script
src/main.rs
-> raffi::run(args)
-> read_config(...)
-> migrate v0 config if needed
-> process_config(...)
-> choose UI backend
-> ui.show(...)
-> execute_chosen_command(...)
Primary modules:
src/lib.rs: CLI args, config schema/types, config loading, migration, validation, command execution, icon cachesrc/ui/mod.rs: backend selection and shared UI settingssrc/ui/fuzzel.rs: builds fuzzel input and reads the selected itemsrc/ui/wayland.rs: launches the native iced applicationsrc/ui/wayland/*.rs: native UI state, routing, rendering, addon logic, tests
The current on-disk format is versioned YAML:
version: 1
general: ...
addons: ...
launchers:
name:
binary: ...Key properties of config processing:
- Old flat configs are migrated in place to v1, with a
.bakbackup. - Launcher values are deserialized into
RaffiConfig. ~/...and${VAR}references are expanded in launcher fields and addon config.- Invalid entries are dropped during load rather than shown in the UI.
Launcher validation currently filters entries by:
- binary existence
- script interpreter existence
ifenveqifenvsetifenvnotsetifexistdisabled: true
The resulting ParsedConfig contains:
general: UI/runtime defaultsaddons: native UI addon configurationentries: executable launcher entries
Backend selection happens in run():
- explicit CLI
-u/--ui-typewins - otherwise
general.ui_typewins - otherwise Raffi prefers
fuzzelif the binary exists inPATH - otherwise, when built with the
waylandfeature, it falls back to the native UI
This split is intentional:
fuzzelmode keeps the binary and runtime surface small- native mode enables richer interactions that cannot fit the external dmenu-style protocol
src/ui/fuzzel.rs is intentionally simple.
- It renders each launcher as a single line.
- When icons are enabled, it uses the cached icon map and the
description\0icon\x1fpathformat expected byfuzzel -d. - It passes Raffi's MRU cache path to fuzzel so repeated launches benefit from backend-level ordering.
This backend only returns the chosen launcher label. All execution still happens centrally in src/lib.rs.
The native backend is an iced application rooted in LauncherApp.
Important state buckets in src/ui/wayland/state.rs:
- launcher list and filtered indices
- current search query and selected row
- icon map and MRU data
- history state
- per-addon transient state:
- calculator
- currency
- script filters
- text snippets
- web searches
- file browser
- emoji
- view state such as widget ids, theme, font sizes, and keyboard modifiers
The app initializes by:
- loading icons unless
no_iconsis active - loading MRU and history caches
- sorting launchers using the configured
SortMode - focusing the search input
- replaying an initial query if one was provided
Native mode does not treat every search as a plain fuzzy match. route_query() applies a precedence order:
- file browser if the query looks like
/...,~, or~/... - script filters by configured keyword
- text snippets by configured keyword
- emoji picker by trigger keyword
- web search by configured keyword
- standard launcher search
Currency handling is layered on top of that routing and is evaluated separately from the main QueryMode.
This structure matters because several addons can use overlapping prefixes. The tests in src/ui/wayland/tests.rs lock down the intended precedence.
Native-only addons are configured under addons and activated from the search box.
- Calculator:
- inline evaluation using
meval - only activates for expression-like input
- inline evaluation using
- Currency converter:
- parses trigger-based queries such as
$10 to eur - fetches rates over HTTP
- caches rates in memory with validity checks
- parses trigger-based queries such as
- File browser:
- activates for filesystem-like queries
- opens files with
xdg-open - supports hidden-file toggling
- Script filters:
- run external commands and expect Alfred Script Filter style JSON
- Text snippets:
- source snippets from inline config, files, directories, or command output
- Web searches:
- fill
{query}into configured URL templates and open them withxdg-open
- fill
- Emoji picker:
- loads cached emoji data or downloads it into the cache directory
- supports primary and secondary actions such as copy or insert
The design pattern is consistent across addons:
- parse a query into addon-specific state
- optionally spawn async work
- render temporary results inside the single launcher window
- perform the action on submit
- persist search history before exit when appropriate
Raffi stores lightweight state under the XDG cache directory, defaulting to ~/.cache/raffi/.
Current cache artifacts:
icon.cache: serialized icon-name to icon-path mapmru.cache: launch frequency and recency datahistory.cache: prior search queries for native history navigationemoji/: downloaded emoji datasets
On startup, run() also writes raffi-schema.json next to the user config if it does not already exist. That improves editor support for YAML authoring.
Launcher ordering in native mode is configurable:
frequencyrecencyhybrid
MRU data is loaded before UI startup. For hybrid mode, Raffi computes a weighted score from normalized frequency and recency, then sorts launchers before any search query is applied. Fuzzy search is then applied on top of that base set.
After a selection is returned from either backend, execution is centralized in execute_chosen_command().
- Binary launchers use
std::process::Command. - Script launchers run through the configured interpreter or the default script shell.
--print-onlyprints the resolved command or script wrapper instead of spawning it.
This separation keeps UI backends focused on selection rather than process management.
Cargo features deliberately shape the product:
- default build enables
wayland, which pulls iniced,ureq,regex, andmeval --no-default-featuresremoves native UI support and the native-only addons
That smaller build path is a first-class design choice, not a degraded afterthought.
Behavior changes usually need updates in more than one place:
README.mdfor high-level user-facing behaviorwebsite/src/content/docs/for detailed docsexamples/raffi.yamlfor canonical config examplesexamples/raffi-schema.jsonwhen config structs changesrc/ui/wayland/tests.rsor nearby unit tests when routing or ranking behavior changes
Current design constraints visible in the code:
- native mode is centered on Wayland/iced, while
fuzzelmode provides the minimal external integration path - launcher identity is largely derived from
descriptionorbinary, which keeps the system simple but couples selection matching to user-visible labels - some addon behavior is native-only by design and is not mirrored in
fuzzelmode - cache files are simple local artifacts rather than a formal database, which keeps the project lightweight