Getting Started with Raxol.Core

View Source

Quick start guide for using Raxol's lightweight buffer primitives.

What is Raxol.Core?

Raxol.Core is a standalone package providing pure functional terminal buffer operations with:

  • Zero dependencies - No external runtime requirements
  • < 100KB compiled - Minimal footprint
  • < 1ms operations - High performance
  • Pure functional - No GenServers, no state, thread-safe

Perfect for:

  • Adding terminal rendering to existing Elixir apps
  • Building custom terminal UIs
  • LiveView terminal components
  • CLI tools and scripts
  • Incremental adoption (use just what you need)

5-Minute Tutorial: Your First Buffer

# 1. Create a buffer
alias Raxol.Core.{Buffer, Box}

buffer = Buffer.create_blank_buffer(40, 10)
# => %{lines: [...], width: 40, height: 10}

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

# 3. Draw a box around it
buffer = Box.draw_box(buffer, 0, 0, 40, 10, :double)

# 4. Render to terminal
IO.puts(Buffer.to_string(buffer))

Output:


                                      
                                      
     Hello, Raxol!                    
                                      
                                      
                                      
                                      
                                      

That's it! You've created your first terminal UI.

10-Minute Tutorial: Interactive Buffer

Let's build a simple interactive display that updates.

defmodule SimpleCounter do
  alias Raxol.Core.{Buffer, Box, Renderer}

  def run do
    # Initial state
    buffer = Buffer.create_blank_buffer(30, 8)

    # Draw static UI
    buffer = Box.draw_box(buffer, 0, 0, 30, 8, :single)
    buffer = Buffer.write_at(buffer, 2, 1, "Counter Demo", %{})
    buffer = Box.draw_horizontal_line(buffer, 1, 2, 28, "-")

    # Render initial state
    IO.write("\e[2J\e[H")  # Clear screen
    IO.puts(Buffer.to_string(buffer))

    # Update loop
    loop(buffer, 0)
  end

  defp loop(old_buffer, count) do
    # Update counter display
    new_buffer = Buffer.write_at(old_buffer, 2, 4, "Count: #{count}    ", %{})

    # Only render what changed
    diff = Renderer.render_diff(old_buffer, new_buffer)
    Enum.each(diff, &IO.write/1)

    # Wait and continue
    Process.sleep(1000)
    loop(new_buffer, count + 1)
  end
end

SimpleCounter.run()

Key concepts demonstrated:

  1. Persistent UI - Draw boxes and labels once
  2. Efficient updates - Only redraw changed parts
  3. Diff rendering - render_diff/2 calculates minimal updates

15-Minute Tutorial: Styled Components

Add colors and styles to make it look professional.

defmodule StyledDashboard do
  alias Raxol.Core.{Buffer, Box, Style}

  def create_dashboard do
    buffer = Buffer.create_blank_buffer(60, 20)

    # Title bar with style
    title_style = Style.new(bold: true, fg_color: :white, bg_color: :blue)
    buffer = Box.fill_area(buffer, 0, 0, 60, 1, " ", title_style)
    buffer = Buffer.write_at(buffer, 2, 0, "System Dashboard", title_style)

    # Status panel (green)
    buffer = draw_panel(buffer, 2, 2, 26, 8, "System Status", :green)
    buffer = Buffer.write_at(buffer, 4, 4, "CPU: 45%", %{})
    buffer = Buffer.write_at(buffer, 4, 5, "Memory: 2.1GB", %{})
    buffer = Buffer.write_at(buffer, 4, 6, "Disk: 450GB free", %{})

    # Alerts panel (yellow)
    buffer = draw_panel(buffer, 32, 2, 26, 8, "Alerts", :yellow)
    buffer = Buffer.write_at(buffer, 34, 4, "3 warnings", %{fg_color: :yellow})
    buffer = Buffer.write_at(buffer, 34, 5, "0 errors", %{fg_color: :green})

    # Log panel
    buffer = draw_panel(buffer, 2, 11, 56, 8, "Recent Logs", :cyan)
    buffer = Buffer.write_at(buffer, 4, 13, "[INFO] Server started", %{})
    buffer = Buffer.write_at(buffer, 4, 14, "[WARN] High memory usage", %{fg_color: :yellow})
    buffer = Buffer.write_at(buffer, 4, 15, "[INFO] Request processed", %{})

    buffer
  end

  defp draw_panel(buffer, x, y, width, height, title, color) do
    header_style = Style.new(bold: true, fg_color: color)

    buffer
    |> Box.draw_box(x, y, width, height, :single)
    |> Buffer.write_at(x + 2, y, " #{title} ", header_style)
  end
end

# Render the dashboard
buffer = StyledDashboard.create_dashboard()
IO.puts(Buffer.to_string(buffer))

Concepts:

  1. Style composition - Build reusable style maps
  2. Color coding - Visual hierarchy with colors
  3. Component patterns - Reusable draw_panel/6 function
  4. Layout - Multi-panel grid layout

Common Patterns

Pattern 1: Double Buffering

Prevent flicker by rendering off-screen:

defmodule DoubleBuffer do
  def render_frame(old_buffer, new_content) do
    # Build new frame completely
    new_buffer = Buffer.create_blank_buffer(80, 24)
    new_buffer = draw_ui(new_buffer, new_content)

    # Calculate diff and render
    diff = Renderer.render_diff(old_buffer, new_buffer)
    Enum.each(diff, &IO.write/1)

    new_buffer  # Return for next frame
  end

  defp draw_ui(buffer, content) do
    buffer
    |> Box.draw_box(0, 0, 80, 24, :double)
    |> Buffer.write_at(5, 5, content, %{})
  end
end

Pattern 2: Partial Updates

Update specific regions efficiently:

def update_status_line(buffer, status) do
  # Clear the line first
  buffer = Box.fill_area(buffer, 0, 23, 80, 1, " ", %{})

  # Write new status
  Buffer.write_at(buffer, 2, 23, status, %{fg_color: :cyan})
end

Pattern 3: Grid Layouts

Create responsive grid layouts:

defmodule GridLayout do
  def create_grid(buffer, cols, rows) do
    {width, height} = {buffer.width, buffer.height}
    cell_width = div(width, cols)
    cell_height = div(height, rows)

    for row <- 0..(rows - 1),
        col <- 0..(cols - 1),
        reduce: buffer do
      acc ->
        x = col * cell_width
        y = row * cell_height
        Box.draw_box(acc, x, y, cell_width, cell_height, :single)
    end
  end
end

Pattern 4: Text Centering

Center text in a region:

def center_text(buffer, x, y, width, text) do
  padding = div(width - String.length(text), 2)
  Buffer.write_at(buffer, x + padding, y, text, %{})
end

Performance Tips

1. Minimize Buffer Operations

# Good - Chain operations
buffer
|> Buffer.write_at(0, 0, "Line 1")
|> Buffer.write_at(0, 1, "Line 2")
|> Buffer.write_at(0, 2, "Line 3")

# Avoid - Intermediate variables
buffer = Buffer.write_at(buffer, 0, 0, "Line 1")
buffer = Buffer.write_at(buffer, 0, 1, "Line 2")
buffer = Buffer.write_at(buffer, 0, 2, "Line 3")

2. Use Diff Rendering

# Good - Only update what changed
diff = Renderer.render_diff(old, new)
Enum.each(diff, &IO.write/1)

# Avoid - Full redraws
IO.write("\e[2J\e[H")  # Clear screen
IO.puts(Buffer.to_string(new))

3. Batch Style Applications

# Good - Reuse style maps
header_style = Style.new(bold: true, fg_color: :blue)
buffer
|> Buffer.write_at(0, 0, "Title 1", header_style)
|> Buffer.write_at(0, 2, "Title 2", header_style)

# Avoid - Creating styles repeatedly
buffer
|> Buffer.write_at(0, 0, "Title 1", %{bold: true, fg_color: :blue})
|> Buffer.write_at(0, 2, "Title 2", %{bold: true, fg_color: :blue})

4. Choose Appropriate Fill Operations

# Good - Use fill_area for large regions
buffer = Box.fill_area(buffer, 0, 0, 80, 24, " ", %{bg_color: :black})

# Avoid - Loop with set_cell
for y <- 0..23, x <- 0..79, reduce: buffer do
  acc -> Buffer.set_cell(acc, x, y, " ", %{bg_color: :black})
end

Debugging Tips

1. Print Buffer State

# Quick debug output
IO.puts("\n--- Buffer Debug ---")
IO.puts(Buffer.to_string(buffer))
IO.puts("--------------------\n")

2. Inspect Specific Cells

# Check what's at a position
cell = Buffer.get_cell(buffer, 5, 3)
IO.inspect(cell, label: "Cell at (5,3)")
# Cell at (5,3): %{char: "H", style: %{bold: true}}

3. Validate Buffer Dimensions

# Ensure buffer size is correct
IO.puts("Buffer: #{buffer.width}x#{buffer.height}")
IO.puts("Lines: #{length(buffer.lines)}")
IO.puts("Cells per line: #{length(hd(buffer.lines).cells)}")

Integration Examples

CLI Script

#!/usr/bin/env elixir
Mix.install([{:raxol, "~> 2.0"}])

alias Raxol.Core.{Buffer, Box}

buffer = Buffer.create_blank_buffer(50, 10)
buffer = Box.draw_box(buffer, 0, 0, 50, 10, :double)
buffer = Buffer.write_at(buffer, 10, 4, "Hello from Raxol!", %{})

IO.puts(Buffer.to_string(buffer))

Phoenix LiveView Component

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

  def mount(_params, _session, socket) do
    buffer = Buffer.create_blank_buffer(80, 24)
    buffer = Box.draw_box(buffer, 0, 0, 80, 24, :single)

    {:ok, assign(socket, buffer: buffer, output: Buffer.to_string(buffer))}
  end

  def render(assigns) do
    ~H"""
    <pre class="terminal"><%= @output %></pre>
    """
  end

  def handle_event("update", %{"text" => text}, socket) do
    buffer = Buffer.write_at(socket.assigns.buffer, 2, 2, text, %{})
    {:noreply, assign(socket, buffer: buffer, output: Buffer.to_string(buffer))}
  end
end

Mix Task

defmodule Mix.Tasks.MyApp.Dashboard do
  use Mix.Task
  alias Raxol.Core.{Buffer, Box}

  def run(_args) do
    buffer = create_dashboard()
    IO.puts(Buffer.to_string(buffer))
  end

  defp create_dashboard do
    Buffer.create_blank_buffer(60, 20)
    |> Box.draw_box(0, 0, 60, 20, :double)
    |> Buffer.write_at(20, 1, "My Dashboard", %{bold: true})
  end
end

Common Pitfalls

1. Coordinate System

Remember: coordinates are (x, y) where x=column, y=row, both 0-indexed.

# Correct
Buffer.write_at(buffer, 5, 3, "Text")  # Column 5, Row 3

# Common mistake - mixing up x/y
Buffer.write_at(buffer, 3, 5, "Text")  # Different position!

2. Buffer Boundaries

Out-of-bounds writes are silently ignored:

buffer = Buffer.create_blank_buffer(10, 5)
buffer = Buffer.write_at(buffer, 100, 100, "Lost!")  # No error, no effect

# Always validate coordinates if needed
if x < buffer.width and y < buffer.height do
  buffer = Buffer.write_at(buffer, x, y, text)
end

3. Style Immutability

Styles are maps, not merged automatically:

# Wrong - second write loses style
buffer
|> Buffer.write_at(0, 0, "Bold", %{bold: true})
|> Buffer.write_at(0, 0, "Blue Bold", %{fg_color: :blue})  # Lost bold!

# Correct - merge styles
base_style = %{bold: true}
new_style = Style.merge(base_style, %{fg_color: :blue})
buffer = Buffer.write_at(buffer, 0, 0, "Blue Bold", new_style)

4. String Length vs Grapheme Count

Use grapheme-aware functions:

# Wrong - byte length
text = "Hello 👋"
length = byte_size(text)  # 10 (includes emoji bytes)

# Correct - grapheme count
length = String.length(text)  # 7 (visual characters)

Next Steps

Getting Help

  • GitHub Issues: Report bugs or request features
  • Documentation: Browse the docs directory
  • Examples: Working code in the examples directory

Happy terminal hacking!