Lua plugins

Omadesign 0.5.8 embeds Lua 5.4 and plugin API 1. Plugins can make editable vector artwork, transform geometry, run pixel filters, set native effects and gradients, supply SVG icons and brush presets, add canvas tools and opt-in behaviors, install color palettes, and process folders of documents.

Install and run

Open Plugins → Manage plugins. Install a .lua file, a folder containing main.lua, or an .omaplug ZIP with main.lua at its root. Each plugin has an Enable checkbox. Updates keep a hidden backup of the previous installed folder. Use Reload after editing an installed plugin.

The release installs Studio starter on first installation. Its 12 actions cover all the categories above. Subsequent app updates preserve your installed copy; fresh source is in ~/.local/share/omadesign/plugin-examples/studio-starter. Download the starter bundle.

Select an action, enter its parameters, and choose Run. A document operation is one Undo step. Errors and cancellation leave the document unchanged. Editing or switching documents while a plugin runs causes its result to be discarded. A brush preset changes the active brush; a palette action saves to your personal color library. Those settings are separate from document Undo.

For Ribbon path, choose Activate tool, then drag on the canvas. Escape exits. The gesture previews as a line; the plugin generates editable output on release. A pixel filter uses the active raster. A page with none is baked first. Brush presets still need a raster layer. Select vector objects before a vector effect.

Document/selection behaviors are off by default. The manager’s behavior checkbox opts in to events from enabled plugins and remembers your choice. A behavior’s own output does not recursively trigger itself.

Your first plugin

Save this as main.lua, then install its folder:

return {
  api = 1,
  id = "org.example.color-dots",
  name = "Color dots",
  version = "1.0.0",
  description = "A small editable pattern generator.",
  actions = {{
    id = "dots", name = "Create dots", category = "Patterns",
    parameters = {
      {id="count", label="Count", default=8, min=1, max=40},
      {id="color", label="Color", kind="color", default="#89B4FA"},
    },
    run = function(ctx, p)
      for i=0,math.floor(p.count)-1 do
        oma.add_shape{
          kind="ellipse", x=32+i*24, y=ctx.height/2,
          width=12, height=12, fill=p.color, name="Dot",
        }
      end
      oma.message("Dots created. Every circle is editable.")
    end,
  }},
}

Use a unique stable id (letters, digits, dots, hyphens, underscores; no leading dot). Actions also need unique IDs. The version is your plugin’s version, independent of the app. Categories in the manager are Filters, Effects, Icons, Brushes, Tools, Behaviors, Batch, Patterns, Gradients, Swatches. Other category names are allowed and appear under All categories.

Parameters support number (default), text, color, and boolean, each with id, label, and a correctly typed default. Numbers optionally specify min and max. The host validates values for desktop and CLI runs. Colors use hex #RRGGBB or #RRGGBBAA. Unknown parameters are rejected.

Each invocation uses a fresh Lua VM. Keep persistent artwork in the document and preset definitions in your plugin; Lua globals do not persist between runs. Top-level code should only return the manifest and define functions. oma is available when an action runs, not during manifest discovery.

Context

run(ctx, params) receives:

FieldMeaning
ctx.apiHost API version, currently 1
ctx.width, ctx.height, ctx.nameDocument dimensions and name
ctx.active_layerZero-based active layer index, or nil
ctx.selectionArray of selected vector objects with layer, id, name, geom, style, rotation
ctx.layersArray of {index, name, locked, visible, raster}; raster is {width,height} or nil
ctx.gestureTool gesture or nil: {points={{x,y},...}, alt, shift, ctrl}

Lua arrays start at 1. Document layer indexes and pixel coordinates start at 0. Shape IDs are integers. Gesture points are in document coordinates. Geometry and fill tables use the native tagged JSON representation. Inspect an example with omadesign --inspect document.oma, or see the source types in src/geom.rs, src/document.rs, and src/filter.rs.

Host API

FunctionResult or behavior
oma.add_shape(options)Returns layer, id; adds a native shape to an editable vector layer, creating a layer when needed
oma.translate(layer, id, dx, dy)Moves a selected or identified vector shape
oma.set_geometry(layer, id, geom)Replaces native geometry, including editable compound paths
oma.remove(layer, id)Removes a vector shape
oma.set_fill(layer, id, fill)Hex color, "none", or native fill table
oma.gradient(colors, kind)Native multistop fill; kind is linear, radial, conic, or shape
oma.set_effects(layer, id, effects)Replaces that shape’s native effect stack
oma.color(hex)Native {r,g,b,a} color, channels 0–255
oma.brush(options)Activates a native Raster brush preset
oma.palette(name, colors)Adds a named palette to the personal library
oma.read_asset(relative_path)Reads a UTF-8 asset within this plugin’s folder
oma.svg(svg_text, x, y, width)Imports SVG paths as editable vector artwork, preserving aspect
oma.pixel(layer, x, y)Returns source r,g,b,a; out-of-bounds returns transparent black
oma.map_pixels(layer, callback)Replaces pixels using callback(r,g,b,a,x,y) → r,g,b,a. A non-raster layer falls back to the topmost visible raster. No raster bakes the visible page onto a new layer and filters that
oma.message(text)Shows a completion message in the app status bar

add_shape supports kind="rect", "ellipse", "line", "path", or "geometry". Common fields: x, y, width, height, fill, stroke, stroke_width, name, optional layer. Rectangles accept radius. Paths use points={{x,y},...} and optional closed=true. Geometry uses geom containing a native tagged geometry table. For example:

oma.add_shape{
  kind="geometry",
  geom={Path={closed=false, anchors={
    {pt={x=20,y=30}, h_in={x=0,y=0}, h_out={x=40,y=-20}, smooth=true, radius=0},
    {pt={x=120,y=60}, h_in={x=-40,y=20}, h_out={x=0,y=0}, smooth=true, radius=0},
  }}},
  fill="none", stroke="#A6E3A1", stroke_width=4,
}

Brush options: size (1–2048), hardness, opacity, flow (0–1), spacing (0.05–4), and color. A pack can expose multiple named actions with different presets. Palettes take an array of hex colors and persist as ordinary personal palettes. Gradients remain editable in the native gradient editor.

Effects use tagged native tables, for example:

for _, s in ipairs(ctx.selection) do
  oma.set_effects(s.layer, s.id, {
    {Shadow={dx=8,dy=8,blur=12,color=oma.color("#00000080")}},
    {Saturate={amount=0.8}},
  })
end

Supported effects: Blur {std}, Shadow/InnerShadow {dx,dy,blur,color}, Offset {dx,dy}, Morphology {erode,radius}, Saturate/Brightness/Contrast/Invert {amount}, HueRotate {degrees}, ColorMatrix {values} (20 numbers), Turbulence {fractal,base,octaves,seed}, Displacement {scale,x_ch,y_ch}. Stacks allow up to 32 effects. Blur is capped at 512, morphology radius at 64, offsets/displacement at 4096, turbulence at 8 octaves and base frequency ≤1.

Pixel callbacks use straight RGBA channels, 0–255. Return alpha explicitly to preserve transparency. oma.pixel reads the source image throughout a mapping pass, allowing neighborhood filters without feedback from already written pixels. Mapping all pixels is one Undo operation. Large or expensive filters can hit the execution limit; reduce input size or work per document in a batch.

SVG icon bundles should contain paths and presentation attributes. Local fragment references such as url(#gradient) are allowed. External references, stylesheets, and CSS escapes are rejected. Bundle fonts as outlined paths for icon artwork.

For a tool, set tool=true on the action and consume ctx.gesture.points. For a behavior set event="selection_changed" or event="document_opened". Normal actions need neither field.

Batch processing

Install/list without opening a window:

omadesign --install-plugin ./my-plugin
omadesign --list-plugins

Run one document, saving a new editable result (PNG/SVG exports also work):

omadesign --plugin ./my-plugin --command dots \
  --input input.oma --output output.oma \
  --params '{"count":12,"color":"#A6E3A1"}'

Process every immediate .oma file in a folder, in filename order:

omadesign --plugin ~/.local/share/omadesign/plugins/org.omadesign.studio-starter \
  --command nudge --batch ./input --output-dir ./output \
  --params '{"dx":20,"dy":0}'

CLI runs select every visible, unlocked nonguide vector shape and use the first unlocked layer as active. Inputs are preserved; existing output paths are refused. A failed document does not stop other files, but the final exit status is nonzero. Brush/palette actions belong in the desktop manager, and canvas tools require a gesture; these actions are not document batch commands.

Runtime limits and distribution

Plugins run on a background worker. They cannot execute programs, access the network, or read arbitrary files. Lua has table/string/math/utf8 and basic functions; io, os, package, debug, require, file loaders, binary chunks, pcall, xpcall, and coroutines are unavailable. Use assert or error to abort.

Limits: 15 seconds per run, 64 MiB Lua heap, 20,000 document edits, 256 MiB queued edit data, 128 MiB raster input, 2 MiB source, 4 MiB per asset/geometry, 128 actions, 24 parameters per action. Installed bundles allow 512 entries and 64 MiB total. Archives reject path traversal and symlinks. Plugin file access stays inside the installed bundle. Hidden/locked shapes and guides cannot be modified.

To distribute, ZIP the contents of your folder, including main.lua, assets, README and license, and name it your-plugin.omaplug. Users install it in the manager. There is no remote plugin marketplace in this release.

Contribute

Fork Omadesign, add a uniquely named folder under plugins/, and open a pull request. Include:

Keep manifest discovery fast and free of side effects. See plugins/studio-starter and src/plugins/tests.rs for complete examples and host regression coverage. Propose host API additions in an issue or PR; do not depend on unexposed app internals.