# Omazel — How It Works and How to Use It
This is the full reference. For ready-to-paste rules, see
[EXAMPLES.md](EXAMPLES.md); for a quick overview, the [README](../README.md).
- [Getting started](#getting-started)
- [Two ways to edit rules](#two-ways-to-edit-rules)
- [Anatomy of a rule](#anatomy-of-a-rule)
- [Conditions reference](#conditions-reference)
- [Actions reference](#actions-reference)
- [Patterns and tokens](#patterns-and-tokens)
- [Rule order and `continue`](#rule-order-and-continue)
- [Testing safely with dry run](#testing-safely-with-dry-run)
- [How the engine works](#how-the-engine-works)
- [Scripting the engine](#scripting-the-engine)
- [Widget settings](#widget-settings)
- [Troubleshooting](#troubleshooting)
## Getting started
```bash
omarchy plugin add https://github.com/bscott/Omazel.git --enable
```
Place the **Omazel** widget in your bar (Omarchy bar settings, category
*System*), click the broom glyph, and either:
- **Build rules visually** — the built-in editor walks you through folders,
conditions, and actions with dropdowns and validation, or
- **Create starter rules** — copies a commented example file to
`~/.config/omazel/rules.toml` and you take it from there in your `$EDITOR`.
Nothing is required out of the box: flat folders are live-watched by a
built-in Qt directory watcher, and the rules file hot-reloads natively.
Installing `inotify-tools` (`sudo pacman -S inotify-tools`) upgrades
watching to inotify — sharper file events, and the only way to live-watch
`recursive = true` folders (without it, recursive folders are covered by
the periodic sweep alone). Everything else is needed only by the rules you
write, and the widget popup warns when a rule wants a tool that is missing:
| Tool | Needed for |
|------|-----------|
| `inotifywait` | preferred watcher; required for recursive folders |
| `age` | `encrypt` action |
| `tailscale` | `upload via = "taildrop"` |
| `rclone` | `upload` to Dropbox, Proton Drive, or any rclone remote |
| `zip` / `unzip` / `7z` / `unrar` | `compress` / `extract` actions |
| `file` | `mime` conditions |
| `getfattr` (package `attr`) | `source` conditions |
| `xdg-open` | `open` action |
## Two ways to edit rules
**Visual editor** — click the broom, then **Edit rules**. Add watched
folders, stack conditions, pick actions from dropdowns that show exactly the
inputs each action takes, and reorder rules with the arrows. Save validates
everything through the same compiler the engine loads with, so mistakes show
up as messages in the editor instead of a broken config. The previous
version of the file is kept as `rules.toml.bak`.
**Text editor** — the pencil button opens `~/.config/omazel/rules.toml` in
your `$EDITOR`, or just edit the file any way you like. Omazel watches its
own rules file and reloads on save, no restart needed.
The two round-trip: a hand-written file opened in the visual editor keeps
its structure. The one thing a visual-editor save drops is comments (it
warns first).
## Anatomy of a rule
```toml
[[folder]] # one block per watched directory
path = "~/Downloads"
recursive = false # optional: watch subdirectories too
enabled = true # optional: false keeps the config, stops watching
ignore = ["*.iso", "keep-*"] # optional: extra globs to skip
[[folder.rule]] # rules run top-down per folder
name = "Sort screenshots"
all = [ # every condition must hold
{ field = "name", contains = "screenshot" },
{ field = "kind", is = "image" },
]
any = [] # optional: at least one must hold
none = [] # optional: none may hold
actions = [ # run in order when the rule matches
{ do = "move", to = "~/Pictures/Screenshots" },
]
```
A rule with no conditions matches every file — useful as a deliberate
catch-all at the bottom of a folder's rule list.
## Conditions reference
Each condition is a table naming a `field` and exactly one operator key.
### Text fields
| Field | What it sees |
|-------|--------------|
| `name` | basename without extension (`Report Final`) |
| `fullname` | basename with extension (`Report Final.pdf`) |
| `extension` | lowercase, without the dot (`pdf`) |
| `path` | the absolute path |
| `kind` | `image`, `video`, `audio`, `document`, `archive`, `code`, `other` (by extension) |
| `mime` | real content type via `file --mime-type` (`application/pdf`) — sees through wrong extensions |
| `source` | the URL the file was downloaded from — Chromium, Firefox, and wget record it in the `user.xdg.origin.url` extended attribute |
Operators: `is`, `is_not`, `contains`, `not_contains`, `starts_with`,
`ends_with`, `matches` (regular expression), `in` / `not_in` (array of
values). All text matching ignores case.
### Numeric fields
| Field | What it measures | Value format |
|-------|------------------|--------------|
| `size` | file size | `"500 KB"`, `"10 MB"`, `"1.5gb"`, or bare bytes |
| `age` | time since last modification | `"45s"`, `"90m"`, `"12h"`, `"14d"`, `"2w"` — a bare number means days |
| `created` | time since the file was created (birth time; falls back to mtime on filesystems without it) | same as `age` |
Operators: `gt`, `lt`, `gte`, `lte`, `is`, `is_not`.
### Probe fields
| Field | What it checks | Operators |
|-------|----------------|-----------|
| `contents` | text inside the file, via grep (case-insensitive) | `contains`, `not_contains`, `matches` |
| `script` | any command you write — the file path is passed as `$1` and `$OMAZEL_FILE`; 30s timeout | `passes` (exit 0), `fails` (non-zero) |
```toml
all = [
{ field = "contents", contains = "Account Statement" },
{ field = "script", passes = "pdfinfo \"$1\" | grep -q Encrypted" },
]
```
`mime`, `source`, `created`, `contents`, and `script` need a small per-file
probe. Probes only gather what a folder's rules actually use, identical
checks across rules run once, and results are cached by path + size + mtime,
so the periodic sweep re-probes only files that changed. A probe that cannot
run counts as *no match*, even under a negated operator — a broken pattern
can never fire a rule.
### Files that are never matched
Hidden files, files inside hidden directories (judged relative to the
watched folder — deliberately watching `~/.config/something` works), and
in-progress downloads (`*.part`, `*.crdownload`, `*.tmp`, and friends) are
always skipped, before any rule runs.
## Actions reference
| Action | Parameters | Notes |
|--------|-----------|-------|
| `move` | `to` (dir, patterns ok), `as` (optional rename pattern) | collision-safe: `report (2).pdf` |
| `copy` | `to`, `as` | same collision handling |
| `rename` | `pattern` | keeps the original extension if the pattern produces none |
| `sort` | `into` (subfolder pattern, relative to the watched folder) | a move under the hood |
| `trash` | — | `gio trash`, recoverable |
| `delete` | — | permanent — prefer `trash` |
| `open` | — | `xdg-open` with the default application |
| `extract` | `to` (optional), `delete_archive` (bool) | zip / tar.* / 7z / rar / gz; always into a subfolder so a tarbomb can't scatter |
| `compress` | `to` (optional), `name` (optional pattern) | zip |
| `notify` | `message` (optional pattern) | desktop notification |
| `run` | `command` | file passed as `$1` and `$OMAZEL_FILE`; 10-minute timeout |
| `encrypt` | `recipient` or `recipients_file`, `identity`, `delete_original` (bool), `verify` (bool, default true) | see below |
| `upload` | `via` = `taildrop` (+`device`) / `dropbox` / `protondrive` / `rclone` (+`remote`, `path`) | see below |
### Encryption safety
`encrypt` runs [age](https://age-encryption.org) and never removes the
plaintext lightly. With `delete_original = true`, Omazel decrypts the fresh
ciphertext using your `identity` file and compares SHA-256 checksums — only
a byte-identical round trip authorizes removal (to the trash, so even that
is recoverable). Asking for deletion without an identity to verify against
is rejected at load time unless you explicitly opt out with `verify = false`.
### Uploads
- `via = "taildrop"` — `tailscale file cp` to the named `device`.
- `via = "dropbox"` / `"protondrive"` — rclone, defaulting the remote name
to `dropbox:` / `protondrive:`; override with `remote`.
- `via = "rclone"` — any remote from `rclone listremotes` (S3, Drive, B2,
SFTP, …); `remote` is required, `path` optional.
When an earlier action in the same rule relocated the file (say
`encrypt` with `delete_original`), later actions automatically target the
new path — encrypt-then-upload ships the `.age` file, not the vanished
plaintext.
## Patterns and tokens
Anywhere a destination, name, or message accepts a pattern:
| Token | Expands to |
|-------|-----------|
| `{name}` / `{fullname}` / `{ext}` | file name parts |
| `{folder}` | basename of the file's directory |
| `{date}` / `{year}` / `{month}` / `{day}` / `{time}` | from the file's own modification time |
| `{1}` … `{9}`, `{0}` | capture groups from the rule's `matches` condition (original casing preserved; `{0}` is the whole match) |
```toml
all = [{ field = "name", matches = "^Invoice-(\\d{4})-(\\d+)" }]
actions = [{ do = "move", to = "~/Documents/Invoices/{1}", as = "{2} {name}" }]
```
## Rule order and `continue`
Rules run top-down. By default, once a rule *relocates* the file (move,
rename, sort, trash, delete, extract with `delete_archive`, encrypt with
`delete_original`), later rules are skipped; non-relocating rules
(copy, notify, …) fall through. Per rule you can override:
```toml
continue = true # keep matching later rules even after relocating
continue = false # stop after this rule matches, no matter what
```
In the visual editor this is the **After match** dropdown.
## Testing safely with dry run
Flip **Dry run** in the widget popup (or `omarchy-shell omazel dryRunToggle`).
Rules evaluate exactly as normal and the activity feed shows
"Would have: …" entries with the computed destinations — but nothing is
touched. Recommended before trusting any new destructive rule, especially
age-based expiry.
Also useful: the **sweep** button evaluates everything already sitting in
your watched folders right now, so you don't have to wait for a new file to
see rules fire.
## How the engine works
- **Watcher** — one `inotifywait` per folder reacts the moment a file
finishes arriving (`close_write` / `moved_to`). Without inotify-tools, a
built-in Qt directory watcher covers flat folders instead — it reacts to
files appearing, disappearing, or changing size, leaving quiet in-place
rewrites to the sweep. Either way, new files get a settle delay (default
3s) before being judged, so nothing is evaluated half-written.
- **Sweep** — a periodic re-scan (default 5 minutes) fires age-based rules
(files don't emit events by getting older) and catches anything that
arrived while the shell was off.
- **Cooldown** — the same rule won't act on the same file more than once per
minute (configurable), so the overlapping watcher/sweep paths never
double-run and a failing action can't retry in a tight loop.
- **Loop protection** — files Omazel itself just produced are ignored
briefly, so a rule can't chew on its own output.
- **Activity log** — every action (including dry runs and failures) is
appended to `~/.local/state/omazel/activity.log` and shown in the popup.
## Scripting the engine
```bash
omarchy-shell omazel status # JSON engine state
omarchy-shell omazel pause # also: resume, toggle
omarchy-shell omazel dryRunToggle # also: dryRunOn, dryRunOff
omarchy-shell omazel sweep # evaluate everything now
omarchy-shell omazel reload # re-read the rules file
omarchy-shell omazel show # open the widget popup
omarchy-shell omazel editor # open the visual rule editor
```
Hyprland binding example:
```ini
bindd = SUPER SHIFT, O, Toggle Omazel, exec, omarchy-shell omazel toggle
```
## Widget settings
All in the Omarchy bar settings for the widget:
| Setting | Default | Meaning |
|---------|---------|---------|
| Rules file | `~/.config/omazel/rules.toml` | where rules live |
| Sweep interval | 300s | 0 disables the periodic sweep |
| Settle delay | 3s | quiet time before a fresh file is judged |
| Rule cooldown | 60s | min. gap before the same rule re-fires on the same file |
| Start paused | off | come up paused after every shell restart |
| Show action count | on | session action count next to the bar glyph |
| Activity rows | 8 | entries shown in the popup feed |
| Update check | 6h | how often to check the plugin's git remote; 0 disables |
## Troubleshooting
- **Rules don't fire** — check the popup: paused state, rule problems
(each broken rule is listed by name; the rest keep working), and
missing-tool warnings. Then flip dry run on and hit sweep — the feed shows
what would happen.
- **A rule fires on the wrong files** — dry run + sweep is the fastest way
to audit; the feed names the rule for every entry.
- **`source` conditions never match** — the xattr only exists on downloads
saved by browsers/wget onto a filesystem with xattr support, and file
managers may strip it on copy. Check with
`getfattr -n user.xdg.origin.url `.
- **History** — `~/.local/state/omazel/activity.log` is a plain TSV of
everything Omazel ever did: `epoch, status, action, rule, file, dest,
message`.
- **Engine state** — `omarchy-shell omazel status`; shell-level errors land
in `journalctl --user` under `omarchy-shell`.