Raxol Quickstart

View Source

Get started with Raxol in 5, 10, or 15 minutes. Choose your path based on what you need.

What is Raxol?

Raxol is a terminal UI framework for Elixir that scales from simple buffers to full applications:

  • Raxol.Core - Lightweight buffer primitives (< 100KB, zero deps)
  • Raxol.LiveView - Render terminals in Phoenix LiveView
  • Raxol (full) - Complete framework with plugins and enterprise features

Start small, add features as needed.


5-Minute Tutorial: Your First Buffer

Just want to draw boxes and text? Use Raxol.Core.

Installation

# mix.exs
def deps do
  [
    {:raxol, "~> 2.0"}  # Or {:raxol_core, "~> 2.0"} for minimal install
  ]
end
mix deps.get

Hello Buffer

Create a file hello.exs:

alias Raxol.Core.{Buffer, Box}

# Create a 40x10 buffer
buffer = Buffer.create_blank_buffer(40, 10)

# Draw a double-line box
buffer = Box.draw_box(buffer, 0, 0, 40, 10, :double)

# Write some text
buffer = Buffer.write_at(buffer, 5, 4, "Hello, Raxol!")

# Render it
IO.puts(Buffer.to_string(buffer))

Run it:

elixir hello.exs

Output:


                                      
                                      
                                      
     Hello, Raxol!                    
                                      
                                      
                                      
                                      

That's it! Pure functional, no servers, no complexity.

Key Concepts (5 min version)

  1. Create - Buffer.create_blank_buffer(width, height)
  2. Draw - Box.draw_box(), Box.fill_area(), etc.
  3. Write - Buffer.write_at(buffer, x, y, text)
  4. Render - Buffer.to_string(buffer) for output

Buffers are just data structures. No magic.


10-Minute Tutorial: LiveView Integration

Want to show a terminal in your Phoenix app? Add LiveView integration.

Add Dependency

# mix.exs
def deps do
  [
    {:raxol_core, "~> 2.0"},
    {:raxol_liveview, "~> 2.0"},
    {:phoenix_live_view, "~> 0.20 or ~> 1.0"}
  ]
end

Create a LiveView

# lib/my_app_web/live/terminal_live.ex
defmodule MyAppWeb.TerminalLive do
  use MyAppWeb, :live_view
  alias Raxol.Core.{Buffer, Box}

  def mount(_params, _session, socket) do
    # Create initial buffer
    buffer = Buffer.create_blank_buffer(80, 24)
    buffer = Box.draw_box(buffer, 0, 0, 80, 24, :rounded)
    buffer = Buffer.write_at(buffer, 10, 10, "Hello from LiveView!", %{})

    # Schedule periodic updates (optional)
    if connected?(socket), do: Process.send_after(self(), :tick, 1000)

    {:ok, assign(socket, buffer: buffer, count: 0)}
  end

  def render(assigns) do
    ~H"""
    <div>
      <h1>Live Terminal</h1>
      <.live_component
        module={Raxol.LiveView.TerminalComponent}
        id="terminal"
        buffer={@buffer}
        theme={:nord}
        on_keypress={&handle_keypress/1}
        on_click={&handle_click/1}
      />
    </div>
    """
  end

  def handle_info(:tick, socket) do
    # Update buffer every second
    count = socket.assigns.count + 1
    buffer = Buffer.write_at(
      socket.assigns.buffer,
      10, 12,
      "Ticks: #{count}",
      %{fg_color: :cyan}
    )

    Process.send_after(self(), :tick, 1000)
    {:noreply, assign(socket, buffer: buffer, count: count)}
  end

  def handle_keypress(key) do
    IO.puts("Key pressed: #{key}")
  end

  def handle_click({x, y}) do
    IO.puts("Clicked at: #{x}, #{y}")
  end
end

Add Route

# lib/my_app_web/router.ex
scope "/", MyAppWeb do
  pipe_through :browser

  live "/terminal", TerminalLive
end

Include CSS

# lib/my_app_web/components/layouts/root.html.heex
<link rel="stylesheet" href={~p"/assets/raxol_terminal.css"} />

Start Server

mix phx.server
# Visit http://localhost:4000/terminal

You now have a live terminal in your web app! Updates in real-time, handles keyboard/mouse events.

Available Themes

Choose from built-in themes:

  • :nord - Nord color scheme
  • :dracula - Dracula theme
  • :solarized_dark - Solarized Dark
  • :solarized_light - Solarized Light
  • :monokai - Monokai

15-Minute Tutorial: Interactive Terminal

Build a fully interactive REPL-style terminal.

The Plan

We'll create:

  1. Command input at the bottom
  2. Scrollable output area
  3. Command history (up/down arrows)
  4. Real-time updates

Full Implementation

defmodule MyAppWeb.InteractiveTerminalLive do
  use MyAppWeb, :live_view
  alias Raxol.Core.{Buffer, Box, Renderer}

  @width 80
  @height 24
  @output_height 22
  @input_height 2

  def mount(_params, _session, socket) do
    socket =
      socket
      |> assign(
        buffer: create_initial_buffer(),
        output_lines: ["Welcome to Interactive Terminal!", "Type 'help' for commands"],
        input: "",
        history: [],
        history_index: 0
      )
      |> update_display()

    {:ok, socket}
  end

  def render(assigns) do
    ~H"""
    <div class="interactive-terminal">
      <.live_component
        module={Raxol.LiveView.TerminalComponent}
        id="terminal"
        buffer={@buffer}
        theme={:nord}
        on_keypress={fn key -> send(self(), {:key, key}) end}
      />
    </div>
    """
  end

  def handle_info({:key, key}, socket) do
    socket =
      case key do
        "Enter" ->
          socket
          |> execute_command()
          |> clear_input()

        "Backspace" ->
          update(socket, :input, fn input ->
            String.slice(input, 0..-2//1)
          end)

        "ArrowUp" ->
          navigate_history(socket, :up)

        "ArrowDown" ->
          navigate_history(socket, :down)

        char when byte_size(char) == 1 ->
          update(socket, :input, fn input -> input <> char end)

        _ ->
          socket
      end
      |> update_display()

    {:noreply, socket}
  end

  defp create_initial_buffer do
    Buffer.create_blank_buffer(@width, @height)
    |> Box.draw_box(0, 0, @width, @height, :double)
    |> Box.draw_horizontal_line(0, @output_height, @width, "=")
  end

  defp update_display(socket) do
    buffer = create_initial_buffer()

    # Render output lines (last N lines that fit)
    output_lines = Enum.take(socket.assigns.output_lines, -(@output_height - 2))
    buffer =
      output_lines
      |> Enum.with_index()
      |> Enum.reduce(buffer, fn {line, idx}, buf ->
        Buffer.write_at(buf, 2, idx + 1, line, %{})
      end)

    # Render input line
    buffer =
      Buffer.write_at(
        buffer,
        2, @output_height + 1,
        "> #{socket.assigns.input}",
        %{fg_color: :cyan}
      )

    assign(socket, buffer: buffer)
  end

  defp execute_command(socket) do
    input = String.trim(socket.assigns.input)

    if input != "" do
      output = process_command(input)

      socket
      |> update(:output_lines, fn lines ->
        lines ++ ["> #{input}"] ++ output
      end)
      |> update(:history, fn hist -> [input | hist] end)
      |> assign(history_index: 0)
    else
      socket
    end
  end

  defp process_command("help") do
    [
      "Available commands:",
      "  help      - Show this help",
      "  clear     - Clear output",
      "  echo TEXT - Echo back text",
      "  time      - Show current time",
      "  exit      - Close terminal"
    ]
  end

  defp process_command("clear") do
    # Clear handled separately
    []
  end

  defp process_command("echo " <> text) do
    [text]
  end

  defp process_command("time") do
    [DateTime.utc_now() |> to_string()]
  end

  defp process_command("exit") do
    ["Goodbye!"]
  end

  defp process_command(cmd) do
    ["Unknown command: #{cmd}. Type 'help' for available commands."]
  end

  defp clear_input(socket) do
    assign(socket, input: "")
  end

  defp navigate_history(socket, direction) do
    history = socket.assigns.history
    index = socket.assigns.history_index

    new_index =
      case direction do
        :up -> min(index + 1, length(history))
        :down -> max(index - 1, 0)
      end

    input =
      if new_index > 0 and new_index <= length(history) do
        Enum.at(history, new_index - 1)
      else
        ""
      end

    socket
    |> assign(input: input)
    |> assign(history_index: new_index)
  end
end

What You've Built

  • Command input - Type commands at the bottom
  • Command history - Up/Down arrows navigate history
  • Scrollable output - Shows last 20 lines
  • Real-time rendering - Instant visual updates
  • Themed UI - Professional Nord theme

Try It

mix phx.server
# Visit http://localhost:4000/terminal
# Type: help
# Type: echo Hello World
# Type: time
# Press up arrow to recall commands

What's Next?

Add More Features

Syntax highlighting:

# Use Style module
style = Raxol.Core.Style.new(fg_color: :green, bold: true)
buffer = Buffer.write_at(buffer, x, y, "def function", style)

Progress bars:

# Draw a progress bar
progress = 0.75  # 75%
width = 40
filled = round(width * progress)

buffer = Box.fill_area(buffer, 2, 10, filled, 1, "█", %{fg_color: :green})
buffer = Box.fill_area(buffer, 2 + filled, 10, width - filled, 1, "░", %{fg_color: :gray})

Multiple panels:

# Split screen layout
buffer = Box.draw_box(buffer, 0, 0, 40, 24, :single)      # Left panel
buffer = Box.draw_box(buffer, 40, 0, 40, 24, :single)     # Right panel

Explore More

Examples Directory

Check out working examples:

# Run examples
mix run examples/core/01_hello_buffer.exs
mix run examples/core/02_box_drawing.exs
mix run examples/liveview/01_simple_terminal/

Performance Targets

Raxol.Core is built for speed:

  • Buffer operations: < 1ms for 80x24 grids
  • Rendering: < 16ms (60fps capable)
  • Memory: < 100KB per buffer
  • Dependencies: Zero runtime dependencies

Get Help


Frequently Asked Questions

Do I need the full Raxol framework?

No! Start with raxol_core for just buffers, add raxol_liveview for web integration, or use raxol for everything.

Can I use this with my existing Phoenix app?

Yes! Raxol.LiveView integrates seamlessly with Phoenix LiveView.

Does this work in production?

Yes. Raxol is production-ready with 99%+ test coverage and performance benchmarks.

Can I customize the themes?

Yes! Either use built-in themes or create custom CSS. See Theming Cookbook.

What about mobile browsers?

Yes! The LiveView component is responsive and works on mobile (though keyboard input is limited to mobile keyboards).


Quick Reference

Buffer Operations

# Create
buffer = Buffer.create_blank_buffer(80, 24)

# Write
buffer = Buffer.write_at(buffer, x, y, "text", style)

# Read
cell = Buffer.get_cell(buffer, x, y)

# Clear
buffer = Buffer.clear(buffer)

# Resize
buffer = Buffer.resize(buffer, new_width, new_height)

# Render
output = Buffer.to_string(buffer)
diff = Renderer.render_diff(old_buffer, new_buffer)

Box Drawing

# Styles: :single, :double, :rounded, :heavy, :dashed
buffer = Box.draw_box(buffer, x, y, width, height, :double)

# Lines
buffer = Box.draw_horizontal_line(buffer, x, y, length, "-")
buffer = Box.draw_vertical_line(buffer, x, y, length, "|")

# Fill
buffer = Box.fill_area(buffer, x, y, width, height, " ", style)

Styles

# Create style
style = Style.new(
  fg_color: :cyan,
  bg_color: :black,
  bold: true,
  italic: false,
  underline: false
)

# Merge styles
new_style = Style.merge(base_style, %{bold: true})

# Colors: :black, :red, :green, :yellow, :blue, :magenta, :cyan, :white
# Or RGB: {255, 128, 0}
# Or 256-color: 42

Ready to build? Pick a tutorial above and start coding!