Start drawing

Drawdy › Docs › Build a plugin

Build a plugin

Add a tool to the Drawdy toolbar, read what's selected on the board, and create elements — in about fifteen minutes.

This API is in design

The plugin API isn't shipped yet. Everything below is the shape we're proposing, published early so you can argue with it before it hardens. Names and signatures will change.

Tell us what you'd build →

What you'll learn

By the end of this guide you'll have a working plugin that adds a Tidy up tool to the toolbar. Select a few nodes, click it, and they snap into an even row. Along the way you'll touch the four things every plugin uses: the manifest, the plugin lifecycle, board events, and the node API.

Prerequisites

Where plugins live

Drawdy loads plugins from your vault folder at <workspace>/.drawdy/plugins/<plugin-id>/. Each plugin is a folder with a manifest and a bundled entry file — no install step, no registry required for local development.

1

Clone the sample plugin

Start from the sample rather than an empty folder — it ships with the build config and type definitions already wired.

Terminal
# from your workspace folder
cd .drawdy/plugins
git clone https://github.com/drawdy/sample-plugin tidy-up
cd tidy-up && npm install && npm run dev

npm run dev watches your source and rebuilds main.js on every save. Drawdy picks up the change the next time you reload the workspace.

Tip — reload without restarting

Press ⌘R (Ctrl+R on Windows) inside Drawdy to reload plugins without closing the app.

2

The manifest

Every plugin needs a drawdy.plugin.json beside its entry file. This is what the app reads before it runs a single line of your code.

drawdy.plugin.json
{
  "id": "tidy-up",
  "name": "Tidy up",
  "version": "0.1.0",
  "minAppVersion": "0.5.0",
  "description": "Align the selected nodes into an even row.",
  "author": "you",
  "main": "main.js"
}

The id must match the folder name and be unique across the marketplace. Treat it as permanent — changing it later looks like a different plugin to anyone who installed yours.

3

Your first tool

A plugin is a class with two lifecycle hooks. onload runs when the workspace opens; onunload must undo anything you registered.

src/main.ts
import { Plugin } from "@drawdy/sdk";

export default class TidyUp extends Plugin {
  async onload() {
    this.addTool({
      id: "tidy-up",
      name: "Tidy up",
      icon: "align-horizontal",
      onRun: () => this.tidy()
    });
  }

  onunload() {
    // tools added with addTool are removed automatically
  }
}

Save, reload, and a new button appears at the end of the toolbar. It does nothing yet.

Clean up after yourself

Anything you attach by hand — timers, DOM listeners, network polling — must be torn down in onunload. A plugin that leaks listeners will keep firing after the user disables it.

4

React to selection

Most plugins act on whatever the user has selected. board.selection gives you the current set; board.on("selection", …) tells you when it changes.

src/main.ts
async onload() {
  this.addTool({ id: "tidy-up", name: "Tidy up", onRun: () => this.tidy() });

  // disable the tool unless two or more nodes are selected
  this.register(
    this.board.on("selection", (nodes) => {
      this.setToolEnabled("tidy-up", nodes.length >= 2);
    })
  );
}

Wrapping the subscription in this.register() hands the teardown to Drawdy — the listener is removed for you when the plugin unloads.

5

Draw on the board

Now the actual work. Read the selection, sort it left to right, and write new positions back inside a single transaction so the whole change is one undo step.

src/main.ts
tidy() {
  const nodes = this.board.selection;
  if (nodes.length < 2) return;

  const sorted = [...nodes].sort((a, b) => a.x - b.x);
  const gap = 32;
  let x = sorted[0].x;

  this.board.transact("Tidy up", () => {
    for (const node of sorted) {
      node.moveTo({ x, y: sorted[0].y });
      x += node.width + gap;
    }
  });
}
Tip — one transaction, one undo

Every mutation inside board.transact() collapses into a single entry in version history. Without it, moving eight nodes costs your user eight presses of ⌘Z.

6

Ship it

Build once, then publish. A release needs three files at the root of your repo: manifest, main.js, and a README.

Terminal
npm run build
git tag 0.1.0 && git push --tags

Tag the release and open a pull request against the community plugin list. Once it's merged, your plugin shows up in the in-app marketplace and users install it with one click.

The API surface, in one screen

The whole proposed surface, so you can judge it quickly:

@drawdy/sdk
class Plugin {
  onload(): void | Promise<void>
  onunload(): void
  addTool(opts): void       // id, name, icon, onRun
  setToolEnabled(id, on): void
  register(teardown): void  // auto-cleanup on unload
  get board: Board
}

interface Board {
  selection: Node[]
  nodes: Node[]
  on(event, handler): Teardown   // "selection" | "change" | "pointer"
  create(spec): Node            // { type, x, y, width, height, text }
  transact(label, fn): void     // one undo step
}

interface Node {
  id: string; type: string
  x: number; y: number; width: number; height: number
  moveTo(pos): void
  update(props): void
  remove(): void
}

What's next

That's the whole model: a class, a manifest, a board, and nodes. If something you need doesn't fit in it, that's the useful thing to tell us — the API is still soft enough to change shape around real plugins.

Help shape this

Open an issue with the plugin you'd build and what's missing. github.com/Porches2/Drawdy