A modular object explorer, search engine, and action toolkit for grandMA3.
The main idea is "Spotlight"/"Raycast" for GrandMA3.
This is a broken example and poorly made plugin purely made by vibe-coding with claude. If anyone wants to pick this up, go ahead and fork it and update it.
This should NOT be used in a show-critical showfile.
- Elevated UI mode is not implemented well.
- The prism (symgrid) framework is being rebuilt from scratch — only
2d topexists so far. - Selections needs refinement.
RayMA grabs the keyboard the same way MA's own patch/editor windows do: the overlay
sets OverrideKeybSC="Yes", which suspends MA's keyboard shortcuts (the ShCuts icon
turns red) and frees the physical keys so they land in RayMA's capture field. Focus is
taken natively with FindBestFocus — no synthetic mouse clicks, and no global
KeyboardShortcutsActive toggle to restore. Shortcuts come back automatically the moment
the window closes.
- Version: 8.0
- Target firmware: grandMA3 2.3.x
- Runtime: Lua 5.4
RayMA turns the MA3 object tree into a fast, keyboard-driven workspace: fuzzy search across the show file, a tree browser, a property inspector, extensible quick actions, a command line, a calculator, a file browser, a persistent shell terminal, and a full plugin framework for building your own tools.
Launches straight into an empty search with the module list grouped by family (Navigate / Tools / Selection). Status line shows the live object index count in the showfiles datapoools — here, 2959 objects.
Typing macr fuzzy-matches across all data pools. Hits are ranked by relevance and show name, type, pool number, datapool, and a short content summary (2 lines, 1 lines). Selected row is highlighted green.
Typing anything will match to object type or name.
Tree navigator starting at Root. Each row shows index, name, class, and children count — press Enter to drill down, Right to inspect properties, Left/Esc to go back up.
Tap-tempo meter with running average and actions to apply the result to a Speed master, reset, or read the current master value. Enter taps, Esc exits.
Toy user module that fetches random cat facts from a public API — included as a live example of async HTTP work from within a RayMA module.
Three-reel slot machine that spins through a palette of named colors and applies the winner (here Teal) to the current selection. Demonstrates animated, stateful UI inside a user module.
Breakdown of a loaded show: total object count and memory use, then per-category rows (Patch — Fixtures / Parameters / Universes; Objects — Groups / Worlds / Filters). Enter drills into any category.
and more...
The plugin is designed to live at:
<ProgramData>/MALightingTechnology/gma3_library/datapools/plugins/RayMA/
- Fork the plugin.
- Git clone your repo to the plugin folder to the path above.
- In grandMA3, import
RayMA.xmlvia the Plugins pool. - Attach claude to the RayMA Folder
- Discuss with claude how to improve and implement features
- commit and push updates to your repo.
- Create pull requests to this main repo.
No external dependencies. The terminal module will opportunistically use LuaSocket if available, but falls back to io.popen.
When iterating, use scripts/deploy.py to copy the working tree into the local MA3
onPC plugin DataPool instead of copying files by hand:
python3 scripts/deploy.py # deploy to the default gma3_library location
python3 scripts/deploy.py --dry-run # preview what would be copied
MA3_PLUGINS_DIR=/some/path python3 scripts/deploy.py # override the target dir
It mirrors RayMA.xml, RayMA.lua, and the core/ modules/ prisms/ photons/ user_modules/ trees into
<ProgramData>/MALightingTechnology/gma3_library/datapools/plugins/RayMA/
(macOS: ~/MALightingTechnology/...), skipping .git/.DS_Store junk. After it runs,
reload the show (or restart MA3 onPC) to pick up the changes.
Invoke the plugin with an optional argument:
| Argument | Behavior |
|---|---|
| (none) | Open the search home screen (default) |
search |
Force search mode |
root |
Open the browser at Root() |
<ModuleName> |
Switch directly into a module (e.g. browser, cmd, math, terminal, bpm) |
<ModuleName>/seg1/seg2/... |
Open module with a route (passed to onRoute) |
#<ModuleName> |
Open the module in a tabbed elevated (full-workspace) UI |
ray <command> |
Headless: execute a Ray shorthand command without opening the UI |
debug |
Enable debug logging (combinable, e.g. debug root) |
Session state (last mode, last query) persists across invocations via GlobalVars.
- Compact – a floating overlay with a single-line search input, result list, and status bar. Default for keyboard-first workflows.
- Elevated – a full-screen tabbed workspace holding up to 5 concurrent module tabs, each with its own state. Enter via
#<ModuleName>or when opening a module markedelevatedOnly.
RayMA.lua Entry point, Main loop
core/
state.lua Global state singleton, color cache, registries
helpers.lua Object introspection helpers (name/class/address/children)
ui.lua Compact UI builder and renderer
keys.lua Key dispatch and signal wiring
elevated.lua Tabbed full-workspace UI
modules/ Built-in modules (XML-declared)
photons/ Value parsers (hex color) — XML-declared
prisms/ Symgrid layout algorithms (2d top) — XML-declared
user_modules/ Additional modules — XML-declared
Every component — core, modules, prisms, photons, and user modules — is declared as a
<ComponentLua> entry in RayMA.xml and loaded by MA3 in declaration order at
plugin-load time. core/state.lua is declared right after the entry point so its
registries and shared namespaces (RayMA.Prism, RayMA.Photon) exist before any
prism/photon/user module registers into them. Adding a new component means adding both the
.lua file and a <ComponentLua> line in RayMA.xml, then redeploying — there is
no runtime directory scan.
| Module | Purpose |
|---|---|
search |
Fuzzy search over the data pools (Sequences, Groups, Macros, Presets, Pages, Layouts, Timecodes, Timers, Worlds, Plugins, Filters, MAtricks, Appearances). Type aliases (seq, grp, pre …) and 4 sort modes (relevance, children, type, datapool). |
browser |
Object tree browser — navigate Root, ShowData, data pools, and any MA3 object. |
properties |
Property inspector — all readable properties with type, value, and enum info. |
qa |
Quick Actions — context-sensitive menu (playback, edit, browse, cmdline, info). Extensible via RayMA.registerQAAction. |
cmd |
MA3 command line with visual caret. |
math |
Sandboxed calculator with history, math functions, pi/tau/phi/e, and ans. |
help |
Two-level help — module list → module details, auto-populated from module metadata. |
terminal |
Persistent shell terminal (LuaSocket TCP bridge, io.popen fallback). Windows cmd.exe / Unix bash / MA3 console. |
selection |
Live view of fixtures currently selected in the programmer. |
filebrowser |
Filesystem browser with copy/cut/paste, multi-select, sort, and filter. |
ray |
Shorthand command interpreter — hex color, symgrid, and module-registered commands; maintains history and result cache. |
Photons live in photons/, are declared in RayMA.xml, and register on load. Ray uses them to parse shorthand values.
- color — hex color on the current selection:
#RRGGBB,#RGB,#RRGGBBAA,#RGBA.
To add a parser, drop a .lua file into photons/ and declare it as a <ComponentLua> in RayMA.xml.
Prisms live in prisms/, are declared in RayMA.xml, and implement fixture grid layout algorithms. This framework is being rebuilt from scratch — the previous prism set is preserved on main and only one layout exists on this branch so far:
| Prism | Layout |
|---|---|
2d top |
Bird's-eye grid — PosX → column, PosY → row. Run with symgrid 2d top. |
Modules in user_modules/ are declared in RayMA.xml like every other component. _template.lua is a reference skeleton — it is intentionally left out of the manifest so it never registers.
| Module | What it does |
|---|---|
banner |
ASCII banner display. |
bpm |
Tap-tempo BPM meter; applies to Speed master. |
catfacts |
Random cat facts. |
christmas |
Seasonal greeting. |
colorbucket |
Color mixing palette. |
colorslots |
3-reel slot machine that applies the resulting color (with strobe). |
countdown |
Configurable countdown timer. |
dmxdip |
DMX DIP-switch calculator (1–512). |
helloworld |
Minimal example module. |
luxcalc |
Lux calculator (aperture / shutter / ISO / ND). |
stats |
Show-file statistics — object counts and memory usage. |
subfixture |
Subfixture navigator grouped by fixture type. |
tcoffset |
Timecode offset calculator. |
timecodeeditor |
Timeline event editor with its own selection state. |
Add a .lua file under user_modules/ and declare it as a <ComponentLua> in RayMA.xml (put user-module entries last, after the core files that define the registries). The module describes itself, draws a list of results into RayMA.T.RESULTS, and handles keys. Here's a complete, runnable example — user_modules/greet.lua:
-- user_modules/greet.lua — a tiny greeting picker
local S = RayMA.getModuleState("greet") -- state survives mode switches
S.greetings = S.greetings or { "Hello", "Hej", "Bonjour", "Ciao", "你好" }
local mod = {
name = "greet",
family = "Community",
description = "Pick a greeting and echo it",
helpEntries = {
{ "Type", "Filter greetings" },
{ "Enter", "Echo the selected greeting" },
{ "Esc", "Go back" },
},
}
local function rebuild(query)
local T, q = RayMA.T, (query or ""):lower()
T.RESULTS = {}
for i, g in ipairs(S.greetings) do
if q == "" or g:lower():find(q, 1, true) then
T.RESULTS[#T.RESULTS + 1] = { name = g, type = "Greeting", no = i }
end
end
end
function mod.onEnter()
RayMA.T.QUERY, RayMA.T.CARET, RayMA.T.CURSOR = "", 0, 1
rebuild("")
RayMA.UI.refresh()
end
function mod.renderRow(entry) return string.format(" #%-2d %s", entry.no, entry.name) end
function mod.getStatusText() return #RayMA.T.RESULTS .. " greetings | Enter: echo | Esc: back" end
function mod.onChar(c) RayMA.H.defaultOnChar(c); rebuild(RayMA.T.QUERY); RayMA.UI.refresh() end
function mod.onKey(keyCode)
local KC = RayMA.H.KC
if keyCode == KC.ESCAPE then RayMA.goBack(); return end
if RayMA.H.handleListKey(keyCode) then return end
if RayMA.H.handleTextKey(keyCode) then rebuild(RayMA.T.QUERY); RayMA.UI.refresh(); return end
if keyCode == KC.ENTER then
local e = RayMA.T.RESULTS[RayMA.T.CURSOR]
if e then Echo("[greet] " .. e.name) end
end
end
RayMA.registerModule(mod)
-- Optional: let users type "greet" to jump in, or launch with arg "greet"
RayMA.registerCommand({
name = "Greet", command = "greet", _module = "greet",
description = mod.description,
handler = function() RayMA.switchMode("greet") end,
})Key pieces:
RayMA.getModuleState(name)— persistent table, survivesonEnter/onExitcycles.RayMA.T— session state:QUERY,CARET,CURSOR,OFFSET,RESULTS(entries the UI renders).RayMA.H.defaultOnChar,handleListKey,handleTextKey,KC— input plumbing so you don't reimplement text editing and arrow-key navigation.RayMA.UI.refresh()— redraw after a state change.- Optional hooks:
onExit,onTick(called ~5×/s, useful for async work),onRoute(segments)(route-style args),onPluginClose,getSearchDisplay,onClick. - Set
elevatedOnly = trueon the module to force it to open in a tabbed workspace.
See user_modules/_template.lua for the full hook list and user_modules/helloworld.lua for a slightly more featured version.
Add a Quick Action:
RayMA.registerQAAction({ ... })Add a Ray command hint:
RayMA.registerRayHints({ ... })Prisms and photons are added by placing a .lua file in the matching directory and declaring it as a <ComponentLua> in RayMA.xml.
A prism registers via RayMA.registerPrism{...} and provides an execute(mode) function. Since the prism framework was rebuilt from scratch, prisms are self-contained: execute collects its own selected fixtures, decides a row/column for each, and applies the grid with Cmd('Grid <col>/<row>/0; Fixture <fids>'). Save this as prisms/diagonal.lua and add a matching <ComponentLua> line to RayMA.xml:
-- prisms/diagonal.lua — place selected fixtures on the main diagonal
RayMA.registerPrism({
name = "diagonal",
description = "One fixture per row on the diagonal",
modes = { "diagonal" }, -- exposes "symgrid diagonal"
execute = function(mode)
-- Gather selected FIDs
local fids, count = {}, 0
pcall(function() count = SelectionCount() end)
if count == 0 then return false, "No fixtures selected" end
local idx
pcall(function() idx = SelectionFirst(true) end)
while idx ~= nil do
pcall(function()
local sf = GetSubfixture(idx)
local fid = sf and (tonumber(sf.FID or 0) or 0) or 0
if fid > 0 then fids[#fids + 1] = fid end
end)
local nxt
pcall(function() nxt = SelectionNext(idx, true) end)
if nxt == nil or nxt == idx then break end
idx = nxt
end
-- Place each on its own diagonal cell
table.sort(fids)
for i, fid in ipairs(fids) do
pcall(function() Cmd(string.format('Grid %d/%d/0; Fixture %d', i - 1, i - 1, fid)) end)
end
return true, string.format("diagonal: %d fixtures", #fids)
end,
})prisms/2dtop.lua is the reference implementation — copy its fixture-collection and position-reading helpers when you need 3D positions.
Each entry in modes gets an auto-registered Ray hint (symgrid <mode>) so it shows up in the command palette.
A photon is a value parser + applier. It attaches helper functions to the shared RayMA.Photon namespace and registers Ray hints so users can discover it. Save this as photons/kelvin.lua and add a matching <ComponentLua> line to RayMA.xml:
-- photons/kelvin.lua — apply a color temperature in Kelvin
local PH = RayMA.Photon
local function parseKelvin(str)
local k = str:match("^(%d+)[kK]$") -- matches "3200k", "5600K"
return k and tonumber(k) or nil
end
local function applyKelvin(k)
Cmd(string.format('Attribute "ColorRGB_R" At %d', k < 4000 and 100 or 80))
Cmd(string.format('Attribute "ColorRGB_G" At %d', 80))
Cmd(string.format('Attribute "ColorRGB_B" At %d', k < 4000 and 40 or 100))
return true, string.format("Colour temperature %dK", k)
end
-- Expose so other photons / the ray module can reuse them
PH.parseKelvin = parseKelvin
PH.applyKelvin = applyKelvin
-- Discoverability: show up in the Ray command list
RayMA.registerRayHints({
{ cmd = "3200k", desc = "Tungsten white", family = "Photon" },
{ cmd = "5600k", desc = "Daylight white", family = "Photon" },
})Photons don't have a dedicated registerPhoton call — they're just Lua files that publish functions onto RayMA.Photon and register Ray hints. Registering a hint makes it appear in the command list, but the Ray module (modules/ray.lua) decides which parser actually handles a given input — so to wire a new photon in, add a dispatch branch to executeRay/getPreview in modules/ray.lua (the color photon's hex branch is the template).
Launch with debug in the argument to enable RayMA.dbg(...) trace output to the MA3 command line. Combinable: debug root, debug search, etc.
No license file is currently shipped with this plugin.






