The rules engine
Actions
An action is a named transformation of text — the thing a pipeline chains, a trigger runs, a shortcut fires and the menu bar applies. About fifty ship with the app, and you can write your own.
Rules are made of two separate, independently defined pieces:
- an action is a named transformation of the clipboard text — what runs;
- a trigger is a named condition for firing, plus the ordered list of actions to run when it does — when it runs.
Each lives in its own preferences pane, and triggers name actions by keyword, so one action can serve any number of triggers.
An action has a keyword, a description, a folder and a help page.
The keyword is the action's identity: a single word — lowercase letters, digits, - and _ — that invokes it in a pipeline ($1 | trim) and that triggers and shortcuts name it by. It is folded to that shape as you type it, since a keyword with a space in it could not be invoked. An action with no keyword cannot be run from the search bar or named by a trigger, and the pane says so.
The description is the words a person reads — "Trim whitespace" — and is what the menu bar shows. It has no effect on how an action is invoked, though the overlay matches against it as well as the keyword, scored below a keyword hit, so half-remembering an action still finds it.
The help is Markdown: what the action is for, what it expects, and an example. It is written and read in the Actions pane, which renders headings, lists, quotes, code, bold, links and fenced code blocks — and it is what the overlay shows under a highlighted action after a second's stillness. Everything the app ships carries one.
Kinds
The three kinds you can make:
- Substitution — a regex and a replacement. Pure and instant, so the search bar previews it live as you type.
- Script — shell, run under
/bin/sh. - Pipeline — a chain of other actions written in the same language the search bar speaks, which is how a chord or a trigger gets a whole chain rather than one step.
Several more kinds ship but are not offered in the picker, because each has one or two shipped actions and nothing to configure that a pipeline does not say better — $1 | undo 2, $1 | ocr, $1 | strftime %A, | now UTC, $1 + 5, $1 ++ "!". A second copy under another keyword would be a way to be confused rather than a way to do anything. (now ships twice, as now and utcnow, because a trigger can name an action but cannot name it a zone.) Anyone wanting one inside an action of their own writes a pipeline action naming it.
That is a matter of what the pane offers rather than of what the app can do: an action written as one of those kinds by hand in config.json still runs, and the pane shows what kind it is instead of a picker with no such option in it.
Substitution actions
The regex engine supports named capture groups and pre-supplied variables. Variables can be used as if they were capture groups, and capture groups override variable values.
- In a pattern,
${name}is replaced by the variable's value, escaped so that it matches literally. A name with no matching variable is left in place. - In a replacement,
$1…$9,${name}and$nameresolve to the capture group of that name or number when the pattern declares one, and otherwise to the variable of that name. A name that is neither is left in place, and$$produces a literal$. - Named groups are written
(?<name>…). A declared group that did not participate in a match resolves to the variable of the same name, or to the empty string.
Substitution replaces all matches by default, or only the first when Replace all matches is off. A pattern that does not compile is flagged in the pane, and an action that fails leaves the text unchanged.
Script actions
A script takes the item on stdin and as $1, and its standard output — minus one trailing newline — becomes the new item. A pipeline's extra arguments arrive as $2 onwards; a script run any other way has none.
The item is also in $PD_CLIPBOARD, and variables in $PD_VAR_<name>. An unlocked secret arrives as $PD_SECRET_<name> without its !.
Scripts run under /bin/sh and are terminated after 10 seconds. A non-zero exit is a failure, and the script's standard error is the reason inside the error. Standard error on a script that still exits 0 is a warning instead — which is what makes writing to it worth doing.
The two roads the text arrives by are not byte-for-byte the same, which matters to a script that counts characters. Anything passed as an argument — the script itself, $1, and the pipeline's arguments — is normalised to NFD on the way, because that is what macOS does to a process argument, so a precomposed é arrives as e and a combining accent. Stdin is written as it stands, in NFC. A script that cares should normalise what it is given rather than assume either.
A script action also carries whether the search bar may run it while a pipeline is still being typed, which is off by default and worth turning on only for scripts that are fast and change nothing.
Writing formatting
A script writes plain text by default. Set Writes to HTML or Rich text and its standard output is read as that markup instead: the bytes become the shape the item goes on the clipboard in, and the text they read as becomes the item's value. With md-rich, it is one of the two ways an action makes formatting rather than carrying forward formatting a copy already had.
printf '<a href="%s">%s</a>' "$1" "$1"
Set to HTML, that turns a copied URL into a real hyperlink. The item's value is still the URL, so the row reads and searches as it always did and an app that takes only text still gets the URL — while one that takes HTML gets the link.
A script that claims a shape and writes something that will not read as one fails, rather than putting markup on the clipboard as though it were text.
Formatting describes one exact run of characters, so it goes exactly as far as the text it was made for: $1 | link | upper pastes plain, because upper produced characters the markup no longer describes.
The other way is md-rich, which needs no script: it reads the text as Markdown and puts what that renders to on the clipboard as formatting, so writing in Markdown and pasting into Mail, Notes or a chat window gives real headings, real bullets and real links. Its value is the words without their marks, and the same rule holds afterwards. md-html is the same conversion handing back the HTML as characters instead.
Scripts that only do something
Turn on Run for what it does — discard the output for a script run for its effect rather than its answer: filing something, opening something, sending something. Nothing it writes is read, and the text it was handed carries on untouched — so the next filter in a pipeline still has its input, and a trigger still has the copy.
Without it, a script with nothing to say has its silence taken for an answer, which leaves the item empty. It says nothing about whether the script worked: a non-zero exit is still a failure.
Folders
Actions are grouped into folders, which nest. A folder is a place to look and nothing more: keywords stay global, so $1 | trim says nothing about which folder trim came from, and two actions in different folders still may not share a keyword. Moving an action never changes how it is invoked.
Deleting a folder never deletes work — everything inside it, actions and subfolders both, moves up into the folder that held it. Renaming one takes everything inside it along, and so does moving one: drag a folder into another folder to file it there, or drop it on the empty part of the list to bring it back out to the top level. A folder's contextual menu does the same without the dragging — Move to Top Level, or Move Into any folder that is not itself or inside itself.
The arrow buttons move a folder among the folders beside it. That works by moving what is inside it, which is where a folder's place in the list comes from, so a folder holding nothing yet sits after the ones that hold something and cannot be lifted above them until you put something in it.
Running one by hand
Actions are the only thing you run by hand: from the overlay with ⌘1–⌘9 on a matching row, from the menu bar's Apply Action, or from a chord you bound in Preferences → Shortcuts. Each of those runs it against the current clipboard item, and the result becomes the clipboard contents and the newest entry in the history — pasted back into the app you were in if Automatically paste selection is on.
An action run by hand on a list puts the first item on the clipboard and queues the rest, since there is a paste in front of it. A trigger's does not.
An action can be disabled, which hides it from the overlay and the menu bar. A trigger that names a disabled action still runs it, and so does a shortcut bound to it; the trigger's own switch is what decides whether it fires at all.
When two actions share a keyword
Keywords should be unique. Where two share one, the topmost definition wins — the same way variables resolve — and both panes flag the clash.
Your own actions are listed above the standard library, which is what makes an action written under a shipped keyword replace it. The pane says so rather than calling it a clash: writing an action that replaces a shipped one is a thing people do on purpose.
Changing an action's keyword does not update the triggers that name it. The Actions pane lists which triggers use the selected action, and a trigger naming an action that no longer exists reports it and skips that step.
Warnings
A warning is what a run that worked has to say about how. It never stops a pipeline and never changes the result: the text alongside it is the real one. Two things produce one today — a script that writes to standard error and still exits 0, and a command that mixes the two kinds of arithmetic operator.
Warnings are named the way errors are: by the action, and by the road taken to it through a pipeline action or a trigger (tidy › noisy: a note). They surface wherever a result does — in orange under the search bar's preview, under the Test area in the Actions and Triggers panes, and in the HUD, which holds them a little longer than an ordinary message. An error is the louder of the two and takes the HUD when both happen.
The standard library
The app ships with about sixty actions, filed under Standard Library in sections: General, Text, Markdown, Lists, Case, Encoding, Math, JSON, Time, Web, Development and Fun. The standard library reference lists every one of them with its own help.
The library is built from code at every launch rather than read from config.json. That is what lets a release add to it and have every install get the additions, and it is why nothing about it can be lost by a damaged config. The only thing saved about it is which of its actions you have switched off.
Its actions are read-only: the pane marks them with a lock, the editor explains why, and the way to change one is to take a copy — which lands outside the library's folder as one of your own, above the library, where its keyword is what that word now resolves to. Nothing of yours can be put inside the library's folders.
Its scripts stick to what is on a stock Mac — /bin/sh, the BSD userland, and osascript -l JavaScript where a real parser is wanted — so none of it needs the Command Line Tools, a package manager or a network. The cheap and pure ones are marked as safe to run while a pipeline is being typed; the ones that start a JavaScript interpreter are not.
The Actions pane
The library as a tree of folders. Actions are listed by keyword with their description beneath and a lock where one is part of the standard library; each folder shows how many actions are inside it. Your own are listed above the standard library, whose sections start collapsed.
A search field above the list replaces the tree with everything the query finds, wherever it is filed, each row saying which folder it lives in; clearing it brings the folders back. The words are matched the way the overlay matches them — the keyword first, then the description, and a hit on the description never outranking a hit on a keyword — so an action answers to the same words everywhere it can be looked for. Results are ranked rather than filed, so the arrow buttons do not reorder while a search is running.
A folder is a row like any other — click it to select it, drag it, and the toolbar acts on it. Click a folder's name to select it; click its chevron to open or close it.
Actions can be dragged into a folder, shown or hidden from the overlay with a checkbox, added, duplicated and removed, and reordered with the arrow buttons — which move an action among its siblings in the same folder. New and duplicated actions get a keyword that is not already taken.
The + button adds an action inside the selected folder, or beside the selected action. The folder button adds a folder beside what is selected rather than inside it, so pressing it twice gives you two folders side by side; to nest one, use a folder's contextual menu or drag it in. With nothing selected both add at the top level. The − button removes a selected folder as readily as a selected action — everything inside it moves up rather than being deleted with it.
Selecting a folder shows it in the editor beside the list: its name, a menu of everywhere it could sit, how much is filed inside it, and a way to delete it. A folder's contextual menu adds an action or a subfolder inside it, renames it, moves it, or deletes it. The standard library's folders offer none of that — they are the app's, so you cannot carry one out or file anything of yours inside.
The editor for the selected action covers its keyword and description — showing how it reads as a pipeline step — the folder it is in, its kind and that kind's fields, and its help, written and read in the same place with a Write/Read switch. It flags regexes that do not compile, empty keywords and keywords shared with another action. Under Used by it lists the triggers that use it and the shortcut that runs it, if one does, saying where that chord works and warning that a rename will not follow it.
It includes a test area that runs the action against sample text — or against the current clipboard — showing the result and any errors. A standard library action shows all of that read-only, with a note saying why and a button that takes a copy.