# SDK reference (/extensions/sdk)



`lumi-extension-api` is the crate your extension depends on. Import it under a
short name and everything below is one path away:

```rust
use lumi_extension_api as lumi;
```

## What each call costs [#what-each-call-costs]

Every function below exists whether or not your manifest declares what it
needs. An undeclared one is refused **when it is called**, with an error that
names the capability — so the person who sees it is you, with the manifest
open.

| Costs                          | Functions                                                                      |
| ------------------------------ | ------------------------------------------------------------------------------ |
| Nothing at all                 | `about`, `license`                                                             |
| One call from the run's budget | `alert`, `settings`, `setting`, `profiles`, `open_window`, `hyper_key_enabled` |
| `accessibility`                | `selection` (the ⌘C fallback needs `clipboard` too)                            |
| `clipboard`                    | `clipboard_text`, `set_clipboard_text`                                         |
| `applications`                 | `open_url` (a web address needs `network` too)                                 |
| `network`                      | `fetch`                                                                        |
| `config`                       | everything in `lumi::config`                                                   |

Every call into Lumi except `about` and `license` spends from one allowance of
200 per run, so a loop of alerts runs dry long before it can flood the
screen. The call is spent before its capability is checked, so a refused call
counts too. Past the allowance every call answers an error — and `settings()`,
which has no error to give, answers an empty object, so read your settings
early in a run rather than after a long loop.

## Your extension [#your-extension]

### `Guest` and `register!` [#guest-and-register]

The trait you implement and the macro that wires it up. Call `register!` once,
at the crate root, on the type that implements `Guest`.

```rust
impl lumi::Guest for MyExtension {
    fn run_command(name: String, params: String) -> Result<String, String>;
    fn run_node(name: String, params: String, items: String) -> Result<String, String>;
    fn run_ui(window: String, request: String) -> Result<String, String>;
    fn on_lifecycle(event: lumi::Lifecycle) -> Result<(), String>;
}

lumi::register!(MyExtension);
```

| Method         | Called for                                             | Receives                                                                           | Answers                                                                                                                    |
| -------------- | ------------------------------------------------------ | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `run_command`  | A bound `[[command]]` pressed                          | The command's `name`; its params as a JSON object, variables already rendered      | Any string, which is not shown — call `alert` to tell the person something — or an error Lumi reports like a failed action |
| `run_node`     | A `[[node]]` reached in a flow                         | The node's `name`, params as a JSON object, and the incoming items as a JSON array | The outgoing items as a JSON array — empty ends the branch                                                                 |
| `run_ui`       | A request from one of your windows                     | The window's `name`, and whatever the page posted                                  | Whatever the page should get back — see [Windows](/extensions/windows#the-bridge)                                          |
| `on_lifecycle` | Your extension being installed, updated or uninstalled | A [`Lifecycle`](#on_lifecycleevent)                                                | `Ok(())`, or an error that is only written to Lumi's log                                                                   |

Answer the names you do not know with an error rather than a panic. A panic is
reported to the person as the extension having crashed on an error of its
own, with no more detail than that; the backtrace goes to Lumi's log. An
error's text is cut to 64 KiB.

### `on_lifecycle(event)` [#on_lifecycleevent]

```rust
pub enum Lifecycle {
    Installed,
    Updated(String), // the version that was replaced
    Uninstalling,
}
```

Three moments in your extension's life, each told to it once:

| Event           | When                                                                                                                                       | Typical use                         |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------- |
| `Installed`     | Right after a first install — after your [installer](/extensions/windows#an-installer-of-your-own), if you ship one, has saved its answers | Open a Welcome window               |
| `Updated(from)` | Right after a new version replaces an installed one                                                                                        | Say what is new since `from`        |
| `Uninstalling`  | Just before your files are removed                                                                                                         | Open a feedback page in the browser |

Nothing to do is the ordinary answer:

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

The rules, all of which follow from nobody having pressed anything:

* **Only while switched on.** A switched-off extension is not told it was
  updated or uninstalled — off means none of its code runs.
* **Nobody sees the answer.** An error is written to Lumi's log, never shown.
* **It cannot stop anything.** An `Uninstalling` that fails or runs out of
  time is logged, and the uninstall goes ahead.
* **`Installed` and `Updated` run after the install has finished**, on their
  own, with the same allowance and deadline as a command.
* **It takes one of your four runs at once.** A hook that arrives while four
  runs of yours are going is skipped and logged — `Uninstalling` included.
* **`Uninstalling` is short and has no windows.** The person is waiting on the
  Uninstall button, so it has five seconds, and your `ui/` files are about to
  be deleted, so `open_window` is refused. Open a web page with `open_url`
  instead — which needs `applications`, plus `network` for a web address.

```rust
fn on_lifecycle(event: lumi::Lifecycle) -> Result<(), String> {
    match event {
        lumi::Lifecycle::Installed => lumi::open_window("welcome"),
        lumi::Lifecycle::Updated(from) => lumi::alert(&format!("Updated from {from}")),
        lumi::Lifecycle::Uninstalling => lumi::open_url("https://example.dev/why-uninstall"),
    }
}
```

A Welcome window is an ordinary [`[[window]]`](/extensions/manifest#window):
declare it, and open it from `Installed`.

### `settings()` and `setting(name)` [#settings-and-settingname]

```rust
pub fn settings() -> serde_json::Value
pub fn setting(name: &str) -> Option<String>
```

Your `[[settings]]` values as the person saved them, one key per field, with
defaults filled in for anything untouched. A field declared
`scope = "profile"` holds the live profile's value. `setting` is the
shortcut for one string field.

Settings are fixed for the length of a run: a save in Lumi while your code is
running reaches the next run.

### `open_window(name)` [#open_windowname]

```rust
pub fn open_window(name: &str) -> Result<(), String>
```

Opens — or brings forward — one of the windows your manifest declares. A name
the manifest does not declare is refused. See [Windows](/extensions/windows).

It is refused from `run_node` too: a flow runs in the background, and a
dialog out of a flow nobody just pressed is not something a flow may do yet.
And it is refused from `Uninstalling`, since your `ui/` files are about to
go.

## Talking to the person [#talking-to-the-person]

### `alert(text)` [#alerttext]

```rust
pub fn alert(text: &str) -> Result<(), String>
```

Draws a line of text over whatever is in front, in the alert style the person
chose in Lumi. It takes no focus and fades on its own — the way an extension
answers somebody who pressed a key with no Lumi window open. Text over 64 KiB
is refused with an error rather than cut.

## The desk [#the-desk]

### `selection()` [#selection]

```rust
pub fn selection() -> Result<Found, String>

pub struct Found {
    pub text: String,
    pub how: String, // "accessibility", "copy" or "nothing"
}
```

The text selected in the frontmost application. Needs `accessibility`.

Lumi asks the application through Accessibility first. If you also declared
`clipboard`, an application that exposes nothing is asked again with a ⌘C,
and the pasteboard is put back afterwards; `how` says which one answered.
Empty text with `how == "nothing"` is the ordinary miss — nothing selected —
not an error.

### `clipboard_text()` and `set_clipboard_text(text)` [#clipboard_text-and-set_clipboard_texttext]

```rust
pub fn clipboard_text() -> Result<Option<String>, String>
pub fn set_clipboard_text(text: &str) -> Result<(), String>
```

The pasteboard's plain text, or `None` when it holds no text — an image, a
file and an empty board are one answer. Writing replaces what was there, and
text over 4 MiB is refused. Both need `clipboard`.

### `open_url(url)` [#open_urlurl]

```rust
pub fn open_url(url: &str) -> Result<(), String>
```

Hands a URL to whichever application handles it. Web and mail addresses only:
`https://`, `http://` and `mailto:`, in any case. Needs `applications`, and a
web address needs `network` too.

## The network [#the-network]

### `fetch(request)` [#fetchrequest]

```rust
pub fn fetch(request: &Request) -> Result<Response, String>

pub struct Request {
    pub method: String,               // empty means GET
    pub url: String,
    pub headers: Vec<(String, String)>,
    pub body: Option<String>,
}

pub struct Response {
    pub status: u16,
    pub ok: bool,       // status in 200..300
    pub url: String,    // the final URL, after redirects
    pub headers: Vec<(String, String)>,
    pub body: String,
}
```

One HTTP request, with a ten-second timeout. Needs `network`. The method is
upper-cased, and empty means `GET`.

* **Redirects** are followed, five at most.
* **The body** of a response is at most 5 MiB; a larger one is an error, not a
  truncated body.
* **Local addresses** — loopback, the local network, link-local — are refused
  unless the person has switched on **Let flows reach this network** under
  Settings → General → Advanced, whatever the request asks for. The address is
  resolved once and judged, and every redirect is judged again, so a
  redirect cannot reach what the first address could not. The switch is
  named for flows because it is the same one: an extension's `fetch` and a
  flow's script reach the network through the same door.

## Profiles [#profiles]

### `profiles()` [#profiles-1]

```rust
pub fn profiles() -> Result<Profiles, String>

pub struct Profiles {
    pub active: String,          // the live profile's id, always one of `profiles`
    pub profiles: Vec<Profile>,  // never empty
}

pub struct Profile {
    pub id: String,   // stable across renames — key by this
    pub name: String, // what the person calls it — show this
}

impl Profiles {
    pub fn active_profile(&self) -> Option<&Profile>;
}
```

A person's [profiles](/features/profiles) are whole sets of shortcuts,
snippets and flows, one live at a time. Your extension is installed once for
the whole Mac, so this is how it tells the sets apart.

One call answers both halves, so the list and the live id are always from the
same moment. Ask whenever you need it rather than holding on to an answer:
the person can switch at any time and nothing tells your extension that they
did. To keep a value per profile, declare the setting
[`scope = "profile"`](/extensions/manifest#one-value-per-profile).

```rust
let book = lumi::profiles()?;
let here = book.active_profile().map(|p| p.name.as_str()).unwrap_or("?");
lumi::alert(&format!("{here} — one of {} profiles", book.profiles.len()))?;
```

## Lumi itself [#lumi-itself]

### `about()` [#about]

```rust
pub fn about() -> About

pub struct About {
    pub version: String,  // "1.22.0"
    pub homepage: String, // "https://lumikeys.app"
    pub author: String,
}
```

Which Lumi is running. Check `version` before relying on something a newer
Lumi added — compare the numbers rather than matching the string.

### `license()` [#license]

```rust
pub fn license() -> Edition

pub enum Edition {
    Free,     // no licence on this Mac
    Pro,      // Pro is on
    Inactive, // a licence that is not granting Pro right now
}
```

Whether this Mac has Lumi Pro. The edition is the whole of what an extension
learns about the licence — never the key, the seat, or who bought it. Ask each
time: the person can buy, renew or lapse while your extension is installed.

<Callout title="Word Pro, never Free">
  Lumi never labels a free copy — a badge is a reward, and a label on a free
  copy is an advertisement. Anything your extension draws inside Lumi should
  follow the same rule: say "Pro" when it is, and nothing otherwise.
</Callout>

### `hyper_key_enabled()` [#hyper_key_enabled]

```rust
pub fn hyper_key_enabled() -> Result<bool, String>
```

Whether Lumi's Hyper key is switched on in the live profile. While it is,
Caps Lock belongs to Lumi — remapped and never delivered as itself — so a page
that listens for keys should not wait for one. Needs no capability; it spends
one call from the budget.

It is the switch alone. The modifiers the Hyper key adds and what tapping it
does are in [`config::hyper_key()`](#reading-the-persons-setup), which needs
`config`. Ask when you need it: the person can flip it, or switch profile, at
any time.

Added in Lumi 1.23.0. An extension that calls it should say
`min-lumi-version = "1.23.0"` in its manifest: on an older Lumi it cannot
start.

## Reading the person's setup [#reading-the-persons-setup]

Everything under `lumi::config` needs the `config` capability, reads the live
profile, and is **read-only**.

```rust
pub mod config {
    pub fn settings() -> Result<serde_json::Value, String>;
    pub fn shortcuts() -> Result<serde_json::Value, String>;
    pub fn snippets() -> Result<serde_json::Value, String>;
    pub fn hyper_key() -> Result<serde_json::Value, String>;
    pub fn double_tap() -> Result<serde_json::Value, String>;
    pub fn fn_key() -> Result<serde_json::Value, String>;
}
```

| Function       | Answers                                                                                                                                                              |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `settings()`   | Lumi's general settings: `appearance`, `startAtLogin`, `showProfileInMenuBar`, `appShortcuts`, `alert`, `leader`, `arrange`                                          |
| `shortcuts()`  | `{ "enabled": …, "bindings": [...] }` — the Shortcuts pane's switch and every row: ordinary combinations, double-taps, Fn combinations, leader menus and their steps |
| `snippets()`   | Expansion's switch, its options, and every snippet under `items`                                                                                                     |
| `hyper_key()`  | Whether the Caps Lock Hyper key is on, the modifiers it holds, and what a tap on its own does                                                                        |
| `double_tap()` | Its switch and timing, plus the rows it arms under `bindings`                                                                                                        |
| `fn_key()`     | Its switch, plus the rows it arms under `bindings`                                                                                                                   |

```rust
let shortcuts = lumi::config::shortcuts()?;
let rows = shortcuts["bindings"].as_array().map(Vec::len).unwrap_or(0);
lumi::alert(&format!("{rows} shortcuts in this profile"))?;
```

**Another extension's inputs are not shown.** A row that runs a command of
some other extension arrives with its `action.params` left out: those are what
the person typed into that extension's form, and may be a token or an account
it asked for. You still see the row, which extension and which command it
runs. Rows that run your own commands keep their `params`.

**The shape is Lumi's own.** Each answer uses the same camelCase field names
and values Lumi saves the person's settings with, so a field you read today
keeps its name in the next Lumi. New fields may appear — read defensively,
never exhaustively. Rows written by a newer Lumi that this one cannot read are
left out rather than guessed at.

**It is content, and the review sheet says so.** Snippets are where people
keep addresses and canned replies. The person installing sees "reads your
shortcuts, snippets and Lumi settings" — beside "sends data over the network"
if you declared that too — so ask for `config` only when the extension really
reads it.

**There is no write.** To change what a keystroke does, offer a command or a
node and let the person bind it. Reading is for fitting around what is already
there: suggesting a combination nobody holds, or not doing work a snippet
already does.

**Ask when you need it.** The person can edit a row or switch profile at any
moment. While Lumi's own settings cannot be read, every call here is refused
with a sentence rather than answered with empty defaults that would look like
a person with nothing set up.
