# 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`, `set_setting`, `profiles`, `transforms::all`, `transforms::apply`, everything in `lumi::translation`, everything in `lumi::dictionary`, `open_window`, `open_window_on`, `open_window_at`, `close_window`, `window_state`, `preload_enabled`, `set_preload_enabled`, `set_tab_label`, `hide_window`, `show_window`, `show_window_on`, `show_window_at`, `post`, `set_material`, `set_position`, `set_anchor`, `set_theme`, `set_pinned`, `raise_window`, `set_size`, `set_titlebar_height`, `hyper_key_enabled`, everything in `lumi::storage`, everything in `lumi::folders` (for the folders your manifest declares), everything in `lumi::files`, everything in `lumi::owner`, `config::extension_shortcuts` and `config::set_extension_shortcut`, `ocr::languages`, and `ocr::read` of a picture in your storage. Some answer only a press — a command, a menu bar row or one of your windows: `open_settings`, `open_permissions`, `switch_off`, `set_setting`, `reveal`, `set_pinned`, `files::open` and `files::save`, `owner::authenticate`, `translation::prepare`, `dictionary::show`, and `folders::choose`, `write` and `reveal` |
| `accessibility`                          | `selection` (the ⌘C fallback needs `clipboard` too), `selection_editable`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `accessibility` and `input`              | `replace_selection` and `replace_selection_and_select` — and only from a command or one of your windows; a replacement too long to type is pasted only with `clipboard` too                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `clipboard`                              | `clipboard_text`, `set_clipboard_text`, `clipboard_image`, `read_clipboard`, `write_clipboard`, and `drag` and `drag_named` — those two only while the mouse button is down in one of your windows                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `clipboard`, `input` and `accessibility` | `paste` and `copy_selection` — and only from a command or one of your windows                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `clipboard-history`                      | hearing `on_event("clipboard", …)` and `on_event("clipboard-ocr", …)`, with `events = [...]` in the manifest                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `selection-watch`                        | `selection_watching`, `set_selection_watching`, `selection_watchers`, and hearing `on_event("selection", …)` and `on_event("selection-watching", …)`, with `events = [...]` in the manifest                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `applications`                           | `open_url` from `on_event`, a lifecycle hook or a flow node — nobody pressed anything (a web address needs `network` too). From a press it costs no capability                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `network`                                | `fetch`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `config`                                 | everything in `lumi::config`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `screen`                                 | everything in `lumi::screen`, `ocr::read` of the screen, `lumi::permissions` for Screen Recording (`ask`, `request` and `open_system_settings` only from a command or one of your windows), and hearing `on_event("screen-capture", …)` with `events = [...]` in the manifest                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `snippets`                               | everything in `lumi::snippets` (an expansion that reads the clipboard needs `clipboard` too)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `menu`                                   | everything in `lumi::menu` — your rows in Lumi's menu bar menu                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `menu-bar-item`                          | everything in `lumi::menu_bar_item` — an item of your own in the menu bar, beside Lumi's — and hearing `on_event("menu-bar-item-shown", …)` with `events = [...]` in the manifest (Lumi 1.61.0)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `ai`                                     | everything in `lumi::ai` — `complete`, `start` and `prepare` only from a command, a menu bar row or one of your windows (Lumi 1.52.0; `prepare` 1.64.0)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `speech`                                 | everything in `lumi::transcribe` — `prepare`, `prepare_for`, `complete`, `start`, `start_after` and `switch` only from a command, a menu bar row or one of your windows; hearing the microphone live costs `microphone` too, what the Mac plays `system-audio`, and a call both (Lumi 1.64.0)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `audio-levels`                           | a `transcribe::Request` with `levels`, or a `recordings::Request` with `levels` (Lumi 1.65.0): how loud what is heard is, for drawing it; declared beside `speech` or `keep-audio` (Lumi 1.64.0)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `voices`                                 | everything in `lumi::voices` — `remember` and `rename` only from a command, a menu bar row or one of your windows — and `Speakers::Named` in `lumi::transcribe`; declared beside `speech` (Lumi 1.64.0)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `keep-audio`                             | everything in `lumi::recordings` but `longest_minutes` and `set_longest_minutes`, which need only `microphone` or `system-audio` — `start` and `keep` only from a command, a menu bar row or one of your windows; `start` costs its source's word too. Declared beside `microphone` or `system-audio`, with no `speech` needed (Lumi 1.65.0)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `timers`                                 | everything in `lumi::timers` (Lumi 1.59.0) and `lumi::schedules` (Lumi 1.60.0), and hearing `on_event("timer", …)` and `on_event("schedule", …)` with `events = [...]` in the manifest                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `time-zone`                              | everything in `lumi::time_zone`, and hearing `on_event("time-zone", …)` with `events = [...]` in the manifest (Lumi 1.59.0)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `microphone-use`                         | everything in `lumi::microphone_use` but `sites`, and hearing `on_event("microphone-use", …)` with `events = [...]` in the manifest (Lumi 1.65.0)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `microphone-use` and `microphone-sites`  | `microphone_use::sites` — the site an app using a microphone shows, by host (Lumi 1.65.0)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `offer`                                  | everything in `lumi::offer` — from any run, an event's or a timer's included (Lumi 1.65.0)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `sound`                                  | everything in `lumi::sound` — from any run, an event's or a timer's included — and hearing `on_event("sound-ended", …)` with `events = [...]` in the manifest (Lumi 1.74.0)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |

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>;
    fn on_event(name: String, payload: String) -> 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                                                                   |
| `on_event`     | Something outside your extension that it asked to hear | The event's `name`, and a JSON payload                                             | `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`.

### `on_event(name, payload)` [#on_eventname-payload]

Something that happened outside your extension, which it asked to hear. The
payload is a JSON object with a `"v"` field for its own version; a new kind
of event is a new `name`, so this method is the last one the trait gains for
it. An extension that asks to hear nothing is never called, and answers
`Ok(())`:

```rust
fn on_event(_name: String, _payload: String) -> Result<(), String> {
    Ok(())
}
```

The events Lumi sends today:

| Name                  | Payload                                                                                                                                             | When                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `clipboard`           | `{"v":1, "at", "hash", "source":{"bundleId","name"}, "origin", "items":[[{"uti", "text" or "blob", "bytes", "path"?, "fileSize"?, "fileToken"?}]]}` | Each copy, less what password managers mark private. Images and large text arrive as blobs already in your [`storage`](#storage); a copied file's `public.file-url` carries `path`, where it is — Finder copies the unreadable `file:///.file/id=…` form — and `fileSize`, its size in bytes (Lumi 1.26+; a folder has no size, a file already gone neither); a PDF, picture, text or code file, sound or film also carries `fileToken`, which a page gives to [`GET /__lumi__/file/<token>`](/extensions/windows) to play or show it; `origin` is your id when the board was your own `write_clipboard` or `paste`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `clipboard-ocr`       | `{"v":1, "hash", "text"}`                                                                                                                           | A moment after a copy holding an image, with the text read in it; `hash` names that copy. Never empty.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `screen-capture`      | `{"v":1, "phase":"start" or "end", "region"}`                                                                                                       | Lumi taking a picture of the screen for itself — Copy Text from Screen, a flow reading the screen — once as it starts and once as it ends, however it ends. `region` is `"area"` or `"window"` when the person chooses on an overlay, which leaves time to step a window aside with [`hide_window`](#hide_windowname-and-show_windowname); `"frontWindow"` or `"display"` is taken the instant it is asked, which leaves none — a window that must never be in the picture says [`capturable = false`](/extensions/manifest). Lumi never waits on your answer. Needs the `screen` capability and Lumi 1.36.0.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `window-closed`       | `{"v":1, "window", "at"}`                                                                                                                           | One of your own windows leaving the screen for good: a [panel](/extensions/windows) put away — Esc, a click away, your `close_window`, a `paste` taking it down — or a declared window closed, by you or by the person. `window` is its manifest name, `at` the moment it closed in milliseconds since the Unix epoch — not when you hear it, which can be later. Once per close: a panel already away says nothing. Not for `hide_window`, whose window comes back with `show_window`, and not for a pinned panel losing the keyboard, which is still up; nor for the windows that close because your extension was switched off, uninstalled or updated. Lumi never waits on your answer. Needs no capability — it is only about your own window — and Lumi 1.37.0.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `preload-enabled`     | `{"v":1, "enabled", "at"}`                                                                                                                          | The person turning loading your panels ahead on or off with the **Preload** switch on your Permissions sheet — see [`preload_enabled()`](#preload_enabled-and-set_preload_enabledon). `enabled` is the switch as it now stands, `at` the moment it moved in milliseconds since the Unix epoch. Only when the person moves it there, never for your own `set_preload_enabled`; `preload_enabled()` answers the same question any time. Lumi never waits on your answer. Needs the `preload` capability — declared without it, the manifest is refused — and Lumi 1.45.0.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `selection`           | `{"v":1, "text", "by", "lines", "editable", "app":{"bundleId","name"}, "at", "clickInterval"}`                                                      | The person selecting text with the mouse in another app: `by` is `"drag"`, `"double-click"`, `"triple-click"` or `"shift-click"` — never a plain click, never with ⌘ or ⌃ held, never from the keyboard, which is `keyboard-selection`. `text` is read through Accessibility only, never by a ⌘C, and is not sent for a password field, Lumi's own windows, blank text or text over 16 KiB; a page's list numbers and bullets are left out, and since Lumi 1.59.0 a page nobody can edit reads as it shows — see [`selection()`](#selection). A Chromium browser — Chrome, Edge — tells Accessibility nothing about a page's own text until the person turns on **General → Advanced → Ask Chromium browsers for a page's text**, so a selection in a page is told only then; its address bar is told either way. An editor that keeps its selection from Accessibility — Visual Studio Code with its screen reader support off — is never told; [`selection()`](#selection) from a shortcut still reads it there by ⌘C with `clipboard`. `editable` is [`selection_editable()`](#selection_editable)'s answer for it. `clickInterval` is the Mac's double-click interval in milliseconds: a click within it continues the gesture, so a double-click may yet become a triple-click — [`open_window_after`](#open_window_aftername-after_ms) waits it out (Lumi 1.50.0). Nothing says where on the screen. Only while the **Selection** switch on your Permissions sheet is on — see [`selection_watching()`](#selection_watching-set_selection_watchingon-and-selection_watchers). The one event whose run may open a window: one [panel declared `position = "selection"`](/extensions/windows), once, beside that selection — below. A newer selection replaces one still waiting, and one more than a second old is not delivered. Needs the `selection-watch` capability and Lumi 1.47.0. |
| `keyboard-selection`  | `selection`'s, with `by` `"keyboard"` and `"keys"`                                                                                                  | The person selecting text with the keyboard in another app — ⇧ and the arrows, ⇧⌥ by words, ⇧⌘ to the line's end, ⌘A, ⌘G — told once the selection has rested: unchanged for 300 ms and every modifier up for 150 ms, so a selection made to type over is never told. Only a change the person's last key just made, and a key that selects — ⇧ with a key that moves the caret, or anything with ⌘ or ⌃ held: a press or a drag is `selection`'s, what an app selects as the person types (a word it completes) is never told, and what an app or Lumi itself selects is nobody's. Its own name so an extension written for the mouse is never told of the keys; hear both to be told of both. `keys` says what kind of key made it, never which: `"extend"` — ⇧ with a key that moves the caret, ⌥ or ⌘ held or not — `"select-all"` — ⌘A, by the letter it types on any layout — or `"command"` — any other key with ⌘ or ⌃ held, a command that left text selected: Find filling its field, an address bar's ⌘L, Find Next, the ⌃⇧ text bindings. An extension that would answer only the person selecting on purpose answers the first two. Read, gated and granted as `selection` is — its run may open the same one panel beside it — and Lumi measures the place without the pointer, which is wherever the person left it. An app that does not report its selection changing to Accessibility, as Visual Studio Code's editor does not unless its screen reader support is on, is never told. Needs the `selection-watch` capability and Lumi 1.50.0.                                                                                                                                                                                                                                                                                                                                |
| `selection-watching`  | `{"v":1, "watching", "at"}`                                                                                                                         | The person turning the **Selection** switch on your Permissions sheet on or off. Only when they move it, never for your own `set_selection_watching`. Needs the `selection-watch` capability and Lumi 1.47.0.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `timer`               | `{"v":1, "name", "at", "late", "missed"}`                                                                                                           | One of your own [timers](#timers) coming due: `name` as you set it, `at` the moment it fired in milliseconds since the Unix epoch — not when you hear it, which can be later. `late` is true when that was more than a minute after it was due: the Mac slept, Lumi was quit or your extension was off. `missed` counts the further repeats that came due meanwhile, folded into this one firing. Told at least once — see [Timers](#timers). `timers::fired` reads it. Needs the `timers` capability and Lumi 1.59.0.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `schedule`            | `{"v":1, "name", "at", "late", "missed"}`                                                                                                           | One of your own [schedules](#schedules) coming due — `timer`'s payload, under its own name: `at` the moment it fired, `late` more than a minute after its moment, `missed` the further moments folded into this firing. `schedules::fired` reads it. Needs the `timers` capability and Lumi 1.60.0.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `time-zone`           | `{"v":1, "id", "offsetMinutes", "at"}`                                                                                                              | This Mac's time zone changing — see [Time zone](#time-zone). `id` and `offsetMinutes` are the new zone as `time_zone::current()` answers it, `at` the moment it changed in milliseconds since the Unix epoch. Once per change; a change of clocks for summer is not a change of zone and is not told. `time_zone::changed` reads it. Needs the `time-zone` capability and Lumi 1.59.0.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `microphone-use`      | `{"v":1, "apps":[{"app", "name", "since"}], "at"}`                                                                                                  | The apps using a microphone changing — see [Microphone in use](#microphone-in-use). The whole set each time, never a change: every app with a microphone open then, by name, `app` `null` for a process without a bundle id. Told once too, as it stands, when your extension starts hearing it. Changes within a quarter of a second are one. `microphone_use::changed` reads it. Needs the `microphone-use` capability, macOS 14 and Lumi 1.65.0.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `menu-shown`          | `{"v":1, "shown", "at"}`                                                                                                                            | The person showing or hiding your rows in Lumi's menu bar menu with the **Menu bar** switch on your Permissions sheet under Extensions — see [Whether your rows show](#whether-your-rows-show). `shown` is the switch as it now stands, `at` the moment it moved in milliseconds since the Unix epoch. Only when the person moves it there, never for your own `menu::set` or `menu::set_shown`; `menu::shown()` answers the same question any time. Lumi never waits on your answer. Needs the `menu` capability and Lumi 1.44.0.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `menu-bar-item-shown` | `{"v":1, "shown", "at"}`                                                                                                                            | The person showing or hiding your own item in the menu bar with the **Show its icon in the menu bar** switch on your Permissions sheet — see [Menu bar item](#menu-bar-item). `menu-shown`'s payload and rules: only when the person moves it there, never for your own `menu_bar_item::set_shown`; `menu_bar_item::shown()` answers the same question any time. Lumi never waits on your answer. Needs the `menu-bar-item` capability and Lumi 1.61.0.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `sound-ended`         | `{"v":1, "sound", "name", "finished", "at"}`                                                                                                        | One of your own [sounds](#sounds) stopping: `sound` the number `play` answered, `name` the one it was played under or null, `finished` true when it came to its end and false when it was stopped — by you, by a sound of the same name, or by the person's Stop row — and `at` when. Not told when your extension is switched off or removed. Lumi never waits on your answer. Needs the `sound` capability and Lumi 1.74.0.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |

It runs under `on_lifecycle`'s rules — only while switched on, its answer
only written to Lumi's log, one of your four runs at once — and two more that
follow from nobody having pressed anything: `open_window` cannot open a
[panel](/extensions/windows) from here, and `paste` is refused.

`selection` and `keyboard-selection` are the exception, because the person
selecting is the moment: its run may `open_window` one panel declared `position = "selection"`, once,
within two seconds of the selection and only while it is still the person's
latest. The panel opens beside that selection without the keyboard, whatever
its `focus` says, and goes at their next click, key or scroll. The run may set
that panel's size, material, theme and anchor and `post` to it; `paste`,
`replace_selection`, `replace_selection_and_select`, `set_pinned` and opening
Settings are still refused.

```rust
fn on_event(name: String, payload: String) -> Result<(), String> {
    if name == "selection" {
        let said: serde_json::Value = serde_json::from_str(&payload).map_err(|e| e.to_string())?;
        lumi::post("bar", &said.to_string())?;
        return lumi::open_window("bar");
    }
    Ok(())
}
```

### `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. Your own [`set_setting`](#set_settingname-value)
is the exception — the rest of the run reads what it saved.

### `set_setting(name, value)` [#set_settingname-value]

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

Saves one setting, as a save from your Settings page does — for a menu row
that switches it without the person opening the page:

```rust
lumi::set_setting("method", "vni")?;
```

The value is text, as every setting is (`"true"` and `"false"` for a
`bool`). It is held to the field's own rules — a value they refuse is
refused with the sentence the page would show — filed under the live
profile for a field declared `scope = "profile"`, and told to your open
pages. Only for something the person did: a command, a menu bar row or one of
your windows. Needs Lumi 1.61.0.

### `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. A window with
[`copies`](/extensions/windows#several-of-one-window) is opened copy by copy,
`open_window("card-3")`, in this call and every other that names a window. One already up is focused, never
closed, your command's own press included; for a key that also puts it away,
see `window_state` below. 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.

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

`open_window_on` is `open_window` on the display your window `beside` stands
on now. A window that opens where it was left (`remember-frame`) opens at that
place carried to this display — as far from the same edges, the nearer of left
and right and of top and bottom, held inside — and one that opens at a corner
or in the middle opens at that one of this display; one that opens at the
pointer or a selection opens there, as ever. One already up is focused where it
stands; with `beside` not on screen, or on the same display, it is
`open_window`. It is for a smaller window standing in for a bigger one, opened
as the bigger one is put away — a browser minimized to a pill — which should
appear on the display the person was looking at. Open it before you
[`hide_window`](#hide_windowname-and-show_windowname) the bigger one, while
that one is still on screen to be measured. Both are names your manifest
declares (Lumi 1.65.0).

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

`open_window_at` is `open_window` at a corner of your window `beside` as it
stands now: the panel opens with its `corner` on that window's same corner —
`"top-left"`, `"top-right"`, `"bottom-left"` or `"bottom-right"`, or
`"center"` for its middle on that one's middle — held inside the display
`beside` is on, against its edge where it would run past it. A window
`hide_window` took away counts as standing where it was, since that is where
`show_window` puts it back. It is for a smaller window standing in for a bigger
one, which should appear where that one was — a pill at the foot of the
browser minimized to it:

```rust
lumi::open_window_at("pill", "browser", "bottom-right")?;
lumi::hide_window("browser")?;
```

A panel only — macOS places a window as it opens — and not one Lumi places
beside the selection, the menu bar item or another window. One already up is
focused where it stands; with `beside` neither on screen nor stepped aside it
is `open_window`. Both are names your manifest declares (Lumi 1.75.0).

### `open_window_after(name, after_ms)` [#open_window_aftername-after_ms]

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

`open_window`, `after_ms` milliseconds from now — from the run of a
`selection` or `keyboard-selection` event only, as its one opening of the
panel beside that selection. It answers at once; the panel opens when the
time is up, unless the person has pressed, typed or scrolled since — then it
never opens — or the selection is two seconds old. At most 1000; 0 is
`open_window`. Refused from any other run.

For a panel that waits for the person to finish: a double-click may yet
become a triple-click, and the `selection` event's `clickInterval` says for
how long. Waited out, the double-click's panel opens; cut short by the third
click, it never shows, and the triple-click's own run opens one.

```rust
let said: serde_json::Value = serde_json::from_str(&payload).map_err(|e| e.to_string())?;
if said["by"] == "double-click" {
    let wait = said["clickInterval"].as_u64().unwrap_or(500) as u32;
    return lumi::open_window_after("bar", wait);
}
lumi::open_window("bar")
```

Needs Lumi 1.50.0.

### `close_window(name)` [#close_windowname]

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

Closes one of your declared windows — what a panel's Esc does. A window that
is not open is not an error.

### `window_state(name)` [#window_statename]

```rust
pub fn window_state(name: &str) -> Result<Presence, String>   // Hidden | Up | Focused
```

Where one of your declared windows stands: `Hidden` (not on screen, a panel
put away included), `Up` (on screen while the keyboard is elsewhere — a pinned
panel the person left, even with the pointer over it) or `Focused`. What a
command asks before it decides — a key that toggles closes a `Focused` panel
and opens the rest. Works from `on_event` too. Needs Lumi 1.31.0.

### `hide_window(name)` and `show_window(name)` [#hide_windowname-and-show_windowname]

```rust
pub fn hide_window(name: &str) -> Result<bool, String>
pub fn show_window(name: &str) -> Result<bool, String>
pub fn show_window_on(name: &str, beside: &str) -> Result<bool, String>
pub fn show_window_at(name: &str, beside: &str, corner: &str) -> Result<bool, String>
```

`hide_window` takes one of your declared windows off screen **without closing
it**: its page keeps running and keeps everything in it, where `close_window`
ends a window's page and blanks a panel's (unless the panel
[keeps its page](/extensions/windows#keep-the-page-between-openings)). `show_window` puts it back where it
stood among everybody's windows — under whatever was over it, with the
keyboard if it had it. A [panel beside the selection](/extensions/windows#a-panel-beside-the-selection)
comes back as it opened: one without the keyboard goes again at the next key,
press or scroll elsewhere, as it would have had it never stepped aside. Each
answers whether it did: `hide_window` is `false`
for a window already off screen, closed or never opened, or in full screen —
it has a Space of its own, and taking it away would leave the person on an
empty one, while nothing is behind it to see — and `show_window` is
`false` for any window `hide_window` did not take away — it restores, it never
opens, and a window the person opened or closed in between stays as they left
it. Lumi remembers what it took away and where, so your code needs to remember
nothing between the two.

A window showing [web pages](/extensions/windows) keeps them playing: it stays
where it is, see-through and letting every click through to what is under it,
rather than leaving the screen, because a site whose window leaves is told its
page is hidden, and some stop playing for that — YouTube's phone layout does
(Lumi 1.65.0). `window_state` says `hidden` for it all the same.

`show_window_on` is `show_window` on the display your window `beside` stands
on now: the window comes back at the place it had on its own display — as far
from the same edges, the nearer of left and right and of top and bottom — held
inside that one. On the same display, or with `beside` not on screen, it is
`show_window`. It is for a window stepped aside in favour of a smaller one the
person can carry to another display — a browser minimized to a pill — which
should come back where that one is. Both are names your manifest declares
(Lumi 1.65.0).

`show_window_at` is `show_window` at a corner of your window `beside` as it
stands now, in [`open_window_at`](#open_windowname)'s words: the window comes
back with its `corner` on that window's same corner, or its middle on that
one's middle, held inside the display `beside` is on. With `beside` not on
screen it is `show_window`. It is for a window that should come back from where
the smaller one stands, wherever the person dragged it, rather than from where
it was left — a browser rising from its pill, `"bottom-right"` both ways, so
that minimizing and restoring without a drag in between puts it back exactly
where it stood. Refused for a panel Lumi places beside something. Both are
names your manifest declares (Lumi 1.75.0).

For getting out of the way of something on the screen — an editor while Lumi
reads the text under it:

```rust
fn on_event(name: String, payload: String) -> Result<(), String> {
    if name == "screen-capture" {
        let event: serde_json::Value = serde_json::from_str(&payload).map_err(|e| e.to_string())?;
        match (event["phase"].as_str(), event["region"].as_str()) {
            (Some("start"), Some("area" | "window")) => { lumi_extension_api::hide_window("editor")?; }
            (Some("end"), _) => { lumi_extension_api::show_window("editor")?; }
            _ => {}
        }
    }
    Ok(())
}
```

Both work from `on_event`. Needs Lumi 1.36.0.

### `post(window, message)` [#postwindow-message]

```rust
pub fn post(window: &str, message: &str) -> Result<bool, String>
```

Sends `message`, JSON text, to one of your own pages while it is on screen —
a declared window by its manifest `name`, or a page of yours drawn in a tab
by the name its `call` arrives under: `:about`, `:settings`, `:shortcuts`,
`:changelog` or `:page:<name>`. The page hears it as a
[`lumi:message` event](/extensions/windows#when-your-extension-has-news),
its `detail` the parsed JSON. It answers `true` when the page was there to
hear it and `false` when it was not, so nothing was sent: a post is news for
whoever is looking, never queued for later. A page that opens afterwards asks
for what it shows with a `call`. A panel put away that
[keeps its page](/extensions/windows#keep-the-page-between-openings) is told
all the same — that page is the one shown next (Lumi 1.45.0).

It is the way to keep an open page current with what just happened — a copy
heard in `on_event`, a count that changed — and the one way to reach an
embedded page while Lumi's Settings window is behind another app, when the
page's own `call` is refused. Data goes to the page and nothing comes back,
so it works from anywhere, `on_event` included, and needs no capability.

```rust
fn on_event(name: String, _payload: String) -> Result<(), String> {
    if name == "clipboard" {
        // … keep the copy under "item.<id>" …
        let copies = lumi::storage::keys("item.")?.len();
        lumi::post(":page:dashboard", &serde_json::json!({ "kind": "count", "copies": copies }).to_string())?;
    }
    Ok(())
}
```

A window your manifest does not declare is refused, and so is a message over
64 KiB or one that is not JSON. A [sheet](/extensions/windows#a-sheet) cannot
be posted to: its page answers the page that opened it.

### `open_settings()` [#open_settings]

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

Brings up Lumi's Settings window on your extension's own page, on its
Settings tab — the "Settings…" row of a panel or a menu. Only for a press: a
command, a menu bar row or one of your windows, never `on_event` or a
lifecycle hook, since Settings coming forward on its own would be a window
jumping out at somebody. No capability: it shows Lumi's own pane, and only
yours.

### `switch_off()` [#switch_off]

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

Switches your extension off, as the switch beside it under **Extensions** in
Lumi's Settings does — for a "Quit" row of your own, in a panel or in your
[menu bar item's](#menu-bar-item) menu. Your windows close, your menu bar
item and rows go, and none of your code runs — commands, shortcuts, events,
timers — until the person switches it on again there. You can switch
yourself off, never on. Only for a press, as `open_settings` is, and refused
while your extension is being uninstalled. No capability: it only takes
away. Needs Lumi 1.61.0.

### `open_permissions()` [#open_permissions]

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

Brings up Lumi's Settings window on your extension's own page with its
Permissions sheet open: what your extension may reach, and the switches Lumi
keeps over it — the folders it writes into, whether its rows show in Lumi's
menu bar menu, whether its panels are loaded ahead. Lumi draws that sheet
itself, whatever your own pages draw. Only for a press, like
`open_settings`. No capability. Needs Lumi 1.44.0.

### `preload_enabled()` and `set_preload_enabled(on)` [#preload_enabled-and-set_preload_enabledon]

```rust
pub fn preload_enabled() -> Result<bool, String>
pub fn set_preload_enabled(on: bool) -> Result<(), String>
```

Whether Lumi loads your panels ahead — those whose `[[window]]` says
[`preload = true`](/extensions/manifest#window) — as your extension is
loaded: at launch, once installed or updated, when switched on. One switch,
kept by Lumi the way your menu bar rows' is — on from install unless your
manifest says [`preload-default = false`](/extensions/manifest#extension),
then where it was left: your own settings move it with `set_preload_enabled`, and the person
moves the same switch with **Start when Lumi opens** on your Permissions sheet.
While it is off, such a panel loads at its first opening instead and keeps
its page from then on, as any [kept panel](/extensions/windows#keep-the-page-between-openings)
does — so the first opening after Lumi starts is the slower one. Turned on,
your panels are loaded ahead at once; turned off, a page loaded ahead and not
opened yet is let go.

```rust
// In run_ui, for a "Start when Lumi opens" in your own settings:
"preload" => lumi::preload_enabled().map(|on| format!(r#"{{"on":{on}}}"#)),
"set-preload" => lumi::set_preload_enabled(on).map(|()| String::new()),
```

Declare `events = ["preload-enabled"]` to hear the person move it on the
sheet, with `{"v":1, "enabled", "at"}` — never for your own
`set_preload_enabled`. Both answer from anywhere, `on_event` included, like
`window_state`; `set_preload_enabled` not from an uninstall. Both need the
`preload` capability, and are refused for an extension none of whose panels
says `preload`. Needs Lumi 1.45.0 — set `min-lumi-version`, since a component
that calls them does not load on an older Lumi.

### `set_tab_label(tab, label)` [#set_tab_labeltab-label]

```rust
pub fn set_tab_label(tab: &str, label: Option<&str>) -> Result<(), String>
```

Puts your own words on one of the tabs of your page under **Settings →
Extensions** — for pages shown in the language the person picked, whose tabs
should follow. `tab` is named as a call from it arrives: `":about"`,
`":settings"`, `":shortcuts"`, `":changelog"` or `":page:<name>"`. `None` puts
back the label your [manifest](/extensions/manifest#labels-that-follow-your-language)
gives it.

```rust
fn on_lifecycle(_event: lumi::Lifecycle) -> Result<(), String> {
    // Right before anybody opens the page: on install and on every update.
    label_tabs()
}

fn label_tabs() -> Result<(), String> {
    let vietnamese = lumi::setting("language").as_deref() == Some("vi");
    lumi::set_tab_label(":settings", Some(if vietnamese { "Cài đặt" } else { "Settings" }))
}
```

Call it wherever your language setting changes too — your settings page's
`call` is the usual place, or the page sends
[`POST /__lumi__/tab-label`](/extensions/windows#tabs-in-the-persons-language)
itself. The label is held to a manifest label's rules: up to 24 characters on
one line, not Permissions, not the name of another of Lumi's tabs — the tab's
own name back is fine — and no two of your tabs alike. A tab your manifest
hides or does not declare is refused. Lumi keeps the label across restarts and
updates, and the same label again is no change, so calling it on every run
costs one call. The review sheet and your Permissions sheet keep the
manifest's labels. From any run but an uninstall; no capability. Needs Lumi
1.61.0 — set `min-lumi-version`, since a component that calls it does not load
on an older Lumi.

### `storage` [#storage]

```rust
pub mod storage {
    pub fn get(key: &str) -> Result<Option<Entry>, String>;   // Entry { value, rev }
    pub fn put(key: &str, value: &str, if_rev: Option<u64>) -> Result<u64, PutError>;
    pub fn delete(key: &str) -> Result<(), String>;
    pub fn keys(prefix: &str) -> Result<Vec<String>, String>;
    pub fn blob_write(bytes: &[u8]) -> Result<String, String>;
    pub fn blob_read(id: &str) -> Result<Vec<u8>, String>;
    pub fn blob_delete(id: &str) -> Result<(), String>;
    pub fn blob_save(id: &str, name: &str) -> Result<(), String>; // Lumi 1.26+
    pub fn blob_save_dated(id: &str, name: &str) -> Result<(), String>; // Lumi 1.34+
    pub fn blobs() -> Result<Vec<(String, u64)>, String>;
    pub fn usage() -> Result<(u64, u64), String>;
}
```

Your extension's own place to keep things between runs — nothing survives in
memory, since every command, node and page request starts a fresh instance.
Lumi keeps it on the Mac, encrypted with a key it holds in the Keychain: your
component never sees the key, no other extension can read it, and it is not in
a Lumi backup. It survives updates and is removed when your extension is
uninstalled.

`put` writes only if the key is still at the revision you read (`None`: it must
not exist yet), and answers `PutError::Conflict` if another run wrote first —
read again, reapply, write again. Any other refusal is `PutError::Failed`, with
the sentence; `into_message()` turns either into a `String` for `?`:

```rust
use lumi::storage::{self, PutError};

loop {
    let seen = storage::get("count")?;
    let n = seen.as_ref().map_or(0, |e| e.value.parse().unwrap_or(0)) + 1;
    match storage::put("count", &n.to_string(), seen.map(|e| e.rev)) {
        Err(PutError::Conflict) => continue,
        other => break other.map(drop).map_err(PutError::into_message)?,
    }
}
```

**Key names are not encrypted**; they are file
names, so keep secrets in values and use random ids in names. A name is 1–128
lowercase letters, digits, `.`, `-` or `_`; a value is at most 1 MB, a blob
written from your component at most 4 MB, and the whole store 512 MB.

Lumi writes blobs into your store too — a copied image, for an extension that
hears `clipboard` (up to 32 MB), a `screen` capture, or what your
page uploads with `PUT /__lumi__/blob` (both up to 160 MB). Reading one that large back
into your component fails at the 4 MB boundary; you rarely need to. Hand its
id to your page, which shows it through `GET /__lumi__/blob/<id>`
([the bridge](/extensions/windows#the-bridge)), or put it back on the
clipboard with `Data::Blob`, which never passes through your component.

`blob_save` hands a blob to the person as a file. Your panels close, and
Lumi's Save panel comes up with `name` filled in and your extension's name
above it; the file goes wherever they choose. It returns once the Save panel
is up and does not say where the file went or whether they cancelled — a write
that fails is reported to them in an alert. To know whether it was saved, use
[`files::save`](#files). `name` is a file name, not a path:
1–255 bytes, no `/`, no control characters, not starting with `.`. Any size of
blob works, Lumi's own large ones included, since the bytes never pass through
your component. Like `paste`, it is refused unless it answers a press — a
command or one of your windows. Needs Lumi 1.26.0; say so with
`min-lumi-version`.

`blob_save_dated` is `blob_save` with every `{date}` and `{time}` in `name`
filled in from the Mac's own clock and time zone — `2026-10-02` and
`14.03.11`, the way macOS names a screenshot. Your component's clock is in UTC,
so a name it writes itself is hours off for most people. The name filled in is
not told back: `blob_save_dated` costs no capability, and Lumi lets a
component learn the person's time zone only through one that says so —
[`time-zone`](#time-zone), and [`timers`](#timers), whose clock times fire on
the person's clock. Needs Lumi 1.34.0.

```rust
lumi::storage::blob_save_dated(&shot.blob, "Screenshot {date} at {time}.png")?;
```

Needs Lumi 1.25.0 or later — set `min-lumi-version` if you call it. A
component imports the interfaces it calls, so an older Lumi refuses to start one
that uses storage; one that never calls it runs as before.

## 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. So
is one whose only answer is an empty text field saying nothing in it is
selected — the stand-in an editor that draws its own text keeps for the keys,
as Visual Studio Code's is with its screen reader support off. Asked from one
of your panels while it holds the keys, where the ⌘C would go into the panel,
Lumi first gives the keys back to that application, as
`replace_selection_and_select` does (Lumi 1.50.0) — and since Lumi 1.55.0
only lends them: once the run is over, the panel holds them again, so the
person goes on typing in its field rather than over the selection. A run
that goes on to write over the selection or paste leaves them with the
application, as before. A panel put away meanwhile — the person pressed or
typed elsewhere while the keys were lent — stays put away.
Empty text with `how == "nothing"` is the ordinary miss — nothing selected —
not an error.

Read from a web page through Accessibility, the text leaves out the numbers
and bullets of any list it runs through, with a line break where each item
began.

Since Lumi 1.59.0 a page nobody can edit reads as the page shows it, where a
browser's own text would lose it. Chrome and the apps built on it answer a
radio button or a checkbox with U+FFFC or with nothing, its label's text gone
with it — a quiz's question and its answers read as the question alone — and
glue a page's blocks together. So Lumi puts each control's label where it
stands, leaves pictures out, starts each paragraph, heading or list item on a
line of its own, and keeps a table row's cells apart with tabs. A selection
across a whole long page may be left as the browser gives it, but never with
U+FFFC in it. Text that can be edited reads as the app gives it — a picture's
U+FFFC included, below.

### `copy_selection()` [#copy_selection]

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

Copies the selection in the frontmost application the way that application
copies it — its own ⌘C — so the board holds what it puts there: the text as
it lays it out, and the formatting, links and pictures it keeps, which no
reading of the text carries. For a Copy button: what the person pastes is
what the app's own Copy would have given them. `true` once the board has
changed; `false` when nothing changed within a moment — nothing selected, or
nothing the app will copy.

Unlike `selection()`'s ⌘C, the board is not put back: this is the person's
copy, and a clipboard history keeps it as one. Asked from one of your panels
while it holds the keys, the keys are lent to the app for the copy and come
back to the panel once the run is over, as `selection()` lends them. Refused
with one of Lumi's own windows in front.

Needs `clipboard`, `input` and `accessibility` — it presses a key in somebody
else's app and changes the board — and answers only a command or a request
from one of your windows; refused from `on_event` and `on_lifecycle`. Needs
Lumi 1.59.0.

### `selection_editable()` [#selection_editable]

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

Whether the selection in the frontmost application can be written over, as
Accessibility says. `Some(false)` is text nobody can edit there — a web page,
a terminal's scrollback — where `replace_selection` is refused; `Some(true)` is
a field or a document; `None` is an app that says neither, where
`replace_selection` still types or pastes. Ask it before offering to replace,
so a button that can do nothing is never shown. Needs `accessibility` and Lumi
1.47.0.

### `replace_selection(expected, replacement)` [#replace_selectionexpected-replacement]

```rust
pub fn replace_selection(expected: &str, replacement: &str) -> Result<String, String>
```

Puts `replacement` where `expected` is selected in the frontmost application.
`expected` is the text `selection()` answered: a selection the person has
changed since is refused, never written over. The answer says how it went in:
`"accessibility"`, `"typed"` or `"pasted"`.

Lumi writes it through Accessibility where the app takes it, types it when it
is short, and otherwise pastes it through the general pasteboard and puts the
board back — that last only when you also declare `clipboard`; without it a
long replacement the app will not take through Accessibility is refused. Where
`selection_editable()` answers `Some(false)` it is refused with nothing typed:
there a keystroke is a key — a page's shortcut, a word at a shell prompt — not
a replacement. Your panels close first, so the keys land where the selection
is.

A picture in the selection — an image, or an emoji an app draws as a picture
rather than a character, as Slack does — reads as U+FFFC where it stands in
the text `selection()` answers, and says nothing about which picture it was.
In text that can be edited it does so in every app since Lumi 1.59.0,
Safari's included, which leaves it out on its own; on a page nobody can edit
the picture is left out of the text since then, as a browser's own copy
leaves it out.
Since Lumi 1.59.0 a write whose `expected` holds one is refused before
anything closes: no text written over it can put the picture back. Ask
`text.contains('\u{FFFC}')` before offering to replace, as you ask
`selection_editable()`.

Needs `accessibility` and `input`, and answers only a command or a request from
one of your windows — refused from `on_event` and `on_lifecycle`, where nobody
asked for anything to change. Needs Lumi 1.47.0.

### `replace_selection_and_select(expected, replacement)` [#replace_selection_and_selectexpected-replacement]

```rust
pub fn replace_selection_and_select(expected: &str, replacement: &str) -> Result<Replaced, String>
```

`replace_selection`, every rule of it, then the text written selected again
where it was written — whatever its length. "I love you" written over as "tôi
yêu em nhiều" is selected as those sixteen characters, so the person sees what
changed and your next action works on it. `Replaced::how` is
`replace_selection`'s answer; `Replaced::selected` says whether the new text is
selected now.

Lumi reads where the selection began before it writes, and selects the new
text only once it can read it there exactly — typed or pasted text is waited
for until it arrives. `selected` is `false` where the app gives no place for a
selection through Accessibility, or rewrote the text on its way in; the caret
is then where the write left it, as after `replace_selection`.

Unlike `replace_selection`, your panels stay up: only a panel holding the
keyboard gives it back first — one beside the selection staying up without it,
any other closed — so a panel beside the text never blinks out and in around
the write. To keep it with the text, open it again when this answers with
`selected`: Lumi measures the new selection and moves the panel beside it.

```rust
let found = lumi::selection()?;
let changed = lumi::transforms::apply("upper", &found.text)?;
if lumi::replace_selection_and_select(&found.text, &changed)?.selected {
    lumi::open_window("peek")?;
}
```

Same capabilities and the same callers as `replace_selection`. Needs Lumi
1.48.0.

### `selection_watching()`, `set_selection_watching(on)` and `selection_watchers()` [#selection_watching-set_selection_watchingon-and-selection_watchers]

```rust
pub fn selection_watching() -> Result<bool, String>
pub fn set_selection_watching(on: bool) -> Result<(), String>
pub fn selection_watchers() -> Result<Vec<String>, String>
```

Whether you are told of the selections the person makes — the `selection`
and `keyboard-selection` events — is one switch, kept by Lumi and shown as **Selection** on your
Permissions sheet. It starts on when you are installed, unless your manifest
says `selection-watch-default = false`, and stays where it was left: your own
settings move it with `set_selection_watching`, and the person moves it on the
sheet. Declare `events = ["selection-watching"]` to hear the person move it.

`selection_watchers` names the other extensions told of the person's
selections now. Lumi does not choose between you — each opens its own panel —
so you can say so, or offer to step aside. All three need `selection-watch`
and Lumi 1.47.0; `set_selection_watching` not from an uninstall.

### `transforms::all()` and `transforms::apply(id, text)` [#transformsall-and-transformsapplyid-text]

```rust
pub mod transforms {
    pub fn all() -> Result<Vec<Transform>, String>;
    pub fn apply(id: &str, text: &str) -> Result<String, String>;
}

pub struct Transform {
    pub id: String,    // "upper", "sortLines", "jsonPretty", …
    pub label: String, // "UPPERCASE", "Sort lines", "Pretty-print JSON", …
    pub group: String, // "case", "lines" or "encode"
}
```

Lumi's own text transforms — the ones a **Transform selection** shortcut
offers — by name. `all` lists every built-in one in Lumi's order; one the person
wrote as a script is not among them, since that is their own code. `apply` runs
`text` through the one `id` names, refused for an id `all` does not list and
for text over 1 MiB; a transform that cannot do what it says — JSON that does
not parse, base64 that is not — answers why rather than an empty string. Pure:
nothing is read or written but the text you hand over, so no capability is
needed. Needs Lumi 1.47.0.

```rust
let found = lumi::selection()?;
if lumi::selection_editable()? != Some(false) {
    let shouted = lumi::transforms::apply("upper", &found.text)?;
    lumi::replace_selection(&found.text, &shouted)?;
}
```

### `translation` [#translation]

```rust
pub mod translation {
    pub fn languages() -> Result<Vec<String>, String>;
    pub fn preferred() -> Result<Vec<String>, String>;
    pub fn status(source: &str, target: &str) -> Result<Readiness, String>;
    pub fn detect(text: &str) -> Result<Vec<Guess>, String>;
    pub fn translate(request: &Request) -> Result<Translated, String>;
    pub fn prepare(source: &str, target: &str) -> Result<Readiness, String>;
}

pub enum Readiness { Installed, Downloadable, Unsupported }
pub enum Quality { Fast, Best }

pub struct Request {
    pub text: String,
    pub source: Option<String>, // None or empty: Lumi's guess, when it is sure
    pub target: Option<String>, // None or empty: the person's own language
    pub quality: Quality,
}

pub struct Translated {
    pub text: String,
    pub source: String, // "en", "vi", "zh-TW", …
    pub target: String,
}

pub struct Guess {
    pub language: String,
    pub confidence: f32, // 0 to 1
}
```

Translation with the translator built into macOS, the one the Translate app
uses. The text is translated on this Mac and never leaves it, so no capability
is needed. Languages are BCP 47 ids as the system names them — `"en"`, `"vi"`,
`"zh-TW"` — and any spelling of one is understood (`"vi-VN"`, `"zh-Hant"`).
Needs Lumi 1.51.0 and macOS 26: on an older macOS every function answers that
translating needs macOS 26, so check `languages` before you offer to translate.

* `languages` lists every language this Mac translates. The list grows with
  macOS, so read it rather than keeping one.
* `preferred` is the person's own languages that the Mac translates: the
  Mac's preferred languages in their order, then the language of its region. A
  Mac set to English in Vietnam answers `["en", "vi"]` — so an English text has
  somewhere to go.
* `status` says whether a pair is `Installed`, `Downloadable` — macOS downloads
  its languages once, for every app — or `Unsupported`. Only an installed pair
  translates.
* `detect` answers up to three guesses at a text's language, most likely first.
  A sentence is told reliably; a word or two often is not ("OK" reads as Polish),
  nor is Vietnamese typed without its accents. Weigh the confidence, and let
  the person correct the language rather than trusting a low guess.
* `translate` translates line by line: blank lines, each line's indent, and web
  and email addresses come back as they were, and a line with no words in it is
  left alone. It does not join a paragraph that was wrapped at a fixed width
  into one line first — that is a guess about the text, and yours to make. With
  no `source`, Lumi takes its top guess when it is at least 0.8 sure, and
  refuses otherwise; with no `target`, the first of `preferred` that is not the
  source. Refused over 20,000 characters, for a pair that is not installed, and
  for a source and target that are the same language — each answers why. The
  first call takes about a second while macOS loads the language; after that, a
  sentence is a few dozen milliseconds. `Quality::Best` asks for Apple
  Intelligence's model, on a Mac that has it with macOS 26.4: more fluent and
  many times slower. Anywhere else it is `Fast`.
* `prepare` puts up macOS's own "Download Languages to Translate" prompt for a
  pair, and answers how ready the pair is once the person is done with it.
  Only from a command or one of your windows — a prompt nobody asked for is
  one somebody answers blind. It stands at the top of the screen the pointer is
  on, as macOS's own sheets do, takes the keyboard while it is up and hands it
  back to the app in front when it closes; one at a time across Lumi, and taken
  down after two minutes unanswered. It answers once the prompt is gone, so
  what you do next — read the selection, open a panel beside it — finds the
  app in front with its selection again, and a transient panel of yours beside
  the selection stays up while the person answers it. A declined prompt is no
  error: the answer is `Downloadable`, as it stays while macOS downloads in the
  background, so ask `status` again at the next press rather than waiting on
  it. Offer it when `status` says `Downloadable`, behind a button the person
  presses.

Apple says it may count which languages an app translates between, but never
what was translated.

```rust
let found = lumi::selection()?;
let guesses = lumi::translation::detect(&found.text)?;
let source = guesses.first().filter(|g| g.confidence >= 0.8).map(|g| g.language.clone());
let done = lumi::translation::translate(&lumi::translation::Request {
    text: found.text,
    source,
    target: None,
    quality: lumi::translation::Quality::Fast,
})?;
lumi::alert(&format!("{} → {}: {}", done.source, done.target, done.text))?;
```

### `dictionary` [#dictionary]

```rust
pub mod dictionary {
    pub fn dictionaries() -> Result<Vec<Book>, String>;
    pub fn lookup(text: &str, books: &[String]) -> Result<Vec<Entry>, String>;
    pub fn term(text: &str, at: u32) -> Result<Option<(u32, u32)>, String>;
    pub fn show(text: &str) -> Result<(), String>;
}

pub struct Book {
    pub id: String,              // "com.apple.dictionary.NOAD"
    pub name: String,            // "New Oxford American Dictionary"
    pub short_name: String,      // "Dictionary"
    pub looks_up: Vec<String>,   // BCP 47: "en-US"; two for a bilingual one
    pub explains_in: Vec<String>,
}

pub struct Entry {
    pub book: String,            // a `Book`'s id
    pub headword: String,
    pub syllables: String,       // "ser·en·dip·i·ty", or empty
    pub pronunciations: Vec<Pronunciation>,
    pub groups: Vec<Group>,
    pub text: String,            // the whole entry as plain text, always there
}

pub struct Group {
    pub heading: String,         // "noun", "transitive verb", "PHRASAL VERBS"
    pub pronunciations: Vec<Pronunciation>, // for this part of speech alone
    pub senses: Vec<Sense>,
}

pub struct Sense {
    pub phrase: String,          // "set off", or empty for the headword's own
    pub definition: String,      // empty in a bilingual dictionary or a thesaurus
    pub translations: Vec<String>,
    pub synonyms: Vec<String>,
    pub antonyms: Vec<String>,
    pub examples: Vec<Example>,  // at most three
    pub labels: Vec<String>,     // "informal", "British English"
}

pub struct Example { pub text: String, pub translation: String }
pub struct Pronunciation { pub text: String, pub notation: Notation, pub dialect: String } // "us", "uk" or ""
pub enum Notation { Ipa, Respelling, Other }
```

Words looked up in the dictionaries built into macOS — the ones the Dictionary
app and Look Up show. Only the dictionaries the person turned on in
**Dictionary › Settings**, in their order: one they did not turn on is never
opened, so a lookup downloads nothing. The word is looked up on this Mac and
nothing leaves it, so no capability is needed. Needs Lumi 1.55.0.

* `dictionaries` lists them. Empty when the person turned them all off; offer
  to look a word up only when it is not. To get another dictionary, such as
  Vietnamese – English, the person turns it on in the Dictionary app, which
  downloads it once, for every app.
* `lookup` looks `text` up in the books you name, in that order, or in every
  one when you name none. An inflected form finds its headword ("houses" finds
  "house"), case does not matter, and phrases work ("look up"). A dictionary
  may also answer near entries ("set" finds "sett") after the exact ones. Each
  entry comes as fields read from the dictionary's own markup and, always, as
  plain text in `text`: a dictionary laid out in a way Lumi does not read still
  answers its headword and its text, with the other fields empty, so draw what
  is there. A monolingual dictionary fills `definition`, a bilingual one
  `translations` (and an example's `translation`), a thesaurus `synonyms` and
  `antonyms`. Pronunciations are on the entry when the word is said one way,
  and on its groups when a part of speech is said its own way. At most 8
  entries a dictionary, 64 senses an entry, 3 examples a sense and 40 words a
  list; empty when nothing matches. Refused over 256 characters, and for a book
  that is not turned on. A lookup takes a few milliseconds.
* `term` finds the dictionary term around character `at` of a sentence — in
  "Please look up the running total", at 19, `(19, 13)`: "running total" — so
  a caller holding a caret can look up the phrase it sits in, not only the word.
* `show` brings the Dictionary app forward at `text`, with every dictionary's
  whole entry — or, given empty text, as the person left it, for them to turn
  on another dictionary in its Settings. Only from a command or one of your
  windows: an app coming forward is something the person does.

No dictionary has audio; to say a word, speak its headword with the Web Speech
API in your window. What a dictionary says is its publisher's: show its name
(a `Book`'s `name`) beside what you draw from it, and keep nothing.

```rust
let found = lumi::selection()?;
for entry in lumi::dictionary::lookup(found.text.trim(), &[])? {
    let said = entry.pronunciations.first().map(|p| p.text.clone()).unwrap_or_default();
    let first = entry.groups.iter().find_map(|g| g.senses.first());
    let meaning = first.map(|s| s.definition.clone()).unwrap_or_default();
    lumi::alert(&format!("{} /{said}/ — {meaning}", entry.headword))?;
    break;
}
```

### `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`.

### `read_clipboard(types)` [#read_clipboardtypes]

```rust
pub fn read_clipboard(types: &[&str]) -> Result<Vec<Vec<Rep>>, String>
```

The pasteboard read back the way [`write_clipboard`](#write_clipboarditems-and-pasteitems)
writes it: one list per item, of the representations it holds among `types` —
uniform type identifiers, in the order you want them — or every type it holds,
in its own order, when `types` is empty. An item holding none of them is left
out. Words come back as `Data::Text`: a type that is text or a URL in macOS's
type tree (`public.utf8-plain-text`, `public.html`, `public.file-url`), whose
bytes are UTF-8. Everything else comes back as a `Data::Blob` written into
your [storage](#storage), so the bytes never pass through your component —
counted against your limits, and yours to delete. At most 160 MB in all; a
board holding more is refused whole and nothing is written. Needs `clipboard`
and Lumi 1.36.0.

```rust
use lumi_extension_api::{read_clipboard, Data};

for item in read_clipboard(&["public.png", "public.utf8-plain-text"])? {
    for rep in item {
        match rep.data {
            Data::Text(text) => { /* the words */ }
            Data::Blob(id) => { /* the picture, in your storage */ }
            Data::Bytes(_) => {}
        }
    }
}
```

### `clipboard_image()` [#clipboard_image]

```rust
pub fn clipboard_image() -> Result<Option<Image>, String>

pub struct Image { pub blob: String, pub width: u32, pub height: u32 }
```

The picture on the pasteboard, written into your [storage](#storage) as a PNG
blob, with its size in pixels — or `None` when the board holds no picture.
Whatever the board held it as, it arrives as PNG: a screenshot, a TIFF, a PDF
page, a picture copied out of a browser. The bytes never pass through your
component, so a page of yours opens the blob at `/__lumi__/blob/<id>` and a
blob function hands it on. It counts against your storage limits like any
blob, and it is yours to delete. Needs `clipboard` and Lumi 1.36.0.

### `write_clipboard(items)` and `paste(items)` [#write_clipboarditems-and-pasteitems]

```rust
pub struct Rep { pub uti: String, pub data: Data }
pub enum Data { Text(String), Bytes(Vec<u8>), Blob(String) }

pub fn write_clipboard(items: &[Vec<Rep>]) -> Result<(), String>
pub fn paste(items: &[Vec<Rep>]) -> Result<(), String>
```

Put whole items on the pasteboard: each item is the same thing in several
types (`public.utf8-plain-text`, `public.html`, `public.png`, …), richest
first. `Data::Blob` names a blob in your own [`storage`](#storage), which goes
on the board without passing through your extension. At most 160 MB in all.
Lumi marks the board with your extension's id under `app.lumi.origin` — a type
you cannot write yourself — so when you hear the copy you can tell it was
yours. `write_clipboard` needs `clipboard`.

`paste` writes the same way and then presses ⌘V into the app in front: what a
clipboard history does when a row is picked. Your panels close first, so the
keystroke lands where the caret was, and the board keeps what was pasted. It
needs `clipboard`, `input` and `accessibility`, and answers only something the
person did — a command, or a request from one of your windows. From
`on_event` or `on_lifecycle` it is refused. A plain-text paste is the same
call with only the text type in the item.

### `drag(items, close_on_drop)` [#dragitems-close_on_drop]

```rust
pub fn drag(items: &[Vec<Rep>], close_on_drop: bool) -> Result<(), String>
```

Drag whole items out of the window the person is pressing in, into whatever
app they drop them on — Finder, a browser's upload field, a chat. Items are
`write_clipboard`'s, and so is the 160 MB limit; the board the person copies
with is not touched. An item with a `public.file-url` carries that file; an
image with no file (`public.png` or `public.tiff`) is dragged as a PNG file and
as the image — every app that takes a file gets one, Finder, a browser's upload
field and an Electron app alike, and an app that takes images gets the image —
named the way macOS names a screenshot; anything else goes as its types.

Call it from the request your page sends when a press on an item starts to
move. It is refused unless one of your windows is up and key and the mouse
button is still down in it — an extension cannot start a drag nobody is
making — and, like `paste`, from `on_event` and `on_lifecycle`. The call
returns as the drag begins. `close_on_drop` takes your panels down after a
drop that took the items; a drag given up never closes anything. While the
drag is up your panel stays open even as the drop target takes the keyboard.
Needs `clipboard` and Lumi 1.26.0; say so with `min-lumi-version`. Before
Lumi 1.34 the image was a file only to an app that reads a file promise, such
as Finder.

### `drag_named(items, names, close_on_drop)` [#drag_nameditems-names-close_on_drop]

```rust
pub fn drag_named(items: &[Vec<Rep>], names: &[Option<String>], close_on_drop: bool) -> Result<(), String>
```

`drag`, with the files named: `names[i]` is what item `i` is called where it
becomes a file, without the extension — the file is a PNG. `{date}` and
`{time}` are filled in as `blob_save_dated` fills them, so a screenshot app
passes its own file-name setting and a picture dragged out lands under the
name it would have been saved with. A `/` or `:` becomes `-`, control
characters and a leading dot go, and the name is cut to 120 characters. `None`,
an empty name or an item past the end of `names` gets the default name; an item
with a file URL keeps its file's own. More names than items is refused.
Everything else is `drag`'s. Needs Lumi 1.34.0.

### `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.

What it costs depends on who is opening it. **From a press** — a command, a
menu bar row, a link in one of your windows — the person is opening it, as
they would by clicking it anywhere else, so it costs no capability, only the
call. **Anywhere else** — `on_event`, `on_lifecycle`, a flow node — it needs
`applications`, and a web address needs `network` too.

### `reveal(target)` [#revealtarget]

```rust
pub fn reveal(target: &str) -> Result<(), String>
```

Selects one file in Finder and brings Finder forward — "Show in Finder" for a
file your extension knows about. `target` is a `file:` URL, Finder's
`file:///.file/id=…` form included, which is what a copied file's
`public.file-url` carries, or an absolute path. No capability: it reads
nothing and changes nothing. Only for a press — a command or one of your
windows — since Finder coming forward on its own would be a window jumping
out at somebody.

## 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 the local 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.

## AI [#ai]

### `ai` [#ai-1]

```rust
pub mod ai {
    pub fn ready() -> Result<bool, String>;
    pub fn prepare(language: Option<&str>) -> Result<bool, String>;
    pub fn models() -> Result<Vec<Model>, String>;
    pub fn complete(request: &Request) -> Result<Answer, Failure>;
    pub fn start(request: &Request) -> Result<u64, Failure>;
    pub fn cancel(id: u64) -> Result<bool, String>;
}

pub struct Model {
    pub id: String,        // "anthropic:claude-haiku-5-5", what Request.model takes
    pub name: String,      // "Claude Haiku 5.5"
    pub provider: String,  // "Anthropic"
    pub place: Place,      // OnDevice or Online
    pub thinks: bool,
    pub is_default: bool,
}

pub struct Request {
    pub system: String,               // your instructions
    pub input: String,                // the text to work on, always sent as data
    pub model: Option<String>,        // None: the person's default
    pub max_tokens: Option<u32>,      // fewer than the person allows, never more
    pub thinking: Thinking,           // Off, Low, Medium, High
    pub answer_language: Option<String>, // "same", "vi", "en-GB", or None
    pub format: Format,               // Text or Json
}

pub struct Answer {
    pub text: String,
    pub model: String,       // "provider:model" that answered
    pub model_name: String,
    pub place: Place,
    pub fell_back: Option<String>, // why another model ran, for the person
    pub thought: bool,
    pub cut_short: bool,
    pub usage: Usage,        // input, output and thinking tokens, where reported
    pub ms: u64,
}

pub struct Failure {
    pub message: String,     // a sentence for the person
    pub kind: FailureKind,   // Declined, Stopped, Warning, Fault
}
```

The AI the person set up in Lumi: Apple Intelligence, the models Lumi downloads
and runs itself (Lumi Models), the models on their Mac (Ollama, LM Studio),
Claude Code on their own Claude plan, and providers on their own keys. The person chooses the models under **Settings → AI**
([AI](/features/ai) says what they see there); you choose among them, and never
see a key. Costs `ai`. Needs Lumi 1.52.0.

* `models` lists the models your extension may ask for now, the person's default
  first: the ones they switched on, that their copy of Lumi may use. Offer a
  model picker for each of your features from it, keep the chosen `id` in your
  settings, and name none to run on the person's default. Read it again when
  you offer the picker: the person switches models on and off.
* `ready` says whether a model answers now — Lumi asks the engines on this Mac
  — so a button that would fail can say why before it is pressed.
* `prepare` makes one ready, from a press. Where a model answers already it
  returns `true` at once and asks nothing. Where none does, Lumi offers the
  person the text model it would download for `language` (`Some("vi")`, or
  `None` for any) in its own panel at the pointer — the model's name, size and
  licence — and returns once it is installed, or once they said Not Now or
  stopped the download: `true` when a model answers then. It never downloads
  without asking, and never offers a model the person has here and switched
  off — that refusal names the switch. Needs Lumi 1.64.0.
* `complete` asks and waits for the whole answer. Use it from a command, which
  has no window to stream into.
* `start` asks and answers at once with the call's id; the answer streams to
  your own windows as [`lumi:ai` events](/extensions/windows#when-an-ai-answer-comes).
* Both answer a press only — a command, a menu bar row or one of your windows.
  From `on_event`, a lifecycle hook or a flow node they are refused: nobody
  asked for that text to go anywhere.
* `cancel` stops one of your calls wherever it is.

**What the model is sent.** `system` as its instructions and `input` as the text
to work on, always as data: Lumi tells the model the text is something to work
on, not instructions, so a selection that says "ignore your instructions" is
just text. Nothing else of the person's goes with it, and the model is given no
tools. `answer_language: Some("same")` has Lumi detect the input's language and
name it to the model, which helps every model answer in it.

**The person decides where text goes.** Text that would leave the Mac — a
provider, or Claude Code — waits on the person: Lumi asks at the pointer,
naming your extension and where the text would go, unless they told it not to
ask again for your extension. They can send it, have a model on their Mac
answer instead (the answer's `place` and `fell_back` say so), or decline: a
`Declined` failure, which is theirs and nothing to show. A panel of yours beside
the selection stays up while they answer. They can switch your extension's AI
off under Settings → AI: then `ready` is false, `models` is empty, and every
call is a `Warning`.

**Their limits and plan hold.** The longest input, the longest answer —
`max_tokens` may ask for fewer, never more — the seconds of silence before a
call is given up, and how many calls your extension may have going at once.
A model that cannot run now — switched off, gone, or on a provider the person's
copy of Lumi does not include — falls back to one that can, and `fell_back`
says why in a sentence you can show as it is. Show a `Warning` or `Fault`
failure's message; `Declined` and `Stopped` are nothing to show.

```rust
let found = lumi::selection()?;
let answer = lumi::ai::complete(&lumi::ai::Request {
    system: "Summarize the text in two sentences.".into(),
    input: found.text,
    model: None,
    max_tokens: None,
    thinking: lumi::ai::Thinking::Off,
    answer_language: Some("same".into()),
    format: lumi::ai::Format::Text,
});
match answer {
    Ok(answer) => lumi::alert(&answer.text)?,
    Err(failure) if matches!(failure.kind, lumi::ai::FailureKind::Declined | lumi::ai::FailureKind::Stopped) => {}
    Err(failure) => lumi::alert(&failure.message)?,
}
```

### `transcribe` [#transcribe]

```rust
pub mod transcribe {
    pub fn status(language: &str, speakers: Speakers) -> Result<Readiness, String>;
    pub fn models() -> Result<Vec<Model>, String>;
    pub fn prepare(language: &str, speakers: Speakers) -> Result<Readiness, String>;
    pub fn status_for(languages: &[&str], speakers: Speakers) -> Result<Readiness, String>;   // Lumi 1.65.0
    pub fn prepare_for(languages: &[&str], speakers: Speakers) -> Result<Readiness, String>;  // Lumi 1.65.0
    pub fn complete(request: &Request) -> Result<Transcript, Failure>;
    pub fn start(request: &Request) -> Result<u64, Failure>;
    pub fn start_after(request: &Request, previous: u64) -> Result<u64, Failure>;  // Lumi 1.65.0
    pub fn stop(id: u64) -> Result<bool, String>;
    pub fn cancel(id: u64) -> Result<bool, String>;
    pub fn switch(id: u64, model: Option<&str>) -> Result<Model, Failure>;  // Lumi 1.65.0
}

pub enum Source {
    Microphone,             // costs `microphone` too
    SystemAudio(Playing),   // what this Mac plays; costs `system-audio` too
    Call(Playing),          // both at once, as two streams; costs both
    Blob(String),           // a recording in your storage, by its blob id
}

pub enum Playing { Everything, OneApp }  // OneApp: the person picks, in Lumi
pub enum Speakers { Off, Apart, Named }  // Apart: each segment says which voice; Named: and who, costs `voices`

pub struct Request {
    pub source: Source,
    pub languages: Vec<String>,     // "vi", "en-GB", most likely first; empty for any; at most 8
    pub model: Option<String>,      // a Model.id; None: the person's default
    pub speakers: Speakers,
    pub hints: Vec<String>,         // names and words the speech is likely to hold
    pub partials: bool,             // text while it is said (live sources)
    pub words: bool,                // each word's times, where the model gives them
    pub max_seconds: Option<u32>,   // shorter than the person's longest recording, never longer
    pub levels: Option<Levels>,     // how loud it is, for drawing it; costs `audio-levels`
}

pub struct Levels {
    pub bands: u8,      // frequency bands with each reading of a live source, 0 to 16; 0: loudness alone
    pub overview: u32,  // a recording from storage: the points its loudness is summed into, 0 to 4096
}

pub struct Segment {
    pub id: u64,
    pub revision: u32,      // said again under its id while partial, then as its final
    pub text: String,
    pub start_ms: u64,      // from the start of the recording
    pub end_ms: u64,
    pub partial: bool,
    pub heard: Heard,       // Microphone, SystemAudio or Recording
    pub language: Option<String>,
    pub speaker: Option<Speaker>,   // with Speakers::Apart or Named: which voice said it
    pub words: Vec<Word>,
    pub confidence: Option<f32>,
}

pub struct Transcript {
    pub id: u64,            // the recording's id: what voices::remember names
    pub text: String,       // every final's text, in time order
    pub segments: Vec<Segment>,
    pub model: String,      // "provider:model" that heard it
    pub model_name: String,
    pub place: Place,       // OnDevice or Online
    pub fell_back: Option<String>,  // why another model ran, or that it does not hear the language spoken, for the person
    pub speakers_model: Option<String>,  // "provider:model" that told the voices apart
    pub duration_ms: u64,
    pub cut_short: bool,    // the longest recording ended it
    pub notes: Vec<String>, // sentences for the person: a stream that heard nothing at all
    pub ms: u64,
    pub overview: Vec<u8>,  // with Levels: a recording's loudness, each point 0 to 100
}

pub struct Failure {
    pub message: String,    // a sentence for the person
    pub kind: FailureKind,  // Declined, Stopped, Warning, Fault, Unprepared, NotPermitted, Busy, Unsupported
}
```

Speech to text: what is said into the microphone, what this Mac plays, both at
once in a call, or a recording in your storage, heard by the speech model the
person chose under **Settings → AI** — on their Mac (Apple Speech on macOS 26,
or a model they downloaded in Lumi Models), or online on their own key (OpenAI,
Groq). You get text, and never the sound. Costs `speech`;
hearing the microphone costs `microphone` too, what the Mac plays
`system-audio`, and a call both — declared only beside `speech` or
`keep-audio` — and an extension declaring `text-input` may not declare
`speech`. To keep the audio itself, as a file, see
[`recordings`](#recordings). Needs Lumi 1.64.0.

* `status` says how ready the Mac is to hear `language` (`""` for any):
  `Installed` when a model runs now, `Downloadable` when Lumi can download one,
  `Unsupported` otherwise. With `Speakers::Apart` or `Named` it is the less
  ready of the speech model and the model that tells voices apart. An error is
  a sentence: AI switched off, your
  extension's AI switched off, or the model the person downloaded switched off
  in Lumi Models.
* `prepare` has Lumi offer the person the model it would pick for `language`,
  in its own panel at the pointer — a model in Lumi Models with its name, size,
  source and licence, or Apple Speech's language from macOS when Apple Speech is
  the person's choice or nothing in Lumi Models hears it — then, with
  `Speakers::Apart` or `Named`, the model that tells voices apart, and answers once they
  are installed, or they said not now, with how ready the Mac is then. Only
  from a press.
* `status_for` and `prepare_for` (Lumi 1.65.0) are `status` and `prepare` for
  a recording heard among several languages — ask with the list your
  `Request` will carry. Several `languages` are heard among them: each
  utterance in the one it was said in, where the model tells them apart — Lumi
  Models' own, and an online model that takes several — while an online model
  that takes one language is given none and tells for itself. Apple Speech
  hears one language at a time, so it is passed over for several, and counts
  only for one in `status_for`. That is why asking `status` once per language
  is no answer: each could say installed with only Apple Speech here.
* `models` lists the speech models your extension may ask for, the person's
  default first, each with the languages its list says it hears well — none
  for Apple Speech, whose languages `status` answers one at a time, nor for a
  model online, which hears most — where it is (`place`), and whether it gives
  `partials`: a model online without them gives each segment when its pause
  comes.
* `complete` hears a recording in your storage (`Source::Blob`) and waits for
  the whole transcript. Use it from a command, which has no window to stream
  into. A live source has no end to wait for, and is refused.
* `start` begins hearing any source and answers at once with the recording's
  id; what is heard streams to your own windows as
  [`lumi:transcribe` events](/extensions/windows#when-speech-is-heard) — each
  segment said again as it grows (`partial`) and once more as its final, under
  one id. A file streams the same way, as it is read.
* `start_after` (Lumi 1.65.0) is `start` going on from the voices of
  `previous`, an earlier recording of yours: a voice heard there keeps its
  `label`, and a new one is numbered after them, so a meeting you hear as
  several recordings — after the longest recording, a pause, the Mac asleep —
  labels its people alike in all of them. Lumi keeps a recording's voices for
  ten minutes after it ends (stopped, cut at the longest recording, or the
  Mac gone away; never a cancel), while your pages are open, and only for a
  live source with speakers told apart by the same speakers model; otherwise
  it starts as `start` does. Your pages' `started` event says `continues`
  with `previous`'s id when it went on, which is when to carry your own map
  of labels over.
* All three answer a press only — a command, a menu bar row or one of your windows.
* `stop` ends a recording and still hears what was said up to it; `cancel`
  throws it away.
* `switch` (Lumi 1.65.0) goes on hearing a live recording with another model
  — an id from `models`, or the person's default for `None` — without
  stopping it, so nothing said is missed: the model hearing now goes on while
  Lumi loads the new one, and asks the person first where it hears online;
  then every utterance after is the new model's, and the one being said
  finishes on the model before. Your windows hear `switched`. It answers the
  model hearing now (at once for the model it already has); refused, the
  model before goes on — `Unprepared` for one that cannot hear now (offer
  `prepare`), `Declined` where the person kept the voice on this Mac. Only from
  a press, and only for a recording of yours that is live and going. Stopping
  and starting again instead loses what is said in between.

**One live recording at a time** for your extension: a second `start` of a live
source while one runs is `Busy`. Several extensions may record at once; Lumi
hears the source once and tells each its own segments.

**Lumi asks the person, never you.** Where the model is online, Lumi asks once
per recording, before any of it is heard, whether what is said may leave the
Mac — answered apart from your extension's text, and kept as the person
chooses; your pages hear `asking` while it is up. `Declined` is their Don't
Send. `Playing::OneApp` has Lumi ask the person
which app to hear, in its own panel, the apps playing sound first — and you are
never told which they chose. The first time the microphone is used, Lumi says
what it is for before macOS asks. A `NotPermitted` failure's message names
where in System Settings to allow it.

**It ends for every reason.** `stop` and `cancel`; the person's longest
recording (`cut_short`); the person pressing Stop beside your extension's name
in the menu bar, which shows every recording while it runs; your extension
switched off or uninstalled; AI switched off; no window of yours left to hear
it — a `keep-alive` panel running unseen still counts; or the Mac going to
sleep or its screen locking. A recording ended by `stop`, the longest recording
or the Mac going away is still heard to its end, and a note says why; the rest
are a failure.

**Voices told apart** (`Speakers::Apart`): each segment's `speaker` says which
voice said it — `label`, from 0, one voice for the whole recording, numbered
as voices are first heard; a call's two sides are never the same voice. A file
is told apart once it is heard, so each final comes again with its `speaker`
and a higher `revision`; live, each utterance's voice is found when it ends,
and a final told before that comes again the same way. While an utterance is
still being said its partials carry a guess (Lumi 1.65.0): from two seconds
into it, measured again as it goes, the voice what was heard of it so far
is — a voice nobody has spoken in yet guessed the `label` it will be
numbered, which another voice heard first may take, and with
`Speakers::Named` a remembered voice by its name. Only the final says who it
was. An utterance too short to tell may have no `speaker`. With `Speakers::Named` (`voices` too), a voice
your extension [remembered](#voices) is named where it speaks: `voice` is its
id, `name` its name and `confidence` how alike it is — given only where it is
clearly that person and nobody else you remember, and once given, kept for the
recording; a final named after it was told comes again the same way. The model
is the person's, chosen under **Settings → AI**, and always runs on their Mac;
`speakers_model` names it.

**How loud, for drawing it** (`levels`, costs `audio-levels`, declared beside
`speech`). A waveform, a spectrum or a shape that swells with the voice needs
how loud the audio is, not the audio: ask with `levels`, and your windows hear a
[`level` event](/extensions/windows#when-speech-is-heard) for each stream about
every 128 ms — its `level`, 0 for silence to 100 for full scale, and `bands`, as
many as you asked for up to 16, lowest pitch first, each 0 to 15. The bands are
eased over a quarter of a second, so they move smoothly and a little behind the
`level`, which is not. Draw at your own frame rate by easing toward the latest
reading. A call's two sides are two streams of readings, told apart by `heard`.
A recording from storage is summed into `overview` points instead — each 0 to
100, told as an `overview` event once it is read and kept on the transcript —
for a picture of the whole of it; a point is never shorter than a reading, so a
short recording has fewer points than asked. Lumi bounds all of it whatever you
ask, so it can never be the sound — measured: speech made back from the
readings is heard as no words and as nobody's voice — and more than 16 bands or
4096 points is refused by name.

**What the model is sent.** The sound, your `hints` and your `languages`, and
nothing else of the person's — on the Mac, or to the provider the person set up
and agreed to. Show a `Warning`, `Fault`, `NotPermitted` or `Unsupported`
failure's message; for `Unprepared`, offer `prepare`; `Declined` and `Stopped`
are nothing to show. A stream that heard nothing at all — exact silence, which
is also what a refused permission records — is a sentence in `notes`.

```rust
use lumi::transcribe::{self, FailureKind, Source, Speakers};

// A recording the person picked, heard whole from a command.
let picked = lumi::files::open(&["public.audio"], false, "Choose a recording to turn into text.")?;
let Some(file) = picked.into_iter().next() else { return Ok(String::new()) };
match transcribe::complete(&transcribe::Request {
    source: Source::Blob(file.blob),
    languages: vec!["vi".into()],
    model: None,
    speakers: Speakers::Off,
    hints: vec![],
    partials: false,
    words: false,
    max_seconds: None,
    levels: None,
}) {
    Ok(transcript) => lumi::alert(&transcript.text)?,
    Err(failure) if failure.kind == FailureKind::Unprepared => {
        transcribe::prepare("vi", Speakers::Off)?;
    }
    Err(failure) if matches!(failure.kind, FailureKind::Declined | FailureKind::Stopped) => {}
    Err(failure) => lumi::alert(&failure.message)?,
}
```

### `voices` [#voices]

```rust
pub mod voices {
    pub fn list() -> Result<Vec<Voice>, String>;
    pub fn remember(recording: u64, label: u32, name: &str) -> Result<Answer, String>;
    pub fn held(recording: u64) -> Result<Vec<HeldVoice>, String>;  // Lumi 1.65.0
    pub fn rename(id: &str, name: &str) -> Result<bool, String>;
    pub fn forget(id: &str) -> Result<bool, String>;
}

pub struct HeldVoice {
    pub label: u32,    // the speaker's label in the recording
    pub ms: u32,       // how much of the voice Lumi keeps, at most ten seconds
    pub enough: bool,  // enough to remember
}

pub struct Voice {
    pub id: String,                  // what a segment's speaker.voice names
    pub name: String,
    pub remembered_at: u64,          // milliseconds since 1970
    pub recognized_at: Option<u64>,  // when a recording last named it
}

pub enum Answer {
    Remembered(Voice),  // the person agreed
    Already(Voice),     // a voice you already keep: nobody was asked
    Cancelled,          // the person said no
}
```

The people your recordings name by their voice. Lumi keeps each one — a print
of how the person sounds and a few seconds of them speaking, encrypted, on this
Mac only — and you get its id and name, never the sound or the print. No other
extension recognizes a voice you remember, and the person can rename or forget
any of them under **Settings → AI → Voices**, whether your extension is
switched on or not; your windows hear
[`lumi:voices`](/extensions/windows#when-your-voices-change) when that happens.
Costs `voices`, declared beside `speech`. Needs Lumi 1.64.0.

* `remember` asks to remember voice `label` of recording `recording` — the id
  `transcribe::start` answered, or a transcript's `id` — as `name`. Lumi asks
  the person in its own panel at the pointer, with the voice's seconds to play
  first, and answers with what they chose. It needs at least five seconds of
  that voice, from a recording that told speakers apart, while Lumi still keeps
  what it heard: as it records, and for ten minutes after while your windows
  are open. A voice you already keep is `Already`, and nobody is asked. An
  extension keeps at most 64 voices. Only from a press.
* `held` (Lumi 1.65.0) says which voices of `recording` `remember` could take
  now, with how much of each Lumi keeps and whether that is `enough` — ask it
  before offering to save a voice, so the offer is off where Lumi no longer has
  the voice (ten minutes after the recording, once your windows closed, after
  Lumi restarted) or has too little of it yet. Labels and lengths only, never
  the sound.
* `rename` gives a voice another name, at most 64 characters, one line. Only
  from a press.
* `forget` lets go of a voice — its print, its seconds and its name — at any
  time. Segments already told keep the name they were told with.
* `list` is every voice you keep, in the order they were remembered.

Name somebody only when they know: Lumi's panel says so to the person too.

```rust
use lumi::voices::{self, Answer};

// From a press beside "Speaker 2" of the recording `id` your window is showing.
match voices::remember(id, 1, &typed_name)? {
    Answer::Remembered(voice) | Answer::Already(voice) => lumi::post("transcribe", &named(voice))?,
    Answer::Cancelled => {}
}
```

### `recordings` [#recordings]

```rust
pub mod recordings {
    pub fn start(request: &Request) -> Result<String, Failure>;
    pub fn stop(id: &str) -> Result<bool, String>;
    pub fn cancel(id: &str) -> Result<bool, String>;
    pub fn keep(recording: u64) -> Result<Kept, String>;
}

pub struct Kept {
    pub audio: String,              // every stream mixed into one channel
    pub sides: Option<String>,      // a call's sides apart: the microphone in the first channel, the Mac in the second
}

pub struct Request {
    pub source: Source,              // Microphone, SystemAudio(Playing) or Call(Playing)
    pub max_seconds: Option<u32>,    // None: until it is stopped; never past the person's longest recording
    pub levels: Option<u8>,          // bands of how loud, told as it is heard; None: nothing of it
}
```

A recording made into a file of your own. Lumi hears a source, compresses it,
and when the recording ends writes it among your blobs as an M4A file, under the
id it answered when the recording began — so you can keep that id with whatever
the recording belongs to before anything can go wrong. From then on it is an
ordinary [blob](#storage): read it with `storage::blob_read`, play it in a page
from `/__lumi__/blob/<id>` (Lumi answers ranges and says it is `audio/mp4`, so
an `<audio>` plays, seeks and changes speed), save it where the person chooses
with `storage::blob_save`, write it into one of your
[`[[folder]]`s](#folders) with `folders::write`, delete it with
`storage::blob_delete`. It counts in your storage like every blob — an hour of
one voice is about 14 MB, a call twice that. Costs `keep-audio`, and **recording
needs nothing of AI**: no `speech`, no model, nothing sent anywhere, and Lumi's
AI switches do not reach it. An extension that only records declares
`keep-audio` and the source it hears; the review sheet tells the person it keeps
recordings of what it hears, as audio files. Needs Lumi 1.65.0.

There are two ways to make one:

* `start` records what `request` hears, with no words heard, and answers the
  blob's id straight away. Lumi may still ask the person which app — for
  `Playing::OneApp`, which you are never told — or about the microphone first;
  your pages hear `recording-started` once it hears, `level` as it is heard
  where `levels` asked (0 bands for the loudness alone, at most 16; costs
  `audio-levels`, the meter [`transcribe`](#transcribe) uses), and `recorded`
  once the blob is written, or `recording-failed`
  ([`lumi:recordings`](/extensions/windows#when-a-recording-is-made)). It records
  until you `stop` it (kept) or `cancel` it (nothing kept), the person presses
  Stop on its row in Lumi's menu bar, it reaches `max_seconds` or your
  [longest recording](#your-longest-recording), no window of yours is left to
  hear it, the Mac sleeps or its screen locks, or you are
  switched off — each of those keeps what was heard but the cancel, and the
  event says which. It is never longer than one blob holds: hours of a voice.
  Costs `keep-audio` and the source's own word — `microphone`, `system-audio`,
  both for a call. One recording of yours at a time, a transcription included.
  Only from a command, a menu bar row or one of your windows. A failure is the
  [`transcribe`](#transcribe) `Failure`: `busy` while another of yours is going,
  `not-permitted` when macOS was refused.
* `keep` keeps what live transcription `recording` — the id `transcribe::start`
  answered — hears, from its first sample until it ends, on the transcript's own
  clock, so a segment's `start_ms` is its place in the file; it answers the
  blobs' ids. Call it in the same run as the start: Lumi holds the first seconds
  until it knows. `audio` is every stream mixed into one channel, so a call
  plays in both ears in any player — the file to play anywhere and to hand out.
  A call is also kept with its two sides apart in `sides` (Lumi 1.65.0), a file
  of two channels made from the same samples, as long as `audio`: the
  microphone in the first channel, what the Mac played in the second. Play it
  through Web Audio — a `ChannelSplitterNode` after a
  `MediaElementAudioSourceNode` — to treat the sides apart, muting the
  person's own side say; any ordinary player would put one side in each ear,
  which is why `audio` is there too. A source of one stream has no `sides`. A
  transcription stopped, capped or cut off by the Mac sleeping keeps what it
  heard; a cancelled one keeps nothing. Asked twice for one transcription, the
  same ids. Only from a press.

Until it ends the blob is not there — `blob_read` answers that there is no such
blob. If Lumi quits or the Mac loses power in the middle, what was heard up to
the last ten seconds or so is written as the blob the next time Lumi starts; if
your storage is full when it ends, it is written once there is room.

```rust
use lumi::recordings::{self, Request, Source};
use lumi::{storage, transcribe};

// A voice memo: record the microphone, nothing more.
let memo = recordings::start(&Request { source: Source::Microphone, max_seconds: None, levels: Some(0) })?;
// … on Stop:
recordings::stop(&memo)?;
// … and on Save Audio…, once your page heard `recorded`:
storage::blob_save(&memo, "Memo.m4a")?;

// A meeting: hear the call as words, and keep its audio — mixed, and its
// sides apart.
let id = transcribe::start(&request)?;
let kept = recordings::keep(id)?;   // kept.audio, and kept.sides for a call
```

```html
<!-- In a page: a click on a line said 61.25 s in plays from there, a bit faster. -->
<audio id="player" src="/__lumi__/blob/AUDIO_ID"></audio>
<script>
  player.currentTime = 61.25;
  player.playbackRate = 1.5;
  player.play();
</script>
```

### Your longest recording [#your-longest-recording]

How long a live recording of yours may run — a `start` here and a live
[`transcribe::start`](#transcribe) alike — is one number Lumi keeps for you. It
starts at your manifest's
[`longest-recording-minutes`](/extensions/manifest#extension) (30 where it says
none), and the person moves it in the Longest recording field on your
Permissions sheet. `longest_minutes` reads it; `set_longest_minutes` moves it,
1 to 480 minutes — for a Longest recording in your own settings, which the
sheet then shows, as it is one number. A recording already going keeps the
length it started with. Every page of yours that is open hears
[`lumi:longest-recording`](/extensions/windows#when-your-longest-recording-changes)
whenever it moves, from either side. Both need only `microphone` or
`system-audio`, not `keep-audio` — a transcription is ended by it too — and
answer from any run; setting it is refused while you are being uninstalled.
Needs Lumi 1.65.0.

```rust
use lumi::recordings;

// Your own settings' Longest recording.
let minutes = recordings::longest_minutes()?;
recordings::set_longest_minutes(120)?;
```

### `audio_levels` [#audio_levels]

```rust
pub mod audio_levels {
    pub fn start(request: &Request) -> Result<String, Failure>;
    pub fn stop(id: &str) -> Result<bool, String>;
}

pub struct Request {
    pub source: Source,   // Microphone, SystemAudio(Playing), Call(Playing) or Lumi
    pub bands: u8,        // frequency bands with each reading, 0 for the loudness alone; at most 16
}
```

How loud a source is, for a page to draw — a waveform, a spectrum, an orb — and
nothing else: no words heard, nothing kept, nothing sent. The readings are the
ones [`transcribe`](#transcribe) and [`recordings`](#recordings) tell, bounded so
they can never be the sound. `start` answers an id straight away; your pages hear
`started`, a `level` about eight times a second, and `ended` or `failed`
([`lumi:audio-levels`](/extensions/windows#when-a-source-is-heard-for-how-loud)).
It hears until you `stop` it, the person presses Stop on its row in Lumi's menu
bar — the menu bar says it is listening, as for any recording — no window of
yours is left, the Mac sleeps or its screen locks, or you are switched off.
Nothing is kept, so it is never capped by your longest recording. One live
recording of yours at a time, a transcription or a recording included. Costs
`audio-levels`. Needs Lumi 1.73.0.

`Source::Lumi` is what Lumi itself plays — your pages and the sites they show,
and every other extension's, which Lumi plays together — and nothing of any other
app. It is the one way to draw a site's sound, since nothing of yours reaches
into a site, and it costs nothing more and needs no press, so a page may start
it when its site starts playing. macOS asks the person once for Lumi to hear
audio, as it does for `system-audio`; on macOS 14.4 or later. The microphone,
the system audio and a call cost their own words besides — `microphone`,
`system-audio`, both — and only start from a command, a menu bar row or one of
your windows, as a recording does.

```rust
use lumi::audio_levels::{self, Request, Source};

// The site your window shows started playing: draw its sound.
let id = audio_levels::start(&Request { source: Source::Lumi, bands: 8 })?;
// … and it paused:
audio_levels::stop(&id)?;
```

## 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 — though a page of yours on screen is told, by a
[`lumi:profiles` event](/extensions/windows#when-the-profile-changes). 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()))?;
```

## Snippets [#snippets]

### `snippets::find(text, within)` and `snippets::expand(profile, snippet, text)` [#snippetsfindtext-within-and-snippetsexpandprofile-snippet-text]

```rust
pub fn find(text: &str, within: Within) -> Result<Vec<Hit>, String>
pub fn expand(profile: &str, snippet: &str, text: &str) -> Result<Expansion, String>

pub enum Within { Active, All, Only(Vec<String>) } // profile ids, as profiles() has them

pub struct Hit {
    pub profile: String,  // the profile it is in
    pub snippet: String,  // its id — what expand takes
    pub trigger: String,  // as written: literal text or a regular expression
    pub revision: String, // changes when anything deciding its expansion does
    pub fixed: bool,      // the same text every time: no {{$…}} variable, no script
}

pub struct Expansion {
    pub text: String,     // plain text, always
    pub sealed: bool,     // it holds something read from the clipboard
    pub revision: String,
    pub fixed: bool,
}
```

Ask about a whole text: is it one of the person's snippet triggers, and what
does it expand to. For text that came from somewhere other than the keyboard —
a copy in a history, a line in a document. Needs the `snippets` capability and
Lumi 1.31.0.

A trigger matches as typing would match it: with the snippet's own case
sensitivity, and a regular expression covering the whole text, first character
to last. A snippet counts when it is switched on and has a trigger; its
applications, the profile's Snippets switch and when it expands are about
typing, and are not asked — so a snippet set to expand **Never from typing**
is found like any other. `find` answers profile by profile, in the order Lumi lists them, then in
each profile's own order. A text longer than 64 characters — more than a trigger
can be typed in — matches nothing. Trim it first if surrounding spaces should
not count.

`expand` renders the snippet the way typing it would: its `{{$…}}` variables
filled in, its script run against what its pattern caught. An expansion with
`fixed` set is the same every time — keep it under its `revision`, and only as
long as `find` answers that revision. Anything else is fresh on each call. A
snippet that reads the clipboard is expanded only for an extension that also
declares `clipboard`.

Nothing tells your component when the snippets change, but a page of yours on
screen is told — a [`lumi:snippets` event](/extensions/windows#when-your-settings-or-the-snippets-change)
— so a window showing an expansion can ask again then.

```rust
use lumi_extension_api::snippets::{self, Within};
for hit in snippets::find(";addr", Within::All)? {
    let expansion = snippets::expand(&hit.profile, &hit.snippet, ";addr")?;
    lumi::alert(&expansion.text)?;
}
```

## Folders [#folders]

```rust
pub mod folders {
    pub fn status(name: &str) -> Result<Option<Chosen>, String>;
    pub fn choose(name: &str) -> Result<Option<Chosen>, String>;
    pub fn forget(name: &str) -> Result<(), String>;
    pub fn write(name: &str, file_name: &str, blob: &str) -> Result<(), Problem>;
    pub fn reveal(name: &str) -> Result<(), String>;
}

pub struct Chosen { pub label: String } // the folder's own name, such as "Desktop"
pub enum Problem { NotChosen, Gone, Refused(String) }
```

Write files into a folder the person chose, without asking where each time —
a screenshot saved straight to a folder, an export, a backup. Each folder is
one your manifest declares with [`[[folder]]`](/extensions/manifest#folder);
a name it does not declare is refused. No capability: the declaration is the
grant, and the review sheet lists it. Needs Lumi 1.34.0.

**The person chooses, and Lumi keeps it.** `choose` opens Lumi's Open panel,
worded with your extension's name and the folder's label, and answers the
folder's name — or `None` when they cancel, which keeps the earlier choice.
They can also choose, change or forget it from your extension's Permissions
sheet. You are told the folder's name and never its path, and Lumi follows the
folder through a rename or a move. `status` answers what is chosen now — for
a line like "Save to Desktop" on your page. A folder declared with a
`default` answers that folder ("Pictures") until the person chooses, and
again after `forget`.

**Write only, never over anything.** `write` puts a blob from your storage
into the folder as a new file. `file_name` follows `blob_save_dated`'s rules:
one name, no `/`, not starting with `.`, with every `{date}` and `{time}`
filled in from the Mac's clock. A name already in the folder is written as
"Name 2", "Name 3". Any size of blob works, since the bytes never pass through
your component. There is no read, list, delete or replace.

**Two answers worth handling.** `Problem::NotChosen` — nothing chosen yet, and no default —
and `Problem::Gone` — the folder chosen is no longer there — are the two
cases where asking this once is the right thing, so fall back to
`blob_save_dated`, or offer `choose`. Lumi never makes a folder that is gone
again.

```rust
use lumi::folders::{self, Problem};

let name = "Screenshot {date} at {time}.png";
match folders::write("saves", name, &shot.blob) {
    Ok(()) => lumi::alert("Saved")?,
    Err(Problem::NotChosen | Problem::Gone) => lumi::storage::blob_save_dated(&shot.blob, name)?,
    Err(Problem::Refused(said)) => return Err(said),
}
```

`choose`, `write` and `reveal` answer only a command the person ran or a press
in one of your windows, never `on_event` or a lifecycle hook. `status` and
`forget` work from any of those too, but not from an event either: they need
the way to your extension's windows, which an event's run does not have.

## Files [#files]

```rust
pub mod files {
    pub fn open(types: &[&str], multiple: bool, message: &str) -> Result<Vec<Opened>, String>;
    pub fn save(blob: &str, name: &str) -> Result<bool, String>;
}

pub struct Opened { pub name: String, pub blob: String }
```

Read in files the person picks, in Lumi's Open panel — a picture to edit, a
document to import. `types` are uniform type identifiers (`public.image`,
`public.png`, `com.adobe.pdf`); every other file is greyed out, and an empty
list lets any file through. `message` is the line above the list, after your
extension's name, so the panel never reads as Lumi asking for itself. The
answer is the files in the order picked — empty for Cancel — each written into
your [storage](#storage) as a blob, with its own name and extension. Needs Lumi
1.36.0.

**The person picking is the grant.** No capability: each file was chosen by
hand, and you learn its name and bytes, never the folder it was in or anything
beside it. So `open` answers only a command the person ran or a press in one
of your windows — never `on_event` or a lifecycle hook — and their time
choosing is not counted against your run.

**All or nothing.** Each file may be at most one blob's limit, sized before it
is read. A file too big, one that cannot be read, or one more than your store
has room for refuses the whole call, and the files read before it are deleted
again — a refusal never leaves a blob you were not told about.

```rust
use lumi_extension_api::files;

let picked = files::open(&["public.image"], false, "Open a picture to edit")?;
if let Some(file) = picked.first() {
    // `file.blob` is in your storage; `file.name` is "Holiday.heic".
}
```

`save` is the other direction, and the one to use when what you do next
depends on the file having been saved — closing the tab or window the picture
was saved from. Lumi's Save panel comes up as it does for
[`blob_save_dated`](#storage), `{date}` and `{time}` filled in the same way,
and the call **waits for the answer**: `true` once the file is written, `false`
when the person cancels. Where it went is still theirs and is never told back,
and a write that fails is this call's error rather than an alert. Only for a
press, like `open`, and their time choosing is not counted against your run.
Needs Lumi 1.36.0.

```rust
if files::save(&blob, "Screenshot {date} at {time}.png")? {
    // Written: safe to close what it came from.
}
```

## Is it you? [#is-it-you]

```rust
pub mod owner {
    pub fn available() -> Result<bool, String>;
    pub fn authenticate(reason: &str) -> Result<bool, String>;
}
```

macOS's own "is it you?" dialog — Touch ID, an Apple Watch or the login
password — before you show or do something only the Mac's owner should:
show something you keep hidden, switch one of your protections off, delete
for good. It is the dialog a page asks for with
[`POST /__lumi__/authenticate`](/extensions/windows), for the runs that
have no page to ask from: a command run from its shortcut, a row of your
[menu](#menu), or a request from one of your windows that your component
answers. Needs Lumi 1.37.0.

`reason` finishes macOS's sentence "Lumi is trying to …", and Lumi adds your
extension's name after it, so `authenticate("delete every saved item")` reads
"Lumi is trying to delete every saved item (Your Extension)". One line, at most
120 characters. The call **waits for the person**: `true` once they confirm,
`false` when they cancel, fail, or leave it for two minutes.

**Only the answer comes back.** Lumi keeps nothing of it: what a yes unlocks,
and for how long, is yours to decide and to forget again. An error is never a
yes — another dialog is up (one at a time across Lumi), the reason is not one
line, or the dialog cannot be shown on this Mac — so treat it as a no.
`available` says beforehand whether the dialog can be shown at all, without
showing it, so you offer what needs it only where it works.

**Only for a press.** No capability: the person pressing is the grant. So
`authenticate` answers a command, a menu bar row or one of your windows —
never `on_event` or a lifecycle hook, where nobody is there to answer — and
their time answering is not counted against your run. Asked from a request
one of your windows sent, the dialog is that window's, as if its page had
asked for it itself: a [panel](/extensions/windows#a-panel) stays up
under it and has the keyboard back after.

```rust
use lumi_extension_api::owner;

if owner::authenticate("delete every saved item")? {
    // The person at the Mac is its owner: do it.
}
```

## The screen [#the-screen]

```rust
pub mod screen {
    pub fn permitted() -> Result<bool, String>;
    pub fn capture(target: Target) -> Result<Shot, String>;
    pub fn capture_as(target: Target, encoding: Encoding) -> Result<Shot, String>; // Lumi 1.34+
    pub fn select_area() -> Result<Option<Rect>, String>;
    pub fn select_window() -> Result<Option<PickedWindow>, String>;
    pub fn about_window(window: Option<u32>) -> Result<Option<WindowInfo>, String>; // Lumi 1.34+
}

pub enum Target {
    Display,         // the whole display the pointer is on
    Window(Window),  // one window
    Area(Rect),      // an area, in points
}
pub struct Window { pub id: Option<u32>, pub shadow: bool }
pub struct PickedWindow { pub id: u32, pub frame: Rect }
pub struct WindowInfo {
    pub id: u32,                    // capture by it
    pub app: String,                // "Safari"
    pub bundle_id: Option<String>,  // "com.apple.Safari"
    pub title: Option<String>,      // needs Screen Recording
}
pub struct Rect { pub x: f64, pub y: f64, pub width: f64, pub height: f64 }

pub struct Shot {
    pub blob: String,  // the picture, in your storage
    pub width: u32,    // in pixels
    pub height: u32,
    pub scale: f64,    // pixels per point
    pub frame: Rect,   // where on screen it came from, in points
}

pub struct Encoding { pub format: Format, pub quality: u8, pub resolution: Resolution }
pub enum Format { Png, Jpeg }
pub enum Resolution { Native, Points }
```

Pictures of the screen. Needs the `screen` capability and Lumi 1.30.0, and the
Mac needs macOS 14 or later — below it every call is refused, with a sentence
saying so.

**Screen Recording is Lumi's permission, not yours.** macOS grants it to Lumi,
once, for every extension that declares `screen`. You do not have to ask first:
without it, `capture`, `select_area` and `select_window` are refused, and Lumi
asks the person itself. `permitted` answers whether Lumi holds it now —
right even for a permission given since Lumi started — for a status line, say.
To ask before the first capture, see [Permissions](#permissions).

**Only for a press.** `capture`, `capture_as`, `select_area`,
`select_window` and `about_window` answer a command the person ran or a press in one of your
windows, never `on_event` or a lifecycle hook. A picture of the screen is not
taken behind anybody's back.

### Taking a picture [#taking-a-picture]

```rust
use lumi::screen::{self, Target, Window};

let shot = screen::capture(Target::Window(Window { id: None, shadow: false }))?;
```

* `Display` is the whole display the pointer is on.
* `Window` with `id: None` is the frontmost ordinary window that is not
  Lumi's; with an id, the window [`select_window`](#letting-the-person-choose)
  answered. An application's window is all of it, its rounded corners
  transparent. `shadow: true` takes the shadow macOS draws round the window
  too, as far as it reaches; `frame` then is the window and its shadow. A
  window macOS draws no shadow for is taken without, and its `frame` is its
  own. A window that has closed since is refused.
* `Area` is clipped to the one display it covers most of.

Coordinates are points, with the origin at the top-left of the main display
and `y` going down — the space macOS reports every display and window in.
**Lumi's own windows are never in the picture**, yours included: the window of
yours that is up when somebody presses Capture is the tool, not the work.
Lumi's Settings window is put away as well: before `select_area` or
`select_window` asks the person to choose, and once a `capture` has been
taken, so nothing on screen stands in front of what the picture holds. It is
left up when the call comes from one of your pages shown inside Settings, and
when the capture is refused because Screen Recording is off, since that is
where Lumi asks for it. Your own windows are yours to put away.

The picture goes into [your storage](#storage) as a blob, not across to your
component — a Retina display is megabytes. Show it in a page at
`GET /__lumi__/blob/<id>`, put it on the clipboard as `Data::Blob`, or hand it
to the person with `blob_save`. It counts against your storage until you
delete it, so delete what you no longer need.

### Points and pixels [#points-and-pixels]

`width` and `height` are pixels; `frame` is points; `scale` is how many pixels
make a point. On a Retina display `scale` is 2, so a window 800 points wide is
a picture 1600 pixels wide. That is what `capture` gives you: every pixel the
display has, as a PNG.

### Writing it as JPEG, or at screen size [#writing-it-as-jpeg-or-at-screen-size]

`capture_as` takes the picture already written the way you want it, so an
extension with no page on screen can copy or save it as it stands.

```rust
use lumi::screen::{self, Encoding, Format, Resolution, Target};

let shot = screen::capture_as(
    Target::Display,
    Encoding { format: Format::Jpeg, quality: 85, resolution: Resolution::Points },
)?;
```

* `Format::Png` keeps transparency: a window's corners and shadow stay
  transparent. `public.png` on the clipboard.
* `Format::Jpeg` is smaller, at `quality` from 1 to 100 (outside that range it
  is brought inside; it is not read for PNG). JPEG holds no transparency, so
  the picture is laid on white first. `public.jpeg` on the clipboard.
* `Resolution::Native` is the display's own pixels, as `capture` takes them.
* `Resolution::Points` is one pixel a point — the size the picture had on
  screen, a quarter of the pixels on a Retina display. `scale` is then 1.

`capture_as` follows every rule `capture` does. Needs Lumi 1.34.0 — say so
with `min-lumi-version`.

### Letting the person choose [#letting-the-person-choose]

`select_area` dims every display and turns the pointer into a crosshair; the
area the person drags is the answer. `select_window` dims them too and lights
up the window under the pointer; the one they click is the answer. Applications'
windows, the menu bar, its items and the Dock are offered, and Lumi's own
windows never are. Both answer `None` when the person presses Escape,
right-clicks, or leaves it for two minutes.

Neither takes a picture — hand what they answer to `capture`. Both are refused
at once without Screen Recording, so nobody drags out an area that cannot be
captured, and the time the person takes is not counted against your run.

```rust
use lumi::screen::{self, Target, Window};

if let Some(picked) = screen::select_window()? {
    let shot = screen::capture(Target::Window(Window { id: Some(picked.id), shadow: true }))?;
}
```

### Whose window it is [#whose-window-it-is]

`about_window` says which application a window belongs to, so a picture can be
labelled or named by it — "Safari · 14:32", `Safari 2026-10-04.png`. Give it
the id `select_window` answered, or `None` for the window in front: the one
`capture` takes for a `Window` with no id.

```rust
use lumi::screen::{self, Target, Window};

if let Some(front) = screen::about_window(None)? {
    let shot = screen::capture(Target::Window(Window { id: Some(front.id), shadow: false }))?;
    // front.app is "Safari", front.title the window's own title
}
```

**Capture by the id it answers.** The front window comes back with its id;
capturing by that id takes the very window just described, even if another
came to the front in between. Asked the other way round — capture the front
window, then ask whose it is — the answer can belong to a different window.

* `app` is the application's own name, as the Dock and ⌘-Tab show it.
* `bundle_id` is `None` for a process that is not an application bundle.
* `title` is the window's title when it has one, and `None` without Screen
  Recording: macOS keeps titles behind that permission. Nothing else needs it.
* `None` back means the window is no longer on screen. Only the windows
  `select_window` offers are described — never Lumi's own.

Takes no picture. Only for a press, as `capture`. Needs Lumi 1.34.0.

## Text in a picture [#text-in-a-picture]

```rust
pub mod ocr {
    pub fn read(source: &Source, options: &Options) -> Result<Reading, String>;
    pub fn languages(level: Level) -> Result<Vec<String>, String>;
}

pub enum Source {
    Blob(String),    // a picture in your storage
    Screen(Target),  // the screen, taken now and not kept — screen::Target
}

pub struct Options {               // Options::default() is Lumi's own reading
    pub level: Level,              // Accurate (default) or Fast
    pub languages: Vec<String>,    // best first, e.g. "vi-VT"; empty = detect
    pub language_correction: bool, // default on
    pub minimum_text_height: Option<f32>, // share of the picture's height, 0–1
    pub alternatives: u8,          // other readings per line, up to 9
    pub words: bool,               // a box per word too
}

pub struct Reading {
    pub text: String,              // every line, joined with \n
    pub lines: Vec<Line>,          // top to bottom
    pub width: u32,                // the picture, in pixels
    pub height: u32,
    pub frame: Option<Rect>,       // Screen: where it came from, in points
    pub scale: Option<f64>,        // Screen: pixels per point
    pub truncated: bool,           // lines past 1 MB of text left out
}

pub struct Line {
    pub text: String,
    pub confidence: f32,           // 0–1
    pub frame: Rect,               // upright box, in the picture's pixels
    pub corners: Vec<Point>,       // top-left, top-right, bottom-right, bottom-left
    pub words: Vec<Word>,          // empty unless options.words
    pub alternatives: Vec<Alternative>,
}
pub struct Word { pub text: String, pub frame: Rect }
pub struct Alternative { pub text: String, pub confidence: f32 }
pub struct Point { pub x: f64, pub y: f64 }
```

The text in a picture and where each line of it is, read by macOS's own
recogniser — the same one Lumi reads copied images with, Vietnamese included.
Lumi hands back everything it reads; what the text is for is up to you. Needs
Lumi 1.34.0 and macOS 13 or later; reading the screen needs macOS 14, as
[the screen](#the-screen) does.

**What a read costs depends on the picture.** A picture already in your
storage is yours: reading it needs no capability, only a call from the run's
budget, and works from anywhere — `on_event` and the lifecycle hooks included.
Reading the screen is taking a picture of it, so it costs exactly what
`screen::capture` costs: the `screen` capability, Screen Recording (Lumi asks
for it when it is off), and only from a command or one of your windows.

```rust
use lumi::ocr::{self, Options, Source};
use lumi::screen::{self, Target};

// Read what the person drags out, and keep nothing.
if let Some(area) = screen::select_area()? {
    let reading = ocr::read(&Source::Screen(Target::Area(area)), &Options::default())?;
    lumi::set_clipboard_text(&reading.text)?;
}

// Keep the picture and draw a box round every word on it.
let shot = screen::capture(Target::Display)?;
let reading = ocr::read(&Source::Blob(shot.blob.clone()), &Options { words: true, ..Options::default() })?;
```

### Where the text is [#where-the-text-is]

Every box is in **the picture's pixels**, with the origin at its top-left and
`y` going down — the space a page draws the picture in, so a box goes straight
over an `<img>` of the same blob. `frame` is the upright box round a line;
`corners` are the line's own four corners, which differ from `frame` when the
text is slanted. A picture of the screen also answers `frame` and `scale`: a
point on screen is `frame.x + x / scale`, `frame.y + y / scale`.

A word is a run of a line between spaces, so a line of Chinese or Japanese is
one word. Asking for words costs the recogniser one more look per word; leave
`words` off unless you draw them.

### What the options do [#what-the-options-do]

* `Level::Accurate` reads thirty languages, Vietnamese among them, in half a
  second to two seconds a screenshot. `Level::Fast` is several times quicker
  and reads a few Latin languages only — no Vietnamese, no Chinese or
  Japanese. `languages(level)` lists what each reads.
* `languages` empty lets the recogniser tell, which is right for most
  pictures. Name languages when you know them better than it does.
* `language_correction` puts a half-seen word right from a dictionary: on for
  prose, off for codes, names and numbers.
* `minimum_text_height` is the smallest text read, as a share of the picture's
  height. Left `None`, it is the recogniser's own for a picture (about 1/32)
  and 1/100 for the screen, where text is the size of an interface's.
* `alternatives` hands back up to nine other readings of each line, likeliest
  first; a line's own text is never among them.

**`confidence` is not a filter.** Measured, a wrong line has scored 1.00 and a
right one 0.30. It is there to show, not to decide which lines to keep.

A blob that is not a PNG, TIFF, JPEG or HEIC is refused, and so is a picture
over 64 megapixels. A photo is read as its pixels are stored: an orientation
its metadata asks for is not applied. Reading counts against the run like
every call, and at most 1 MB of text comes back — `truncated` says when lines
were left out.

## Permissions [#permissions]

macOS permissions belong to Lumi — macOS credits the app, never an extension
inside it — so you ask Lumi, and Lumi asks macOS. Each permission needs the
capability it is for: `Permission::ScreenRecording` needs `screen`. Needs Lumi
1.33.0 or later — say so with `min-lumi-version`.

You often need none of this: without Screen Recording, `screen::capture`,
`select_area` and `select_window` are refused and Lumi opens its own sheet by
itself. Use it to ask *before* — on a Welcome page, say.

```rust
pub fn check(permission: Permission) -> Result<State, String>
pub fn ask(permission: Permission) -> Result<(), String>
pub fn request(permission: Permission) -> Result<Requested, String>
pub fn open_system_settings(permission: Permission) -> Result<(), String>

pub enum Permission { ScreenRecording }
pub enum State { Granted, NotGranted }
pub enum Requested { Asked, AlreadyGranted, FlowEditorOpen, NoWindow }
```

### Lumi's sheet, in one call [#lumis-sheet-in-one-call]

`ask` opens Lumi's Settings on a sheet with your extension's name. It explains
macOS's three questions — which button to choose on each — and notices on its
own when the permission is switched on. It returns as soon as the sheet is up,
and does nothing when Lumi already holds the permission.

```rust
use lumi::permissions::{self, Permission};

permissions::ask(Permission::ScreenRecording)?;
```

### Your own page [#your-own-page]

Explain it in your own words in one of your windows, then ask macOS from
there. Say first that macOS's first button is **Deny** and the one to choose
is **Open System Settings**.

```rust
use lumi::permissions::{self, Permission, Requested, State};

let screen = Permission::ScreenRecording;
if permissions::check(screen)? == State::NotGranted {
    match permissions::request(screen)? {
        Requested::Asked | Requested::AlreadyGranted => {}
        // Not asked — Lumi's sheet says why and waits it out.
        Requested::FlowEditorOpen | Requested::NoWindow => permissions::ask(screen)?,
    }
}
```

* `request` asks only from one of your windows. A panel never makes Lumi the
  app in front, and macOS lists only an app that asked from a window: from a
  panel, or with no window up, it answers `NoWindow`.
* macOS shows its dialog **once** in Lumi's life. After that, asking is silent
  and the switch is in System Settings: `open_system_settings` opens that list.
* `FlowEditorOpen`: Lumi's flow editor has unsaved changes, and switching the
  permission on can offer to quit Lumi, which would stop to ask about them. A
  saved flow left open does not count.
* `check` is a question and works anywhere. `ask`, `request` and
  `open_system_settings` answer only a command or a press in one of your
  windows — never `on_event` or a lifecycle hook.

## Menu [#menu]

Your extension can put rows of its own in Lumi's menu bar menu. Declare
`menu` in `capabilities` and set a tree with `lumi::menu::set`. It needs
Lumi 1.33.0, so set `min-lumi-version`.

Lumi draws your tree in a section of its own, after Recent, under a dimmed
**Extensions** label. If the tree is
one plain item, Lumi shows that item on its own with your icon beside it, so
its label has to say what it does. Any other tree becomes a submenu with your
extension's name and icon. None of your rows go among Lumi's own.

```rust
use lumi::menu::{self, Entry};

fn draw(target: &str) -> Result<(), String> {
    menu::set(&[
        Entry::item("translate", "Translate Selection"),
        Entry::separator("s1"),
        Entry::submenu("to", "Translate To"),
        Entry::check("to.vi", "Vietnamese", target == "vi").under("to"),
        Entry::check("to.en", "English", target == "en").under("to"),
        Entry::item("status", "Last run: 2 min ago").disabled(),
    ])
}
```

Lumi keeps the last tree you set, across restarts and updates. It draws the
tree without running your code, so one `set` from `on_lifecycle` on
`Installed` and `Updated` is enough. Set it again whenever a row should
change. Switching the extension off hides your rows, and uninstalling removes
them. `menu::clear()` takes every row of yours out of the menu until the next
`set`, as `set` with an empty list does — for an extension whose rows are a
setting the person can switch off. Nothing writes the menu from
`Uninstalling`: the rows go with the extension anyway.

A row can carry an icon. Give it one with `.icon(…)` and set the tree with
`menu::set_rows` in place of `menu::set`:

```rust
menu::set_rows(&[
    Entry::item("capture", "Capture an Area").icon("scan"),
    Entry::submenu("to", "Translate To").icon("globe"),
    Entry::check("to.vi", "Vietnamese", true).under("to"),
    Entry::separator("s1"),
    Entry::item("upload", "Upload").icon("icons/upload.svg"),
])
```

An icon is spelled the way a [`[[command]]`](/extensions/manifest#command)
icon is: the name of a Lucide icon from Lumi's set, or an `.svg` under your
`ui/` of at most 64 KB. Lumi draws it in the menu's own ink, light or dark,
whatever colours the file has. A name Lumi does not have, or a file that has
gone, leaves the row without an icon rather than without the row. Items and
submenus take one. A check row is refused one, since its tick sits where the
icon would, and so is a separator. A tree that is one plain item still wears
your extension's icon, since it sits among the top-level rows. `set_rows`
needs Lumi 1.34.0; `set` refuses an entry with an icon, so a tree without
icons keeps working on 1.33.

When someone presses a row, Lumi calls `run_ui` with the window `:menu`, and
`menu::pressed` reads which row it was. A press counts as something the
person did, so the handler may open a window, paste, or ask for a
permission:

```rust
fn run_ui(window: String, request: String) -> Result<String, String> {
    if let Some(id) = menu::pressed(&window, &request) {
        // … act on `id`, then set the tree again if a row changed …
        return Ok(String::new());
    }
    // … your windows' requests …
}
```

The rules:

* At most 64 entries.
* Every id must be unique and 1 to 64 bytes long. A separator needs an id
  too.
* A `parent` must name a submenu listed earlier in the list.
* You can nest at most two levels of submenu below your own.
* A submenu with nothing in it is not drawn.
* Lumi never ticks or unticks a check row on its own. The tick shows what
  you last set.

### Whether your rows show [#whether-your-rows-show]

One switch decides whether your rows are drawn, kept by Lumi and on until
it is turned off. Two places move it, and they are the same switch:

* your own settings, if you offer a "Show in the menu bar" — with
  `menu::set_shown(on)`;
* the **Menu bar** switch on your Permissions sheet under Extensions, which
  Lumi draws for every extension with `menu`, so one without a setting of
  its own still has it.

While it is off your rows are not drawn. `menu::set` and `set_rows` go on
keeping your tree, so it comes back exactly as you left it when the switch is
on again. Switching your extension off hides the rows too, and leaves the
switch as it was.

Read it with `menu::shown()` to draw your own setting where the switch
stands. To hear when the person moves it on the sheet, declare
`events = ["menu-shown"]`: [`on_event`](#on_eventname-payload) gets
`{"v":1, "shown", "at"}` — never for your own `set_shown` — and
[`post`](#postwindow-message) can pass it on to a window that is open:

```rust
fn run_ui(window: String, request: String) -> Result<String, String> {
    match request.as_str() {
        // Your page asks how to draw its "Show in menu bar" row…
        "menu" => Ok(format!(r#"{{"shown":{}}}"#, menu::shown()?)),
        // …and sets it, the same switch as Lumi's sheet.
        "show-menu" => menu::set_shown(true).map(|()| String::new()),
        "hide-menu" => menu::set_shown(false).map(|()| String::new()),
        _ => Err(format!("unknown request from {window}")),
    }
}
```

`menu::shown()`, `"menu-shown"` and `open_permissions` need Lumi 1.44.0;
`menu::set_shown` needs 1.45.0. Before 1.45 the switch was Lumi's alone, and
an extension could only read it.

## Menu bar item [#menu-bar-item]

An item of your own in the menu bar, beside Lumi's icon: an icon, a few
characters, or both — a state somebody wants at a glance without opening a
menu, the way the Mac shows the battery or the input source. Needs the
`menu-bar-item` capability and Lumi 1.61.0.

```rust
use lumi::menu_bar_item::{self, Item};

// An icon from Lumi's Lucide set, with two letters beside it…
menu_bar_item::set(&Item::icon("globe").text("VI").tooltip("Typing in Vietnamese"))?;
// …an .svg of your own under ui/…
menu_bar_item::set(&Item::icon("icons/on.svg"))?;
// …or text alone.
menu_bar_item::set(&Item::text_only("15/9"))?;
// Gone from the menu bar until you set it again.
menu_bar_item::clear()?;
```

`menu_bar_item::set(&item)` draws your item, or changes it; the same item
again is no change, so you may set it from every run of an event that may
or may not have changed what it shows. An item needs an icon, text, or both:

* `Item::icon(name)` — a Lucide icon's name from Lumi's set, or an `.svg`
  under your `ui/` of at most 64 KiB, spelled as a `[[command]]` icon is.
  Drawn 18 points tall in the menu bar's own ink, whatever colours the file
  has, and no wider than twice its height. A file that is not in the
  package is refused when you set it.
* `Item::text_only(text)`, or `.text(text)` beside an icon — at most 16
  characters, on one line.
* `.tooltip(text)` — what a pointer resting on it reads, at most 120
  characters. Your extension's name without one.
* `.frames(&icons, per_second)` — the icon played as a loop, to show that
  something is going on, never what: a recording, a sync. From 2 to 24
  frames, each spelled as `Item::icon` is, and from 1 to 12 a second. Lumi
  plays them on its own, with none of your code running per frame, and draws
  the item's icon still while Reduce Motion is on — the first frame when the
  item has none. Setting the item again with the same frames and other text
  keeps the loop going where it was, so a clock beside it may change every
  second; an item set without frames stops it. Needs Lumi 1.65.0.

```rust
let wave: Vec<String> = (1..=12).map(|k| format!("icons/wave-{k}.svg")).collect();
let wave: Vec<&str> = wave.iter().map(String::as_str).collect();
menu_bar_item::set(&Item::icon("audio-lines").text("12:34").frames(&wave, 8))?;
```

Lumi keeps the last item you set, on this Mac, across restarts and updates,
and draws it with none of your code running — set it when what it shows
changes, from a command, an event, a timer or `on_lifecycle`. Switching your
extension off takes it away; uninstalling forgets it. A loop is the one part
it does not keep: after Lumi restarts, or your item is hidden and shown, it
comes back still until you set the loop again.

**A press opens a menu Lumi draws.** Your extension's name first, then your
[menu](#menu) tree when you declare `menu` too — the same rows, told to you
the same way, through `menu::pressed` — and last &#x2A;*Settings…**, onto your
Settings tab in Lumi. When your page draws no Settings tab — nothing to set,
or `[tabs.settings] hidden` — that row reads &#x2A;*Open in Lumi…** and opens your
page on the tab it opens on by itself.

Lumi's last row is always last. To place a settings row of your own — with
rows below it, or worded your way — leave Lumi's out with
`menu-bar-item-settings = false`, add the row to your tree, and answer its
press with [`open_settings()`](#open_settings), which a
press on a menu row may call. Your tree is the one Lumi's own menu shows too;
hide it there with `menu::set_shown(false)` if it belongs to the item alone. When your rows already say whose they are, leave the name out with
[`menu-bar-item-heading = false`](/extensions/manifest) in your manifest; when
they already offer your settings, leave the last row out with
[`menu-bar-item-settings = false`](/extensions/manifest) — kept anyway while
your menu has no enabled row of its own, so a click never opens onto nothing. Your rows are there whatever the person's Menu bar switch says:
that one is about Lumi's own menu, and this menu is your item's. There is no
press of the item itself: what a press does is what a row says.

**Or a panel of your own.** Declare a panel `position = "menu-bar-item"` and
a click on the item opens it under the item instead — switches, a calendar,
anything your page draws — while the menu above answers a right-click, a
⌃-click and VoiceOver. See [A panel under your menu bar
item](/extensions/windows#a-panel-under-your-menu-bar-item). And name one of
your commands with [`menu-bar-item-right-click`](/extensions/manifest) for a
right-click to run it instead — a quick switch — leaving the menu to
VoiceOver alone. Needs Lumi 1.61.0.

**The person decides where it stands.** They can ⌘-drag it anywhere on the
menu bar, and it keeps that place across restarts, updates and its being
switched off and on.

**macOS decides what fits.** A menu bar too full for every item hides the
ones that do not fit, without telling them — beside a notch, most often —
and your item is one item among the others, Lumi's own icon included. The
person decides which stay: by ⌘-dragging, with the switch on your Permissions
sheet, or in macOS's own Menu Bar settings. Keep it narrow: an icon, or a
few characters.

### Whether your item shows [#whether-your-item-shows]

The **Show its icon in the menu bar** switch on your Permissions sheet,
kept by Lumi and on until it is turned off — `menu::set_shown`'s shape: your
own settings move the same switch with `menu_bar_item::set_shown(on)`, and
`menu_bar_item::shown()` reads it. While it is off your item is not drawn;
`menu_bar_item::set` goes on keeping it, so it comes back as you left it. To
hear when the person moves it, declare `events = ["menu-bar-item-shown"]`:
[`on_event`](#on_eventname-payload) gets `{"v":1, "shown", "at"}`, never for
your own `set_shown`.

## Timers [#timers]

Lumi can run your extension at times it sets — with every one of your windows
closed, and after Lumi restarts. Declare `timers` in `capabilities` and
`"timer"` in `events`, set a timer with `lumi::timers::set`, and Lumi calls
your [`on_event`](#on_eventname-payload) with `"timer"` when it comes due.
It needs Lumi 1.59.0, so set `min-lumi-version`.

```rust
use lumi::timers::{self, Clock, Repeat, When};

fn on_lifecycle(event: lumi::Lifecycle) -> Result<(), String> {
    match event {
        // Every day as the Mac's clock reaches 00:00.
        lumi::Lifecycle::Installed | lumi::Lifecycle::Updated(_) => {
            timers::set("day", When::Clock(Clock { date: None, hour: 0, minute: 0 }), Repeat::Daily)
        }
        lumi::Lifecycle::Uninstalling => Ok(()),
    }
}

fn on_event(name: String, payload: String) -> Result<(), String> {
    if let Some(fired) = timers::fired(&name, &payload) {
        if fired.name == "day" {
            // … redraw what names today …
        }
    }
    Ok(())
}
```

A timer is a name of yours, a `When` and a `Repeat`:

| `When`                                      | Fires                                                                                                              |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `When::After(ms)`                           | this many milliseconds from now                                                                                    |
| `When::At(ms)`                              | at this moment, in milliseconds since the Unix epoch                                                               |
| `When::Clock(Clock { date, hour, minute })` | when the Mac's clock shows `hour`:`minute` — on `date` when it has one, otherwise the next time the clock shows it |

| `Repeat`            |                                                                         |
| ------------------- | ----------------------------------------------------------------------- |
| `Repeat::Once`      | fires once, then is gone                                                |
| `Repeat::Every(ms)` | every this many milliseconds from the first firing, at least 30 seconds |
| `Repeat::Daily`     | every day at the time of a `When::Clock` without a `date`               |

`timers::set(name, when, repeat)` replaces any timer of that name.
`timers::cancel(name)` removes one and answers whether there was one.
`timers::list()` answers every timer still to fire, by name, each as it was
set — an `After` reads back as the `At` it named — with `due`, when it next
fires in milliseconds since the Unix epoch. A timer firing once is not listed
while it is being told.

Lumi keeps your timers on the Mac, across restarts and updates. Switching your
extension off holds them, and uninstalling removes them. Lumi never learns
what one is for: the name is handed back exactly as you wrote it, so keep
anything secret in [`storage`](#storage), not in a name.

A `Clock` time is read on the Mac's own clock, in the person's time zone, and
moves when the zone or the clock changes. A time the clocks skip that day —
the hour summer time leaves out — fires at the first minute after it; one that
comes twice fires the first time. Your component's own clock stays UTC, but a
timer set on the Mac's clock tells you where the person is, which is one reason
timers need a capability. To read the zone itself, see
[Time zone](#time-zone).

What one firing tells you is in [the `timer` event](#on_eventname-payload),
and `timers::fired` reads it into a `Fired` — `name`, `at`, `late` and
`missed`:

* **Late, once.** A timer that came due while the Mac slept, Lumi was quit or
  your extension was off fires once, as soon as it can, with `late` set.
  However many repeats came due meanwhile are folded into that one firing and
  counted in `missed`. A repeating timer whose last firing you are still
  hearing is not told again on top of it.
* **At least once.** A firing Lumi could not finish telling — Lumi quit part
  way, or every run your extension may have was taken — is told again. A run
  that must not act twice keeps its own note of what it did.
* **No windows.** It runs under [`on_event`](#on_eventname-payload)'s rules:
  no window opens from it and `paste` is refused. `alert`, `post` to a window
  that is open, `menu::set` and `storage` all work.

The rules:

* At most 128 timers.
* A name is 1 to 128 bytes, with no control characters.
* A time less than 30 seconds ahead is set 30 seconds ahead, so a run that
  sets its own timer again can never turn into a busy loop.
* A time more than five years ahead is refused, and so is `Repeat::Every`
  under 30 seconds or over five years.
* `Repeat::Daily` needs a `When::Clock` without a `date`.
* `timers::set` is refused without `"timer"` in `events`, since nothing would
  hear it, and from `Uninstalling`.

## Schedules [#schedules]

A schedule is a timer that repeats by a cron expression: Lumi runs your
[`on_event`](#on_eventname-payload) with `"schedule"` every time the Mac's
clock reaches a moment the expression names — with every one of your windows
closed, and after Lumi restarts. Declare `timers` in `capabilities` and
`"schedule"` in `events`. It needs Lumi 1.60.0, so set `min-lumi-version`.

```rust
use lumi::schedules;

// 09:00 on weekdays, and every quarter hour from 09:00 to 17:45.
schedules::set("morning", "0 9 * * mon-fri")?;
schedules::set("tick", "*/15 9-17 * * *")?;

fn on_event(name: String, payload: String) -> Result<(), String> {
    if let Some(fired) = lumi::schedules::fired(&name, &payload) {
        // … `fired.name`, `fired.at`, `fired.late`, `fired.missed` …
    }
    Ok(())
}
```

`schedules::set(name, expr)` replaces any schedule of that name, and set again
to the expression it already has it changes nothing — not even when it fires
next — so you may set it from every run. `schedules::cancel(name)` removes one
and answers whether there was one. `schedules::list()` answers every schedule,
by name, with `expr` as you set it and `due`, when it next fires in
milliseconds since the Unix epoch.

The expression is the five fields every cron has:

| Field            | Values                                   |
| ---------------- | ---------------------------------------- |
| minute           | 0–59                                     |
| hour             | 0–23                                     |
| day of the month | 1–31                                     |
| month            | 1–12, or `jan`–`dec`                     |
| day of the week  | 0–7 — Sunday is 0 and 7 — or `sun`–`sat` |

Each field is `*`, a number, a range `a-b`, a list `a,b`, or any of them with a
`/step`: `*/15` is every 15 minutes, `9-17/2` every other hour from 9 to 17,
and `5/15` is `5-59/15`. Names may be any case. Instead of five fields, one of
`@yearly` (`@annually`), `@monthly`, `@weekly`, `@daily` and `@hourly`.

* Given both a day of the month and a day of the week, neither starting with
  `*`, a day is **either** one: `0 9 13 * fri` is the 13th and every Friday,
  as in every cron.
* There are no seconds, and no `L`, `W`, `#` or `?`: an expression copied from
  a cron that has them is refused, rather than read as something else.
  `@reboot` is refused too: Lumi tells no schedule that it started.
* The expression is read on the Mac's own clock, in the person's time zone,
  and moves when the zone or the clock changes. Minutes a change of clocks
  skips fire once, at the first minute after; minutes that come twice fire the
  first time only.
* An expression that names no moment in the next five years — `0 0 30 2 *` —
  is refused. A first moment less than 30 seconds away is 30 seconds away.

Schedules are kept beside your [timers](#timers), on the same clock and under
the same rules: told at least once, a firing that came due while the Mac slept
or Lumi was quit told once with `late` and the moments folded into it counted
in `missed`, held while your extension is off and removed when it is
uninstalled. A schedule's names are its own — one may share a timer's — and the
128 an extension may hold count timers and schedules together.

## Time zone [#time-zone]

Your component's clock is UTC, and it carries no time zone database. To know
what the time or the date is where the person is — "today" for a calendar, the
time an alert should name — declare `time-zone` in `capabilities` and ask Lumi.
It needs Lumi 1.59.0, so set `min-lumi-version`.

```rust
use lumi::time_zone;

let zone = time_zone::current()?;    // zone.id "Europe/Lisbon", zone.offset_minutes 60
let offset = time_zone::offset_at(at_ms)?;
let today = time_zone::local_day(now_ms)?; // a timers::Day: year, month, day
```

* `time_zone::current()` answers the zone the Mac's clock is in now: `id`, its
  name in the IANA time zone database as macOS has it, and `offset_minutes`,
  whole minutes east of UTC now, summer time included.
* `time_zone::offset_at(ms)` answers the offset at any moment, in milliseconds
  since the Unix epoch, past or future — summer time and every change the
  zone's rules have had included. Add it to a UTC moment to read the person's
  clock.
* `time_zone::local_day(ms)` is the date on the person's calendar at that
  moment: `offset_at`, then the date, worked out in your component.

To hear the zone change — the person travels, or picks another city in System
Settings — declare `events = ["time-zone"]`. [`on_event`](#on_eventname-payload)
gets `{"v":1, "id", "offsetMinutes", "at"}` once per change, and
`time_zone::changed` reads it:

```rust
fn on_event(name: String, payload: String) -> Result<(), String> {
    if let Some(zone) = lumi::time_zone::changed(&name, &payload) {
        // … redraw what names today, in `zone` …
    }
    Ok(())
}
```

A change of clocks for summer is not a change of zone and is not told: ask
`offset_at` for the moment you need. The zone is what tells your extension
where the person is, near enough, which is why it needs a capability of its
own; the review sheet says it "knows your time zone".

## Typing [#typing]

An extension can stand between the keyboard and the text: every key typed in
any app passes through it, and it answers how the word changes. A way to type
a language whose letters the keyboard does not have is the case it is built
for — the keys `t i e e n g s` becoming *tiếng* — but Lumi knows no language,
and what a key becomes is entirely yours. Declare `text-input` in
`capabilities` and `events = ["text-input"]`. It needs Lumi 1.61.0, so set
`min-lumi-version`.

```toml
capabilities = ["text-input"]
events = ["text-input", "text-input-enabled"]
```

Each key arrives at [`on_event`](#on_eventname-payload) as `"text-input"`,
with the keys typed since the word began, oldest first:
`{"v":1, "keys":[{"text":"t","code":"KeyT"}, …]}`. `text` is the character the
key typed on the person's keyboard layout, empty for Backspace, Escape and
Return; `code` is its `KeyboardEvent.code` name, the physical key, for an
engine that works by key rather than by character. `text_input::keys` reads
them. From Lumi 1.68.0 a `"before"` list of the same shape can stand beside
them — what came before the word — and `text_input::before` reads it:

```rust
use lumi::text_input;

fn on_event(name: String, payload: String) -> Result<(), String> {
    let Some(keys) = text_input::keys(&name, &payload) else { return Ok(()) };
    let before = text_input::before(&name, &payload).unwrap_or_default();
    // Replay the whole word, after what came before it: nothing of the last
    // run is left in this one.
    let (delete, insert, word_ended) = my_engine::replay(&before, &keys);
    text_input::replace(delete, &insert, word_ended)
}
```

* `text_input::keys(name, payload)` answers the keys, or `None` for any other
  event. `Key::is_backspace()` tells Backspace and `Key::is_escape()` Escape,
  which both arrive only while a word is open — and a Backspace that reaches
  back past the start of the word into one your `commit` ended since the
  caret last moved (Lumi 1.67.2): it arrives after that word's keys and the
  key that ended it, and the word goes on. Word by word, as far back as 32
  keys in all: `h o a`, comma, Space, Backspace, Backspace, `s` is told as
  one list, so a tone can still land on the word. An answer to it that
  deletes nothing leaves that Backspace to the app, as before, and the word
  stays ended. The word ends after Escape
  whatever you answer: not answering lets Escape do what it does, and answering
  with the keys as they were typed is how an engine undoes what it made of
  them.
* `text_input::before(name, payload)` answers what came before the word
  since the caret last moved, oldest first (Lumi 1.68.0): the keys of the
  words your answers committed, and a Return — `code` `"Enter"` or
  `"NumpadEnter"` — when the caret moved by one, as many whole words as fit
  beside the word's keys in 32. Empty when nothing did, and from an older
  Lumi. Replay it through your engine ahead of the word so it knows the
  sentence it is in — where the last one ended, to capitalize the next —
  and answer for the last of the word's keys alone: you still delete only
  what you typed into this word. A click, another app, a key that moves the
  caret or a shortcut forgets it, as it ends the word.
* `text_input::replace(delete_before, insert, commit)` answers the key: delete
  `delete_before` characters before the caret, type `insert`, and with
  `commit` end the word, so the next key starts a new list — unless a
  Backspace reaches back into it. Counts are Unicode
  scalar values — `"ệ".chars().count()` — and you may delete only what your
  own answers typed into this word.
* `text_input::answer(&Edit)` is the same answer as a record, with
  `delete_after` — 0 in this release; the caret stays at the end of what you
  typed.
* Not answering lets the key type itself, and the word goes on.

`insert` is printable text, at most 64 characters: no Return, no Tab, no
control character, so a typing extension can never submit or send anything.

**The run is sealed.** Only `text_input::answer` (and `replace`) and
`settings()` work in it; every other call is refused, whatever the manifest
declares. It has about 25 milliseconds — the key is held until it answers —
and a run that is late, traps or refuses has its key typed as pressed. Three
failures in a minute and Lumi stops typing through your extension until it is
switched on again. And a manifest declaring `text-input` may not declare
`network`, `ai` or `speech` (`speech` since 1.64.0): the review sheet tells the person every key passes through
your code, and that Lumi lets it do nothing with them but change the word.

A word ends — and the next key arrives with a list of one — after a click, an
app or field changing, a key that moves the caret, Return, Tab, a ⌘ or ⌃ chord,
a key one of Lumi's shortcuts takes, a key another app posts, Escape, the Mac
sleeping or its screen locking, and after an answer that says `commit`. You
are not told; the list is simply shorter. Keys never reach a
password field: macOS hides them from every app while one has the keyboard.
Nor do they reach you while an input method of the Mac's own is chosen, or
while Screen Sharing, a remote desktop or a virtual machine is in front: those
pass keys to another computer by key, so text Lumi types would not arrive
there as text, and keys go through as typed.

### `text_input::enabled()` and `text_input::set_enabled(on)` [#text_inputenabled-and-text_inputset_enabledon]

Whether Lumi types through your extension is a switch on your Permissions
sheet, and yours to move too — a command bound to a shortcut that switches
languages is the usual way. One extension is typed through at a time:
`text_input::set_enabled(true)` switches off whichever other had it. Both are
refused from the `text-input` run. Where the switch starts on a fresh install
is `text-input-default` in the [manifest](/extensions/manifest).

When somebody else moves it — the person on the Permissions sheet, or Lumi
switching you off because another extension was switched on to type through —
`events = ["text-input-enabled"]` tells you, and
`text_input::enabled_changed(name, payload)` reads where it stands now. Your
own `set_enabled` is never told back.

### Which app is in front [#which-app-is-in-front]

To remember something per app — the switch, typically, the way input methods
remember a language for each — declare the `front-app` capability and
`events = ["front-app"]`. Every switch of app is told, Lumi's own windows
coming forward included, and `front_app::changed` reads it:

```rust
fn on_event(name: String, payload: String) -> Result<(), String> {
    if let Some(app) = lumi::front_app::changed(&name, &payload) {
        // app.id: Some("com.apple.TextEdit"); app.name: Some("TextEdit")
        let on = remembered_for(app.id.as_deref());
        lumi::text_input::set_enabled(on)?;
    }
    Ok(())
}
```

`front_app::changed(name, payload)` answers the bundle id — `None` for a
process that has none — the name macOS shows, and when, in milliseconds since
the Unix epoch. Nothing is told when your extension starts, so what is in front
then is unknown until the person next switches. Needs Lumi 1.61.0; the review
sheet says it "knows which app you are using".

## Microphone in use [#microphone-in-use]

Which apps have a microphone open — every process capturing from any input
device, named by the app responsible for it: a browser for its tabs and its
audio helper, an app for its helpers. One app is one entry however many of its
processes capture, and Lumi itself is never in it. What an app does with the
microphone is not told and not known — a call, dictation and a voice memo look
the same — so what a use means is yours to decide, from the apps you know.

Declare the `microphone-use` capability. Read the set with `apps`, and hear it
with `events = ["microphone-use"]`: the whole set is told whenever it changes,
and once as it stands when your extension starts hearing it — installed,
switched on, or Lumi launched — so a call already going on is not missed.
Changes within a quarter of a second are told as one.

```rust
fn on_event(name: String, payload: String) -> Result<(), String> {
    if let Some(apps) = lumi::microphone_use::changed(&name, &payload) {
        // apps: every app using a microphone now, by name — compare it with
        // the set you were told last to see what came and what went.
        for app in &apps {
            // app.id: Some("com.example.app"); app.name; app.since
        }
    }
    Ok(())
}
```

### `microphone_use::apps()` [#microphone_useapps]

```rust
pub fn apps() -> Result<Vec<AppUsing>, String>
```

Every app using a microphone now, in the order of their names. Each
`AppUsing` carries `id` — its bundle id, `None` for a process that has none —
`name`, the name macOS shows for it, and `since`, when Lumi first saw it using
a microphone this time, in milliseconds since the Unix epoch. Refused below
macOS 14, which has no way to tell.

### `microphone_use::sites(app)` [#microphone_usesitesapp]

```rust
pub fn sites(app: &str) -> Result<Vec<String>, String>
```

The hosts of the web pages the windows of `app` show now — `app` a bundle id
from `apps` — the window in front first, at most 16. A browser using the
microphone says which browser, never which page; this says which site, by
host (`meet.example.com`), never a path, a query or a title. Refused for an
app not using a microphone now, so it reads only the app the orange
microphone dot already points at. An app that shows no web page answers none,
and so may a browser whose page Accessibility cannot read. Needs the
`microphone-sites` capability, declared beside `microphone-use`, and Lumi's
Accessibility grant; the review sheet says it "sees which website is using
your microphone". Lumi 1.65.0.

### `microphone_use::changed(name, payload)` [#microphone_usechangedname-payload]

```rust
pub fn changed(name: &str, payload: &str) -> Option<Vec<AppUsing>>
```

The whole set, when `on_event` was called with it: `{"v":1, "apps":[{"app",
"name", "since"}], "at"}`. Always every app using a microphone then, never a
change, so a missed or merged event cannot leave you believing a use goes on.
Needs Lumi 1.65.0; the review sheet says it "knows which apps are using your
microphone".

## Offers [#offers]

A run nobody pressed — an event, a timer — opens no window of yours. When
something happens that wants an answer now, offer it: one line of your words
and one button, drawn by Lumi at the top right of the display. The press runs
one of your `[[command]]`s as the person's press, so that run may open your
windows, as a shortcut's press would.

```rust
fn on_event(name: String, payload: String) -> Result<(), String> {
    if let Some(fired) = lumi::timers::fired(&name, &payload) {
        let offer = lumi::offer::Offer {
            text: "Your ten minutes are up.".into(),
            command: "again".into(),
            button: Some("Again".into()),
            data: Some(fired.name.clone()),
            after: 0,
        };
        if lumi::offer::show(&offer).is_err() {
            lumi::alert("Your ten minutes are up.")?;
        }
    }
    Ok(())
}

fn run_command(name: String, params: String) -> Result<String, String> {
    if let Some(pressed) = lumi::offer::pressed(&params) {
        // pressed.id, pressed.data: Some("…") as given
    }
    // …
}
```

Lumi decides where and for how long: up for 15 seconds, held while the
pointer is over it, never taking the keyboard, put away by its own ✕, its
button, its time running out, or `withdraw` — not by a press elsewhere, since
the person is often clicking in another app just then. One card per extension
(a new one replaces your last), at most three up from every extension
together, the rest waiting their turn for a minute at most. You may show one
at most every 5 seconds and six in 10 minutes; past that `show` is refused.

### `offer::show(offer)` [#offershowoffer]

```rust
pub fn show(offer: &Offer) -> Result<u64, String>
```

Show `offer`, replacing yours that is up or waiting; answers its id. `text`
is one line of at most 120 characters, `command` one of your
`[[command]]`s, `button` its label (at most 24 characters; `None` for the
command's own label), `data` at most 256 bytes handed back to the command as
it was — never rendered, so a `{{$…}}` in it stays as written — and `after`
milliseconds to wait before it shows, at most 10000. The card names you by
your manifest's name and icon. Refused while the person has turned offers off
on your Permissions sheet ("May ask you from the background").

### `offer::withdraw(id)` [#offerwithdrawid]

```rust
pub fn withdraw(id: u64) -> Result<bool, String>
```

Take back offer `id`, up or waiting — true when it was still there. Within
`after` it never shows.

### `offer::allowed()` [#offerallowed]

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

Whether the person lets you offer from the background: the switch on your
Permissions sheet, theirs alone.

### `offer::pressed(params)` [#offerpressedparams]

```rust
pub fn pressed(params: &str) -> Option<Pressed>
```

The offer a command's run was pressed from — its `id` and the `data` you gave
— read from the run's `params`, where Lumi puts `"offer": {"id", "data"}`
beside the command's declared defaults; `None` for a run that came from
anywhere else. Needs Lumi 1.65.0; the review sheet says it "can offer you a
button to press, even with its windows closed".

## Sounds [#sounds]

Sounds of your own, played by Lumi from your component, which has no page to
play from: a cue that something happened — a switch moved by a shortcut, a
timer gone off — or music for an hour with every window closed. A page of
yours plays its own sounds with `<audio>`, as any page does, and needs none of
this.

```rust
fn run_command(name: String, _params: String) -> Result<String, String> {
    if name == "toggle" {
        let on = !lumi::text_input::enabled()?;
        lumi::text_input::set_enabled(on)?;
        // Named, so a second press stops the first press's cue.
        let cue = lumi::sound::Options { name: Some("switch".into()), ..Default::default() };
        lumi::sound::play(if on { "sounds/on.wav" } else { "sounds/off.wav" }, &cue)?;
    }
    if name == "play-music" {
        let music = lumi::sound::Options { repeat: true, volume: 0.6, name: Some("music".into()) };
        lumi::sound::play_blob(&lumi::storage::get("track")?.unwrap_or_default(), &music)?;
    }
    if name == "stop-music" {
        lumi::sound::stop_named("music")?;
    }
    Ok(String::new())
}

fn on_event(name: String, payload: String) -> Result<(), String> {
    if let Some(ended) = lumi::sound::ended(&name, &payload) {
        // ended.finished: false when it was stopped — by you, by a sound
        // of the same name, or by the person's Stop row.
    }
    Ok(())
}
```

A sound is a file your package carries under `ui/`, or one of your blobs —
anything you wrote, downloaded or recorded — in any format macOS plays: WAV,
AIFF, CAF, M4A, MP3. No length is imposed: your package's and your storage's
own bounds are the bounds. It plays at the volume the Mac is set to, scaled by
`volume`, through the Mac's sound output. Each sound playing is a number; up
to eight of yours play at once, and another extension's and Lumi's own are
never cut by yours.

While a sound of yours plays, Lumi's menu holds &#x2A;*"‹your name› is playing —
Stop"**, and Stop stops every one of yours: the person can always silence
what has no window to close. Say `sound-stop-row = false` in `[extension]` to
leave that row out — for cues that are over before anybody could open a menu,
or an extension that draws a Stop of its own. Switching your extension off or
removing it stops its sounds either way. A sound the person did not ask for
is rude: a setting that starts off is the polite default.

### `sound::play(path, how)` [#soundplaypath-how]

```rust
pub fn play(path: &str, how: &Options) -> Result<u64, String>
```

Play `path`, a file under `ui/` (`"sounds/on.wav"` for `ui/sounds/on.wav`),
and answer its number. `how` is `Options { repeat, volume, name }`, and
`Options::default()` is once, at full volume, unnamed: `repeat` plays it from
its start again each time it ends until stopped, `volume` is 0 to 1, and
`name` (1 to 128 bytes) stops yours playing under the same name first, so a
run that remembers no number can still replace "music". Refused, by name, for
a path out of `ui/`, a file the package does not carry, one that is no sound
macOS plays, a volume out of 0 to 1, and a ninth sound of yours at once —
nothing of yours stops when it is refused. Needs the `sound` capability and
Lumi 1.74.0; the review sheet says it "plays sounds, even with its windows
closed".

### `sound::play_blob(id, how)` [#soundplay_blobid-how]

```rust
pub fn play_blob(id: &str, how: &Options) -> Result<u64, String>
```

`play` for one of your blobs, by id — a recording, a file the person handed
you, a track you downloaded with `fetch`. Read from your own store; no other
extension's blob can be named.

### `sound::stop(sound)` [#soundstopsound]

```rust
pub fn stop(sound: u64) -> Result<bool, String>
```

Stop your sound `sound`; true when it still played.

### `sound::stop_named(name)` [#soundstop_namedname]

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

Stop your sounds playing under `name`; true when one still played.

### `sound::playing()` [#soundplaying]

```rust
pub fn playing() -> Result<Vec<Playing>, String>
```

Your sounds that play now, oldest first, each its `sound` number and `name`.

### `sound::ended(name, payload)` [#soundendedname-payload]

```rust
pub fn ended(name: &str, payload: &str) -> Option<Ended>
```

The sound that stopped, when `on_event` was called with `"sound-ended"`
(`events = ["sound-ended"]` in the manifest, beside the `sound` capability):
`{"v":1, "sound", "name", "finished", "at"}` — its number, its name, whether
it came to its end (false when it was stopped, by you, by a sound of the same
name or by the person's Stop row) and when. Not told when your extension is
switched off or removed, which stops its sounds too. Needs Lumi 1.74.0.

## 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>;
    // Your own [[shortcut]]s — no capability needed for these two.
    pub fn extension_shortcuts() -> Result<serde_json::Value, String>;
    pub fn set_extension_shortcut(command: &str, key: Option<&str>, replace: bool) -> 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`                                                                                                                                                                                                                                                                                                                                    |
| `extension_shortcuts()`                         | Your own [`[[shortcut]]`](/extensions/manifest#shortcut)s as Lumi holds them: a list, one per declaration, each `{"command", "label", "key", "declared", "state", "reason", "holder"}`. Read from `on_lifecycle` to learn whether install could arm your key, and who held it if not. No capability                                                                                   |
| `set_extension_shortcut(command, key, replace)` | Arm, change or clear one of them; `key` in the recorder's spelling (`Shift+Super+KeyC`), a tap of modifiers (`Tap+Fn+Shift`, Lumi 1.61.0 — see [`[[shortcut]]`](/extensions/manifest#shortcut)) or `None`. Refused with a sentence when something holds the key unless `replace`, which takes it off the person's row — never off another extension. No capability. Needs Lumi 1.26.0 |

```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 settings" — beside "sends and receives data over the
internet" 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.
