Плагины WexClient пишутся на Lua и запускаются в песочнице: свой экран, свои разрешения, свой пакет. Ниже — весь API: интерфейс, платформа, работа с железом и правила публикации.
423методов40разделов37уровень APILua 5.2песочница
Читаемая версия для ИИ
Один файл со всем API в Markdown — дайте эту ссылку ассистенту, и он прочитает документацию целиком.
Writing a plugin: the contractКак писать плагин: контракт
13
#A plugin is two things: metadata and Plugin.build(ui)
The smallest plugin that runs is below. Plugin.name and function Plugin.build(ui) are required; everything else is optional. A single .lua file is a complete plugin. A .wexp package is a zip with manifest.json, main.lua, and optional lib/ and assets/ folders, nothing else.
Минимальный работающий плагин — ниже. Plugin.name и function Plugin.build(ui) обязательны, всё остальное необязательно. Один файл .lua — уже готовый плагин. Пакет .wexp — это zip с manifest.json, main.lua и необязательными папками lib/ и assets/, больше ничего.
Plugin.name = "My Plugin"functionPlugin.build(ui)
ui:title("Hello")
end
#ui exists only inside build; capture it if a second screen needs it
ui is the argument of Plugin.build, not a global. Container callbacks receive their own builder (usually named c or row) which works the same way. If another function needs the top-level builder, store it in an upvalue during build.
ui — это аргумент Plugin.build, а не глобальная переменная. Колбэки контейнеров получают собственный билдер (обычно c или row), который работает так же. Если билдер верхнего уровня нужен другой функции, сохраните его в локальную переменную во время build.
local UI
functionPlugin.build(ui)
UI = uiui:card(function(c)
c:text("c is a builder too")
end)
endfunctionopenSecond()
UI:push("Details", function(c) c:text("second screen") end)
end
ui:text(...) and handle:width(...) are method calls: the colon is required. Every builder returns a handle, and every style setter returns the same handle, so styling is a chain. Keep the handle in a variable when the value has to change later.
ui:text(...) и handle:width(...) — вызовы методов: двоеточие обязательно. Каждый билдер возвращает хендл, а каждый стилевой сеттер возвращает тот же хендл, поэтому оформление пишется цепочкой. Сохраняйте хендл в переменную, если значение потом должно меняться.
local out = ui:text("waiting"):color("muted"):margin("sm")
ui:button("Go", function()
out:set("done"):color("success")
end):width("match")
#Change a widget through its handle, never by rebuilding the screen
There is no way to re-run build. A number that ticks is a handle you keep and call :set() on; a list that grows is ui:list with a render callback and list:count(n) to change its length. Rebuilding is what makes a plugin flicker and lose scroll position.
Перезапустить build нельзя. Меняющееся число — это сохранённый хендл, которому вы вызываете :set(); растущий список — это ui:list с колбэком render и list:count(n) для смены длины. Пересборка — причина мигания и потери позиции прокрутки.
local rows = {}
local list = ui:list({ count = 0, render = function(row, i)
row:item({ title = rows[i].title, subtitle = rows[i].subtitle })
end })
-- later, when data arrives
rows[#rows + 1] = { title = "new", subtitle = "row" }
list:count(#rows)
#Callbacks fire during creation and on :set(), not only on a tap
The host applies a widget's initial value through the same listener a user gesture uses, so some callbacks run before the builder call has even returned its handle. Inside the constructor: ui:segmented calls fn(initial, label) every time (initial defaults to 1); ui:radio calls fn only when initial is passed; ui:bottomNav runs the initial pane's build and then onChange(initial); ui:tabs runs its builder for the initial tab. From code: :set(i) on segmented and radio, :set(bool) and :toggle() on switch and checkbox, :set(v) on a slider when the value changes, and :set(text) on input, textarea, number, password and search all invoke the callback exactly like a gesture. Silent both at creation and on :set(): chips, multiChips, dropdown, stepper, rating. Write every callback as if it can run at any moment: everything it touches — local functions, handles, tables — must already exist, or the callback checks a flag and returns. A callback that reaches a name defined further down the file fails with attempt to call nil at the callback's line, and the screen finishes building without it.
Хост применяет начальное значение виджета через тот же слушатель, что и жест пользователя, поэтому часть колбэков срабатывает ещё до того, как билдер вернул хендл. Внутри конструктора: ui:segmented вызывает fn(initial, label) всегда (initial по умолчанию 1); ui:radio вызывает fn только если передан initial; ui:bottomNav строит начальную панель и затем вызывает onChange(initial); ui:tabs запускает билдер для начальной вкладки. Из кода: :set(i) у segmented и radio, :set(bool) и :toggle() у switch и checkbox, :set(v) у слайдера при смене значения, :set(text) у input, textarea, number, password и search вызывают колбэк точно так же, как жест. Молчат и при создании, и при :set(): chips, multiChips, dropdown, stepper, rating. Пишите каждый колбэк так, будто он может сработать в любой момент: всё, к чему он обращается — локальные функции, хендлы, таблицы — должно уже существовать, либо колбэк проверяет флаг и выходит. Колбэк, который обращается к имени, определённому ниже по файлу, падает с attempt to call nil на строке колбэка, а экран достраивается без него.
runs inside the constructorsegmented (always) · radio (when initial is given) · bottomNav onChange(initial) · tabs buildersegmented (всегда) · radio (если передан initial) · bottomNav onChange(initial) · билдер tabs
runs on :set() / :toggle()segmented · radio · switch · checkbox · slider · input · textarea · number · password · searchsegmented · radio · switch · checkbox · slider · input · textarea · number · password · search
never runs from codechips · multiChips · dropdown · stepper · ratingchips · multiChips · dropdown · stepper · rating
-- wrong: refresh is assigned below, so it is nil while the widget is createdui:segmented({ "All", "Favorites" }, function(i)
favOnly = (i == 2)
refresh() -- history.lua:29 attempt to call nilend, 1)
localfunction refresh() list:count(#rows) end-- right: state and functions first, widgets second, a flag until the screen is completelocal ready, favOnly, list = false, false, nillocalfunction refresh()
ifnot ready thenreturnend
list:count(#rows)
endui:segmented({ "All", "Favorites" }, function(i)
favOnly = (i == 2)
refresh()
end, 1)
list = ui:list({ count = 0, render = renderRow })
ready = true
refresh()
#A handle has only the methods its widget documents
Every builder returns a handle. All of them share the style chain (size, margin, padding, color, bg, border, radius, alpha, visible / show / hide, enabled, onClick / onLongClick, the animations, :text(s) for anything that shows text, :bind(key), :remove()). Methods that read or change a value exist only where the reference lists them, and calling one that is not there fails with attempt to call nil: a button has :text(s) but no :set, a stepper has :set(v) but no :onChange, a kv handle points at the value column. The table below is the complete map; a widget not in it has only the shared chain.
Каждый билдер возвращает хендл. У всех есть общая цепочка стилей (размеры, margin, padding, color, bg, border, radius, alpha, visible / show / hide, enabled, onClick / onLongClick, анимации, :text(s) для всего, что показывает текст, :bind(key), :remove()). Методы, которые читают или меняют значение, есть только там, где их перечисляет справочник; вызов несуществующего метода падает с attempt to call nil: у кнопки есть :text(s), но нет :set, у stepper есть :set(v), но нет :onChange, хендл kv указывает на колонку значения. Таблица ниже — полная карта; виджет, которого в ней нет, имеет только общую цепочку.
local count = ui:kv("Selected", "0") -- kv handle = the value columnlocal btn = ui:button("Generate", generate)
count:set("3")
btn:text("Generate 3") -- every text-bearing handle has :text(s)-- btn:set("Generate 3") -- attempt to call nil: not a button methodlocal n = ui:stepper("Count", 1, 10, 3, function(v) count:set(tostring(v)) end)
n:set(5) -- silent: stepper :set never calls fn
count:set("5") -- so update dependants yourself
#The nested ui is the full ui (screen calls excepted)
The ui passed to a container builder — ui:card / column / row / grid / hscroll / expander / tabs / stack / bottomNav, a handle's :rebuild / :add / :ui, ui:view, ui:load's render, and list / carousel / accordion rows — is the same full ui as the root. Every widget and every framework call is available on it, ui:load / ui:view / nested containers included, and what you add lands inside that container. The only exceptions are screen-level calls, which always act on the whole screen no matter which ui you use: navigation (ui:push / pop / back / close), the toolbar (ui:setTitle / setSubtitle / fab / hideFab / actions / menu / scrollTo), ui:keyboard, and the modal overlays (ui:toast / snack / dialog / alert / confirm / prompt / sheet / sheetList).
ui, переданный в билдер контейнера — ui:card / column / row / grid / hscroll / expander / tabs / stack / bottomNav, методы хендла :rebuild / :add / :ui, ui:view, render у ui:load, строки list / carousel / accordion — это тот же полный ui, что и корневой. На нём доступны все виджеты и все каркасные вызовы, включая ui:load / ui:view / вложенные контейнеры, а добавленное попадает внутрь этого контейнера. Исключение — только экранные вызовы: они всегда действуют на весь экран, каким бы ui вы ни пользовались: навигация (ui:push / pop / back / close), тулбар (ui:setTitle / setSubtitle / fab / hideFab / actions / menu / scrollTo), ui:keyboard и модальные окна (ui:toast / snack / dialog / alert / confirm / prompt / sheet / sheetList).
local results = ui:card(function(c) c:text("Loading…") end)
results:rebuild(function(c)
-- c is a full ui: c:load works here
c:load(function(done) Wex.http(url, done) end, function(c2, res)
c2:list(rowsFrom(res))
end)
end)
There is no sleep and no synchronous request. Plugin.build has four seconds and every hook has three; a loop that waits is killed by the watchdog. Network, files, devices and timers all take a function and call it back on the main thread.
Ни sleep, ни синхронных запросов нет. У Plugin.build четыре секунды, у каждого хука три; цикл ожидания убивает сторож. Сеть, файлы, устройства и таймеры принимают функцию и вызывают её на главном потоке.
io, os.execute, os.remove, os.getenv, dofile, loadfile, load, loadstring, package and debug are not there. Files go through Wex.files (the plugin's own folder) or Wex.pickFile (the user picks). Time is Wex.now and Wex.time. require works only for lib/ modules inside the package.
io, os.execute, os.remove, os.getenv, dofile, loadfile, load, loadstring, package и debug отсутствуют. Файлы — через Wex.files (папка самого плагина) или Wex.pickFile (файл выбирает пользователь). Время — Wex.now и Wex.time. require работает только для модулей из lib/ внутри пакета.
-- io.open("/sdcard/log.txt", "w") -- does not existWex.files.write("log.txt", text) -- the plugin's own folderlocal util = require("util") -- lib/util.lua inside the package
#Declare a permission before using the call it guards
A call whose permission is missing is refused and writes a line into the plugin log; it does not throw. Risk Check also compares the two directions and reports a permission declared but never used. Wex.can(name) tells you at runtime what was granted, since the user can revoke one after installing.
Вызов без объявленного разрешения отклоняется и пишет строку в лог плагина, исключения не будет. Risk Check дополнительно сверяет обе стороны и сообщает про объявленное, но неиспользуемое разрешение. Wex.can(имя) во время работы показывает, что выдано: пользователь может отозвать разрешение после установки.
Plugin.permissions = { "network", "bluetooth" }
ifnotWex.can("bluetooth") thenui:banner("Bluetooth was revoked in the plugin settings", "warning")
returnend
#Lua 5.2 rules apply, including the reserved words
This is Lua 5.2 without bitwise operators, integer division or goto. A reserved word cannot be a bare table key: write ["repeat"] = true. Concatenation needs strings, so wrap numbers in tostring or use string.format. Arrays are 1-based and # gives the length.
Это Lua 5.2 без битовых операторов, целочисленного деления и goto. Зарезервированное слово нельзя использовать как ключ напрямую: пишите ["repeat"] = true. Конкатенация требует строк, поэтому оборачивайте числа в tostring или используйте string.format. Массивы нумеруются с единицы, длина — #.
Everything that exists is listed in this reference. If a call is not here, it does not exist, and guessing produces a plugin that fails at the first tap. Wex.has(feature) and Wex.api.level exist precisely so a plugin can check instead of assuming.
Всё, что существует, перечислено в этом справочнике. Если вызова здесь нет, его нет и в системе, а догадка даёт плагин, который падает на первом нажатии. Wex.has(возможность) и Wex.api.level существуют именно для того, чтобы проверять, а не предполагать.
ifWex.has("usb") andWex.api.level >= 7thenopenPort()
elseui:caption("Update WexClient for USB")
end
#How an error looks on the device, and where the log is
Two places, two looks. If the script itself or the top-level Plugin.build throws, the screen keeps whatever was built and a red line ⚠ file:line message is appended under it; the same happens when build overruns its 4-second budget (plugin execution timed out). Everything the host calls later runs inside a guard — a tap, a selection callback, a timer, a network callback, a bottomNav or tabs pane builder, a list row renderer, the lifecycle hooks with their 3-second budget: an error there shows a red toast ⚠ file:line message with a traceback, writes ✖ message to the plugin log, abandons that one callback and leaves the plugin running. So a half-built pane plus a toast means a callback failed mid-build. The log is under Plugins → long-press → Logs, readable in code with Wex.log.lines(), and the hot-reload client tails it on the desktop. The wording is LuaJ's: attempt to call nil (a function defined further down, or a method the handle does not have), attempt to index ? (a nil value) (a handle or table that was never assigned), bad argument (wrong type). Wrap genuinely optional calls in pcall and check Wex.has / Wex.api.level instead of guessing.
Два места, два вида. Если падает сам скрипт или верхнеуровневый Plugin.build, экран сохраняет всё построенное и под ним добавляется красная строка ⚠ файл:строка сообщение; то же при превышении бюджета build в 4 секунды (plugin execution timed out). Всё, что хост вызывает позже, выполняется под защитой — тап, колбэк выбора, таймер, сетевой колбэк, билдер панели bottomNav или tabs, рендер строки списка, хуки жизненного цикла с бюджетом 3 секунды: ошибка там показывает красный тост ⚠ файл:строка сообщение с трейсбеком, пишет ✖ сообщение в лог плагина, прерывает только этот колбэк и оставляет плагин работать. Наполовину построенная панель плюс тост означают, что колбэк упал посреди сборки. Лог лежит в Плагины → долгое нажатие → Логи, читается из кода через Wex.log.lines(), а клиент hot reload показывает его на компьютере. Формулировки — от LuaJ: attempt to call nil (функция, определённая ниже, или метод, которого у хендла нет), attempt to index ? (a nil value) (хендл или таблица, которые так и не были присвоены), bad argument (не тот тип). Действительно необязательные вызовы оборачивайте в pcall и проверяйте Wex.has / Wex.api.level, а не угадывайте.
-- the toast after a failing callback:-- ⚠ lib/screens/history.lua:29 attempt to call nil-- stack traceback:-- lib/screens/history.lua:29: in function 'segmented'-- reading it: line 29 called something that was still nil at that momentWex.log("history built, rows=" .. tostring(#rows)) -- Plugins → long-press → Logslocal ok, cfg = pcall(function() returnWex.assets.json("presets.json") end)
ifnot ok thenWex.log.warn("presets.json missing: " .. tostring(cfg))
cfg = {}
end
Plugin.name and Plugin.build present. Every guarded call has its permission declared, and nothing extra is declared. No loop waits for anything. Every handle whose value changes is stored in a variable. Timers, scans, ports and connections are released in Plugin.onClose. Every callback checks res.ok before reading the rest. No API name that is not in this reference. Every callback is safe to run before the rest of the screen exists.
Есть Plugin.name и Plugin.build. У каждого защищённого вызова объявлено разрешение, и ничего лишнего не объявлено. Ни один цикл ничего не ждёт. Каждый хендл с меняющимся значением сохранён в переменную. Таймеры, сканы, порты и соединения освобождаются в Plugin.onClose. Каждый колбэк проверяет res.ok перед чтением остального. Ни одного имени API, которого нет в этом справочнике. Каждый колбэк безопасно вызывать до того, как построен остальной экран.
functionPlugin.onClose()
Wex.cancelAllTimers()
Wex.ble.stopScan()
if port then port:close() endend
Plugins run on LuaJ (org.luaj:luaj-jse:3.0.1) — a pure-JVM Lua 5.2 interpreter, NOT LuaJava and not a native/JNI Lua. No reflection or Java class reaches a script. The sandbox starts from LuaJ's standard globals, then strips every escape hatch: io, require, package, load/loadstring/dofile/loadfile, luajava, and the dangerous os.* (execute, exit, remove, rename, tmpname, getenv, setlocale); the debug table is hidden. A per-instruction debug hook enforces a wall-clock timeout, so a runaway loop is interrupted instead of freezing the app. Everything a plugin can reach comes through the Wex and ui tables — nothing else.
Плагины исполняются на LuaJ (org.luaj:luaj-jse:3.0.1) — чистом JVM-интерпретаторе Lua 5.2, НЕ LuaJava и не нативном/JNI-Lua. Ни рефлексии, ни Java-классов скрипту недоступно. Песочница берёт стандартные globals LuaJ и срезает все лазейки: io, require, package, load/loadstring/dofile/loadfile, luajava и опасные os.* (execute, exit, remove, rename, tmpname, getenv, setlocale); таблица debug скрыта. Per-instruction debug-хук держит настенный таймаут, поэтому зациклившийся код прерывается, а не вешает приложение. Всё, до чего дотягивается плагин, идёт через таблицы Wex и ui — и только через них.
-- these are NOT available inside a plugin (stripped by the sandbox):-- io, require, package, load, loadstring, dofile, loadfile, luajava,-- os.execute, os.exit, os.remove, os.rename, os.getenv, debug
print(os.time(), os.date("%H:%M")) -- safe os.* stays
Every plugin run gets its OWN fresh Globals: its own Lua state, its own copy of the standard library, its own global table. Plugins never share a Lua state, so one plugin cannot read or overwrite another's globals, functions or upvalues. Shared code loaded with Wex.import runs in a child environment whose reads fall through to the importer's globals but whose writes stay local — shared code, not shared authority. The only cross-plugin channel is explicit and data-only: Wex.bus.
Каждый запуск плагина получает СВОЙ свежий Globals: свой lua-state, свою копию стандартной библиотеки, свою таблицу globals. Плагины никогда не делят lua-state, поэтому один плагин не может прочитать или перезаписать globals, функции или upvalue другого. Общий код через Wex.import выполняется в дочернем окружении: чтения проваливаются в globals импортёра, а записи остаются локальными — общий код, но не общие полномочия. Единственный межплагинный канал — явный и только для данных: Wex.bus.
local util = Wex.import("acme.util") -- runs in a child env: sees your globals,-- but its own writes never leak into yoursWex.bus.emit("track", { id = 7 }) -- the only cross-plugin channel (data only)
ui:* builds real Android Views — the platform widgets and Material Components (LinearLayout, MaterialCardView, MaterialButton, RecyclerView, WebView, …) — not a Compose tree and not an HTML shim. Each builder returns a handle: a Lua table wrapping the View, carrying the shared style chain (size, color, margin, padding, radius, alpha, visibility, animations, :bind, :remove) plus that widget's own methods. All UI construction and mutation runs on the Android main thread; anything slow you start (http, image, folder, io) runs on a background executor and calls you back on the main thread.
ui:* строит настоящие Android View — системные виджеты и Material Components (LinearLayout, MaterialCardView, MaterialButton, RecyclerView, WebView, …), а не дерево Compose и не HTML-обёртку. Каждый билдер возвращает хендл: lua-таблицу поверх View с общей цепочкой стилей (размеры, цвет, margin, padding, radius, alpha, видимость, анимации, :bind, :remove) плюс методами самого виджета. Всё построение и изменение UI идёт в главном потоке Android; всё медленное, что вы запускаете (http, image, folder, io), уходит на фоновый executor и возвращается колбэком в главный поток.
local card = ui:card() -- a MaterialCardViewlocal btn = ui:button("Go") -- a MaterialButton, returned as a handle
btn:bg("#2563EB"):radius(12):onClick(function() ui:toast("hi") end)
Permissions are not honour-based. Before every sensitive call the Kotlin bridge checks the plugin's DECLARED capabilities (needs("network", …)) and refuses with a readable error if it was never declared — the Lua side cannot bypass this. Capabilities that map to an OS runtime permission (camera, microphone, location, contacts, calendar, bluetooth, …) pass a second gate: RuntimePerms.ensure runs immediately if already granted, otherwise asks the user through the host and reports a denial back to the plugin's error channel. So a sensitive call needs BOTH: declared in the manifest AND granted by the OS/user.
Разрешения не на доверии. Перед каждым чувствительным вызовом Kotlin-мост проверяет ОБЪЯВЛЕННЫЕ возможности плагина (needs("network", …)) и отказывает с читаемой ошибкой, если оно не объявлено — со стороны Lua это не обойти. Возможности, отображаемые на системное разрешение (камера, микрофон, геолокация, контакты, календарь, bluetooth, …), проходят второй гейт: RuntimePerms.ensure срабатывает сразу, если уже выдано, иначе спрашивает пользователя через хост и сообщает об отказе в канал ошибок плагина. Значит, чувствительному вызову нужно И объявление в манифесте, И согласие ОС/пользователя.
-- Without Plugin.permissions = { "network" } this line fails with:-- "Wex.http blocked — declare Plugin.permissions = {\"network\"}"Wex.http({ url = "https://api.example.com" }, function(res) end)
When a plugin screen closes, Plugin.onClose() runs, then the bridge disposes everything transient: timers (every/after/interval) cancelled; open sockets / websockets / TCP / UDP closed; DB handles closed; sensor and location listeners unregistered; audio / TTS / mic / speech / torch / media released; Wex.bus subscriptions removed. In-flight async work is neutralised, not awaited — a disposed flag turns every Lua callback into a no-op, so a late HTTP or download result arriving after close never runs plugin code (and a partial download file is deleted). Persistent state SURVIVES: Wex.storage, Wex.secure, Wex.cache, the plugin DB, files, and granted folders. Transient in-memory state is released with the Lua state for the GC; reopening the plugin starts a brand-new Lua state.
Когда экран плагина закрывается, выполняется Plugin.onClose(), затем мост освобождает всё временное: таймеры (every/after/interval) отменяются; открытые сокеты / websocket / TCP / UDP закрываются; хендлы БД закрываются; слушатели сенсоров и геолокации снимаются; audio / TTS / микрофон / речь / фонарик / media освобождаются; подписки Wex.bus удаляются. Асинхронная работа в полёте гасится, а не дожидается — флаг disposed превращает каждый Lua-колбэк в no-op, поэтому поздний результат HTTP или загрузки, пришедший после закрытия, не выполнит код плагина (а частично скачанный файл удаляется). Постоянное состояние ПЕРЕЖИВАЕТ закрытие: Wex.storage, Wex.secure, Wex.cache, БД плагина, файлы и выданные папки. Временное состояние в памяти освобождается вместе с lua-state под GC; повторное открытие плагина стартует совершенно новый lua-state.
function Plugin.onClose()
-- last chance to persist; runs BEFORE the bridge tears timers/sockets downWex.storage.set("draft", editor:text())
end-- an http() still in flight here will simply never call back (disposed = no-op)
Recommendations and pitfallsРекомендации и типичные ошибки
Risk Check compares what the manifest declares against what the code calls, and reports both directions: a permission you declared but never use looks careless, and a call without its permission is refused at runtime with an error in the log. Add a permission when you add the call, not before.
Risk Check сверяет объявленное в манифесте с тем, что вызывает код, и сообщает про оба перекоса: объявленное, но неиспользуемое разрешение выглядит небрежно, а вызов без разрешения будет отклонён во время работы с ошибкой в логе. Добавляйте разрешение вместе с вызовом, а не заранее.
Plugin.permissions = { "network" } -- and nothing else until you need itifnotWex.can("network") thenui:banner("Offline", "warning")
returnend
Plugin.build has four seconds and every hook has three; a loop that waits for something kills the screen instead of waiting. There is no sleep in the sandbox on purpose. Anything slow is a callback: Wex.http, Wex.timer, Wex.interval, the BLE and USB handlers.
У Plugin.build есть четыре секунды, у каждого хука — три; цикл ожидания не ждёт, а убивает экран. Функции sleep в песочнице нет намеренно. Всё медленное — это колбэк: Wex.http, Wex.timer, Wex.interval, обработчики BLE и USB.
-- wrong: freezes the screen and gets killed by the watchdog-- while not done do end-- rightWex.interval(1000, function()
status:set(Wex.time.now())
end)
Turning the phone destroys and rebuilds the screen, and your locals go with it. What must survive belongs in Wex.state (kept across a hot reload too) or in Wex.storage (kept across restarts). The host restores full-screen mode and the rotation lock before your first frame, so do not re-apply them in build.
Поворот телефона уничтожает и пересобирает экран, локальные переменные исчезают вместе с ним. То, что должно пережить это, живёт в Wex.state (переживает и hot reload) или в Wex.storage (переживает перезапуск). Полноэкранный режим и блокировку ориентации хост восстановит до первого кадра сам, повторно применять их в build не нужно.
functionPlugin.onPause()
Wex.storage.set("draft", input:value())
endfunctionPlugin.build(ui)
local draft = Wex.storage.get("draft") or""
input = ui:field({ value = draft })
end
Timers, BLE scans and connections, USB ports, RFCOMM links, sensors and the NFC reader all keep running until something stops them. The host disposes the bridge when the screen goes away, but a plugin that stops its own work behaves predictably during a hot reload and while the screen is merely paused.
Таймеры, сканы и подключения BLE, USB-порты, каналы RFCOMM, датчики и считыватель NFC работают, пока их не остановят. Хост освобождает мост при уходе с экрана, но плагин, который останавливает свою работу сам, ведёт себя предсказуемо и при hot reload, и когда экран просто на паузе.
functionPlugin.onClose()
Wex.cancelAllTimers()
Wex.ble.stopScan()
Wex.sensors.stopAll()
if port then port:close() endend
Most phones have no infrared blaster, many have no NFC, and USB host mode is not universal. Every hardware namespace answers available() without asking for anything, so branch on it and show a clear empty state instead of failing silently.
У большинства телефонов нет инфракрасного передатчика, у многих нет NFC, а режим USB-host есть не везде. Каждое аппаратное пространство имён отвечает на available() ничего не запрашивая, поэтому ветвитесь по нему и показывайте понятную заглушку вместо тихого отказа.
ifnotWex.ir.available() thenui:empty("info", "No IR blaster", "This phone cannot send infrared")
returnend
A plugin installed on an older build must degrade, not crash. minApi in the manifest stops the install outright; Wex.api.level and Wex.has(feature) let one plugin work on both. Hardware arrived at level 7, share targets and hot reload at level 6.
Плагин, установленный на старую сборку, должен деградировать, а не падать. Поле minApi в манифесте запрещает установку совсем; Wex.api.level и Wex.has(feature) позволяют одному плагину работать и там и там. Работа с железом появилась на уровне 7, приём «Поделиться» и hot reload — на уровне 6.
ifWex.api.level >= 7andWex.has("usb") thenopenSerial()
elseui:caption("Update WexClient for USB support")
end
Network, file and device results carry ok and error, and reading a field of a failed result is how a plugin ends up showing nil. Check ok first; it costs one line and turns a crash into a message.
Результаты сети, файлов и устройств несут ok и error, а чтение поля неудачного результата — это как раз то, из-за чего плагин показывает nil. Проверяйте ok первым: одна строка превращает падение в сообщение.
#repeat, end, function: Lua keywords cannot be field names
Wex.ir.repeat would be a parse error, which is why that call is named resend and the Pronto result field is repeatPattern. If you ever need a reserved name on a table, index it with brackets.
Wex.ir.repeat — это синтаксическая ошибка, поэтому вызов называется resend, а поле результата Pronto — repeatPattern. Если зарезервированное имя в таблице всё же нужно, обращайтесь через скобки.
Wex.ir.resend({ protocol = "nec", address = 0x04, command = 0x08 }, 3, 40)
local alias = Wex.ir["repeat"] -- the same function, awkward spelling
#Build long lists with ui:list, not a loop of widgets
A render callback creates rows as they scroll into view; a for loop over a thousand items creates a thousand views before the first frame. The same applies to charts: push points into an existing handle instead of rebuilding the screen.
Колбэк render создаёт строки по мере прокрутки; цикл for по тысяче элементов создаёт тысячу View до первого кадра. То же с графиками: добавляйте точки в существующий хендл, а не пересобирайте экран.
ui:list({ count = #items, render = function(row, i)
row:item({ title = items[i].name, subtitle = items[i].ip })
end })
margin, padding, gap and radius accept xs, sm, md, lg, xl and xxl. Screens built from tokens share one rhythm, survive a design change, and give an assistant a vocabulary to reuse instead of inventing numbers.
margin, padding, gap и radius принимают xs, sm, md, lg, xl и xxl. Экраны, собранные на токенах, имеют общий ритм, переживают смену дизайна и дают ассистенту готовый словарь вместо выдуманных чисел.
A package can carry lib/name.lua files that require("name") loads, and assets/ files that ship with the plugin. One 2000-line main.lua is hard to review, hard for an assistant to edit and impossible to reuse.
Пакет может нести файлы lib/name.lua, которые загружает require("name"), и файлы assets/, которые едут вместе с плагином. Один main.lua на две тысячи строк тяжело читать, тяжело править ассистентом и невозможно переиспользовать.
-- lib/net.lualocal M = {}
function M.ping(host, cb) Wex.net.ping(host, cb) endreturn M
-- main.lualocal net = require("net")
#Never build UI text from raw input without escaping
ui:html and ui:web render what you give them. Anything that came from the network, a file, an NFC tag or a serial port is untrusted; pass it through Wex.str.escapeHtml or put it in a plain ui:text, which never interprets markup.
ui:html и ui:web отображают то, что им дали. Всё, что пришло из сети, файла, NFC-метки или последовательного порта — недоверенное; пропускайте это через Wex.str.escapeHtml или показывайте обычным ui:text, который разметку не интерпретирует.
ui:text(res.body) -- safeui:html(Wex.str.escapeHtml(res.body)) -- safe-- ui:html(res.body) -- not safe
The editor can start a small HTTP server on your Wi-Fi and re-run the preview after every save from VS Code, Zed or Neovim, keeping Wex.state across reloads. The same menu exports wex-api.def.lua, which gives the Lua Language Server full completion for ui, Wex and Plugin.
Редактор поднимает небольшой HTTP-сервер в вашей сети и перезапускает предпросмотр после каждого сохранения из VS Code, Zed или Neovim, сохраняя Wex.state между перезапусками. Это же меню экспортирует wex-api.def.lua, который даёт Lua Language Server полное автодополнение по ui, Wex и Plugin.
# on the computer, Node 18+, nothing to install
node wex-dev.mjs "http://192.168.1.20:8765/?token=abcd2345" ./my-plugin
#Ship assets with the package instead of downloading them
An icon or a data file inside assets/ works offline, needs no network permission and cannot change under you. Download at runtime only what genuinely has to be fresh.
Иконка или файл данных внутри assets/ работает офлайн, не требует разрешения на сеть и не может подмениться. Скачивайте во время работы только то, что действительно должно быть свежим.
ui:image("asset:logo.png"):width(120)
local cfg = Wex.assets.readJson("defaults.json")
The id decides what an update replaces; the version decides whether the app offers the update at all. Changing the id publishes a second, unrelated plugin and orphans the user's data and granted permissions.
Идентификатор решает, что именно заменит обновление; версия решает, предложит ли приложение обновление вообще. Смена идентификатора публикует второй, никак не связанный плагин и оставляет данные и выданные разрешения пользователя сиротами.
Plugin.id = "ip_info"-- never changesPlugin.version = "1.3"-- goes up with every release
margin / padding / gap / radius all accept "xs"(4) / "sm"(8) / "md"(12) / "lg"(16) / "xl"(24) / "xxl"(32) — or "full" for radius — instead of a raw dp number. Every screen then shares one rhythm, and a script pasted into an AI has a vocabulary to reuse instead of inventing new numbers each time.
margin / padding / gap / radius принимают "xs"(4) / "sm"(8) / "md"(12) / "lg"(16) / "xl"(24) / "xxl"(32) — или "full" для radius — вместо произвольного dp. Тогда у всех экранов один ритм, а у вставленного в ИИ скрипта есть готовый словарь вместо новых чисел каждый раз.
Pass gap as the 2nd argument to ui:row / ui:column / ui:card instead of :marginStart()/:marginEnd() on every child — one number, applied evenly, nothing forgotten on the first or last item.
Передавай gap вторым аргументом в ui:row / ui:column / ui:card вместо :marginStart()/:marginEnd() на каждом ребёнке — одно число, применяется равномерно, ничего не забыто у первого/последнего элемента.
A card (or a row of cards) reads as one idea. Avoid nesting more than ~2 containers deep — flatten with sibling row/column calls instead of boxes inside boxes inside boxes.
Карточка (или ряд карточек) читается как одна мысль. Не вкладывай больше ~2 контейнеров — используй соседние row/column вместо коробок в коробках.
title / subtitle / text / caption already encode the type scale (20/16/14/11sp). Drop to :textSize()/:color() only for a genuine one-off accent, not for routine headings.
title / subtitle / text / caption уже задают шкалу типографики (20/16/14/11sp). К :textSize()/:color() обращайся только для настоящего разового акцента, не для обычных заголовков.
title + a card of kv rows + a badge for state + one action button. A complete, consistent screen in a dozen lines, built entirely from the primitives above.
title + карточка со строками kv + бейдж состояния + одна кнопка действия. Цельный, согласованный экран в десяток строк — целиком из примитивов выше.
functionPlugin.build(ui)
ui:title("Tunnel")
ui:card(function(c)
c:row(function(r)
r:subtitle("Status"):grow()
r:badge("connected", "success")
end)
c:kv("Endpoint", "10.0.4.1:51820")
c:kv("Latency", "23 ms")
c:kv("Data", "128 MB ↓ · 9 MB ↑")
end, "sm")
ui:spacer("md")
ui:button("Disconnect", function()
Wex.confirm({ title = "Disconnect?" }, function(ok)
if ok thenui:toast("disconnected") endend)
end):width("match")
end
Check the data first and show ui:empty instead of a blank card — a list with zero rows is a state to design for, not a bug to ignore.
Сначала проверь данные и покажи ui:empty вместо пустой карточки — список из нуля строк — это состояние для дизайна, а не баг, который можно игнорировать.
if #items == 0thenui:empty("info", "No scans yet", "Run a scan to see results here")
elseui:list({ count = #items,
render = function(row, i) row:kv(items[i].name, items[i].ip) end })
end
ui:theme() returns the whole system: { dark, color = { primary, surface, onSurface, onSurfaceVariant, success, warning, danger, info, … }, space, radius, type, motion }. Shortcuts: ui:color("primary") → "#RRGGBB", ui:space("lg") → 16, ui:radius("md"), ui:type("title"), ui:dark() → bool. Use the same tokens the widgets use when you draw a canvas, colour a chart or branch on the theme — nothing hard-coded, and it follows light/dark automatically.
ui:theme() возвращает всю систему: { dark, color = { primary, surface, onSurface, onSurfaceVariant, success, warning, danger, info, … }, space, radius, type, motion }. Короткие формы: ui:color("primary") → "#RRGGBB", ui:space("lg") → 16, ui:radius("md"), ui:type("title"), ui:dark() → bool. Бери те же токены, что и виджеты, когда рисуешь canvas, красишь график или ветвишься по теме — ничего не захардкожено, и всё само подстраивается под светлую/тёмную.
Put custom fonts (.ttf/.otf) and images in your plugin’s assets/ folder, then apply them anywhere: :font("assets/fonts/Manrope.ttf") on any text handle, and :bgImage("assets/bg.png", { fit, radius, overlay }) on cards, rows and most views. One package carries its own look, works offline, and stays consistent across screens.
Сложи свои шрифты (.ttf/.otf) и картинки в папку assets/ плагина и применяй где угодно: :font("assets/fonts/Manrope.ttf") на любом текстовом хендле и :bgImage("assets/bg.png", { fit, radius, overlay }) на карточках, рядах и большинстве вью. Один пакет несёт свой вид, работает оффлайн и выглядит одинаково на всех экранах.
For anything that changes, prefer ui:view over hand-held handles. Put the truth in Wex.state, describe the screen inside the view, and set state to change it — the container re-renders itself. Reach for :show()/:hide()/:set() only for a single widget you are updating in a tight loop (a counter, a progress bar), where a full rebuild would be wasteful.
Всё, что меняется, лучше описывать через ui:view, а не держать пачку хендлов. Держи истину в Wex.state, описывай экран внутри view и меняй состояние — контейнер перерисуется сам. Ручные :show()/:hide()/:set() оставь для одиночного виджета, который обновляешь часто (счётчик, прогресс), где полная пересборка была бы лишней.
Optional metadata shown under the name and used for update detection.
Необязательные данные под именем; используются для детекта обновлений.
versione.g. "1.2" — compared numerically when re-installing.напр. "1.2" — сравнивается численно при переустановке.
authoryour name; shown as the credited author.твоё имя; показывается как автор.
Plugin.version = "1.0"Plugin.author = "you"
#API levels & changelog (set minApi to the lowest you need)
Wex.api.level rises as the platform gains features; manifest.minApi is the lowest level your plugin needs — older apps refuse to install instead of crashing. 5 — full-screen play mode, orientation, ui:web. 6 — share targets (Plugin.handles) and desktop hot reload. 7 — hardware: Wex.ir / nfc / usb / bt / wifi. 8 — Wex.ok / err result guards, storage schema migrations, Wex.apiLevel. 9 — library plugins (Plugin.provides / exports, Wex.import), the cross-plugin Wex.bus, Plugin.settings, and expressive widgets (ui:splitButton, ui:wavyProgress, ui:floatingToolbar). 10 — the Expressive motion system: :spring physics, :shape / :shapeMorph, :glass / :blur, :shared container-transform on ui:push, and predictive back (Plugin.onBackPredictive), and the ui:scaffold screen chrome (collapsing large title, FAB, pull-to-refresh). 11 — media & device: Wex.image processing, ui:video, Wex.torch. 12 — reactive SQLite (db:live/onChange), a fused compass (Wex.sensors.orientation), streamed download progress/cancel. 13 — exact alarms (Wex.alarm) and on-device speech recognition (Wex.audio.listen). 14 — read-only content providers: the address book (Wex.contacts) and the agenda (Wex.calendar). 15 — a still photo from the system camera app (Wex.camera.capture). 16 — durable folder access: pick a folder once, read and write inside it across runs (Wex.folder). 17 — structured WebView RPC: a ui:web page calls Wex.call(method, args) and Lua answers with w:handle. 18 — launch another app for its result (Wex.intent.startForResult). 19 — binary read/write in a granted folder (folder:read / folder:write). 20 — biometric / device-credential auth gate (Wex.auth.biometric). 21 — background media session with lock-screen / notification transport controls (Wex.media). 22 (WexClient Next) — declarative binary-packet synthesis & parse DSL (Wex.packet). 23 — typed schema-validated service RPC across plugins with semver discovery (Wex.bus.provide/discover/call). 24 — composable DSP pipeline over the primitives (Wex.dsp.graph). 25 — declarative plug-and-play USB device drivers (Wex.driver). 26 — wex-pm: versioned semantic imports, Wex.import(name, semverRange). 27 — unified privilege: root (Wex.su) and Shizuku shell (Wex.shizuku), consented and audited. 28 — distributed cross-plugin state with HLC/LWW convergence (Wex.bus.state). 29 — an aspect-oriented action pipeline: observe/transform/intercept/replace/fire (Wex.hook). 30 — zero-lag reconciliation: ui:view coalesces state-driven rebuilds to one per display frame. Anything above your minApi must be guarded with Wex.api.level or Wex.has(feature).
Wex.api.level растёт по мере появления возможностей; manifest.minApi — минимальный нужный уровень, старое приложение откажется устанавливать, а не упадёт. 5 — полноэкранный режим, ориентация, ui:web. 6 — цели «Поделиться» (Plugin.handles) и hot reload с компьютера. 7 — железо: Wex.ir / nfc / usb / bt / wifi. 8 — стражи результата Wex.ok / err, миграции схемы storage, Wex.apiLevel. 9 — библиотечные плагины (Plugin.provides / exports, Wex.import), межплагинная шина Wex.bus, Plugin.settings и экспрессивные виджеты (ui:splitButton, ui:wavyProgress, ui:floatingToolbar). 10 — выразительная система движения: :spring-физика, :shape / :shapeMorph, :glass / :blur, :shared container-transform на ui:push и предиктивный back (Plugin.onBackPredictive) и chrome экрана ui:scaffold (схлопывающийся крупный заголовок, FAB, pull-to-refresh). 11 — медиа и устройство: обработка Wex.image, ui:video, Wex.torch. 12 — реактивный SQLite (db:live/onChange), слитый компас (Wex.sensors.orientation), загрузка потоком с прогрессом/отменой. 13 — точные будильники (Wex.alarm) и распознавание речи на устройстве (Wex.audio.listen). 14 — контент-провайдеры только для чтения: адресная книга (Wex.contacts) и календарь (Wex.calendar). 15 — снимок через системную камеру (Wex.camera.capture). 16 — постоянный доступ к папке: выдать папку один раз, читать и писать в ней между запусками (Wex.folder). 17 — структурный WebView-RPC: страница ui:web вызывает Wex.call(method, args), Lua отвечает через w:handle. 18 — запуск другого приложения ради результата (Wex.intent.startForResult). 19 — бинарные чтение/запись в выданной папке (folder:read / folder:write). 20 — биометрия / учётные данные устройства как гейт (Wex.auth.biometric). 21 — фоновая медиасессия с управлением на экране блокировки / в шторке (Wex.media). 22 (WexClient Next) — декларативный DSL синтеза и разбора бинарных пакетов (Wex.packet). 23 — типизированный RPC сервисов между плагинами со схемой и semver-discovery (Wex.bus.provide/discover/call). 24 — компонуемый DSP-конвейер поверх примитивов (Wex.dsp.graph). 25 — декларативные plug-and-play USB-драйверы устройств (Wex.driver). 26 — wex-pm: версионные семантические импорты, Wex.import(name, semverRange). 27 — единая привилегия: root (Wex.su) и shell через Shizuku (Wex.shizuku), с согласием и аудитом. 28 — распределённое межплагинное состояние с HLC/LWW-сходимостью (Wex.bus.state). 29 — аспектно-ориентированный конвейер действий: observe/transform/intercept/replace/fire (Wex.hook). 30 — zero-lag reconciliation: ui:view коалесцирует перестройки по состоянию до одной на кадр. Всё, что выше вашего minApi, нужно защищать через Wex.api.level или Wex.has(feature).
ifWex.api.level >= 9thenlocal lib = Wex.import("crypto_utils")
endifWex.has("wavyProgress") thenui:wavyProgress({ value = 30 }) end
Makes the plugin a Share target: MIME patterns it accepts from the system Share sheet (a link, text, photos, documents, several files at once). The user picks the plugin in the sheet (each handler is its own row) or in a chooser; files are copied into the plugin's own folder and the plugin opens with Wex.args.share. No permission needed — the user chose you.
Делает плагин целью «Поделиться»: MIME-шаблоны, которые он принимает из системного меню (ссылка, текст, фото, документы, несколько файлов сразу). Пользователь выбирает плагин прямо в меню (каждый обработчик — отдельная строка) или в списке; файлы копируются в папку плагина, и он открывается с Wex.args.share. Разрешение не нужно — пользователь выбрал вас сам.
"text/plain"shared text and links (Wex.args.share.text).текст и ссылки (Wex.args.share.text).
"image/*"any image; "*/*" accepts everything, "application/pdf" one exact type.любое изображение; "*/*" — всё подряд, "application/pdf" — один точный тип.
Plugin.handles = { "text/plain", "image/*" }
functionPlugin.build(ui)
local s = Wex.args.share
if s and s.text thenui:text(s.text) endfor _, f inipairs(s and s.files or {}) doui:image(f.name) -- already a file in Wex.filesendend
Turn a plugin into a library other plugins reuse. Plugin.provides lists the API name(s) this plugin publishes; Plugin.exports() returns the table of functions under those names. A consumer lists Plugin.requires and pulls the table in with Wex.import(name). A pure library has exports and no Plugin.build — it never shows a screen. Imported code runs inside the caller's sandbox with the caller's permissions, so importing a library never grants extra power.
Превращает плагин в библиотеку, которую переиспользуют другие. Plugin.provides перечисляет имя(имена) API, которое плагин публикует; Plugin.exports() возвращает таблицу функций под этими именами. Потребитель указывает Plugin.requires и получает таблицу через Wex.import(name). Чистая библиотека имеет exports и не имеет Plugin.build — у неё нет своего экрана. Импортированный код выполняется в песочнице вызывающего с его разрешениями, поэтому импорт библиотеки не даёт лишних прав.
-- library plugin (no Plugin.build)
Plugin.name = "Text utils"
Plugin.provides = { "text_utils" }
function Plugin.exports()
return { shout = function(s) return s:upper() .. "!"end }
end-- consumer plugin
Plugin.requires = { "text_utils" }
local text = Wex.import("text_utils")
Optional contribution: when defined, the host shows a ⚙ action in the toolbar that opens a dedicated settings screen built by this function. Same ui builder as Plugin.build, on its own pushed screen.
Необязательная точка расширения: если задана, хост показывает действие ⚙ в тулбаре, открывающее отдельный экран настроек, который строит эта функция. Тот же билдер ui, что и в Plugin.build, на отдельном экране.
function Plugin.settings(ui)
ui:title("Settings")
ui:switch("Dark mode", Wex.storage.get("dark") == "1", function(on)
Wex.storage.set("dark", on and"1"or"0")
end)
end
Packages (.wexp), modules & assetsПакеты (.wexp), модули и ассеты
A plugin is a zip with the .wexp extension. main.lua is the entry point (unchanged from single-file plugins); lib/ holds modules for require(); assets/ holds any files — png, jpg, mp3, wav, json, xml, csv, html, fonts. Single-file .lua plugins keep working: they are wrapped into the same layout on install.
Плагин — это zip с расширением .wexp. main.lua — точка входа (как у одиночных плагинов); в lib/ лежат модули для require(); в assets/ — любые файлы: png, jpg, mp3, wav, json, xml, csv, html, шрифты. Одиночные .lua продолжают работать: при установке они оборачиваются в ту же структуру.
Written by the editor from Plugin.* fields when you don't write one yourself. icon is an emoji or a package path like "assets/icon.png" (used in lists and on the forum). minApi = the Wex.api.level your plugin needs — older apps refuse to install instead of crashing. runtime is "lua".
Редактор генерирует его из полей Plugin.*, если вы не написали свой. icon — эмодзи или путь внутри пакета вроде "assets/icon.png" (показывается в списках и на форуме). minApi — нужный Wex.api.level: старое приложение откажется устанавливать, а не упадёт. runtime — "lua".
Loads lib/name.lua (dots map to folders: require("net.http") → lib/net/http.lua or lib/net/http/init.lua). Cached per run; a module returning nothing yields true. Only the package's own lib/ is reachable.
Загружает lib/name.lua (точки — папки: require("net.http") → lib/net/http.lua или lib/net/http/init.lua). Кэшируется на запуск; модуль без return даёт true. Доступен только lib/ своего пакета.
-- lib/utils.lualocal M = {}
function M.fmt(ms) returnWex.format.duration(ms) endreturn M
-- main.lualocal utils = require("utils")
Load the public API of the library plugin that declared Plugin.provides = { name } and return its Plugin.exports() table. The result is cached, so a name is a singleton per run (like require). The library runs in this plugin's sandbox with this plugin's permissions — shared code, not shared authority. Returns nil (with a logged error) when no enabled plugin provides name (a disabled provider is not resolved), on a dependency cycle, or on a self-import. Works in the foreground and in background jobs. Plugin.exports may be a function (called) or a table (used as-is).
Загружает публичный API библиотечного плагина, объявившего Plugin.provides = { name }, и возвращает его таблицу Plugin.exports(). Результат кэшируется, поэтому имя — синглтон на запуск (как require). Библиотека выполняется в песочнице этого плагина с его разрешениями — общий код, а не общие права. Возвращает nil (с записью в лог), если ни один включённый плагин не предоставляет name (выключенный провайдер не резолвится), при цикле зависимостей или импорте самого себя. Работает на переднем плане и в фоновых задачах. Plugin.exports может быть функцией (вызывается) или таблицей (используется как есть).
local text = Wex.import("text_utils")
if text thenui:text(text.shout("hi")) end
#Wex.import(name, range?) → exports — range: a semver range
The versioned form of Wex.import (a wex-pm slice): pass a semantic-version range as the second argument and the import binds only if the installed provider's version satisfies it — otherwise it fails loudly (returns nil and reports an error) instead of silently binding an incompatible library. Ranges: exact "1.2.3", caret "^2.3.0" (>=2.3.0 <3.0.0; for 0.x, ^0.2.3 means >=0.2.3 <0.3.0), tilde "~1.2.0" (>=1.2.0 <1.3.0), comparators and space-separated conjunctions ">=1.0 <2.0", or "*" / omitted for any version. The provider's version is its Plugin.version (manifest version). Pair it with Plugin.requires to pin a compatible dependency.
Версионная форма Wex.import (срез wex-pm): передай semver-диапазон вторым аргументом, и импорт свяжется только если версия установленного провайдера ему удовлетворяет — иначе он падает явно (возвращает nil и сообщает об ошибке), а не связывает молча несовместимую библиотеку. Диапазоны: точная "1.2.3", caret "^2.3.0" (>=2.3.0 <3.0.0; для 0.x, ^0.2.3 значит >=0.2.3 <0.3.0), tilde "~1.2.0" (>=1.2.0 <1.3.0), сравнения и конъюнкции через пробел ">=1.0 <2.0", или "*" / без аргумента — любая версия. Версия провайдера — это его Plugin.version (версия манифеста). Сочетай с Plugin.requires, чтобы закрепить совместимую зависимость.
Plugin.requires = { "acme.dsp" }
local dsp = Wex.import("acme.dsp", "^2.3.0") -- nil + error if the installed acme.dsp is <2.3.0 or >=3.0.0if dsp thenlocal mags = dsp.fft(samples)
elseWex.toast("please install acme.dsp 2.x")
end
#Wex.hook.observe/transform/intercept/replace(action, [prio,] fn) fire(action, ctx?) → result off(id) has(action)
An aspect-oriented action pipeline for a plugin's own extensibility — the plugin, and any library it imports into its VM via Wex.import, can hook named actions the plugin fires. Four hook kinds run in priority order (higher first) when you fire(action, ctx): observe(fn) sees the context (no change); transform(fn) returns a new context, chained through each transform; intercept(fn) returns true to VETO, so fire returns { consumed = true } and stops; replace(fn) supplants the default and its return becomes fire's result (the highest-priority replace wins). fire(action, ctx) runs the pipeline and returns the transformed context, a replacement, or { consumed = true }; an action with no hooks returns ctx untouched. Each register returns an id for off(id); has(action) tests for any hook. A thrown hook is caught and logged, never breaking the pipeline. Same-VM and per-plugin — the sandbox-appropriate way a library extends its host plugin's behaviour without reaching into another plugin's VM.
Аспектно-ориентированный конвейер действий для расширяемости самого плагина — плагин и любая библиотека, импортированная в его VM через Wex.import, могут перехватывать именованные действия, которые плагин запускает. При fire(action, ctx) четыре типа хуков выполняются по приоритету (выше — раньше): observe(fn) видит контекст (без изменений); transform(fn) возвращает новый контекст, сцепляется через каждый transform; intercept(fn) возвращает true для ВЕТО, тогда fire возвращает { consumed = true } и останавливается; replace(fn) подменяет поведение по умолчанию, и его возврат становится результатом fire (побеждает replace с наибольшим приоритетом). fire(action, ctx) прогоняет конвейер и возвращает преобразованный контекст, замену или { consumed = true }; действие без хуков возвращает ctx без изменений. Каждая регистрация возвращает id для off(id); has(action) проверяет наличие. Исключение в хуке ловится и логируется, не ломая конвейер. В той же VM и per-plugin — подходящий для песочницы способ, которым библиотека расширяет поведение своего плагина, не залезая в чужую VM.
-- the plugin exposes an extensible "export" action; a library it imports can hook it tooWex.hook.transform("export", 50, function(ctx)
ctx.rows = dropHidden(ctx.rows); return ctx
end)
Wex.hook.intercept("export", 100, function(ctx)
if #ctx.rows == 0thenWex.toast("nothing to export"); returntrueend-- vetoend)
local result = Wex.hook.fire("export", { rows = data, format = "csv" })
ifnot result.consumed then writeExport(result) end
Read-only access to assets/. Names may carry an asset: prefix. dataUri() feeds ui:image / ui:web; copyToFiles() clones a file into the writable Wex.files sandbox.
Доступ к assets/ только на чтение. Имена могут быть с префиксом asset:. dataUri() подходит для ui:image / ui:web; copyToFiles() копирует файл в записываемую песочницу Wex.files.
local cfg = Wex.assets.json("config.json")
for _, f inipairs(Wex.assets.list("sounds")) doprint(f) end
ui:image("asset:hero.png"), ui:icon("asset:btn.png"), ui:avatar("asset:me.jpg"), ui:web("asset:page.html"), Wex.audio.play("asset:boom.mp3"). No network permission needed — the file ships inside the package.
ui:image("asset:hero.png"), ui:icon("asset:btn.png"), ui:avatar("asset:me.jpg"), ui:web("asset:page.html"), Wex.audio.play("asset:boom.mp3"). Разрешение network не нужно — файл едет внутри пакета.
#Your own icons: icon = "assets/icon.svg" wherever an icon is accepted
Every icon slot takes a built-in name, an emoji, or an icon file from the package: ui:icon, ui:item{icon}, ui:tile, ui:stat{icon}, button:icon, ui:iconButton, ui:actions{icon}, ui:fab, input:icon / :endIcon, ui:dialog{icon}, ui:avatar and manifest.icon. Formats: png/jpg/webp/gif as-is; Android vector drawable XML and SVG (paths, circle/rect/ellipse/line/polyline/polygon, groups with transforms) are rasterized by WEX. Monochrome vectors are tinted to the theme like built-ins — download a Material icon from fonts.google.com as SVG or Android XML, drop it into assets/ and reference it.
Любой слот иконки принимает встроенное имя, эмодзи или файл из пакета: ui:icon, ui:item{icon}, ui:tile, ui:stat{icon}, button:icon, ui:iconButton, ui:actions{icon}, ui:fab, input:icon / :endIcon, ui:dialog{icon}, ui:avatar и manifest.icon. Форматы: png/jpg/webp/gif как есть; Android vector drawable XML и SVG (path, circle/rect/ellipse/line/polyline/polygon, группы с transform) WEX растеризует сам. Монохромные векторы окрашиваются в цвет темы как встроенные — скачай иконку Material с fonts.google.com в SVG или Android XML, положи в assets/ и укажи путь.
The IDE edits the whole package. The explorer button lists every file grouped by role (rename, duplicate, delete, replace an asset); open files become tabs. Typing offers completion from this very catalog with signatures and descriptions, inserting a template whose caret lands where you continue. Diagnostics run the real Lua compiler plus manifest, sandbox, require() and permission checks — findings appear in Problems, in the gutter and on the status bar, and tapping one jumps to the line. Run checks first and only then previews. Save installs the package; the workspace is the draft and survives closing the app.
Студия редактирует весь пакет. Кнопка проводника показывает все файлы, сгруппированные по роли (переименовать, дублировать, удалить, заменить ассет); открытые файлы становятся вкладками. При вводе работает автодополнение из этого же справочника — с сигнатурами и описанием, вставкой шаблона и курсором там, где продолжать. Диагностика запускает настоящий компилятор Lua плюс проверки манифеста, песочницы, require() и разрешений — находки видны в «Проблемах», в нумерации строк и в статус-баре, тап переходит к строке. «Запустить» сначала проверяет и только потом открывает превью. «Сохранить» устанавливает пакет; рабочая папка — черновик и переживает закрытие приложения.
#Forum: packages upload as .wexp, plain scripts stay compatible
Publishing a plugin with lib/ or assets/ creates the post and uploads the archive (up to 20 MB); the card shows files and size, Install downloads and unpacks. A plugin that is just main.lua is published the old way so older WEX builds can still install it. Opening a .wexp from a file manager or a chat installs it too.
Публикация плагина с lib/ или assets/ создаёт пост и загружает архив (до 20 МБ); карточка показывает файлы и размер, «Установить» скачивает и распаковывает. Плагин из одного main.lua публикуется по-старому, чтобы старые сборки WEX могли его поставить. Открытие .wexp из файлового менеджера или чата тоже устанавливает его.
Called once after the plugin has been replaced by a newer version, before the first Plugin.build. Installing an update rewrites only the package — main.lua, lib/ and assets/. Everything the plugin owns lives in a separate folder that install never touches: Wex.storage, Wex.secure, Wex.files and the Wex.db database all survive intact. That is exactly why the hook exists: when the new code expects a different shape, this is where the old data is migrated, before anything reads it.
Вызывается один раз после замены плагина новой версией, до первого Plugin.build. Установка обновления переписывает только пакет: main.lua, lib/ и assets/. Всё, что принадлежит плагину, лежит в отдельной папке, которую установка не трогает: Wex.storage, Wex.secure, Wex.files и база Wex.db остаются нетронутыми. Ради этого хук и нужен: если новый код ожидает другую структуру, именно здесь старые данные мигрируют, пока их никто не прочитал.
oldVersionthe version that was installed before, as a string.версия, которая стояла раньше, строкой.
newVersionthe version that has just been installed.версия, которая только что установлена.
functionPlugin.onUpdate(oldVersion, newVersion)
if oldVersion == "1.0"then-- 1.1 keeps hosts as a table instead of one stringlocal single = Wex.storage.get("host")
if single thenWex.storage.setJson("hosts", { single })
Wex.storage.remove("host")
endendWex.log("migrated " .. oldVersion .. " -> " .. newVersion)
end
A link that installs a package in one tap. The app downloads it over https only, then runs the same flow a file tap uses: the manifest is read, the risk report is shown, and nothing is installed until the user confirms. Useful for an author distributing a .wexp from their own site; a plugin published on the forum is opened with wex://report/<id> instead, where the app already has the account that may install it.
Ссылка, которая ставит пакет в одно нажатие. Приложение скачивает его только по https, а дальше идёт тот же путь, что и при открытии файла: читается манифест, показывается отчёт о рисках, и без подтверждения пользователя ничего не устанавливается. Годится автору, который раздаёт .wexp со своего сайта; плагин с форума открывается через wex://report/<id>, где у приложения уже есть аккаунт, которому разрешена установка.
<a href="wex://install?url=https://example.com/my-plugin.wexp">Install in WEX</a>
Every field a .wexp package's manifest.json may carry. Only name is required; a single-file .lua plugin needs no manifest at all (its Plugin.* fields stand in). Types: strings unless noted. id — unique package id (reverse-DNS, e.g. "com.me.ipinfo"); name (required) — display name; version, author, description — text; icon — an emoji OR an asset path ("assets/icon.png"); main — entry script (default "main.lua"); runtime — "lua"; minApi — number, the lowest Wex.api.level the package needs (0 = any); permissions — string[] of capability ids; handles — string[] of MIME patterns making the plugin a Share target ("text/plain", "image/*"); provides — string[] of library API names this package exports (Wex.import); requires — string[] of library API names it imports. Fields on Plugin.* in the script and in manifest.json mean the same thing; the manifest wins for a package.
Все поля, которые может нести manifest.json пакета .wexp. Обязательно только name; однофайловому .lua-плагину манифест не нужен вовсе (его заменяют поля Plugin.*). Типы: строки, если не указано иное. id — уникальный id пакета (reverse-DNS, напр. "com.me.ipinfo"); name (обязательно) — отображаемое имя; version, author, description — текст; icon — эмодзи ИЛИ путь к ассету ("assets/icon.png"); main — входной скрипт (по умолчанию "main.lua"); runtime — "lua"; minApi — число, минимальный Wex.api.level для пакета (0 = любой); permissions — string[] id возможностей; handles — string[] MIME-шаблонов, делающих плагин целью Share ("text/plain", "image/*"); provides — string[] имён библиотечных API, которые пакет экспортирует (Wex.import); requires — string[] имён библиотечных API, которые он импортирует. Поля Plugin.* в скрипте и в manifest.json значат одно и то же; для пакета манифест приоритетнее.
Settings → Plugins → long-press → Share sends the actual file — .lua for a plain script, .wexp when the plugin has lib/ or assets/ — so anyone can re-import it.
Настройки → Плагины → долгое нажатие → Поделиться отправляет сам файл — .lua для одиночного скрипта, .wexp если у плагина есть lib/ или assets/ — его можно снова импортировать.
Forum → Plugins → tap a post → the page shows author, permissions, description (and files + size for a package) → Install. Same name already installed → Update / Reinstall / Separate copy.
Форум → Плагины → нажми на пост → страница с автором, разрешениями, описанием (для пакета — ещё файлы и размер) → Установить. То же имя уже стоит → Обновить / Переустановить / Отдельная копия.
Write the plugin in VS Code, Zed or Neovim and see every save on the phone. The editor starts a small HTTP server inside WEX (port 8765, Wi-Fi only) with a token that is new on every start; the dialog shows the full URL. Files pushed to it land in this editor's workspace, the open tab refreshes, and the preview re-runs in place — same screen, same rotation, Wex.state carried over. The server lives while the editor is open and stops when you install the plugin (the workspace folder is gone then).
Пишите плагин в VS Code, Zed или Neovim и видьте каждое сохранение на телефоне. Редактор поднимает маленький HTTP-сервер внутри WEX (порт 8765, только по Wi-Fi) с токеном, новым на каждый запуск; диалог показывает полный URL. Присланные файлы попадают в рабочую папку этого редактора, открытая вкладка обновляется, а предпросмотр перезапускается на месте: тот же экран, та же ориентация, Wex.state сохраняется. Сервер живёт, пока открыт редактор, и останавливается при установке плагина (рабочая папка удаляется).
# on the computer, once (Node 18+, no npm install):
node wex-dev.mjs "http://192.168.1.20:8765/?token=abcd2345" ./my-plugin
# or push one file on save (VS Code "Run on Save", a Makefile, anything):
curl -T main.lua -H "X-Wex-Token: abcd2345" http://192.168.1.20:8765/files/main.lua
#wex-dev.mjs · init <name> · defs <url> · pack [folder] · GET / · PUT /files/<path> · POST /reload · GET /log?since=N · GET /defs
The dialog exports wex-dev.mjs, a dependency-free Node 18+ script with four jobs. init <name> --permissions a,b scaffolds a folder: manifest.json, main.lua with a working Plugin.build for the permissions you asked for, lib/util.lua, assets/icon.svg and a .luarc.json that points the Lua Language Server at the types. defs <url> downloads wex-api.def.lua from the phone, so the completion always matches the build you are testing against. With a URL and a folder it syncs once, watches, pushes each changed file and tails the plugin log. The HTTP API behind it is plain: every request carries X-Wex-Token (or ?token=), paths are relative to the workspace, bodies are raw file contents, and /log long-polls up to 25 s. Dotfiles, node_modules and the definitions file are never pushed.
Диалог экспортирует wex-dev.mjs — скрипт для Node 18+ без зависимостей, у него четыре задачи. init <имя> --permissions a,b разворачивает каркас: manifest.json, main.lua с рабочим Plugin.build под запрошенные разрешения, lib/util.lua, assets/icon.svg и .luarc.json, который указывает Lua Language Server на файл типов. defs <url> скачивает wex-api.def.lua с телефона, поэтому автодополнение всегда совпадает со сборкой, на которой вы тестируете. С URL и папкой он один раз синхронизирует, следит за изменениями, отправляет каждый изменённый файл и выводит лог плагина. HTTP API под ним простой: каждый запрос несёт X-Wex-Token (или ?token=), пути относительны рабочей папки, тело — содержимое файла, /log держит long-poll до 25 с. Dot-файлы, node_modules и файл типов не отправляются.
# a new plugin, types included, in one command
node wex-dev.mjs init my-sensor-tool --permissions "bluetooth,files" --url "http://192.168.1.20:8765/?token=abcd2345"
# then edit in VS Code and let the phone follow along
node wex-dev.mjs "http://192.168.1.20:8765/?token=abcd2345" ./my-sensor-tool
Secondary heading (16sp bold) and small muted meta text (11sp) — semantic sizes from the type scale, so screens stay visually consistent without guessing sizes.
Второстепенный заголовок (16sp, жирный) и мелкий приглушённый текст (11sp) — семантические размеры для единообразия экранов без подбора размеров на глаз.
{..}array of option labels.массив подписей вариантов.
fn(index, value)on select; index is 1-based.при выборе; index с 1.
initialoptional — 1-based index to preselect; when given, fn(initial, label) runs inside the constructor.необязательно — индекс с 1 для предвыбора; если передан, fn(initial, label) выполняется внутри конструктора.
initialoptional — preselect: chips take a 1-based index, dropdown an index or the label string. Preselecting this way does not call fn.необязательно — предвыбор: chips принимают индекс с 1, dropdown — индекс или строку-подпись. Такой предвыбор fn не вызывает.
fn(c)builder; `c` is a fresh ui bound to the container.билдер; `c` — новый ui, привязанный к контейнеру.
gapoptional — spacing between children: a dp number or a token ("xs"/"sm"/"md"/"lg"/"xl"/"xxl"). Replaces per-widget default spacing with one uniform value; omit to keep old behavior.необязательно — отступ между детьми: число dp или токен ("xs"/"sm"/"md"/"lg"/"xl"/"xxl"). Заменяет отступы по умолчанию одним значением; не указывай — прежнее поведение сохранится.
A whole screen's chrome in one call: a large title that collapses into the toolbar as the content scrolls (set large=false for a plain toolbar title), an optional subtitle, a FAB, toolbar actions, pull-to-refresh, and the content. fab is { icon, label, onClick }; actions is the same list as ui:actions; refresh(done) is called on a pull and must call done() when it finishes; build(c) fills the content with a normal ui. It decorates the current screen (it is not itself a pushed screen).
Весь chrome экрана одним вызовом: крупный заголовок, схлопывающийся в тулбар по мере прокрутки (large=false — обычный заголовок в тулбаре), необязательный подзаголовок, FAB, действия тулбара, pull-to-refresh и контент. fab — { icon, label, onClick }; actions — тот же список, что у ui:actions; refresh(done) вызывается на потягивании и обязан вызвать done() по завершении; build(c) наполняет контент обычным ui. Оформляет текущий экран (сам он не является push-экраном).
A list. Omit height and it grows to fit all rows (the whole page scrolls). Give a dp height for a fixed scrollable window that virtualizes — only visible rows are built.
Список. Без height растёт под все строки (скроллится вся страница). Число dp — фиксированное окно со скроллом и виртуализацией (строятся только видимые строки).
countnumber of items.число элементов.
heightoptional: a dp number for a fixed scroll window, "match" to fill, or omit to grow to fit.необязательно: число dp для фикс. окна скролла, "match" — заполнить, или опусти, чтобы растянуть под содержимое.
render = fn(row, i)builds item i (1-based) on the given `row` ui.строит элемент i (с 1) на данном `row` ui.
An expressive wavy progress bar. Pass a number 0–100, or { value=, max=, color= }; omit the argument for an indeterminate bar. Drawn by WEX, so it looks the same on every device.
Экспрессивный «волнистый» индикатор прогресса. Передай число 0–100 или { value=, max=, color= }; без аргумента — бесконечный. Рисуется самим WEX, поэтому выглядит одинаково на любом устройстве.
A declarative container: the builder re-runs whenever any watched Wex.state key changes, so you write `if Wex.state.ready then … else … end` instead of tracking handles and calling :show()/:hide() by hand. Pass no keys to react to every state change. h:refresh() forces a rebuild, h:stop() detaches. Watchers drop automatically when the view leaves the screen.
Декларативный контейнер: билдер перезапускается при изменении любого отслеживаемого ключа Wex.state, поэтому пишешь `if Wex.state.ready then … else … end` вместо ручного хранения хендлов и вызовов :show()/:hide(). Без ключей реагирует на любое изменение состояния. h:refresh() пересобрать принудительно, h:stop() отписаться. Наблюдатели снимаются сами, когда вью уходит с экрана.
#ui:view — frame-coalesced reconciliation (≤ 1 rebuild per display frame)
Zero-lag reconciliation for reactive UI. A ui:view rebuilds at most ONCE per display frame no matter how many of its watched Wex.state keys change in between. When a firehose — a multi-megabaud UART, a GNSS / sensor stream, a driver + Wex.dsp.graph pipeline, or a db:live / Wex.bus.state query — updates state many times inside one ~8–16 ms frame, the view coalesces them (via the display Choreographer) into a single rebuild on the next vsync, which sees the latest state: no wasted layout passes, no mid-frame flicker, smooth at 60/120 Hz under load. The first paint is immediate; w:refresh() still forces an immediate rebuild; and writing a key its current value never triggers a rebuild. Pair ui:view with Wex.bus.state or db:live to drive a live dashboard that stays fluid while ingesting a continuous stream.
Zero-lag reconciliation для реактивного UI. ui:view перестраивается не чаще ОДНОГО раза за кадр дисплея, сколько бы его watched-ключей Wex.state ни изменилось между кадрами. Когда firehose — UART на мегабоды, поток GNSS / сенсоров, конвейер драйвер + Wex.dsp.graph или запрос db:live / Wex.bus.state — обновляет состояние много раз за один ~8–16 мс кадр, view коалесцирует их (через дисплейный Choreographer) в одну перестройку на следующем vsync, видящую последнее состояние: без лишних layout-проходов, без дёрганья в середине кадра, плавно на 60/120 Гц под нагрузкой. Первая отрисовка немедленная; w:refresh() по-прежнему форсирует немедленную перестройку; запись ключу его же значения перестройку не вызывает. Сочетай ui:view с Wex.bus.state или db:live для живого дашборда, остающегося плавным при непрерывном потоке.
local v = ui:view({ "snr", "spectrum" }, function(ui)
ui:text(("SNR %.1f dB"):format(Wex.state.snr or0))
ui:spectrum(Wex.state.spectrum)
end)
-- a driver/DSP firehose writes state many times per frame; ui:view rebuilds once per vsync
dev.rx(function(frame)
local r = graph:process(frame)
Wex.state.snr = r.snr
Wex.state.spectrum = r.mag -- coalesced → a single rebuild on the next frameend)
Skeleton instantly, async work off the UI thread, then the real content. start(done) kicks off the op (Wex.http, Wex.worker():next, a timer…) and calls done(result) when finished; render(ui, result) then fills the container on the main thread. One convention: result is a value you check with Wex.ok in render — normalise a Wex.worker :catch to done({ ok = false, error = e }). Heavy work never sits in Plugin.build, so the 4-second build watchdog is never in play. opts is the ui:skeleton shape ({ lines, height, gap }); skeleton = false shows nothing. Leaving the screen invalidates in-flight work, so ui:load is safe to nest; the ui passed to render is a full nested ui — you can call ui:load / ui:view / containers on it again. h:reload() runs start again.
Скелет мгновенно, async-работа вне UI-потока, затем настоящий контент. start(done) запускает операцию (Wex.http, Wex.worker():next, таймер…) и зовёт done(result) по завершении; render(ui, result) наполняет контейнер на главном потоке. Одна конвенция: result проверяется через Wex.ok в render — worker :catch нормализуй в done({ ok = false, error = e }). Тяжёлое не сидит в Plugin.build, поэтому сторож 4 сек не в игре. opts — форма ui:skeleton ({ lines, height, gap }); skeleton = false скрывает плейсхолдер. Уход с экрана инвалидирует незавершённую работу, поэтому ui:load безопасно вкладывать; переданный в render ui — полноценный вложенный ui, на нём снова доступны ui:load / ui:view / контейнеры. h:reload() запускает start заново.
A Z-stack: each layer is its own builder drawn over the previous one, so an interactive widget can sit on top of an image, chart or canvas instead of the strict vertical/horizontal flow of ui:column / ui:row. align places a layer ("center", "bottom_end", …), offsetX/offsetY nudge it in dp, fill = true stretches it. A bare function works as a layer too. h:layer(i) returns that layer's handle.
Z-стек: каждый слой — свой билдер поверх предыдущего, поэтому интерактивный виджет можно положить на картинку, график или canvas, а не выстраивать строго по вертикали/горизонтали как в ui:column / ui:row. align задаёт место слоя ("center", "bottom_end", …), offsetX/offsetY смещают в dp, fill = true растягивает. Голая функция тоже работает как слой. h:layer(i) вернёт хендл слоя.
#ui:bottomNav(items, { initial, position, onChange }) → h
A navigation bar whose panes keep their own state: each pane is built once on first visit and then only shown or hidden, so switching tabs preserves scroll position, typed input and anything the pane created — the part ui:tabs cannot do, because it rebuilds. items are { icon, label, build = fn(pane) }. position "bottom" (default) or "top". h:select(i), h:current(), h:pane(i). The initial pane's build and onChange(initial) run inside the constructor; h:select(i) runs onChange only when the pane actually changes.
Панель навигации, вкладки которой сохраняют своё состояние: каждая панель строится один раз при первом открытии, дальше только показывается и прячется — поэтому при переключении сохраняются прокрутка, введённый текст и всё, что панель создала. Это то, чего не умеет ui:tabs: он пересобирает. items — { icon, label, build = fn(pane) }. position "bottom" (по умолчанию) или "top". h:select(i), h:current(), h:pane(i). build начальной панели и onChange(initial) выполняются внутри конструктора; h:select(i) вызывает onChange только при реальной смене панели.
Outer margins / inner padding — one value for all sides, or four. Each value is a dp number or a token ("xs"=4 "sm"=8 "md"=12 "lg"=16 "xl"=24 "xxl"=32).
Внешние / внутренние отступы — одно значение на все стороны или четыре. Каждое значение — число dp или токен ("xs"=4 "sm"=8 "md"=12 "lg"=16 "xl"=24 "xxl"=32).
The type scale on any text handle: one size + weight vocabulary instead of raw textSize, so headings and body line up across every plugin. ui:type("title") also returns the size in sp if you need the number.
Типографическая шкала на любом текстовом хендле: единый набор размер+начертание вместо голого textSize, чтобы заголовки и текст совпадали во всех плагинах. ui:type("title") также вернёт размер в sp, если нужно число.
Set the typeface: a built-in family, or a custom font shipped in your plugin’s assets/ (.ttf/.otf), loaded once and cached. Preload/validate with ui:font("assets/…") (returns the path or nil).
Задать шрифт: встроенное семейство или свой шрифт из assets/ плагина (.ttf/.otf), загружается один раз и кэшируется. Предзагрузить/проверить через ui:font("assets/…") (вернёт путь или nil).
A background image from the plugin’s assets on any view (and cards). fit = "stretch" | "contain" | "tile"; radius as a token or dp; overlay = a colour drawn over the image so text stays legible. Pairs well with ui:hero and full-bleed cards.
Фоновая картинка из assets плагина на любой вью (и карточках). fit = "stretch" | "contain" | "tile"; radius токеном или в dp; overlay = цвет поверх картинки, чтобы текст оставался читаемым. Хорошо сочетается с ui:hero и карточками во весь экран.
Nil-safe result guards. ok(res) is true only when res is a table whose .ok is set — safe even if the callback got nil. err(res) returns its .error string, or nil. Put them at the top of every callback so a failed result is never read.
Nil-безопасные проверки результата. ok(res) истинно, только если res — таблица с истинным .ok (безопасно даже при nil). err(res) возвращает .error (строку) или nil. Ставьте их в начале каждого колбэка, чтобы упавший результат не читался дальше.
WEX file picker — browse the on-device /WEX folder (bases, downloads, …) with no SAF dialog.
Файлпикер WEX — обзор папки /WEX на устройстве (базы, загрузки, …) без SAF-диалога.
fn(res)res = { name, path, size, content } or { error }. content only for files under 2 MB.res = { name, path, size, content } или { error }. content только для файлов до 2 МБ.
Wex.pickWexFile(function(res)
if res.content thenui:text(res.content) endend)
Durable folder access through the system tree picker (Storage Access Framework). pick(fn) asks the user to grant a folder once; the grant is persisted, so saved() lists the folders THIS plugin was granted as { uri, name } and open(uri) reopens one on a later run without asking again (a plugin never sees a folder another plugin or the app granted). A folder handle carries uri and name and does async I/O: folder:list(fn) → { name, isDir, size, mime, uri }; folder:readText(name, fn); folder:writeText(name, text, fn(ok)) (creates or overwrites); folder:delete(name, fn(ok)). Files are addressed by their direct-child name — a flat view of the chosen folder. Foreground plugins only. Needs Plugin.permissions = {"files"}.
Постоянный доступ к папке через системный выбор дерева (Storage Access Framework). pick(fn) один раз просит пользователя выдать папку; доступ сохраняется, поэтому saved() перечисляет папки, выданные ИМЕННО этому плагину, как { uri, name }, а open(uri) открывает её при следующем запуске без повторного запроса (папку, выданную другим плагином или самим приложением, плагин не видит). Хендл папки хранит uri и name и делает async-I/O: folder:list(fn) → { name, isDir, size, mime, uri }; folder:readText(name, fn); folder:writeText(name, text, fn(ok)) (создаёт или перезаписывает); folder:delete(name, fn(ok)). Файлы адресуются по имени прямого потомка — плоский вид выбранной папки. Только для плагинов на переднем плане. Нужно Plugin.permissions = {"files"}.
Plugin.permissions = { "files" }
Wex.folder.pick(function(dir)
ifnot dir thenreturnend-- user cancelled
dir:writeText("note.txt", "hello", function(ok)
dir:list(function(items)
for _, f in ipairs(items) doWex.log(f.name) endend)
end)
end)
The binary counterparts of readText / writeText on a Wex.folder handle. read hands the file's bytes to fn as a base64 string (nil when the file is missing, unreadable, or larger than 24 MB); write creates-or-overwrites the file from a base64 string and reports ok. base64 is the same convention as Wex.pickBinary and Wex.saveBase64 — so a photo from Wex.camera.capture (via Wex.image … :toBase64()) or any binary can be dropped straight into the granted folder. Both run off the main thread.
Бинарные аналоги readText / writeText у хендла Wex.folder. read отдаёт байты файла в fn как строку base64 (nil, если файла нет, он нечитаем или больше 24 МБ); write создаёт или перезаписывает файл из строки base64 и сообщает ok. base64 — та же схема, что у Wex.pickBinary и Wex.saveBase64, поэтому фото из Wex.camera.capture (через Wex.image … :toBase64()) или любой бинарник можно положить прямо в выданную папку. Обе операции работают вне главного потока.
Wex.folder.pick(function(dir)
ifnot dir thenreturnend
dir:read("photo.jpg", function(b64)
if b64 then
dir:write("copy.jpg", b64, function(ok) Wex.toast(ok and"copied"or"failed") end)
endend)
end)
32-bit bitwise operations for parsing binary packets (vanilla Lua 5.2 has no bitwise operators). LuaJIT-compatible core plus field helpers: Wex.bit.extract(x, from, width) pulls an unsigned bitfield, Wex.bit.test(x, n) checks one bit, Wex.bit.replace(x, v, from, width) writes a field. Example: local hi = Wex.bit.extract(header, 4, 4).
32-битные битовые операции для разбора бинарных пакетов (в ванильном Lua 5.2 нет битовых операторов). Ядро в стиле LuaJIT плюс помощники для полей: Wex.bit.extract(x, from, width) достаёт беззнаковое битовое поле, Wex.bit.test(x, n) проверяет один бит, Wex.bit.replace(x, v, from, width) пишет поле. Пример: local hi = Wex.bit.extract(header, 4, 4).
Signal processing for the mic and radio streams. Wex.dsp.fft(samples) → magnitude spectrum (N/2 bins); dominantFreq(samples, rate) → the strongest frequency in Hz; rms/peak level; hann(n) window. Pure math, runs headless too.
Обработка сигналов для микрофона и радиопотоков. Wex.dsp.fft(samples) → спектр магнитуд (N/2 бинов); dominantFreq(samples, rate) → сильнейшая частота в Гц; уровень rms/peak; окно hann(n). Чистая математика, работает и headless.
Composes the Wex.dsp primitives into a reusable pipeline you declare once and re-run on every block — the DSP surface for RF, audio and sensor analysis. graph{ stages } takes an ordered list of "name:arg" stages: window:hann|hamming, decimate:N, normalize, clip:X, fft (alias magnitude), rms, peak, dominant, peaks:N. g:process(samples, rate?) runs them over a sample array (rate defaults to 44100) and returns a table with whatever the stages produced — samples (the transformed time-domain signal), mag (spectrum magnitude bins), rms, peak, dominant (Hz), peaks (top-N { bin, freq, mag }). Pure and stateless per call, so it runs anywhere — foreground, headless or a worker. Feed it blocks from Wex.audio.record, an SDR/driver, or any sample source to build a live spectrum or a tone / FSK detector.
Собирает примитивы Wex.dsp в переиспользуемый конвейер, который объявляешь один раз и прогоняешь на каждом блоке — surface DSP для RF, аудио и сенсоров. graph{ stages } принимает упорядоченный список стадий "name:arg": window:hann|hamming, decimate:N, normalize, clip:X, fft (алиас magnitude), rms, peak, dominant, peaks:N. g:process(samples, rate?) прогоняет их по массиву сэмплов (rate по умолчанию 44100) и возвращает таблицу с тем, что произвели стадии — samples (преобразованный сигнал во временной области), mag (бины спектра), rms, peak, dominant (Гц), peaks (топ-N { bin, freq, mag }). Чистый и без состояния между вызовами, поэтому работает где угодно — на переднем плане, headless или в worker. Подавай блоки из Wex.audio.record, SDR/драйвера или любого источника сэмплов, чтобы строить живой спектр или детектор тона / FSK.
local g = Wex.dsp.graph{ "window:hann", "fft", "peaks:3", "dominant" }
-- feed it blocks of samples (from Wex.audio.record, an SDR driver, a sensor…)Wex.audio.record({ rate = 44100, frame = 2048, raw = true }, function(block)
local r = g:process(block.pcm, 44100)
Wex.log(("dominant %.0f Hz"):format(r.dominant))
for _, p in ipairs(r.peaks) doWex.log((" peak %.0f Hz"):format(p.freq)) endend)
A declarative binary-protocol DSL — compose byte-aligned layer schemas to build and parse packets (a lightweight Scapy inside Lua), for RF, networking and hardware-bus diagnostics. layer(name, fields) registers a schema and RETURNS a constructor; each field is { name, type, { default, compute, len } } with types u8 / i8 / u16be / u16le / u24be / u32be / u32le / mac / ip4 / bytes. build{ CtorA{…}, CtorB{…} } fills a buffer and resolves computed fields — compute = "auto_len" (length from that layer to the end), "ipv4_checksum" / "checksum16", "crc16" (CCITT) or "sum8". The packet handle gives :bytes() (a binary string to hand a driver or socket), :hex(), :base64(), :len(). parse("Eth/IPv4", bytes) decodes into a nested table (v.IPv4.ttl). Pure — it synthesises and parses bytes, it never transmits; egress stays a separate, capability-gated act. Available in the foreground bridge and in headless / worker contexts.
Декларативный DSL бинарных протоколов — из байт-выровненных схем-слоёв собираешь и разбираешь пакеты (лёгкий Scapy внутри Lua), для RF, сетевой и аппаратно-шинной диагностики. layer(name, fields) регистрирует схему и ВОЗВРАЩАЕТ конструктор; каждое поле — { name, type, { default, compute, len } } с типами u8 / i8 / u16be / u16le / u24be / u32be / u32le / mac / ip4 / bytes. build{ CtorA{…}, CtorB{…} } заполняет буфер и вычисляет поля — compute = "auto_len" (длина от этого слоя до конца), "ipv4_checksum" / "checksum16", "crc16" (CCITT) или "sum8". Хендл пакета даёт :bytes() (бинарная строка для драйвера или сокета), :hex(), :base64(), :len(). parse("Eth/IPv4", bytes) разбирает во вложенную таблицу (v.IPv4.ttl). Чистый — синтезирует и разбирает байты, но не передаёт; отправка — отдельное действие под разрешением. Доступен и на переднем плане, и в headless / worker.
Asks the user for a fingerprint or face — or, with allowDeviceCredential = true, the device PIN / pattern / password — to gate a screen or a stored secret. fn gets { ok = true } on success, or { ok = false, error } when the user cancels or it fails. canAuthenticate() reports whether the device can authenticate right now (hardware present and something enrolled). It uses a weak biometric class: right for gating access, not for unlocking a crypto key. No permission needed — the user always completes the prompt themselves. Foreground plugins only.
Просит у пользователя отпечаток или лицо — либо, при allowDeviceCredential = true, PIN / графический ключ / пароль устройства — чтобы закрыть экран или хранимый секрет. fn получает { ok = true } при успехе или { ok = false, error }, если пользователь отменил или не прошёл. canAuthenticate() сообщает, может ли устройство аутентифицировать прямо сейчас (есть железо и что-то настроено). Используется слабый биометрический класс: годится для ограничения доступа, но не для разблокировки крипто-ключа. Разрешение не нужно — пользователь всегда сам проходит запрос. Только для плагинов на переднем плане.
ifWex.auth.canAuthenticate({ deviceCredential = true }) thenWex.auth.biometric({ title = "Unlock notes", allowDeviceCredential = true }, function(r)
if r.ok then openNotes() elseWex.toast(r.error or"cancelled") endend)
end
Read-only info tables — the same spacing/radius scale the ui: token strings ("sm"/"md"/…) resolve to, as plain numbers for custom math.
Таблицы информации (только чтение) — та же шкала spacing/radius, к которой приводятся строки-токены ui: ("sm"/"md"/…), но как числа для собственных расчётов.
theme{ surface, onSurface, primary, error, … } hex colors; theme.spacing / theme.radius / theme.font = the token scales as numbers (dp/sp).{ surface, onSurface, primary, error, … } hex-цвета; theme.spacing / theme.radius / theme.font — те же шкалы токенов числами (dp/sp).
device{ model, manufacturer, sdk, release, lang }.{ model, manufacturer, sdk, release, lang }.
ui:text(""):color(Wex.theme.primary)
local gap = Wex.theme.spacing.lg -- 16
#Wex.audio.record({ rate, frame, fft, raw }?, fn(data)) Wex.audio.stopRecording()
Captures the microphone as raw PCM. Each frame's data has rms and peak (level), rate, and — opt-in — spectrum (magnitudes, with fft=true) or pcm (normalised samples, with raw=true). For spectrum analysers, acoustic detectors, sonar and FSK/Morse decoders. Needs Plugin.permissions = { "microphone" }.
Захватывает микрофон как сырой PCM. У каждого кадра data есть rms и peak (уровень), rate и — по желанию — spectrum (магнитуды, при fft=true) или pcm (нормализованные сэмплы, при raw=true). Для спектроанализаторов, акустических детекторов, сонара и декодеров FSK/Морзе. Нужно Plugin.permissions = { "microphone" }.
#Wex.overlay.show(fn(ui), { x, y, width, draggable }) .hide() .canDraw() .request()
Floating windows over other apps, built from the same ui: toolkit — diagnostic widgets, dashboards, on-screen controls. The window is draggable by default; :hide() removes it. canDraw() checks the OS grant and request() opens the settings screen to allow it. Needs Plugin.permissions = { "overlay" }.
Плавающие окна поверх других приложений, собранные из того же ui:-тулкита — диагностические виджеты, панели, экранные кнопки. Окно по умолчанию перетаскивается; :hide() убирает его. canDraw() проверяет разрешение ОС, request() открывает экран настроек. Нужно Plugin.permissions = { "overlay" }.
Scans QR codes and barcodes through a full-screen viewfinder (framework Camera2 + zxing, no native library). fn receives { text, format } for each decode; once=true (default) closes after the first, facing="front"/"back". :stop() closes it. Needs Plugin.permissions = { "camera" }.
Сканирует QR-коды и штрихкоды через полноэкранный видоискатель (встроенный Camera2 + zxing, без нативных библиотек). fn получает { text, format } на каждый декод; once=true (по умолчанию) закрывает после первого, facing="front"/"back". :stop() закрывает. Нужно Plugin.permissions = { "camera" }.
Takes one still photo through the system camera app and hands back a real file path — feed it straight to Wex.image.load. fn gets { ok, path } (path is present only when ok is true; ok is false if the user cancels or no camera app is available). The plugin owns the file — it lands in the plugin's own scoped folder, so nothing is written to the shared gallery. name optionally sets the filename. Foreground plugins only. Needs Plugin.permissions = {"camera"}.
Делает один снимок через системное приложение камеры и возвращает настоящий путь к файлу — его можно сразу передать в Wex.image.load. fn получает { ok, path } (path есть только при ok = true; ok = false, если пользователь отменил съёмку или приложения камеры нет). Файл принадлежит плагину — он попадает в его собственную scoped-папку, в общую галерею ничего не пишется. name по желанию задаёт имя файла. Только для плагинов на переднем плане. Нужно Plugin.permissions = {"camera"}.
Decode, transform and re-encode a bitmap so you can downscale/crop/rotate a picked or captured image before uploading or showing it. src is "asset:…", a Wex.files path, a data: URI or base64. The ops chain and mutate the working bitmap; :toBase64/:toDataUri/:save("name.jpg") produce the result (fmt = jpeg|png|webp, q = 1..100) and :exif() reads orientation/date/make/model/GPS. Decoding is capped at 4096 px so a huge source can't OOM. Works in the foreground and in a background job.
Декодирует, преобразует и перекодирует битмап, чтобы уменьшить/обрезать/повернуть выбранное или снятое изображение перед загрузкой или показом. src — "asset:…", путь в Wex.files, data: URI или base64. Операции — цепочкой, меняют рабочий битмап; :toBase64/:toDataUri/:save("name.jpg") дают результат (fmt = jpeg|png|webp, q = 1..100), а :exif() читает ориентацию/дату/производителя/модель/GPS. Декодирование ограничено 4096 px, чтобы не словить OOM. Работает на переднем плане и в фоне.
local img = Wex.image.load("asset:photo.jpg")
img:rotate(90):thumbnail(512)
ui:image(img:toDataUri("jpeg", 80))
img:save("thumb.jpg")
The camera flashlight/LED — no permission required. available() is false on a device with no flash; on()/off()/toggle() return the new on-state. The torch is turned off automatically when the plugin closes.
Фонарик/LED камеры — разрешение не нужно. available() — false, если вспышки нет; on()/off()/toggle() возвращают новое состояние. При закрытии плагина фонарик гасится автоматически.
ifWex.torch.available() thenui:switch("Flashlight", false, function(on) Wex.torch.toggle() end)
end
Fire an Android intent — one API for call, maps, share, opening a link, launching an app or a specific activity.
Отправить Android-intent — один API для звонка, карт, шаринга, открытия ссылки, запуска приложения или конкретной активности.
action"view" / "send" / "dial" / "edit" / … or a full "android.intent.action.X"."view" / "send" / "dial" / "edit" / … или полный "android.intent.action.X".
Launches another app for its result. spec is the same table Wex.intent.start takes — { action, data, type, package, component, category, flags, extras }. When the app returns, fn gets { ok (true on RESULT_OK), code (the raw resultCode), uri (the result's data URI, if any), text (its EXTRA_TEXT, if any), extras (the result's string / number / boolean extras) }. Use it for a barcode scanner, a contact or document picker, a share-with-result, or any app that hands data back. One result at a time; foreground plugins only. Needs Plugin.permissions = {"intent"}.
Запускает другое приложение ради результата. spec — та же таблица, что и у Wex.intent.start: { action, data, type, package, component, category, flags, extras }. Когда приложение вернётся, fn получит { ok (true при RESULT_OK), code (сырой resultCode), uri (URI данных результата, если есть), text (его EXTRA_TEXT, если есть), extras (строковые / числовые / логические extra результата) }. Годится для сканера штрихкодов, выбора контакта или документа, «поделиться с результатом» и любого приложения, которое возвращает данные. По одному результату за раз; только для плагинов на переднем плане. Нужно Plugin.permissions = {"intent"}.
Optional hooks. onBack() returning true consumes the system Back press (e.g. to close your own panel first). onClose() runs when the plugin screen is destroyed. onResize(w, h) and onRotate(landscape) fire when the screen turns — dp, content area, not the raw display; onImmersive(on) when full screen goes on or off. Save on onPause: a rotation may restart the plugin, and the host puts the rotation and full-screen mode back before your first frame.
Необязательные хуки. onBack(), вернувший true, поглощает системный Back (например, чтобы сначала закрыть свою панель). onClose() вызывается при уничтожении экрана плагина. onResize(w, h) и onRotate(landscape) срабатывают при повороте экрана — в dp и по области контента, не по «сырому» дисплею; onImmersive(on) — при входе и выходе из полного экрана. Сохраняйтесь в onPause: поворот может перезапустить плагин, а хост вернёт ориентацию и полноэкранный режим до вашего первого кадра.
functionPlugin.onResume() refresh() endfunctionPlugin.onResize(w, h) canvasHeight = h endfunctionPlugin.onBack()
if panelOpen then panel:hide(); panelOpen = false; returntrueendend
#functionPlugin.onBackPredictive(progress, edge) … end
Optional hook for the predictive back gesture (Android 14+). progress goes 0→1 as the user drags the Back gesture; edge is "left", "right", or "cancel" when the gesture is released without committing. The host already peeks the screen back by default; use this to drive your own gesture-linked animation. A committed gesture then runs the normal Back (see Plugin.onBack).
Необязательный хук жеста предиктивного «назад» (Android 14+). progress идёт 0→1 по мере протягивания жеста; edge — "left", "right" или "cancel", если жест отпущен без подтверждения. Хост по умолчанию уже «отодвигает» экран; используй это для собственной анимации, привязанной к жесту. Подтверждённый жест дальше выполняет обычный Back (см. Plugin.onBack).
function Plugin.onBackPredictive(progress, edge)
panel:alpha(1 - progress)
end
Present when the plugin was opened from the system Share sheet (see Plugin.handles). action is "send" or "send_multiple"; type is the shared MIME type; every entry in files is already a copy in the plugin's folder, so Wex.files.read / readBase64 / share work on it by name. skipped counts files that were too large (24 MB) or unreadable.
Есть, когда плагин открыли из системного «Поделиться» (см. Plugin.handles). action — "send" или "send_multiple"; type — MIME переданного; каждая запись в files уже скопирована в папку плагина, так что Wex.files.read / readBase64 / share работают по имени. skipped — сколько файлов пропущено (больше 24 МБ или не читаются).
local s = Wex.args.share
if s thenif s.text thenWex.clipboard.copy(s.text) endfor _, f inipairs(s.files) doWex.log(f.name .. " " .. f.type .. " " .. f.size)
endend
Lists every library API importable with Wex.import right now: an array of { name, id, plugin } — one entry per Plugin.provides name of each installed, enabled plugin. Use it to discover libraries or check a dependency is present before importing.
Перечисляет все библиотечные API, доступные для Wex.import прямо сейчас: массив { name, id, plugin } — по записи на каждое имя из Plugin.provides каждого установленного включённого плагина. Годится, чтобы найти библиотеки или проверить наличие зависимости перед импортом.
for _, lib in ipairs(Wex.plugins.libraries()) do
print(lib.name .. " (from " .. lib.plugin .. ")")
end
API level (bumped with every API extension), the list of available Wex features, and whether the script declared a permission — degrade gracefully instead of crashing. Wex.apiLevel is a shortcut for Wex.api.level.
Уровень API (растёт с каждым расширением), список доступных возможностей Wex и проверка объявленного разрешения — деградируйте мягко, а не падайте. Wex.apiLevel — короткий алиас для Wex.api.level.
Runs a Lua source string on a background thread with a fresh pure sandbox (Wex.dsp/bit/hash/crypto/json/str/…, no UI or IO) — no 4-second UI watchdog, for parsing big dumps, heavy crypto and DSP. `args` is passed in via JSON as the global `args`; the return value comes back through a promise: :next(fn(result)) / :catch(fn(err)). Both fire on the main thread.
Выполняет строку Lua-кода в фоновом потоке в свежей чистой песочнице (Wex.dsp/bit/hash/crypto/json/str/…, без UI и IO) — без 4-секундного watchdog, для разбора больших дампов, тяжёлой криптографии и DSP. `args` передаётся через JSON как глобальный `args`; результат возвращается промисом: :next(fn(result)) / :catch(fn(err)). Оба вызываются на главном потоке.
A true foreground service that keeps running with a persistent notification after the UI closes — continuous telemetry, an on-device server, long-lived beacons — instead of the 15-minute Plugin.background() quantum. Wex.service.start runs your Plugin.service() on a background thread where its timers Wex.every(ms, fn) / Wex.after(ms, fn) / Wex.cancel(id) keep firing; Wex.service.notify(title, text) updates the notification; Wex.service.stop() ends it. Needs Plugin.permissions = { "background" }. On HyperOS/OneUI and custom kernels the OS may still doze a foreground service with the screen off — call Wex.service.requestUnrestricted() (checks Wex.service.isBatteryOptimized() first) to ask the user to exempt the app from battery optimization.
Настоящий foreground-сервис, который продолжает работать с постоянным уведомлением после закрытия UI — непрерывная телеметрия, сервер на устройстве, долгоживущие маяки — вместо 15-минутного кванта Plugin.background(). Wex.service.start запускает твой Plugin.service() в фоновом потоке, где его таймеры Wex.every(ms, fn) / Wex.after(ms, fn) / Wex.cancel(id) продолжают срабатывать; Wex.service.notify(title, text) обновляет уведомление; Wex.service.stop() завершает. Нужно Plugin.permissions = { "background" }. На HyperOS/OneUI и кастомных ядрах система может усыплять foreground-сервис при выключенном экране — вызови Wex.service.requestUnrestricted() (сначала проверь Wex.service.isBatteryOptimized()), чтобы попросить пользователя исключить приложение из оптимизации батареи.
A scrolling monospace log panel. t:append(line, color?) / t:info / t:ok / t:warn / t:error / t:clear() / t:get() / t:copy() / t:autoScroll(bool). Appends are collected and rendered once per frame, so a serial console at 115200 baud stays readable instead of re-laying out the view on every line.
Прокручиваемая моноширинная лог-панель. t:append(line, color?) / t:info / t:ok / t:warn / t:error / t:clear() / t:get() / t:copy() / t:autoScroll(bool). Добавления накапливаются и отрисовываются раз в кадр, поэтому последовательная консоль на 115200 бод остаётся отзывчивой вместо перелейаута на каждую строку.
heightdp, default 240.dp, по умолчанию 240.
maxLinesring buffer size (default 500).размер кольцевого буфера (по умолчанию 500).
local term = ui:terminal({ height = 260 })
term:info("connecting…")
term:ok("200 OK")
term:error("timeout")
A Material list row: leading emoji/icon/avatar initials, title + subtitle, trailing text or badge, chevron. h:title/subtitle/trailing/badge(text, tone).
Строка списка в стиле Material: эмодзи/иконка/инициалы слева, заголовок + подзаголовок, текст или бейдж справа, шеврон. h:title/subtitle/trailing/badge(text, tone).
ui:item({ icon = "wifi", title = "Office AP", subtitle = "-52 dBm · ch 36", badge = "WPA3", tone = "success",
onClick = function() open(ap) end })
Specialised inputs. search debounces fn as the user types and fires immediately on the keyboard's search key; number:get() returns text, :number() returns a number.
Специализированные поля. search вызывает fn с задержкой при вводе и сразу — по клавише поиска; number:get() возвращает текст, :number() — число.
ui:search("Filter", function(q) list:count(#filter(q)) end)
local port = ui:number("Port", 443)
print(port:number())
Multi-select chips; a segmented control (exclusive, always visible); a star rating. segmented calls fn(initial, label) once inside the constructor (initial defaults to 1) and again on :set(i) — see «Callbacks fire during creation».
Мульти-выбор чипов; сегментированный контрол (один из, всегда видим); звёздный рейтинг. segmented вызывает fn(initial, label) один раз внутри конструктора (initial по умолчанию 1) и снова при :set(i) — см. «Callbacks fire during creation».
ui:segmented({ "GET", "POST" }, function(i, m) method = m end, 1)
#ui:date(label, initialMs?, fn(ms, text)) ui:time(label, "HH:mm"?, fn(h, m, text))
Date / time pickers rendered as outlined buttons. Handles: :get() / :set() / :text().
Пикеры даты / времени в виде контурных кнопок. Хендлы: :get() / :set() / :text().
ui:date("From", nil, function(ms, s) from = ms end)
Declarative form. Field: { name, type, label, default, options, min, max, step, required, helper, icon }. Types: text password number int email url phone textarea switch checkbox dropdown chips multiChips segmented radio slider stepper date time rating title subtitle label. f:values() / f:get(name) / f:set(name, v) / f:error(name, msg) / f:valid() / f:field(name) / f:clear().
Декларативная форма. Поле: { name, type, label, default, options, min, max, step, required, helper, icon }. Типы: text password number int email url phone textarea switch checkbox dropdown chips multiChips segmented radio slider stepper date time rating title subtitle label. f:values() / f:get(name) / f:set(name, v) / f:error(name, msg) / f:valid() / f:field(name) / f:clear().
ui:form({
{ name = "host", label = "Host", required = true },
{ name = "port", type = "number", default = 22 },
{ name = "tls", type = "switch", label = "TLS", default = true },
{ name = "mode", type = "dropdown", options = { "fast", "deep" } },
}, "Connect", function(v) connect(v.host, v.port, v.tls) end)
#ui:tabs({ names }, fn(c, i, name), initial?) → t ui:expander(title, fn(c), open?) → e
Tabs rebuild their content on every switch (t:select(i), t:get(), t:refresh(), t:badge(i, n)). Expander is a collapsible card whose content builds lazily on first open (e:open/close/toggle/isOpen).
Вкладки перестраивают содержимое при каждом переключении (t:select(i), t:get(), t:refresh(), t:badge(i, n)). Expander — сворачиваемая карточка, содержимое строится лениво при первом открытии (e:open/close/toggle/isOpen).
ui:tabs({ "Status", "Log" }, function(c, i)
if i == 1thenstatus(c) else c:terminal({ lines = Wex.log.lines() }) endend)
Horizontal scrolling row (cards, chips, tiles); a data table with header row, striped rows and row taps. Rows are arrays or { col = value } tables. h:set(rows) / h:add(row) / h:clear().
Горизонтально прокручиваемая строка (карточки, чипы, плитки); таблица данных с шапкой, полосатыми строками и тапами. Строки — массивы или таблицы { col = value }. h:set(rows) / h:add(row) / h:clear().
Native charts: bar, hbar, line, area, spark, pie, donut. c:set(values, labels?) / c:push(v, label?) for live series (keeps maxPoints) / c:labels / c:colors / c:type / c:clear. Passing a plain array draws bars. c:push(value, label) appends one point and drops the oldest past maxPoints; it is meant for live feeds and costs one insert, with the repaint left to the next frame, so a burst of a thousand samples between two frames still paints once.
Нативные графики: bar, hbar, line, area, spark, pie, donut. c:set(values, labels?) / c:push(v, label?) для живых серий (хранит maxPoints) / c:labels / c:colors / c:type / c:clear. Простой массив рисует столбцы. c:push(value, label) добавляет точку и выбрасывает самую старую после maxPoints; он рассчитан на живой поток, стоит одну вставку, а перерисовка откладывается до следующего кадра, поэтому пачка из тысячи отсчётов между двумя кадрами рисуется один раз.
local rtt = ui:chart({ type = "spark", values = {}, maxPoints = 40, color = "#38BDF8" })
Wex.interval(1000, function() rtt:push(measure()) end)
#ui:canvas(height | { height, width, bg }, fn(d)) → d · d:onTouch(fn(x, y, phase, p))
Immediate-mode drawing, all coordinates in dp. The handle draws (line, rect, circle, arc, polygon, path, text, fill, stroke, color, clear, redraw) and animates with d:loop(fps, fn(d, dt, t)). onTouch reports every finger: phase is down, pointerdown, move, pointerup, up or cancel, and p is the pointer table — p.count, p[i].x / p[i].y / p[i].id, and for two fingers p.scale (pinch, relative to the moment the second finger landed), p.panX, p.panY, p.distance, p.midX, p.midY and p.angle. That is enough for a pan-and-zoom spectrum view or an oscilloscope without dropping to native code.
Рисование в режиме немедленного вывода, все координаты в dp. Хендл рисует (line, rect, circle, arc, polygon, path, text, fill, stroke, color, clear, redraw) и анимирует через d:loop(fps, fn(d, dt, t)). onTouch сообщает о каждом пальце: phase — это down, pointerdown, move, pointerup, up или cancel, а p — таблица касаний: p.count, p[i].x / p[i].y / p[i].id, а для двух пальцев p.scale (щипок относительно момента, когда лёг второй палец), p.panX, p.panY, p.distance, p.midX, p.midY и p.angle. Этого хватает на анализатор спектра с зумом или осциллограф без перехода на нативный код.
local zoom, offset = 1, 0ui:canvas(240, function(d)
d:color("primary")
for i = 1, 64do d:line(i * 4 * zoom + offset, 220, i * 4 * zoom + offset, 220 - bins[i]) endend):onTouch(function(x, y, phase, p)
if p.count >= 2then
zoom = p.scale
offset = p.panX
d:redraw()
endend)
#d:line(x1,y1,x2,y2) rect(x,y,w,h,radius) circle(x,y,r) oval arc(x,y,r,start,sweep,center) polygon(x,y,r,sides,rotation) point path(points,closed) text(s,x,y) grid(step)
The drawing calls on a canvas handle, all in dp. State first, shape second: color, alpha, stroke(width), fill(), outline(), style("fill"|"stroke") and font(size, bold, align) set how the next shapes are painted. path takes a flat list of x, y numbers. Each call adds one operation to the frame; redraw() paints it, and clear() starts a new frame.
Рисующие вызовы у хендла canvas, всё в dp. Сначала состояние, потом фигура: color, alpha, stroke(ширина), fill(), outline(), style("fill"|"stroke") и font(размер, жирный, выравнивание) задают, чем будут нарисованы следующие фигуры. path принимает плоский список чисел x, y. Каждый вызов добавляет операцию в кадр; redraw() его рисует, а clear() начинает новый.
returnsвозвращаетthe same handle, so the calls chainтот же хендл, вызовы можно объединять в цепочку
Embedded WebView. HTML snippets need no permission; http(s) pages need "network". With js = true the page can call `Wex.send(text)` (→ w:onMessage(fn)) and w:eval(js, fn(result)) runs JavaScript. height takes dp or "match". `fullscreen = true` is the game preset: immersive host, one-viewport size, page keeps every gesture, screen stays on. w:load(s) / w:back() / w:reload() / w:onLoad(fn) / w:post(text) / w:fullscreen(bool) / w:orientation(mode) / w:grab(bool) / w:keepAwake(bool) / w:size() / w:focus().
Встроенный WebView. HTML-фрагменты без разрешений; http(s)-страницы требуют "network". С js = true страница может вызывать `Wex.send(text)` (→ w:onMessage(fn)), а w:eval(js, fn(result)) выполняет JavaScript. height принимает dp или "match". `fullscreen = true` — игровой режим: хост уходит в полный экран, вью ровно на один вьюпорт, все жесты остаются странице, экран не гаснет. w:load(s) / w:back() / w:reload() / w:onLoad(fn) / w:post(text) / w:fullscreen(bool) / w:orientation(mode) / w:grab(bool) / w:keepAwake(bool) / w:size() / w:focus().
Structured request/response between a ui:web page and Lua — local pages with js = true. In the page, Wex.call(method, args) returns a Promise; in Lua, w:handle(method, fn) answers it. The handler gets the decoded args and either returns the result (which resolves the Promise) or, for async work, takes a second respond argument and calls respond(result) whenever it is ready. An error thrown in the handler rejects the Promise; calling an unregistered method rejects too. args and the result cross as JSON, so pass plain tables/values. A remote (http/https) page can never reach a handler. The older fire-and-forget channels — Wex.send(msg)/w:onMessage and w:eval(js, cb) — still work alongside it.
Структурный запрос-ответ между страницей ui:web и Lua — для локальных страниц с js = true. На странице Wex.call(method, args) возвращает Promise; в Lua на него отвечает w:handle(method, fn). Обработчик получает разобранные args и либо возвращает результат (он разрешает Promise), либо для асинхронной работы принимает второй аргумент respond и вызывает respond(result), когда будет готов. Исключение в обработчике отклоняет Promise; вызов незарегистрированного метода — тоже. args и результат передаются как JSON, поэтому передавайте простые таблицы/значения. Удалённая (http/https) страница до обработчика не дотянется. Прежние каналы «выстрелил-и-забыл» — Wex.send(msg)/w:onMessage и w:eval(js, cb) — продолжают работать рядом.
local w = ui:web([[
<button onclick="go()">refresh</button>
<script>
async function go() {
const s = await Wex.call("stats", { tag: "x" })
document.title = s.total
}
</script>
]], { js = true })
w:handle("stats", function(a)
return { total = 42, tag = a.tag } -- the returned table resolves the page Promiseend)
#ui:video(src, { height, controls, loop, autoplay, mute }) → v
Inline video playback. src is "asset:…", a Wex.files path, or an http(s) URL (needs Plugin.permissions = {"network"}). controls shows the scrubber; loop/autoplay/mute are booleans; height is dp. The handle drives playback.
Встроенное воспроизведение видео. src — "asset:…", путь в Wex.files или http(s)-URL (нужно Plugin.permissions = {"network"}). controls показывает перемотку; loop/autoplay/mute — булевы; height в dp. Хендл управляет воспроизведением.
local v = ui:video("asset:intro.mp4", { height = 200, controls = true })
ui:button("Play", function() v:play() end)
#Web games: fullscreen, gestures and the page-side Wex object
A canvas/WebGL page becomes playable with three settings: `fullscreen = true` (toolbar and system bars go, a notch pill with the plugin name and ✕ takes their place), height "match" (exactly one viewport, so the host has nothing left to scroll) and the default gesture grab (a look-stick drag no longer drags the layout). Inside a LOCAL page (snippet or package asset) window.Wex also has: send(text), log(text), get(key), set(key, value) — the plugin's own storage, toast(text), vibrate(ms), fullscreen(bool), orientation(mode), keepAwake(bool), viewport() → JSON. A remote http(s) page only gets send/log.
Страница с canvas/WebGL становится играбельной от трёх настроек: `fullscreen = true` (тулбар и системные бары уходят, вместо них — «челка» с названием плагина и ✕), height "match" (ровно один вьюпорт, хосту нечего скроллить) и захват жестов по умолчанию (свайп обзора больше не тащит лайаут). В ЛОКАЛЬНОЙ странице (фрагмент или ассет пакета) у window.Wex есть ещё: send(text), log(text), get(key), set(key, value) — то же хранилище плагина, toast(text), vibrate(ms), fullscreen(bool), orientation(mode), keepAwake(bool), viewport() → JSON. Внешней http(s)-странице достаются только send/log.
-- LuaWex.screen.orientation("landscape")
local w = ui:web("asset:game.html", { js = true, fullscreen = true })
-- в самой странице-- Wex.set("world", JSON.stringify(state))-- const saved = Wex.get("world")-- window.onWexMessage = function(text) { … }
#ui:splitButton({ label, onClick, items, onSelect, style }) → h
A Material 3 split button: a primary action attached to a menu trigger that drops the alternatives. style is "filled" (default), "tonal", "outlined", "danger" or "success".
Split-кнопка Material 3: основное действие плюс триггер меню с вариантами. style — "filled" (по умолчанию), "tonal", "outlined", "danger" или "success".
A rounded, elevated pill of quick actions — the Material 3 floating-toolbar look — centred and sized to its contents. Each action is an icon button with a tooltip label.
Скруглённая приподнятая «пилюля» быстрых действий — вид floating toolbar из Material 3 — по центру и по размеру содержимого. Каждое действие — иконка-кнопка с подсказкой-подписью.
A bold gradient header block: large title, subtitle and an optional icon, centred on a two-colour gradient (theme roles or #hex). The text colour is picked automatically for contrast. Great as the top of a screen.
Яркий градиентный заголовочный блок: крупный заголовок, подзаголовок и необязательная иконка по центру двухцветного градиента (роли темы или #hex). Цвет текста подбирается автоматически для контраста. Отлично смотрится вверху экрана.
A circular progress ring with a value in the centre and a caption under it. h:set(value, label?) / h:label(s) / h:caption(s) / h:color(c) update it live — perfect for dashboards (CPU, battery, score). ui:ring is an alias.
Круговое кольцо прогресса со значением в центре и подписью под ним. h:set(value, label?) / h:label(s) / h:caption(s) / h:color(c) обновляют его на лету — идеально для дашбордов (CPU, батарея, счёт). ui:ring — псевдоним.
#ui:sparkline(numbers, { type = "line"|"area"|"bar", color, fill, height }) → h
An inline mini-chart from a list of numbers, no axes. type line/area/bar, optional fill. h:set(numbers) redraws it and h:color(c) recolours — cheap enough to update on a timer.
Компактный мини-график из списка чисел, без осей. type line/area/bar, необязательная заливка. h:set(numbers) перерисует, h:color(c) перекрасит — достаточно дёшево, чтобы обновлять по таймеру.
A shimmering placeholder shown while data loads: N bars with an animated sweep. Swap it for the real content once your fetch returns — feels faster than a bare spinner.
Мерцающий плейсхолдер на время загрузки: N полос с бегущим бликом. Замени его на реальный контент, когда придут данные — ощущается быстрее, чем голый спиннер.
#ui:timeline({ { title, text, time, tone }, … }) → h
A vertical event rail: a coloured dot and connecting line per item, with time, bold title and body text. Good for history, activity feeds and step-by-step results.
Вертикальная лента событий: цветная точка и соединительная линия у каждого пункта, время, жирный заголовок и текст. Подходит для истории, лент активности и пошаговых результатов.
#ui:accordion({ { title, build = fn(ui), open }, … }) → h
A group of collapsible sections where opening one closes the others. Each section’s build(ui) fills its body with any widgets. Set open=true to expand one at start.
Группа сворачиваемых секций, где открытие одной закрывает остальные. build(ui) каждой секции наполняет её тело любыми виджетами. open=true раскрывает одну на старте.
#ui:carousel(items | count, fn(page, item), { dots }) → h
Swipeable, snapping pages built on the fly: pass a list (or a count) and a builder that fills each page(ui). Dot indicators track the current page. Great for onboarding, galleries and cards.
Свайпаемые страницы с прилипанием, строятся на лету: передай список (или число) и билдер, наполняющий каждую page(ui). Точки-индикаторы показывают текущую страницу. Хорошо для онбординга, галерей и карточек.
#ui:tagInput({ initial, hint, onChange = fn(tags) }) → h
An editable set of chips: type and press Done to add, tap ✕ to remove. onChange(tags) fires on every change; h:tags() reads them, h:add(s) / h:clear() edit them.
Редактируемый набор чипов: введи и нажми Готово, чтобы добавить, тапни ✕, чтобы убрать. onChange(tags) вызывается на каждое изменение; h:tags() читает их, h:add(s) / h:clear() меняют.
A pie or donut chart from weighted segments; colours default to a rotating palette. h:set(segments) redraws it. Good for breakdowns (storage, spend, votes).
Круговая или пончиковая диаграмма из взвешенных сегментов; цвета по умолчанию берутся из ротации палитры. h:set(segments) перерисует. Хорошо для разбивок (память, траты, голоса).
#ui:colorField({ colors, initial, onChange = fn(hex) }) → h
A row of tappable colour swatches (theme roles, tones or #hex); the selected one gets a ring. onChange(hex) fires on pick; h:get() → "#RRGGBB", h:name() → the chosen token. Let users theme your plugin.
Ряд кликабельных образцов цвета (роли темы, тоны или #hex); у выбранного появляется обводка. onChange(hex) вызывается при выборе; h:get() → "#RRGGBB", h:name() → выбранный токен. Дай пользователю темизировать твой плагин.
More styling on any widget. Colors accept #hex, theme roles (primary, error, surface, onSurface, muted, secondaryContainer …) and tones (success, warning, danger, info, purple, cyan …).
Больше стилей у любого виджета. Цвета: #hex, роли темы (primary, error, surface, onSurface, muted, secondaryContainer …) и тона (success, warning, danger, info, purple, cyan …).
Reactive text: the widget re-renders whenever Wex.state.set(key, v) changes the key. Inputs, switches and checkboxes bind two-way. Stats bind their value.
Реактивный текст: виджет перерисовывается при каждом Wex.state.set(key, v). Поля ввода, переключатели и чекбоксы привязываются в обе стороны. Stat привязывает значение.
Button styles at creation ("filled" | "outlined" | "text" | "tonal" | "danger" | "success") or later via the handle. :loading(true) disables and shows "…". :text(s) changes the label after creation; :onClick(fn) receives the handle as its argument. There is no :set on a button.
Стили кнопки при создании ("filled" | "outlined" | "text" | "tonal" | "danger" | "success") или позже через хендл. :loading(true) блокирует и показывает "…". :text(s) меняет подпись после создания; :onClick(fn) получает хендл аргументом. Метода :set у кнопки нет.
local b = ui:button("Scan", scan, "tonal"):icon("wifi")
b:loading(true); … b:loading(false)
Selection handles — read or drive the control from code. :get() returns the 1-based index (or an index array for multiChips). :set() invokes the callback on segmented and radio, and :set()/:toggle() on switch and checkbox; chips, dropdown and stepper stay silent — see «Callbacks fire during creation».
Хендлы выбора — читайте или управляйте контролом из кода. :get() возвращает индекс с 1 (или массив индексов для multiChips). :set() вызывает колбэк у segmented и radio, а :set()/:toggle() — у switch и checkbox; chips, dropdown и stepper молчат — см. «Callbacks fire during creation».
local mode = ui:dropdown("Mode", modes, nil, 1)
print(mode:label())
Image sources: http(s) (needs "network"), data:…;base64, or a Wex.files / Wex.download file name. Progress can switch between determinate and indeterminate.
Источники картинок: http(s) (нужен "network"), data:…;base64 или имя файла из Wex.files / Wex.download. Прогресс переключается между определённым и неопределённым.
Wex.download(u, "pic.jpg", function(ok) if ok then img:set("pic.jpg") endend)
Entrances and attention cues on any widget: continuous :spin (stop with :stopSpin), a :bounce, a 3D :flip, :zoomIn / :dropIn reveals, and :highlight to flash a colour over it and fade out — all non-destructive to the widget’s own background.
Появления и акценты на любом виджете: непрерывный :spin (останавливается :stopSpin), :bounce, 3D-:flip, появления :zoomIn / :dropIn и :highlight — вспышка цветом поверх с угасанием. Ничего не портит из собственного фона виджета.
Animated text on any text handle: :countTo rolls the number up (with grouping and optional prefix/suffix), :typewriter reveals a string character by character. Great for stats and reveals.
Анимация текста на любом текстовом хендле: :countTo прокручивает число вверх (с разрядами и необязательным префиксом/суффиксом), :typewriter печатает строку по буквам. Хорошо для показателей и эффектных выводов.
Height reveals and list choreography: :expand / :collapse animate a view’s height open/closed; :stagger fades a container’s children in one after another for a lively list.
Раскрытие по высоте и хореография списков: :expand / :collapse анимируют высоту вью открыто/закрыто; :stagger по очереди проявляет детей контейнера, оживляя список.
Expressive motion on any handle. :spring drives properties to their targets with a real damped spring (physics, not a fixed tween): scale, x / y (dp translation), rotate (degrees), alpha; tune stiffness (20–1500, ~380) and damping (ratio 0.1–2, ~0.72). :shape sets an expressive corner shape and :shapeMorph animates to it — tokens pill, square, cookie, soft, xl, lg, md, sm (or a dp number).
Выразительное движение на любом хендле. :spring гонит свойства к целям настоящей пружиной с затуханием (физика, не фиксированный твин): scale, x / y (сдвиг в dp), rotate (градусы), alpha; настраивай stiffness (20–1500, ~380) и damping (коэффициент 0.1–2, ~0.72). :shape задаёт выразительную форму углов, а :shapeMorph анимирует к ней — токены pill, square, cookie, soft, xl, lg, md, sm (или число dp).
:glass makes a card or surface a frosted translucent panel (Expressive chrome). :blur blurs the view's own content (Android 12+). :shared(name) marks a view as a shared element — the next ui:push flies a same-named view on the new screen out from where this one sits (container transform).
:glass делает карточку или поверхность матовой полупрозрачной панелью (выразительный chrome). :blur размывает собственное содержимое вью (Android 12+). :shared(name) помечает вью как общий элемент — следующий ui:push вылетает одноимённым вью нового экрана из места, где стоит этот (container transform).
-- screen Aui:avatar("me.jpg", 56):shared("hero")
-- inside the ui:push build (screen B)ui:avatar("me.jpg", 120):shared("hero") -- morphs out from A
Delight and feedback: ui:confetti drops a burst of colour over the whole screen (a win, a completed task); ui:haptic fires a short vibration through the device’s haptics — no permission needed.
Радость и отклик: ui:confetti сыпет всплеск конфетти на весь экран (победа, завершённая задача); ui:haptic даёт короткую вибрацию через haptics устройства — разрешений не нужно.
Gestures on ordinary widgets, not just ui:canvas. :onSwipe reports a direction ("left"/"right"/"up"/"down"); :swipeToDismiss drags the widget with the finger and, past ~38% of its width, animates it out and calls back — swipe-to-delete for list rows; :draggable moves a widget freely and reports where it was dropped in dp; :offset nudges any widget without touching layout. Each keeps :onClick working and only claims the touch stream once a real drag starts, so the surrounding scroll still behaves.
Жесты у обычных виджетов, а не только у ui:canvas. :onSwipe сообщает направление ("left"/"right"/"up"/"down"); :swipeToDismiss тянет виджет за пальцем и после ~38% ширины улетает и вызывает колбэк — свайп-удаление строк списка; :draggable свободно перетаскивает виджет и сообщает точку броска в dp; :offset смещает любой виджет, не трогая разметку. Всё это не ломает :onClick и перехватывает касания только когда началось настоящее перетаскивание, поэтому окружающая прокрутка работает как прежде.
Request options: json = table sends JSON (Content-Type set, method defaults to POST); form = { k = v } sends urlencoded; binary = true returns base64 body; save = "name" streams to the sandbox file; auth = { user, pass } (Basic) or "token" (Bearer); timeout in ms.
Опции запроса: json = таблица шлёт JSON (Content-Type выставляется, метод по умолчанию POST); form = { k = v } — urlencoded; binary = true возвращает body в base64; save = "name" пишет в файл песочницы; auth = { user, pass } (Basic) или "token" (Bearer); timeout в мс.
Wex.http({ url = api .. "/login", json = { user = u, pass = p }, timeout = 8000 }, function(r)
if r.ok then token = r.json.token elseui:toast(r.error) endend)
Raw TCP client for custom protocols (telnet, redis, SMTP banners…). s:send(text) / s:sendLine(text) / s:sendBase64(b64) / s:close() / s:isOpen(). Data arrives in chunks on the main thread.
Сырой TCP-клиент для своих протоколов (telnet, redis, баннеры SMTP…). s:send(text) / s:sendLine(text) / s:sendBase64(b64) / s:close() / s:isOpen(). Данные приходят чанками в главном потоке.
local s = Wex.tcp.connect(host, 6379, {
onConnect = function(s) s:sendLine("PING") end,
onData = function(d) term:append(d) end })
Streamed download with progress and cancellation — pass a table instead of a callback (needs the network permission). onProgress(done, total) reports bytes (total is -1 when the length is unknown); onDone(res) gets { ok = true, path, name } or an error result ({ ok = false, error }, error = "cancelled" when you call handle:cancel()). The file lands in the Wex.files sandbox and a cancelled/failed transfer leaves no partial file. The legacy download(url, name, fn(ok, res)) is unchanged.
Загрузка потоком с прогрессом и отменой — передай таблицу вместо колбэка (нужно разрешение network). onProgress(done, total) отдаёт байты (total = -1, если длина неизвестна); onDone(res) получает { ok = true, path, name } либо ошибку ({ ok = false, error }, error = "cancelled" при handle:cancel()). Файл ложится в песочницу Wex.files, а отменённая/упавшая загрузка не оставляет частичного файла. Старый download(url, name, fn(ok, res)) не изменился.
Schema versioning for saved data. version() reads the stored schema number (0 if never set). migrate(to, fn) runs fn(currentVersion) once when the stored version is below to, then records to — call it before the first read (in build or Plugin.onUpdate) so old data is reshaped exactly once.
Версионирование схемы сохранённых данных. version() читает номер схемы (0, если не задан). migrate(to, fn) один раз вызывает fn(текущаяВерсия), когда сохранённая версия меньше to, и записывает to — вызывайте до первого чтения (в build или Plugin.onUpdate), чтобы старые данные переформатировались ровно один раз.
Wex.storage.migrate(2, function(from)
local single = Wex.storage.get("host")
if single thenWex.storage.setJson("hosts", { single }) endend)
#db:rows(sql, params) db:one(sql, params) db:scalar(sql, params) db:insert(table, { col = v }) → id db:update(table, { col = v }, where, params) db:delete(table, where, params)
Typed SQLite helpers: rows() returns numbers as numbers and NULL as nil (query() keeps the legacy all-strings shape). insert/update/delete build the SQL for you.
Типизированные хелперы SQLite: rows() возвращает числа числами, NULL как nil (query() сохраняет старый формат «всё строки»). insert/update/delete собирают SQL сами.
local id = db:insert("hosts", { ip = ip, rtt = ms })
local row = db:one("SELECT * FROM hosts WHERE id = ?", { id })
Schema introspection, batched writes in a transaction (rolled back if fn errors), database file management.
Интроспекция схемы, пакетная запись в транзакции (откатывается при ошибке fn), управление файлами БД.
db:transaction(function(d) for _, h inipairs(hosts) do d:insert("hosts", h) endend)
#db:live(sql, params?, fn(rows)) → sub db:onChange(fn(table)) / onChange(table, fn) → sub
Reactive SQLite. db:live re-runs its query and calls fn(rows) after every write (and once immediately), so a table can drive ui:view directly; db:onChange fires after a write to a given table (or any). Each returns a { stop() } handle. Callbacks run synchronously on the writing thread; writes inside a db:transaction fire a single coalesced notification after commit (never intermediate rows), and a callback that itself writes is coalesced rather than recursing. Keep live queries selective — every write re-runs them all.
Реактивный SQLite. db:live перезапускает свой запрос и зовёт fn(rows) после каждой записи (и один раз сразу), поэтому таблица может напрямую питать ui:view; db:onChange срабатывает после записи в указанную таблицу (или в любую). Каждый возвращает хендл { stop() }. Колбэки идут синхронно на потоке записи; записи внутри db:transaction дают одно объединённое уведомление после коммита (без промежуточных строк), а колбэк, который сам пишет, объединяется, а не рекурсирует. Держи live-запросы избирательными — каждая запись перезапускает их все.
local sub = db:live("SELECT * FROM todo ORDER BY done, id", function(rows)
listView:rebuild(function(c)
for _, r in ipairs(rows) do c:item({ title = r.title }) endend)
end)
db:insert("todo", { title = "Buy milk", done = 0 }) -- the live query re-fires-- sub:stop() to unsubscribe
#Wex.random.int(a, b) float(a, b) bool(p?) string(n, charset?) hex(bytes) uuid() pick(t) shuffle(t) sample(t, n) color()
Cryptographically seeded randomness. int(a, b) includes both ends. shuffle returns a new table and leaves the argument untouched; pick returns nil for an empty table.
Криптографически инициализированная случайность. int(a, b) включает обе границы. shuffle возвращает новую таблицу и не трогает аргумент; pick возвращает nil для пустой таблицы.
local pass = Wex.random.string(16)
#Wex.math.clamp lerp map(x, a1, b1, a2, b2) round(x, digits) sum avg min max median stddev distance geoDistance(lat1, lon1, lat2, lon2) isInt sign range(from, to, step?) percent(part, total)
Numeric helpers over arrays and coordinates (geoDistance in meters).
Числовые хелперы над массивами и координатами (geoDistance в метрах).
A process-wide bus that lets separate plugin instances talk to each other, while Wex.events / Wex.state stay private to one plugin. emit(topic, data) delivers data to every plugin subscribed with on(topic, fn); set/get/keys are a shared key-value store. Payloads travel as JSON, so pass plain data (no functions); each subscriber gets its own copy on its own thread. Subscriptions drop when the plugin closes. In a background job only emit/set/get/keys are available (no on).
Общая для всего процесса шина: разные экземпляры плагинов общаются между собой, тогда как Wex.events / Wex.state приватны для одного плагина. emit(topic, data) доставляет data всем, кто подписан через on(topic, fn); set/get/keys — общее хранилище ключ-значение. Данные ходят как JSON, поэтому передавай простые значения (без функций); каждый подписчик получает свою копию в своём потоке. Подписки снимаются при закрытии плагина. В фоне доступны только emit/set/get/keys (без on).
Request/reply over the bus, for service-style plugins. respond(topic, fn) registers a responder whose fn(data) returns the reply. request(topic, data, fn(reply, err), { timeout = 5000 }) sends a request and delivers the first responder's reply to fn(reply, nil), or fn(nil, "timeout") when none answers within the timeout (100–60000 ms). once(topic, fn) is like on() but auto-unsubscribes after the first delivery. Foreground only — a background job can emit/set/get on the bus but not subscribe or respond.
Запрос-ответ поверх шины, для плагинов-сервисов. respond(topic, fn) регистрирует обработчик, чей fn(data) возвращает ответ. request(topic, data, fn(reply, err), { timeout = 5000 }) шлёт запрос и отдаёт ответ первого обработчика в fn(reply, nil) либо fn(nil, "timeout"), если никто не ответил за таймаут (100–60000 мс). once(topic, fn) — как on(), но снимает подписку после первой доставки. Только на переднем плане — фоновая задача может emit/set/get, но не подписываться и не отвечать.
-- service pluginWex.bus.respond("geoip", function(ip)
return { country = lookup(ip) }
end)
-- client pluginWex.bus.request("geoip", "1.1.1.1", function(res, err)
if res thenui:text(res.country) elseui:text("no answer: " .. err) endend)
Bus v2 — typed, schema-validated service RPC across plugins, layered on request/respond. provide(name, schema, impl) registers a service; schema is { request = { field = "number"|"string"|"boolean"|"table" }, response = {...}, version = "x.y.z" }. call(name, range, req, fn(res, err)) finds a provider whose version satisfies the semver range ("*", "^1.2.0", "~1.2.0", ">=1.0 <2.0", or exact), validates the request, runs the provider on its own thread, validates the response, and returns res or err — one of "no_provider", "schema_mismatch: …", "timeout". discover(name, range) → { name, version } (or nil) for capability probing; services() lists everything provided this session. Validation runs on the Kotlin edge, so a malformed payload never reaches an impl, and a service is withdrawn automatically when the providing plugin closes. Foreground plugins.
Bus v2 — типизированный RPC сервисов между плагинами со схема-валидацией, поверх request/respond. provide(name, schema, impl) регистрирует сервис; schema — { request = { поле = "number"|"string"|"boolean"|"table" }, response = {...}, version = "x.y.z" }. call(name, range, req, fn(res, err)) находит провайдера, чья версия удовлетворяет semver-диапазону ("*", "^1.2.0", "~1.2.0", ">=1.0 <2.0" или точная), проверяет запрос, выполняет провайдера в его потоке, проверяет ответ и возвращает res или err — один из "no_provider", "schema_mismatch: …", "timeout". discover(name, range) → { name, version } (или nil) для проверки возможностей; services() перечисляет всё, что предоставлено в этой сессии. Валидация — на Kotlin-стороне, поэтому кривой payload не доходит до impl, а сервис снимается автоматически при закрытии плагина-провайдера. Плагины переднего плана.
-- provider (often a library plugin)Wex.bus.provide("geo.reverse", {
request = { lat = "number", lon = "number" },
response = { name = "string" },
version = "1.2.0",
}, function(req)
return { name = reverseGeocode(req.lat, req.lon) }
end)
-- consumer: discover by range, then call with a typed requestifWex.bus.discover("geo.reverse", ">=1.0 <2.0") thenWex.bus.call("geo.reverse", ">=1.0", { lat = 55.75, lon = 37.62 }, function(res, err)
if res then label:set(res.name) elseWex.log("rpc failed: " .. err) endend)
end
Bus v2 distributed state — a namespaced key/value store shared by every plugin in the process, persisted and reactive. Wex.bus.state(ns) returns a handle: set(key, value) (a nil value deletes), get(key, default?), delete(key), keys(), and watch(pattern, fn(key, value)) which fires whenever ANY plugin writes a matching key — pattern is "*" (all), a trailing-star prefix like "node/*", or an exact key. Concurrent writes from different plugins converge deterministically through a Hybrid Logical Clock (last-writer-wins), and each namespace survives across sessions (persisted). Values cross as JSON, so store plain tables and scalars. This is how independent plugins share live state without reaching into each other's VM. Foreground plugins.
Распределённое состояние Bus v2 — именованное key/value хранилище, общее для всех плагинов в процессе, персистентное и реактивное. Wex.bus.state(ns) возвращает хендл: set(key, value) (значение nil удаляет), get(key, default?), delete(key), keys() и watch(pattern, fn(key, value)), который срабатывает, когда ЛЮБОЙ плагин пишет совпадающий ключ — pattern это "*" (все), префикс со звездой в конце вроде "node/*" или точный ключ. Параллельные записи от разных плагинов сходятся детерминированно через Hybrid Logical Clock (last-writer-wins), а каждое пространство имён переживает перезапуск (персист). Значения передаются как JSON — храните простые таблицы и скаляры. Так независимые плагины делят живое состояние, не залезая в VM друг друга. Плагины переднего плана.
local st = Wex.bus.state("fleet")
st:watch("node/*", function(key, val) -- fires for writes from ANY plugin
map:place(key, val)
end)
st:set("node/42", { lat = 55.75, lon = 37.62 }) -- converges (HLC/LWW) + persists + notifies watchersWex.log(st:get("node/42").lat)
Rate-limit noisy callbacks (typing, sensors, websocket floods); defer runs on the next main-loop tick. Timer callbacks receive their own id.
Ограничение частоты шумных колбэков (ввод, сенсоры, поток websocket); defer выполняется на следующем тике главного цикла. Колбэки таймеров получают свой id.
local save = Wex.debounce(500, function(text) Wex.storage.set("draft", text) end)
editor:onChange(save)
#Wex.sensors.start(type, fn(v), everyMs?) → id stop(id) stopAll() has(type) list()
Motion & environment sensors: accelerometer gyroscope magnetometer light proximity pressure gravity linear rotation steps humidity temperature. v = { x, y, z, value, values, accuracy, time }.
Сенсоры движения и среды: accelerometer gyroscope magnetometer light proximity pressure gravity linear rotation steps humidity temperature. v = { x, y, z, value, values, accuracy, time }.
#Wex.sensors.orientation(fn({ azimuth, pitch, roll }), everyMs?) → id
A fused compass / attitude from the rotation-vector sensor, computed by the host (no matrix math in Lua). azimuth is the heading 0–360° (0 = north); pitch and roll are in degrees. Throttle with everyMs; stop with Wex.sensors.stop(id).
Слитый компас / ориентация из вектора вращения, считается хостом (без матричной математики в Lua). azimuth — курс 0–360° (0 = север); pitch и roll в градусах. Троттлинг через everyMs; остановка через Wex.sensors.stop(id).
local id = Wex.sensors.orientation(function(o)
needle:rotation(-o.azimuth)
end, 100)
On-device speech recognition. fn is called with { text, final = true, confidence } for a final result and, if partial = true, with interim { text, final = false }. lang is a BCP-47 tag (default: device locale). Recognition is single-shot — call listen again from your final-result callback to keep listening. Needs Plugin.permissions = {"microphone"}.
Распознавание речи на устройстве. fn зовётся с { text, final = true, confidence } для финального результата и, если partial = true, с промежуточным { text, final = false }. lang — тег BCP-47 (по умолчанию — локаль устройства). Распознавание одноразовое — вызови listen снова из финального колбэка, чтобы слушать дальше. Нужно Plugin.permissions = {"microphone"}.
Screen control for monitors, dashboards and games. fullscreen(bool) hides or shows the system bars, nothing else — same as it always did. immersive(bool) goes further: the toolbar goes away, cutout/gesture insets stop padding the page, content runs edge to edge and a notch pill (plugin name + ✕, long-press to leave) takes the toolbar's place; position your own controls with env(safe-area-inset-*). The rotation you ask for is remembered by the host and re-applied before the first frame, so a plugin stays landscape even when Android restarts the screen to rotate it, and asking for the rotation you already have is ignored (it would restart the Activity for nothing). Both are released when the plugin closes. viewport() reports the real drawable area in dp.
Управление экраном для мониторов, дэшбордов и игр. fullscreen(bool) скрывает и показывает системные бары — и только их, как было всегда. immersive(bool) идёт дальше: уходит тулбар, вставки выреза и полосы жестов перестают поджимать страницу, контент занимает весь экран, а вместо тулбара появляется «челка» с названием плагина и ✕ (долгое нажатие — выйти); свои элементы управления отступайте через env(safe-area-inset-*). Запрошенная ориентация запоминается хостом и применяется до первого кадра, поэтому плагин остаётся в ландшафте даже если Android перезапустил экран для поворота, а повторная просьба о той же ориентации игнорируется (иначе это лишний перезапуск Activity). При закрытии плагина и то и другое сбрасывается. viewport() даёт настоящую область рисования в dp.
Wex.screen.keepOn(true)
Wex.screen.orientation("landscape")
Wex.screen.immersive(true) -- полный экран с «челкой»local v = Wex.screen.viewport() -- { width, height, landscape, immersive, density }
Own-app controls plus other apps (launch/installed/info/list need the "intent" permission). openSettings: app | wifi | bluetooth | location | display | sound | date | developer | vpn | apps | battery | security | accessibility | notifications | all.
Управление своим приложением и другими (launch/installed/info/list требуют разрешение "intent"). openSettings: app | wifi | bluetooth | location | display | sound | date | developer | vpn | apps | battery | security | accessibility | notifications | all.
ifnotWex.net.isOnline() thenWex.app.openSettings("wifi") end
Exact wall-clock alarms via AlarmManager. at is an epoch-ms number or a "HH:mm" string (the next occurrence); repeat is "once" (default) or "daily". When it fires, a notification (title/text) opens the plugin with args in Wex.args. set() returns whether it was scheduled; canSchedule() is false when the OS withholds exact-alarm permission (Android 12+). Needs Plugin.permissions = {"notifications"}. (Alarms do not yet survive a reboot.)
Точные будильники по настенным часам через AlarmManager. at — число epoch-ms или строка "HH:mm" (следующее наступление); repeat — "once" (по умолчанию) или "daily". При срабатывании уведомление (title/text) открывает плагин с args в Wex.args. set() возвращает, удалось ли запланировать; canSchedule() — false, если система не даёт право на точные будильники (Android 12+). Нужно Plugin.permissions = {"notifications"}. (Пока будильники не переживают перезагрузку.)
Plugin.permissions = { "notifications" }
Wex.alarm.set({ id = 1, at = "09:00", repeat = "daily",
title = "Standup", text = "Time!", args = { screen = "today" } })
-- when tapped, the plugin opens with Wex.args = { screen = "today" }
#Wex.media.start({ title, artist }) stop() isRunning() — in Plugin.media():onCommand(fn(cmd,arg)) play/pause/resume/seek/stop/setMetadata/setState
Background audio with a real MediaSession — a lock-screen and notification player that keeps running after the app is backgrounded, because the session, the player and your media logic live in a foreground service, not in the screen you launched from. Two halves. (1) From your foreground UI: Wex.media.start({ title, artist }) launches the media service, stop() ends it, isRunning() checks — needs Plugin.permissions = {"background"}. (2) Inside the service, your Plugin.media() function drives playback: play(url), pause(), resume(), seek(ms), stop(), position(), isPlaying(); setMetadata({ title, artist, album, duration }) and setState("playing"|"paused"|"stopped", position?) update the lock screen; onCommand(fn(cmd, arg)) receives transport — "play"/"pause"/"stop"/"seek" (the service already applied these to the player) and "next"/"previous"/"completed"/"focusLoss", which are yours to act on. Audio focus and the headphones-unplugged event are handled for you. Mirror state into your UI with Wex.bus.
Фоновое аудио с настоящей MediaSession — плеер на экране блокировки и в шторке, продолжающий играть после сворачивания приложения, потому что сессия, плеер и ваша медиа-логика живут в foreground-сервисе, а не на экране, с которого вы запустили. Две половины. (1) Из переднего UI: Wex.media.start({ title, artist }) запускает медиа-сервис, stop() завершает, isRunning() проверяет — нужно Plugin.permissions = {"background"}. (2) Внутри сервиса ваша функция Plugin.media() управляет воспроизведением: play(url), pause(), resume(), seek(ms), stop(), position(), isPlaying(); setMetadata({ title, artist, album, duration }) и setState("playing"|"paused"|"stopped", position?) обновляют экран блокировки; onCommand(fn(cmd, arg)) получает транспорт — "play"/"pause"/"stop"/"seek" (сервис уже применил их к плееру) и "next"/"previous"/"completed"/"focusLoss", которые обрабатываете вы. Аудиофокус и событие «наушники выдернули» обрабатываются за вас. Зеркальте состояние в UI через Wex.bus.
Plugin.permissions = { "background" }
-- foreground UI: launch the playerui:button("Play"):onClick(function() Wex.media.start({ title = "My Radio" }) end)
-- runs INSIDE the media service (survives backgrounding):function Plugin.media()
local queue = { "https://ex.com/1.mp3", "https://ex.com/2.mp3" }
local i = 1Wex.media.onCommand(function(cmd)
if cmd == "next"or cmd == "completed"then
i = i % #queue + 1Wex.media.setMetadata({ title = "Track " .. i })
Wex.media.play(queue[i])
endend)
Wex.media.setMetadata({ title = "Track " .. i })
Wex.media.play(queue[i])
end
Reads the address book. fn receives a list of { id, name, phones = {...}, emails = {...} }, sorted by name. search filters by name (substring match); limit caps how many contacts come back. Read-only — a plugin cannot change or delete a contact. Runs off the main thread, so a large address book never blocks the UI; the list arrives on the callback. Foreground plugins only. Needs Plugin.permissions = {"contacts"}.
Читает адресную книгу. fn получает список { id, name, phones = {...}, emails = {...} }, отсортированный по имени. search фильтрует по имени (по подстроке), limit ограничивает число контактов. Только чтение — изменить или удалить контакт плагин не может. Работает вне главного потока, поэтому большая книга не блокирует UI; список приходит в колбэк. Только для плагинов на переднем плане. Нужно Plugin.permissions = {"contacts"}.
Plugin.permissions = { "contacts" }
Wex.contacts.query({ search = "Ann", limit = 50 }, function(list)
for _, c in ipairs(list) doWex.log(c.name .. " — " .. (c.phones[1] or"no phone"))
endend)
#Wex.calendar.events({ from, to, limit }?, fn(list)) calendars(fn(list))
Reads the agenda. events() calls fn with a list of { id, title, start, end, allDay, location, calendar } falling in [from, to] (epoch-ms; default: now to +7 days) — recurring events are already expanded into their occurrences and the list is sorted by start. calendars() lists the device's calendars as { id, name, account }. Read-only, off the main thread. Foreground plugins only. Needs Plugin.permissions = {"calendar"}.
Читает календарь. events() зовёт fn со списком { id, title, start, end, allDay, location, calendar } в диапазоне [from, to] (epoch-ms; по умолчанию — сейчас .. +7 дней) — повторяющиеся события уже развёрнуты в отдельные вхождения, список отсортирован по началу. calendars() перечисляет календари устройства как { id, name, account }. Только чтение, вне главного потока. Только для плагинов на переднем плане. Нужно Plugin.permissions = {"calendar"}.
Plugin.permissions = { "calendar" }
Wex.calendar.events({}, function(list)
for _, e in ipairs(list) doWex.log(os.date("%H:%M", e.start / 1000) .. " " .. e.title)
endend)
Adapter state and setup. Needs Plugin.permissions = { "bluetooth" }. Android's own runtime permissions (Bluetooth scan/connect on Android 12+, fine location before) are requested from the user automatically by the first call that needs them; permission(fn) asks up front. 16-bit UUIDs like "180d" are accepted everywhere and expand to the Bluetooth base UUID.
Состояние адаптера и подготовка. Нужно Plugin.permissions = { "bluetooth" }. Runtime-разрешения Android (скан и подключение на Android 12+, точная геолокация до него) запрашиваются у пользователя автоматически первым вызовом, которому они нужны; permission(fn) спрашивает заранее. 16-битные UUID вроде "180d" принимаются везде и разворачиваются в полный.
ifnotWex.ble.enabled() thenWex.ble.requestEnable() endWex.ble.permission(function(ok) if ok thenstartScan() endend)
Scan for advertisements. services filters by service UUID, name by prefix, address by exact MAC; mode = lowPower | balanced | lowLatency; timeoutMs default 10 s (0 = until stopScan). onDevice fires once per device, or on every advertisement with repeat = true (RSSI tracking). d = { address, name, rssi, txPower, connectable, services, manufacturer = { [companyId] = hex }, serviceData = { [uuid] = hex }, raw, time }. onDone gets every device seen.
Скан рекламных пакетов. services — фильтр по UUID сервиса, name — по префиксу имени, address — точный MAC; mode = lowPower | balanced | lowLatency; timeoutMs по умолчанию 10 с (0 = до stopScan). onDevice вызывается раз на устройство, а с repeat = true — на каждый пакет (слежение за RSSI). d = { address, name, rssi, txPower, connectable, services, manufacturer = { [companyId] = hex }, serviceData = { [uuid] = hex }, raw, time }. onDone получает всех увиденных.
GATT client. Services are discovered right after connecting and delivered to onServices as { { uuid, primary, characteristics = { { uuid, properties = { read, write, writeNoResponse, notify, indicate }, descriptors } } } }. Operations are queued one at a time and time out (timeoutMs, default 8 s), so a silent peripheral never blocks the plugin. dev:isConnected() dev:disconnect() dev:close() dev:services() dev:rssi(fn(rssi)) dev:mtu(n, fn(mtu)).
GATT-клиент. Сервисы находятся сразу после подключения и приходят в onServices как { { uuid, primary, characteristics = { { uuid, properties = { read, write, writeNoResponse, notify, indicate }, descriptors } } } }. Операции выполняются по одной и с таймаутом (timeoutMs, по умолчанию 8 с), молчащая периферия не заблокирует плагин. dev:isConnected() dev:disconnect() dev:close() dev:services() dev:rssi(fn(rssi)) dev:mtu(n, fn(mtu)).
Characteristic access. res = { ok, uuid, hex, base64, text, bytes = { … }, len, time } or { ok = false, error }. data for write: a string (UTF-8), { hex = "0102" }, { base64 = "…" } or a byte array { 1, 2, 255 }; noResponse = true uses write-without-response. notify enables notifications (or indications when that is all the characteristic offers) by writing the CCCD for you.
Доступ к характеристикам. res = { ok, uuid, hex, base64, text, bytes = { … }, len, time } или { ok = false, error }. data для write: строка (UTF-8), { hex = "0102" }, { base64 = "…" } или массив байт { 1, 2, 255 }; noResponse = true — запись без ответа. notify включает уведомления (или индикации, если характеристика умеет только их), сам записывая CCCD.
Broadcast BLE advertisements: an iBeacon (Apple company id 0x004C, txPower default -59), an AltBeacon (0x0118), raw manufacturer data, service data, or just service UUIDs. mode = lowPower | balanced | lowLatency; power = ultraLow | low | medium | high; timeoutMs up to 180 s (0 = until stopAdvertise). The whole packet must fit in 31 bytes — a device name rarely fits next to a beacon payload.
Трансляция BLE-рекламы: iBeacon (company id Apple 0x004C, txPower по умолчанию -59), AltBeacon (0x0118), сырые manufacturer data, service data или просто UUID сервисов. mode = lowPower | balanced | lowLatency; power = ultraLow | low | medium | high; timeoutMs до 180 с (0 = до stopAdvertise). Весь пакет должен уместиться в 31 байт: имя устройства рядом с маячком обычно не влезает.
Wex.ble.advertise({ ibeacon = { uuid = "e2c56db5-dffb-48d2-b060-d0f5a71096e0", major = 1, minor = 7 },
onStart = function(ok, err) ui:toast(ok and"beacon on"or err) end })
The phone's IR blaster, on the devices that have one. available() is the only thing worth branching on: most phones do not. frequencies() lists the carrier ranges the emitter supports; 38 kHz works for almost everything.
ИК-передатчик телефона, если он есть. available() — единственное, что стоит проверять: у большинства телефонов передатчика нет. frequencies() перечисляет поддерживаемые несущие; 38 кГц подходит почти для всего.
ifnotWex.ir.available() thenui:empty("info", "No IR blaster", "This phone has no infrared emitter") returnend
Encodes and transmits in one call. protocol = nec (the common one), necext (16-bit address), sony, rc5 or raw. encode() returns { freq, pattern, duration } without sending, so a plugin can show or store the timings. Transmission runs off the UI thread and blocks for the length of the burst.
Кодирует и отправляет одним вызовом. protocol = nec (самый частый), necext (16-битный адрес), sony, rc5 или raw. encode() возвращает { freq, pattern, duration } без отправки, чтобы плагин мог показать или сохранить тайминги. Передача идёт вне UI-потока и длится столько же, сколько сам посыл.
The raw path: pattern is an array of microseconds, alternating mark and space, starting with a mark. Up to 2048 slots and 8 seconds in total. resend() sends the same frame several times with a gap, which is what most remotes actually do when you hold a button. It is spelled resend because repeat is a reserved word in Lua.
Сырой путь: pattern — массив микросекунд, чередующихся импульс и пауза, начиная с импульса. До 2048 значений и 8 секунд суммарно. resend() отправляет один и тот же кадр несколько раз с паузой — именно так ведёт себя пульт при удержании кнопки. Название resend, потому что repeat — зарезервированное слово Lua.
Pronto CCF is the hex format every remote database on the internet publishes. pronto() converts it to { freq, pattern, repeatPattern } in microseconds; sendPronto() plays the intro burst and then the repeat burst, as the format intends.
Pronto CCF — тот самый hex-формат, в котором выложены все базы пультов в интернете. pronto() переводит его в { freq, pattern, repeatPattern } в микросекундах; sendPronto() играет вступительный кадр, а затем повторный, как и задумано форматом.
NFC state. The reader only runs while your plugin screen is in the foreground; the host switches it off when the screen goes away and back on when it returns, so a plugin does not have to think about it.
Состояние NFC. Считыватель работает, только пока экран плагина на переднем плане; хост выключает его при уходе с экрана и включает обратно при возврате, так что плагину об этом думать не нужно.
ifnotWex.nfc.enabled() thenWex.nfc.openSettings() end
Starts the reader. Every tag held against the phone arrives as a table: id, idDec, techs, type, size, maxSize, writable, atqa, sak, records, and the convenience fields text and uri taken from the first matching record. opts = { skipNdef, silent, presenceDelayMs }.
Запускает считыватель. Каждая поднесённая метка приходит таблицей: id, idDec, techs, type, size, maxSize, writable, atqa, sak, records плюс удобные поля text и uri из первой подходящей записи. opts = { skipNdef, silent, presenceDelayMs }.
Wex.nfc.listen(function(tag)
ui:kv("UID", tag.id)
if tag.text thenui:text(tag.text) endend)
spec is { text = , lang = } for a text record, { uri = } for a link, { mime = , data = } for arbitrary bytes, or { records = { … } } for several at once; readonly = true locks the tag afterwards, permanently. An unformatted tag is formatted on first write.
spec — это { text = , lang = } для текстовой записи, { uri = } для ссылки, { mime = , data = } для произвольных байт или { records = { … } } для нескольких сразу; readonly = true после записи блокирует метку навсегда. Неформатированная метка форматируется при первой записи.
Low-level access. transceive() sends a raw command over IsoDep, NfcA, NfcB, NfcF or NfcV, whichever the tag offers. Ultralight works in 4-byte pages; Classic in 16-byte blocks with per-sector authentication, opts = { key = "FFFFFFFFFFFF", keyType = "A" }. Results are { ok, hex, len, bytes } for transceive, { ok, page, hex, len } for ultralightRead, { ok, sector, block, hex } for classicRead, and { ok = false, error } on failure.
Низкий уровень. transceive() шлёт сырую команду по IsoDep, NfcA, NfcB, NfcF или NfcV — что метка поддерживает. Ultralight работает страницами по 4 байта; Classic — блоками по 16 байт с авторизацией по секторам, opts = { key = "FFFFFFFFFFFF", keyType = "A" }. Результат: { ok, hex, len, bytes } для transceive, { ok, page, hex, len } для ultralightRead, { ok, sector, block, hex } для classicRead и { ok = false, error } при ошибке.
#Wex.nfc.emulate({ aid | aids }, fn(apduHex) → responseHex) Wex.nfc.stopEmulate()
Host Card Emulation: the phone answers a contactless reader as a smart card, pass or terminal. Register your AID(s); fn receives each command APDU as hex and returns the response APDU (hex, status word included). Works while the plugin runs. Needs Plugin.permissions = { "nfc" } and an NFC reader to test.
Эмуляция карты (HCE): телефон отвечает бесконтактному считывателю как смарт-карта, пропуск или терминал. Зарегистрируй свои AID; fn получает каждый командный APDU в hex и возвращает ответный APDU (hex, со статус-словом). Работает, пока плагин запущен. Нужно Plugin.permissions = { "nfc" } и NFC-считыватель для проверки.
USB serial over a plain OTG cable, with the chip drivers built into WEX: CDC-ACM, CH340 / CH341 / CH9102, CP2102 / CP2104 and FTDI. devices() reports vendorId, productId, product, driver and whether the user has allowed access; Android asks about each device separately the first time it is opened.
USB-serial через обычный OTG-кабель, драйверы чипов встроены в WEX: CDC-ACM, CH340 / CH341 / CH9102, CP2102 / CP2104 и FTDI. devices() отдаёт vendorId, productId, product, driver и выдан ли доступ; про каждое устройство Android спрашивает отдельно при первом открытии.
for _, d inipairs(Wex.usb.devices()) doui:kv(d.product, d.vendorHex .. ":" .. d.productHex .. " " .. d.driver)
end
Opens the first matching device, asking the user for access if needed. Defaults are 115200 8N1 with DTR asserted. onLine splits the incoming stream on newlines, which is what a serial console wants; onData gives every chunk as { hex, text, bytes, len }.
Открывает первое подходящее устройство, спросив доступ, если нужно. По умолчанию 115200 8N1 с поднятым DTR. onLine режет входящий поток по переводам строк — то, что нужно последовательной консоли; onData отдаёт каждый кусок как { hex, text, bytes, len }.
local port = Wex.usb.open({ baud = 115200, onLine = function(line)
log:append(line)
end })
port:writeLine("AT")
Writes go through the worker, so a slow device never blocks the screen. control() is the escape hatch for vendor-specific setup: a raw USB control transfer on the open connection. Wex.usb.onAttach / onDetach fire when hardware is plugged in or pulled out, and an unplugged port closes itself with reason "unplugged".
Запись идёт через рабочий поток, так что медленное устройство не блокирует экран. control() — запасной выход для вендорских настроек: сырой USB control transfer на открытом соединении. Wex.usb.onAttach и onDetach срабатывают при подключении и отключении железа, а выдернутый порт закрывается сам с причиной "unplugged".
#Wex.usb.openRaw(device) → { endpoints, transferIn, transferOut, control, close }
Raw access to any USB endpoint, beyond the serial (UART) bridge: bulk and interrupt transfers plus control transfers for SDR receivers (RTL-SDR/HackRF), UVC thermal cameras/endoscopes, custom HID, CAN adapters and JTAG/SWD probes. endpoints() lists every endpoint (address, type, direction, maxPacketSize); transferIn/transferOut move bytes on one; control() does a setup packet. Transfers block — run streaming reads inside Wex.worker. Needs Plugin.permissions = { "usb" }.
Прямой доступ к любому USB-endpoint, помимо серийного (UART) моста: bulk и interrupt передачи плюс control-передачи для SDR-приёмников (RTL-SDR/HackRF), UVC-тепловизоров/эндоскопов, кастомных HID, CAN-адаптеров и JTAG/SWD-программаторов. endpoints() перечисляет все endpoints (адрес, тип, направление, maxPacketSize); transferIn/transferOut двигают байты по одному; control() шлёт setup-пакет. Передачи блокирующие — потоковое чтение запускай внутри Wex.worker. Нужно Plugin.permissions = { "usb" }.
Declarative plug-and-play device drivers layered over Wex.usb — a driver is a small table, so supporting a new module is a handful of lines. register(name, spec) stores a driver where spec = { match = { vendorId, productId } (or { vid, pid }), baud?, open = function(port) → device }. open(name, fn(device, err)) probes for an attached device matching the driver, requests USB permission, opens the serial port, and hands it to your open(port) to return a typed device table; on failure err is "no_driver", "denied" or "open_failed". Drivers ship as library plugins and are shared through Wex.import, and each pairs naturally with Wex.packet to build and parse the device's frames. Needs Plugin.permissions = {"usb"} (inherited from the serial access it drives). Typical targets: a CC1101 board on a USB-UART bridge, a Flipper Zero over CDC-ACM, an RTL-SDR, a Marauder.
Декларативные plug-and-play драйверы устройств поверх Wex.usb — драйвер это маленькая таблица, так что поддержать новый модуль — несколько строк. register(name, spec) сохраняет драйвер, где spec = { match = { vendorId, productId } (или { vid, pid }), baud?, open = function(port) → device }. open(name, fn(device, err)) ищет подключённое устройство под драйвер, запрашивает разрешение USB, открывает serial-порт и передаёт его в твой open(port), возвращающий типизированную таблицу устройства; при неудаче err — "no_driver", "denied" или "open_failed". Драйверы поставляются как библиотечные плагины и шарятся через Wex.import, и каждый естественно сочетается с Wex.packet для сборки и разбора кадров устройства. Нужно Plugin.permissions = {"usb"} (наследуется от serial-доступа, которым он управляет). Типичные цели: плата CC1101 на USB-UART мосте, Flipper Zero по CDC-ACM, RTL-SDR, Marauder.
Plugin.permissions = { "usb" }
-- a CC1101-over-serial driver, registered once (often in a library plugin)Wex.driver.register("cc1101", {
match = { vendorId = 0x1A86, productId = 0x7523 }, -- CH340 USB-UART bridge
baud = 115200,
open = function(port)
return {
setFreq = function(mhz) port:write(Wex.packet.build{ Cmd{ reg = "FREQ", mhz = mhz } }:bytes()) end,
rx = function(cb) port:onData(cb) end,
}
end,
})
Wex.driver.open("cc1101", function(dev, err)
ifnot dev thenreturnWex.toast("no radio: " .. (err or"")) end
dev.setFreq(433.92)
dev.rx(function(frame) Wex.bus.emit("rf.frame", frame) end)
end)
Wex · Bluetooth ClassicWex · Классический Bluetooth
Classic Bluetooth, for the HC-05 modules, OBD-II readers, thermal printers and shop equipment that never moved to BLE. Shares the "bluetooth" permission with Wex.ble. address() returns the fixed placeholder Android gives every app since 6.0.
Классический Bluetooth — для модулей HC-05, OBD-II сканеров, чековых принтеров и прочего оборудования, которое так и не перешло на BLE. Использует то же разрешение "bluetooth", что и Wex.ble. address() возвращает заглушку, которую Android отдаёт всем приложениям с версии 6.0.
for _, d inipairs(Wex.bt.bonded()) doui:kv(d.name, d.address) end
Classic discovery. Each device arrives once as { address, name, rssi, bonded, type, deviceClass }; onDone gets everything seen when the scan finishes or the timeout expires. pair() starts the system pairing flow. Unpairing is not open to apps, so unpair() opens Bluetooth settings.
Классический поиск. Каждое устройство приходит один раз как { address, name, rssi, bonded, type, deviceClass }; onDone получает всё увиденное по завершении или таймауту. pair() запускает системное сопряжение. Разрыв пары приложениям недоступен, поэтому unpair() открывает настройки Bluetooth.
Wex.bt.discover({ timeoutMs = 12000,
onDevice = function(d) list:append({ title = d.name, subtitle = d.address }) end })
Opens an RFCOMM (Serial Port Profile) link. The default UUID is SPP, which is what almost every module answers on; secure = false falls back to an insecure socket for modules that refuse pairing. link:write(text), writeLine(text), writeHex(hex), isOpen(), close().
Открывает RFCOMM-канал (Serial Port Profile). UUID по умолчанию — SPP, на нём отвечает почти любой модуль; secure = false переключает на незащищённый сокет для модулей, которые отказываются от сопряжения. link:write(text), writeLine(text), writeHex(hex), isOpen(), close().
local link = Wex.bt.connect(addr, {
onLine = function(line) term:append(line) end,
onDisconnect = function(why) ui:toast(why) end,
})
link:writeLine("AT+NAME?")
The Wi-Fi radio as a survey tool. Apps have not been able to switch Wi-Fi on since Android 10, so panel() opens the system panel where the user can do it without leaving your screen.
Wi-Fi как инструмент обзора. Приложения не могут включать Wi-Fi с Android 10, поэтому panel() открывает системную панель, где это делает сам пользователь, не уходя с вашего экрана.
Scans for networks. Each entry is { ssid, hidden, bssid, rssi, level, freq, channel, band, width, security, capabilities }, strongest first. Android throttles scans to a few per two minutes; when a request is refused the cached list is handed over instead and note is "cached".
Сканирует сети. Каждая запись — { ssid, hidden, bssid, rssi, level, freq, channel, band, width, security, capabilities }, сильные первыми. Android ограничивает сканы несколькими за две минуты; при отказе отдаётся кэш, а note равен "cached".
Continuous position updates until :stop(). Each fix has lat, lon, alt, accuracy, speed, bearing, time, provider. interval in ms, distance in m. Needs Plugin.permissions = { "location" }.
Непрерывные обновления позиции до :stop(). У каждого fix есть lat, lon, alt, accuracy, speed, bearing, time, provider. interval в мс, distance в м. Нужно Plugin.permissions = { "location" }.
once() delivers a single fresh fix (last-known if recent, else the next one). last() returns the best last-known fix immediately without waiting, or nil.
once() отдаёт один свежий fix (последний известный, если недавний, иначе следующий). last() сразу возвращает лучший последний известный fix без ожидания, или nil.
The live GNSS constellation: per satellite svid, cn0 (C/N0 dBHz), azimuth, elevation, used (in fix), type (GPS/GLONASS/Galileo/BeiDou/…). For sky plots and signal-strength views.
Живое созвездие GNSS: по спутнику svid, cn0 (C/N0 dBHz), azimuth, elevation, used (в fix), type (GPS/GLONASS/Galileo/BeiDou/…). Для карт неба и графиков уровня сигнала.
Computes a cryptographic digest of data using MD5, SHA-1, SHA-256, SHA-384 or SHA-512. The optional opts controls how the input is interpreted (input=utf8|hex|base64|raw) and how the digest is encoded on return (encoding=hex|base64|raw, defaulting to hex). Use it to fingerprint files, build cache keys, or verify a downloaded payload against a known checksum.
Вычисляет криптографический хеш данных с помощью MD5, SHA-1, SHA-256, SHA-384 или SHA-512. Необязательный opts задаёт интерпретацию входа (input=utf8|hex|base64|raw) и кодировку результата (encoding=hex|base64|raw, по умолчанию hex). Используйте для отпечатков файлов, построения ключей кеша или проверки скачанного содержимого по известной контрольной сумме.
Produces a keyed HMAC over a message for integrity and authentication, using the same hash family as hash(). opts lets you set input, keyEncoding and encoding independently, so the key and message can arrive in different formats. A typical use is validating an inbound webhook by recomputing its HMAC-SHA256 signature and comparing it to the header the sender supplied.
Формирует HMAC сообщения с ключом для контроля целостности и аутентификации, используя то же семейство хеш-функций, что и hash(). opts позволяет отдельно задать input, keyEncoding и encoding, поэтому ключ и сообщение могут поступать в разных форматах. Типичное применение — проверка входящего webhook: пересчитываете подпись HMAC-SHA256 и сравниваете её со значением из заголовка отправителя.
local sig = Wex.crypto.hmac("sha256", body, secret, { encoding = "base64" })
if sig == header_signature thenWex.log("webhook signature ok")
elseWex.log("rejected: bad signature")
end
Generates a fresh random AES key of 128, 192 or 256 bits (default 256) and returns it base64-encoded. The key is suitable for the aes.encrypt/decrypt pair and should be stored securely — for long-lived secrets prefer the hardware-backed keystore. Call it once per encrypted store and reuse the returned string.
Генерирует новый случайный ключ AES на 128, 192 или 256 бит (по умолчанию 256) и возвращает его в base64. Ключ подходит для пары aes.encrypt/decrypt и должен храниться безопасно — для долговременных секретов лучше использовать аппаратный keystore. Вызывайте один раз для каждого шифрованного хранилища и повторно используйте полученную строку.
Encrypts and decrypts data with AES-GCM authenticated encryption; encrypt returns a single base64 blob packing the random IV, ciphertext and auth tag, and decrypt reverses it, returning nil if the key is wrong or the blob was tampered with. opts.keyEncoding selects how the key string is decoded (hex|base64|raw, default base64) and opts.aad binds optional associated data that must match on decrypt. Use it to store confidential fields with a key from genKey().
Шифрует и расшифровывает данные с аутентифицированным шифрованием AES-GCM; encrypt возвращает единый base64-блоб, упаковывающий случайный IV, шифртекст и тег аутентичности, а decrypt выполняет обратную операцию и возвращает nil, если ключ неверен или блоб был изменён. opts.keyEncoding задаёт декодирование строки ключа (hex|base64|raw, по умолчанию base64), а opts.aad привязывает дополнительные данные, которые должны совпасть при расшифровке. Используйте для хранения конфиденциальных полей с ключом из genKey().
local key = Wex.crypto.aes.genKey(256)
local blob = Wex.crypto.aes.encrypt("card #4111", key, { aad = "v1" })
local text = Wex.crypto.aes.decrypt(blob, key, { aad = "v1" })
Wex.log(text or"decrypt failed")
A high-level password box: seal derives a key from the password and returns a self-contained AES-GCM blob, while open reverses it and returns nil on a wrong password or corrupted data. It bundles salt, IV and tag for you, so you never handle raw key material — ideal for quick, user-password-protected snippets like an encrypted note or exported settings.
Высокоуровневый парольный контейнер: seal выводит ключ из пароля и возвращает самодостаточный блоб AES-GCM, а open выполняет обратную операцию и возвращает nil при неверном пароле или повреждённых данных. Соль, IV и тег упаковываются автоматически, поэтому вам не нужно работать с сырым ключевым материалом — удобно для быстрых защищённых паролем фрагментов, например зашифрованной заметки или экспорта настроек.
local box = Wex.crypto.seal("diary entry", "hunter2")
local text = Wex.crypto.open(box, "hunter2")
Wex.log(text or"wrong password")
Derives a fixed-length key from a password and salt via PBKDF2, returning the derived bytes as hex. iters sets the work factor (higher is slower and safer), dkLen the output length in bytes, and algo the underlying PRF (sha1|sha256|sha512, default sha1). Use it to stretch a user password into an AES key or to store a slow, salted password verifier.
Выводит ключ фиксированной длины из пароля и соли по алгоритму PBKDF2, возвращая полученные байты в hex. iters задаёт фактор трудоёмкости (больше — медленнее и безопаснее), dkLen — длину результата в байтах, а algo — базовую PRF (sha1|sha256|sha512, по умолчанию sha1). Используйте, чтобы растянуть пользовательский пароль в ключ AES или сохранить медленный проверочный хеш пароля с солью.
local salt = Wex.crypto.hash("sha256", "[email protected]")
local dk = Wex.crypto.pbkdf2("hunter2", salt, 100000, 32, "sha256")
Wex.log("derived key: " .. dk)
Generates an asymmetric key pair and returns a table with algo plus base64 DER publicKey and privateKey. Pass "rsa" with opts.bits of 2048, 3072 or 4096, or "ec" with opts.curve P-256 or P-384. The returned keys feed directly into sign()/verify(); keep the private key secret and distribute only the public half.
Генерирует асимметричную пару ключей и возвращает таблицу с полем algo и ключами publicKey и privateKey в формате base64 DER. Передайте "rsa" с opts.bits равным 2048, 3072 или 4096, либо "ec" с opts.curve P-256 или P-384. Полученные ключи напрямую подходят для sign()/verify(); храните приватный ключ в секрете и распространяйте только публичную часть.
Creates and checks digital signatures over a message; sign returns a base64 signature made with the private key, and verify returns true only if the signature matches the message under the public key. Both accept keys as PEM or base64 DER, matching the output of keygen(). Use them to prove authorship of an update manifest or to authenticate messages between two parties.
Создаёт и проверяет цифровые подписи сообщения; sign возвращает подпись в base64, сделанную приватным ключом, а verify возвращает true только если подпись соответствует сообщению под данным публичным ключом. Оба принимают ключи в формате PEM или base64 DER, совпадающем с выводом keygen(). Используйте для подтверждения авторства манифеста обновления или аутентификации сообщений между двумя сторонами.
local kp = Wex.crypto.keygen("rsa", { bits = 2048 })
local sig = Wex.crypto.sign("rsa", kp.privateKey, "release-1.2.0")
local ok = Wex.crypto.verify("rsa", kp.publicKey, "release-1.2.0", sig)
Wex.log(ok and"valid"or"tampered")
Encrypts and decrypts data with a per-plugin AES key held in the Android Keystore, referenced only by name — the key is non-exportable and hardware-backed where the device supports it, so raw key bytes never enter your Lua code or storage. encrypt returns an opaque blob and decrypt returns nil if the blob is wrong or the key is missing. This is the safest place to keep tokens or credentials that must survive restarts.
Шифрует и расшифровывает данные ключом AES, привязанным к плагину и хранящимся в Android Keystore, к которому обращаются только по имени: ключ неэкспортируемый и аппаратно защищённый там, где устройство это поддерживает, поэтому сырые байты ключа никогда не попадают в ваш Lua-код или хранилище. encrypt возвращает непрозрачный блоб, а decrypt возвращает nil, если блоб неверен или ключ отсутствует. Это самое безопасное место для токенов или учётных данных, которые должны переживать перезапуски.
local blob = Wex.crypto.keystore.encrypt("token", "ya29.a0secret")
-- key never leaves the device; hardware-backed where availablelocal token = Wex.crypto.keystore.decrypt("token", blob)
Wex.log(token or"decrypt failed")
Manages a named EC P-256 signing key whose private half never leaves the Android Keystore: keygen creates it and returns only the publicKey, sign produces a base64 signature the device backs, has and delete manage its lifecycle, and info reports { exists, hardwareBacked, algo }. Because signing happens inside the keystore, a compromised plugin can request signatures but can never extract the key. Use it for device attestation or per-device request signing.
Управляет именованным подписывающим ключом EC P-256, приватная часть которого никогда не покидает Android Keystore: keygen создаёт его и возвращает только publicKey, sign формирует подпись в base64 силами устройства, has и delete управляют жизненным циклом, а info сообщает { exists, hardwareBacked, algo }. Поскольку подпись выполняется внутри keystore, скомпрометированный плагин может запросить подпись, но не может извлечь ключ. Используйте для аттестации устройства или подписи запросов на уровне устройства.
ifnotWex.crypto.keystore.has("signing") thenlocal pub = Wex.crypto.keystore.keygen("signing")
Wex.log("device pubkey: " .. pub.publicKey)
endlocal sig = Wex.crypto.keystore.sign("signing", "nonce-42")
local info = Wex.crypto.keystore.info("signing")
Wex.log("hw-backed: " .. tostring(info.hardwareBacked))
Wex.crypto.keystore.delete("signing")
Builds and signs a JSON Web Token from a claims table. opts.alg selects the algorithm (HS256/384/512 with a shared secret, or RS256/ES256 with a private key), and opts.expiresIn adds an exp claim that many seconds into the future. Returns the compact token string. Use it to mint short-lived tokens for your own backend calls.
Строит и подписывает JSON Web Token из таблицы claims. opts.alg выбирает алгоритм (HS256/384/512 с общим секретом либо RS256/ES256 с приватным ключом), а opts.expiresIn добавляет claim exp на указанное число секунд в будущее. Возвращает компактную строку токена. Используйте для выпуска короткоживущих токенов к собственному бэкенду.
local claims = { sub = "user-7", role = "admin" }
local token = Wex.jwt.sign(claims, secret, { alg = "HS256", expiresIn = 3600 })
Wex.log("token: " .. token)
Verifies a JWT's signature and standard claims, returning { ok = true, claims } on success or { ok = false, error } on failure. opts.alg is a REQUIRED comma-separated allowlist of accepted algorithms — this blocks algorithm-confusion attacks (such as a forged token switching to none or HS256), so verify never trusts the algorithm named in the token's own header. opts.ignoreExpiration skips the exp check when you need it.
Проверяет подпись JWT и стандартные claims, возвращая { ok = true, claims } при успехе или { ok = false, error } при неудаче. opts.alg — ОБЯЗАТЕЛЬНЫЙ список разрешённых алгоритмов через запятую: он блокирует атаки на подмену алгоритма (например, подделку токена с переключением на none или HS256), поэтому verify никогда не доверяет алгоритму, указанному в заголовке самого токена. opts.ignoreExpiration отключает проверку exp, когда это нужно.
local res = Wex.jwt.verify(token, secret, { alg = "HS256" })
if res.ok thenWex.log("hello " .. res.claims.sub)
elseWex.log("rejected: " .. res.error)
end
#Wex.jwt.decode(token) → { header, payload } (no verification)
Decodes a JWT and returns its { header, payload } without any signature check — useful for reading claims like exp or sub before deciding how to handle a token. Because it performs no verification, never trust its output for authorization; always run jwt.verify() with an alg allowlist before acting on the claims. Handy for logging or routing based on a token's contents.
Декодирует JWT и возвращает его { header, payload } без какой-либо проверки подписи — удобно, чтобы прочитать claims вроде exp или sub, прежде чем решить, как обработать токен. Поскольку проверка не выполняется, никогда не доверяйте этому результату для авторизации; перед использованием claims всегда вызывайте jwt.verify() со списком разрешённых алгоритмов. Пригодится для логирования или маршрутизации по содержимому токена.
local t = Wex.jwt.decode(token)
Wex.log("alg = " .. t.header.alg)
Wex.log("sub = " .. t.payload.sub)
Wex · Codecs & SerializationWex · Кодеки и сериализация
Wex.codec.base58 encodes and decodes binary data with the Bitcoin Base58 alphabet, which omits the visually ambiguous characters 0, O, I and l. encode(bytes) turns raw bytes into a compact string and decode(str) restores the original bytes, raising an error on characters outside the alphabet. Use it to render key material, wallet-style addresses or short identifiers in a copy-paste-safe form.
Wex.codec.base58 кодирует и декодирует бинарные данные с использованием алфавита Base58 из Bitcoin, в котором отсутствуют визуально неоднозначные символы 0, O, I и l. encode(bytes) превращает сырые байты в компактную строку, а decode(str) восстанавливает исходные байты и выдаёт ошибку при символах вне алфавита. Применяйте для представления ключевого материала, адресов в стиле кошельков или коротких идентификаторов в безопасном для копирования виде.
local s = Wex.codec.base58.encode(pubkey)
Wex.log("addr = " .. s)
local raw = Wex.codec.base58.decode(s)
ui:text("bytes: " .. #raw)
Wex.codec.base85 implements ascii85, packing four bytes into five printable ASCII characters for about 25 percent overhead versus 33 percent for Base64. encode(bytes) returns the ascii85 string and decode(str) returns the original bytes, erroring on malformed input. Reach for it when you need a more compact text encoding for embedding a binary blob in logs or config.
Wex.codec.base85 реализует ascii85, упаковывая четыре байта в пять печатных ASCII-символов, что даёт около 25 процентов накладных расходов против 33 процентов у Base64. encode(bytes) возвращает строку ascii85, а decode(str) — исходные байты, выдавая ошибку при некорректном вводе. Используйте, когда нужно более компактное текстовое кодирование для встраивания бинарного блока в логи или конфигурацию.
local txt = Wex.codec.base85.encode(blob)
Wex.log(txt)
local blob2 = Wex.codec.base85.decode(txt)
Wex.log("ok: " .. tostring(#blob2 == #blob))
#Wex.codec.cbor.encode(value) → bytes / Wex.codec.cbor.decode(bytes) → value
Wex.codec.cbor serializes Lua values to and from Concise Binary Object Representation per RFC 8949, a compact self-describing binary format for maps, arrays, numbers, strings and booleans. encode(value) returns bytes and decode(bytes) returns the reconstructed value. It is ideal for space-efficient message payloads or persisting structured state, and it preserves explicit array nulls via the Wex.codec.null sentinel.
Wex.codec.cbor сериализует значения Lua в формат Concise Binary Object Representation по RFC 8949 и обратно — компактный самоописывающий бинарный формат для отображений, массивов, чисел, строк и логических значений. encode(value) возвращает байты, а decode(bytes) — восстановленное значение. Он хорошо подходит для экономичных полезных нагрузок сообщений или сохранения структурированного состояния и сохраняет явные null внутри массивов через маркер Wex.codec.null.
local msg = { id = 42, ok = true, tags = { "a", "b" } }
local bytes = Wex.codec.cbor.encode(msg)
local back = Wex.codec.cbor.decode(bytes)
Wex.log("id=" .. back.id .. " n=" .. #bytes)
#Wex.codec.msgpack.encode(value) → bytes / Wex.codec.msgpack.decode(bytes) → value
Wex.codec.msgpack serializes Lua values to and from MessagePack, a compact binary format similar in spirit to JSON but smaller and faster to parse. encode(value) returns bytes and decode(bytes) returns the value, mapping Lua tables to maps or arrays and numbers to the tightest MessagePack type. Use it for interop with services or caches that already speak MessagePack.
Wex.codec.msgpack сериализует значения Lua в формат MessagePack и обратно — компактный бинарный формат, близкий по духу к JSON, но меньше по размеру и быстрее в разборе. encode(value) возвращает байты, а decode(bytes) — значение, отображая таблицы Lua в map или array, а числа — в наиболее компактный тип MessagePack. Применяйте для взаимодействия с сервисами или кешами, которые уже работают с MessagePack.
local packed = Wex.codec.msgpack.encode({ user = "ada", lvl = 7 })
local t = Wex.codec.msgpack.decode(packed)
Wex.log(t.user .. " -> " .. t.lvl)
Wex.codec.deflate compresses bytes with DEFLATE and Wex.codec.inflate decompresses them; both take an optional opts.wrap of zlib (default, adds a zlib header and checksum) or raw (headerless stream). deflate returns the compressed bytes and inflate returns the original, capping its output at 32 MiB to guard against decompression bombs. Use it to shrink a payload before storage or transport, pairing the same wrap mode on both sides.
Wex.codec.deflate сжимает байты алгоритмом DEFLATE, а Wex.codec.inflate распаковывает их; оба принимают необязательный opts.wrap со значением zlib (по умолчанию, добавляет заголовок zlib и контрольную сумму) или raw (поток без заголовка). deflate возвращает сжатые байты, а inflate — исходные, ограничивая свой вывод 32 MiB для защиты от декомпрессионных бомб. Используйте для уменьшения полезной нагрузки перед хранением или передачей, указывая одинаковый режим wrap на обеих сторонах.
local packed = Wex.codec.deflate(payload)
Wex.log("saved " .. (#payload - #packed) .. " bytes")
local raw = Wex.codec.inflate(packed, { wrap = "zlib" })
Wex.log("restored: " .. #raw)
Wex.codec.gzip wraps DEFLATE in the gzip container (with header, CRC32 and length) and Wex.codec.gunzip reverses it, producing output interchangeable with the standard gzip tool. gzip(bytes) returns the compressed bytes and gunzip(bytes) returns the original. Use it when you need .gz-compatible data, for example to write a log archive or to decode a gzipped HTTP response body.
Wex.codec.gzip оборачивает DEFLATE в контейнер gzip (с заголовком, CRC32 и длиной), а Wex.codec.gunzip выполняет обратное преобразование, создавая данные, совместимые со стандартной утилитой gzip. gzip(bytes) возвращает сжатые байты, а gunzip(bytes) — исходные. Применяйте, когда нужны данные, совместимые с .gz, например для записи архива логов или для разбора gzip-сжатого тела HTTP-ответа.
local gz = Wex.codec.gzip(logText)
Wex.log("gz size: " .. #gz)
local plain = Wex.codec.gunzip(gz)
ui:text(plain)
Wex.codec.hexdump renders bytes as a classic hex-plus-ASCII dump string for debugging, with opts.width setting the bytes per row (default 16) and opts.offset setting the starting address shown in the left column. It returns a ready-to-print multi-line string. Use it to inspect the exact contents of a binary buffer, such as a decoded packet or a file header.
Wex.codec.hexdump формирует классический дамп байтов в виде строки «hex плюс ASCII» для отладки: opts.width задаёт число байтов в строке (по умолчанию 16), а opts.offset — начальный адрес, отображаемый в левом столбце. Функция возвращает готовую к выводу многострочную строку. Применяйте для просмотра точного содержимого бинарного буфера, например декодированного пакета или заголовка файла.
Wex.codec.varint encodes a non-negative integer as unsigned LEB128 variable-length bytes and decodes them back. encode(n) returns the byte sequence, and decode(bytes, pos?) reads a varint starting at the optional 1-based position pos and returns two values: the decoded number and how many bytes it consumed. Use it to build or parse compact framed formats where small numbers should take few bytes.
Wex.codec.varint кодирует неотрицательное целое число в байты переменной длины unsigned LEB128 и декодирует их обратно. encode(n) возвращает последовательность байтов, а decode(bytes, pos?) читает varint с необязательной позиции pos (отсчёт с 1) и возвращает два значения: декодированное число и количество использованных байтов. Применяйте для построения или разбора компактных форматов с кадрами, где малые числа должны занимать мало байтов.
local b = Wex.codec.varint.encode(300)
Wex.log("bytes used: " .. #b)
local n, consumed = Wex.codec.varint.decode(b, 1)
Wex.log(n .. " in " .. consumed .. " bytes")
Wex.codec.null is a shared sentinel value that represents an explicit null inside an array, since a plain Lua nil would truncate the sequence or leave a hole in it. Store it in a table slot before encoding with cbor or msgpack to round-trip a real null, and compare a decoded slot against Wex.codec.null to detect one. This keeps array positions and null semantics intact across serialization.
Wex.codec.null — это общий маркер, обозначающий явный null внутри массива, поскольку обычный nil в Lua обрезал бы последовательность или оставил в ней дыру. Помещайте его в ячейку таблицы перед кодированием через cbor или msgpack, чтобы корректно передать настоящий null, и сравнивайте декодированную ячейку с Wex.codec.null для его обнаружения. Так позиции в массиве и семантика null сохраняются при сериализации.
local arr = { 1, Wex.codec.null, 3 }
local bytes = Wex.codec.cbor.encode(arr)
local back = Wex.codec.cbor.decode(bytes)
Wex.log("slot2 is null: " .. tostring(back[2] == Wex.codec.null))
Wex.proto.message defines a proto3 schema from a name and a fields list, where each field is a table with name, tag, type and an optional repeated flag. Supported types are int32, int64, uint, sint, bool, string, bytes, float, double and message:Name for nesting; the returned schema exposes schema:encode(table) and schema:decode(bytes) using proto3 wire format with packed repeated scalars. Use it to exchange a defined Protocol Buffers message with a real service.
Wex.proto.message определяет схему proto3 по имени и списку полей, где каждое поле — таблица с name, tag, type и необязательным флагом repeated. Поддерживаются типы int32, int64, uint, sint, bool, string, bytes, float, double и message:Name для вложенности; возвращённая схема предоставляет schema:encode(table) и schema:decode(bytes) в формате proto3 wire с упакованными повторяющимися скалярами. Применяйте для обмена определёнными сообщениями Protocol Buffers с реальным сервисом.
local Person = Wex.proto.message("Person", {
{ name = "id", tag = 1, type = "int32" },
{ name = "name", tag = 2, type = "string" },
{ name = "tags", tag = 3, type = "string", repeated = true },
})
local bytes = Person:encode({ id = 7, name = "ada", tags = { "x", "y" } })
local p = Person:decode(bytes)
Wex.log(p.name .. " #" .. p.id)
Wex.proto.raw.decode parses protobuf bytes without a schema, returning a list of entries each carrying field (the tag number), wireType and the raw value, which is invaluable for reverse-engineering an unknown message. The helpers Wex.proto.hex(bytes) and Wex.proto.fromHex(str) convert between bytes and a hex string so you can paste a captured payload directly. Together they let you inspect any Protocol Buffers blob field by field.
Wex.proto.raw.decode разбирает байты protobuf без схемы, возвращая список записей, у каждой из которых есть field (номер тега), wireType и сырое значение, что незаменимо при реверс-инжиниринге неизвестного сообщения. Вспомогательные функции Wex.proto.hex(bytes) и Wex.proto.fromHex(str) преобразуют байты в hex-строку и обратно, чтобы можно было вставить перехваченную полезную нагрузку напрямую. Вместе они позволяют исследовать любой блок Protocol Buffers поле за полем.
local bytes = Wex.proto.fromHex("089601120361646d")
for _, f in ipairs(Wex.proto.raw.decode(bytes)) doWex.log("field " .. f.field .. " wt=" .. f.wireType)
endWex.log("hex: " .. Wex.proto.hex(bytes))
Creates a stateful RBJ-cookbook biquad filter whose kind is one of lowpass, highpass, bandpass, notch, peak, lowshelf or highshelf, configured through opts.freq, opts.q, opts.sampleRate and (for peak and shelf kinds) opts.gainDb. Call filter:process(samples) on successive PCM blocks to filter them; internal state carries across calls so streaming audio stays continuous, and filter:reset() clears that state between independent streams. process returns the filtered sample array. A common use is rolling off hiss from Wex.audio.record output with a lowpass before further analysis.
Создаёт биквадратный фильтр по RBJ cookbook с сохранением состояния; kind задаёт тип — lowpass, highpass, bandpass, notch, peak, lowshelf или highshelf, а параметры opts.freq, opts.q, opts.sampleRate и (для peak и shelf) opts.gainDb настраивают его характеристику. Метод filter:process(samples) фильтрует последовательные блоки PCM, сохраняя внутреннее состояние между вызовами, что обеспечивает непрерывность потокового звука, а filter:reset() сбрасывает это состояние между независимыми потоками. process возвращает массив отфильтрованных отсчётов. Типичное применение — срезать шипение из вывода Wex.audio.record фильтром lowpass перед анализом.
Runs the Goertzel algorithm over samples to measure the energy at a single targetFreq (in Hz) for the given sampleRate, returning a scalar magnitude. It is far cheaper than a full FFT when you only need one or a few bins, which makes it ideal for detecting DTMF tones or a fixed pilot frequency. Compare the returned magnitude against a threshold to decide whether the tone is present.
Выполняет алгоритм Гёрцеля по массиву samples, измеряя энергию на одной целевой частоте targetFreq (в Гц) для заданной sampleRate, и возвращает скалярную магнитуду. Он намного дешевле полного FFT, когда нужны лишь одна или несколько частотных корзин, что делает его идеальным для обнаружения тонов DTMF или фиксированной пилотной частоты. Сравните возвращённую магнитуду с порогом, чтобы решить, присутствует ли тон.
local pcm = Wex.audio.record(0.1)
local mag = Wex.dsp.goertzel(pcm, 941, 8000)
if mag > 0.5thenWex.log("row tone 941 Hz present")
end
Linearly resamples samples from fromRate to toRate (both in Hz) and returns a new array whose length scales by toRate/fromRate. Use it to bring recordings to a common rate before goertzel or convolution, or to downsample 44100 Hz capture to 8000 Hz for DTMF work. Linear interpolation keeps it fast and fully headless at the cost of some high-frequency accuracy.
Линейно передискретизирует samples с частоты fromRate на toRate (обе в Гц) и возвращает новый массив, длина которого масштабируется как toRate/fromRate. Используйте, чтобы привести записи к общей частоте перед goertzel или свёрткой, либо чтобы понизить частоту захвата 44100 Гц до 8000 Гц для работы с DTMF. Линейная интерполяция сохраняет скорость и работу без разрешений ценой некоторой потери точности на высоких частотах.
Follows the amplitude envelope of samples and returns a same-length array that rises with opts.attack and falls with opts.release (both in seconds, relative to opts.sampleRate); with no opts it uses sensible defaults. It is an amplitude follower, so the output tracks loudness rather than the waveform itself. Handy for gating, VU-style level meters, or triggering an action when loudness crosses a threshold.
Отслеживает амплитудную огибающую массива samples и возвращает массив той же длины, который нарастает со скоростью opts.attack и спадает со скоростью opts.release (обе в секундах, относительно opts.sampleRate); без opts используются разумные значения по умолчанию. Это следящий за амплитудой детектор, поэтому выход отражает громкость, а не саму форму волны. Удобно для гейтинга, индикаторов уровня в стиле VU или запуска действия, когда громкость пересекает порог.
local pcm = Wex.audio.record(1.0)
local env = Wex.dsp.envelope(pcm, { attack = 0.01, release = 0.2, sampleRate = 44100 })
local peak = 0for _, v in ipairs(env) doif v > peak then peak = v endendWex.log("peak level " .. peak)
Both smooth a sample array over a sliding window of window points: movingAvg takes the arithmetic mean of each window for gentle low-pass smoothing, while median takes the middle value, which removes clicks and impulse spikes without blurring edges. Larger windows smooth more aggressively. Use movingAvg to denoise a slow control signal and median to strip crackle from a recording before envelope or tone detection.
Обе функции сглаживают массив отсчётов по скользящему окну из window точек: movingAvg берёт среднее арифметическое каждого окна для мягкого низкочастотного сглаживания, а median берёт срединное значение, что убирает щелчки и импульсные выбросы, не размывая фронты. Чем больше окно, тем сильнее сглаживание. Применяйте movingAvg для подавления шума в медленном управляющем сигнале, а median — чтобы убрать треск из записи перед детектированием огибающей или тона.
local raw = Wex.audio.record(0.3)
local smooth = Wex.dsp.movingAvg(raw, 8)
local despiked = Wex.dsp.median(smooth, 5)
Wex.log("cleaned " .. #despiked .. " samples")
#Wex.dsp.convolve(a, b) → samples ; Wex.dsp.correlate(a, b) → samples
convolve returns the linear convolution of arrays a and b (length #a + #b - 1), applying an FIR impulse response b to signal a for reverb, echo, or a custom filter kernel. correlate returns their cross-correlation, whose peak position marks the lag where the two signals best align, useful for matched-filter detection or finding a known pattern inside a recording. Autocorrelation via correlate(a, a) exposes periodicity, which is handy for pitch estimation.
convolve возвращает линейную свёртку массивов a и b (длиной #a + #b - 1), применяя FIR-импульсную характеристику b к сигналу a — для ревербации, эха или произвольного ядра фильтра. correlate возвращает их взаимную корреляцию, положение пика которой указывает задержку наилучшего совмещения двух сигналов, что полезно для согласованной фильтрации или поиска известного шаблона в записи. Автокорреляция через correlate(a, a) выявляет периодичность, что удобно для оценки высоты тона.
local sig = Wex.audio.record(0.5)
local kernel = { 0.25, 0.5, 0.25 }
local echoed = Wex.dsp.convolve(sig, kernel)
local xcorr = Wex.dsp.correlate(sig, kernel)
local peak, at = -1, 0for i, v in ipairs(xcorr) doif v > peak then peak, at = v, i endendWex.log("conv len " .. #echoed .. ", best lag at " .. at)
Creates a cold stream that emits an incrementing tick counter (0, 1, 2, …) every ms milliseconds once subscribed. The timer starts only on the first subscribe and is torn down automatically on dispose, so nothing runs while the stream is idle. Useful for polling, animations, or driving periodic UI refreshes.
Создаёт холодный поток, который после подписки испускает возрастающий счётчик тактов (0, 1, 2, …) каждые ms миллисекунд. Таймер запускается только при первой подписке и автоматически останавливается при dispose, поэтому в простое ничего не работает. Полезно для опроса, анимаций или периодического обновления UI.
local h = Wex.stream.interval(1000)
:take(5)
:subscribe(function(n)
Wex.log("tick " .. n)
end)
Builds a stream that synchronously emits each argument in order and then completes, on every subscription. Handy for seeding a pipeline with fixed values, testing operators, or turning a small known set into a stream. Because it is cold, the same values replay for each new subscriber.
Создаёт поток, который при каждой подписке синхронно испускает каждый аргумент по порядку и завершается. Удобно для инициализации конвейера фиксированными значениями, тестирования операторов или превращения небольшого известного набора в поток. Так как поток холодный, те же значения воспроизводятся для каждого нового подписчика.
Wraps a custom producer function into a stream; producer receives an emit callback for pushing values and may return a teardown function that runs when the last subscriber unsubscribes or on dispose. This is the low-level bridge for adapting callbacks, sockets, or event listeners into the reactive world. Return the teardown to guarantee resources are released.
Оборачивает пользовательскую функцию-producer в поток; producer получает колбэк emit для отправки значений и может вернуть функцию teardown, которая вызывается при отписке последнего подписчика или при dispose. Это низкоуровневый мост для адаптации колбэков, сокетов или слушателей событий в реактивный мир. Возвращайте teardown, чтобы гарантированно освобождать ресурсы.
local s = Wex.stream.from(function(emit)
local conn = net.connect("wss://feed")
conn.onmsg = emit
returnfunction() conn:close() endend)
s:subscribe(function(msg) Wex.log(msg) end)
Emits the current value of Wex.state[key] and then a new value each time that state entry changes, letting you react to shared plugin state declaratively. Combined with the re-entrancy guard, a fromState→operators→toState chain on the same key will not loop infinitely. Ideal for keeping derived values in sync with a source of truth.
Испускает текущее значение Wex.state[key], а затем новое значение при каждом изменении этой записи состояния, позволяя декларативно реагировать на общее состояние плагина. Благодаря защите от повторного входа цепочка fromState→операторы→toState по одному ключу не зациклится. Идеально для поддержания производных значений в синхронизации с источником истины.
Combines two or more streams into one that emits every value from any input as soon as it arrives, interleaving them by time. All sources are subscribed together and torn down together when the merged stream is disposed. Use it to funnel several event sources (taps, timers, sockets) into a single handler.
Объединяет два или более потока в один, который испускает каждое значение любого из входов сразу по мере его поступления, чередуя их по времени. Все источники подписываются вместе и вместе останавливаются при dispose объединённого потока. Используйте, чтобы свести несколько источников событий (нажатия, таймеры, сокеты) в один обработчик.
local ticks = Wex.stream.interval(1000)
local taps = Wex.stream.fromState("tap")
Wex.stream.merge(ticks, taps)
:subscribe(function(v) Wex.log(tostring(v)) end)
Emits fn(latestA, latestB) whenever either input stream emits, but only after both have produced at least one value, giving you the current combination of two sources. It is the go-to for computing a value that depends on multiple live inputs, such as merging a slider position with a mode flag. Each emission carries the freshest value from each side.
Испускает fn(latestA, latestB) при каждом испускании любого из входных потоков, но только после того, как оба выдали хотя бы одно значение, давая текущую комбинацию двух источников. Это основной выбор для вычисления значения, зависящего от нескольких живых входов, например объединения положения ползунка с флагом режима. Каждое испускание несёт самое свежее значение с каждой стороны.
local w = Wex.stream.fromState("width")
local h = Wex.stream.fromState("height")
Wex.stream.combine(w, h, function(a, b) return a * b end)
:subscribe(function(area) ui:text("area: " .. area) end)
Transforms each emitted value by applying fn and emitting its result, producing a new stream without touching the source. This is the workhorse operator for reshaping data — parsing, formatting, or extracting a field — before it reaches downstream operators. It is lazy, so fn only runs while there is an active subscriber.
Преобразует каждое испущенное значение, применяя fn и испуская его результат, создавая новый поток и не затрагивая источник. Это рабочая лошадка для изменения формы данных — разбора, форматирования или извлечения поля — перед передачей дальше по конвейеру. Оператор ленивый, поэтому fn выполняется только при наличии активного подписчика.
Wex.stream.of(1, 2, 3)
:map(function(n) return n * n end)
:subscribe(function(sq) Wex.log(sq) end)
Emits only the values for which fn(value) returns true, silently dropping the rest. Use it to gate a stream on a predicate, such as ignoring empty inputs or events below a threshold, before doing further work. The predicate runs per value and does not alter the values that pass.
Испускает только те значения, для которых fn(value) возвращает true, молча отбрасывая остальные. Используйте, чтобы фильтровать поток по предикату, например игнорировать пустой ввод или события ниже порога, перед дальнейшей работой. Предикат выполняется для каждого значения и не изменяет прошедшие значения.
Runs an accumulator, emitting fn(acc, value) for each input starting from seed and carrying the running result forward, much like a streaming fold. It emits the intermediate accumulation on every value, making it perfect for running totals, counters, or building up state over time. The seed is the initial accumulator before the first emission.
Запускает аккумулятор, испуская fn(acc, value) для каждого входа, начиная с seed и перенося текущий результат дальше, подобно потоковой свёртке. Он испускает промежуточное накопление на каждом значении, что идеально для нарастающих сумм, счётчиков или построения состояния во времени. seed — это начальное значение аккумулятора до первого испускания.
Wex.stream.of(10, 20, 30)
:scan(function(sum, x) return sum + x end, 0)
:subscribe(function(total) Wex.log("sum " .. total) end)
Suppresses consecutive duplicate values, emitting a value only when it differs from the one immediately before it. This is useful for eliminating redundant work when a source re-emits the same value, such as unchanged state or repeated sensor readings. Note it compares against the previous emission, not the entire history.
Подавляет подряд идущие дубликаты, испуская значение только когда оно отличается от непосредственно предыдущего. Это полезно для устранения лишней работы, когда источник повторно испускает одно и то же значение, например неизменное состояние или повторяющиеся показания датчика. Обратите внимание: сравнение идёт с предыдущим испусканием, а не со всей историей.
Emits a value only after ms milliseconds have passed with no new value arriving, collapsing rapid bursts into a single trailing emission. It is the classic choice for search-as-you-type or resize handling, where you want to act only once activity settles. Each new value restarts the quiet-period timer.
Испускает значение только после того, как прошло ms миллисекунд без поступления нового значения, схлопывая быстрые всплески в одно завершающее испускание. Это классический выбор для поиска по мере ввода или обработки изменения размера, когда действовать нужно только после того, как активность утихнет. Каждое новое значение перезапускает таймер тишины.
Emits a value and then ignores subsequent values for ms milliseconds, capping the emission rate to at most one per window. Unlike debounce it keeps the first value of each burst, which suits high-frequency sources like scroll or pointer moves where periodic sampling is enough. It guarantees a bounded flow downstream.
Испускает значение, а затем игнорирует последующие значения в течение ms миллисекунд, ограничивая частоту испусканий не более одного за окно. В отличие от debounce он сохраняет первое значение каждого всплеска, что подходит для высокочастотных источников вроде прокрутки или движения указателя, где достаточно периодической выборки. Он гарантирует ограниченный поток вниз по конвейеру.
Groups incoming values into arrays of length n, emitting each full batch as a single table and starting a fresh buffer. Use it to process items in fixed-size chunks, such as batching network writes or drawing points in groups. Values are held until the buffer fills before being emitted.
Группирует входящие значения в массивы длины n, испуская каждую заполненную партию как одну таблицу и начиная новый буфер. Используйте для обработки элементов фиксированными порциями, например пакетной сетевой записи или отрисовки точек группами. Значения удерживаются, пока буфер не заполнится, и только затем испускаются.
Emits the source's most recent value on a fixed timer every ms milliseconds, ignoring how fast or slow the source itself emits. It decouples a bursty or irregular source from a steady downstream cadence, which is handy for updating a display at a fixed frame rate. Each tick forwards whatever the latest value happens to be.
Испускает самое последнее значение источника по фиксированному таймеру каждые ms миллисекунд, независимо от того, как быстро или медленно испускает сам источник. Это отвязывает всплесковый или нерегулярный источник от устойчивого темпа ниже по конвейеру, что удобно для обновления дисплея с фиксированной частотой кадров. Каждый такт передаёт то значение, которое оказалось последним.
Passes through the first n values and then completes the stream, automatically unsubscribing from the source. Use it to grab a bounded prefix of an otherwise infinite source, such as the first few ticks of an interval, or a one-shot with take(1). After n emissions the pipeline is torn down cleanly.
Пропускает первые n значений, а затем завершает поток, автоматически отписываясь от источника. Используйте, чтобы взять ограниченный префикс из в остальном бесконечного источника, например первые несколько тактов interval или одноразовое значение через take(1). После n испусканий конвейер аккуратно демонтируется.
Activates the cold stream and calls fn for each emitted value, returning a handle whose handle:stop() unsubscribes and triggers teardown of the whole chain. This is the terminal sink that actually starts the work; nothing upstream runs until you subscribe. Always keep the handle if you need to stop the stream before the plugin is disposed.
Активирует холодный поток и вызывает fn для каждого испущенного значения, возвращая handle, чей handle:stop() отписывает и запускает демонтаж всей цепочки. Это конечный приёмник, который фактически запускает работу; ничего выше по конвейеру не выполняется до подписки. Всегда сохраняйте handle, если нужно остановить поток до dispose плагина.
local h = Wex.stream.interval(1000)
:subscribe(function(n) Wex.log(n) end)
-- later
h:stop()
Subscribes the stream and writes every emitted value into Wex.state[key], returning a handle you can stop; it is the declarative counterpart to subscribe for pushing results back into shared state. Thanks to the re-entrancy guard, a fromState(key)→operators→toState(key) round-trip on the same key is safe and will not recurse. Use it to keep a derived state entry continuously up to date.
Подписывается на поток и записывает каждое испущенное значение в Wex.state[key], возвращая handle, который можно остановить; это декларативный аналог subscribe для отправки результатов обратно в общее состояние. Благодаря защите от повторного входа цикл fromState(key)→операторы→toState(key) по одному ключу безопасен и не рекурсирует. Используйте, чтобы непрерывно поддерживать производную запись состояния в актуальном виде.
local h = Wex.stream.fromState("celsius")
:map(function(c) return c * 9 / 5 + 32end)
:toState("fahrenheit")
Computes the great-circle distance between two WGS84 coordinates using the haversine formula and returns the result in meters. It is a pure function that needs no permissions, so it is safe to call in tight loops for proximity checks or route-length estimates. Latitude/longitude pairs are passed as decimal degrees; a common use is deciding whether a point of interest falls within a given radius.
Вычисляет расстояние по большому кругу между двумя координатами WGS84 по формуле гаверсинуса и возвращает результат в метрах. Это чистая функция, не требующая разрешений, поэтому её безопасно вызывать в плотных циклах для проверки близости или оценки длины маршрута. Пары широта/долгота передаются в десятичных градусах; типичное применение — определить, попадает ли объект в заданный радиус.
local d = Wex.geo.distance(55.7558, 37.6173, 59.9343, 30.3351)
Wex.log(string.format("Moscow to SPb: %.1f km", d / 1000))
if d < 1000thenui:text("You are within 1 km")
end
Returns the initial compass bearing (forward azimuth) in degrees from the first coordinate to the second, measured clockwise from true north in the range 0-360. Pure and permission-free, it pairs naturally with distance for navigation UIs or for pointing an arrow toward a target. Coordinates are given as decimal degrees.
Возвращает начальный компасный азимут (направление) в градусах от первой координаты ко второй, отсчитываемый по часовой стрелке от истинного севера в диапазоне 0–360. Функция чистая и не требует разрешений, она естественно сочетается с distance для навигационных интерфейсов или для наведения стрелки на цель. Координаты задаются в десятичных градусах.
local brg = Wex.geo.bearing(55.75, 37.62, 55.76, 37.64)
Wex.log("Heading: " .. math.floor(brg) .. " deg")
ui:text(string.format("Turn to %d deg", math.floor(brg)))
#Wex.geo.destination(lat, lon, bearingDeg, meters) → { lat, lon }
Given a start coordinate, a bearing in degrees and a distance in meters, computes the destination point along that great-circle path and returns it as a { lat, lon } table. It is the inverse companion to distance and bearing and is pure with no permissions. Use it to project a point ahead of the user or to draw a range ring on a map.
По начальной координате, азимуту в градусах и расстоянию в метрах вычисляет точку назначения вдоль дуги большого круга и возвращает её в виде таблицы { lat, lon }. Это обратная пара к distance и bearing, функция чистая и не требует разрешений. Используйте её, чтобы спроецировать точку впереди пользователя или нарисовать кольцо дальности на карте.
local p = Wex.geo.destination(55.75, 37.62, 90, 5000)
Wex.log(string.format("5 km east: %.5f, %.5f", p.lat, p.lon))
ui:text(p.lat .. ", " .. p.lon)
encode turns a coordinate into a compact geohash string, where the optional precision controls the string length and thus the cell size; decode reverses it back to a center { lat, lon } plus the cell bounds. Both are pure and require no permissions. Geohashes are handy for bucketing points into grid cells or as short spatial keys in a cache.
encode превращает координату в компактную строку-геохеш, при этом необязательный параметр precision задаёт длину строки и, соответственно, размер ячейки; decode выполняет обратное преобразование, возвращая центр { lat, lon } и границы ячейки bounds. Обе функции чистые и не требуют разрешений. Геохеши удобны для группировки точек по ячейкам сетки или как короткие пространственные ключи в кэше.
local h = Wex.geo.geohash.encode(55.7558, 37.6173, 7)
Wex.log("geohash: " .. h)
local g = Wex.geo.geohash.decode(h)
ui:text(string.format("center %.4f, %.4f", g.lat, g.lon))
Wex.log("lat span: " .. (g.bounds.latMax - g.bounds.latMin))
#Wex.geo.geocode(address, cb, max?) → cb([{ lat, lon, name }])
Resolves a free-form address or place name into coordinates asynchronously, invoking cb with an array of matches, each carrying lat, lon and a display name; the optional max caps how many results are returned. It requires the "location" permission and runs off the main thread, so read the results only inside the callback. Use it to turn user-typed search text into a map pin.
Асинхронно преобразует произвольный адрес или название места в координаты, вызывая cb с массивом совпадений, каждое из которых содержит lat, lon и отображаемое имя name; необязательный параметр max ограничивает число возвращаемых результатов. Функция требует разрешения «location» и работает вне главного потока, поэтому читайте результаты только внутри колбэка. Применяется, чтобы превратить введённый пользователем текст поиска в метку на карте.
#Wex.geo.reverse(lat, lon, cb) → cb({ address, city, country, postalCode })
The inverse of geocode: given a coordinate it looks up a human-readable place asynchronously and calls cb with a table holding address, city, country and postalCode. It needs the "location" permission and delivers its result through the callback. A typical use is labeling the user's current position or a tapped map point.
Обратная к geocode: по координате асинхронно находит человекочитаемое место и вызывает cb с таблицей, содержащей address, city, country и postalCode. Требует разрешения «location» и передаёт результат через колбэк. Типичное применение — подписать текущее положение пользователя или точку, по которой он нажал на карте.
Returns whether network service discovery (mDNS/DNS-SD) is available on the current device, as a boolean. It is a cheap synchronous check that needs no permission, and you should call it before attempting discover, register or resolve so the plugin fails gracefully on unsupported devices. Use it to gate LAN features behind a capability check.
Возвращает булево значение — доступно ли на текущем устройстве обнаружение сетевых служб (mDNS/DNS-SD). Это дешёвая синхронная проверка без разрешений, и её стоит вызывать перед discover, register или resolve, чтобы плагин корректно завершался на неподдерживаемых устройствах. Используйте её, чтобы включать функции локальной сети по результату проверки.
ifnotWex.nsd.available() thenui:text("Service discovery not supported")
returnendWex.log("NSD ready")
Starts browsing the local network for services of the given serviceType (for example "_http._tcp"), invoking onFound and onLost as services appear and vanish and onResolved once a service's host and port are known; it returns a handle whose stop() ends the scan. It requires the "network" permission. Always keep the handle and call handle:stop() when done so the scan does not keep running in the background.
Запускает поиск в локальной сети служб указанного типа serviceType (например «_http._tcp»), вызывая onFound и onLost по мере появления и исчезновения служб и onResolved, когда становятся известны хост и порт службы; возвращает handle, метод stop() которого останавливает поиск. Требует разрешения «network». Всегда сохраняйте handle и вызывайте handle:stop() по завершении, чтобы сканирование не продолжало работать в фоне.
#Wex.nsd.register({ name, type, port }, cb) → handle (handle:unregister())
Advertises your own service on the local network with the given name, type and port so other devices can discover it, calling cb once registration succeeds; it returns a handle whose unregister() removes the advertisement. It needs the "network" permission. Use it to make a plugin-hosted server visible to peers on the same Wi-Fi.
Публикует вашу собственную службу в локальной сети с заданными name, type и port, чтобы другие устройства могли её обнаружить, и вызывает cb после успешной регистрации; возвращает handle, метод unregister() которого снимает публикацию. Требует разрешения «network». Используйте, чтобы сделать размещённый плагином сервер видимым для узлов в той же сети Wi-Fi.
local reg = Wex.nsd.register({ name = "WexNode", type = "_http._tcp", port = 8080 }, function(s)
Wex.log("registered as " .. s.name)
ui:text("Advertising on " .. s.port)
end)
-- later: reg:unregister()
Resolves a discovered service table into its connection details, calling cb with the full record including host, port and any txt metadata. It requires the "network" permission and is typically applied to a service handed to you by discover's onFound before you actually connect. The returned host and port are what you feed into a socket or HTTP request.
Преобразует таблицу обнаруженной службы в её параметры подключения, вызывая cb с полной записью, включающей host, port и любые метаданные txt. Требует разрешения «network» и обычно применяется к службе, переданной из onFound функции discover, перед фактическим подключением. Значения host и port из результата — это то, что вы передаёте в сокет или HTTP-запрос.
Wex.nsd.resolve(service, function(s)
Wex.log(s.name .. " at " .. s.host .. ":" .. s.port)
ui:text("Connect to " .. s.host)
end)
Wex · Telephony & HealthWex · Телефония и датчики тела
Returns true when the device has telephony hardware and the plugin holds the "telephony" capability, letting you branch before calling any other telephony method. It takes no arguments and is a cheap synchronous check that performs no I/O and reads no data. Use it as a guard at plugin start so tablets or Wi-Fi-only devices skip the phone-status UI gracefully.
Возвращает true, если на устройстве есть телефонный модуль и плагину выдана возможность «telephony», что позволяет сделать проверку перед вызовом остальных методов телефонии. Не принимает аргументов и представляет собой дешёвую синхронную проверку без ввода-вывода и без чтения данных. Используйте её при запуске плагина, чтобы планшеты или устройства только с Wi-Fi корректно пропускали интерфейс со статусом телефона.
ifnotWex.telephony.available() thenWex.log("no telephony on this device")
returnendlocal i = Wex.telephony.info()
Wex.log("carrier: " .. i.carrier)
Prompts the user for the READ_PHONE_STATE permission and invokes cb with a boolean once they respond. It is asynchronous and must be called before telephony.info, signal or cells will return real data; the sandbox rejects the call entirely if the plugin lacks the "telephony" capability. Request it lazily, right before you need phone state, so the user understands why the dialog appears.
Запрашивает у пользователя разрешение READ_PHONE_STATE и вызывает cb с булевым значением после ответа. Метод асинхронный и должен быть вызван до того, как telephony.info, signal или cells начнут возвращать реальные данные; песочница полностью отклоняет вызов, если у плагина нет возможности «telephony». Запрашивайте разрешение лениво, прямо перед тем как оно понадобится, чтобы пользователю было понятно, зачем появляется диалог.
Wex.telephony.request(function(granted)
if granted thenWex.log("net: " .. Wex.telephony.info().networkType)
elseWex.log("phone state denied")
endend)
Returns a read-only snapshot of the current carrier and SIM state — operator name, MCC/MNC codes, networkType ("2G".."5G"), phoneType, simState, simCountry and a roaming flag. It needs the "telephony" capability and granted READ_PHONE_STATE, and it never places calls, sends SMS or reads contacts. Handy for showing users their live network tier or warning them when roaming is active.
Возвращает доступный только для чтения снимок текущего состояния оператора и SIM-карты: название оператора, коды MCC/MNC, networkType («2G»…«5G»), phoneType, simState, simCountry и флаг roaming. Требует возможность «telephony» и выданное разрешение READ_PHONE_STATE; метод никогда не совершает звонков, не отправляет SMS и не читает контакты. Удобно показывать пользователю его текущий тип сети или предупреждать о включённом роуминге.
local i = Wex.telephony.info()
ui:text("Carrier: " .. i.carrier)
ui:text("Network: " .. i.networkType)
if i.roaming thenui:text("Roaming ON (" .. i.simCountry .. ")")
end
Subscribes to live signal-strength updates, calling cb with { level, dbm } each time the radio reports a change, and returns a handle whose :stop() method ends the stream. It requires the "telephony" capability and is read-only — no location or identifiers are exposed. Use it to draw a live signal bar, and always call handle:stop() when your view closes to free the sandboxed listener.
Подписывается на обновления уровня сигнала в реальном времени, вызывая cb с { level, dbm } при каждом изменении, о котором сообщает радиомодуль, и возвращает handle с методом :stop() для завершения потока. Требует возможность «telephony» и работает только на чтение — местоположение и идентификаторы не раскрываются. Используйте его для живого индикатора сигнала и обязательно вызывайте handle:stop() при закрытии экрана, чтобы освободить слушатель в песочнице.
local h = Wex.telephony.signal(function(s)
ui:text(s.dbm .. " dBm (level " .. s.level .. ")")
end)
-- later, when the view closes:
h:stop()
Returns an array describing the currently visible cell towers, each entry carrying its radio type ("lte"/"gsm"/"wcdma"), cell id, tac or lac, MCC/MNC, rssi, level and a registered flag. It needs the "telephony" capability, is strictly read-only and performs no network calls. Useful for diagnostics screens that list nearby cells or highlight the serving (registered) tower.
Возвращает массив с описанием видимых сотовых вышек; каждый элемент содержит тип радио («lte»/«gsm»/«wcdma»), идентификатор соты, tac или lac, MCC/MNC, rssi, level и флаг registered. Требует возможность «telephony», работает строго на чтение и не выполняет сетевых запросов. Полезно для диагностических экранов, которые перечисляют соседние соты или выделяют обслуживающую (registered) вышку.
for _, c in ipairs(Wex.telephony.cells()) dolocal tag = c.registered and"*"or" "Wex.log(tag .. c.type .. " cid=" .. c.cid .. " " .. c.rssi .. "dBm")
end
Reports whether health sensing is usable, optionally for a specific sensor type passed as the argument (e.g. "heartRate", "steps", "accelerometer"). With no argument it checks that the "health" capability is granted and at least one sensor exists; with a type it verifies that particular sensor is present. Call it first so plugins degrade cleanly on devices without a heart-rate or step sensor.
Сообщает, доступно ли считывание показателей здоровья, при необходимости для конкретного типа датчика, переданного аргументом (например «heartRate», «steps», «accelerometer»). Без аргумента проверяет, что выдана возможность «health» и существует хотя бы один датчик; с типом проверяет наличие именно этого датчика. Вызывайте его первым, чтобы плагин корректно работал на устройствах без датчика пульса или шагомера.
Starts the heart-rate sensor and calls cb with { bpm, accuracy } on each reading. It requires the "health" capability plus the BODY_SENSORS runtime permission, and the sandbox blocks it otherwise; readings stay on-device and are read-only. Ideal for a live pulse readout during a workout view, filtering on accuracy before you trust a value; call Wex.health.stopAll() to end it on teardown.
Запускает датчик пульса и вызывает cb с { bpm, accuracy } при каждом измерении. Требует возможность «health» и системное разрешение BODY_SENSORS — иначе песочница блокирует вызов; данные остаются на устройстве и доступны только для чтения. Идеально для живого отображения пульса на экране тренировки, отфильтровывая значения по accuracy; для завершения при закрытии вызовите Wex.health.stopAll().
Wex.health.heartRate(function(r)
if r.accuracy >= 2thenui:text("Pulse: " .. r.bpm .. " bpm")
endend)
-- on teardown:Wex.health.stopAll()
Subscribes to the step counter and returns a numeric id you later pass to Wex.health.stop; cb receives { count } with the running step total. It needs the "health" capability and is read-only, reporting only step counts with no location or activity detail. Use it to update a step-goal indicator live.
Подписывается на счётчик шагов и возвращает числовой id, который позже передаётся в Wex.health.stop; cb получает { count } с текущим суммарным числом шагов. Требует возможность «health» и работает только на чтение, сообщая лишь число шагов без данных о местоположении или активности. Используйте его для живого обновления индикатора цели по шагам.
local id = Wex.health.steps(function(s)
ui:text("Steps: " .. s.count)
end)
-- stop later:Wex.health.stop(id)
Streams a high-frequency motion sensor selected by type ("accelerometer", "gyroscope", "magnetometer", ...) at an optional sampling rate hz, returning an id for later stopping; cb receives a table of the sensor's axes such as x, y and z. It requires the "health" capability and is read-only. Use it for gesture or shake detection, keeping hz modest to save battery.
Передаёт данные высокочастотного датчика движения, выбранного по type («accelerometer», «gyroscope», «magnetometer», …), с необязательной частотой дискретизации hz и возвращает id для последующей остановки; cb получает таблицу с осями датчика, например x, y и z. Требует возможность «health» и работает только на чтение. Подходит для распознавания жестов или встряхивания; держите hz умеренным, чтобы экономить батарею.
local id = Wex.health.highRate("accelerometer", function(a)
local m = math.sqrt(a.x*a.x + a.y*a.y + a.z*a.z)
if m > 25thenWex.log("shake!") endend, 50)
Wex.health.stop(id)
Wex.health.stop(id) ends a single health stream started by steps or highRate using the id it returned, while Wex.health.stopAll() tears down every active health sensor at once. Always call one of them when your view closes so sandboxed sensor listeners and their battery cost are released; stopAll is the simplest way to clean up heartRate and any streams whose ids you did not keep. Both are safe to call with a stale or already-stopped id.
Wex.health.stop(id) завершает один поток здоровья, запущенный через steps или highRate, по возвращённому id, а Wex.health.stopAll() разом останавливает все активные датчики здоровья. Всегда вызывайте один из них при закрытии экрана, чтобы освободить слушатели датчиков в песочнице и снизить расход батареи; stopAll — самый простой способ убрать heartRate и потоки, чьи id вы не сохранили. Оба безопасно вызывать с устаревшим или уже остановленным id.
local id = Wex.health.steps(function(s) ui:text(s.count) end)
-- stop just this one:Wex.health.stop(id)
-- or stop everything on teardown:Wex.health.stopAll()
Wex · Hardware v2Wex · Железо v2 (серверы и сессии)
Runs a BLE GATT peripheral (server): other devices connect to YOUR app and read/write/subscribe to its characteristics. spec.services lists services, each with characteristics that declare properties {read,write,notify,indicate} and optional onRead/onWrite/onSubscribe callbacks. The handle exposes :notify(svc,chr,value), :connected(), :isRunning() and :close(). Needs the bluetooth permission; onRead is consulted once per read (cached for long reads).
Поднимает BLE GATT-периферию (сервер): другие устройства подключаются к ВАШЕМУ приложению и читают/пишут/подписываются на его характеристики. spec.services перечисляет сервисы, у каждого характеристики со свойствами {read,write,notify,indicate} и колбэками onRead/onWrite/onSubscribe. Хендл даёт :notify(svc,chr,value), :connected(), :isRunning() и :close(). Нужно разрешение bluetooth; onRead вызывается один раз на чтение (кэш для длинных чтений).
local hr = 72local srv = Wex.ble.server({
services = { {
uuid = "180d", -- Heart Rate service
characteristics = { {
uuid = "2a37",
properties = { read = true, notify = true },
onRead = function(addr) return string.char(0, hr) end,
onSubscribe = function(on, addr) Wex.log(addr .. (on and" subscribed"or" left")) end,
} },
} },
}, function(ok) Wex.log("gatt server up: " .. tostring(ok)) end)
-- push a new value to every subscribed centralWex.every(1000, function() hr = 60 + math.random(0, 40); srv:notify("180d", "2a37", string.char(0, hr)) end)
Tears the GATT server down: closes it, drops every registered service and clears subscriptions. Called automatically when the plugin closes; call it yourself to stop serving early. handle:close() does the same via the handle returned by Wex.ble.server.
Останавливает GATT-сервер: закрывает его, снимает все сервисы и очищает подписки. Вызывается автоматически при закрытии плагина; вызовите сами, чтобы прекратить обслуживание раньше. handle:close() делает то же через хендл из Wex.ble.server.
local srv = Wex.ble.server(spec, cb)
-- ... later ...
srv:close() -- or Wex.ble.stopServer()
Bluetooth Classic RFCOMM server (the SPP server side): waits for a peer to connect on spec.uuid (default SPP) and hands each accepted connection to spec.onConnect(link) — the same link object Wex.bt.connect returns, with :write/:writeLine/:writeHex and onData/onLine/onDisconnect. spec.name names the SDP record, spec.secure toggles the secure/insecure socket. serverHandle:isListening()/:close(). Needs the bluetooth permission.
RFCOMM-сервер Bluetooth Classic (серверная сторона SPP): ждёт подключения пира на spec.uuid (по умолчанию SPP) и отдаёт каждое принятое соединение в spec.onConnect(link) — тот же объект link, что возвращает Wex.bt.connect, с :write/:writeLine/:writeHex и onData/onLine/onDisconnect. spec.name задаёт SDP-запись, spec.secure переключает защищённый/незащищённый сокет. serverHandle:isListening()/:close(). Нужно разрешение bluetooth.
local server = Wex.bt.listen({
name = "WEX-SPP",
onConnect = function(link)
Wex.log("client connected: " .. link.address)
link.onLine = function(line) link:writeLine("echo: " .. line) endend,
})
-- ... later ...
server:close()
Opens a HELD NFC technology session (IsoDep|NfcA|NfcB|NfcF|NfcV) on a tag, so a multi-command exchange (e.g. an APDU conversation with a smartcard) reuses one connection instead of reconnecting per command. session:transceive(hex, cb) runs each command on a dedicated thread and returns { ok, hex, len, bytes }; session:isConnected() and session:close() manage its lifetime. A dropped tag ends the session and reports 'tag lost'.
Открывает УДЕРЖИВАЕМУЮ NFC-сессию технологии (IsoDep|NfcA|NfcB|NfcF|NfcV) на метке, чтобы серия команд (например APDU-диалог со смарт-картой) переиспользовала одно соединение вместо переподключения на каждую команду. session:transceive(hex, cb) выполняет команду на выделенном потоке и возвращает { ok, hex, len, bytes }; session:isConnected() и session:close() управляют временем жизни. Снятая метка завершает сессию с сообщением 'tag lost'.
Wi-Fi Direct (P2P) peer discovery: starts scanning and calls onFound(peers) as nearby devices appear; each peer is { address, name, status }. Wex.wifi.p2p.peers() returns the last known list. handle:stop() ends discovery. Gated by the wifi permission; at runtime needs NEARBY_WIFI_DEVICES (Android 13+) or location.
Обнаружение пиров Wi-Fi Direct (P2P): запускает сканирование и вызывает onFound(peers) по мере появления устройств рядом; каждый пир — { address, name, status }. Wex.wifi.p2p.peers() возвращает последний список. handle:stop() завершает поиск. За разрешением wifi; в рантайме нужно NEARBY_WIFI_DEVICES (Android 13+) или геолокация.
local scan = Wex.wifi.p2p.discover({
onFound = function(peers)
for _, p in ipairs(peers) doWex.log(p.name .. " @ " .. p.address) endend,
})
-- ... later ...
scan:stop()
Forms a Wi-Fi Direct connection. connect(address, cb) invites the peer at that address; createGroup(cb) makes this device the group owner (a soft-AP others join); removeGroup(cb) tears the group down. Each cb receives (ok) or (false, reason). All gated by the wifi permission.
Устанавливает соединение Wi-Fi Direct. connect(address, cb) приглашает пира по адресу; createGroup(cb) делает это устройство владельцем группы (soft-AP, к которому подключаются другие); removeGroup(cb) распускает группу. Каждый cb получает (ok) или (false, reason). Всё за разрешением wifi.
Wex.wifi.p2p.connect("aa:bb:cc:dd:ee:ff", function(ok) Wex.log("connect: " .. tostring(ok)) end)
-- or become the group owner:Wex.wifi.p2p.createGroup(function(ok) Wex.log("group: " .. tostring(ok)) end)
Reports the current Wi-Fi Direct connection: cb({ groupFormed, isOwner, ownerAddress }). info(cb) is a one-shot query; watchInfo(cb) subscribes and fires again on every connection change (returns a handle with :stop()). Use it to learn the group owner's address after a connect so you can open a socket to it.
Сообщает текущее соединение Wi-Fi Direct: cb({ groupFormed, isOwner, ownerAddress }). info(cb) — разовый запрос; watchInfo(cb) подписывается и срабатывает при каждом изменении соединения (возвращает хендл с :stop()). Используйте, чтобы узнать адрес владельца группы после connect и открыть к нему сокет.
Put a d8-compiled classes.dex into the package as native.dex, declare it in the manifest and ask for the "native" permission. Lua stays the entry point and keeps the whole ui toolkit; the DEX side is for what Lua can't do — heavy computation, direct Android APIs, custom Views.
Положи собранный d8 classes.dex в пакет как native.dex, объяви его в манифесте и запроси разрешение "native". Lua остаётся точкой входа и сохраняет весь тулкит ui; DEX нужен для того, чего Lua не может — тяжёлых вычислений, прямых Android API, своих View.
DEX classes load with the app's own ClassLoader and run with WEX's permissions — they can do anything the app can. Lua's sandbox does not apply. The installer warns the user, and the plugin cannot load a .dex without the "native" permission. Review the source before installing someone else's native plugin.
Классы DEX загружаются штатным загрузчиком приложения и работают с правами WEX — им доступно всё, что доступно приложению. Песочница Lua на них не распространяется. Установщик предупреждает пользователя, а без разрешения "native" .dex не загрузится. Проверяйте исходники чужого нативного плагина перед установкой.
load() constructs an instance (a leading Context parameter is passed automatically), static() exposes static members, call() is a one-shot static call. On a handle every Java method is callable by name with colon syntax; arguments and return values convert automatically: numbers, strings, booleans, arrays/List/Map ↔ tables, byte[] ↔ base64 string, anything else stays a handle.
load() создаёт экземпляр (первый параметр Context передаётся сам), static() открывает статические члены, call() — разовый статический вызов. У хендла любой Java-метод вызывается по имени через двоеточие; аргументы и результаты конвертируются сами: числа, строки, булевы, массивы/List/Map ↔ таблицы, byte[] ↔ base64-строка, остальное остаётся хендлом.
local tools = Wex.native.load("com.example.Tools")
ui:kv("Sum", tools:sum(2, 40))
local Sys = Wex.native.static("java.lang.System")
ui:kv("Nano", Sys:nanoTime())
#Wex.priv.available(fn({ su, shizuku })) Wex.su.request/exec/sysfs/revokeWex.shizuku.request/exec
Elevated system access — run commands as root (Wex.su) or at shell/ADB uid via Shizuku (Wex.shizuku), for deep diagnostics and hardware / network / package management. Wex.priv.available(fn(info)) probes what is usable and calls fn with { su, shizuku, shizukuInstalled } — asynchronously, since probing su can spawn a subprocess (never on the UI thread). Wex.su.request(fn(ok, msg)) raises the root prompt; Wex.su.exec(cmd, fn(out, err)) runs a shell command as root and streams the result back on the main thread; Wex.su.sysfs.read(path, fn(out, err)) / write(path, data, fn) are shell-quoted convenience wrappers for direct sysfs I/O; revoke() drops the grant. Wex.shizuku.available(fn(ok)) / request(fn(granted)) / exec(cmd, fn(out, err)) do the same at shell uid without root (via the installed Shizuku service). Needs Plugin.permissions = {"privileged"} — the most powerful permission there is: you approve the root/Shizuku prompt yourself, and EVERY command the plugin runs is written to its log as an audit trail. Foreground plugins.
Повышенный доступ к системе — выполнение команд от root (Wex.su) или на уровне shell/ADB через Shizuku (Wex.shizuku), для глубокой диагностики и управления железом / сетью / пакетами. Wex.priv.available(fn(info)) проверяет, что доступно, и зовёт fn с { su, shizuku, shizukuInstalled } — асинхронно, так как проверка su может форкнуть подпроцесс (никогда на UI-потоке). Wex.su.request(fn(ok, msg)) поднимает root-запрос; Wex.su.exec(cmd, fn(out, err)) выполняет shell-команду от root и возвращает результат в главный поток; Wex.su.sysfs.read(path, fn(out, err)) / write(path, data, fn) — обёртки с shell-quoting для прямого sysfs-I/O; revoke() снимает доступ. Wex.shizuku.available(fn(ok)) / request(fn(granted)) / exec(cmd, fn(out, err)) — то же на уровне shell без root (через установленный сервис Shizuku). Нужно Plugin.permissions = {"privileged"} — самое мощное разрешение: root/Shizuku-запрос подтверждаете вы сами, и КАЖДАЯ выполненная плагином команда пишется в его лог как аудит. Плагины переднего плана.
Plugin.permissions = { "privileged" }
Wex.priv.available(function(p) -- async: p = { su = false, shizuku = true, shizukuInstalled = true }ifnot p.shizuku thenreturnendWex.shizuku.request(function(granted)
ifnot granted thenreturnendWex.shizuku.exec("ip -o link show", function(out, err)
if out thenWex.log(out) endend)
end)
end)
-- root variant + direct sysfs readWex.su.request(function(ok)
ifnot ok thenreturnendWex.su.sysfs.read("/sys/class/power_supply/battery/temp", function(t, err)
if t thenWex.log("batt temp " .. t) endend)
end)
#Callbacks: a Lua function where Java wants an interface
nativeнативный код
Pass a Lua function for any single-method interface (Runnable, listeners, your own callback types) — WEX builds the proxy. A callback fired from a background thread is hopped onto the UI thread before Lua runs, so its return value is not sent back to Java; return values work when Java calls you on the UI thread.
Передавай Lua-функцию туда, где Java ждёт интерфейс с одним методом (Runnable, слушатели, свои колбэки) — WEX сам создаст прокси. Колбэк из фонового потока переносится в UI-поток перед вызовом Lua, поэтому его результат не возвращается в Java; возврат работает, когда Java вызывает вас в UI-потоке.
Discovery from Lua: list what a class actually exposes (great while developing), read/write public fields, check types. info() reports the dex file, its size, whether it is loaded and the declared classes.
Интроспекция из Lua: список того, что реально есть у класса (удобно при разработке), чтение и запись публичных полей, проверка типов. info() сообщает файл dex, размер, загружен ли он и объявленные классы.
for _, m inipairs(tools:methods()) doprint(m) end
#ui:native(class, { height, card, radius, bg, … }) → h
nativeнативный код
Embeds a View built by your own code: a View subclass with a (Context) or (Context, Map) constructor, or any class with static View create(Context[, Map]). The handle mixes the usual style setters with every method of the View, plus h:call(name, …) and h:invalidate().
Встраивает View, созданный твоим кодом: наследник View с конструктором (Context) или (Context, Map), либо любой класс со static View create(Context[, Map]). Хендл объединяет обычные стилевые сеттеры со всеми методами View, плюс h:call(name, …) и h:invalidate().
local v = ui:native("com.example.WaveView", { height = 200 })
v:setSpeed(2.5)
Compile against android.jar with --release 8, then run d8 from the SDK build-tools. Everything the plugin needs must be inside that one dex (no external libraries are resolved for you).
Компилируй против android.jar с --release 8, затем прогони d8 из build-tools SDK. Всё нужное плагину должно лежать в этом одном dex (внешние библиотеки не подгружаются).
The complete set of capability ids you may put in Plugin.permissions / manifest.permissions, and what each unlocks. network — HTTP, sockets, downloads, ui:web(url). files — the system open/save dialogs and durable folders (Wex.folder). clipboard — read the clipboard (writing is always allowed). native — load the package's own compiled classes (Wex.native). background — Plugin.service()/background(), Wex.alarm, Wex.media. intent — launch other apps / query them (Wex.intent). notifications — post notifications. location — GPS/network position. microphone — Wex.audio.record and Wex.audio.listen. camera — Wex.camera.scan and Wex.camera.capture. sensors — accelerometer/gyroscope/compass. bluetooth — Bluetooth & BLE. nfc — read/write NFC tags. usb — USB-serial. infrared — the IR blaster. wifi — Wi-Fi scan. overlay — floating windows over other apps. contacts — read the address book. calendar — read the agenda. (Wex.auth.biometric needs no permission.)
Полный набор id возможностей, которые можно указать в Plugin.permissions / manifest.permissions, и что каждая открывает. network — HTTP, сокеты, загрузки, ui:web(url). files — системные диалоги открытия/сохранения и постоянные папки (Wex.folder). clipboard — чтение буфера (запись разрешена всегда). native — загрузка скомпилированных классов пакета (Wex.native). background — Plugin.service()/background(), Wex.alarm, Wex.media. intent — запуск/опрос других приложений (Wex.intent). notifications — показ уведомлений. location — GPS/сетевая позиция. microphone — Wex.audio.record и Wex.audio.listen. camera — Wex.camera.scan и Wex.camera.capture. sensors — акселерометр/гироскоп/компас. bluetooth — Bluetooth и BLE. nfc — чтение/запись NFC-меток. usb — USB-serial. infrared — ИК-передатчик. wifi — сканирование Wi-Fi. overlay — окна поверх других приложений. contacts — чтение адресной книги. calendar — чтение календаря. (Wex.auth.biometric разрешения не требует.)
WEX scans a plugin's Lua source and manifest and reports what the code can do — network, your files, clipboard, background work, sensors, other apps, native code — with the exact lines as evidence. It runs on every install path (forum, a file opened from a chat or file manager, SAF import), before the first run of a high-risk plugin, and on demand from the plugin list and the forum page. It also flags a manifest that declares less than the code uses, obfuscated blobs, runtime-built code, and data smuggled into an opened URL. It is a capability report, not a verdict: nothing is blocked, and native code cannot be scanned at all.
WEX разбирает исходник и манифест плагина и показывает, что код умеет — сеть, ваши файлы, буфер обмена, фоновую работу, датчики, другие приложения, нативный код — с точными строками как доказательством. Проверка идёт на каждом пути установки (форум, файл из чата или файлового менеджера, импорт через SAF), перед первым запуском рискованного плагина и по требованию из списка плагинов и со страницы на форуме. Она также помечает манифест, объявляющий меньше, чем использует код, закодированные блоки, код, собираемый на ходу, и данные, спрятанные в открываемую ссылку. Это отчёт о возможностях, а не приговор: ничего не блокируется, а нативный код проверить нельзя вовсе.