Writing a Coffee plugin
A plugin adds buttons to Coffee's top bar, commands to the Plugins menu and the command palette, and can work
with your notes. Plugins are JavaScript, run on your Mac only, and each stays off until you turn it on in
Settings > Plugins.
Where plugins live
Each plugin is a folder in ~/Library/Application Support/Coffee/Plugins/, with two files:
Plugins/
word-goal/
manifest.json
main.js
Settings > Plugins > Open Plugins Folder takes you there, and Add Examples writes three small plugins you can
read and change. After editing a plugin, press Reload.
Plugins are never kept in your notes folder, so code synced from another Mac can't run by itself.
The directory
Settings > Plugins > Browse Plugins lists plugins reviewed by Coffee. Install puts one in the Plugins folder and
turns it on. Every version is pinned by the SHA-256 of its package, so what installs is exactly what was reviewed;
anything else is refused. Updates install by themselves once a day, unless a new version asks for a permission
the plugin didn't have: that one waits under the plugin in Settings until you choose Update.
A plugin can say it needs a newer Coffee with "minApp": "1.6" in its manifest; older copies show it but don't
install it.
manifest.json
{
"id": "word-goal",
"name": "Word goal",
"version": "1.0",
"author": "You",
"description": "Shows how close the note is to 1,000 words.",
"permissions": ["read"]
}
id: 1 to 40 lowercase letters, numbers and dashes. Unique among your plugins.name,version: shown in Settings.permissions(optional): what the plugin may do beyond the note you have open.read: list and read your other notes. Private notes are never readable.write: create notes and change other notes. A change lands like any outside edit: an open note shows it
for you to review, and version history keeps the text it replaced.
network:api.example.com:coffee.fetchto that host, over HTTPS only. One entry per host.settings(optional): choices shown under the plugin in Settings > Plugins. See Settings.
Settings shows these permissions in plain words before you turn a plugin on.
main.js
main.js runs once when the plugin is turned on, and whenever you press Reload. It talks to Coffee through
the coffee object.
coffee.commands.add({
id: "shout",
title: "Shout Selection",
run: function () {
var s = coffee.editor.selection();
if (s.length) coffee.editor.replace(s.start, s.length, s.text.toUpperCase());
}
});
Commands and the top bar
| Call | What it does |
|---|---|
coffee.commands.add({ id, title, run, key }) | Adds a command to the Plugins menu and the command palette. key (optional) is a shortcut like "cmd+shift+u"; it needs cmd or ctrl, and one Coffee already uses is left off, with a note in Settings. |
coffee.topbar.add({ id, icon, title, label, run }) | Adds a button to the top bar. icon is an SF Symbol name, title its tooltip, label optional short text beside it. |
coffee.topbar.update(id, { icon, title, label }) | Changes a button you added. |
The open note
| Call | Returns |
|---|---|
coffee.editor.text() | The note's text. |
coffee.editor.selection() | { start, length, text }. Positions count UTF-16 units, like JavaScript strings. |
coffee.editor.insert(text) | Types text at the caret, replacing any selection. One step in undo. |
coffee.editor.replace(start, length, text) | Replaces part of the note. One step in undo. |
coffee.editor.select(start, length) | Selects part of the note and scrolls to it. |
coffee.editor.mark([{ start, length, style, note }]) | Draws marks on the note, in place of the plugin's earlier ones. style is "highlight", "underline" or "error" (a wave); note shows when the pointer rests on the mark. Marks follow their words as you type, go when another note opens, and are never written into the note. mark([]) clears them. |
coffee.notes.current() | { path, title, tags }, or null. |
Other notes
| Call | Needs | Returns |
|---|---|---|
coffee.notes.list() | read | [{ path, title, modified }], modified in milliseconds. |
coffee.notes.read(path) | read | The note's text, or null. |
coffee.notes.write(path, text) | write | true if written. Only existing notes. |
coffee.notes.create(title, body) | write | The new note's path, or null. |
Paths are the ones Coffee shows: relative to your notes folder, with folders from elsewhere starting @name/.
The sidebar
coffee.sidebar.add({ id: "recent", title: "Recently changed", icon: "clock",
run: function (item, index) { coffee.ui.toast("Clicked " + item.label); } });
coffee.sidebar.set("recent", [
{ label: "Groceries", detail: "2m", path: "Groceries.md" },
{ label: "Ideas", icon: "lightbulb" }
]);
sidebar.add adds a section above Notes, which folds away like the others; a plugin can add up to 5.sidebar.set fills it, up to 200 rows. A row with a path opens that note when clicked, and run hears
which row it was.
Code blocks drawn as pictures
coffee.render.add("bars", function (code, ctx) {
// ctx.dark is true in dark mode; ctx.width is the column's width in points.
return '<svg xmlns="http://www.w3.org/2000/svg" width="300" height="40">…</svg>';
});
A closed `bars block then shows the returned SVG under its code, the way Mermaid charts show. Wider
pictures are scaled to the column. The note keeps only the code, and exports show the code too. mermaid andmath stay Coffee's own.
Settings
"settings": [
{ "key": "goal", "title": "Daily goal", "type": "number", "default": 500, "help": "Words a day." },
{ "key": "loud", "title": "Celebrate", "type": "toggle", "default": true },
{ "key": "unit", "title": "Count", "type": "choice", "options": ["words", "characters"] },
{ "key": "name", "title": "Signature", "type": "text" }
]
Up to 30, each with a key (letters, numbers, - and _), a title and a type. They show under the plugin
in Settings > Plugins while it's on. coffee.settings.get(key) returns the chosen value, or the default; the"settings" event says when one changes, with detail.key.
Events
coffee.on(event, function (detail) { … }), where event is:
"open": a note was opened.detail.pathis its path."save": the note was saved."change": you typed. Sent at most a few times a second."settings": one of the plugin's settings changed.detail.keysays which.
Talking to you
| Call | What it does |
|---|---|
coffee.ui.toast(text) | A short message at the bottom of the window. |
coffee.ui.prompt(title, placeholder) | Asks for a line of text. Returns it, or null if cancelled. |
coffee.ui.confirm(title, detail) | Asks OK or Cancel. Returns true for OK. |
Keeping settings
coffee.storage.get(key) and coffee.storage.set(key, value) keep small values (text, numbers, lists, objects,
under 1 MB in all) for your plugin on this Mac.
Fetching from the web
coffee.fetch("https://api.example.com/v1/quote", { method: "GET" })
.then(function (r) { coffee.editor.insert(JSON.parse(r.text).quote); })
.catch(function (e) { coffee.ui.toast("Couldn't fetch: " + e); });
fetch needs a network: permission for the host, and only uses HTTPS.
When something goes wrong
An error in a plugin is caught and shown under it in Settings > Plugins. Coffee carries on, and the plugin's
JavaScript runs apart from the editor, so a slow plugin doesn't hold up your typing. console.log writes to
the Mac's log, readable in Console.app.