# WexClient plugin API — full reference Lua plugin platform of the WexClient Android app. API level 37, 40 sections, 423 entries. Every entry is given in English and Russian. Plugins are single Lua scripts or `.wexp` packages (`manifest.json` + `main.lua` + `lib/` + `assets/`). They run in a sandbox with declared permissions, draw native Android UI through `ui:`, and reach the platform through `Wex.`. HTML version: https://wexclient.org/plugins ## If you are generating a plugin, read this first This file is the complete API. Follow these rules and the result runs; break one and it does not. 1. Required: `Plugin.name` and `function Plugin.build(ui)`. Nothing else is mandatory. 2. `ui` is the argument of `Plugin.build`, never a global. Container callbacks get their own builder. 3. Call with a colon: `ui:text("x")`, `handle:width("match")`. Builders return a handle; setters chain. 4. To change something on screen, keep its handle and call `:set(...)`. There is no way to re-run `build`. 5. Nothing blocks. No `sleep`, no synchronous requests. Slow work takes a callback. 6. The sandbox has no `io`, `os.execute`, `dofile`, `load`, `loadstring`, `package` or `debug`. Files go through `Wex.files`; `require` only reaches `lib/` inside the package. 7. Declare a permission in `Plugin.permissions` before using the call it guards, and declare nothing else. 8. Lua 5.2: 1-based arrays, no bitwise operators, no integer division, no `goto`. A reserved word as a table key needs brackets: `["repeat"] = true`. Concatenation needs strings: `tostring(n)`. 9. Do not invent names. If a call is not in this file, it does not exist. 10. Release timers, scans, ports and connections in `Plugin.onClose`. 11. Some callbacks run inside the constructor: `ui:segmented` always calls `fn(initial, label)` (initial defaults to 1), `ui:radio` does when `initial` is given, `ui:bottomNav` runs the initial pane's `build` and `onChange`. `:set()` calls the callback on segmented, radio, switch, checkbox, slider and every text input; chips, dropdown, stepper and rating stay silent. Define every function and handle a callback uses before creating the widget, or guard the callback with a `ready` flag. 12. A handle has only the methods its widget documents (see «A handle has only the methods its widget documents»). All handles share the style chain and `:text(s)`; `:set` exists on text, kv, inputs, switch, selections, slider, stepper, progress, image, stat, gauge, qr — not on a button. Calling an unknown method fails with `attempt to call nil`. 13. An error in the script or the top-level `Plugin.build` appends a red ⚠ line under what was built; an error in anything called later (callbacks, timers, pane builders, hooks) shows a red toast, logs ✖, and aborts only that callback. Logs: Plugins → long-press → Logs, `Wex.log.lines()`. Budgets: 4 s for build, 3 s per hook. A complete, runnable plugin: ```lua Plugin.name = "IP Info" Plugin.description = "Show the public IP" Plugin.version = "1.0" Plugin.permissions = { "network" } local out function Plugin.build(ui) ui:title("IP Info") out = ui:text("Not checked yet"):color("muted") ui:button("Check", function() Wex.http("https://api.ipify.org", function(res) if not res.ok then ui:toast(res.error or "request failed", "error") return end out:set(res.body):color("success") end) end):width("match") end function Plugin.onClose() Wex.cancelAllTimers() end ``` --- ## Writing a plugin: the contract — Как писать плагин: контракт ### 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/, больше ничего. ```lua Plugin.name = "My Plugin" function Plugin.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. ```lua local UI function Plugin.build(ui) UI = ui ui:card(function(c) c:text("c is a builder too") end) end function openSecond() UI:push("Details", function(c) c:text("second screen") end) end ``` ### Call with a colon, and chain the setters 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(...) — вызовы методов: двоеточие обязательно. Каждый билдер возвращает хендл, а каждый стилевой сеттер возвращает тот же хендл, поэтому оформление пишется цепочкой. Сохраняйте хендл в переменную, если значение потом должно меняться. ```lua 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) для смены длины. Пересборка — причина мигания и потери позиции прокрутки. ```lua 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 constructor` — segmented (always) · radio (when initial is given) · bottomNav onChange(initial) · tabs builder / segmented (всегда) · radio (если передан initial) · bottomNav onChange(initial) · билдер tabs - `runs on :set() / :toggle()` — segmented · radio · switch · checkbox · slider · input · textarea · number · password · search / segmented · radio · switch · checkbox · slider · input · textarea · number · password · search - `never runs from code` — chips · multiChips · dropdown · stepper · rating / chips · multiChips · dropdown · stepper · rating ```lua -- wrong: refresh is assigned below, so it is nil while the widget is created ui:segmented({ "All", "Favorites" }, function(i) favOnly = (i == 2) refresh() -- history.lua:29 attempt to call nil end, 1) local function refresh() list:count(#rows) end -- right: state and functions first, widgets second, a flag until the screen is complete local ready, favOnly, list = false, false, nil local function refresh() if not ready then return end list:count(#rows) end ui: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 указывает на колонку значения. Таблица ниже — полная карта; виджет, которого в ней нет, имеет только общую цепочку. - `text · subtitle · caption · badge · kv · html · markdown` — :set(s) :get() :append(s) :html(s) :markdown(md) :tone(t) :copyOnTap() :link(url) / :set(s) :get() :append(s) :html(s) :markdown(md) :tone(t) :copyOnTap() :link(url) - `input · textarea · number · password · search` — :get() :set(s) :value() :number() :append(s) :clear() :isEmpty() :onChange(fn) :onSubmit(fn) :error(msg) :helper(s) :hint(s) :prefix(s) :suffix(s) :focus() :blur() :selectAll() :readonly() :maxLength(n) :type(t) :password() :lines(n) :icon(name) :clearButton() :endIcon(name, fn) :mono() :bind(key) / :get() :set(s) :value() :number() :append(s) :clear() :isEmpty() :onChange(fn) :onSubmit(fn) :error(msg) :helper(s) :hint(s) :prefix(s) :suffix(s) :focus() :blur() :selectAll() :readonly() :maxLength(n) :type(t) :password() :lines(n) :icon(name) :clearButton() :endIcon(name, fn) :mono() :bind(key) - `switch · checkbox` — :get() :set(bool) :toggle() :onChange(fn) :label(s) :bind(key) / :get() :set(bool) :toggle() :onChange(fn) :label(s) :bind(key) - `chips · multiChips` — :get() → index or {indexes} :label() :set(i | {i, …}) :clear() :items({…}) :count() / :get() → индекс или {индексы} :label() :set(i | {i, …}) :clear() :items({…}) :count() - `dropdown` — :get() :label() :set(i | "label") :items({…}) :hint(s) :error(msg) :onChange(fn) / :get() :label() :set(i | "label") :items({…}) :hint(s) :error(msg) :onChange(fn) - `radio · segmented` — :get() :label() :set(i) · radio :clear() · segmented :count() / :get() :label() :set(i) · radio :clear() · segmented :count() - `slider · rangeSlider` — slider :get() :set(v) :label(s) :format(fn) :onChange(fn) · rangeSlider :get() → { a, b } :set(a, b) :label(s) / slider :get() :set(v) :label(s) :format(fn) :onChange(fn) · rangeSlider :get() → { a, b } :set(a, b) :label(s) - `stepper · rating` — stepper :get() :set(v) :label(s) · rating :get() :set(v) :half() :readonly(bool) / stepper :get() :set(v) :label(s) · rating :get() :set(v) :half() :readonly(bool) - `button · iconButton` — :text(s) :icon(name) :outlined() :tonal() :danger() :success() :loading(bool, text?) :onClick(fn(h)) :click() — no :set / :text(s) :icon(name) :outlined() :tonal() :danger() :success() :loading(bool, text?) :onClick(fn(h)) :click() — без :set - `card · row · column · hscroll` — :clear() :rebuild(fn) :add(fn) :count() :removeAt(i) :gravity(g) :ui() / :clear() :rebuild(fn) :add(fn) :count() :removeAt(i) :gravity(g) :ui() - `list` — :count(n) :refresh() :update(i) :render(fn) :scrollTo(i) :scrollToEnd() / :count(n) :refresh() :update(i) :render(fn) :scrollTo(i) :scrollToEnd() - `expander · tabs · bottomNav` — expander :open() :close() :toggle() :isOpen() :title(s) · tabs :select(i) :get() :refresh() · bottomNav :select(i) :current() :pane(i) / expander :open() :close() :toggle() :isOpen() :title(s) · tabs :select(i) :get() :refresh() · bottomNav :select(i) :current() :pane(i) - `progress · image` — progress :set(pct) :get() :max(n) :indeterminate() :color(c) :show() :hide() · image :set(url) :fit(mode) :round(r) :circle() :tint(c) :clear() / progress :set(pct) :get() :max(n) :indeterminate() :color(c) :show() :hide() · image :set(url) :fit(mode) :round(r) :circle() :tint(c) :clear() - `stat · gauge · qr · item · tile` — stat :set(v) :get() :label(s) :delta(text, tone) :valueColor(c) :bind(key) · gauge :set(value, label?) :label(s) :caption(s) :color(c) · qr :set(text) · item :title(s) :subtitle(s) :trailing(s) :badge(text, tone) · tile :label(s) / stat :set(v) :get() :label(s) :delta(text, tone) :valueColor(c) :bind(key) · gauge :set(value, label?) :label(s) :caption(s) :color(c) · qr :set(text) · item :title(s) :subtitle(s) :trailing(s) :badge(text, tone) · tile :label(s) ```lua local count = ui:kv("Selected", "0") -- kv handle = the value column local 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 method local 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). ```lua 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) ``` ### Nothing blocks: slow work is a callback 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 четыре секунды, у каждого хука три; цикл ожидания убивает сторож. Сеть, файлы, устройства и таймеры принимают функцию и вызывают её на главном потоке. ```lua Wex.http("https://api.ipify.org", function(res) if not res.ok then ui:toast(res.error, "error") return end out:set(res.body) end) Wex.interval(1000, function() clock:set(Wex.time.format(Wex.now(), "HH:mm:ss")) end) ``` ### The sandbox removed the usual Lua escape hatches 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/ внутри пакета. ```lua -- io.open("/sdcard/log.txt", "w") -- does not exist Wex.files.write("log.txt", text) -- the plugin's own folder local 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(имя) во время работы показывает, что выдано: пользователь может отозвать разрешение после установки. ```lua Plugin.permissions = { "network", "bluetooth" } if not Wex.can("bluetooth") then ui:banner("Bluetooth was revoked in the plugin settings", "warning") return end ``` ### 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. Массивы нумеруются с единицы, длина — #. ```lua Wex.ble.scan({ ["repeat"] = true, onDevice = function(d) end }) ui:text("rssi: " .. tostring(d.rssi)) ui:text(string.format("%.1f ms", rtt)) ``` ### Do not invent API names 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 существуют именно для того, чтобы проверять, а не предполагать. ```lua if Wex.has("usb") and Wex.api.level >= 7 then openPort() else ui: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, а не угадывайте. ```lua -- 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 moment Wex.log("history built, rows=" .. tostring(#rows)) -- Plugins → long-press → Logs local ok, cfg = pcall(function() return Wex.assets.json("presets.json") end) if not ok then Wex.log.warn("presets.json missing: " .. tostring(cfg)) cfg = {} end ``` ### Checklist before you call a plugin finished 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, которого нет в этом справочнике. Каждый колбэк безопасно вызывать до того, как построен остальной экран. ```lua function Plugin.onClose() Wex.cancelAllTimers() Wex.ble.stopScan() if port then port:close() end end ``` --- ## Architecture — под капотом ### Runtime — LuaJ 5.2, sandboxed 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 — и только через них. ```lua -- 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 ``` ### Isolation — one Lua state per plugin 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. ```lua local util = Wex.import("acme.util") -- runs in a child env: sees your globals, -- but its own writes never leak into yours Wex.bus.emit("track", { id = 7 }) -- the only cross-plugin channel (data only) ``` ### The ui: bridge — real Android Views 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 и возвращается колбэком в главный поток. ```lua local card = ui:card() -- a MaterialCardView local btn = ui:button("Go") -- a MaterialButton, returned as a handle btn:bg("#2563EB"):radius(12):onClick(function() ui:toast("hi") end) ``` ### Permission gates — enforced in Kotlin 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 срабатывает сразу, если уже выдано, иначе спрашивает пользователя через хост и сообщает об отказе в канал ошибок плагина. Значит, чувствительному вызову нужно И объявление в манифесте, И согласие ОС/пользователя. ```lua -- 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) ``` ### Lifecycle & cleanup — what onClose tears down 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. ```lua function Plugin.onClose() -- last chance to persist; runs BEFORE the bridge tears timers/sockets down Wex.storage.set("draft", editor:text()) end -- an http() still in flight here will simply never call back (disposed = no-op) ``` ## Recommendations and pitfalls — Рекомендации и типичные ошибки ### Declare exactly the permissions you use 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 сверяет объявленное в манифесте с тем, что вызывает код, и сообщает про оба перекоса: объявленное, но неиспользуемое разрешение выглядит небрежно, а вызов без разрешения будет отклонён во время работы с ошибкой в логе. Добавляйте разрешение вместе с вызовом, а не заранее. ```lua Plugin.permissions = { "network" } -- and nothing else until you need it if not Wex.can("network") then ui:banner("Offline", "warning") return end ``` ### Never block the main thread 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. ```lua -- wrong: freezes the screen and gets killed by the watchdog -- while not done do end -- right Wex.interval(1000, function() status:set(Wex.time.now()) end) ``` ### Rotation restarts the plugin: save in onPause 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 не нужно. ```lua function Plugin.onPause() Wex.storage.set("draft", input:value()) end function Plugin.build(ui) local draft = Wex.storage.get("draft") or "" input = ui:field({ value = draft }) end ``` ### Release what you started in onClose 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, и когда экран просто на паузе. ```lua function Plugin.onClose() Wex.cancelAllTimers() Wex.ble.stopScan() Wex.sensors.stopAll() if port then port:close() end end ``` ### Check the hardware before you use it 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() ничего не запрашивая, поэтому ветвитесь по нему и показывайте понятную заглушку вместо тихого отказа. ```lua if not Wex.ir.available() then ui:empty("info", "No IR blaster", "This phone cannot send infrared") return end ``` ### Guard new APIs with Wex.api.level or Wex.has 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. ```lua if Wex.api.level >= 7 and Wex.has("usb") then openSerial() else ui:caption("Update WexClient for USB support") end ``` ### Handle the error branch of every callback 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 первым: одна строка превращает падение в сообщение. ```lua Wex.http("https://example.com/api", function(res) if not res.ok then ui:toast(res.error or "request failed", "error") return end render(res.json) end) ``` ### 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. Если зарезервированное имя в таблице всё же нужно, обращайтесь через скобки. ```lua 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 до первого кадра. То же с графиками: добавляйте точки в существующий хендл, а не пересобирайте экран. ```lua ui:list({ count = #items, render = function(row, i) row:item({ title = items[i].name, subtitle = items[i].ip }) end }) ``` ### Use the spacing tokens instead of raw dp 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. Экраны, собранные на токенах, имеют общий ритм, переживают смену дизайна и дают ассистенту готовый словарь вместо выдуманных чисел. ```lua ui:card(function(c) c:kv("Host", host) end, "sm"):margin("md") ``` ### Split the code into lib/ modules 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 на две тысячи строк тяжело читать, тяжело править ассистентом и невозможно переиспользовать. ```lua -- lib/net.lua local M = {} function M.ping(host, cb) Wex.net.ping(host, cb) end return M -- main.lua local 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, который разметку не интерпретирует. ```lua ui:text(res.body) -- safe ui:html(Wex.str.escapeHtml(res.body)) -- safe -- ui:html(res.body) -- not safe ``` ### Develop with hot reload and the type definitions 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. ```lua # 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/ работает офлайн, не требует разрешения на сеть и не может подмениться. Скачивайте во время работы только то, что действительно должно быть свежим. ```lua ui:image("asset:logo.png"):width(120) local cfg = Wex.assets.readJson("defaults.json") ``` ### Version the plugin and keep the id stable 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. Идентификатор решает, что именно заменит обновление; версия решает, предложит ли приложение обновление вообще. Смена идентификатора публикует второй, никак не связанный плагин и оставляет данные и выданные разрешения пользователя сиротами. ```lua Plugin.id = "ip_info" -- never changes Plugin.version = "1.3" -- goes up with every release ``` --- ## Design guide — Гайд по дизайну ### Use spacing tokens, not magic numbers 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. Тогда у всех экранов один ритм, а у вставленного в ИИ скрипта есть готовый словарь вместо новых чисел каждый раз. ```lua ui:card(f):padding("lg"):radius("md") ui:spacer("sm") ``` ### Space with gap, not manual margins 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() на каждом ребёнке — одно число, применяется равномерно, ничего не забыто у первого/последнего элемента. ```lua ui:row(function(c) c:button("A", f):grow() c:button("B", f):grow() end, "md") ``` ### Group related controls in a card 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 вместо коробок в коробках. ### Reach for semantic text before raw styling 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() обращайся только для настоящего разового акцента, не для обычных заголовков. ### Status → badge, not colored text ui:badge(text, tone) is legible in both themes by construction — a hand-picked hex text color usually isn't once the user flips theme. ui:badge(text, tone) по построению читаем в обеих темах — ручной hex-цвет текста обычно перестаёт быть читаемым при смене темы. ### Recipe — a status card 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 + бейдж состояния + одна кнопка действия. Цельный, согласованный экран в десяток строк — целиком из примитивов выше. ```lua function Plugin.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 then ui:toast("disconnected") end end) end):width("match") end ``` ### Recipe — an empty list, handled gracefully 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 вместо пустой карточки — список из нуля строк — это состояние для дизайна, а не баг, который можно игнорировать. ```lua if #items == 0 then ui:empty("info", "No scans yet", "Run a scan to see results here") else ui:list({ count = #items, render = function(row, i) row:kv(items[i].name, items[i].ip) end }) end ``` ### Query the design system with ui:theme() 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, красишь график или ветвишься по теме — ничего не захардкожено, и всё само подстраивается под светлую/тёмную. ### Ship fonts and backgrounds in assets/ 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 }) на карточках, рядах и большинстве вью. Один пакет несёт свой вид, работает оффлайн и выглядит одинаково на всех экранах. ### Describe the screen, do not patch it 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() оставь для одиночного виджета, который обновляешь часто (счётчик, прогресс), где полная пересборка была бы лишней. --- ## Getting started — С чего начать ### Plugin.name / icon / description The manifest — plain fields you assign at the top of the script. Манифест — простые поля, которые ты задаёшь вверху скрипта. Parameters: - `name` — title in the plugin list and the menu. / заголовок в списке плагинов и в меню. - `icon` — an emoji, shown as the tile/menu icon. / эмодзи, показывается как иконка плитки/меню. - `description` — one line under the name on the card. / одна строка под названием на карточке. ```lua Plugin.name = "IP Info" Plugin.icon = "🌐" Plugin.description = "Look up an IP" ``` ### Plugin.version / author Optional metadata shown under the name and used for update detection. Необязательные данные под именем; используются для детекта обновлений. Parameters: - `version` — e.g. "1.2" — compared numerically when re-installing. / напр. "1.2" — сравнивается численно при переустановке. - `author` — your name; shown as the credited author. / твоё имя; показывается как автор. ```lua 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). ```lua if Wex.api.level >= 9 then local lib = Wex.import("crypto_utils") end if Wex.has("wavyProgress") then ui:wavyProgress({ value = 30 }) end ``` ### Plugin.permissions = { … } Array of sensitive capabilities the plugin needs. Anything not listed is blocked. Массив чувствительных возможностей, которые нужны плагину. Что не указано — заблокировано. Parameters: - `"network"` — Wex.http / websocket / download / upload / ui:image. / Wex.http / websocket / download / upload / ui:image. - `"files"` — Wex.pickFile / saveFile (the user's real files). / Wex.pickFile / saveFile (настоящие файлы пользователя). - `"notifications"` — Wex.notifications. / Wex.notifications. - `"background"` — Wex.background periodic task. / периодическая задача Wex.background. ```lua Plugin.permissions = { "network", "notifications" } ``` ### Plugin.handles = { "text/plain", "image/*" } 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. Разрешение не нужно — пользователь выбрал вас сам. Parameters: - `"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" — один точный тип. ```lua Plugin.handles = { "text/plain", "image/*" } function Plugin.build(ui) local s = Wex.args.share if s and s.text then ui:text(s.text) end for _, f in ipairs(s and s.files or {}) do ui:image(f.name) -- already a file in Wex.files end end ``` ### function Plugin.build(ui) … end Entry point that builds the screen. Runs on the main thread each time the plugin opens. Точка входа, строит экран. Выполняется в главном потоке при каждом открытии плагина. Parameters: - `ui` — the builder — call ui:title, ui:button, … on it. / билдер — вызывай на нём ui:title, ui:button, … Returns: nothing — you build the UI by side effect. / ничего — интерфейс строится побочным эффектом. ```lua function Plugin.build(ui) ui:title("Hello") ui:text("Ready.") end ``` ### function Plugin.background() … end Optional headless task run on a schedule (see Wex.background). No `ui` — only the `Wex` global. Необязательная фоновая задача по расписанию (см. Wex.background). Без `ui` — только глобальный `Wex`. ```lua function Plugin.background() Wex.http("https://…", function(res) end) end ``` ### Plugin.provides = { … } Plugin.exports() Plugin.requires = { … } 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 — у неё нет своего экрана. Импортированный код выполняется в песочнице вызывающего с его разрешениями, поэтому импорт библиотеки не даёт лишних прав. ```lua -- 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") ``` ### function Plugin.settings(ui) … end 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, на отдельном экране. ```lua 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), модули и ассеты ### Package layout: manifest.json · main.lua · lib/*.lua · assets/** 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 продолжают работать: при установке они оборачиваются в ту же структуру. ```lua my_plugin.wexp ├─ manifest.json ├─ main.lua ├─ lib/utils.lua └─ assets/logo.png ``` ### manifest.json: { id, name, version, author, description, icon, permissions, minApi, main, background, tags, runtime } 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". ```lua { "name": "Radar", "version": "1.2", "icon": "assets/icon.png", "permissions": ["network"], "minApi": 3 } ``` ### require("name") 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/ своего пакета. ```lua -- lib/utils.lua local M = {} function M.fmt(ms) return Wex.format.duration(ms) end return M -- main.lua local utils = require("utils") ``` ### Wex.import(name) 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 может быть функцией (вызывается) или таблицей (используется как есть). ```lua local text = Wex.import("text_utils") if text then ui:text(text.shout("hi")) end ``` ### Wex.import(name, range?) → exports — range: a semver range ("^2.3.0", "~1.2", ">=1.0 <2.0", exact, "*") 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, чтобы закрепить совместимую зависимость. ```lua 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.0 if dsp then local mags = dsp.fft(samples) else Wex.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. ```lua -- the plugin exposes an extensible "export" action; a library it imports can hook it too Wex.hook.transform("export", 50, function(ctx) ctx.rows = dropHidden(ctx.rows); return ctx end) Wex.hook.intercept("export", 100, function(ctx) if #ctx.rows == 0 then Wex.toast("nothing to export"); return true end -- veto end) local result = Wex.hook.fire("export", { rows = data, format = "csv" }) if not result.consumed then writeExport(result) end ``` ### Wex.assets.path(name) read(name, default?) lines base64 dataUri json xml csv(name, sep?, header?) list(sub?) exists size mime dir() copyToFiles(name, newName?) 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. ```lua local cfg = Wex.assets.json("config.json") for _, f in ipairs(Wex.assets.list("sounds")) do print(f) end ``` ### "asset:name" everywhere a source is accepted 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 не нужно — файл едет внутри пакета. ```lua ui:image("asset:hero.png"):round("md") ui:button("Play", function() Wex.audio.play("asset:boom.mp3") end) ``` ### 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/ и укажи путь. ```lua ui:item({ icon = "assets/wifi.svg", title = "Office AP" }) ui:button("Scan", scan):icon("assets/radar.xml") ui:actions({ { label = "Sync", icon = "asset:sync.svg", onClick = sync } }) ``` ### Studio: explorer · tabs · completion · Problems · Output · Reference 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 из файлового менеджера или чата тоже устанавливает его. ### function Plugin.onUpdate(oldVersion, newVersion) 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 остаются нетронутыми. Ради этого хук и нужен: если новый код ожидает другую структуру, именно здесь старые данные мигрируют, пока их никто не прочитал. Parameters: - `oldVersion` — the version that was installed before, as a string. / версия, которая стояла раньше, строкой. - `newVersion` — the version that has just been installed. / версия, которая только что установлена. ```lua function Plugin.onUpdate(oldVersion, newVersion) if oldVersion == "1.0" then -- 1.1 keeps hosts as a table instead of one string local single = Wex.storage.get("host") if single then Wex.storage.setJson("hosts", { single }) Wex.storage.remove("host") end end Wex.log("migrated " .. oldVersion .. " -> " .. newVersion) end ``` ### wex://install?url=https://…/plugin.wexp 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/ instead, where the app already has the account that may install it. Ссылка, которая ставит пакет в одно нажатие. Приложение скачивает его только по https, а дальше идёт тот же путь, что и при открытии файла: читается манифест, показывается отчёт о рисках, и без подтверждения пользователя ничего не устанавливается. Годится автору, который раздаёт .wexp со своего сайта; плагин с форума открывается через wex://report/, где у приложения уже есть аккаунт, которому разрешена установка. ```lua Install in WEX ``` --- ### manifest.json — the .wexp schema 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 значат одно и то же; для пакета манифест приоритетнее. ```lua { "id": "com.me.ipinfo", "name": "IP Info", "version": "1.2.0", "author": "me", "description": "Shows your public IP", "icon": "assets/icon.png", "main": "main.lua", "runtime": "lua", "minApi": 12, "permissions": ["network", "clipboard"], "handles": ["text/plain"], "provides": [], "requires": ["acme.util"] } ``` ## Sharing & forum — Публикация и форум ### Share the .lua / .wexp file 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/ — его можно снова импортировать. ### Publish to the forum Long-press → Publish. The plugin name auto-fills the post title; you add a description. The script travels base64-encoded. Долгое нажатие → Опубликовать. Имя плагина само становится заголовком; ты добавляешь описание. Скрипт едет в base64. ### Install / update from the forum 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. Форум → Плагины → нажми на пост → страница с автором, разрешениями, описанием (для пакета — ещё файлы и размер) → Установить. То же имя уже стоит → Обновить / Переустановить / Отдельная копия. ### Delete a post In the install sheet, the author can delete their own post; a forum admin can delete any. В окне установки автор может удалить свой пост; админ форума — любой. ### Open a .lua or .wexp file Tap a .lua or .wexp in a file manager / chat / download and WEX offers to install it (a .lua that isn't a plugin opens in the editor). Тапни .lua или .wexp в файловом менеджере / чате / загрузках — WEX предложит установить его (.lua, не являющийся плагином, откроется в редакторе). --- ## Hot reload from your desktop — Hot reload с компьютера ### Editor menu → Hot reload → Start 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 сохраняется. Сервер живёт, пока открыт редактор, и останавливается при установке плагина (рабочая папка удаляется). ```lua # 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 · defs · pack [folder] · GET / · PUT /files/ · POST /reload · GET /log?since=N · GET /defs The dialog exports wex-dev.mjs, a dependency-free Node 18+ script with four jobs. init --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 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 скачивает wex-api.def.lua с телефона, поэтому автодополнение всегда совпадает со сборкой, на которой вы тестируете. С URL и папкой он один раз синхронизирует, следит за изменениями, отправляет каждый изменённый файл и выводит лог плагина. HTTP API под ним простой: каждый запрос несёт X-Wex-Token (или ?token=), пути относительны рабочей папки, тело — содержимое файла, /log держит long-poll до 25 с. Dot-файлы, node_modules и файл типов не отправляются. ```lua # 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 ``` --- ## UI · basics — UI · базовые ### ui:title(s) → h Bold heading. Жирный заголовок. Parameters: - `s` — the heading text. / текст заголовка. Returns: a style handle (see UI · styling). / хендл стиля (см. UI · стилизация). ```lua ui:title("Settings") ``` ### ui:text(s) → h ui:html(s) → h Body text. html() renders basic tags (b, i, a, br, font color…). Обычный текст. html() рендерит базовые теги (b, i, a, br, font color…). Parameters: - `s` — plain text, or HTML for html(). / простой текст, или HTML для html(). Returns: a handle: h:get() reads, h:set(v) replaces the text. / хендл: h:get() читает, h:set(v) заменяет текст. ```lua local out = ui:text("") out:set("done") ``` ### ui:subtitle(s) → h ui:caption(s) → h 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) — семантические размеры для единообразия экранов без подбора размеров на глаз. Parameters: - `s` — the text. / текст. Returns: a handle: h:get() reads, h:set(v) replaces the text. / хендл: h:get() читает, h:set(v) заменяет текст. ```lua ui:subtitle("Connection") ui:caption("updated 2s ago") ``` ### ui:kv(label, value) → h A settings-style row: muted label on the left, bold value on the right. Ideal for status/info screens. Строка вида «настройка»: приглушённая подпись слева, жирное значение справа. Для экранов статуса/инфо. Parameters: - `label` — left-aligned caption. / подпись слева. - `value` — right-aligned, bold. / значение справа, жирным. Returns: a handle on the value: h:get()/h:set(v) to update it live. / хендл значения: h:get()/h:set(v) для живого обновления. ```lua local rtt = ui:kv("Ping", "…") Wex.interval(1000, function() rtt:set(measure() .. " ms") end) ``` ### ui:badge(text, tone) Small status pill — tone tints both text and background so it reads clearly in light and dark. Маленькая плашка статуса — tone задаёт цвет текста и мягкий фон, читаемо в светлой и тёмной теме. Parameters: - `text` — short label, e.g. "online". / короткая подпись, напр. "online". - `tone` — "success" / "warning" / "danger" / "info" / "neutral" (default). / "success" / "warning" / "danger" / "info" / "neutral" (по умолчанию). ```lua ui:row(function(c) c:text("Status"):grow() c:badge("online", "success") end) ``` ### ui:empty(icon, title, subtitle) Centered placeholder for an empty list or a not-yet-loaded state. Центрированная заглушка для пустого списка или состояния до загрузки. Parameters: - `icon` — an emoji shown large on top. / эмодзи крупно сверху. - `title / subtitle` — bold heading and muted detail line. / жирный заголовок и приглушённая строка. ```lua ui:empty("info", "Nothing yet", "Pull down to refresh") ``` ### ui:button(label, fn) Filled button. Заполненная кнопка. Parameters: - `label` — button caption. / надпись на кнопке. - `fn` — called on tap, no arguments. / вызов по тапу, без аргументов. ```lua ui:button("Run", function() ui:toast("tapped") end) ``` ### ui:link(label, url) Outlined button that opens a URL in the browser. Кнопка-контур, открывает URL в браузере. Parameters: - `label` — button caption. / надпись. - `url` — the address to open. / адрес для открытия. ```lua ui:link("Docs", "https://lua.org") ``` ### ui:input(hint, init) → h Single-line text field. :set(s) triggers :onChange like typing does. Однострочное поле ввода. :set(s) вызывает :onChange так же, как ввод с клавиатуры. Parameters: - `hint` — placeholder shown when empty. / подсказка, когда пусто. - `init` — initial text (optional). / начальный текст (необязательно). Returns: a handle: h:get() → current text, h:set(v) writes it. / хендл: h:get() → текущий текст, h:set(v) записывает. ```lua local url = ui:input("URL", "https://") -- later: url:get() ``` ### ui:switch(label, on, fn) ui:checkbox(label, on, fn) Boolean toggle / checkbox. :set(b) and :toggle() call fn as well; nothing is called while the widget is created. Булев переключатель / чекбокс. :set(b) и :toggle() тоже вызывают fn; при создании виджета ничего не вызывается. Parameters: - `label` — text next to the control. / текст рядом с контролом. - `on` — initial state, true/false. / начальное состояние, true/false. - `fn(checked)` — called with the new boolean. / вызов с новым булевым значением. ```lua ui:switch("Dark", true, function(on) ui:toast(tostring(on)) end) ``` ### ui:slider(label, from, to, step, init, fn) Value slider. :set(v) calls fn when the value changes; nothing is called while the widget is created. Слайдер значения. :set(v) вызывает fn при смене значения; при создании виджета ничего не вызывается. Parameters: - `from / to` — range bounds. / границы диапазона. - `step` — increment; 0 = smooth. / шаг; 0 = плавно. - `init` — starting value. / стартовое значение. - `fn(value)` — called as it moves. / вызов при движении. ```lua ui:slider("Vol", 0, 100, 5, 30, function(v) print(v) end) ``` ### ui:radio({..}, fn, initial?) Radio group from a list of strings. Радио-группа из списка строк. Parameters: - `{..}` — array of option labels. / массив подписей вариантов. - `fn(index, value)` — on select; index is 1-based. / при выборе; index с 1. - `initial` — optional — 1-based index to preselect; when given, fn(initial, label) runs inside the constructor. / необязательно — индекс с 1 для предвыбора; если передан, fn(initial, label) выполняется внутри конструктора. ```lua ui:radio({"A", "B"}, function(i, v) ui:toast(v) end) ``` ### ui:stepper(label, min, max, init, fn) Number stepper with − / + buttons. Числовой степпер с кнопками − / +. Parameters: - `min / max` — bounds. / границы. - `init` — starting number. / стартовое число. - `fn(value)` — on each change. / при каждом изменении. ```lua ui:stepper("Qty", 1, 9, 1, function(n) end) ``` ### ui:chips({..}, fn, initial?) ui:dropdown(hint, {..}, fn, initial?) Single choice from a list. chips = inline pills; dropdown = exposed menu. Одиночный выбор из списка. chips = плитки в ряд; dropdown = выпадающее меню. Parameters: - `hint` — dropdown placeholder (dropdown only). / подсказка выпадашки (только dropdown). - `{..}` — array of option labels. / массив вариантов. - `fn(index, value)` — on selection. / при выборе. - `initial` — optional — 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 не вызывает. ```lua ui:dropdown("Method", {"GET", "POST"}, function(i, v) end) ``` ### ui:icon(name, size) Built-in vector icon. Встроенная векторная иконка. Parameters: - `name` — search / delete / key / qr / mail / wifi / copy / refresh / … / search / delete / key / qr / mail / wifi / copy / refresh / … - `size` — dp (default 24). / dp (по умолчанию 24). ```lua ui:icon("wifi", 32) ``` ### ui:image(url) Requires `Plugin.permissions = { "network" }` Shows an image (downsampled). http(s) needs "network"; a data:…;base64 URI is decoded locally with no permission. Показывает картинку (с уменьшением). http(s) требует "network"; data:…;base64 декодируется локально без разрешения. Parameters: - `url` — http(s) address, or a data:image/…;base64,… URI. / адрес http(s), или data:image/…;base64,… URI. ```lua ui:image("data:image/png;base64,iVBORw0K…") ``` ### ui:divider() ui:spacer(dp) ui:toast(s) ui:clear() Divider line; vertical gap; brief message; remove all widgets on the screen. Линия-разделитель; вертикальный отступ; короткое сообщение; очистить экран. Parameters: - `dp` — spacer height — a dp number, or a token ("xs"/"sm"/"md"/"lg"/"xl"/"xxl"). / высота отступа — число dp или токен ("xs"/"sm"/"md"/"lg"/"xl"/"xxl"). - `s` — the toast text. / текст всплывашки. ```lua ui:spacer("md") ui:toast("saved") ``` ### ui:rangeSlider(label, from, to, step, a, b, fn) A two-thumb slider for a range; fn(a, b) fires as either thumb moves. h:get() → { a, b }, h:set(a, b). Ползунок с двумя бегунками для диапазона; fn(a, b) вызывается при движении любого. h:get() → { a, b }, h:set(a, b). --- ## UI · layout & advanced — UI · компоновка и продвинутое ### ui:card(fn(c), gap?) ui:row(fn(c), gap?) ui:column(fn(c), gap?) Nested containers — card box, horizontal row, vertical column. Вложенные контейнеры — карточка, горизонтальная строка, вертикальная колонка. Parameters: - `fn(c)` — builder; `c` is a fresh ui bound to the container. / билдер; `c` — новый ui, привязанный к контейнеру. - `gap` — optional — 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"). Заменяет отступы по умолчанию одним значением; не указывай — прежнее поведение сохранится. ```lua ui:row(function(c) c:button("A", f):grow() c:button("B", f):grow() end, "md") ``` ### ui:scaffold({ title, large, subtitle, fab, actions, refresh, build }) 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-экраном). ```lua function Plugin.build(ui) ui:scaffold({ title = "Inbox", subtitle = "3 unread", fab = { icon = "edit", label = "Compose", onClick = compose }, actions = { { icon = "refresh", onClick = reload } }, refresh = function(done) fetch(function() done() end) end, build = function(c) c:list({ count = #items, render = function(row, i) row:text(items[i]) end }) end, }) end ``` ### ui:list({ count, height?, render }) 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 — фиксированное окно со скроллом и виртуализацией (строятся только видимые строки). Parameters: - `count` — number of items. / число элементов. - `height` — optional: 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. ```lua ui:list({ count = #items, render = function(row, i) row:text(items[i]) end }) ``` ### ui:code(text, lang) Read-only, selectable, syntax-highlighted viewer. Только чтение, выделяемый, с подсветкой синтаксиса. Parameters: - `text` — the content to show. / содержимое для показа. - `lang` — "json" or "lua". / "json" или "lua". ```lua ui:code(body, "json") ``` ### ui:sheet(title, fn(sheet)) Bottom sheet. Нижняя шторка. Parameters: - `title` — sheet heading. / заголовок шторки. - `fn(sheet)` — builds it; call sheet:close() to dismiss. / строит её; sheet:close() закрывает. ```lua ui:sheet("More", function(s) s:button("Close", function() s:close() end) end) ``` ### ui:push(title, fn(screen)) screen:pop() Stack navigation — push opens a new screen; pop / system Back returns. Стековая навигация — push открывает новый экран; pop / системный Back возвращает. Parameters: - `title` — new screen's toolbar title. / заголовок тулбара нового экрана. - `fn(screen)` — builds the pushed screen. / строит открытый экран. ```lua ui:push("Detail", function(s) s:button("Back", function() s:pop() end) end) ``` ### ui:progress() → h ui:circularProgress() → h Linear / circular progress indicator. Линейный / круговой индикатор прогресса. Returns: a handle: h:show(), h:hide(); linear also h:set(pct) 0–100. / хендл: h:show(), h:hide(); у линейного ещё h:set(pct) 0–100. ```lua local p = ui:progress() p:show(); p:set(40) ``` ### ui:wavyProgress(value?|opts?) → h 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, поэтому выглядит одинаково на любом устройстве. ```lua local w = ui:wavyProgress({ value = 40 }) w:set(70, 100) ``` ### ui:view({ "key", … }, fn(v)) → h 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 для живого дашборда, остающегося плавным при непрерывном потоке. ```lua local v = ui:view({ "snr", "spectrum" }, function(ui) ui:text(("SNR %.1f dB"):format(Wex.state.snr or 0)) 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 frame end) ``` ### ui:load(start, render, opts?) → h 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 заново. ```lua ui:load(function(done) Wex.http(url, function(res) done(res) end) -- или Wex.worker(src, args):next(done) end, function(ui, res) if not Wex.ok(res) then ui:text("Ошибка"):color("error") return end ui:title(res.body) end, { lines = 3 }) ``` ### ui:stack({ { align, offsetX, offsetY, fill, build = fn(l) }, … }, { height }) → h 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 только при реальной смене панели. --- ## UI · styling (chained) — UI · стилизация (цепочки) ### every widget returns a style handle Setters chain — each returns the handle, so you keep calling on the result. Сеттеры чейнятся — каждый возвращает хендл, вызывай дальше по результату. ```lua ui:text("Hi"):align("center"):color("#38BDF8"):textSize(16) ``` ### h:width(v) h:height(v) h:size(w, h) Widget dimensions. Размеры виджета. Parameters: - `v / w / h` — a number in dp, or "match" / "wrap". / число в dp, либо "match" / "wrap". ```lua ui:button("Wide", f):width("match"):height(56) ``` ### h:align(g) h:gravity(g) align = position within the parent; gravity = inner content alignment. align = позиция в родителе; gravity = выравнивание содержимого. Parameters: - `g` — start / center / end / top / bottom / center_h / center_v. / start / center / end / top / bottom / center_h / center_v. ```lua ui:text("R"):align("end") ``` ### h:margin(all) h:margin(l, t, r, b) h:padding(…) 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). ```lua ui:card(f):margin("sm", "md", "sm", "md"):padding("lg") ``` ### h:weight(n) h:color(hex) h:textSize(sp) weight = LinearLayout weight (use inside ui:row/column, with width 0). color / textSize apply to text widgets. weight = вес в LinearLayout (внутри ui:row/column, ширина 0). color / textSize — для текстовых виджетов. ```lua c:button("Half", f):width(0):weight(1) ui:text("Red"):color("#F00"):textSize(18) ``` ### h:minWidth(dp) minHeight(dp) maxWidth(dp) maxHeight(dp) Size constraints in dp. max* apply to text and images. Ограничения размера в dp. max* — для текста и картинок. ```lua ui:text(long):maxLines(2):maxWidth(200) ``` ### h:grow() Shorthand for width 0 + weight 1 — fills the remaining space in a ui:row. Сокращение для ширины 0 + веса 1 — занимает остаток места в ui:row. ```lua ui:row(function(c) c:button("A", f):grow() c:button("B", f):grow() end) ``` ### h:marginTop / marginBottom / marginStart / marginEnd(dp) Set one margin side without touching the others. dp or a spacing token. Задать одну сторону внешнего отступа, не трогая остальные. dp или токен отступа. ```lua ui:card(f):marginTop("lg"):marginBottom("sm") ``` ### h:paddingH(dp) h:paddingV(dp) Horizontal / vertical inner padding, keeping the other axis. dp or a spacing token. Горизонтальный / вертикальный внутренний отступ, сохраняя другую ось. dp или токен отступа. ```lua ui:button("Wide", f):paddingH("xl"):paddingV("sm") ``` ### h:center() Center the widget within its parent (same as :align("center")). Центрировать виджет в родителе (как :align("center")). ```lua ui:title("Hi"):center() ``` ### h:bg(hex) h:background(hex) Background color — on a card sets the card color, on a button the tint, else the view background. Цвет фона — у карточки цвет карточки, у кнопки тинт, иначе фон вью. ```lua ui:card(f):bg("#111827") ``` ### h:radius(dp) h:elevation(dp) Corner radius (dp or a token: "sm"=8 "md"=16 "lg"=24 "full"=pill) and shadow — for cards and buttons. Скругление (dp или токен: "sm"=8 "md"=16 "lg"=24 "full"=таблетка) и тень — для карточек и кнопок. ```lua ui:card(f):radius("lg"):elevation(4) ``` ### h:alpha(0..1) h:rotation(deg) Opacity and rotation of any widget. Прозрачность и поворот любого виджета. ```lua ui:icon("star", 40):alpha(0.5):rotation(45) ``` ### h:bold() h:italic() h:mono() h:maxLines(n) Text styling — bold / italic / monospace font / line clamp. Стиль текста — жирный / курсив / моноширинный / ограничение строк. ```lua ui:text("Code"):mono():bold() ``` ### h:visible(bool) h:show() h:hide() h:enabled(bool) Toggle visibility (works on any widget) and interactivity. Переключить видимость (у любого виджета) и активность. ```lua local b = ui:button("Go", f) b:enabled(false) ``` ### h:onClick(fn) Attach a tap handler to any widget — e.g. make a card or text tappable. Повесить обработчик тапа на любой виджет — например, сделать карточку или текст кликабельными. ```lua ui:card(function(c) c:text("tap me") end) :onClick(function() ui:toast("tapped") end) ``` ### :type("display"|"title"|"headline"|"subtitle"|"body"|"callout"|"label"|"caption") 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, если нужно число. ### :font("mono"|"serif"|"sans" | "assets/fonts/My.ttf") 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). ### :bgImage("assets/img.png", { fit, radius, overlay }) 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 и карточками во весь экран. --- ## UI · dialogs — UI · диалоги ### ui:dialog{ … } Full custom dialog built from a table of fields. Полный кастомный диалог из таблицы полей. Parameters: - `title / message` — header and body text. / заголовок и текст. - `content = fn(d)` — builds custom widgets via a nested ui `d`. / строит виджеты вложенным ui `d`. - `positive / negative / neutral` — button labels (default OK). / подписи кнопок (по умолчанию OK). - `onPositive / onNegative / onNeutral` — per-button callbacks (optional). / колбэки кнопок (необязательно). ```lua local f ui:dialog{ title = "Name", content = function(d) f = d:input("You", "") end, positive = "OK", onPositive = function() ui:toast(f:get()) end } ``` ### ui:confirm(message, fn) Yes / No dialog. Диалог Да / Нет. Parameters: - `message` — the question. / вопрос. - `fn` — runs only when the user confirms. / вызов только при подтверждении. ```lua ui:confirm("Delete?", function() remove() end) ``` ### ui:prompt(title, hint, fn(text)) Single-input dialog. Диалог с одним полем. Parameters: - `title / hint` — header and field placeholder. / заголовок и подсказка поля. - `fn(text)` — receives the entered text on OK. / получает введённый текст по OK. ```lua ui:prompt("Rename", "new name", function(t) end) ``` --- ## Wex · network — Wex · сеть ### Wex.http(url | opts, fn(res)) Requires `Plugin.permissions = { "network" }` HTTP request. Runs off the main thread; fn is posted back to the main thread. HTTP-запрос. Выполняется вне главного потока; fn возвращается в главный поток. Parameters: - `url` — a string for a simple GET. / строка для простого GET. - `opts` — { url, method, headers = {k=v}, body }. / { url, method, headers = {k=v}, body }. - `fn(res)` — res = { ok, status, body, error }. / res = { ok, status, body, error }. ```lua Wex.http({ url = u, method = "POST", body = j }, function(res) if res.ok then print(res.body) end end) ``` ### Wex.websocket(url, handlers) → ws Requires `Plugin.permissions = { "network" }` Opens a websocket connection. Открывает websocket-соединение. Parameters: - `url` — ws:// or wss:// address. / адрес ws:// или wss://. - `handlers` — { onOpen, onMessage(text), onClose(reason), onError(msg) }. / { onOpen, onMessage(text), onClose(reason), onError(msg) }. Returns: ws with ws:send(text) and ws:close(). / ws с ws:send(text) и ws:close(). ```lua local ws = Wex.websocket(u, { onMessage = function(t) print(t) end }) ws:send("hi") ``` ### Wex.download(url, filename, fn(ok)) Requires `Plugin.permissions = { "network" }` Downloads a binary into the plugin's file sandbox. Скачивает бинарь в файловую песочницу плагина. Parameters: - `url` — source address. / адрес источника. - `filename` — sandbox file name to write. / имя файла в песочнице. - `fn(ok)` — boolean success. / булев успех. ```lua Wex.download(u, "pic.jpg", function(ok) end) ``` ### Wex.upload(url, opts, fn(res)) Requires `Plugin.permissions = { "network" }` multipart/form-data POST. POST multipart/form-data. Parameters: - `opts.fields` — { k = v } form fields. / { k = v } поля формы. - `opts.files` — {{ field, path, filename }} — path is a sandbox file. / {{ field, path, filename }} — path из песочницы. - `fn(res)` — res = { ok, status, body }. / res = { ok, status, body }. ```lua Wex.upload(u, { files = {{ field="f", path="pic.jpg" }} }, function(res) end) ``` --- ### Wex.ok(res) Wex.err(res) Nil-safe result guards. `Wex.ok(res)` is true only when res is a table whose `.ok` is set — safe even if the callback got `nil`. `Wex.err(res)` returns its `.error` string, or nil. Put them at the top of every callback so a failed result is never read. Nil-безопасные проверки результата. `Wex.ok(res)` истинно, только если res — таблица с истинным `.ok` (безопасно даже при `nil`). `Wex.err(res)` возвращает `.error` (строку) или nil. Ставьте их в начале каждого колбэка, чтобы упавший результат не читался дальше. ```lua Wex.http("https://api.ipify.org", function(res) if not Wex.ok(res) then Wex.log(Wex.err(res)) return end out:set(res.body) end) ``` ## Wex · data — Wex · данные ### Wex.storage Per-plugin key/value strings that persist across restarts. Пер-плагинные строки ключ/значение, переживают перезапуск. Parameters: - `get(k)` — → string or nil. / → строка или nil. - `set(k, v)` — store a string. / сохранить строку. - `remove(k) keys()` — delete one; list all key names. / удалить один; список всех ключей. ```lua Wex.storage.set("count", "1") local c = Wex.storage.get("count") ``` ### Wex.secure Like storage but AES-256-GCM encrypted with an OS Keystore key — for secrets. Как storage, но шифруется AES-256-GCM ключом из Keystore — для секретов. Parameters: - `get(k) set(k, v) remove(k)` — same shape as storage. / как у storage. ```lua Wex.secure.set("api_key", key) ``` ### Wex.files Text files inside the plugin's private folder. Names are basenames (no path escape). Текстовые файлы в приватной папке плагина. Имена без путей (без выхода). Parameters: - `read(name)` — → text or nil. / → текст или nil. - `write(name, text)` — create / overwrite. / создать / перезаписать. - `delete(name) exists(name) list()` — remove; check; list names. / удалить; проверить; список имён. ```lua Wex.files.write("log.txt", "hi") ``` ### Wex.db.open(name) → db SQLite database in the sandbox. База SQLite в песочнице. Parameters: - `db:exec(sql, {params})` — run a write / DDL statement. / запись / DDL. - `db:query(sql, {params})` — → array of row tables. / → массив строк-таблиц. - `db:close()` — release the handle. / освободить хендл. ```lua local db = Wex.db.open("data.db") db:exec("CREATE TABLE IF NOT EXISTS t(x)") local rows = db:query("SELECT * FROM t") ``` ### Wex.pickFile(fn(res)) Wex.saveFile(name, content, fn(ok)) Requires `Plugin.permissions = { "files" }` System file dialogs for the user's real files. Системные файловые диалоги для настоящих файлов пользователя. Parameters: - `pick fn(res)` — res = { name, content } or { error }. / res = { name, content } или { error }. - `save name / content` — suggested name and text to write. / предлагаемое имя и текст для записи. - `save fn(ok)` — boolean success. / булев успех. ```lua Wex.pickFile(function(res) if res.content then ui:text(res.content) end end) ``` ### Wex.pickWexFile(fn(res)) Requires `Plugin.permissions = { "files" }` WEX file picker — browse the on-device /WEX folder (bases, downloads, …) with no SAF dialog. Файлпикер WEX — обзор папки /WEX на устройстве (базы, загрузки, …) без SAF-диалога. Parameters: - `fn(res)` — res = { name, path, size, content } or { error }. content only for files under 2 MB. / res = { name, path, size, content } или { error }. content только для файлов до 2 МБ. ```lua Wex.pickWexFile(function(res) if res.content then ui:text(res.content) end end) ``` --- ## Wex · utilities — Wex · утилиты ### Wex.folder.pick(fn(folder)) .saved() → { { uri, name } } .open(uri) → folder folder:list/readText/writeText/delete 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"}. ```lua Plugin.permissions = { "files" } Wex.folder.pick(function(dir) if not dir then return end -- user cancelled dir:writeText("note.txt", "hello", function(ok) dir:list(function(items) for _, f in ipairs(items) do Wex.log(f.name) end end) end) end) ``` ### folder:read(name, fn(base64)) folder:write(name, base64, fn(ok)) 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()) или любой бинарник можно положить прямо в выданную папку. Обе операции работают вне главного потока. ```lua Wex.folder.pick(function(dir) if not dir then return end dir:read("photo.jpg", function(b64) if b64 then dir:write("copy.jpg", b64, function(ok) Wex.toast(ok and "copied" or "failed") end) end end) end) ``` ### Wex.json.encode(t) Wex.json.decode(s) Lua table ↔ JSON string. Arrays use 1-based integer keys. Lua-таблица ↔ JSON-строка. Массивы — целочисленные ключи с 1. Parameters: - `encode(t)` — table → JSON string. / таблица → JSON-строка. - `decode(s)` — JSON string → table (nil on error). / JSON-строка → таблица (nil при ошибке). ```lua local t = Wex.json.decode(res.body) print(t.name) ``` ### Wex.base64.encode(s) Wex.base64.decode(s) Base64 of a UTF-8 string. Base64 от UTF-8-строки. Returns: encode → string; decode → string or nil on invalid input. / encode → строка; decode → строка или nil при неверном вводе. ```lua local b = Wex.base64.encode("hi") ``` ### Wex.hash.md5(s) sha1(s) sha256(s) Hex digest of the input string. Hex-дайджест входной строки. Parameters: - `s` — the data to hash. / данные для хеша. ```lua local h = Wex.hash.sha256("password") ``` ### Wex.crypto.hmacSha1 / hmacSha256 / hmacSha512(msg, key) HMAC hex signature — for API auth / webhooks. HMAC-подпись (hex) — для авторизации API / вебхуков. Parameters: - `msg` — the data to sign. / данные для подписи. - `key` — the secret. / секрет. ```lua local sig = Wex.crypto.hmacSha256(payload, secret) ``` ### Wex.crypto.aesEncrypt(text, pass) aesDecrypt(blob, pass) AES-256-GCM with a passphrase (PBKDF2). AES-256-GCM с парольной фразой (PBKDF2). Parameters: - `text / blob` — plaintext to encrypt / base64 blob to decrypt. / текст для шифра / base64-blob для расшифровки. - `pass` — the passphrase. / парольная фраза. Returns: encrypt → base64 blob; decrypt → text or nil. / encrypt → base64-blob; decrypt → текст или nil. ```lua local b = Wex.crypto.aesEncrypt("secret", pw) ``` ### Wex.now() Current time. Текущее время. Returns: epoch milliseconds (number). / миллисекунды эпохи (число). ```lua local t = Wex.now() ``` ### Wex.eval(luaString) Compiles & runs Lua in a fresh sandbox (bounded — a runaway loop is killed). Компилирует и выполняет Lua в свежей песочнице (ограничено — зависший цикл прерывается). Parameters: - `luaString` — source to run. / исходник для запуска. Returns: its first result, or nil on error. / первый результат, или nil при ошибке. ```lua local r = Wex.eval("return 2 + 2") ``` ### Wex.uuid() A random UUID v4 string. Случайный UUID v4 (строка). ```lua local id = Wex.uuid() ``` ### Wex.time.now / format / parse / iso / startOfDay Date/time helpers — os.date is unavailable in the sandbox. Хелперы даты/времени — os.date в песочнице недоступен. Parameters: - `now()` — epoch milliseconds. / миллисекунды эпохи. - `format(ms, pattern)` — → string, e.g. "yyyy-MM-dd HH:mm". / → строка, напр. "yyyy-MM-dd HH:mm". - `parse(str, pattern)` — → ms, or nil if it doesn't match. / → ms, или nil если не совпало. - `iso(ms) startOfDay(ms)` — ISO-8601 string; midnight of that day. / строка ISO-8601; полночь того дня. ```lua Wex.time.format(Wex.time.now(), "HH:mm:ss") ``` ### Wex.bit — band/bor/bxor/bnot/lshift/rshift/arshift/rol/ror/tobit/tohex + test/extract/replace 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). ### Wex.dsp — fft / rms / peak / hann / dominantFreq 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. --- ## Wex · system — Wex · система ### Wex.dsp.graph{ "window:hann", "fft", "peaks:5", "rms", "dominant" } → g g:process(samples, rate?) → { samples, mag, rms, peak, dominant, peaks } 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. ```lua 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) do Wex.log((" peak %.0f Hz"):format(p.freq)) end end) ``` ### Wex.packet.layer(name, fields) → ctor ctor{ … } → layer Wex.packet.build{ … } → pkt (:bytes/:hex/:base64/:len) Wex.packet.parse("A/B", bytes) 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. ```lua local Eth = Wex.packet.layer("Eth", { { "dst", "mac" }, { "src", "mac" }, { "type", "u16be", { default = 0x0800 } }, }) local IPv4 = Wex.packet.layer("IPv4", { { "ver_ihl", "u8", { default = 0x45 } }, { "tos", "u8" }, { "len", "u16be", { compute = "auto_len" } }, { "id", "u16be" }, { "frag", "u16be" }, { "ttl", "u8", { default = 64 } }, { "proto", "u8" }, { "csum", "u16be", { compute = "ipv4_checksum" } }, { "src", "ip4" }, { "dst", "ip4" }, }) local pkt = Wex.packet.build{ Eth{ dst = "ff:ff:ff:ff:ff:ff", src = "02:11:22:33:44:55" }, IPv4{ src = "10.0.0.2", dst = "10.0.0.1", proto = 17 }, } Wex.log(pkt:hex()) -- len + checksum filled in automatically local v = Wex.packet.parse("Eth/IPv4", pkt:bytes()) Wex.log(v.IPv4.ttl .. " → " .. v.IPv4.dst) -- 64 → 10.0.0.1 ``` ### Wex.timer(ms, fn) → id Wex.interval(ms, fn) → id Wex.cancelTimer(id) One-shot / repeating callbacks on the main thread. Одноразовые / повторяющиеся колбэки в главном потоке. Parameters: - `ms` — delay / period in milliseconds. / задержка / период в миллисекундах. - `fn` — the callback. / колбэк. - `cancelTimer(id)` — stops a timer by its id. / останавливает таймер по id. Returns: an id you pass to cancelTimer. / id, который передаёшь в cancelTimer. ```lua local id = Wex.interval(1000, tick) Wex.cancelTimer(id) ``` ### Wex.background.enable(minutes) Wex.background.disable() Requires `Plugin.permissions = { "background" }` Schedule Plugin.background() periodically via WorkManager. Runs with the UI closed. Планирует Plugin.background() периодически через WorkManager. Работает при закрытом UI. Parameters: - `minutes` — period; minimum 15. / период; минимум 15. ```lua Wex.background.enable(15) ``` ### Wex.notifications.show(opts) Wex.notifications.cancel(id) Requires `Plugin.permissions = { "notifications" }` Local notification. Локальное уведомление. Parameters: - `opts` — { id, title, body }. / { id, title, body }. - `id` — integer to update / cancel later. / целое, чтобы обновить / отменить позже. ```lua Wex.notifications.show({ id = 1, title = "Done", body = "ok" }) ``` ### Wex.clipboard.set(t) Wex.clipboard.get() Write / read the system clipboard. Записать / прочитать системный буфер обмена. Returns: get() → the clipboard text. / get() → текст буфера. ```lua Wex.clipboard.set("copied") ``` ### Wex.openUrl(url) Wex.share(text) Wex.toast(s) Wex.log(s) Open a link; share text via the system sheet; brief message; write to the plugin log (viewable in the manager). Открыть ссылку; поделиться текстом системным шитом; короткое сообщение; запись в лог плагина (виден в менеджере). ```lua Wex.log("started") Wex.share("check this") ``` ### Wex.dialog("title", "message") Quick alert. For interactive dialogs use ui:dialog / confirm / prompt. Быстрый алерт. Для интерактивных — ui:dialog / confirm / prompt. ```lua Wex.dialog("Note", "Saved") ``` ### Wex.prompt({ title, hint, default }, fn(text)) Global single-input dialog — works anywhere, not only inside a ui builder. Глобальный диалог с одним полем — работает где угодно, не только внутри ui. Parameters: - `opts` — { title, hint, default } (or just a title string). / { title, hint, default } (или просто строка-заголовок). - `fn(text)` — the entered text on OK, nil on cancel. / введённый текст по OK, nil при отмене. ```lua Wex.prompt({ title = "Name" }, function(t) Wex.toast(t) end) ``` ### Wex.confirm({ title, message }, fn(ok)) Global Yes / No dialog. Глобальный диалог Да / Нет. Parameters: - `fn(ok)` — boolean: true = confirmed. / boolean: true = подтвердил. ```lua Wex.confirm({ title = "Delete?" }, function(ok) if ok then run() end end) ``` ### Wex.vibrate(ms) Haptic buzz for ms milliseconds (1–5000). Виброотклик на ms миллисекунд (1–5000). ```lua Wex.vibrate(40) ``` ### Wex.auth.biometric({ title, subtitle, cancel, allowDeviceCredential }?, fn({ ok, error })) Wex.auth.canAuthenticate({ deviceCredential }?) → bool 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() сообщает, может ли устройство аутентифицировать прямо сейчас (есть железо и что-то настроено). Используется слабый биометрический класс: годится для ограничения доступа, но не для разблокировки крипто-ключа. Разрешение не нужно — пользователь всегда сам проходит запрос. Только для плагинов на переднем плане. ```lua if Wex.auth.canAuthenticate({ deviceCredential = true }) then Wex.auth.biometric({ title = "Unlock notes", allowDeviceCredential = true }, function(r) if r.ok then openNotes() else Wex.toast(r.error or "cancelled") end end) end ``` ### Wex.battery() Current battery state. Текущее состояние батареи. Returns: { level = 0–100, charging = bool }. / { level = 0–100, charging = bool }. ```lua local b = Wex.battery() Wex.toast(b.level .. "%") ``` ### Wex.theme Wex.device 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"/…), но как числа для собственных расчётов. Parameters: - `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 }. ```lua 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" }. ### Wex.camera.scan(fn(code), { once, facing }) .stop() .hasPermission() .available() 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" }. --- ## Wex · Android + version — Wex · Android и версия ### Wex.camera.capture({ name }?, fn({ ok, path })) 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"}. ```lua Plugin.permissions = { "camera" } Wex.camera.capture(function(r) if r.ok then local img = Wex.image.load(r.path) Wex.log("captured " .. img:width() .. "x" .. img:height()) end end) ``` ### Wex.image.load(src) → img img:resize/scale/crop/rotate/thumbnail/width/height/exif/toBase64/toDataUri/save 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. Работает на переднем плане и в фоне. ```lua local img = Wex.image.load("asset:photo.jpg") img:rotate(90):thumbnail(512) ui:image(img:toDataUri("jpeg", 80)) img:save("thumb.jpg") ``` ### Wex.torch.available() on() off() toggle() isOn() 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() возвращают новое состояние. При закрытии плагина фонарик гасится автоматически. ```lua if Wex.torch.available() then ui:switch("Flashlight", false, function(on) Wex.torch.toggle() end) end ``` ### Wex.intent(spec) Wex.intent.start(spec) Requires `Plugin.permissions = { "intent" }` Fire an Android intent — one API for call, maps, share, opening a link, launching an app or a specific activity. Отправить Android-intent — один API для звонка, карт, шаринга, открытия ссылки, запуска приложения или конкретной активности. Parameters: - `action` — "view" / "send" / "dial" / "edit" / … or a full "android.intent.action.X". / "view" / "send" / "dial" / "edit" / … или полный "android.intent.action.X". - `data` — a URI — tel:, geo:, https:, mailto:, wex://plugin/… / URI — tel:, geo:, https:, mailto:, wex://plugin/… - `type` — MIME type (e.g. "text/plain"). / MIME-тип (напр. "text/plain"). - `extras` — { key = value } put as intent extras. / { key = value } — кладутся в extras. - `package / component` — target an app, or "pkg/Activity". / нацелить на приложение, или "pkg/Activity". Returns: true if launched, false otherwise. / true если запущено, иначе false. ```lua Wex.intent({ action = "dial", data = "tel:+123" }) Wex.intent({ action = "view", data = "geo:0,0?q=Berlin" }) ``` ### Wex.intent.startForResult(spec, fn({ ok, code, uri, text, extras })) 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"}. ```lua Plugin.permissions = { "intent" } Wex.intent.startForResult({ action = "com.google.zxing.client.android.SCAN" }, function(r) if r.ok and r.extras then Wex.toast("scanned: " .. (r.extras.SCAN_RESULT or "?")) end end) ``` ### Wex.intent.query(spec) → list Requires `Plugin.permissions = { "intent" }` Apps that can handle the intent, without launching anything. Приложения, способные обработать intent, ничего не запуская. Returns: array of { package, activity, label }. / массив { package, activity, label }. ```lua for _, app in ipairs(Wex.intent.query({ action = "view", data = "https://x" })) do print(app.label) end ``` ### Wex.api.version Wex.api.build Wex.has(feature) App version + capability detection — so a plugin degrades gracefully on older builds. Версия приложения + детект возможностей — плагин мягко деградирует на старых сборках. Returns: version: string; build: number; has: bool. / version: строка; build: число; has: bool. ```lua if Wex.has("intent") then Wex.intent(...) end ui:text("v" .. Wex.api.version) ``` ### Deep link: wex://plugin/ A link or home-screen shortcut that opens an installed plugin by its id. Ссылка или ярлык на рабочем столе, открывающие установленный плагин по его id. ```lua Wex.intent({ action = "view", data = "wex://plugin/notes" }) ``` --- ## Lifecycle, args & versioning — Жизненный цикл, аргументы и версии ### Plugin.onResume() onPause() onBack() onClose() onResize(w, h) onRotate(landscape) onImmersive(on) 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: поворот может перезапустить плагин, а хост вернёт ориентацию и полноэкранный режим до вашего первого кадра. ```lua function Plugin.onResume() refresh() end function Plugin.onResize(w, h) canvasHeight = h end function Plugin.onBack() if panelOpen then panel:hide(); panelOpen = false; return true end end ``` ### Wex.args Table of arguments the plugin was opened with — from Wex.plugins.open(id, { … }), a notification's `args`, or a deep link wex://plugin/?k=v. Таблица аргументов, с которыми открыли плагин — из Wex.plugins.open(id, { … }), `args` уведомления или диплинка wex://plugin/?k=v. ```lua local host = Wex.args.host or "10.0.0.1" ``` ### Wex.args.share = { action, type, text?, subject?, files = { { name, type, size } … }, count, skipped? } 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 МБ или не читаются). ```lua local s = Wex.args.share if s then if s.text then Wex.clipboard.copy(s.text) end for _, f in ipairs(s.files) do Wex.log(f.name .. " " .. f.type .. " " .. f.size) end end ``` ### Wex.plugins.list() .self() .get(id) .exists(id) .open(id, args?) Installed plugins registry (id, name, icon, version, enabled, permissions). open() launches another plugin, passing `args`. Реестр установленных плагинов (id, name, icon, version, enabled, permissions). open() запускает другой плагин, передавая `args`. ```lua if Wex.plugins.exists("scanner") then Wex.plugins.open("scanner", { target = ip }) end ``` ### Wex.plugins.libraries() 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 каждого установленного включённого плагина. Годится, чтобы найти библиотеки или проверить наличие зависимости перед импортом. ```lua for _, lib in ipairs(Wex.plugins.libraries()) do print(lib.name .. " (from " .. lib.plugin .. ")") end ``` ### Wex.api.level Wex.api.features Wex.has(name) Wex.can(permission) 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. Returns: level: number; features: array; has/can: bool. / level: число; features: массив; has/can: bool. ```lua if Wex.api.level >= 2 and Wex.has("tcp") then … end if not Wex.can("network") then ui:banner("Offline mode", "warning") end ``` ### Wex.id Wex.permissions Wex.headless Wex.lang() Wex.tr({ en=, ru= }) Own plugin id, granted permissions, whether running in Plugin.background(), device language and a one-line localizer. Свой id плагина, выданные разрешения, признак запуска в Plugin.background(), язык устройства и однострочный локализатор. ```lua ui:title(Wex.tr({ en = "Settings", ru = "Настройки" })) ``` ### Wex.shortcut(label?) Asks the launcher to pin a home-screen shortcut that opens this plugin (uses the plugin's avatar as the icon). Просит лаунчер закрепить ярлык на рабочем столе, открывающий этот плагин (иконка — аватар плагина). Returns: true if the request was shown. / true, если запрос показан. ```lua ui:button("Add to home screen", function() Wex.shortcut() end) ``` ### Wex.worker(luaSource, args?, { timeout }?) → promise 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)). Оба вызываются на главном потоке. ### function Plugin.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). ```lua function Plugin.onBackPredictive(progress, edge) panel:alpha(1 - progress) end ``` ### Plugin.service() Wex.service.start/stop/running/notify .requestUnrestricted() .isBatteryOptimized() 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()), чтобы попросить пользователя исключить приложение из оптимизации батареи. --- ## UI · more widgets — UI · ещё виджеты ### ui:heading(s, level) ui:markdown(md) ui:json(t) Heading with level 1–3; Markdown (headings, **bold**, *italic*, `code`, lists, links, quotes); a pretty-printed JSON viewer of any table. Заголовок уровня 1–3; Markdown (заголовки, **жирный**, *курсив*, `код`, списки, ссылки, цитаты); красиво отформатированный JSON любой таблицы. ```lua ui:markdown("## Report\n- **ok**: 12\n- *failed*: 1") ui:json(res.json) ``` ### ui:terminal({ height, maxLines, lines }) → t 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 бод остаётся отзывчивой вместо перелейаута на каждую строку. Parameters: - `height` — dp, default 240. / dp, по умолчанию 240. - `maxLines` — ring buffer size (default 500). / размер кольцевого буфера (по умолчанию 500). ```lua local term = ui:terminal({ height = 260 }) term:info("connecting…") term:ok("200 OK") term:error("timeout") ``` ### ui:item({ title, subtitle, icon | avatar, trailing, badge, tone, chevron, onClick, onLongClick }) → h 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). ```lua ui:item({ icon = "wifi", title = "Office AP", subtitle = "-52 dBm · ch 36", badge = "WPA3", tone = "success", onClick = function() open(ap) end }) ``` ### ui:tile(icon, label, fn) ui:grid(cols, count | items, fn(cell, i, item), gap?) Square tappable tiles and an equal-width grid (1–6 columns). The grid builder receives a `cell` ui, the 1-based index and the item. Квадратные плитки и сетка равных колонок (1–6). Билдер сетки получает `cell` ui, индекс с 1 и элемент. ```lua ui:grid(3, tools, function(cell, i, t) cell:tile(t.icon, t.name, function() run(t) end) end, "sm") ``` ### ui:stat(label, value, { delta, tone, icon, color }) → h KPI card: small label, big value, optional delta pill. h:set(v) / h:delta(text, tone) / h:bind(key, fmt?). KPI-карточка: подпись, крупное значение, необязательная плашка дельты. h:set(v) / h:delta(text, tone) / h:bind(key, fmt?). ```lua ui:row(function(r) r:stat("Latency", "23 ms", { delta = "-4 ms", tone = "success" }):grow() r:stat("Loss", "0.2%", { tone = "warning" }):grow() end, "sm") ``` ### ui:banner(text | { text, tone, icon, action, onAction }, tone?, actionLabel?, fn?) ui:steps({…}, current) → s Tinted inline message with an optional action; a stage indicator (s:set(i) / s:next() / s:prev()). Подсвеченное встроенное сообщение с действием; индикатор этапов (s:set(i) / s:next() / s:prev()). ```lua ui:banner("Update available", "info", "Install", update) local s = ui:steps({ "Scan", "Select", "Done" }, 1) ``` ### ui:avatar(nameOrUrl, size) ui:loading(text) → l ui:iconButton(icon, fn, tooltip?) Initials avatar (hashed color) or circular image; a spinner with text (l:text(s), l:done("✓"), l:hide()); a compact icon-only button. Аватар с инициалами (цвет по хешу) или круглая картинка; спиннер с текстом (l:text(s), l:done("✓"), l:hide()); компактная кнопка-иконка. ```lua local l = ui:loading("Fetching…") Wex.http(u, function(r) l:done("Loaded") end) ``` ### ui:search(hint, fn(text), debounceMs?) ui:textarea(hint, init, lines) ui:password(hint) ui:number(hint, init) 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() — число. ```lua ui:search("Filter", function(q) list:count(#filter(q)) end) local port = ui:number("Port", 443) print(port:number()) ``` ### ui:multiChips({…}, fn(indices, labels), initial?) ui:segmented({…}, fn(i, label), initial?) ui:rating(max, init, fn(v)) 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». ```lua 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(). ```lua ui:date("From", nil, function(ms, s) from = ms end) ``` ### ui:form({ fields }, "Submit", fn(values)) → f 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(). ```lua 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). ```lua ui:tabs({ "Status", "Log" }, function(c, i) if i == 1 then status(c) else c:terminal({ lines = Wex.log.lines() }) end end) ``` ### ui:hscroll(fn(c), gap?) ui:table({ headers, rows, onRow, minWidth }) 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(). ```lua ui:table({ headers = { "Host", "RTT" }, rows = { { "1.1.1.1", "12 ms" } }, onRow = function(i, row) ui:toast(row[1]) end }) ``` ### ui:chart({ type, values, labels, colors, max, height, showValues, grid }) → c 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; он рассчитан на живой поток, стоит одну вставку, а перерисовка откладывается до следующего кадра, поэтому пачка из тысячи отсчётов между двумя кадрами рисуется один раз. ```lua 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. Этого хватает на анализатор спектра с зумом или осциллограф без перехода на нативный код. ```lua local zoom, offset = 1, 0 ui:canvas(240, function(d) d:color("primary") for i = 1, 64 do d:line(i * 4 * zoom + offset, 220, i * 4 * zoom + offset, 220 - bins[i]) end end):onTouch(function(x, y, phase, p) if p.count >= 2 then zoom = p.scale offset = p.panX d:redraw() end end) ``` ### 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 / тот же хендл, вызовы можно объединять в цепочку ```lua ui:canvas(200, function(d) d:grid(20) d:color("primary"):stroke(2) d:path({ 0, 100, 40, 60, 80, 120, 120, 30 }, false) d:color("warning"):fill() d:circle(120, 30, 4) d:font(12, true, "left"):text("peak", 128, 34) end) ``` ### ui:web(htmlOrUrl, { height, js, external, zoom, fullscreen, scroll, card }) → w Requires `Plugin.permissions = { "network (for URLs)" }` 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(). ```lua local w = ui:web("asset:game.html", { height = "match", js = true, fullscreen = true }) w:onMessage(function(m) ui:toast(m) end) w:post("hello page") -- → window.onWexMessage("hello page") ``` ### w:handle(method, fn(args [, respond])) — page: Wex.call(method, args) → Promise 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) — продолжают работать рядом. ```lua local w = ui:web([[ ]], { js = true }) w:handle("stats", function(a) return { total = 42, tag = a.tag } -- the returned table resolves the page Promise end) ``` ### 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. Хендл управляет воспроизведением. Returns: a handle: v:play(), v:pause(), v:seek(ms), v:position(), v:duration(), v:mute(bool), v:onEnd(fn). ```lua 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. ```lua -- Lua Wex.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:fab(icon, fn, label?) ui:hideFab() ui:actions({ { label, icon, onClick }, … }) ui:menu({…}, fn(i, label)) Floating action button; toolbar action buttons for the current screen (icon names or emoji); a popup menu anchored to the last widget. Плавающая кнопка; кнопки действий в тулбаре текущего экрана (имена иконок или эмодзи); всплывающее меню у последнего виджета. ```lua ui:actions({ { label = "Refresh", icon = "refresh", onClick = reload }, { label = "Help", icon = "info", onClick = help } }) ui:fab("bolt", scan, "Scan") ``` ### 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". ```lua ui:splitButton({ label = "Run", style = "tonal", onClick = function() start() end, items = { "Fast", "Thorough" }, onSelect = function(i, name) ui:toast(name) end, }) ``` ### ui:floatingToolbar({ actions = { { icon, label, onClick } … }, color? }) → h 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 — по центру и по размеру содержимого. Каждое действие — иконка-кнопка с подсказкой-подписью. ```lua ui:floatingToolbar({ actions = { { icon = "refresh", label = "Reload", onClick = function() reload() end }, { icon = "share", label = "Share", onClick = function() share() end }, } }) ``` ### ui:setTitle(s) ui:setSubtitle(s) ui:close() ui:scrollTo("top"|"bottom") ui:keyboard(false) ui:count() ui:last() Toolbar title/subtitle, close the plugin, scroll the page, hide the keyboard, number of widgets on this ui, style handle of the last one. Заголовок/подзаголовок тулбара, закрыть плагин, прокрутить страницу, скрыть клавиатуру, число виджетов на этом ui, хендл последнего. ```lua ui:setSubtitle("connected · " .. host) ``` ### ui:snack(text, action?, fn?) ui:alert(title, message, fn?) ui:sheetList(title, {…}, fn(i, label)) Snackbar with an action; simple alert; a quick bottom-sheet list of actions. Снекбар с действием; простой алерт; быстрый нижний список действий. ```lua ui:sheetList("Host", { "Ping", "Ports", "Copy IP" }, function(i) … end) ``` ### ui:confirm(message, fn, { title, yes, no, onNo, danger }) ui:prompt(title, hint, fn(text), { default, type, multiline, ok, cancel }) ui:dialog{ … keepOpen, cancelable, onDismiss, icon } Dialog options grew: custom button labels, danger styling, typed prompts (number → returns a number), keepOpen = true keeps ui:dialog open while onPositive returns false (validation). Диалоги получили опции: подписи кнопок, стиль danger, типизированный prompt (number → число), keepOpen = true держит ui:dialog открытым, пока onPositive возвращает false (валидация). ```lua ui:confirm("Delete all?", wipe, { yes = "Delete", danger = true }) ui:prompt("Port", "1-65535", function(n) end, { type = "number", default = "22" }) ``` ### ui:hero({ title, subtitle, icon, gradient = {c1, c2}, height }) → h 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). Цвет текста подбирается автоматически для контраста. Отлично смотрится вверху экрана. ### ui:gauge({ value=0..1, label, caption, color, track, size, thickness }) → h ui:ring(…) 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) перекрасит — достаточно дёшево, чтобы обновлять по таймеру. ### ui:skeleton({ lines, height, gap, radius }) → h 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() меняют. ### ui:marquee(text) → h A single line that auto-scrolls horizontally when the text is longer than the screen. Handy for tickers and long status lines. Одна строка, автоматически прокручивающаяся по горизонтали, когда текст длиннее экрана. Удобно для бегущих строк и длинных статусов. ### ui:pie({ { value, color, label }, … }, { size, donut, thickness }) → h 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() → выбранный токен. Дай пользователю темизировать твой плагин. ### ui:avatarStack({ "Ann", "Bob", … }, { max, size }) → h Overlapping circular avatars with initials and a +N overflow chip. Handy for members, participants and assignees. Перекрывающиеся круглые аватары с инициалами и чипом +N для остатка. Удобно для участников, членов и исполнителей. ### ui:qr(text, { size, color, background, margin }) → h A QR code for any text — a link, Wi-Fi credentials, a contact, a payment string. h:set(text) re-encodes it. Generated on-device, works offline. QR-код для любого текста — ссылка, данные Wi-Fi, контакт, платёжная строка. h:set(text) перекодирует. Генерируется на устройстве, работает оффлайн. --- ## UI · handles (extended) — UI · хендлы (расширенные) ### every handle: :gradient(c1, c2, dir?) :border(w, color?, radius?) :ripple() :tooltip(s) :scale(x) :marginH/:marginV :tag(s) 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 …). ```lua ui:card(f):gradient("#312E81", "#0EA5E9", "diagonal"):border(1, "outline") ``` ### :fadeIn(ms?) :fadeOut(ms?) :slideIn(ms?) :shake() :pulse() :animate({ alpha, x, y, scale, rotation, duration, onDone }) Built-in animations for feedback (shake an invalid field, pulse a stat that changed, fade rows in). Встроенные анимации для отклика (потрясти невалидное поле, пульснуть изменившийся показатель, плавно показать строки). ```lua if not ok then host:shake():error("Unreachable") end ``` ### :onLongClick(fn) :remove() :scrollTo() :toggle() :invisible() :isVisible() :font("mono"|"serif") :underline() :strike() :letterSpacing(x) Interaction and text extras available on every handle. Взаимодействие и текстовые дополнения у каждого хендла. ```lua row:onLongClick(function() ui:menu({ "Copy", "Delete" }, act) end) ``` ### :bind(stateKey, fmt?) 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 привязывает значение. ```lua ui:kv("Status", ""):bind("status") ui:stat("Packets", "0"):bind("packets", function(n) return Wex.format.number(n) end) Wex.state.set("status", "online") ``` ### input: :onChange(fn) :onSubmit(fn) :error(msg) :helper(s) :prefix/:suffix :focus() :blur() :readonly() :maxLength(n) :type(t) :icon(name) :clearButton() :endIcon(name, fn) :number() :isEmpty() Text-field handle methods. :onSubmit fires on the keyboard's Done/Search/Enter. Методы хендла поля ввода. :onSubmit срабатывает по Done/Search/Enter на клавиатуре. ```lua local q = ui:input("Host"):icon("search"):clearButton():onSubmit(lookup) ``` ### button: :icon(name) :outlined() :tonal() :danger() :success() :loading(bool, text?) :click() ui:button(label, fn, style) 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 у кнопки нет. ```lua local b = ui:button("Scan", scan, "tonal"):icon("wifi") b:loading(true); … b:loading(false) ``` ### switch/checkbox: :get() :set(b) :toggle() :onChange(fn) chips/dropdown/radio/segmented: :get() :label() :set(i) :items({…}) :clear() slider/stepper/rating: :get() :set(v) 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». ```lua local mode = ui:dropdown("Mode", modes, nil, 1) print(mode:label()) ``` ### card/row/column/hscroll: :clear() :rebuild(fn?) :add(fn) :count() :removeAt(i) :ui() list: :refresh() :count(n) :update(i) :scrollTo(i) :scrollToEnd() :render(fn) Containers can be emptied and re-filled without touching the rest of the screen; lists re-render rows from changed data. Контейнеры можно очищать и наполнять заново, не трогая остальной экран; списки перерисовывают строки по изменившимся данным. ```lua local box = ui:card(function(c) c:text("loading…") end) Wex.http(u, function(r) box:rebuild(function(c) render(c, r.json) end) end) ``` ### image: :set(url) :fit("cover"|"contain"|"stretch") :round(r) :circle() :tint(color) progress: :set(pct) :get() :max(n) :indeterminate() :color(c) 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. Прогресс переключается между определённым и неопределённым. ```lua Wex.download(u, "pic.jpg", function(ok) if ok then img:set("pic.jpg") end end) ``` ### :spin(ms?) :stopSpin() :bounce() :flip(ms?) :zoomIn(ms?) :dropIn(ms?) :highlight(color?, ms?) 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 — вспышка цветом поверх с угасанием. Ничего не портит из собственного фона виджета. ### :countTo(n, ms?, decimals?, prefix?, suffix?) :typewriter(text?, perChar?) 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 печатает строку по буквам. Хорошо для показателей и эффектных выводов. ### :spring({ scale, x, y, rotate, alpha, stiffness, damping }) :shape(token) :shapeMorph(token, ms?) 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). ```lua local box = ui:card(function(c) c:text("Springy") end) ui:button("Bounce", function() box:spring({ y = -18, stiffness = 600, damping = 0.55 }) box:shapeMorph("pill", 280) end) ``` ### :glass(alpha?) :blur(radius) :shared(name) :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). ```lua -- screen A ui:avatar("me.jpg", 56):shared("hero") -- inside the ui:push build (screen B) ui:avatar("me.jpg", 120):shared("hero") -- morphs out from A ``` ### :expand(ms?) :collapse(ms?) :stagger(perMs?, ms?) 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 по очереди проявляет детей контейнера, оживляя список. ### ui:confetti({ count, colors }) ui:haptic("tick"|"long"|"confirm"|"reject") 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 устройства — разрешений не нужно. ### :onSwipe(fn) :swipeToDismiss(fn, "left"|"right"|"both") :draggable(fn(x, y)) :offset(x, y) 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 и перехватывает касания только когда началось настоящее перетаскивание, поэтому окружающая прокрутка работает как прежде. --- ## Wex · network (extended) — Wex · сеть (расширенная) ### Wex.http({ url, method, headers, body | json | form, timeout, follow, binary, save, auth, userAgent }, fn(res)) Requires `Plugin.permissions = { "network" }` 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 в мс. Returns: res = { ok, status, body, json (auto-decoded when JSON), headers = { lowercased }, contentType, url (final), ms, size, error, file }. / res = { ok, status, body, json (декодируется автоматически), headers = { в нижнем регистре }, contentType, url (финальный), ms, size, error, file }. ```lua Wex.http({ url = api .. "/login", json = { user = u, pass = p }, timeout = 8000 }, function(r) if r.ok then token = r.json.token else ui:toast(r.error) end end) ``` ### Wex.http.get(url, fn) .post(url, body, fn) .put .patch .delete .head .json(url, table, fn) Requires `Plugin.permissions = { "network" }` Shorthands. A table body is sent as JSON automatically. Сокращения. Табличное тело отправляется как JSON автоматически. ```lua Wex.http.json(api .. "/items", { name = n }, function(r) print(r.status) end) ``` ### Wex.websocket(url, { onOpen(ws), onMessage(text, base64?), onClose(reason, code), onError(msg), headers, pingSec }) → ws Requires `Plugin.permissions = { "network" }` ws:send(text | table→JSON) / ws:sendJson(t) / ws:close() / ws:isOpen(). Binary frames arrive as (utf8, base64). ws:send(text | таблица→JSON) / ws:sendJson(t) / ws:close() / ws:isOpen(). Бинарные кадры приходят как (utf8, base64). ### Wex.tcp.connect(host, port, { onConnect(s), onData(text, base64), onClose(), onError(msg) }, timeoutMs?) → s Requires `Plugin.permissions = { "network" }` 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(). Данные приходят чанками в главном потоке. ```lua local s = Wex.tcp.connect(host, 6379, { onConnect = function(s) s:sendLine("PING") end, onData = function(d) term:append(d) end }) ``` ### Wex.udp.send(host, port, data, fn(reply | nil)?, timeoutMs?) Wex.udp.listen(port, fn(data, fromHost, fromPort)) → l Requires `Plugin.permissions = { "network" }` Fire a datagram (optionally wait for one reply); listen on a port (l:close(), l:send(host, port, data), l.port). Отправить датаграмму (опционально дождаться одного ответа); слушать порт (l:close(), l:send(host, port, data), l.port). ```lua Wex.udp.send("255.255.255.255", 9, "wake") ``` ### Wex.net.dns(host, fn(ips)) .reverse(ip, fn(name)) .ping(host, timeoutMs, fn(ms | nil)) .tcp(host, port, timeoutMs, fn(open, ms)) Requires `Plugin.permissions = { "network" }` Resolve names, reverse-lookup, latency probe (ICMP/echo, falling back to a TCP knock on 443/80), single-port check. Резолв имён, обратный резолв, замер задержки (ICMP/echo с фолбэком на TCP 443/80), проверка одного порта. ```lua Wex.net.ping("1.1.1.1", 1500, function(ms) rtt:set(ms and (ms .. " ms") or "—") end) ``` ### Wex.net.ports(host, { 22, 80, 443 } | { from = 1, to = 1024 }, timeoutMs, fn({ open }), onProgress(done, total)?) Requires `Plugin.permissions = { "network" }` Parallel TCP port check, capped at 512 ports per call (default list of common ports when omitted). Параллельная проверка TCP-портов, до 512 портов за вызов (без списка — распространённые порты). ```lua Wex.net.ports(ip, { from = 1, to = 512 }, 300, function(open) term:ok(table.concat(open, ", ")) end, function(d, t) prog:set(d * 100 / t) end) ``` ### Wex.net.localIp() .interfaces() .isOnline() .type() .isMetered() .isPrivateIp(ip) .ipToInt(ip) .intToIp(n) .inCidr(ip, cidr) .cidrRange(cidr) Local addressing and connectivity info — no permission needed. type() → wifi | cellular | ethernet | vpn | bluetooth | none. Локальная адресация и состояние сети — без разрешений. type() → wifi | cellular | ethernet | vpn | bluetooth | none. ```lua local r = Wex.net.cidrRange("192.168.1.0/24") print(r.first, r.last, r.count) ``` ### Wex.download(url, name, fn(ok, res)) Wex.upload(url, { fields, files = {{ field, path, filename }}, headers }, fn(res)) Requires `Plugin.permissions = { "network" }` Download streams into the sandbox file `name` (no size limit in memory); upload is multipart/form-data with optional headers. Download пишет поток в файл песочницы `name` (без лимита в памяти); upload — multipart/form-data с необязательными заголовками. --- ## Wex · data (extended) — Wex · данные (расширенные) ### Wex.download(url, name, { onProgress = fn(done, total), onDone = fn(res), headers }) → handle:cancel() 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)) не изменился. ```lua local dl = Wex.download("https://example.com/big.zip", "big.zip", { onProgress = function(done, total) bar:set(done, total) end, onDone = function(res) if res.ok then Wex.toast("saved " .. res.path) end end, }) -- dl:cancel() ``` ### Wex.storage.getJson/setJson getNumber/setNumber getBool/setBool increment(k, by?) has(k) all() clear() Typed helpers over the same key/value store; set(k, table) stores JSON automatically; get(k, default) returns the default when missing. Типизированные хелперы над тем же хранилищем; set(k, таблица) сохраняет JSON; get(k, default) возвращает default, если ключа нет. ```lua local n = Wex.storage.increment("runs") Wex.storage.setJson("hosts", hosts) ``` ### Wex.cache.set(k, v, ttlSec) get(k) getJson(k) has(k) remove(k) clear() remember(k, ttlSec, fn) Key/value with expiry — ideal for API responses. remember() returns the cached value or computes, stores and returns fn()'s result. Ключ/значение с истечением — идеально для ответов API. remember() возвращает кэш или вычисляет, сохраняет и возвращает результат fn(). ```lua local geo = Wex.cache.remember("geo:" .. ip, 3600, function() return lookup(ip) end) ``` ### Wex.files.append(name, text) lines(name) size modified path dir() info() copy(a, b) rename(a, b) clear() More sandbox file operations. info() lists { name, size, modified }; path()/dir() give absolute paths (e.g. for ui:image or audio.play). Больше операций с файлами песочницы. info() — список { name, size, modified }; path()/dir() дают абсолютные пути (для ui:image или audio.play). ```lua Wex.files.append("log.txt", os.time() .. " ok\n") ``` ### Wex.files.readBase64(name) writeBase64(name, b64) readJson(name) writeJson(name, t) hash(name) share(name, mime?) open(name, mime?) Binary files via base64, JSON round-trips, SHA-256 of a file, and handing a file to another app (share sheet / viewer). Бинарные файлы через base64, JSON туда-обратно, SHA-256 файла и передача файла другому приложению (шаринг / просмотр). ```lua Wex.files.writeJson("report.json", report) Wex.files.share("report.json", "application/json") ``` ### 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 сами. ```lua local id = db:insert("hosts", { ip = ip, rtt = ms }) local row = db:one("SELECT * FROM hosts WHERE id = ?", { id }) ``` ### db:count(table, where?, params?) db:tables() db:columns(table) db:transaction(fn(db)) db:isOpen() Wex.db.exists(name) Wex.db.delete(name) Schema introspection, batched writes in a transaction (rolled back if fn errors), database file management. Интроспекция схемы, пакетная запись в транзакции (откатывается при ошибке fn), управление файлами БД. ```lua db:transaction(function(d) for _, h in ipairs(hosts) do d:insert("hosts", h) end end) ``` ### 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-запросы избирательными — каждая запись перезапускает их все. ```lua 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 }) end end) end) db:insert("todo", { title = "Buy milk", done = 0 }) -- the live query re-fires -- sub:stop() to unsubscribe ``` ### Wex.secure.get(k, default?) set(k, v) → bool remove(k) has(k) Keystore-encrypted storage; set() now reports success. Хранилище, шифруемое Keystore; set() теперь сообщает об успехе. --- ### Wex.storage.version() setVersion(n) migrate(to, fn(from)) 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 Plugin.build or Plugin.onUpdate) so old data is reshaped exactly once. Версионирование схемы сохранённых данных. `version()` читает номер схемы (0, если не задан). `migrate(to, fn)` один раз вызывает `fn(текущаяВерсия)`, когда сохранённая версия меньше `to`, и записывает `to` — вызывайте до первого чтения (в Plugin.build или Plugin.onUpdate), чтобы старые данные переформатировались ровно один раз. ```lua Wex.storage.migrate(2, function(from) local single = Wex.storage.get("host") if single then Wex.storage.setJson("hosts", { single }); Wex.storage.remove("host") end end) ``` ## Wex · utilities (extended) — Wex · утилиты (расширенные) ### Wex.json.pretty(t) get(t | jsonString, "a.b[2].c", default?) merge(t1, t2, …) keys(t) values(t) size(t) isArray(t) Pretty printing, safe deep access by path (nil/default instead of an error), shallow merge and table introspection. Красивая печать, безопасный доступ по пути (nil/default вместо ошибки), поверхностное слияние и интроспекция таблиц. ```lua local city = Wex.json.get(res.json, "location.city", "?") ``` ### Wex.str.trim split(s, sep, trimEmpty?) join startsWith endsWith contains replace padStart padEnd repeat capitalize title upper lower reverse truncate lines words count isBlank isNumber isEmail isUrl isIp slug urlEncode urlDecode htmlEscape htmlUnescape stripHtml template(s, vars) levenshtein similarity bytes chars wrap String helpers Lua lacks. template() fills ${name} / {{name}} from a table. Строковые хелперы, которых нет в Lua. template() подставляет ${name} / {{name}} из таблицы. ```lua ui:text(Wex.str.template("Hello, ${name}!", { name = user })) for _, p in ipairs(Wex.str.split("a, b,,c", ",", true)) do … end ``` ### Wex.url.encode decode parse(url) → { scheme, host, port, path, query = {}, fragment, segments } build(base, { k = v }) join(a, b) domain(url) URL parsing and building with proper encoding. Разбор и сборка URL с корректным кодированием. ```lua local u = Wex.url.build(api .. "/search", { q = query, limit = 20 }) ``` ### Wex.regex.test(s, pattern, flags?) match(s, p) → { full, g1, g2, …, start, stop } matchAll(s, p) find(s, p) replace(s, p, repl | fn(m)) split(s, p) escape(s) Real (Java) regular expressions — groups, lookarounds, flags "i" (ignore case), "m" (multiline), "s" (dot matches newline). Настоящие (Java) регулярные выражения — группы, lookaround, флаги "i" (без регистра), "m" (многострочно), "s" (точка ловит перевод строки). ```lua for _, m in ipairs(Wex.regex.matchAll(log, "(\\d+\\.\\d+\\.\\d+\\.\\d+)")) do ips[#ips+1] = m[1] end ``` ### Wex.format.bytes(n) duration(ms) relative(ms) number(n, decimals?) percent(x, digits?) fixed(n, digits) speed(bytesPerSec) clock(ms) ordinal(n) plural(n, one, few, many) Human-readable formatting localized to the device language ("1.5 MB", "2h 03m", "5 min ago", "01:42"). Человекочитаемое форматирование с учётом языка устройства («1.5 MB», «2ч 03м», «5 мин назад», «01:42»). ```lua ui:kv("Downloaded", Wex.format.bytes(total) .. " · " .. Wex.format.speed(rate)) ``` ### 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 для пустой таблицы. ```lua 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 в метрах). ```lua ui:kv("Avg RTT", Wex.math.round(Wex.math.avg(samples), 1) .. " ms") ``` ### Wex.color.parse(hex) → { r, g, b, a } rgb(r, g, b, a?) hsl(h, s, l) toHsl alpha(hex, a) lighten darken mix(a, b, t) contrast(hex) luminance isDark invert gradient(from, to, steps) heat(0..1) Color math for charts and custom styling. heat() maps 0..1 to green → yellow → red — great for latency / load. Цветовая математика для графиков и стилей. heat() отображает 0..1 в зелёный → жёлтый → красный — удобно для задержки / нагрузки. ```lua rtt:color(Wex.color.heat(ms / 300)) ``` ### Wex.csv.parse(text, sep?, header?) encode(rows, sep?) Wex.xml.parse(s) → { tag, attrs, children, text } find(node, tag) findAll(node, tag) Wex.gzip.compress(s) → base64 decompress(b64) CSV with quotes (header = true yields { col = value } rows), a plain XML tree, and gzip via base64. CSV с кавычками (header = true даёт строки { col = value }), простое XML-дерево и gzip через base64. ```lua local rows = Wex.csv.parse(Wex.files.read("hosts.csv"), ",", true) for _, r in ipairs(rows) do print(r.ip) end ``` ### Wex.time.parseIso(s) endOfDay(ms) add(ms, { days, hours, minutes, seconds, months, years }) diff(a, b) → { ms, seconds, minutes, hours, days } date(ms) → { year, month, day, hour, min, sec, weekday, yday } make({ year, month, day, … }) zone() relative(ms) uptime() More date/time helpers (os.date is unavailable in the sandbox). Больше хелперов даты/времени (os.date в песочнице недоступен). ```lua local tomorrow = Wex.time.add(Wex.time.startOfDay(), { days = 1 }) ``` ### Wex.crypto.totp(secretBase32, digits?, periodSec?, algo?) → { code, remaining } hotp(secret, counter, digits?) pbkdf2(pass, salt, iterations?, bytes?) randomHex(n) randomBase64(n) base32Encode/Decode Wex.hash.sha512 crc32 Wex.base64.urlEncode/urlDecode toHex fromHex Authenticator-grade TOTP/HOTP, key derivation, extra digests and base64 variants. TOTP/HOTP уровня аутентификатора, вывод ключей, дополнительные дайджесты и варианты base64. ```lua local t = Wex.crypto.totp(Wex.secure.get("otp_secret")) code:set(t.code); left:set(t.remaining .. "s") ``` --- ## Wex · system (extended) — Wex · система (расширенная) ### Wex.events.on(name, fn) once(name, fn) off(name, fn?) emit(name, …) names() In-plugin event bus — decouple screens, timers and network callbacks. emit() returns the number of listeners called. Шина событий внутри плагина — развязывает экраны, таймеры и сетевые колбэки. emit() возвращает число вызванных слушателей. ```lua Wex.events.on("host:found", function(ip) list:count(#hosts) end) Wex.events.emit("host:found", ip) ``` ### Wex.state.get(k, default?) set(k, v) update(k, fn(old)) toggle(k) increment(k, by?) watch(k, fn(v, k)) → id unwatch(id) all() remove(k) persist(k) restore(k, default?) Reactive state shared with widget handles' :bind(key). persist()/restore() mirror a key into Wex.storage. Реактивное состояние, общее с :bind(key) у хендлов. persist()/restore() зеркалят ключ в Wex.storage. ```lua Wex.state.restore("count", 0) ui:stat("Count", "0"):bind("count") ui:button("+1", function() Wex.state.increment("count"); Wex.state.persist("count") end) ``` ### Wex.bus.emit(topic, data) on(topic, fn) → id off(id) set(k, v) get(k, default?) keys() 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). ```lua Wex.bus.on("scan/done", function(m) ui:toast("found " .. m.count) end) Wex.bus.emit("scan/done", { count = 3 }) Wex.bus.set("last_target", "10.0.0.1") ``` ### Wex.bus.request(topic, data, fn(reply, err), opts?) respond(topic, fn(data) → reply) once(topic, fn) 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, но не подписываться и не отвечать. ```lua -- service plugin Wex.bus.respond("geoip", function(ip) return { country = lookup(ip) } end) -- client plugin Wex.bus.request("geoip", "1.1.1.1", function(res, err) if res then ui:text(res.country) else ui:text("no answer: " .. err) end end) ``` ### Wex.bus.provide(name, { request, response, version }, impl(req)→resp) discover(name, range) call(name, range, req, fn(res, err)) services() 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, а сервис снимается автоматически при закрытии плагина-провайдера. Плагины переднего плана. ```lua -- 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 request if Wex.bus.discover("geo.reverse", ">=1.0 <2.0") then Wex.bus.call("geo.reverse", ">=1.0", { lat = 55.75, lon = 37.62 }, function(res, err) if res then label:set(res.name) else Wex.log("rpc failed: " .. err) end end) end ``` ### Wex.bus.state(ns) → { set(k, v), get(k, default?), delete(k), keys(), watch(pattern, fn(key, value)) } 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 друг друга. Плагины переднего плана. ```lua 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 watchers Wex.log(st:get("node/42").lat) ``` ### Wex.debounce(ms, fn) → fn Wex.throttle(ms, fn) → fn Wex.defer(fn) Wex.delay(ms, fn) Wex.every(ms, fn) Wex.cancelAllTimers() 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. ```lua 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 }. ```lua Wex.sensors.start("accelerometer", function(v) ball.x = ball.x - v.x; ball.y = ball.y + v.y end, 33) ``` ### 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). ```lua local id = Wex.sensors.orientation(function(o) needle:rotation(-o.azimuth) end, 100) ``` ### Wex.audio.beep(freqHz?, ms?, volume?) play(fileOrUrl, { loop, volume }?, onDone?) stop() pause() resume() isPlaying() volume(v) seek(ms) position() duration() Tone generator (no assets needed) and a media player for sandbox files or URLs (URLs need "network"). Генератор тона (без ассетов) и медиаплеер для файлов песочницы или URL (URL требуют "network"). ```lua if down then Wex.audio.beep(440, 300) end ``` ### Wex.audio.speak(text, lang?, rate?) stopSpeaking() isSpeaking() Text-to-speech via the system engine (language as a BCP-47 tag: "ru", "en-US"). Синтез речи системным движком (язык как BCP-47: "ru", "en-US"). ```lua Wex.audio.speak("Host is down", "en") ``` ### Wex.audio.listen({ lang, partial }?, fn({ text, final, confidence })) Wex.audio.stopListening() isListening() 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"}. ```lua Plugin.permissions = { "microphone" } Wex.audio.listen({ partial = true }, function(r) box:set(r.text) if r.final then Wex.audio.listen({ partial = true }, onSpeech) end -- keep listening end) ``` ### Wex.screen.keepOn(bool) orientation("portrait"|"landscape"|"sensor"|"locked"|"auto") fullscreen(bool) immersive(bool) isImmersive() lockScroll(bool) viewport() brightness(0..1 | nil) size() isDark() isLandscape() 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. ```lua Wex.screen.keepOn(true) Wex.screen.orientation("landscape") Wex.screen.immersive(true) -- полный экран с «челкой» local v = Wex.screen.viewport() -- { width, height, landscape, immersive, density } ``` ### Wex.app.exit() minimize() restart() openSettings(which) package version build Wex.app.launch(pkg) installed(pkg) info(pkg) list() 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. ```lua if not Wex.net.isOnline() then Wex.app.openSettings("wifi") end ``` ### Wex.open.url(u) map(query | lat, lon) dial(number) sms(number, text?) email(to, subject?, body?) search(q) settings(which) file(name, mime?) plugin(id, args?) User-completed system actions (the target app asks for confirmation) — no permission needed. Системные действия, завершаемые пользователем (целевое приложение спросит подтверждение) — без разрешений. ```lua Wex.open.map("Berlin Hauptbahnhof") Wex.open.email("admin@example.com", "Report", body) ``` ### Wex.notifications.show({ id, title, body, bigText, silent, ongoing, important, progress = 0..100 | -1, openPlugin, args, subText, group }) progress(id, title, pct, body?) cancel(id) cancelAll() enabled() Requires `Plugin.permissions = { "notifications" }` Richer notifications: expandable text, progress bars, high-importance channel, and tapping opens the plugin with `args` (Wex.args). Богаче уведомления: раскрывающийся текст, прогресс-бары, канал высокой важности, а тап открывает плагин с `args` (Wex.args). ```lua Wex.notifications.show({ id = 7, title = "Host down", body = ip, important = true, args = { host = ip } }) ``` ### Wex.alarm.set({ id, at, repeat, title, text, args }) → bool cancel(id) list() canSchedule() 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"}. (Пока будильники не переживают перезагрузку.) ```lua 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.contacts.query({ search, limit }?, fn(list)) 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"}. ```lua Plugin.permissions = { "contacts" } Wex.contacts.query({ search = "Ann", limit = 50 }, function(list) for _, c in ipairs(list) do Wex.log(c.name .. " — " .. (c.phones[1] or "no phone")) end end) ``` ### Wex.calendar.events({ from, to, limit }?, fn(list)) Wex.calendar.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"}. ```lua Plugin.permissions = { "calendar" } Wex.calendar.events({}, function(list) for _, e in ipairs(list) do Wex.log(os.date("%H:%M", e.start / 1000) .. " " .. e.title) end end) ``` ### Wex.media.start({ title, artist }) stop() isRunning() — in Plugin.media(): onCommand(fn(cmd,arg)) play/pause/resume/seek/stop/position/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. ```lua Plugin.permissions = { "background" } -- foreground UI: launch the player ui: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 = 1 Wex.media.onCommand(function(cmd) if cmd == "next" or cmd == "completed" then i = i % #queue + 1 Wex.media.setMetadata({ title = "Track " .. i }) Wex.media.play(queue[i]) end end) Wex.media.setMetadata({ title = "Track " .. i }) Wex.media.play(queue[i]) end ``` ### Wex.background.runNow() Wex.background.isHeadless Requires `Plugin.permissions = { "background" }` Trigger Plugin.background() once right now (for testing); in the headless run Wex.headless == true and `ui` is empty. Запустить Plugin.background() один раз прямо сейчас (для теста); в headless-запуске Wex.headless == true, а `ui` пустой. ### Wex.pickImage(fn(res)) Wex.pickBinary({ mimes }, fn(res)) Wex.saveBase64(name, mime, b64, fn(ok)) Requires `Plugin.permissions = { "files" }` Binary file dialogs: res = { name, mime, size, base64, dataUri } or { error }. dataUri feeds ui:image directly. Бинарные файловые диалоги: res = { name, mime, size, base64, dataUri } или { error }. dataUri сразу годится для ui:image. ```lua Wex.pickImage(function(r) if r.dataUri then img:set(r.dataUri) end end) ``` ### Wex.clipboard.copy(text, toastText?) has() clear() Wex.share(text, subject?) Wex.toast(text, tone?) Wex.vibrate(ms | { pattern }) copy() writes and shows a toast; toast tones: success | error | neutral; vibrate accepts an off/on pattern in ms. copy() записывает и показывает тост; тона тоста: success | error | neutral; vibrate принимает паттерн пауза/вибрация в мс. ```lua Wex.clipboard.copy(ip, "IP copied") Wex.vibrate({ 0, 60, 80, 60 }) ``` ### Wex.log(…) .info .warn .error .dump(t) .clear() .lines() Wex.battery() → { level, charging, temperature, voltage, health, plugged } Wex.device.screen / isTablet / isDark / locale / timezone / abi / memory() / storage() Wex.theme.isDark / secondary / tertiary / outline / … Leveled logging (viewable in Plugins → long-press → Logs), richer battery/device/theme info. Уровни логирования (смотреть в Плагины → долгое нажатие → Логи), больше данных о батарее/устройстве/теме. ```lua Wex.log.dump(res.json) local mem = Wex.device.memory(); ui:kv("RAM free", Wex.format.bytes(mem.free)) ``` --- ## Wex · Bluetooth LE — Wex · Bluetooth LE ### Wex.ble.available() enabled() requestEnable() permission(fn(ok)) hasPermission() bonded() uuid(short) Requires `Plugin.permissions = { "bluetooth" }` 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" принимаются везде и разворачиваются в полный. ```lua if not Wex.ble.enabled() then Wex.ble.requestEnable() end Wex.ble.permission(function(ok) if ok then startScan() end end) ``` ### Wex.ble.scan({ services?, name?, address?, minRssi?, mode?, timeoutMs?, repeat?, onDevice = fn(d), onDone = fn(list) }) stopScan() isScanning() Requires `Plugin.permissions = { "bluetooth" }` 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 получает всех увиденных. ```lua Wex.ble.scan({ services = { "180d" }, timeoutMs = 8000, onDevice = function(d) list:append({ title = d.name, subtitle = d.address .. " " .. d.rssi .. " dBm" }) end, onDone = function(all) ui:toast(#all .. " devices") end }) ``` ### local dev = Wex.ble.connect(address, { onConnect = fn(dev), onServices = fn(services), onDisconnect = fn(reason), autoConnect?, timeoutMs? }) Requires `Plugin.permissions = { "bluetooth" }` 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)). ```lua local dev = Wex.ble.connect(addr, { onServices = function(list) dev:notify("180d", "2a37", function(r) hr:set(r.bytes[2] .. " bpm") end) end, onDisconnect = function(why) ui:toast("disconnected: " .. why) end, }) ``` ### dev:read(svc, chr, fn(res)) dev:write(svc, chr, data, { noResponse }?, fn(ok, status)) dev:notify(svc, chr, fn(res), fn(ok)?) dev:unnotify(svc, chr) Requires `Plugin.permissions = { "bluetooth" }` 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. ```lua dev:read("180f", "2a19", function(r) if r.ok then batt:set(r.bytes[1] .. "%") end end) dev:write("ffe0", "ffe1", { hex = "a55a0001" }, { noResponse = true }) ``` ### Wex.ble.advertise({ ibeacon = { uuid, major, minor, txPower? } | altbeacon = { uuid, major, minor } | manufacturer = { id, hex } | serviceData = { uuid, hex } | uuid | services, name?, mode?, power?, connectable?, timeoutMs?, onStart = fn(ok, err) }) stopAdvertise() isAdvertising() Requires `Plugin.permissions = { "bluetooth" }` 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 байт: имя устройства рядом с маячком обычно не влезает. ```lua 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 }) ``` --- ## Wex · Infrared — Wex · Инфракрасный порт ### Wex.ir.available() frequencies() info() isBusy() stop() Requires `Plugin.permissions = { "infrared" }` 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 кГц подходит почти для всего. ```lua if not Wex.ir.available() then ui:empty("info", "No IR blaster", "This phone has no infrared emitter") return end ``` ### Wex.ir.send({ protocol, address, command }, fn(ok, err)) Wex.ir.encode(protocol, spec) Requires `Plugin.permissions = { "infrared" }` 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-потока и длится столько же, сколько сам посыл. ```lua Wex.ir.send({ protocol = "nec", address = 0x04, command = 0x08 }, function(ok, err) ui:toast(ok and "sent" or err) end) ``` ### Wex.ir.transmit(freqHz, pattern, fn(ok, err)) Wex.ir.resend(spec, times, gapMs, fn) Requires `Plugin.permissions = { "infrared" }` 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. ```lua Wex.ir.transmit(38000, { 9000, 4500, 560, 560, 560, 1690, 560 }) Wex.ir.resend({ protocol = "nec", address = 0x04, command = 0x08 }, 3, 40) ``` ### Wex.ir.pronto(hex) Wex.ir.sendPronto(hex, times, fn(ok, err)) Requires `Plugin.permissions = { "infrared" }` 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() играет вступительный кадр, а затем повторный, как и задумано форматом. ```lua Wex.ir.sendPronto("0000 006D 0022 0002 0157 00AC 0016 0016", 2) ``` --- ## Wex · NFC — Wex · NFC ### Wex.nfc.available() enabled() openSettings() isListening() last() info() Requires `Plugin.permissions = { "nfc" }` 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. Считыватель работает, только пока экран плагина на переднем плане; хост выключает его при уходе с экрана и включает обратно при возврате, так что плагину об этом думать не нужно. ```lua if not Wex.nfc.enabled() then Wex.nfc.openSettings() end ``` ### Wex.nfc.listen(fn(tag), opts?) Wex.nfc.stop() Requires `Plugin.permissions = { "nfc" }` 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 }. ```lua Wex.nfc.listen(function(tag) ui:kv("UID", tag.id) if tag.text then ui:text(tag.text) end end) ``` ### tag:write(spec, fn(ok, err)) tag:read(fn(res)) tag:erase(fn) tag:makeReadOnly(fn) Requires `Plugin.permissions = { "nfc" }` 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 после записи блокирует метку навсегда. Неформатированная метка форматируется при первой записи. ```lua Wex.nfc.listen(function(tag) tag:write({ uri = "https://example.com" }, function(ok, err) ui:toast(ok and "written" or err) end) end) ``` ### tag:transceive(hex, fn(res)) tag:ultralightRead/Write tag:classicRead/Write Requires `Plugin.permissions = { "nfc" }` 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 } при ошибке. ```lua tag:classicRead(0, 1, { key = "FFFFFFFFFFFF" }, function(r) if r.ok then ui:code(r.hex) else ui:toast(r.error) end end) ``` ### 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-считыватель для проверки. --- ## Wex · USB serial — Wex · USB-serial ### Wex.usb.available() devices() info(vid, pid) hasPermission(spec) request(spec, fn(ok)) drivers() Requires `Plugin.permissions = { "usb" }` 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 спрашивает отдельно при первом открытии. ```lua for _, d in ipairs(Wex.usb.devices()) do ui:kv(d.product, d.vendorHex .. ":" .. d.productHex .. " " .. d.driver) end ``` ### local port = Wex.usb.open({ vendorId, productId, baud, dataBits, stopBits, parity, dtr, rts, onData, onLine, onOpen, onError, onClose }) Requires `Plugin.permissions = { "usb" }` 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 }. ```lua local port = Wex.usb.open({ baud = 115200, onLine = function(line) log:append(line) end }) port:writeLine("AT") ``` ### port:write(text) writeLine(text) writeHex(hex) writeBytes({…}) setBaud(n) dtr(bool) rts(bool) control(type, req, value, index, hex?, fn) isOpen() info() close() Requires `Plugin.permissions = { "usb" }` 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". ```lua Wex.usb.onDetach(function(d) ui:toast(d.product .. " unplugged") end) port:writeHex("A5 5A 00 01") ``` ### 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" }. --- ## Wex · Bluetooth Classic — Wex · Классический Bluetooth ### Wex.driver.register(name, { match, baud, open }) → bool Wex.driver.open(name, fn(device, err)) list() registered(name) 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. ```lua 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) if not dev then return Wex.toast("no radio: " .. (err or "")) end dev.setFreq(433.92) dev.rx(function(frame) Wex.bus.emit("rf.frame", frame) end) end) ``` ### Wex.bt.available() enabled() requestEnable() name() address() bonded() isDiscovering() Requires `Plugin.permissions = { "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. ```lua for _, d in ipairs(Wex.bt.bonded()) do ui:kv(d.name, d.address) end ``` ### Wex.bt.discover({ timeoutMs, onDevice = fn(d), onDone = fn(list) }) stopDiscovery() pair(address, fn(ok)) Requires `Plugin.permissions = { "bluetooth" }` 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. ```lua Wex.bt.discover({ timeoutMs = 12000, onDevice = function(d) list:append({ title = d.name, subtitle = d.address }) end }) ``` ### local link = Wex.bt.connect(address, { uuid, secure, onConnect, onData, onLine, onDisconnect }) Requires `Plugin.permissions = { "bluetooth" }` 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(). ```lua local link = Wex.bt.connect(addr, { onLine = function(line) term:append(line) end, onDisconnect = function(why) ui:toast(why) end, }) link:writeLine("AT+NAME?") ``` --- ## Wex · Wi-Fi — Wex · Wi-Fi ### Wex.wifi.available() enabled() openSettings() panel() info() Requires `Plugin.permissions = { "wifi" }` 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() открывает системную панель, где это делает сам пользователь, не уходя с вашего экрана. ```lua if not Wex.wifi.enabled() then Wex.wifi.panel() end ``` ### Wex.wifi.scan(fn(list, note)) results() channels() Requires `Plugin.permissions = { "wifi" }` 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". ```lua Wex.wifi.scan(function(list) for _, n in ipairs(list) do ui:kv(n.ssid ~= "" and n.ssid or "(hidden)", n.rssi .. " dBm ch " .. n.channel .. " " .. n.security) end end) ``` ### Wex.wifi.connection() Wex.wifi.ip() Requires `Plugin.permissions = { "wifi" }` What this phone is attached to right now: { ssid, bssid, rssi, level, freq, channel, band, linkSpeed, ip, standard }, or nil when it is not on Wi-Fi. К чему подключён телефон прямо сейчас: { ssid, bssid, rssi, level, freq, channel, band, linkSpeed, ip, standard } или nil, если Wi-Fi не подключён. ```lua local c = Wex.wifi.connection() if c then ui:kv(c.ssid, c.ip .. " " .. c.linkSpeed .. " Mbps") end ``` --- ## Wex · Location (GPS) — Wex · Геолокация (GPS) ### Wex.location.start({ interval, distance }, fn(fix)) 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" }. ### Wex.location.once(fn(fix)) Wex.location.last() → fix|nil 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. ### Wex.location.onNmea(fn(sentence)) The raw NMEA 0183 stream from the GNSS chip — $GPGGA, $GPRMC and friends — for parsing DOP, fix quality and anything the cooked fix drops. Сырой поток NMEA 0183 от GNSS-чипа — $GPGGA, $GPRMC и прочие — для разбора DOP, качества fix и всего, что теряет готовый fix. ### Wex.location.satellites(fn({ count, satellites })) 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/…). Для карт неба и графиков уровня сигнала. ### Wex.location.available() → bool Wex.location.hasPermission() → bool Wex.location.stop() available() is true when a provider is enabled; hasPermission() checks the OS grant; stop() ends every stream (updates, NMEA, satellites). available() истинно, когда провайдер включён; hasPermission() проверяет разрешение ОС; stop() завершает все потоки (обновления, NMEA, спутники). --- ## Wex · Crypto & Keys — Wex · Криптография и ключи ### Wex.crypto.hash(algo, data, opts?) → string (algo md5|sha1|sha256|sha384|sha512) 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). Используйте для отпечатков файлов, построения ключей кеша или проверки скачанного содержимого по известной контрольной сумме. ```lua local digest = Wex.crypto.hash("sha256", "hello world") Wex.log("sha256 = " .. digest) local b64 = Wex.crypto.hash("sha512", payload, { input = "utf8", encoding = "base64" }) Wex.log("checksum: " .. b64) ``` ### Wex.crypto.hmac(algo, message, key, opts?) → string (opts.input/keyEncoding/encoding) 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 и сравниваете её со значением из заголовка отправителя. ```lua local sig = Wex.crypto.hmac("sha256", body, secret, { encoding = "base64" }) if sig == header_signature then Wex.log("webhook signature ok") else Wex.log("rejected: bad signature") end ``` ### Wex.crypto.aes.genKey(bits?) → base64 (bits 128|192|256, default 256) 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. Вызывайте один раз для каждого шифрованного хранилища и повторно используйте полученную строку. ```lua local key = Wex.crypto.aes.genKey(256) Wex.log("new AES-256 key (base64): " .. key) ``` ### Wex.crypto.aes.encrypt(plaintext, key, opts?) → base64(iv||ct||tag) ; Wex.crypto.aes.decrypt(blob, key, opts?) → plaintext|nil 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(). ```lua 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") ``` ### Wex.crypto.seal(plaintext, password) → blob ; Wex.crypto.open(blob, password) → plaintext|nil 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 и тег упаковываются автоматически, поэтому вам не нужно работать с сырым ключевым материалом — удобно для быстрых защищённых паролем фрагментов, например зашифрованной заметки или экспорта настроек. ```lua local box = Wex.crypto.seal("diary entry", "hunter2") local text = Wex.crypto.open(box, "hunter2") Wex.log(text or "wrong password") ``` ### Wex.crypto.pbkdf2(password, salt, iters?, dkLen?, algo?) → hex (algo sha1|sha256|sha512, default sha1) 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 или сохранить медленный проверочный хеш пароля с солью. ```lua local salt = Wex.crypto.hash("sha256", "user@example.com") local dk = Wex.crypto.pbkdf2("hunter2", salt, 100000, 32, "sha256") Wex.log("derived key: " .. dk) ``` ### Wex.crypto.keygen(algo, opts?) → { algo, publicKey, privateKey } ("rsa" opts.bits=2048|3072|4096 | "ec" opts.curve=P-256|P-384) 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(); храните приватный ключ в секрете и распространяйте только публичную часть. ```lua local kp = Wex.crypto.keygen("ec", { curve = "P-256" }) Wex.log("algo: " .. kp.algo) Wex.log("public key: " .. kp.publicKey) ``` ### Wex.crypto.sign(algo, privateKey, message) → base64 ; Wex.crypto.verify(algo, publicKey, message, signature) → bool 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(). Используйте для подтверждения авторства манифеста обновления или аутентификации сообщений между двумя сторонами. ```lua 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") ``` ### Wex.crypto.keystore.encrypt(name, plaintext) → blob ; Wex.crypto.keystore.decrypt(name, blob) → plaintext|nil 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, если блоб неверен или ключ отсутствует. Это самое безопасное место для токенов или учётных данных, которые должны переживать перезапуски. ```lua local blob = Wex.crypto.keystore.encrypt("token", "ya29.a0secret") -- key never leaves the device; hardware-backed where available local token = Wex.crypto.keystore.decrypt("token", blob) Wex.log(token or "decrypt failed") ``` ### Wex.crypto.keystore.keygen(name) → { publicKey } ; .sign(name, message) → base64 ; .has(name) ; .delete(name) ; .info(name) → { exists, hardwareBacked, algo } 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, скомпрометированный плагин может запросить подпись, но не может извлечь ключ. Используйте для аттестации устройства или подписи запросов на уровне устройства. ```lua if not Wex.crypto.keystore.has("signing") then local pub = Wex.crypto.keystore.keygen("signing") Wex.log("device pubkey: " .. pub.publicKey) end local 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") ``` ### Wex.jwt.sign(claims, key, opts?) → token (opts.alg HS256|HS384|HS512|RS256|ES256, opts.expiresIn seconds) 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 на указанное число секунд в будущее. Возвращает компактную строку токена. Используйте для выпуска короткоживущих токенов к собственному бэкенду. ```lua local claims = { sub = "user-7", role = "admin" } local token = Wex.jwt.sign(claims, secret, { alg = "HS256", expiresIn = 3600 }) Wex.log("token: " .. token) ``` ### Wex.jwt.verify(token, key, opts) → { ok, claims } | { ok=false, error } (opts.alg REQUIRED comma-separated allowlist) 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, когда это нужно. ```lua local res = Wex.jwt.verify(token, secret, { alg = "HS256" }) if res.ok then Wex.log("hello " .. res.claims.sub) else Wex.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() со списком разрешённых алгоритмов. Пригодится для логирования или маршрутизации по содержимому токена. ```lua local t = Wex.jwt.decode(token) Wex.log("alg = " .. t.header.alg) Wex.log("sub = " .. t.payload.sub) ``` ## Wex · Codecs & Serialization — Wex · Кодеки и сериализация ### Wex.codec.base58.encode(bytes) → str / Wex.codec.base58.decode(str) → bytes 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) восстанавливает исходные байты и выдаёт ошибку при символах вне алфавита. Применяйте для представления ключевого материала, адресов в стиле кошельков или коротких идентификаторов в безопасном для копирования виде. ```lua 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.encode(bytes) → str / Wex.codec.base85.decode(str) → bytes 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) — исходные байты, выдавая ошибку при некорректном вводе. Используйте, когда нужно более компактное текстовое кодирование для встраивания бинарного блока в логи или конфигурацию. ```lua 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. ```lua 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. ```lua 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(bytes, opts?) → bytes / Wex.codec.inflate(bytes, opts?) → bytes 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 на обеих сторонах. ```lua 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(bytes) → bytes / Wex.codec.gunzip(bytes) → bytes 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-ответа. ```lua local gz = Wex.codec.gzip(logText) Wex.log("gz size: " .. #gz) local plain = Wex.codec.gunzip(gz) ui:text(plain) ``` ### Wex.codec.hexdump(bytes, opts?) → string 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 — начальный адрес, отображаемый в левом столбце. Функция возвращает готовую к выводу многострочную строку. Применяйте для просмотра точного содержимого бинарного буфера, например декодированного пакета или заголовка файла. ```lua local dump = Wex.codec.hexdump(header, { width = 8, offset = 0 }) Wex.log(dump) ui:text(dump) ``` ### Wex.codec.varint.encode(n) → bytes / Wex.codec.varint.decode(bytes, pos?) → value, bytesConsumed 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) и возвращает два значения: декодированное число и количество использованных байтов. Применяйте для построения или разбора компактных форматов с кадрами, где малые числа должны занимать мало байтов. ```lua 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 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 сохраняются при сериализации. ```lua 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(name, fields) → schema ; schema:encode(table) → bytes ; schema:decode(bytes) → table 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 с реальным сервисом. ```lua 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(bytes) → [{ field, wireType, value }] ; Wex.proto.hex(bytes) → str ; Wex.proto.fromHex(str) → bytes 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 поле за полем. ```lua local bytes = Wex.proto.fromHex("089601120361646d") for _, f in ipairs(Wex.proto.raw.decode(bytes)) do Wex.log("field " .. f.field .. " wt=" .. f.wireType) end Wex.log("hex: " .. Wex.proto.hex(bytes)) ``` ## Wex · Signal & DSP v2 — Wex · Сигналы и DSP v2 ### Wex.dsp.biquad(kind, opts) → filter ; filter:process(samples) → samples ; filter:reset() 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 перед анализом. ```lua local lp = Wex.dsp.biquad("lowpass", { freq = 3000, q = 0.707, sampleRate = 44100 }) local pcm = Wex.audio.record(0.5) local clean = lp:process(pcm) Wex.log("filtered " .. #clean .. " samples") lp:reset() ``` ### Wex.dsp.goertzel(samples, targetFreq, sampleRate) → magnitude 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 или фиксированной пилотной частоты. Сравните возвращённую магнитуду с порогом, чтобы решить, присутствует ли тон. ```lua local pcm = Wex.audio.record(0.1) local mag = Wex.dsp.goertzel(pcm, 941, 8000) if mag > 0.5 then Wex.log("row tone 941 Hz present") end ``` ### Wex.dsp.resample(samples, fromRate, toRate) → samples 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. Линейная интерполяция сохраняет скорость и работу без разрешений ценой некоторой потери точности на высоких частотах. ```lua local pcm = Wex.audio.record(0.5) -- 44100 Hz local down = Wex.dsp.resample(pcm, 44100, 8000) Wex.log(#pcm .. " -> " .. #down .. " samples") ``` ### Wex.dsp.envelope(samples, opts?) → samples 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 или запуска действия, когда громкость пересекает порог. ```lua local pcm = Wex.audio.record(1.0) local env = Wex.dsp.envelope(pcm, { attack = 0.01, release = 0.2, sampleRate = 44100 }) local peak = 0 for _, v in ipairs(env) do if v > peak then peak = v end end Wex.log("peak level " .. peak) ``` ### Wex.dsp.movingAvg(samples, window) → samples ; Wex.dsp.median(samples, window) → samples 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 — чтобы убрать треск из записи перед детектированием огибающей или тона. ```lua 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) выявляет периодичность, что удобно для оценки высоты тона. ```lua 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, 0 for i, v in ipairs(xcorr) do if v > peak then peak, at = v, i end end Wex.log("conv len " .. #echoed .. ", best lag at " .. at) ``` ## Wex · Reactive Streams — Wex · Реактивные потоки ### Wex.stream.interval(ms) → stream 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. ```lua local h = Wex.stream.interval(1000) :take(5) :subscribe(function(n) Wex.log("tick " .. n) end) ``` ### Wex.stream.of(...) → stream 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. Создаёт поток, который при каждой подписке синхронно испускает каждый аргумент по порядку и завершается. Удобно для инициализации конвейера фиксированными значениями, тестирования операторов или превращения небольшого известного набора в поток. Так как поток холодный, те же значения воспроизводятся для каждого нового подписчика. ```lua Wex.stream.of("a", "b", "c") :map(string.upper) :subscribe(function(v) Wex.log(v) end) ``` ### Wex.stream.from(producer) → stream 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, чтобы гарантированно освобождать ресурсы. ```lua local s = Wex.stream.from(function(emit) local conn = net.connect("wss://feed") conn.onmsg = emit return function() conn:close() end end) s:subscribe(function(msg) Wex.log(msg) end) ``` ### Wex.stream.fromState(key) → stream 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 по одному ключу не зациклится. Идеально для поддержания производных значений в синхронизации с источником истины. ```lua Wex.stream.fromState("query") :debounce(300) :filter(function(q) return #q >= 2 end) :subscribe(function(q) Wex.log("search: " .. q) end) ``` ### Wex.stream.merge(a, b, ...) → stream 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 объединённого потока. Используйте, чтобы свести несколько источников событий (нажатия, таймеры, сокеты) в один обработчик. ```lua 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) ``` ### Wex.stream.combine(a, b, fn) → stream 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) при каждом испускании любого из входных потоков, но только после того, как оба выдали хотя бы одно значение, давая текущую комбинацию двух источников. Это основной выбор для вычисления значения, зависящего от нескольких живых входов, например объединения положения ползунка с флагом режима. Каждое испускание несёт самое свежее значение с каждой стороны. ```lua 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) ``` ### stream:map(fn) → stream 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 выполняется только при наличии активного подписчика. ```lua Wex.stream.of(1, 2, 3) :map(function(n) return n * n end) :subscribe(function(sq) Wex.log(sq) end) ``` ### stream:filter(fn) → stream 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, молча отбрасывая остальные. Используйте, чтобы фильтровать поток по предикату, например игнорировать пустой ввод или события ниже порога, перед дальнейшей работой. Предикат выполняется для каждого значения и не изменяет прошедшие значения. ```lua Wex.stream.interval(500) :filter(function(n) return n % 2 == 0 end) :subscribe(function(n) Wex.log("even " .. n) end) ``` ### stream:scan(fn, seed) → stream 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 — это начальное значение аккумулятора до первого испускания. ```lua Wex.stream.of(10, 20, 30) :scan(function(sum, x) return sum + x end, 0) :subscribe(function(total) Wex.log("sum " .. total) end) ``` ### stream:distinct() → stream 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. Подавляет подряд идущие дубликаты, испуская значение только когда оно отличается от непосредственно предыдущего. Это полезно для устранения лишней работы, когда источник повторно испускает одно и то же значение, например неизменное состояние или повторяющиеся показания датчика. Обратите внимание: сравнение идёт с предыдущим испусканием, а не со всей историей. ```lua Wex.stream.fromState("status") :distinct() :subscribe(function(s) Wex.log("status -> " .. s) end) ``` ### stream:debounce(ms) → stream 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 миллисекунд без поступления нового значения, схлопывая быстрые всплески в одно завершающее испускание. Это классический выбор для поиска по мере ввода или обработки изменения размера, когда действовать нужно только после того, как активность утихнет. Каждое новое значение перезапускает таймер тишины. ```lua Wex.stream.fromState("input") :debounce(400) :subscribe(function(text) Wex.log("committed: " .. text) end) ``` ### stream:throttle(ms) → stream 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 он сохраняет первое значение каждого всплеска, что подходит для высокочастотных источников вроде прокрутки или движения указателя, где достаточно периодической выборки. Он гарантирует ограниченный поток вниз по конвейеру. ```lua Wex.stream.fromState("scroll") :throttle(100) :subscribe(function(y) ui:text("y=" .. y) end) ``` ### stream:buffer(n) → stream 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, испуская каждую заполненную партию как одну таблицу и начиная новый буфер. Используйте для обработки элементов фиксированными порциями, например пакетной сетевой записи или отрисовки точек группами. Значения удерживаются, пока буфер не заполнится, и только затем испускаются. ```lua Wex.stream.interval(200) :buffer(5) :subscribe(function(batch) Wex.log("got " .. #batch .. " items") end) ``` ### stream:sample(ms) → stream 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 миллисекунд, независимо от того, как быстро или медленно испускает сам источник. Это отвязывает всплесковый или нерегулярный источник от устойчивого темпа ниже по конвейеру, что удобно для обновления дисплея с фиксированной частотой кадров. Каждый такт передаёт то значение, которое оказалось последним. ```lua Wex.stream.fromState("cursor") :sample(1000) :subscribe(function(pos) Wex.log("pos " .. pos) end) ``` ### stream:take(n) → stream 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 испусканий конвейер аккуратно демонтируется. ```lua Wex.stream.interval(1000) :take(3) :subscribe(function(n) Wex.log("only " .. n) end) ``` ### stream:subscribe(fn) → handle 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 плагина. ```lua local h = Wex.stream.interval(1000) :subscribe(function(n) Wex.log(n) end) -- later h:stop() ``` ### stream:toState(key) → handle 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) по одному ключу безопасен и не рекурсирует. Используйте, чтобы непрерывно поддерживать производную запись состояния в актуальном виде. ```lua local h = Wex.stream.fromState("celsius") :map(function(c) return c * 9 / 5 + 32 end) :toState("fahrenheit") ``` ## Wex · Geo & Discovery — Wex · Гео и обнаружение ### Wex.geo.distance(lat1, lon1, lat2, lon2) → meters (haversine) 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 по формуле гаверсинуса и возвращает результат в метрах. Это чистая функция, не требующая разрешений, поэтому её безопасно вызывать в плотных циклах для проверки близости или оценки длины маршрута. Пары широта/долгота передаются в десятичных градусах; типичное применение — определить, попадает ли объект в заданный радиус. ```lua 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 < 1000 then ui:text("You are within 1 km") end ``` ### Wex.geo.bearing(lat1, lon1, lat2, lon2) → degrees 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 для навигационных интерфейсов или для наведения стрелки на цель. Координаты задаются в десятичных градусах. ```lua 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, функция чистая и не требует разрешений. Используйте её, чтобы спроецировать точку впереди пользователя или нарисовать кольцо дальности на карте. ```lua 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) ``` ### Wex.geo.geohash.encode(lat, lon, precision?) → string | Wex.geo.geohash.decode(hash) → { lat, lon, bounds={latMin,latMax,lonMin,lonMax} } 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. Обе функции чистые и не требуют разрешений. Геохеши удобны для группировки точек по ячейкам сетки или как короткие пространственные ключи в кэше. ```lua 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» и работает вне главного потока, поэтому читайте результаты только внутри колбэка. Применяется, чтобы превратить введённый пользователем текст поиска в метку на карте. ```lua Wex.geo.geocode("Red Square, Moscow", function(results) if #results == 0 then return end local top = results[1] Wex.log(top.name .. ": " .. top.lat .. ", " .. top.lon) ui:text(top.name) end, 5) ``` ### 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» и передаёт результат через колбэк. Типичное применение — подписать текущее положение пользователя или точку, по которой он нажал на карте. ```lua Wex.geo.reverse(55.7539, 37.6208, function(loc) ui:text(loc.address) Wex.log(loc.city .. ", " .. loc.country .. " " .. loc.postalCode) end) ``` ### Wex.nsd.available() → bool 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, чтобы плагин корректно завершался на неподдерживаемых устройствах. Используйте её, чтобы включать функции локальной сети по результату проверки. ```lua if not Wex.nsd.available() then ui:text("Service discovery not supported") return end Wex.log("NSD ready") ``` ### Wex.nsd.discover(serviceType, { onFound, onLost, onResolved }) → handle (handle:stop()) 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() по завершении, чтобы сканирование не продолжало работать в фоне. ```lua local h = Wex.nsd.discover("_http._tcp", { onFound = function(s) Wex.log("found " .. s.name) end, onResolved = function(s) ui:text(s.host .. ":" .. s.port) end, onLost = function(s) Wex.log("lost " .. s.name) end, }) -- later: h: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. ```lua 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() ``` ### Wex.nsd.resolve(service, cb) → cb({ name, type, host, port, txt }) 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-запрос. ```lua Wex.nsd.resolve(service, function(s) Wex.log(s.name .. " at " .. s.host .. ":" .. s.port) ui:text("Connect to " .. s.host) end) ``` ## Wex · Telephony & Health — Wex · Телефония и датчики тела ### Wex.telephony.available() → bool 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 корректно пропускали интерфейс со статусом телефона. ```lua if not Wex.telephony.available() then Wex.log("no telephony on this device") return end local i = Wex.telephony.info() Wex.log("carrier: " .. i.carrier) ``` ### Wex.telephony.request(cb) → cb(granted) 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». Запрашивайте разрешение лениво, прямо перед тем как оно понадобится, чтобы пользователю было понятно, зачем появляется диалог. ```lua Wex.telephony.request(function(granted) if granted then Wex.log("net: " .. Wex.telephony.info().networkType) else Wex.log("phone state denied") end end) ``` ### Wex.telephony.info() → { carrier, mcc, mnc, networkType, phoneType, simState, simCountry, roaming } 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 и не читает контакты. Удобно показывать пользователю его текущий тип сети или предупреждать о включённом роуминге. ```lua local i = Wex.telephony.info() ui:text("Carrier: " .. i.carrier) ui:text("Network: " .. i.networkType) if i.roaming then ui:text("Roaming ON (" .. i.simCountry .. ")") end ``` ### Wex.telephony.signal(cb) → handle (handle:stop()) 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() при закрытии экрана, чтобы освободить слушатель в песочнице. ```lua local h = Wex.telephony.signal(function(s) ui:text(s.dbm .. " dBm (level " .. s.level .. ")") end) -- later, when the view closes: h:stop() ``` ### Wex.telephony.cells() → [{ type, cid, tac/lac, mcc, mnc, rssi, level, registered }] 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) вышку. ```lua for _, c in ipairs(Wex.telephony.cells()) do local tag = c.registered and "*" or " " Wex.log(tag .. c.type .. " cid=" .. c.cid .. " " .. c.rssi .. "dBm") end ``` ### Wex.health.available(type?) → bool 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» и существует хотя бы один датчик; с типом проверяет наличие именно этого датчика. Вызывайте его первым, чтобы плагин корректно работал на устройствах без датчика пульса или шагомера. ```lua if Wex.health.available("heartRate") then Wex.health.heartRate(function(r) ui:text(r.bpm .. " bpm") end) else ui:text("no heart-rate sensor") end ``` ### Wex.health.heartRate(cb) → cb({ bpm, accuracy }) 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(). ```lua Wex.health.heartRate(function(r) if r.accuracy >= 2 then ui:text("Pulse: " .. r.bpm .. " bpm") end end) -- on teardown: Wex.health.stopAll() ``` ### Wex.health.steps(cb) → id 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» и работает только на чтение, сообщая лишь число шагов без данных о местоположении или активности. Используйте его для живого обновления индикатора цели по шагам. ```lua local id = Wex.health.steps(function(s) ui:text("Steps: " .. s.count) end) -- stop later: Wex.health.stop(id) ``` ### Wex.health.highRate(type, cb, hz?) → 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 умеренным, чтобы экономить батарею. ```lua 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 > 25 then Wex.log("shake!") end end, 50) Wex.health.stop(id) ``` ### Wex.health.stop(id) / Wex.health.stopAll() 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. ```lua 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 v2 — Wex · Железо v2 (серверы и сессии) ### Wex.ble.server(spec, cb) → handle 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 вызывается один раз на чтение (кэш для длинных чтений). ```lua local hr = 72 local 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 central Wex.every(1000, function() hr = 60 + math.random(0, 40); srv:notify("180d", "2a37", string.char(0, hr)) end) ``` ### Wex.ble.stopServer() · handle:close() 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. ```lua local srv = Wex.ble.server(spec, cb) -- ... later ... srv:close() -- or Wex.ble.stopServer() ``` ### Wex.bt.listen(spec) → serverHandle 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. ```lua 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) end end, }) -- ... later ... server:close() ``` ### tag:session(tech) → session · session:transceive(hex, cb) 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'. ```lua Wex.nfc.listen(function(tag) local s = tag:session("IsoDep") s:transceive("00A4040007A0000002471001", function(r) -- SELECT if r.ok then Wex.log("resp: " .. r.hex) s:transceive("00B0000000", function(r2) Wex.log(r2.hex); s:close() end) else s:close() end end) end) ``` ### Wex.wifi.p2p.discover({ onFound, onLost, onResolved }) → handle · Wex.wifi.p2p.peers() 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+) или геолокация. ```lua local scan = Wex.wifi.p2p.discover({ onFound = function(peers) for _, p in ipairs(peers) do Wex.log(p.name .. " @ " .. p.address) end end, }) -- ... later ... scan:stop() ``` ### Wex.wifi.p2p.connect(address, cb) · createGroup(cb) / removeGroup(cb) 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. ```lua 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) ``` ### Wex.wifi.p2p.info(cb) · Wex.wifi.p2p.watchInfo(cb) 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 и открыть к нему сокет. ```lua Wex.wifi.p2p.watchInfo(function(i) if i.groupFormed then Wex.log((i.isOwner and "owner" or "client") .. " · GO=" .. i.ownerAddress) end end) ``` ## Native code (.dex) — Нативный код (.dex) ### Ship compiled classes next to your Lua Requires `Plugin.permissions = { "native" }` 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. ```lua manifest.json: { "runtime": "dex", "dex": "native.dex", "classes": ["com.example.Tools"], "permissions": ["native"] } ``` ### There is no sandbox around native code 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 не загрузится. Проверяйте исходники чужого нативного плагина перед установкой. ### Wex.native.load(class, args…) → handle .static(class) → handle .call(class, method, args…) Requires `Plugin.permissions = { "native" }` 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-строка, остальное остаётся хендлом. ```lua 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, shizukuInstalled })) Wex.su.available(fn(ok)) request(fn(ok,msg)) exec(cmd, fn(out,err)) sysfs.read|write revoke() Wex.shizuku.available(fn(ok)) request(fn(granted)) exec(cmd, fn(out,err)) 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-запрос подтверждаете вы сами, и КАЖДАЯ выполненная плагином команда пишется в его лог как аудит. Плагины переднего плана. ```lua Plugin.permissions = { "privileged" } Wex.priv.available(function(p) -- async: p = { su = false, shizuku = true, shizukuInstalled = true } if not p.shizuku then return end Wex.shizuku.request(function(granted) if not granted then return end Wex.shizuku.exec("ip -o link show", function(out, err) if out then Wex.log(out) end end) end) end) -- root variant + direct sysfs read Wex.su.request(function(ok) if not ok then return end Wex.su.sysfs.read("/sys/class/power_supply/battery/temp", function(t, err) if t then Wex.log("batt temp " .. t) end end) end) ``` ### Callbacks: a Lua function where Java wants an interface Requires `Plugin.permissions = { "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-потоке. ```lua tools:runAsync(function(percent) prog:set(percent) end) ``` ### handle:methods() :fields() :class() :get(field) :set(field, v) :instanceOf(class) Wex.native.info() Requires `Plugin.permissions = { "native" }` 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, размер, загружен ли он и объявленные классы. ```lua for _, m in ipairs(tools:methods()) do print(m) end ``` ### ui:native(class, { height, card, radius, bg, … }) → h Requires `Plugin.permissions = { "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(). ```lua local v = ui:native("com.example.WaveView", { height = 200 }) v:setSpeed(2.5) ``` ### Building the .dex 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 (внешние библиотеки не подгружаются). ```lua javac --release 8 -cp $SDK/platforms/android-34/android.jar \ -d out com/example/*.java d8 --lib $SDK/platforms/android-34/android.jar \ --output . $(find out -name '*.class') mv classes.dex native.dex ``` --- ## Permissions — Разрешения ### All permission strings, in one place 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 разрешения не требует.) ```lua Plugin.permissions = { "network", "files", "clipboard", "native", "background", "intent", "notifications", "location", "microphone", "camera", "sensors", "bluetooth", "nfc", "usb", "infrared", "wifi", "overlay", "contacts", "calendar", } ``` ### "network" Wex.http, Wex.websocket, Wex.tcp, Wex.udp, Wex.net.dns/ping/tcp/ports, Wex.download, Wex.upload, ui:image(url), ui:web(url), Wex.audio.play(url). Wex.http, Wex.websocket, Wex.tcp, Wex.udp, Wex.net.dns/ping/tcp/ports, Wex.download, Wex.upload, ui:image(url), ui:web(url), Wex.audio.play(url). ### "files" Wex.pickFile, Wex.pickImage, Wex.pickBinary, Wex.saveFile, Wex.saveBase64, Wex.pickWexFile (the user's real files). Wex.pickFile, Wex.pickImage, Wex.pickBinary, Wex.saveFile, Wex.saveBase64, Wex.pickWexFile (настоящие файлы пользователя). ### "notifications" Wex.notifications. Wex.notifications. ### "background" Wex.background — periodic headless Plugin.background(). Wex.background — периодический headless Plugin.background(). ### "intent" Wex.intent, Wex.intent.query, Wex.app.launch/installed/info/list — launch or query other apps. Wex.intent, Wex.intent.query, Wex.app.launch/installed/info/list — запуск или поиск других приложений. ### "clipboard" Wex.clipboard.get — reading the system clipboard. Writing (set) is always allowed. Wex.clipboard.get — чтение системного буфера. Запись (set) разрешена всегда. ### "bluetooth" Wex.ble and Wex.bt — BLE scanning, GATT, advertising, classic discovery and RFCOMM. Android's own runtime permission dialog appears on first use. Wex.ble и Wex.bt — скан BLE, GATT, трансляция, классический поиск и RFCOMM. Системный диалог runtime-разрешения появится при первом использовании. ### "nfc" Wex.nfc — reading tags and writing to them. Granted at install; the reader only runs while the plugin screen is in the foreground. Wex.nfc — чтение меток и запись в них. Выдаётся при установке; считыватель работает, только пока экран плагина на переднем плане. ### "usb" Wex.usb — serial links to hardware in the USB port. Android asks about each device separately when it is opened. Wex.usb — последовательные каналы к железу в USB-порту. Про каждое устройство Android спрашивает отдельно при открытии. ### "infrared" Wex.ir — the infrared blaster. Granted at install; there is no system dialog. Wex.ir — инфракрасный передатчик. Выдаётся при установке, системного диалога нет. ### "wifi" Wex.wifi — the list of networks nearby and the current connection. Android's nearby-devices dialog appears on the first scan. Wex.wifi — список сетей рядом и текущее подключение. Системный диалог доступа к устройствам поблизости появится при первом скане. ### Risk check before install and before running 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), перед первым запуском рискованного плагина и по требованию из списка плагинов и со страницы на форуме. Она также помечает манифест, объявляющий меньше, чем использует код, закодированные блоки, код, собираемый на ходу, и данные, спрятанные в открываемую ссылку. Это отчёт о возможностях, а не приговор: ничего не блокируется, а нативный код проверить нельзя вовсе. ### "native" Wex.native, ui:native — loading the package's own .dex. Native code is NOT sandboxed: it runs with the app's permissions. Wex.native, ui:native — загрузка собственного .dex пакета. Нативный код НЕ в песочнице: он работает с правами приложения. ### always allowed storage, cache, secure, scoped files, db, json/base64/hash/crypto, str/url/regex/format/random/math/color/csv/time/gzip/xml, clipboard.set/copy, timers, events, state, sensors, audio & speech, screen, app (own), open.*, shortcut, plugins, all ui, dialogs, theme, device, api/has. storage, cache, secure, файлы-песочница, db, json/base64/hash/crypto, str/url/regex/format/random/math/color/csv/time/gzip/xml, clipboard.set/copy, таймеры, events, state, сенсоры, аудио и речь, screen, app (своё), open.*, shortcut, plugins, весь ui, диалоги, theme, device, api/has. ---