Raxol. Headless
(Raxol v2.7.0)
View Source
Manages headless Raxol application sessions for non-interactive use.
Starts TEA apps in :agent environment (no terminal driver, no IO output)
and provides functions to inspect screen state, send keystrokes, and read
the application model. Designed for use via Tidewave project_eval or
programmatic testing.
Usage
# Start a session from a module
{:ok, :demo} = Raxol.Headless.start(RaxolDemo, id: :demo)
# Start from an example script (compiles module, skips boot code)
{:ok, :demo} = Raxol.Headless.start("examples/demo.exs", id: :demo)
# Take a text screenshot
{:ok, text} = Raxol.Headless.screenshot(:demo)
# Send a key and see the result
{:ok, text} = Raxol.Headless.send_key_and_screenshot(:demo, :tab)
# Inspect the model
{:ok, model} = Raxol.Headless.get_model(:demo)
# Stop
:ok = Raxol.Headless.stop(:demo)
Summary
Types
Why a name could not become a runnable module.
Functions
Returns a specification to start this module under a supervisor.
Returns the raw screen buffer for the session's current frame.
Returns the application model from the session's dispatcher.
Lists all active sessions. Returns [] if the Headless server is not running.
Takes a text screenshot of the session's current screen.
Sends a key event to the session's dispatcher.
Sends a key and returns a screenshot after waiting for re-render.
Delivers msg to the application's update/2 and waits for it to fold.
Sends a terminal resize event to the session's dispatcher.
Starts a headless session.
Starts the Headless session manager.
Stops a headless session.
Types
Why a name could not become a runnable module.
:not_a_raxol_application is deliberately distinct from :module_not_found.
One says nothing on the code path answers to that name at all; the other says
the name resolved to real code that does not implement TEA. An operator fixes
those two by doing completely different things, so collapsing them into one
refusal costs them the diagnosis.
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
Returns the raw screen buffer for the session's current frame.
Unlike screenshot/1, which returns a text capture, this returns the
Raxol.Core.Buffer itself, preserving per-cell style for downstream
rendering targets such as the LiveView-to-video pipeline.
Returns the application model from the session's dispatcher.
@spec list() :: [atom()]
Lists all active sessions. Returns [] if the Headless server is not running.
Takes a text screenshot of the session's current screen.
Sends a key event to the session's dispatcher.
@spec send_key_and_screenshot(atom(), String.t() | atom(), keyword()) :: {:ok, String.t()} | {:error, term()}
Sends a key and returns a screenshot after waiting for re-render.
Options
:wait_ms- Milliseconds to wait for dispatch processing (default: 50)- All key modifier options (
:ctrl,:alt,:shift)
Delivers msg to the application's update/2 and waits for it to fold.
The message takes the same path a subscription tick does, so
send_message(id, :tick) is indistinguishable from the app's own
subscribe_interval firing. Paired with subscriptions: false at start,
this is how a caller advances an app by an exact number of ticks: the
return means the model has folded the message, so a following
get_buffer/1 renders its effect.
@spec send_resize(atom(), pos_integer(), pos_integer()) :: :ok | {:error, term()}
Sends a terminal resize event to the session's dispatcher.
The dispatcher forwards the new dimensions to the rendering engine
(resizing its buffer) and to the application's update/2 as a
%Event{type: :resize, data: %{width: w, height: h}}.
Starts a headless session.
First argument is either a module atom or a file path string. Either way the
module has to be a Raxol application: it must export init/1, update/2 and
view/1. Declaring Raxol.Core.Runtime.Application is not sufficient on its
own, since the attribute can be present with none of the callbacks behind it.
When given a path, the file is compiled and the first module meeting that
contract is used.
Passing a path executes code
Compiling a defmodule runs its body, so whoever chooses the path string
chooses what runs in this VM, with the VM's privileges. That is arbitrary
code execution, not a file read, and it is a property of this function rather
than of any one caller.
The AST filter that keeps only defmodule nodes is a convenience -- it skips
an example script's boot code -- and never a sandbox. What bounds the compile
is a monitored child on a budget, which bounds the DAMAGE to this process
(a body that raises, throws, exits, or never returns is answered rather than
taking the session manager down with it). It does not bound what the body may
do while it runs.
So the path argument is only ever as safe as the caller that chooses it.
Callers that take one from outside the VM must confine it themselves:
Raxol.Headless.McpTools refuses the argument entirely unless a deployment
sets RAXOL_HEADLESS_PATH_ROOT, and then confines it under that root with
Raxol.Core.Boundary.Path.confine/3. The programmatic callers
(Raxol.Recording.Video, Raxol.MCP.Test) pass a path they already chose.
The module argument does not compile source, but loading a beam can execute
an @on_load callback. The TEA contract is checked after the module loads and
before a session starts; callers must therefore treat the VM's code path as
trusted deployment input.
Options
:id- Session identifier (default: module name as atom):width- Screen width (default: 120):height- Screen height (default: 40):subscriptions-falseboots the app with its declared subscriptions (timers, event sources) unarmed, so the model advances only on messages delivered explicitly viasend_message/2orsend_key/3. This is what makes a recording reproducible: with a timer running, which tick a sampled frame shows is a property of scheduler timing rather than of the caller. (default:true)
@spec start_link(keyword()) :: GenServer.on_start()
Starts the Headless session manager.
Stops a headless session.