Janet Scripting in Bunny

This document describes the Janet scripting surface available in Bunny today.

Getting Started

Bunny loads Janet in two phases:

  1. Global bootstrap
    • ~/Library/Application Support/Bunny/init.janet
    • ~/Library/Application Support/Bunny/lib/
  2. Vault bootstrap
    • <vault>/.vault/init.janet
    • <vault>/.vault/extensions/

Only the two init.janet files are loaded automatically. The lib/ and .vault/extensions/ directories are import roots, not auto-discovery folders.

Minimal global setup

Create ~/Library/Application Support/Bunny/init.janet:

(print "Bunny global init loaded")

(command/register "notes.hello" "Notes: Hello"
  (fn []
    (print "hello from Bunny")))

Packages placed under ~/Library/Application Support/Bunny/lib/<name>/init.janet can be imported with @bunny-global-lib:

(import @bunny-global-lib/mytools)

Minimal vault setup

Create <vault>/.vault/init.janet:

(import @bunny-ext/todo)

(hook/add "document-did-save"
  (fn [doc-id]
    (print "saved" doc-id)))

Vault scripts are trust-gated. If Bunny detects .vault/init.janet in a vault that is not trusted yet, it opens the vault without running that file and shows the trust prompt. Trust is stored app-wide and is currently path-based.

Packages placed under <vault>/.vault/extensions/<name>/init.janet can be imported with @bunny-ext/<name>.

Exact file loading

Bunny also exposes:

These execute files directly, resolve relative paths against the calling file, and load definitions into the caller’s environment.

(bunny/load "extra/setup.janet")
(bunny/load-if-exists "local-overrides.janet")

Use import for Janet modules and bunny/load when you want literal file-execution semantics.

Janet’s normal module cache still applies to import. If you need a fresh module reload, use Janet’s standard :fresh true import option.

API Rules

Naming

Timing

Freshness

Table keys

How The Runtime Works

The scripting runtime lives on a dedicated Janet host thread, not on the main UI thread.

The practical model is:

  1. Bunny boots a Janet VM on the host thread.
  2. Core bindings are registered.
  3. Global init.janet runs.
  4. When a vault opens, vault bindings are registered.
  5. If trusted, vault .vault/init.janet runs.
  6. On vault switch, Bunny resets the VM, replays global bootstrap, then replays vault bootstrap.

Janet’s own event loop, timers, fibers, and networking work without blocking the app UI:

(def ch (ev/chan 1))
(ev/go (fn [] (ev/give ch :ok)))
(ev/take ch)

Dynamic roots

Bunny sets these Janet dynamic bindings:

They back the @bunny-global-lib/... and @bunny-ext/... import roots and are also visible via (dyn ...) or bunny/diagnostics.

Execution lifecycle

The scripting surface is backed by a ScriptingContext snapshot model:

  1. Refresh index/document snapshot before eval.
  2. Evaluate Janet — reads come from the snapshot; writes are queued.
  3. Apply queued document writes after eval via drainWrites.

This means:

Common Patterns

Register a command

Commands are parameterless. Read any needed context from Bunny bindings.

(command/register "todo.uppercase" "Todo: Uppercase Current Note"
  (fn []
    (when-let [doc (doc/current)]
      (when-let [content (doc/content doc)]
        (doc/set-content! doc (string/ascii-upper content))))))

React to hooks

(hook/add "document-did-save"
  (fn [doc-id]
    (print "saved" doc-id)))

Define a mode from frontmatter

If a note has:

---
mode: todo
---

then Bunny will activate the matching Janet mode when that document opens:

(mode/define "todo"
  {:on-activate
   (fn [doc-id]
     (style/add-rule! "todo.done"
       {:pattern "- \\[x\\] .*"
        :color "#888888"
        :strikethrough true}))
   :on-deactivate
   (fn [doc-id]
     (style/remove-rule! "todo.done"))})

Customize the status bar

(statusbar/set-left! [:mode " | " :cursor])
(statusbar/set-right!
  [:dirty
   (fn [ctx]
     (string "Words: " (ctx :word-count)))])

Status-bar function items receive a table with:

Return a string to show text, or nil to hide the item.

API Reference

doc/*

Document IDs are vault-relative paths such as "notes/today.md".

Notes:

Notes:

tags/*

Notes:

command/*

Notes:

hook/*

Supported hook names:

Only those hook names are currently exposed to Janet.

mode/*

opts accepts a Janet table or struct with:

Mode behavior:

style/*

opts keys:

Style rules apply immediately and are UI/editor state, not deferred vault writes.

statusbar/*

items may be a Janet array or tuple containing:

get-left and get-right serialize function items as :fn, not as callable values.

cal/*

Accepted date formats:

Calendar table fields: "id", "title", "type", "color"

Event table fields: "id", "title", "start", "end", "all-day", "calendar", "calendar-title", "location", "notes"

Creating or deleting events throws if calendar access is not authorized.

bunny/*

Notes:

bunny/diagnostics includes:

Status sub-tables use keyword values such as :not-attempted, :missing, :loaded, :failed, :trusted, :untrusted, and :no-vault.

Current Caveats