notesy::events

What happens in notesy, as it happens. A script subscribes to the events it wants, one by one or a whole kind at once, and its handler is called for each, one at a time, in order.

rustuse notesy::{events, log, Event};

pub fn ready() {
    events::on(Event::NoteSaved, |event| log::info(`saved ${event.path}`));
    events::on(Event::AnyNote, |event| log::info(event.name));
}
Function What it does
events::on(event, handler) Calls handler(event) for every event event covers: one of Event's below.
events::off(event) Forgets every handler added for it.

Event is notesy's list of what a script can hear, so a name that's wrong doesn't compile, and an editor that knows Rune can offer the rest. (A name as text, "note.saved" or "note.*", still works too, checked as the script runs.)

Hearing about notes takes the notes.read permission: every note and vault event, and Event::SecondInstance, need it. Subscribing to one without it doesn't stop the script; the refusal goes to its log and the handler never runs.

#Events

Every event reaches the handler as an object with its name (the text form) and the fields below. Paths are relative to the vault, with / between folders.

Event:: name Fields When Needs
AppReady app.ready notesy is up, with its plugins loaded
SecondInstance app.second_instance args (list of strings), cwd notesy was started again while running; that start's arguments and folder notes.read
VaultOpened vault.opened name_of_vault a vault was opened notes.read
NoteOpened note.opened path a note was opened in a tab notes.read
NoteClosed note.closed path a note's tab was closed notes.read
NoteChanged note.changed path its text changed; a few times a second at most while typing notes.read
NoteSaved note.saved path it was saved notes.read
NoteCreated note.created path a new note notes.read
NoteRenamed note.renamed from, to it was renamed or moved notes.read
NoteDeleted note.deleted path it was deleted notes.read
DrawingOpened drawing.opened path a drawing (notesy::drawings) was opened in a tab notes.read
DrawingSaved drawing.saved path a drawing's tab wrote it to its file notes.read
HiddenChanged hidden.changed path, hidden a note, file or folder was hidden from the tree, or shown again notes.read
ZoneChanged zone.changed zone a place this plugin opened for others (notesy::zones) gained or lost an entry: its name, <plugin>/<key> ui
ThemeChanged theme.changed id, dark (bool) the theme changed
CommandRan command.ran id a command ran: its id, like plugin.you.saves.sync
SidebarItem sidebar.item section, item an item in one of its sidebar sections was clicked: section is the key it gave sections::add, item the item's id. Only the plugin whose section it is hears it.
KeyDown key.down repeat (bool: a key held down, repeating), source (a KeySource: where it went), place (the same, in text); with keys.which, key, shift, ctrl, alt, command, which, taken (which key) a key went down, anywhere in notesy but its settings and plugins' dialogs: that one did, never which. Shift, Ctrl, Alt or Cmd going down alone isn't one: it's held for the key after it, which says so keys
KeyUp key.up source, place; with keys.which, key and the held modifiers a key came back up keys
SoundWants sound.wants id, ms a stream it feeds (notesy::sound) has ms left of what it was given: time to push more. Only the plugin whose sound it is hears it. sound
SoundEnded sound.ended id one of its sounds finished, or was stopped. Only the plugin whose sound it is hears it. sound

And each kind at once:

Event:: Covers
AnyApp app.ready and app.second_instance (app.*)
AnyVault vault.opened (vault.*)
AnyNote every note. event (note.*)
AnyDrawing drawing.opened and drawing.saved (drawing.*)
AnyKey key.down and key.up (key.*)
AnySound sound.wants and sound.ended (sound.*)

#Keys

A plugin with the keys permission hears keys go down and up, never which ones: enough for a cat that bangs a bongo as you type, and nothing to spell out what's typed. Shortcuts count too; the settings and plugins' dialogs, where secrets are typed, don't.

rustuse notesy::{events, status, Event};

pub fn ready() {
    let cat = status::add("bongo");
    cat.set("(=^・ω・^=)");
    events::on(Event::KeyDown, |key| if !key.repeat { cat.set("(=^・ω・^=)ノ") });
    events::on(Event::KeyUp, |key| cat.set("(=^・ω・^=)"));
}

Each says where it went, its source, a place rather than a key: a plugin that should only answer typing in a note matches on it, and one that answers every key, like the cat above, needn't. Where a key went says no more about which it was.

KeySource:: Where
Editor a note's editor, being written in
Board a board: one of its text cards being written in, or the board itself
Palette the palette or the quick switcher
Sidebar the sidebar's tree of notes
Field any other field of notesy's: a property, the find bar, a name being changed
Window nothing that takes typing: a shortcut, pressed anywhere
Plugin one of a plugin's views (a tab, a panel, a sidebar section's), yours or another's: a field in it has the keyboard, or it was pressed in last and nothing else has it (a game's panel, a picker)

place says the same in text: notesy's own by its name (editor, palette), and a plugin's view as <plugin id>/<view>, the way zones are named (acme.tasks/search). Every view a plugin adds is a place of its own, with nothing to declare: a field in it is where keys typed there come from, and so is the view itself once it's clicked in, until something else is. More places may come; a match on them keeps a _ for those.

rustuse notesy::{events, log, Event, KeySource};

pub fn ready() {
    events::on(Event::KeyDown, |key| {
        match key.source {
            KeySource::Plugin => log::info(`typing in ${key.place}`),
            _ => {}
        }
    });
}
rustuse notesy::{events, sound, Event, KeySource};

pub fn ready() {
    events::on(Event::KeyDown, |key| {
        match key.source {
            KeySource::Editor => sound::tone(880.0, 30),
            KeySource::Board => sound::tone(660.0, 30),
            _ => {}
        }
    });
}

#Which key

With keys.which as well as keys, a plugin hears which key it was, letters and all, and which modifiers are held: for a vim-style plugin's motions, shortcuts of its own, a game. which is the key, a Key to match on: Key::A, Key::Num7, Key::Enter, Key::Escape, Key::ArrowUp, Key::F5, Key::Space, Key::Minus, Key::OpenBracket; and which.is_letter(), which.is_digit() and which.is_modifier() (Shift, Ctrl, Alt or Cmd itself, as a key going up can be). key is its name, lowercase, its words joined by -: a, 7, enter, escape, arrow-up, f5, space, minus, open-bracket. It's the key, not what it typed: Shift and a is a with shift. shift, ctrl, alt and command (Cmd on a Mac, Ctrl elsewhere) say what's held. taken says whether it was taken from the editor (editor::take_keys) rather than reaching the note. A plugin without keys.which gets none of them.

Only keys typed in notesy's window: nothing typed in another program ever reaches a plugin. And no plugin hears a key at all, not even that one went, while a secret's being typed: a passphrase, a password, or a field a plugin's view says is secret (view::input(hint) with secret: true).

rustuse notesy::{events, log, Event, Key};

pub fn ready() {
    events::on(Event::KeyDown, |key| {
        match key.which {
            Key::S if key.command => log::info("saved, by a plugin of its own"),
            Key::Escape => log::info("let go"),
            _ => {}
        }
    });
}

Each event object also has its kind, an Event, to match on:

rustuse notesy::{events, log, Event};

pub fn ready() {
    events::on(Event::AnyNote, |event| {
        match event.kind {
            Event::NoteSaved => log::info(`saved ${event.path}`),
            Event::NoteDeleted => log::info(`gone: ${event.path}`),
            _ => {}
        }
    });
}

#Its own events

A big plugin's parts can talk without calling each other: a channel of its own carries events from one part to every part listening. Needs nothing.

rustuse notesy::{events, log};

pub fn ready() {
    let sync = events::channel("sync");
    sync.on(|done| log::info(`${done.notes} notes synced`));
    sync.emit(#{ notes: 12 });
}
Function What it does
events::channel(name) One of its own channels, by a name of its own (lowercase letters, digits, -, _, .).
channel.on(handler) handler(value) for everything emitted on it.
channel.emit(value) Sends value, plain data (numbers, text, lists and objects of those), to its handlers: after what's running now, in the order it was sent.
channel.off() Forgets every handler on it.

A channel is its plugin's alone: no other plugin hears it. At most 1,024 events wait on its channels at once; past that, emit is an error.

#Other plugins

Plugins talk to each other only when both say so. One shares, under names of its own, with the plugins.share permission; one that wants to hear it names it in its plugin.toml ([plugins] hears = ["ana.sync"], see plugin.toml), which asks the user for that too. When they approve it they see what each plugin it hears can do, and a plugin that hears one that's since been allowed more stops until they look again.

rustuse notesy::events;

// In Sync, which has plugins.share.
pub fn ready() {
    let synced = events::share("synced");
    synced.emit(#{ notes: 12, at: "2026-09-12" });
}
rustuse notesy::{events, log};

// In Board, whose plugin.toml has [plugins] hears = ["ana.sync"].
pub fn ready() {
    events::from("ana.sync", "synced").on(|done| log::info(`Sync did ${done.notes} notes`));
}
Function What it does
events::share(name) What it shares under name (lowercase letters, digits, -, _, .). Needs plugins.share.
share.emit(value) Sends value, plain data, to every running plugin that hears it, after what's running there now. At most 20 a second, of 64 KB each; past that, emit is an error.
events::from(plugin, name) What plugin shares under name: one its [plugins] hears names.
heard.on(handler) handler(value) for everything it shares under that name.
heard.off() Forgets every handler on it.

A plugin that isn't running hears nothing, and nothing's kept for one that starts later.

#Ordering and dropping

Events wait in a queue for each plugin: up to 512. A plugin that falls that far behind loses the oldest (its log says how many), never a lifecycle call (Scripts).

Every page