Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ryzhpolsos/redeye/llms.txt

Use this file to discover all available pages before exploring further.

Plugins can register expression functions that are callable from any RWML attribute value, event handler, or inline script. Exported functions follow the same calling convention as built-in functions: they receive an ordered list of evaluated arguments and the current variable storage, and return a value that the expression engine substitutes at the call site. This lets you extend RWML with side-effecting operations or computed values that the built-in function set does not cover.

Function signature

Every exported function must conform to the following delegate type:
Func<IEnumerable<object>, IVariableStorage<string>, object>
  • First parameter (IEnumerable<object> args) — the evaluated arguments passed by the caller, in order.
  • Second parameter (IVariableStorage<string> varStorage) — the variable storage of the expression context where the function was called.
  • Return value (object) — the value substituted at the call site. Return string.Empty for side-effect-only functions.

Exporting a function

1

Write the function implementation

Define the function inline in Main() or as a separate method. Use args.ElementAt(n) to read positional arguments and args.Count() to check how many were supplied.
using System.Linq;
using RedEye.PluginAPI;

namespace MyPlugin {
    public class MyPlugin : Plugin {
        public override void Main() {
            ExportFunction("toUpperCase", (args, vars) => {
                if (!args.Any()) return string.Empty;
                return args.ElementAt(0)?.ToString().ToUpper() ?? string.Empty;
            });
        }
    }
}
2

Access arguments safely

Arguments are passed as object values. Always call .ToString() or cast with a null check. Use args.Count() before calling ElementAt when the argument count is variable.
ExportFunction("join", (args, vars) => {
    if (args.Count() < 2) return string.Empty;

    var separator = args.ElementAt(0)?.ToString() ?? string.Empty;
    var parts = args.Skip(1).Select(a => a?.ToString() ?? string.Empty);
    return string.Join(separator, parts);
});
3

Read variables from the calling context (optional)

varStorage exposes the variable scope of the RWML node that triggered the call. Use varStorage.GetVariable(name) to read layout variables like ${window.title} or custom variables set by <var> elements.
ExportFunction("logVariable", (args, vars) => {
    var varName = args.ElementAt(0)?.ToString() ?? string.Empty;
    var value = vars.GetVariable(varName);
    Logger.LogDebug($"Variable \"{varName}\" = {value}");
    return string.Empty;
});
4

Return a value or string.Empty

For functions that produce a value (used in an expression like my-plugin.formatBytes(${file.size})), return the computed value as a string or a type the expression engine can convert. For side-effect-only functions (like killing a process), return string.Empty so the call site is replaced with an empty string.
ExportFunction("formatBytes", (args, vars) => {
    if (!args.Any()) return "0 B";
    if (!long.TryParse(args.ElementAt(0)?.ToString(), out var bytes)) return "0 B";

    if (bytes >= 1_073_741_824) return $"{bytes / 1_073_741_824.0:0.#} GB";
    if (bytes >= 1_048_576)     return $"{bytes / 1_048_576.0:0.#} MB";
    if (bytes >= 1_024)         return $"{bytes / 1_024.0:0.#} KB";
    return $"{bytes} B";
});
5

Call the function from RWML

After restarting RedEye, call the function using the fully-qualified name pluginId.functionName(arg1, arg2) from any RWML attribute that supports expressions.
<label text="my-plugin.formatBytes(${disk.usedBytes})" updateInterval="2000" />
For side-effect functions, call them from event handlers:
<button text="Kill" onClick="my-plugin.killProcess(${window.handle})" />

Real example — killProcess from the built-in taskbar

The built-in taskbar.xml layout exports a killProcess function directly from an inline <script> block. The pattern is identical to what a plugin does: call PluginManager.ExportFunction with a function that reads a window handle from args.ElementAt(0).
<script>
  <![CDATA[
    using System.Linq;
    using System.Diagnostics;
    using System.Runtime.InteropServices;

    static class NativeFunctions {
        [DllImport("user32.dll")]
        public static extern int GetWindowThreadProcessId(IntPtr hWnd, ref int lpdwProcessId);
    }

    // --main--
    PluginManager.ExportFunction("killProcess", (args, _) => {
        var hWnd = (IntPtr)long.Parse(args.ElementAt(0).ToString());
        int pid = 0;

        NativeFunctions.GetWindowThreadProcessId(hWnd, ref pid);
        Process.GetProcessById(pid).Kill();

        return string.Empty;
    });
  ]]>
</script>
The function is then called from a context menu item:
<contextMenu for="icon">
    <item action="wapi.closeWindow(${window.handle})">Close window</item>
    <item action="killProcess(${window.handle})">Kill process</item>
</contextMenu>
The inline script version does not qualify the name with a plugin ID. When exporting from a plugin, the full call name is always pluginId.functionName.
The equivalent plugin implementation:
using System.Linq;
using System.Diagnostics;
using System.Runtime.InteropServices;
using RedEye.PluginAPI;

namespace WindowUtils {
    static class NativeFunctions {
        [DllImport("user32.dll")]
        public static extern int GetWindowThreadProcessId(IntPtr hWnd, ref int lpdwProcessId);
    }

    public class WindowUtilsPlugin : Plugin {
        public override void Main() {
            ExportFunction("killProcess", (args, _) => {
                var hWnd = (IntPtr)long.Parse(args.ElementAt(0).ToString());
                int pid = 0;

                NativeFunctions.GetWindowThreadProcessId(hWnd, ref pid);
                Process.GetProcessById(pid).Kill();

                return string.Empty;
            });
        }
    }
}
Called in RWML as:
<item action="window-utils.killProcess(${window.handle})">Kill process</item>

Argument patterns summary

ScenarioCode
Read first argumentargs.ElementAt(0)?.ToString()
Read second argumentargs.ElementAt(1)?.ToString()
Count argumentsargs.Count()
Skip first argumentargs.Skip(1)
Read a layout variablevarStorage.GetVariable("window.title")
No return value neededreturn string.Empty;
Unhandled exceptions inside an exported function propagate to the expression engine and can crash the rendering cycle for the widget that triggered the call. Wrap logic that may throw in a try/catch and log the error with Logger.LogError().

Build docs developers (and LLMs) love