Scripts

A plugin with main = "main.rn" in its manifest runs that script, written in Rune. notesy compiles it when the plugin starts and runs it on a thread of its own, so the window never waits for it.

#More than one file

A script can be split up with Rune's modules: mod helpers; in main.rn takes in helpers.rn beside it (or helpers/mod.rn), and its public functions are then helpers::name(). Modules can have modules of their own the same way, a folder down each time.

rust// main.rn
use notesy::log;
mod helpers;

pub fn ready() {
    log::info(`twice 2 is ${helpers::double(2)}`);
}

// helpers.rn
pub fn double(n) { n * 2 }

Every file comes from the plugin's own folder, and none through a link: a script can't take in a file from anywhere else. Packing takes every file in the folder, so the modules go with it.

#What a script can reach

Rune itself has no files, network or processes. notesy adds its own modules, under notesy::, and only the ones the plugin's permissions cover: a script that uses a module it wasn't given doesn't compile.

rustuse notesy::{events, log, settings, store};
Module Needs Page
notesy::log nothing notesy::log
notesy::events per event notesy::events
notesy::store nothing notesy::store
notesy::settings nothing notesy::settings
notesy::secrets nothing notesy::secrets
notesy::json nothing notesy::json
notesy::toml nothing notesy::toml
notesy::yaml nothing notesy::yaml
notesy::files nothing (its own folder only) notesy::files
notesy::icons, notesy::canvas nothing (its [[icons]] without a file) notesy::icons
notesy::Event nothing: what events::on takes notesy::events
notesy::{Icon, Color, Tone, Side, Method, Command, Zone} nothing: notesy's names, as enums Names: Icon, Color, Tone, Side, Method, Command, Zone, Event
notesy::view nothing notesy::view
notesy::commands commands notesy::commands
notesy::views ui notesy::views
notesy::sections ui notesy::sections
notesy::status ui notesy::status
notesy::tools ui notesy::tools
notesy::zones ui notesy::zones
notesy::notices notify notesy::notices
notesy::clipboard clipboard.write to copy, clipboard.read to paste notesy::clipboard
notesy::sound sound (and [[sounds]] for files of its own) notesy::sound
notesy::links links.open to open pages; [net] to name links to its sites notesy::links
notesy::notes notes.read to read, notes.write to change notesy::notes
notesy::editor editor notesy::editor
notesy::net net (hosts in [net]) notesy::net
notesy::ipc ipc (sockets in [ipc]) notesy::ipc
notesy::accounts accounts (and [[accounts]]) notesy::accounts
notesy::blocks markdown (languages in [contributes] blocks) notesy::blocks

How the runtime was built, and why: plan.md.

#Its lifecycle

A script may define any of these; notesy calls each one it defines, in this order over a plugin's life:

Function When
pub fn load() It's loaded, before anything else reaches it: set yourself up here.
pub fn updated(from, to) It runs another version than last time (from and to are version strings): bring your store along.
pub fn ready() notesy is up, with a vault open and its tabs back: start what needs the app or the notes. A plugin turned on later hears this right after load.
pub fn setting_changed(key) The user changed one of its settings.
pub fn held_back(permissions) The vault that's open keeps some of what it was granted from it: permissions is a list of ids (notes.read, editor), empty when the vault keeps nothing. Told when it starts and again whenever a vault is opened.
pub fn unloading() It's being turned off, or notesy is quitting: save and let go, quickly.

A vault kept in one encrypted file holds plugins off its notes unless it's been asked about them. Your plugin still runs, draws and adds its commands; what it can't do is read or change what's in the vault, or hear the events about it. held_back is how you find out, so you can say so in your panel rather than appearing to work and do nothing. The user can let you in by name from your plugin's page in Settings.

Events it subscribed to with events::on arrive in between, one at a time, in the order they happened.

#Limits

Every call into a script (a lifecycle function, an event handler) runs within limits:

Limit Default When it's reached
Instructions per call 1,000,000 the call is halted: "it ran too long"
Instructions per view 5,000,000 the view is halted
Memory, across its calls 64 MiB the allocation fails, and the call with it (what earlier calls kept counts too)
Failures a minute 3 the plugin is stopped

Values going between a script and notesy (what it keeps, shares, sends as JSON, or builds a view from) nest at most 64 deep: a list or object nested deeper is an error where it's handed over.

A failure is a call that errors (a panic, a ? on an error, a wrong type) or is halted. Each goes to the plugin's log on the Plugins page. The third in a minute stops the plugin, with a notice saying which and why; it stays stopped until notesy starts again or it's turned off and on.

Waiting costs nothing: a script that has nothing to do isn't running.

#Errors

If the script doesn't compile, the plugin doesn't start, and its log shows what the compiler said, with the file and line:

error: Expected expression but got `;`
  ┌─ main.rn:1:26
  │
1 │ pub fn ready() { let x = ; }
  │                          ^

Errors while it runs show the function or event they happened in, like note.saved: there's no event “note.eaten”.

Warnings go to its log as it starts, and notesy plugin check prints them: the compiler's (a template with nothing in it), and what notesy knows Rune runs wrong (the trap below), each with its file and line.

#Values

What a script keeps and is given are plain values: true and false, integers, floats, strings, vectors and objects (#{ key: value }) of those. An event is an object with its name and its fields (notesy::events). Paths are always relative to the vault (Daily/2026-09-12.md), never the machine's.

#A trap in Rune 0.14

In a match on text or a number, an arm whose block ends in a variable of its own gives nothing, not the variable:

rustuse notesy::view;

fn part(kind) {
    match kind {
        // Comes out as nothing: a view with it says “item 2: nothing can't be kept”.
        "tags" => { let chips = view::chips(["#a"]); chips.target = "tag"; chips },
        _ => view::note(kind),
    }
}

Build such a part in a function of its own ("tags" => tag_chips()), or end the block in a call (view::chips(…)). A match on an Option (Some(x) => { … }) is fine.

#More of Rune 0.14's ways

  • An arm matches one pattern: "a" | "b" => … doesn't compile. Give each its own arm.
  • len(), a note's words() and other counts are unsigned. A sum that goes below nothing (a.len() - b.len() with b the longer) fails as it runs, not as it compiles: compare first, or keep counts in integers of the script's own.
  • Taking a tuple apart (let (a, b) = pair;, for (k, v) in object) is warned about as a pattern that might panic. Read its parts by place (pair.0) or by key (for k in object.keys()), or keep objects (#{ a, b }) instead.
  • In a closure, if let Some(x) = x { … } with x the closure's own argument doesn't compile ("Missing variable"). Give the inner one another name.
  • Some words are Rune's own and can't be a function's name: select, is, not, yield and the like (hence editor::set_selection).
  • return can't be followed by a template string: return `a${b}`; doesn't compile ("Expected a closing brace"). Let it be the block's last value, or write return "a" + b;.
  • An object's insert(key, value) takes the text in key away from the variable it was in: reading that variable after fails as it runs ("Cannot read, value is M-000000"). Insert a copy, key.clone(), when the key is wanted again.
  • A string's split can be kept, in an object, and read on with next() in a later call: a long text worked through a step at a time without being cut into a list first (see example.import's enml.rn).

Every page