# The manifest (/extensions/manifest)



`manifest.toml` is what Lumi's installer checks and what the review sheet reads
to the person before anything is installed. Everything an extension
contributes is declared here; code can only reach what the manifest names.

Every refusal names the field it is about, so a manifest Lumi will not accept
tells you why the moment you install it.

## `[extension]` [#extension]

```toml
[extension]
id = "dev.you.thing"
name = "Thing"
version = "1.2.0"
description = "One line for the extension's card."
summary = "A paragraph for the About page."
author = "You"
homepage = "https://example.dev/thing"
about = "about.html"
settings-page = "settings.html"
min-lumi-version = "1.22.0"
capabilities = ["accessibility", "clipboard"]
```

| Key                | Required | Notes                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| ------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `id`               | yes      | 1–100 characters of lowercase letters, digits, `.`, `-` and `_`, not starting with `.`. Reverse-DNS by convention, in lowercase: `dev.you.thing`, never `Dev.You.Thing`. It never changes: an update is the same `id` with a higher `version`.                                                                                                                                                                                                                                                                                                                     |
| `name`             | yes      | What the person sees.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `version`          | yes      | Your version, shown on the card. Write it as `major.minor.patch` — `1.2.0`, not `1.2` or `1.2.0-beta`: Lumi installs any version, but compares two only when both have that shape — so an update is offered, and a store package older than the installed one is refused, only between versions written that way. A file you install yourself may be older; the review sheet then says **Downgrade**.                                                                                                                                                              |
| `description`      | no       | One line, shown in the Extensions list.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `summary`          | no       | A paragraph that leads the About page Lumi writes for you. Falls back to `description`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `author`           | no       | Shown on the card and the review sheet.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `homepage`         | no       | An `https://` address of at most 512 characters, drawn as a link on the About page. Any other scheme is refused.                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `menu`             | no       | `"auto"`, `"group"` or `"flat"` — how the Action picker lists your commands. See [how commands are listed](#how-commands-are-listed).                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `about`            | no       | A page of your own under `ui/`, drawn in place of the About page Lumi writes — see [About and Settings pages](/extensions/windows#about-and-settings-pages). The file must ship in the package.                                                                                                                                                                                                                                                                                                                                                                    |
| `settings-page`    | no       | Likewise for the Settings tab, in place of the form Lumi draws from `[[settings]]`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `settings-tab`     | no       | `false` hides the Settings tab while keeping your `[[settings]]` — for fields that are storage for a [page of your own](#page) rather than something the person sets. Your code and pages still read and write them. Cannot be combined with `settings-page`. Defaults to `true`; with no settings and no settings page there is no tab either way.                                                                                                                                                                                                                |
| `min-lumi-version` | no       | The oldest Lumi this manifest was written for, as exactly `major.minor.patch` — `1.22` is refused. Installing on an older Lumi is refused with a sentence naming both versions, before anything is written to disk — the same install-time refusal a new capability or param kind gets, but worded so the person knows to update Lumi rather than wonder what `teleport` is. An update that needs a newer Lumi is listed as needing it rather than offered. Left out, there is no floor. Extensions first shipped in Lumi 1.22.0, so a value below that sets none. |
| `capabilities`     | no       | What the code may reach — below.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |

## Capabilities [#capabilities]

Declare only what the code calls. A call whose capability is missing is
refused by name when it runs, so an under-declared manifest fails loudly on
your own machine, and an over-declared one costs trust on the review sheet.

| Capability      | Unlocks                                                                              | The review sheet says                                          |
| --------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------------------- |
| `accessibility` | `selection()`                                                                        | reads what you have selected                                   |
| `clipboard`     | `clipboard_text()`, `set_clipboard_text()`, and the ⌘C fallback inside `selection()` | reads and writes the clipboard                                 |
| `applications`  | `open_url()`                                                                         | opens and quits applications, and sees which one you are using |
| `network`       | `fetch()`, and web addresses in `open_url()`                                         | sends data over the network                                    |
| `config`        | everything in `lumi::config`                                                         | reads your shortcuts, snippets and Lumi settings               |

Everything else in the SDK needs no capability — see
[what each call costs](/extensions/sdk#what-each-call-costs).

`input`, `windows`, `files`, `system` and `shell` are reserved: a manifest
asking for one is refused with a sentence saying it is not available yet. Any
other word is refused as unknown.

## `[[command]]` [#command]

An action the person can bind to a shortcut, a leader menu step, a double-tap
or an Fn combination.

```toml
[[command]]
name = "translate"
label = "Translate selection"
icon = "globe"

[[command.params]]
name = "target"
label = "Translate into"
kind = "select"
default = "vi"

[[command.params.options]]
value = "vi"
label = "Vietnamese"
```

`name` is what `run_command` receives, unique among your commands; `label` is
what the action picker shows. Both are required, on a `[[node]]` too. Params become fields on the
binding — each shortcut bound to the command keeps its own values — and reach
your code as a JSON object, with any `{{$…}}` variables already rendered. A
field the person left alone arrives at its `default`, the value the shortcut
sheet showed them, so your code does not need a second copy of your manifest's
defaults.

`icon` is the command's mark in the action picker and the shortcut list, in
one of two spellings:

* **A Lucide icon name** — `icon = "globe"`: lowercase letters and digits in
  words joined by `-`, as on the Lucide site, 64 characters at most. Lumi ships
  a chosen set of Lucide icons — the ones its own icon picker offers — and a
  well-formed name outside it is drawn with your extension's `icon.svg`
  instead. A value that is neither a name of that shape nor an `.svg` path
  refuses the manifest.
* **An SVG file under `ui/`** — `icon = "icons/translate.svg"`, 64 KiB at
  most. The file must ship in the package.

Without an `icon`, the command wears your extension's `icon.svg`.

### How commands are listed [#how-commands-are-listed]

In the shortcut sheet, the Action picker lists extensions under an
**Extensions** heading. By default an extension with two or more commands is
one row that opens a submenu of them, and an extension with a single command
lists that command directly — a submenu of one is a click spent on nothing.
Set `menu` under `[extension]` to choose for yourself:

| `menu`             | The picker shows                                                                         |
| ------------------ | ---------------------------------------------------------------------------------------- |
| `"auto"` (default) | A submenu for two or more commands, the command itself for one.                          |
| `"group"`          | Always a submenu, even for one command — so the row does not move when you add a second. |
| `"flat"`           | Every command directly under Extensions, each with your extension's name beside it.      |

A search in the picker lists every matching command directly, whatever `menu`
says.

## `[[node]]` [#node]

A step in the Flow Editor's palette.

```toml
[[node]]
name = "shout"
label = "Shout"
summary = "Upper-cases each item's text field."

[[node.params]]
name = "suffix"
label = "Suffix"
kind = "text"
default = "!"
```

`name` may hold only letters, digits, `-` and `_`: it becomes part of the
node's type in every flow that places it, so it cannot change once people use
it. `run_node` receives the params as a JSON object and the incoming items as
a JSON array, and answers with the outgoing items as a JSON array. **An empty
array ends that branch of the flow** — the same rule every built-in node
follows.

## `[[settings]]` [#settings]

A form for the extension as a whole, drawn under **Settings → Extensions** and
read with `lumi::settings()`.

```toml
[[settings]]
name = "defaultTarget"
label = "Default language"
kind = "select"
default = "vi"
scope = "profile"

[[settings.options]]
value = "vi"
label = "Vietnamese"
```

Lumi stores the values for you. A setting the person never touched reads as
its `default`, so your code never has to tell a chosen value from a defaulted
one.

### One value per profile [#one-value-per-profile]

Add `scope = "profile"` and Lumi keeps a separate value in each of the
person's [profiles](/features/profiles). Your code does not change:
`lumi::settings()` hands back the live profile's value. Leave `scope` out, or
write `"mac"`, for one value across every profile.

Changing a field's scope in an update is safe: made per-profile, the value
chosen Mac-wide carries into every profile until one of them changes it; made
Mac-wide again, it reads the Mac-wide value. `scope` is for `[[settings]]`
only — a command's or a node's param is already per shortcut or per node — and
no setting may be named `byProfile`.

The Settings tab lays settings out one under another. To arrange them, add a
layout after your `[[settings]]` entries — the same keys as a
[command's layout](#commandlayout): sections, rows of two, `when` and `help`.

```toml
[[settings.layout]]
section = "Service"
rows = [["service"], ["apiKey", "formality"]]
when.apiKey = { param = "service", equals = "deepl" }
help.apiKey = "From your DeepL account page."
```

Settings save as they change. A value that breaks `required`, `pattern` or
the bounds is not saved: the field says why when it is left, and your code
goes on reading the last value that passed. The file the values live in is
still a plain file anybody can edit, so treat a stored value as unchecked.

## Fields [#fields]

Command params, node params and settings share one schema:

| Key                  | Notes                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`               | Required; unique within its table. The key in the JSON your code receives.                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `label`              | Required. What the form shows beside the field.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `kind`               | One of the kinds below.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `default`            | Written as a string, except for `multiselect`: a list of option values, such as `default = ["en", "vi"]`, each one of its `options`; left out, nothing is ticked.                                                                                                                                                                                                                                                                                                                                                                              |
| `required`           | `true` marks the field as needing a value; a shortcut will not save with it empty. A required `multiselect` needs at least one option ticked.                                                                                                                                                                                                                                                                                                                                                                                                  |
| `options`            | For `select`, `segmented` and `multiselect`: a list of `{ value, label }`, at least one, each `value` different. An option may add `note`, a line of explanation under its label.                                                                                                                                                                                                                                                                                                                                                              |
| `search`             | For `select` and `multiselect` only: `true` or `false` forces a search box in the list on or off. Left out, a list longer than twelve gets one.                                                                                                                                                                                                                                                                                                                                                                                                |
| `min`, `max`, `step` | For `number` and `slider` only; on any other kind they are refused. `step` must be above zero. A slider needs both `min` and `max`, with `min` below `max`.                                                                                                                                                                                                                                                                                                                                                                                    |
| `pattern`            | For `text`, `textarea` and `template` only: a regular expression the value must match before a shortcut saves. Unanchored — write `^…$` to match the whole value. It is matched against the value with its leading and trailing spaces removed, and an empty value is not matched at all — so a pattern does not make a field required; `required` does. Character classes, groups, alternation and repetition are supported; lookaround and backreferences are not, and a pattern using either is refused at install. At most 512 characters. |
| `scope`              | Settings only — see above.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |

Every value reaches your code as a string, whatever the kind:

| `kind`        | Lumi draws                                                                                 | The value                                                                                   |
| ------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- |
| `text`        | A text field                                                                               | The text                                                                                    |
| `textarea`    | A text field of several lines                                                              | The text                                                                                    |
| `number`      | A number field                                                                             | The number, as text                                                                         |
| `bool`        | A switch                                                                                   | `"true"` or `"false"`                                                                       |
| `select`      | A pop-up list of `options`                                                                 | The chosen `value`                                                                          |
| `segmented`   | The `options` side by side — five at most; more is refused, use a `select`                 | The chosen `value`                                                                          |
| `multiselect` | A tick beside each of up to five `options`, or a pop-up list that stays open while ticking | A JSON array of the ticked values, in the order `options` lists them, such as `["en","vi"]` |
| `slider`      | A track from `min` to `max`                                                                | The number, as text                                                                         |
| `template`    | A text field with **Insert variable** beside it                                            | The text, with its `{{$…}}` variables filled in                                             |
| `app`         | Lumi's application picker                                                                  | A bundle identifier, such as `com.apple.Safari`                                             |
| `keys`        | Lumi's key recorder                                                                        | A combination in Lumi's spelling, such as `Super+Shift+KeyZ`                                |

A `multiselect` is not available to `[[node.params]]` yet: the flow editor has
no control for several answers, so a node that declares one is refused. Use
one `bool` per option there.

### Which control a choice gets [#which-control-a-choice-gets]

You say what the answers are, and Lumi chooses how to draw them, so a form
looks like the rest of Lumi and changes with it. There is no field for picking
a widget. The rules:

* A `select` is a pop-up list. If any option has a `note` and there are five
  options or fewer, it is drawn as radio buttons instead, each note under its
  label, so the notes are read before choosing rather than after. With more
  than five, it stays a pop-up list and each note is drawn under its row.
* A `segmented` whose options carry a `note` is drawn as radio buttons too, for
  the same reason; without notes it stays side by side.
* A `multiselect` of five options or fewer is a column of ticks. Longer, it is
  a pop-up list that stays open while you tick.
* A pop-up list of more than twelve options opens with a search box, which
  matches labels and notes. `search` overrides this either way.

```toml
[[command.params]]
name = "tone"
label = "Tone"
kind = "select"
default = "neutral"

[[command.params.options]]
value = "neutral"
label = "Neutral"
note = "Keeps the register of the original."

[[command.params.options]]
value = "formal"
label = "Formal"
note = "For email and documents."

[[command.params]]
name = "also"
label = "Also translate into"
kind = "multiselect"
default = ["en"]

[[command.params.options]]
value = "en"
label = "English"

[[command.params.options]]
value = "ja"
label = "Japanese"
```

Reading a `multiselect` in Rust:

```rust
let also: Vec<String> = serde_json::from_str(&value).unwrap_or_default();
```

Lumi reads a manifest leniently where it can and strictly where it must. A key
it does not know is ignored, so a key a later Lumi adds is skipped by this one.
A value it does not know — a new `kind`, a new capability — refuses the
manifest and names the value, because drawing a field it cannot draw, or
running code that needs something it cannot give, would be a guess.

## `[[command.layout]]` [#commandlayout]

By default a command's params are drawn one under another beside the Action
field. A command that needs more room declares a layout, and Lumi then draws
its form at the full width of the shortcut sheet, in sections:

```toml
[[command.layout]]
section = "Languages"
rows = [["from", "to"], ["code"]]         # one or two params per row
when.code = { param = "to", equals = "other" }
help.code = "A BCP 47 code, as the translation service expects it."

[[command.layout]]
section = "Result"
rows = [["output"], ["keep"], ["text"]]
when.keep = { param = "output", equals = "replace" }
when.text = { param = "output", not-equals = "replace" }
```

| Key            | Notes                                                                                                                                                                                                                                                                 |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `section`      | The heading. Leave it out for a section with none.                                                                                                                                                                                                                    |
| `rows`         | Rows of one or two param names; two sit side by side. Each param may be placed once.                                                                                                                                                                                  |
| `when.<param>` | Show that param only while another param `equals` — or does not equal, `not-equals` — a value. Write exactly one of the two. Both params must be placed in the same section, and a param cannot depend on itself. A hidden param keeps its value and is not required. |
| `help.<param>` | A line of help under that param, which must be placed in the same section.                                                                                                                                                                                            |

The layout is presentation only: the values a shortcut saves are the same one
string per param either way, so you can change a layout in an update without
touching anybody's shortcuts. A param the layout does not place is drawn after
the sections. An empty row is refused.

Lumi draws every control itself, so your form always looks like the rest of
the sheet. There is no way to add styling or controls of your own.

## `[[window]]` [#window]

A dialog of the extension's own, drawn from files under `ui/` in the package.
See [Windows](/extensions/windows) for what a page can do.

```toml
[[window]]
name = "settings"
title = "My settings"
path = "index.html"
width = 440
height = 420
resizable = false
maximizable = false
```

| Key                                       | Notes                                                                                                                                  |
| ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `name`                                    | Required. Letters, digits, `-` and `_`; what `open_window` addresses.                                                                  |
| `title`                                   | The title bar, drawn as "*title* — *extension name*", or the title alone when it is exactly your extension's name. Defaults to `name`. |
| `path`                                    | The entry file under `ui/`. Defaults to `index.html`. It must ship in the package.                                                     |
| `width`, `height`                         | In points, clamped to 240–1600 and 180–1200. Left out, 480 × 360.                                                                      |
| `resizable`, `maximizable`, `minimizable` | Default `true`; write only the ones you turn off.                                                                                      |

There is no way to hide the close button or the title bar, on purpose: a
dialog the person cannot dismiss, or one that does not name the extension that
drew it, is a way to impersonate something else.

## `[[page]]` [#page]

A tab of the extension's own on its page under **Settings → Extensions**,
drawn from a file under `ui/` — a keyboard tester, a dashboard, a log. It sits
between About and Settings and works exactly like an
[About or Settings page](/extensions/windows#tabs-of-your-own).

```toml
[[page]]
name = "keytest"
label = "Keyboard test"
path = "keytest.html"
```

| Key             | Notes                                                                                                                                                                                                                                                            |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`          | Required. Up to 64 letters, digits, `-` and `_`. Your `run_ui` sees it as `:page:keytest`.                                                                                                                                                                       |
| `label`         | The tab's text, up to 24 characters. Defaults to `name`. It may not be About, Settings or Permissions, in any case — those tabs are Lumi's.                                                                                                                      |
| `path`          | The file under `ui/`. Defaults to `index.html`. It must ship in the package.                                                                                                                                                                                     |
| `function-keys` | `true` takes F1–F12 from macOS's own shortcuts while the page is in front — F11 is Show Desktop, which otherwise moves every window aside — and sends them as `lumi:key` events. Defaults to `false`. See [taking F-keys](/extensions/windows#tabs-of-your-own). |
| `fn-key`        | `true` sends the page fn / Globe pressed on its own — a key a web page is otherwise never sent — as a `lumi:fn` event. Defaults to `false`. See [hearing fn](/extensions/windows#tabs-of-your-own).                                                              |
| `focus`         | `true` gives the page the keyboard as soon as its tab is opened, so keys reach it without a click first — for a page whose whole use is keystrokes, like a keyboard tester. Defaults to `false`: a focused page receives what the person types.                  |

At most four pages. Neither a name nor a label may be used twice.

## `[install]` [#install]

An installer of your own: a page under `ui/` that opens after Lumi's review
of the package and asks whatever you need before the extension is installed.
See [An installer of your own](/extensions/windows#an-installer-of-your-own).

```toml
[install]
page = "install.html"
title = "Set up My extension"
width = 420
height = 380
```

| Key               | Notes                                                                                                               |
| ----------------- | ------------------------------------------------------------------------------------------------------------------- |
| `page`            | Required. The installer's file under `ui/`; it must ship in the package.                                            |
| `title`           | The title bar. Lumi adds your extension's name if the title does not already hold it. Defaults to "Install *name*". |
| `width`, `height` | In points, clamped to 240–1600 and 180–1200. The window cannot be resized, maximised or minimised.                  |

The table is optional. Without it the extension installs in one press, with
every setting at its default.

## The package [#the-package]

A package is a gzipped tar. Lumi finds `manifest.toml`, `extension.wasm` and
`icon.svg` by name wherever they sit, and a package holding two of any of them
is refused. `ui/` may sit at the top of the archive or inside one directory.

| File             | Required | Limit                                             |
| ---------------- | -------- | ------------------------------------------------- |
| `manifest.toml`  | yes      | 256 KiB                                           |
| `extension.wasm` | yes      | 48 MiB                                            |
| `icon.svg`       | no       | 64 KiB. Drawn on the card; its scripts never run. |
| `ui/…`           | no       | 200 files, 5 MiB each, 24 MiB in all.             |

The whole package is at most 64 MiB. `manifest.toml` and `icon.svg` must be
UTF-8. Paths under `ui/` must be plain relative paths of at most 512
characters — letters, digits, `.`, `-` and `_` in each segment, nothing
hidden, no path twice — and a segment named `__lumi__` is reserved for the
[bridge](/extensions/windows#the-bridge). A precompiled `extension.cwasm` is
refused: a package ships WebAssembly, never native code.

The `._` files macOS's `tar` adds beside every file are ignored, so a package
built on a Mac needs no special flags.
