Packages

Multi-surface application runtime for Elixir. One TEA module renders to terminal, browser (LiveView), SSH, and MCP (agents). 30+ widgets, flexbox + CSS grid, AI agent runtime, distributed swarm with CRDTs, time-travel debugging, session recording, sandboxed REPL, and agentic commerce.

Current section

Files

Jump to
raxol lib raxol minimal.ex
Raw

lib/raxol/minimal.ex

defmodule Raxol.Minimal do
@moduledoc """
Minimal Raxol terminal interface for ultra-fast startup.
This module provides a stripped-down version of Raxol optimized for
minimal startup time and memory footprint while still providing core
terminal functionality.
## Features Included
- Core terminal emulation with full ANSI/CSI support
- Efficient buffer management with scrollback
- Keyboard/mouse input handling
- Performance telemetry
- Character sets (UTF-8, ASCII)
- Color support (16/256/RGB)
## Features Excluded
- Web interface (Phoenix)
- Database layer
- Advanced animations
- Plugin system (unless explicitly enabled)
- Audit logging
- Enterprise features
## Usage
# Start minimal terminal
{:ok, terminal} = Raxol.Minimal.start_terminal()
# Or start with options
{:ok, terminal} = Raxol.Minimal.start_terminal(
width: 80,
height: 24,
mode: :raw,
features: [:colors, :scrollback]
)
## Configuration
The minimal terminal can be configured via application environment:
config :raxol, Raxol.Minimal,
default_width: 80,
default_height: 24,
max_scrollback: 1000,
enable_telemetry: true
"""
use Raxol.Core.Behaviours.BaseManager
alias Raxol.Terminal.ANSI.Utils.AnsiParser
alias Raxol.Terminal.Buffer
alias Raxol.Terminal.Buffer.Cell
alias Raxol.Terminal.ScreenBuffer.Operations
alias Raxol.Core.Utils.GenServerHelpers
alias Raxol.Core.Runtime.Log
@type terminal_mode :: :raw | :cooked | :cbreak
@type color_mode :: :none | :ansi16 | :ansi256 | :rgb
@type feature ::
:colors | :scrollback | :mouse | :bracketed_paste | :alternate_screen
@type terminal_options :: [
width: pos_integer(),
height: pos_integer(),
mode: terminal_mode(),
color_mode: color_mode(),
features: [feature()],
scrollback_lines: non_neg_integer(),
charset: :utf8 | :ascii,
telemetry: boolean(),
hibernate_after: timeout()
]
defmodule State do
@moduledoc false
defstruct [
:width,
:height,
:mode,
:color_mode,
:features,
:main_buffer,
:alternate_buffer,
:active_buffer,
:scrollback,
:cursor,
:saved_cursor,
:char_attrs,
:charset,
:started_at,
:telemetry_enabled,
:metrics,
:input_buffer,
:parser_state
]
@type t :: %__MODULE__{
width: pos_integer(),
height: pos_integer(),
mode: Raxol.Minimal.terminal_mode(),
color_mode: Raxol.Minimal.color_mode(),
features: MapSet.t(Raxol.Minimal.feature()),
main_buffer: Buffer.t(),
alternate_buffer: Buffer.t() | nil,
active_buffer: :main | :alternate,
scrollback: list(list(Cell.t())),
cursor: {non_neg_integer(), non_neg_integer()},
saved_cursor: {non_neg_integer(), non_neg_integer()} | nil,
char_attrs: map(),
charset: :utf8 | :ascii,
started_at: integer(),
telemetry_enabled: boolean(),
metrics: map(),
input_buffer: binary(),
parser_state: term()
}
end
@default_options [
width: 80,
height: 24,
mode: :raw,
color_mode: :ansi256,
features: [:colors, :scrollback],
scrollback_lines: 1000,
charset: :utf8,
telemetry: true,
hibernate_after: 15_000
]
# Public API
@doc """
Start a minimal terminal session with ultra-fast startup.
Returns `{:ok, pid}` on success.
"""
@spec start_terminal(terminal_options()) :: {:ok, pid()} | {:error, term()}
def start_terminal(opts \\ []) do
merged_opts = Keyword.merge(get_config_defaults(), opts)
# Use BaseManager's start_link
start_link =
Application.get_env(:raxol, :start_link_fn, &GenServer.start_link/3)
start_link.(__MODULE__, merged_opts,
hibernate_after: merged_opts[:hibernate_after]
)
end
@doc """
Get current terminal state for debugging.
"""
@spec get_state(pid()) :: map()
def get_state(pid) do
GenServer.call(pid, :get_state)
end
@doc """
Send input to terminal.
"""
@spec send_input(pid(), binary()) :: :ok
def send_input(pid, data) do
GenServer.cast(pid, {:input, data})
end
@doc """
Get terminal dimensions.
"""
@spec get_dimensions(pid()) :: {pos_integer(), pos_integer()}
def get_dimensions(pid) do
GenServer.call(pid, :get_dimensions)
end
@doc """
Resize the terminal.
"""
@spec resize(pid(), pos_integer(), pos_integer()) :: :ok
def resize(pid, width, height) do
GenServer.call(pid, {:resize, width, height})
end
@doc """
Get performance metrics.
"""
@spec get_metrics(pid()) :: map()
def get_metrics(pid) do
GenServer.call(pid, :get_metrics)
end
@doc """
Clear the terminal screen.
"""
@spec clear(pid()) :: :ok
def clear(pid) do
GenServer.cast(pid, :clear)
end
@doc """
Reset terminal to initial state.
"""
@spec reset(pid()) :: :ok
def reset(pid) do
GenServer.cast(pid, :reset)
end
# BaseManager Implementation
@impl Raxol.Core.Behaviours.BaseManager
def init_manager(opts) do
start_time = System.monotonic_time(:microsecond)
state = initialize_state(opts)
startup_time = System.monotonic_time(:microsecond) - start_time
with true <- state.telemetry_enabled do
:telemetry.execute(
[:raxol, :minimal, :startup],
%{duration: startup_time},
%{width: state.width, height: state.height}
)
end
Log.info("Minimal terminal started in #{startup_time}μs")
{:ok, state}
end
@impl Raxol.Core.Behaviours.BaseManager
def handle_manager_call(:get_state, _from, state) do
GenServerHelpers.handle_get_state(state)
end
def handle_manager_call(:get_dimensions, _from, state) do
{:reply, {state.width, state.height}, state}
end
def handle_manager_call({:resize, width, height}, _from, state) do
new_state = resize_terminal(state, width, height)
{:reply, :ok, new_state}
end
def handle_manager_call(:get_metrics, _from, state) do
GenServerHelpers.handle_get_metrics(state)
end
@impl Raxol.Core.Behaviours.BaseManager
def handle_manager_cast({:input, data}, state) do
start_time =
case state.telemetry_enabled do
true -> System.monotonic_time(:microsecond)
false -> nil
end
new_state = process_input(data, state)
final_state =
case {state.telemetry_enabled, start_time} do
{true, start_time} when is_integer(start_time) ->
duration = System.monotonic_time(:microsecond) - start_time
:telemetry.execute(
[:raxol, :minimal, :input],
%{duration: duration, bytes: byte_size(data)},
%{}
)
update_metrics(new_state, :input_processing, duration)
{false, _} ->
new_state
end
{:noreply, final_state}
end
def handle_manager_cast(:clear, state) do
new_buffer = Buffer.clear(get_active_buffer(state))
new_state = put_active_buffer(state, new_buffer)
{:noreply, %{new_state | cursor: {0, 0}}}
end
def handle_manager_cast(:reset, state) do
{:noreply, initialize_state(get_init_opts(state))}
end
@impl Raxol.Core.Behaviours.BaseManager
def handle_manager_info(:hibernate, state) do
{:noreply, state, :hibernate}
end
def handle_manager_info(_msg, state) do
{:noreply, state}
end
# Private Functions
defp get_config_defaults do
Application.get_env(:raxol, __MODULE__, [])
|> Keyword.merge(@default_options)
end
defp initialize_state(opts) do
width = opts[:width]
height = opts[:height]
features = MapSet.new(opts[:features] || [])
main_buffer = Buffer.new({width, height})
alternate_buffer =
case MapSet.member?(features, :alternate_screen) do
true -> Buffer.new({width, height})
false -> nil
end
%State{
width: width,
height: height,
mode: opts[:mode],
color_mode: opts[:color_mode],
features: features,
main_buffer: main_buffer,
alternate_buffer: alternate_buffer,
active_buffer: :main,
scrollback: [],
cursor: {0, 0},
saved_cursor: nil,
char_attrs: default_char_attrs(),
charset: opts[:charset],
started_at: System.monotonic_time(:millisecond),
telemetry_enabled: opts[:telemetry],
metrics: %{
input_count: 0,
output_count: 0,
avg_input_time: 0,
max_input_time: 0,
memory_bytes: 0
},
input_buffer: <<>>,
parser_state: nil
}
end
defp default_char_attrs do
%{
bold: false,
italic: false,
underline: false,
blink: false,
reverse: false,
hidden: false,
strikethrough: false,
fg_color: :default,
bg_color: :default
}
end
defp process_input(data, state) do
# Parse ANSI sequences using the full parser
sequences = AnsiParser.parse(data)
Enum.reduce(sequences, state, fn sequence, acc_state ->
handle_sequence(sequence, acc_state)
end)
end
defp handle_sequence(%{type: :csi} = seq, state) do
handle_csi_sequence(seq, state)
end
defp handle_sequence(%{type: :osc} = seq, state) do
handle_osc_sequence(seq, state)
end
defp handle_sequence(%{type: :text, text: text}, state) do
write_text(state, text)
end
defp handle_sequence(_, state) do
# Ignore unsupported sequences in minimal mode
state
end
defp handle_csi_sequence(%{command: command, params: params}, state) do
case command do
# Cursor movement
"A" -> move_cursor(state, :up, parse_param(params, 0, 1))
"B" -> move_cursor(state, :down, parse_param(params, 0, 1))
"C" -> move_cursor(state, :right, parse_param(params, 0, 1))
"D" -> move_cursor(state, :left, parse_param(params, 0, 1))
"H" -> set_cursor_position(state, params)
"f" -> set_cursor_position(state, params)
# Erasing
"J" -> erase_display(state, parse_param(params, 0, 0))
"K" -> erase_line(state, parse_param(params, 0, 0))
# Scrolling
"S" -> scroll_up(state, parse_param(params, 0, 1))
"T" -> scroll_down(state, parse_param(params, 0, 1))
# Graphics
"m" -> set_graphics_rendition(state, params)
# Save/Restore cursor
"s" -> %{state | saved_cursor: state.cursor}
"u" -> %{state | cursor: state.saved_cursor || state.cursor}
_ -> state
end
end
defp handle_osc_sequence(%{params: params}, state) do
case params do
["0", title] -> set_window_title(state, title)
["2", title] -> set_window_title(state, title)
_ -> state
end
end
defp parse_param(params, index, default) do
case Enum.at(params, index) do
nil -> default
"" -> default
param -> String.to_integer(param)
end
end
defp move_cursor(state, direction, count) do
{x, y} = state.cursor
new_cursor =
case direction do
:up -> {x, max(0, y - count)}
:down -> {x, min(state.height - 1, y + count)}
:left -> {max(0, x - count), y}
:right -> {min(state.width - 1, x + count), y}
end
%{state | cursor: new_cursor}
end
defp set_cursor_position(state, params) do
row = parse_param(params, 0, 1) - 1
col = parse_param(params, 1, 1) - 1
new_cursor = {
max(0, min(state.width - 1, col)),
max(0, min(state.height - 1, row))
}
%{state | cursor: new_cursor}
end
defp write_text(state, text) do
buffer = get_active_buffer(state)
{x, y} = state.cursor
{new_buffer, new_x, new_y} =
text
|> String.graphemes()
|> Enum.reduce({buffer, x, y}, fn char, {buf, cur_x, cur_y} ->
case char do
"\r" ->
{buf, 0, cur_y}
"\n" ->
new_y = min(state.height - 1, cur_y + 1)
{buf, 0, new_y}
"\t" ->
# Move to next tab stop (every 8 columns)
new_x = min(state.width - 1, (div(cur_x, 8) + 1) * 8)
{buf, new_x, cur_y}
_ ->
cond do
cur_x < state.width and cur_y < state.height ->
# Write character directly to buffer
cell = Cell.new(char, state.char_attrs)
new_buf = Buffer.set_cell(buf, cur_x, cur_y, cell)
{new_buf, cur_x + 1, cur_y}
cur_x >= state.width and cur_y < state.height - 1 ->
# Wrap to next line if at end of line
cell = Cell.new(char, state.char_attrs)
new_buf = Buffer.set_cell(buf, 0, cur_y + 1, cell)
{new_buf, 1, cur_y + 1}
true ->
{buf, cur_x, cur_y}
end
end
end)
new_state = put_active_buffer(state, new_buffer)
%{new_state | cursor: {new_x, new_y}}
end
defp erase_display(state, mode) do
buffer = get_active_buffer(state)
{x, y} = state.cursor
new_buffer =
case mode do
0 -> Operations.Erasing.erase_in_display(buffer, 0, {y, x})
1 -> Operations.Erasing.erase_in_display(buffer, 1, {y, x})
2 -> Buffer.clear(buffer)
# Clear with scrollback not directly available
3 -> Buffer.clear(buffer)
_ -> buffer
end
put_active_buffer(state, new_buffer)
end
defp erase_line(state, mode) do
buffer = get_active_buffer(state)
{x, y} = state.cursor
new_buffer =
case mode do
0 -> Operations.Erasing.erase_in_line(buffer, 0, {y, x})
1 -> Operations.Erasing.erase_in_line(buffer, 1, {y, x})
2 -> Operations.Erasing.erase_in_line(buffer, 2, {y, x})
_ -> buffer
end
put_active_buffer(state, new_buffer)
end
defp scroll_up(state, lines) do
buffer = get_active_buffer(state)
new_buffer = Operations.scroll_up(buffer, lines)
put_active_buffer(state, new_buffer)
end
defp scroll_down(state, lines) do
buffer = get_active_buffer(state)
new_buffer = Operations.scroll_down(buffer, lines)
put_active_buffer(state, new_buffer)
end
defp set_graphics_rendition(state, params) do
attrs =
params
|> Enum.map(&parse_sgr_param/1)
|> Enum.reduce(state.char_attrs, &apply_sgr/2)
%{state | char_attrs: attrs}
end
defp parse_sgr_param(""), do: 0
defp parse_sgr_param(param), do: String.to_integer(param)
defp apply_sgr(0, _attrs), do: default_char_attrs()
defp apply_sgr(1, attrs), do: %{attrs | bold: true}
defp apply_sgr(3, attrs), do: %{attrs | italic: true}
defp apply_sgr(4, attrs), do: %{attrs | underline: true}
defp apply_sgr(5, attrs), do: %{attrs | blink: true}
defp apply_sgr(7, attrs), do: %{attrs | reverse: true}
defp apply_sgr(8, attrs), do: %{attrs | hidden: true}
defp apply_sgr(9, attrs), do: %{attrs | strikethrough: true}
defp apply_sgr(n, attrs) when n >= 30 and n <= 37,
do: %{attrs | fg_color: ansi_color(n - 30)}
defp apply_sgr(n, attrs) when n >= 40 and n <= 47,
do: %{attrs | bg_color: ansi_color(n - 40)}
defp apply_sgr(n, attrs) when n >= 90 and n <= 97,
do: %{attrs | fg_color: bright_ansi_color(n - 90)}
defp apply_sgr(n, attrs) when n >= 100 and n <= 107,
do: %{attrs | bg_color: bright_ansi_color(n - 100)}
defp apply_sgr(_, attrs), do: attrs
defp ansi_color(0), do: :black
defp ansi_color(1), do: :red
defp ansi_color(2), do: :green
defp ansi_color(3), do: :yellow
defp ansi_color(4), do: :blue
defp ansi_color(5), do: :magenta
defp ansi_color(6), do: :cyan
defp ansi_color(7), do: :white
defp ansi_color(_), do: :default
defp bright_ansi_color(n), do: {:bright, ansi_color(n)}
defp set_window_title(state, _title) do
# In minimal mode, we don't actually set window titles
# but we track the attempt for compatibility
state
end
defp resize_terminal(state, width, height) do
main_buffer = Buffer.resize(state.main_buffer, width, height)
alternate_buffer =
case state.alternate_buffer do
nil -> nil
buffer -> Buffer.resize(buffer, width, height)
end
{cur_x, cur_y} = state.cursor
new_cursor = {
min(cur_x, width - 1),
min(cur_y, height - 1)
}
%{
state
| width: width,
height: height,
main_buffer: main_buffer,
alternate_buffer: alternate_buffer,
cursor: new_cursor
}
end
defp get_active_buffer(%State{
active_buffer: :alternate,
alternate_buffer: buffer
})
when not is_nil(buffer),
do: buffer
defp get_active_buffer(%State{main_buffer: buffer}), do: buffer
defp put_active_buffer(%State{active_buffer: :alternate} = state, buffer)
when not is_nil(state.alternate_buffer) do
%{state | alternate_buffer: buffer}
end
defp put_active_buffer(state, buffer) do
%{state | main_buffer: buffer}
end
defp update_metrics(state, :input_processing, duration) do
metrics = state.metrics
count = metrics.input_count + 1
avg = (metrics.avg_input_time * metrics.input_count + duration) / count
max_time = max(metrics.max_input_time, duration)
memory =
case rem(count, 100) do
0 ->
{:memory, bytes} = Process.info(self(), :memory)
bytes
_ ->
metrics.memory_bytes
end
new_metrics = %{
metrics
| input_count: count,
avg_input_time: avg,
max_input_time: max_time,
memory_bytes: memory
}
%{state | metrics: new_metrics}
end
defp get_init_opts(state) do
[
width: state.width,
height: state.height,
mode: state.mode,
color_mode: state.color_mode,
features: MapSet.to_list(state.features),
charset: state.charset,
telemetry: state.telemetry_enabled
]
end
end