# Writing an extension (/extensions)



An extension is a small Rust crate, compiled to a WebAssembly component and
shipped beside a `manifest.toml`. It can give Lumi:

* **Actions** — commands a person binds to a shortcut, a leader menu step, a
  double-tap or an Fn combination, like any built-in action.
* **Flow nodes** — steps that appear in the Flow Editor's palette and run on
  the items that reach them.
* **Settings** — a form Lumi draws for it under **Settings → Extensions**.
* **Windows** — dialogs of its own, drawn from HTML files in the package.

It runs in Lumi's sandbox and reaches **exactly what its manifest declares**.
That is enforced by Lumi on every call the extension makes, not by review: an
extension that never declared `network` cannot reach the network whatever its
code tries.

<Callout type="warn" title="The API is 0.2">
  An extension built against version 0.2 of the SDK installs on every Lumi
  that speaks 0.2. When a later Lumi moves the interface to a new version, it
  says so at install — rebuild against the new SDK, or update Lumi — rather
  than loading an extension it cannot run. Coming from 0.1? Point the
  dependency at 0.2, add `on_lifecycle` (returning `Ok(())` is enough), and
  rebuild — see [Lifecycle](/extensions/sdk#on_lifecycleevent).
</Callout>

## What you need [#what-you-need]

* A Rust toolchain with the WebAssembly component target:

  ```bash
  rustup target add wasm32-wasip2
  ```

* Lumi on the same Mac, to install and run what you build.

## A first extension [#a-first-extension]

<Steps>
  <Step>
    ### Make the crate [#make-the-crate]

    ```bash
    cargo new --lib hello-lumi
    cd hello-lumi
    ```

    An extension is a library with no `main` — Lumi calls into it. In
    `Cargo.toml`:

    ```toml
    [lib]
    crate-type = ["cdylib"]

    [dependencies]
    # Until the crate is on crates.io, it is taken from the store's repository.
    # The tag names the interface version, so an update to the SDK is a choice
    # you make.
    lumi-extension-api = { git = "https://github.com/thiennguyen93/lumi-store", tag = "api-v0.2.0" }
    serde_json = "1"
    ```
  </Step>

  <Step>
    ### Write the code [#write-the-code]

    `src/lib.rs` implements one trait and registers it:

    ```rust
    use lumi_extension_api as lumi;

    struct Hello;

    impl lumi::Guest for Hello {
        fn run_command(name: String, _params: String) -> Result<String, String> {
            match name.as_str() {
                "greet" => {
                    let found = lumi::selection()?;
                    lumi::alert(&format!("You selected {} characters", found.text.chars().count()))?;
                    Ok("greeted".to_string())
                }
                other => Err(format!("no {other} command")),
            }
        }

        fn run_node(name: String, _params: String, _items: String) -> Result<String, String> {
            Err(format!("no {name} node"))
        }

        fn run_ui(window: String, _request: String) -> Result<String, String> {
            Err(format!("no {window} window"))
        }

        fn on_lifecycle(_event: lumi::Lifecycle) -> Result<(), String> {
            Ok(())
        }
    }

    lumi::register!(Hello);
    ```
  </Step>

  <Step>
    ### Declare it [#declare-it]

    `manifest.toml` beside `Cargo.toml` says who the extension is, what it
    contributes, and what it may reach:

    ```toml
    [extension]
    id = "dev.you.hello"
    name = "Hello"
    version = "0.1.0"
    description = "Counts what you selected."
    author = "You"
    capabilities = ["accessibility"]

    [[command]]
    name = "greet"
    label = "Count the selection"
    ```

    `lumi::selection()` needs `accessibility`, so the manifest asks for it. Leave
    it out and the call is refused, by name, the first time it runs — see
    [Capabilities](/extensions/manifest#capabilities).
  </Step>

  <Step>
    ### Build and package [#build-and-package]

    ```bash
    cargo build --release --target wasm32-wasip2

    mkdir -p package
    cp manifest.toml package/
    cp target/wasm32-wasip2/release/hello_lumi.wasm package/extension.wasm
    tar -czf dev.you.hello.tar.gz -C package .
    ```

    A package is a gzipped tar holding `manifest.toml` and `extension.wasm`, and
    optionally `icon.svg` and a `ui/` directory — see
    [The package](/extensions/manifest#the-package).
  </Step>

  <Step>
    ### Install it [#install-it]

    In Lumi, open **Settings → Extensions** and drag the `.tar.gz` onto the
    list. Or press **Install from file** — the package icon at the end of the row
    above the list, beside **Store** — and pick it. The review sheet lists what the extension declares; it says
    **not signed** because only the store signs packages, which is the truth
    about a file you built yourself.

    Bind **Count the selection** to a shortcut like any other action, select
    some text anywhere, and press it.
  </Step>
</Steps>

## How a run works [#how-a-run-works]

Every call into an extension — a command, a flow node, a request from one of
its windows — is one **run**, and a run is bounded:

| Bound           | Limit                                              |
| --------------- | -------------------------------------------------- |
| Memory          | 64 MiB, across every memory your component defines |
| Wall-clock time | 15 seconds, waiting included — a `sleep` counts    |
| Calls into Lumi | 200 per run                                        |
| Answer size     | 4 MiB                                              |
| Runs at once    | 4 per extension                                    |

The first two stop the run where it stands: a run that asks for more memory,
or is still going at fifteen seconds, is ended and the person is told the
extension was stopped. Lumi itself carries on — that is the point of the
sandbox.

The next two are refusals rather than stops. Past the 200th call, every call
into Lumi answers an error instead of doing anything, and with `?` that ends
your run on the spot. A call refused for a missing capability still counts
toward the 200. An answer over 4 MiB is replaced with an error saying how
large it was.

The last one is not per run: a fifth run started while four of yours are
still going is refused straight away rather than queued, with a sentence
saying the extension is busy. A window that fires requests in a loop is the
usual way to meet it — send the next one when the last has answered.

A component may also define at most 4 memories, 16 tables and 32 instances.
Rust's `wasm32-wasip2` toolchain builds one memory, two tables and three
instances, so this only matters to a component built some other way.

A flow node's run is ended when the person presses **Stop** on the flow, not
only the steps after it.

A run starts from nothing each time: there is no state carried between runs
in memory. What must last goes in your [settings](/extensions/manifest#settings),
which Lumi stores for you.

## Where next [#where-next]

<Cards>
  <Card title="The manifest" href="/extensions/manifest" description="Every table and key, capabilities, and what goes in a package." />

  <Card title="SDK reference" href="/extensions/sdk" description="Every function the SDK offers, and what each one costs." />

  <Card title="Windows" href="/extensions/windows" description="Dialogs of your own, and the bridge back to your code." />

  <Card title="Publishing" href="/extensions/publishing" description="Getting an extension into the store." />
</Cards>
