Plugins

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"]
}

for you to review, and version history keeps the text it replaced.

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

CallWhat 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

CallReturns
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

CallNeedsReturns
coffee.notes.list()read[{ path, title, modified }], modified in milliseconds.
coffee.notes.read(path)readThe note's text, or null.
coffee.notes.write(path, text)writetrue if written. Only existing notes.
coffee.notes.create(title, body)writeThe 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 and
math 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:

Talking to you

CallWhat 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.

Send a plugin for review