Lua Scripts

rex do runs a Lua script against a Rex server.

A script is extremely powerful. It can do anything a full-fledged client can: inspect servers, create sessions, create windows, start terminals, read a terminal screen, react to events, and more.

This script opens a new window running htop:

-- htop.lua
rex.session.new_window{
  layout = rex.layout.block{
    flavor  = "com.superlogical.terminal.shell",
    options = { command = { "htop" } },
  },
}
rex do htop.lua

A script uses the same rex API as the configuration file. Unlike the configuration file, a script runs once from top to bottom and exits (unless there are active event handlers running).

Running a Script

rex do takes a script in three ways:

rex do ./layout.lua                           # a file
rex do -e 'return rex.call("session.list")'   # inline with -e
rex do - < layout.lua                         # stdin

The argument is treated as a file when it ends in .lua or contains a slash. Anything else is the name of an action to run.

A script run inside a Rex terminal acts on that terminal’s session and block. Use -s and -b to target a different one:

rex do -s build ./layout.lua

Scripts have Lua’s full standard library, including io and os, and require loads other Lua files.

Scripts are executed locally where the rex CLI is, so security and program access is up to you.

Arguments

Arguments are passed as key=value pairs after the script and read from the rex.args table:

-- greet.lua
return "hello " .. (rex.args.name or "world")
$ rex do greet.lua name=rex
"hello rex"

A value’s type comes from how it is written. n=2 is a number, on=true is a boolean, and name=x is a string. To pass a string that looks like something else, or a nested value, pass all the arguments as JSON with --args:

rex do greet.lua --args '{"name": "true"}'
rex do greet.lua --args @args.json

Results and Errors

The value a script returns is printed as JSON. Tables become objects or arrays. Add --json for indented output. A script that returns nothing prints nothing.

$ rex do -e 'return { name = "rex", tabs = { 1, 2 } }'
{"name":"rex","tabs":[1,2]}

A script that raises an error prints the message and exits with status 1:

$ rex do -e 'error("no tests found", 0)'
Error: no tests found

To exit with a status of your choosing, call rex.stop with it. The script ends immediately:

rex.stop(3)

print writes to the terminal as usual, and rex.log writes a log line.

Calling Rex

A script starts with a few values describing where it was run:

ValueDescription
rex.session_idThe session the script is running against.
rex.block_idThe block the script is running against, when there is one.
rex.argsThe script’s arguments.

Rex’s API is grouped under rex.session. Each call takes one table of arguments and returns one table:

rex.session.set_block_label{
  block_id = rex.block_id,
  label    = "tests",
}

Calls fill in the script’s session, so most calls don’t need a session_id. A call that acts on one block, such as set_block_label, still needs its block_id.

A failed call doesn’t stop the script. It returns nil and an error message, so check the second value when the result matters:

local w, err = rex.session.new_window{ layout = layout }
if err ~= nil then error(err, 0) end

Three more ways to call Rex:

  • rex.call(method, args) calls any server method by name, including those outside rex.session, such as rex.call("session.list").
  • rex.block.call(creator, method, args) calls a method on a block, such as reading a terminal’s screen.
  • rex.invoke(name, args) runs an action, either one the script defines or one from your configuration file. rex.client.queue(name, args) asks the app to perform one of its own.

The Lua API reference has the full list.

Layouts

Calls that create blocks take a layout. Build one with rex.layout:

local shell = "com.superlogical.terminal.shell"

rex.session.new_window{
  layout = rex.layout.horizontal(0.5,
    rex.layout.block{ flavor = shell },
    rex.layout.block{
      flavor  = shell,
      options = { command = { "htop" } },
    }),
}

rex.layout.block is one block. rex.layout.horizontal and rex.layout.vertical split the space between two layouts, with the first argument giving the first one’s share. They can be nested.

Sleeping and Waiting

A script often needs to start something and then act on how it ended.

rex.sleep(seconds) pauses the script:

rex.sleep(0.5)

rex.wait(event, filter, timeout) pauses until an event arrives:

local ev = rex.wait(
  "terminal.child_exited", { block_id = id }, 600)
  • event is the name of the event to wait for.
  • filter is optional. A table matches an event whose fields equal the table’s. A function is called with the event and matches when it returns true.
  • timeout is optional, in seconds. Without it, rex.wait waits forever.

rex.wait returns the event as a table. If the timeout passes first, it returns nil and "timeout".

The events reference lists the events and their fields.

Example: Run Tests in a Split

This script runs a command in a new split beside the current terminal. If the command fails, it zooms the split so the failure is front and center.

-- tests.lua
local cmd = rex.args.cmd or "make test"

local split, err = rex.session.new_split{
  direction = "horizontal",
  side      = "after",
  focus     = false,
  layout    = rex.layout.block{
    flavor  = "com.superlogical.terminal.shell",
    options = {
      command = { "sh", "-c", cmd },
      exit    = { on_completion = false },  -- keep the output
    },
  },
}
if err ~= nil then error(err, 0) end
local block_id = split.block_ids[1]

-- Wait up to ten minutes for the command to exit
local ev = rex.wait(
  "terminal.child_exited", { block_id = block_id }, 600)
if ev == nil then error("timed out waiting for: " .. cmd, 0) end

if ev.exit_code ~= 0 then
  rex.session.zoom_block{ block_id = block_id, zoom = true }
end

return { block_id = block_id, exit_code = ev.exit_code }
$ rex do tests.lua cmd="go test ./..."
{"block_id":"block:01a10deb-c752-789f-9add-5dfd3a5b3214","exit_code":1}