What even is a plug?
A plug is the most powerful way to extend Silverbullet. You can use Javascript inside a WebWorker (in the browser) to do pretty much whatever you want.
Requirements
Know stuff about the web, know JavaScript, know Space Lua. Some Typescript is good.
Your Environment
First, you are going to need a controlled environment to test your plug. The easiest is to just run the latest docker container. Alternatively - That’s what I do - you can have the Silverbullet repository cloned, this way you can quickly look up docs and code.
git clone https://github.com/silverbulletmd/silverbullet
cd silverbullet
# You'll need to install node/npm and go
make setup
make
# Generally using the default docs for testing is useful
./silverbullet website/
# You can now access it at http://localhost:3000/, just leave it running in the background
The Boilerplate
I’m generally not a fan of templates and thus also not a fan of the plug template, so let’s start from scratch, it’s not that much.
# Decide on a name, ideally one that’s not already taken
mkdir mycoolplug
cd mycoolplug
# Use git, create a README, add a LICENSE
git init
touch README.md # TODO
touch LICENSE # TODO
Now, like any good javascript project, we will need a package.json.
{
"name": "mycoolplug",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "npx plug-compile mycoolplug.plug.yaml"
},
"dependencies": {
"@silverbulletmd/silverbullet": ">=2.5.3"
}
}
Two things here:
- The dependency on
@silverbulletmd/silverbullet, which is a tiny wrapper to make interactions with Silverbullet easier. We’ll definitely need it later. It also provides theplug-compilecommand npx plug-compile mycoolplug.plug.yaml, this invokes a build script to actually build and bundle your plug. It needs amycoolplug.plug.yamlthough, called a manifest, so let’s create that.
name: mycoolplug
functions:
sayHi:
path: main.ts:sayHi
sayBye:
path: main.ts:sayBye
Name seems obvious, but what are functions? Before getting to that, let’s create this main.ts file referenced here. (You could also use Javascript, but I’m a big sucker for Typescript, so I’ll continue with that)
export function sayHi(): void {
// Be patient! We'll get to it
}
// Async and parameters work too. Careful though, only json serializable types can be passed.
export async function sayBye(person: string): Promise<void> {
// I said be patient
}
So, duh, functions are just (javascript/typescript) functions and as we’ll see later, everything is just a function, you’ll always start with a function. And be careful here, I’m explicit about naming here, don’t confuse functions with any upcoming terms (e.g. syscalls, commands)!
For your own sake, I would avoid doing any of the following.
# PLEASE DON'T DO THIS
# ...
functions:
# This should align with the function name
sayAWarmGreetingToTheUser:
path: main.ts:sayHi
sayBye:
# I would keep all functions in a single (or at least few selected) typescript file(s).
path: some/random/file/somewhere/lost/in/your/repo.ts:sayBye
Writing some code
To avoid testing your patience any further, we will write some code now.
import { editor, system } from "@silverbulletmd/silverbullet/syscalls";
export async function sayHi(): Promise<void> {
const version = await system.getVersion();
await editor.flashNotification(`Hiiii! You are running version ${version} of Silverbullet!`);
}
I think it’s very readable, but we’ll get to the in’s and out’s of what these functions are in a second, let’s first try the code. To do that, we’ll need to build and load the code.
# If you haven't already
npm install
# This uses the build command from the `package.json`
# I generally add a cp command to directly copy the file to the space folder of my running Silverbullet instance
npm run build && cp mycoolplug.plug.js WHEREEVER-YOUR-SB-IS/silverbullet/website
Now let’s reload SB and see if our plug is loaded. Generally it should autoload, if not, do a Plugs: Reload and/or refresh the page a couple of times.
There isn’t a good way to know if it’s loaded (maybe in the future), sometimes logs in the browser console can indicate that, but I wouldn’t trust it.
Running some code
We can now invoke the function we just wrote by using system.invokeFunction inside of Space Lua, so let’s try this. The easiest way to execute Space Lua on demand is using an expression.
${system.invokeFunction("mycoolplug.sayHi")}
...
${system.invokeFunction("mycoolplug.sayBye", "Bob")}
Now if you let the expression render, it will call the function and run our code. That’s a big milestone! Just one more thing, before we get to doing some actually useful stuff.
Syscalls
This is an important concept and if you used Silverbullet for a while you have probably heard about it and most definitely interacted with it.
All those functions you constantly use to do anything, e.g. the ones we used above system.getVersion and editor.flashNotification, are syscalls.
A few important points:
- They are (/should be) grouped into namespaces, in this case
systemandeditorrespectively, which we imported in our typescript file, and by which the documentation is grouped. Your own syscalls (yes you can create those), will generally be namespaced under you plugs name, e.g.mycoolplug.sayHi. - Syscalls are always async (at least for the caller)! We will always have to await here even though it may not make sense. (This stems from the fact, that Silverbullet uses messages to talk to our WebWorker, which are asynchronous by nature). This is a big difference from Space Lua.
- Syscalls are not typed. While it may seem like they are, these are just thin, typed wrappers for the most essential syscalls.
- “Why do syscalls exist? We already have functions?” Syscalls are (generally) functions. Furthermore, syscalls are for functions, which you want users to be able to call from Space Lua or other plugs, i.e. in code. Functions are way more general, you may want to use those for other things.
Now that we have all the most basic things out of the way, I’ll start with different sections on specific things, that you don’t necessarily need to read in order.
Your own syscalls
To create your own syscalls, just annotate a function inside your manifest.
# ...
functions:
sayHi:
path: main.ts:sayHi
# It's good practice to use your plug name as a namespace
syscall: "mycoolplug.sayHi"
Now you can call your syscall inside of Space Lua.
${mymycoolplug.sayHi()}
To call them inside a plug, do the following.
import { syscall } from "@silverbulletmd/silverbullet/syscalls";
// ...
syscall("mymycoolplug.sayHi")
// ...
Creating custom widgets
Widgets in space lua are just objects of a specific form
{
_isWidget: true,
html: "<h1>This is a header<h1>"
}
// or
{
_isWidget: true,
markdown: "This is *markdown*"
}
You can easily return these from functions and if you make these functions syscalls, they are very easy to call from Space Lua.
(To be very exact you can also pass DOM nodes, but that’s not practically possible, because you don’t have access to the DOM Tree and you also cannot serialize DOM nodes)
Commands
Commands are, well, commands, e.g. System: Reload, Page: Delete, etc. To create a command, just annotate the function.
# ...
functions:
sayHi:
path: main.ts:sayHi
command:
# It's good practice to use your plug as a namespace
name: "MyCoolPlug: Say Hi"
## OPTIONAL
# Puts the command higher in the list
priority: 10
# Keybind for Windows/Linux
key: "Ctrl-x"
# Keybind for MacOS
mac: "Cmd-x"
# Hide this command from the list
hide: true
# Can be "rw" or "ro", if it's "rw", the command is only available in read/write mode, not in read only mode
requireMode: "rw"
# Can be either "any", "page", "notpage" or any string. Specifies for which editor this command is visible, when working with document editors.
requireEditor: ""
# Whether to disable this keybind if vim mode is enabled
disableInVim: false
Events
You can dispatch and listen to events. This is as easy as annotating a function
# ...
functions:
sayHi:
path: main.ts:sayHi
events:
- "editor:init"
The list of available events can be found here.
Dispatching events is done via the event.dispatch syscall (for some reason the typescript wrapper is called events.dispatchEvent).
One very useful thing is that you can return data from event listeners, with the “fastest” result returned first from the dispatch syscall.