# Plugin API reference

> Everything a Python plugin can use: manifest, hooks, widgets, events, wm.*, modules, limits.

URL: https://wmkeyboard.pages.dev/plugins/api-reference/
Section: Plugins

Everything a plugin can use. If it is not here, it does not exist.

Plugins are written in Python, a subset of it that matches
[PyMini](https://github.com/anti-ltd/PyMini), the small interpreter Clink uses.
WM Keyboard runs it on its own Rust port of PyMini, the same one on the phone
and in the web editor's live preview. [The Python you get](#the-python-you-get)
lists what that subset has.

## The manifest

`plugin.json`, at the top level of the archive.

| Field | Required | Meaning |
|---|---|---|
| `format` | yes | Always `"wmkeyboard-plugin"`. The one tag that identifies the file. |
| `version` | no | Container format version. `1`. |
| `id` | yes | Unique, lowercase, 3–64 characters of `a-z 0-9 . _ -`, starting alphanumeric. Becomes the plugin's folder on the device. Reverse-DNS by convention. |
| `name` | yes | Shown everywhere. Trimmed to 40 characters, control and direction-override characters removed. |
| `pluginVersion` | yes | Your version, e.g. `"1.2.0"`. |
| `author` | no | Shown on the install screen. |
| `description` | no | One or two sentences. |
| `apiVersion` | yes | `4`. A higher number than the app knows is refused. Left out, it counts as `1`, which a manifest without `language` cannot be; see [Lua is refused](#lua-is-refused). |
| `language` | no | `"python"`, the only language there is. Leave it out or set it; nothing else is accepted. |
| `entry` | no | The script inside the archive. Defaults to `main.py`. |
| `permissions` | no | See [Permissions](https://wmkeyboard.pages.dev/plugins/permissions/). Usually `[]`. |
| `actions` | no | Buttons on the bar over selected text. See [Selection actions](#selection-actions). Needs the `selection` permission. |
| `panel` | no | `false` for a plugin made of actions alone: it has no `render()` and is left out of the keyboard's plugin list. Defaults to `true`. |

A manifest this build does not fully understand is refused. That includes any
permission string it does not recognize. Half-understood code does not get to
run.

### Lua is refused

Plugins were written in Lua before 0.6.6. A manifest that says
`"language": "lua"`, or that leaves `language` out and gives an `apiVersion` of
3 or lower, is a Lua plugin, and the install is refused with this reason:

> written in Lua, which WM Keyboard no longer runs; look for its Python version.

A manifest that says `"language": "python"` with a lower `apiVersion` still
installs. That is how an action plugin made from a Clink action before 0.6.6
looks; a [Clink action](https://wmkeyboard.pages.dev/reference/importing-other-keyboards/#clink-actions)
converted today gets `"apiVersion": 4`. What happened to Lua plugins already on
a phone is under [Lua plugins were retired](https://wmkeyboard.pages.dev/plugins/overview/#lua-plugins-were-retired).

## Your functions

```python
def render():                # a widget, or a list of widgets
    ...

def on_event(e):             # optional
    ...

def transform(text, action): # only for selection actions
    ...
```

`render()` is called after loading and after every event. `on_event(e)` is
called when the user does something. Change your variables in `on_event`,
describe the result in `render()`.

All three are plain module-level functions. There is no registration step.

State lives in module globals, the variables at the top level of your script.
They keep their values for as long as the panel is open. A function that
assigns to one has to say so with `global` first, the way Python always asks:

```python
count = 0

def on_event(e):
    global count
    count += 1
```

Leave the `global` line out and the assignment makes a local variable instead,
with no error to tell you.

`wm` and `ui` are globals too. Neither needs an `import`.

## Selection actions

A plugin can add its own buttons to the bar that appears over selected text.
List them in the manifest, ask for the `selection` permission, and write
`transform`:

```json title="plugin.json (excerpt)"
"apiVersion": 4,
"permissions": ["selection"],
"actions": [
  { "id": "rot13", "name": "ROT13", "summary": "Shift each letter 13 places", "icon": "lock" },
  { "id": "quote", "name": "Wrap in quotes" }
]
```

```python
def transform(text, action):   # a string, or None to leave the text alone
    if action == "rot13":
        return rot13(text)
    if action == "quote":
        return "“" + text + "”"
```

A plugin with a single action can take one parameter, `transform(text)`.

When the user selects text and taps one of your buttons, the keyboard loads
your script in a fresh interpreter, calls `transform` with the selected text and
the action's `id`, and puts the string you return in place of the selection.
Undo on the bar takes it back. Nothing is kept between taps except what you
write to `wm.storage`: a global you set during one call is gone by the next.

| Action field | Meaning |
|---|---|
| `id` | Unique within the plugin, up to 32 characters of `a-z 0-9 _ -`. |
| `name` | The button's label. Trimmed to 24 characters. |
| `summary` | One line for the settings list. Optional. |
| `icon` | A key icon name, the same names a layout key takes (`lock`, `translate`, `link`). Optional; a plugin glyph otherwise. |
| `replaces` | `false` keeps the selection and adds your result after it. Defaults to `true`. |
| `input` | `"selection"`, the default, or `"none"` to be called with an empty string. |

Up to 16 actions. A call gets the same `wm` as the panel, without
`wm.ui.set_input`, since there is no panel for it to write into. It has the same
step and time allowance as loading your script, because every tap loads it. The
user can switch each action off in
Advanced / Selection actions, where actions from plugins
come after the built-in ones. They also appear in the **WM Keyboard** entry of
other apps' selection menus.

A plugin can have a panel and actions at once, the way the **Selection
actions** template keeps the quote style its action uses. One with actions and
no panel sets `"panel": false`. A
[Clink action](https://wmkeyboard.pages.dev/reference/importing-other-keyboards/#clink-actions) installs as
exactly that: one action, no panel.

## Widgets

Built by `ui.*`, which return plain dicts. You can write the dicts yourself if
you prefer: every builder below gives a dict with a `type` key and one key per
parameter, named as the parameter is. `column` and `row` keep their children
under `children`, `tabs` its pages under `pages`, and a page is
`{"title": ..., "children": [...]}`.

### Layout

```python
ui.column(child, child, ...)   # stacked vertically
ui.row(child, child, ...)      # side by side, equal widths
ui.spacer(height=8)            # vertical gap, 0-64
ui.divider()                   # a horizontal line
```

`column` and `row` also take one list, which is the easy way to build children
in a loop:

```python
ui.column([ui.label(item) for item in items])
```

### Text

```python
ui.label(text, style=None)     # style: "title", "body" or "caption"
```

`body` is the default. Labels are not interactive and have no id.

```python
ui.output(
    text,
    id=None,
    mono=False,         # monospace, for anything with alignment
    insertable=True,    # show the Insert button
    copyable=True,      # show the Copy button
)
```

**`ui.output` is how your results reach the user's text.** The keyboard draws
Insert and Copy buttons under it. A tap on Insert puts the text where they are
writing. There is no function that types for them.

### Controls

```python
ui.button(id, text, style=None, enabled=True)
ui.toggle(id, label, checked=False)
ui.input(id, label=None, placeholder=None)
```

`style="primary"` highlights a button. Anything else is plain. `enabled=False`
grays one out.

**`ui.input` carries no value.** The keyboard owns what is in the box: a tap
points the keys at it, and you are told the contents through an `input_changed`
event. Use `wm.ui.set_input(id, text)` to write to one. This is why a plugin
never sees a keystroke. It is handed the whole contents of its own box after
each one, and nothing else.

### Tabs

```python
ui.tabs("modes",
    ui.page("First", child, child),
    ui.page("Second", child),
)
```

`ui.tabs(id, *pages)` takes its pages after the id, and `ui.page(title,
*children)` its children after the title. Top level only, up to 8 pages. Which
page is showing is the keyboard's business. You get a `tab_selected` event if
you care.

### Date

```python
ui.date(
    id,
    label=None,
    value=None,     # seconds since 1970, as time.time() or wm.timestamp() give; None for no date yet
    time=False,     # pick a time of day too
    step=15,        # minutes the smaller time buttons move, 1-60
)
```

The keyboard draws the date between a back and a forward arrow, the time (with
`time=True`) between buttons that move it by `step` minutes and by an hour,
and **Today** and **Clear** under them. With no `value` it shows **Set date**,
which picks today, or the next whole hour when `time` is on.

Like `ui.input`, the widget keeps nothing. Each press sends a `date_changed`
event with the new value, and the next render draws whatever you kept. Without
`time` the value is the start of a day. Days move on the calendar in the phone's
time zone, so 9:00 stays 9:00 across a clock change.

### Progress

```python
ui.progress()   # an indeterminate bar
```

## Events

`on_event(e)` receives a dict. It always has `type` and `id`; some events carry
more. Read them as `e.type` or `e["type"]`, whichever you like.

| `e.type` | Extra | Fired when |
|---|---|---|
| `"click"` | none | A button was tapped. |
| `"toggle"` | `e.value` (`True` or `False`) | A toggle was flipped. |
| `"input_changed"` | `e.value` (a string) | The contents of one of *your* boxes changed. |
| `"tab_selected"` | `e.index` (an int, from 0) | A tab was picked. |
| `"date_changed"` | `e.value` (seconds, or `None`) | A date widget moved, or was cleared. |

`input_changed` fires per keystroke into your own input widget. That is how a
live preview works. It is bounded to your panel: you are told what is in your
box, never what is typed anywhere else.

## `wm.*`

The complete host API.

### Always available

```python
wm.api_version      # 4
wm.plugin_id        # your manifest id
wm.plugin_version   # your manifest pluginVersion
wm.log(*values)     # a line in your log, readable in Settings
```

`print` writes to the same log. A line is cut at 512 characters.

```python
wm.ui.set_input(id, text)   # write to one of your own input widgets
```

Applied after your handler returns. Capped at 8 KB. Panel only: a selection
action has no boxes to write to.

```python
wm.timestamp(year, month, day, hour=12, minute=0, second=0)
```

That moment in the phone's time zone, in seconds since 1970. A field past its
range rolls over, so `day=d + 1` on the last of the month is the first of the
next, and `month=13` is January of the following year. This is what a date
widget and a reminder take.

### With the `storage` permission

```python
wm.storage.get(key, default=None)   # the string, or default
wm.storage.set(key, value)          # strings only
wm.storage.remove(key)
wm.storage.keys()                   # a list of strings
```

Strings only, so use `json.dumps` and `json.loads` for anything structured.
Local to your plugin, local to the device, deleted when the user uninstalls you.
`set` raises `ValueError` when you are over quota. Catch it and say something
rather than losing the data silently:

```python
try:
    wm.storage.set("items", json.dumps(items))
except ValueError as err:
    status = "Not saved: " + str(err)
```

If the manifest did not declare `storage`, `wm.storage` is `None`.

### With the `reminders` permission

```python
wm.reminders.set(id, seconds, text)
wm.reminders.cancel(id)   # True when there was one
wm.reminders.list()       # [{"id": ..., "time": ..., "text": ...}, ...]
```

At `seconds`, a time as `wm.timestamp()` or `time.time()` gives it, the app
shows `text` in a notification with your plugin's name as its title. Setting an
`id` that is already set replaces it. `list` gives every reminder not yet shown,
soonest first; a reminder leaves the list once it is shown.

Your script does not run to show it. The app keeps the id, the time and the
text, and posts the notification itself, so a reminder fires with the keyboard
closed. It does not fire while plugins, or your plugin, are switched off; a
reminder that comes due then is dropped.

`set` raises `ValueError` for a time already past, for empty text, or past 64
reminders. Text is cut to 200 characters, with control and direction-override
characters removed.

If the manifest did not declare `reminders`, `wm.reminders` is `None`. A plugin
can check for that and work without it.

### Not present, and never will be

`wm.text`, `wm.clipboard`, `wm.http`, `wm.net`, `wm.files`. None of these exists.
There is no API for reading what the user types, reading the field, reading the
clipboard, or reaching the network. See [Security](https://wmkeyboard.pages.dev/plugins/security/).

## The Python you get

A subset of Python 3 that matches PyMini.

**The language:** functions with default and `*args` parameters, `lambda`,
`if`/`elif`/`else`, `for` and `while` with `break`, `continue` and `else`,
`try`/`except`/`finally` and `raise`, `global`, `del`, list, dict and set
comprehensions, f-strings, slicing, tuple unpacking, chained comparisons and
`import`.

**Not there:** `class`, `with`, `yield`, `async` and `await`, `match`,
decorators, `nonlocal`, `assert` and `**kwargs`. A script that uses one is
refused with a message that names it, not a vague syntax error.

**Built-in functions:** `abs`, `all`, `any`, `bin`, `bool`, `callable`, `chr`,
`dict`, `divmod`, `enumerate`, `filter`, `float`, `format`, `hex`, `int`,
`isinstance`, `len`, `list`, `map`, `max`, `min`, `oct`, `ord`, `pow`, `print`,
`range`, `repr`, `reversed`, `round`, `set`, `sorted`, `str`, `sum`, `tuple`,
`type` and `zip`. There is no `open`, `eval`, `exec`, `compile` or
`__import__`.

**Strings** count characters as you see them, so an emoji with a skin tone or a
letter with a combining accent is one character to `len`, indexing and `for`.
Two strings that look the same compare equal even when one spells an accent as a
separate mark.

**Dicts answer attribute reads.** `d.name` is `d["name"]` when the key is
there, and otherwise a dict method such as `d.get`. That is why `e.type` works
on an event. Writing still takes brackets: `d["name"] = value`.

**Modules:** `import` reaches these six and nothing else.

| Module | What it has |
|---|---|
| `math` | `sqrt`, `floor`, `ceil`, `trunc`, `fabs`, `pow`, `exp`, `log`, `log2`, `log10`, the trigonometric functions, `degrees`, `radians`, `hypot`, `gcd`, `lcm`, `factorial`, `isnan`, `isinf`, `isfinite`, `copysign`, `fmod`, `comb`, `prod`, and `pi`, `e`, `tau`, `inf`, `nan` |
| `json` | `dumps` and `loads`. Bad JSON raises an error. |
| `re` | `search`, `match`, `fullmatch`, `findall`, `finditer`, `sub`, `subn`, `split`, `escape`, and the flags `I`, `M`, `S` (also spelled out). Matching spends steps like any loop. |
| `time` | `time()` and `monotonic()` in seconds; `parts()` or `parts(seconds)`, a dict of `year`, `month`, `day`, `hour`, `minute`, `second`, `weekday` (Monday is 0) and `yearday` in the phone's time zone; `format(pattern)` or `format(pattern, seconds)` with a Unicode date pattern such as `"EEE d MMM yyyy"`, which writes day and month names in English. Left out, `seconds` is now. |
| `random` | `random`, `randint`, `randrange`, `choice`, `choices`, `shuffle`, `sample`, `uniform`, `seed` |
| `sys` | `version` |

`time.format` takes the patterns Android and Java use (`yyyy`, `MM`, `d`, `HH`,
`mm`, `EEE`, `MMMM` and so on), not C's `%Y-%m-%d`.

## Limits

Exceeding one of these is something you can see: either an error, or a note on
the panel saying what had to be dropped. Two are quiet, and the table says which.

| | |
|---|---|
| Script size | 256 KB |
| Archive size | 1 MB, 16 entries |
| Steps | 3M on load, 2M per event, 400k per render, 3M per selection action |
| Time | 3 s on load, 2 s per event, 0.5 s per render, 3 s per selection action |
| Lists, dicts and sets | 10,000 items each |
| Strings | 100,000 characters |
| Recursion | 300 calls deep |
| Selection action result | 64K characters |
| Widgets | 256 nodes, 12 deep, 8 tabs *(dropped, and reported)* |
| Widget text | 2 KB per node, 64 KB per tree *(truncated, and reported)* |
| Storage | 128 keys, 64 chars per key, 8 KB per value, 64 KB total |
| Reminders | 64 pending, 64 chars per id, 200 chars of text, up to ten years ahead |
| Log | 200 lines of 512 characters *(truncated quietly)* |
| Input box | 8 KB *(truncated quietly)* |

A step is one statement, one turn of a loop, or one step of a regular
expression's matching. Going past the container, string or recursion limit
stops the call with an error that `try` cannot catch.

A plugin that runs out of steps or time is stopped, the user is told, and it
takes a strike. So does one the watchdog has to abandon for going unresponsive.
Two strikes and the plugin is switched off until they turn it back on, which
also forgives the strikes. That last part is a setting: **Switch off a plugin
that hangs**, on the Plugins screen, on by default. With it off the strikes are
still counted and still shown, and only the automatic switch-off goes away.
