Repository navigation
Writing a plug in
A plug-in adds something to ThoughtKnot through one typed, versioned interface. The built-in exports use the same interface, so a plug-in is never second class.
This page describes main, which the nightly build is made from. The latest release is 0.7.0: ✅ is in it, 🌙 is only in the nightly build, 🟡 is being built and ⏳ is planned.
flowchart LR
P["Your plug-in<br/><i>:Plugin</i>"] -->|names in its manifest| M["Extension points<br/><i>:Manifest</i>"]
M -->|is given only those| R["Plug-in registry<br/><i>:Registry</i>"]
R -->|checks what it adds| K["Contract checks<br/><i>:Contract</i>"]
R -->|offers it in| A["Every app<br/><i>:Host</i>"]
- Your plug-in names itself, its version, the interface version it was built for and the extension points it uses.
- The registry gives it those points and nothing else.
- Everything it adds is checked as it is added. If one thing is refused, none of it is installed.
- Once installed, what it adds is offered in every app: Obsidian, the browser editor and, later, the standalone app and VS Code.
- If your code throws while running, ThoughtKnot catches it and names your plug-in. A drawing is never left blank.
The interface is version 1. A plug-in built for another version is refused.
| Extension point | What a plug-in adds through it |
|---|---|
exports |
A data export: a format offered in Export, with its title, file extension, media type and writer. |
theme |
Colours of its own, named under its id, in light and dark, each meeting the locked contrast. |
Diagram types, graph steps and sources, storage, AI providers and commands join these one at a time, each with its own contract checks.
This plug-in adds a plain text export: one line for each thing, then one for each link. The wiki's tests install this exact code, so it always works with the current interface.
import { PLUGIN_API, pluginRegistry } from '@thoughtknot/core';
const textList = {
id: 'acme.text-list',
version: '1.0.0',
api: PLUGIN_API,
uses: ['exports'],
register: (points) => {
points.exports.add({
format: 'text-list',
title: 'Text list',
extension: 'txt',
mime: 'text/plain',
write: (data) =>
[
...data.things.map(([id, kinds]) => `${id} (${kinds.join(', ')})`),
...data.links.map(([from, type, to]) => `${from} ${type} ${to}`),
].join('\n'),
});
},
};
const registry = pluginRegistry();
registry.register(textList);-
Its id is dotted lower-case words, such as
acme.text-list. Use your own name first, so no two plug-ins clash. - Its version is major.minor.patch.
- Its writer gives the same text every time for the same graph, and never changes the graph it is given.
-
Its colours, if it adds any, are named
--gn-plugin-<your-id>-<word>. Each gives a light and a dark colour that meets the locked contrast on the background it sits on.
Run the contract checks against your own plug-in in your tests. They need no test framework: each check throws, saying what was wanted, when something breaks a promise.
import { exporterContract } from '@thoughtknot/core';
for (const check of exporterContract(myExporter)) check.run();Every data export (exporterContract):
- ☑ it writes text for every export: none, a graph with notes, and a view with its faded context
- ☑ it writes the same bytes each time for the same export
- ☑ it leaves the export it is given as it was
- ☑ it is offered under a title, with a file extension and a media type
Every theme colour (themeTokenContract):
- ☑ it is named under its plug-in’s own prefix, so it takes no core token’s name and no other plug-in’s
- ☑ it gives a colour, as #rrggbb or #rgb, for light and dark, on a core background
- ☑ it meets the locked contrast in the light theme, on the background it sits on
- ☑ it meets the locked contrast in the dark theme, on the background it sits on
A plug-in's styles may use any of the core's design tokens, so it follows the app's theme, light or dark, with no work. The app's own theme always wins: each token reads the app's own colours where it is shown.
Every design token
| Token | Kind | What it is for |
|---|---|---|
--gn-bg |
colour | the background of a card, a pane or the page |
--gn-bg-alt |
colour | the background of a bar, a strip, a sheet or a control |
--gn-bg-hover |
colour | the background of a row the pointer is over |
--gn-border |
colour | the line around a card, a control or a section |
--gn-border-hover |
colour | that line where the pointer is over it |
--gn-border-focus |
colour | that line, or a ring, with the focus |
--gn-text-normal |
colour | body text, and the focus ring drawn in it |
--gn-text-muted |
colour | secondary text: captions, hints and counts |
--gn-text-faint |
colour | a mark that only accompanies text, such as an underline |
--gn-text-error |
colour | the text and mark of a problem |
--gn-text-warning |
colour | the text and mark of a warning |
--gn-text-on-accent |
colour | text on the accent colour |
--gn-text-selection |
colour | the background of selected text |
--gn-accent |
colour | the accent of a chosen or leading control |
--gn-link |
colour | a link to a note that exists |
--gn-link-unresolved |
colour | a link to a note that does not exist yet |
--gn-hue-red |
colour | the red of a mark that says a thing went wrong |
--gn-hue-orange |
colour | the orange of a mark that says a thing needs a look |
--gn-hue-green |
colour | the green of a mark that says a thing is well |
--gn-code-background |
colour | the background of a block of code |
--gn-code-normal |
colour | the text of code |
--gn-code-keyword |
colour | a keyword in highlighted code |
--gn-code-important |
colour | a label or type in highlighted code |
--gn-code-operator |
colour | an operator or arrow in highlighted code |
--gn-code-value |
colour | a number or constant in highlighted code |
--gn-code-tag |
colour | a variable in highlighted code |
--gn-code-string |
colour | a string in highlighted code |
--gn-code-property |
colour | a property name in highlighted code |
--gn-code-function |
colour | a function name in highlighted code |
--gn-shadow-s |
depth | the shadow of a card or a menu lifted a little |
--gn-shadow-l |
depth | the shadow of a sheet or a tooltip lifted over the page |
--gn-layer-popover |
depth | the stacking level of a menu or a notice |
--gn-layer-tooltip |
depth | the stacking level of a tooltip |
--gn-layer-modal |
depth | the stacking level of a full-screen view |
--gn-font-ui |
type | the typeface of the interface |
--gn-font-mono |
type | the typeface of code and source text |
--gn-font-size-xs |
type | the smallest text of the interface |
--gn-font-size-s |
type | the small text of the interface, for most controls |
--gn-font-size-l |
type | the large text of a heading in the interface |
--gn-font-size-text-s |
type | text a step smaller than the note’s own |
--gn-code-size |
type | the size of code |
--gn-font-weight-bold |
type | the weight of bold text |
--gn-font-weight-semibold |
type | the weight of a label that stands out |
--gn-icon-xs |
spacing | the size of a small icon |
--gn-space-half-1 |
spacing | the finest step of space, 2 pixels by default |
--gn-space-half-2 |
spacing | the second fine step of space, 4 pixels by default |
--gn-space-1 |
spacing | one step of space, 4 pixels by default |
--gn-space-2 |
spacing | two steps of space, 8 pixels by default |
--gn-space-3 |
spacing | three steps of space, 12 pixels by default |
--gn-space-4 |
spacing | four steps of space, 16 pixels by default |
--gn-space-5 |
spacing | five steps of space, 20 pixels by default |
--gn-space-6 |
spacing | six steps of space, 24 pixels by default |
--gn-space-9 |
spacing | nine steps of space, 36 pixels by default |
--gn-radius-s |
corners | the rounding of a small control’s corners |
--gn-radius-m |
corners | the rounding of a menu’s or a notice’s corners |
--gn-radius-l |
corners | the rounding of a card’s corners |
--gn-cursor-link |
pointer | the pointer over something that opens a note |
These come with ThoughtKnot, added through the same interface:
| Plug-in | Version | Uses | Adds |
|---|---|---|---|
thoughtknot.data-exports |
1.0.0 | exports |
GQL export, CSV export, GraphML export |
Today plug-ins are built into ThoughtKnot. Loading one from outside, with its manifest limiting what it may reach, is being built under the requirements below.
flowchart LR
s43(["§43 Plug-ins, layers and their contracts<br/><i>:Section</i>"])
subgraph g_released["✅ Released"]
affected_tests_by_imports["affected_tests_by_imports<br/><i>:Requirement</i>"]
end
s43 --- g_released
subgraph g_part["🟡 Being built"]
plugin_interface["plugin_interface<br/><i>:Requirement</i>"]
plugin_manifest_limits["plugin_manifest_limits<br/><i>:Requirement</i>"]
plugin_contract_suites["plugin_contract_suites<br/><i>:Requirement</i>"]
hosts_pass_contract["hosts_pass_contract<br/><i>:Requirement</i>"]
end
s43 --- g_part
subgraph g_open["⏳ Planned"]
layers_checked["layers_checked<br/><i>:Requirement</i>"]
plugin_rtl["plugin_rtl<br/><i>:Requirement</i>"]
end
s43 --- g_open
| Requirement | Where it stands | Level | Rule |
|---|---|---|---|
plugin_interface |
🟡 Being built | must | Diagram types, data exports, graph steps and sources, storage backends, AI providers and the commands and panels a plug-in adds must be reached through one typed, versioned plug-in interface, published with the library, and the built-in ones must register through it too. |
plugin_manifest_limits |
🟡 Being built | must | A plug-in must declare in a manifest the extension points and host abilities it uses and get nothing else, and a plug-in that throws or runs too long must be stopped and named, never leave a drawing blank. |
plugin_contract_suites |
🟡 Being built | must | Each extension point must ship a contract suite that any plug-in can run against itself, and every built-in must pass it in CI. |
hosts_pass_contract |
🟡 Being built | must | Every host's adapter, Obsidian's, the browser page's, the standalone app's and VS Code's, must pass the one shared host contract suite in CI, run against the real adapter. |
layers_checked |
⏳ Planned | must | Each layer must import only the layers below it, from the graph format library through the core, the display layer and the plug-ins to the hosts, checked in CI on every change. |
affected_tests_by_imports |
✅ Released | must | The quick test run must choose, besides the tests named after a changed module, every test that reaches it through the import graph, by importing it directly or through others or by naming it to build, and run the whole suite only for a change no test reaches or one every test shares. |
plugin_rtl |
⏳ Planned | must | Every plug-in must work in a right-to-left language: what it adds reads in the host's direction, keeps the person's names in their own direction and uses logical sides only, checked by its extension point's contract suite (Roger, 18 Oct 2026). |
Using it
The project
Building it
Designs