Plugin API reference
Everything a plugin can use: manifest, widgets, events, wm.*, limits.
Everything a plugin can use. If it is not here, it does not exist.
The manifest
Section titled “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 | — | 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 | — | Shown on the install screen. |
description | — | One or two sentences. |
apiVersion | — | The API level you need. 1. A higher number than the app knows is refused. |
entry | — | The script inside the archive. Defaults to main.lua. |
permissions | — | See Permissions. Usually []. |
Your two functions
Section titled “Your two functions”function render() --> a widget, or a list of widgetsfunction on_event(e) -- optionalrender() 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().
Both are plain globals. There is no registration step.
Widgets
Section titled “Widgets”Built by ui.*, which are pure-Lua helpers that return tables — you can write
the tables yourself if you prefer.
Layout
Section titled “Layout”ui.column { child, child, ... } -- stacked verticallyui.row { child, child, ... } -- side by side, equal widthsui.spacer { height = 12 } -- vertical gap, 0-64ui.divider() -- a horizontal lineChildren go in the array part, which is why they have no name = in front.
ui.label { text = "…", style = "title" | "body" | "caption" }body is the default. Labels are not interactive and have no id.
ui.output { id = "result", text = "…", mono = false, -- monospace, for anything with alignment insertable = true, -- show the Insert button (default true) copyable = true, -- show the Copy button (default true)}ui.output is how your results reach the user’s text. The keyboard draws
Insert and Copy buttons under it; tapping Insert puts the text where they are
writing. There is no function that types for them.
Controls
Section titled “Controls”ui.button { id = "go", text = "Go", style = "primary", enabled = true }ui.toggle { id = "caps", label = "Uppercase", checked = false }ui.input { id = "msg", label = "Message", placeholder = "Type here" }style = "primary" highlights a button; anything else is plain. enabled = false greys one out.
ui.input carries no value. The keyboard owns what is in the box: tapping it
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 — only the finished contents of its own box.
ui.tabs { id = "modes", ui.page { title = "First", child, child }, ui.page { title = "Second", child },}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.
Progress
Section titled “Progress”ui.progress() -- an indeterminate barEvents
Section titled “Events”on_event(e) receives a table. Always e.type and e.id; some carry more.
e.type | Extra | Fired when |
|---|---|---|
"click" | — | A button was tapped. |
"toggle" | e.value (boolean) | A toggle was flipped. |
"input_changed" | e.value (string) | The contents of one of your boxes changed. |
"tab_selected" | e.index (number, 1-based) | A tab was picked. |
input_changed fires per keystroke into your own input widget. That is how a
live preview works, and it is bounded to your panel — you are told what is in
your box, never what is typed anywhere else.
The complete host API.
Always available
Section titled “Always available”wm.api_version -- 1wm.plugin_id -- your manifest idwm.plugin_version -- your manifest pluginVersionwm.log(message) -- a line in your log, readable in Settingswm.ui.set_input(id, text) -- write to one of your own input widgetsApplied after your handler returns. Capped at 8 KB.
wm.json.decode(text) --> table, or nil + reasonwm.json.encode(value) --> string, or nil + reasonA Lua table encodes as a JSON array when its keys are exactly 1..n, and as an
object otherwise. Whole numbers stay whole. Depth is capped, which is also what
stops a self-referencing table from encoding forever.
With the storage permission
Section titled “With the storage permission”wm.storage.get(key) --> string, or nilwm.storage.set(key, val) --> true, or nil + reasonwm.storage.remove(key)wm.storage.keys() --> list of stringsStrings only — use wm.json for anything structured. Local to your plugin, local
to the device, deleted when the user uninstalls you. set returns nil and a
reason when you are over quota; say something rather than losing the data
silently.
If the manifest did not declare storage, wm.storage is simply nil.
Not present, and never will be
Section titled “Not present, and never will be”wm.text, wm.clipboard, wm.http, wm.net, wm.files — there is no API for
reading what the user types, reading the field, reading the clipboard, or
reaching the network. See Security.
The Lua you get
Section titled “The Lua you get”Lua 5.2 via LuaJ, with these libraries:
Available: string, table, math, bit32, the base functions (assert,
error, getmetatable, setmetatable, ipairs, pairs, next, pcall,
xpcall, rawget, rawset, rawequal, rawlen, select, tonumber,
tostring, type, _G, _VERSION), print, and a reduced os.
os has os.time(), os.clock() and os.date(format, time) — with "*t"
and a strftime subset (%Y %y %m %d %H %M %S %j %p %A %a %B %b %c %x %X %%), and
a leading ! for UTC. Nothing else: no getenv, execute, exit, remove,
rename or tmpname.
Absent: io, require, package, coroutine, debug, luajava, load,
loadstring, loadfile, dofile. You cannot load code at runtime, and
precompiled Lua is refused at install.
collectgarbage exists but does nothing and returns 0.
Limits
Section titled “Limits”Exceeding one of these is an error you can see, not a silent truncation, except where noted.
| Script size | 256 KB |
| Archive size | 1 MB, 16 entries |
| Instructions | 30M on load, 20M per event, 4M per render |
| Time | 3 s on load, 2 s per event, 0.5 s per render |
| Widgets | 256 nodes, 12 deep, 8 tabs |
| Widget text | 2 KB per node, 64 KB per tree (truncated, and reported) |
string.rep output | 256 KB |
| Pattern subject / pattern | 256 KB / 256 bytes |
table.concat output | 1 MB |
| Storage | 128 keys, 64 chars per key, 8 KB per value, 64 KB total |
| Log | 200 lines of 512 characters |
| Input box | 8 KB |
A plugin that runs out of instructions or time is stopped and the user is told. One that becomes unresponsive twice is switched off until they turn it back on.
