Skip to content
WM KeyboardWM Keyboard
Accessibility

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.

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

FieldRequiredMeaning
formatyesAlways "wmkeyboard-plugin". The one tag that identifies the file.
versionnoContainer format version. 1.
idyesUnique, lowercase, 3–64 characters of a-z 0-9 . _ -, starting alphanumeric. Becomes the plugin’s folder on the device. Reverse-DNS by convention.
nameyesShown everywhere. Trimmed to 40 characters, control and direction-override characters removed.
pluginVersionyesYour version, e.g. "1.2.0".
authornoShown on the install screen.
descriptionnoOne or two sentences.
apiVersionnoThe API level you need. 1. A higher number than the app knows is refused.
entrynoThe script inside the archive. Defaults to main.lua.
permissionsnoSee Permissions. Usually [].
function render() --> a widget, or a list of widgets
function on_event(e) -- optional

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().

Both are plain globals. There is no registration step.

Built by ui.*, which are pure-Lua helpers that return tables. You can write the tables yourself if you prefer.

ui.column { child, child, ... } -- stacked vertically
ui.row { child, child, ... } -- side by side, equal widths
ui.spacer { height = 12 } -- vertical gap, 0-64
ui.divider() -- a horizontal line

Children 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. A tap on Insert puts the text where they are writing. There is no function that types for them.

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

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.

ui.progress() -- an indeterminate bar

on_event(e) receives a table. Always e.type and e.id. Some carry more.

e.typeExtraFired when
"click"noneA 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, 0-based)A tab was picked.

tab_selected counts from 0, not from 1 the way the rest of Lua does. The first page is 0.

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.

The complete host API.

wm.api_version -- 1
wm.plugin_id -- your manifest id
wm.plugin_version -- your manifest pluginVersion
wm.log(message) -- a line in your log, readable in Settings
wm.ui.set_input(id, text) -- write to one of your own input widgets

Applied after your handler returns. Capped at 8 KB.

wm.json.decode(text) --> table, or nil + reason
wm.json.encode(value) --> string, or nil + reason

A 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.

wm.storage.get(key) --> string, or nil
wm.storage.set(key, val) --> true, or nil + reason
wm.storage.remove(key)
wm.storage.keys() --> list of strings

Strings only, so 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 nil.

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.

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). os.date takes "*t" and a strftime subset (%Y %y %m %d %H %M %S %j %p %A %a %B %b %c %x %X %%), plus 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.

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 size256 KB
Archive size1 MB, 16 entries
Instructions30M on load, 20M per event, 4M per render
Time3 s on load, 2 s per event, 0.5 s per render
Widgets256 nodes, 12 deep, 8 tabs (dropped, and reported)
Widget text2 KB per node, 64 KB per tree (truncated, and reported)
string.rep output256 KB
Pattern subject / pattern256 KB / 256 bytes
table.concat output1 MB
Storage128 keys, 64 chars per key, 8 KB per value, 64 KB total
Log200 lines of 512 characters (truncated quietly)
Input box8 KB (truncated quietly)

A plugin that runs out of instructions 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.