The rules engine

Triggers

A trigger is a condition plus the actions to run when it holds. Every time something is copied, the triggers are consulted in order, and every one that matches runs.

Triggers are ranked from top to bottom in the Triggers pane. Each time an item is copied, it runs through them from most prioritised to least, and every trigger whose conditions hold runs. Each one is asked about the text as it stands, so a trigger is tested against what the triggers above it made of the copy and its actions carry on from there — the same way the actions inside one trigger feed each other.

A trigger can end the run where it fires: see Stopping the run below.

A trigger participates in that only when it is enabled and defines at least one condition. Nothing runs a trigger by hand — choosing an action from the overlay is the manual path, and triggers are not even listed there.

Conditions

A trigger's conditions can be any or all of the listed conditions, and each one either

  • matches, or does not match, a regex — where ${name} is replaced by a variable's value, escaped so that it matches literally; or
  • is a script, which uses its exit code to say whether it holds.

A condition script is passed one parameter, the item on the clipboard. The item is also on stdin and in $PD_CLIPBOARD, and variables arrive as $PD_VAR_<name>. A secret is never handed to one: a trigger cannot prompt, so a trigger that names a locked secret leaves the reference as written rather than putting a Touch ID dialog on screen because something was copied.

A condition whose regex does not compile never matches, in either direction.

Conditions short-circuit, which is worth arranging deliberately: the shipped JSON copied trigger asks first that the text start with { or [, and only then that it parse, so the second condition starts a process for text that already looks like JSON rather than on every copy.

No trigger fires on an image, and none fires while recording is paused or while an excluded app is in front.

Stopping the run

Each trigger carries a checkbox — stop evaluating triggers if fired — that ends the run where it fires: nothing below it is consulted, however well it matches. It is the way to say "if this one is what the copy is, the rest do not apply."

It is about firing, not about being consulted: a trigger that carries it and does not match leaves the rest of the list exactly where it was.

New triggers are made with it off, so a trigger you add joins the run rather than cutting it short. Every trigger written before the checkbox existed has it on, because until then firing always ended the run — so nothing you already had starts behaving differently the day you take the update. Switch it off on one of those and triggers below it start being consulted after it fires.

What happens when one fires

The trigger's actions run in the order listed, each one receiving the previous action's output. A step that fails is reported and leaves the text as it was; the remaining steps still run.

When a trigger rewrites a copy, the rewritten text becomes the current value of the entry you copied rather than stacking a second entry on top of it — and one copy is one rewrite however many triggers took part in it. The text as originally copied stays in that entry's history, along with the names of the triggers that changed it, in the order they fired — so $1 | undo is the way back, and so is ⌃↩ on the row. Running an action by hand on the newest entry works the same way.

Writing the result back to the pasteboard does not re-enter the engine, so triggers cannot loop.

Two exceptions are worth knowing:

  • A trigger naming forget asks for the opposite: the entry is dropped rather than given a new value, and the text — rewritten or not — is left on the clipboard with nothing behind it in the history. A pin does not save an entry from it.
  • A trigger that produces a list stores it as one and does not start the paste queue: a trigger fires on the copy, and there is no paste yet to deal anything out into. The row keeps its items, so using it later starts the queue.

Because a trigger fires on a copy rather than on a paste, what unfmt and html decide when a trigger names them is about the copy: the copy is stored in that shape and stays that way however often it is used. That is how "anything copied out of that app is plain text" gets said.

Folders and order

Triggers are filed into folders the way actions are, and for the same reason: a pane of forty of them is a pane nobody can find anything in. Folders nest, deleting one lifts everything inside it into the folder that held it, and renaming one takes everything along. A folder is a row like any other — select it, drag it into another folder or out to the top level, move it with the arrows or its contextual menu, and see it in the editor beside the list.

The one thing that is not like the Actions pane is what the order means. There it settles which of two actions a keyword resolves to; here it is when a trigger fires, so the list read top to bottom is the order the app consults them in. The app keeps the triggers in the order the tree draws them — at each level the triggers at that level, then the folders below it — which means filing a trigger moves it in the order: into a folder it lands at the end of that folder's run, and a folder as a whole is consulted after the triggers sitting beside it.

What ships

A first launch — one with no config.json to read — adds three triggers to the standard library.

JSON copied is the only one armed. Copy anything that parses as JSON and it comes back indented by two spaces, with the keys left in the order they were written. Pretty printing is idempotent, and the text as it was copied stays in that entry's history either way, so a reformat is never lost work. The pretty printer runs osascript -l JavaScript, which is on every Mac and needs no Command Line Tools — and unlike plutil -convert json it does not sort the keys.

Jira ticket copied and Remove Tracking Parameters From URL ship disabled. They show the shape without rewriting anybody's clipboard uninvited.

Switching the second of those on is the last thing the tutorial asks for — it is the one lesson whose effect outlives it. Once armed, a link copied with utm_source, fbclid and the rest on it arrives clean, and what you actually copied stays behind it in that entry.

Some shapes worth stealing

A copy that should never have been kept, dropped the instant it is made:

Matches regex   ^[A-Za-z0-9_-]{32,}$
Actions         forget

Everything copied out of one app kept as plain text:

Condition       a script:  [ "$PD_VAR_source" = "Notes" ]
Actions         unfmt

A date normalised on the way in — which is what a copy of strftime carrying its own format is for, since a trigger names actions without arguments:

Matches regex   ^\d{4}-\d{2}-\d{2}
Actions         my-strftime      (format on the action: %B %e, %Y)

The Triggers pane

Triggers are listed in priority order, as a tree of folders drawn the way the Actions pane draws its library, each folder saying how many triggers are inside it however deep they sit. The first armed trigger drawn is the first one consulted.

A search field answers with everything it finds, wherever it is filed, each row saying which folder it lives in. A trigger is found by its name first and then by the keywords of the actions it runs — so "which one was it that trims things?" is a question the field answers — and a hit on an action it happens to run never outranks a trigger actually called that.

Triggers can be dragged into a folder, enabled with a checkbox, added, duplicated and removed; the arrow buttons move one among its siblings in the same folder. So can a folder: dragging one into another folder, or onto the empty part of the list to bring it back out, carries every trigger in it — and since a folder is consulted where it sits, that moves all of them in the firing order too. The folder button on the toolbar adds a folder beside what is selected rather than inside it; nesting is done through a folder's own menu, or by dragging.

The editor covers the trigger's name, whether it is enabled, whether firing ends the run, the folder it is in, its match mode and conditions, and the ordered list of actions to run, each reorderable. A trigger that ends the run says so on its row too, so the pane read top to bottom shows where a copy stops. The folder menu says what filing does: a trigger moved into a folder lands at the end of it, which changes when it is consulted. It flags regexes that do not compile and keywords that no action defines, and includes a test area that runs the trigger against sample text — or against the current clipboard — showing whether the conditions fire, the result, and any errors.

Picking the action a step runs

A step's action is chosen from a list that opens the library the shape it really is: collection, then folder, then action. The top level is your own actions and the Standard Library beside them; opening a folder shows what is directly inside it, and a folder says how many actions are below it however deep they sit. or goes back up a level.

Typing anything searches the whole library at once, wherever an action is filed, and each result says where it lives — Standard Library ▸ Time. So somebody who knows what the action is called does not have to remember where it was filed, and somebody who does not can walk to it.

and move the highlight, takes it, and a click does either. The list opens on the folder the step's current action lives in. An action that has been switched off in the Actions pane is marked off, which is worth knowing before picking it — though a trigger that names one still runs it; the switch hides it from the overlay and the menu bar, and the trigger's own switch is what decides whether it fires. A step naming an action that no longer exists shows that name as it stands rather than quietly replacing it.