Migration Guide: DIY to Raxol

View Source

Already built your own terminal rendering? This guide shows how to integrate or migrate to Raxol.

New to Raxol? See the Package Guide to choose the right package for your needs.

Table of Contents


Why Migrate?

You've built a working terminal renderer. Why consider Raxol?

What Raxol Adds

1. Phoenix LiveView Integration

If you're rendering terminals in web apps, Raxol.LiveView handles:

  • Buffer → HTML conversion
  • CSS theming (5 built-in themes)
  • Event handling (keyboard, mouse, paste)
  • 60fps rendering optimizations

2. Testing Utilities

# Your current testing (probably):
output = render_buffer(buffer)
assert output =~ "expected text"  # Fragile string matching

# With Raxol:
buffer = Buffer.write_at(buffer, 5, 3, "expected text")
cell = Buffer.get_cell(buffer, 5, 3)
assert cell.char == "e"  # Test actual data structure

3. Performance Optimizations

  • Diff rendering (50x faster updates)
  • Benchmarking suite
  • Memory profiling
  • Automated performance regression detection

4. Documentation & Examples

  • Comprehensive API docs
  • Cookbook recipes
  • Working examples
  • Active community

What You Keep

Your code. Raxol is designed for incremental adoption:

# Keep your existing code
MyApp.TerminalRenderer.render(your_buffer)

# Add Raxol for specific features
html = Raxol.LiveView.TerminalBridge.buffer_to_html(your_buffer)

Migration Strategies

Choose the approach that fits your needs.

Strategy 1: Side-by-Side (Lowest Risk)

Run both implementations, compare outputs:

defmodule MyApp.Renderer do
  def render(data) do
    # Your implementation
    your_buffer = YourRenderer.create_buffer(data)
    your_output = YourRenderer.render(your_buffer)

    # Raxol implementation (parallel)
    raxol_buffer = RaxolAdapter.from_your_format(your_buffer)
    raxol_output = Raxol.Core.Buffer.to_string(raxol_buffer)

    # Compare (in dev/test)
    if Mix.env() != :prod do
      compare_outputs(your_output, raxol_output)
    end

    # Use your implementation (for now)
    your_output
  end
end

Pros:

  • Zero risk to production
  • Validate Raxol behavior
  • Identify edge cases

Cons:

  • Doubled rendering cost (dev/test only)
  • Maintenance overhead

When to use: Validating migration, finding gaps

Strategy 2: Feature Flagging

Gradually roll out Raxol to users:

defmodule MyApp.Renderer do
  def render(data, opts \\ []) do
    use_raxol? = Keyword.get(opts, :use_raxol, false) ||
                 Application.get_env(:my_app, :raxol_enabled, false)

    if use_raxol? do
      render_with_raxol(data)
    else
      render_with_your_code(data)
    end
  end
end

# config/config.exs
config :my_app,
  raxol_enabled: System.get_env("RAXOL_ENABLED") == "true"

Pros:

  • Gradual rollout (1% → 10% → 100%)
  • Easy rollback
  • A/B testing possible

Cons:

  • Maintain both paths
  • Feature flag complexity

When to use: Production migration with safety net

Strategy 3: Module Replacement

Replace your module with Raxol adapter:

# Before:
defmodule MyApp.Buffer do
  def create(width, height), do: # your code
  def write_at(buffer, x, y, text), do: # your code
end

# After:
defmodule MyApp.Buffer do
  # Delegate to Raxol
  defdelegate create(width, height), to: Raxol.Core.Buffer, as: :create_blank_buffer
  defdelegate write_at(buffer, x, y, text), to: Raxol.Core.Buffer
  defdelegate write_at(buffer, x, y, text, style), to: Raxol.Core.Buffer

  # Keep custom functions
  def your_special_function(buffer), do: # your code
end

Pros:

  • Minimal code changes
  • Keep existing API
  • Gradual internal migration

Cons:

  • API compatibility required
  • May hide Raxol features

When to use: Drop-in replacement, minimal changes

Strategy 4: Clean Break

Rewrite using Raxol from scratch:

# Old code (delete):
defmodule MyApp.OldRenderer do
  # 500 lines of custom buffer code
end

# New code:
defmodule MyApp.Renderer do
  alias Raxol.Core.{Buffer, Box, Renderer}

  def render(data) do
    Buffer.create_blank_buffer(80, 24)
    |> Box.draw_box(0, 0, 80, 24, :single)
    |> Buffer.write_at(10, 5, data.title)
  end
end

Pros:

  • Simplest long-term
  • Full Raxol feature access
  • No legacy code

Cons:

  • Highest risk
  • Requires comprehensive testing
  • All-or-nothing

When to use: Small codebases, greenfield projects


Adapting Your Buffer Format

Most DIY implementations use similar structures. Here's how to adapt.

Common DIY Format

# Your buffer (typical structure)
%{
  width: 80,
  height: 24,
  cells: [
    # Flat array of cells
    %{x: 0, y: 0, char: "H", fg: :cyan},
    %{x: 1, y: 0, char: "e", fg: :cyan},
    # ...
  ]
}

Raxol Format

# Raxol buffer (nested structure)
%{
  width: 80,
  height: 24,
  lines: [
    # Array of lines
    %{cells: [
      # Each line has cells
      %{char: "H", style: %{fg_color: :cyan}},
      %{char: "e", style: %{fg_color: :cyan}},
      # ...
    ]},
    # ...
  ]
}

Adapter: Your Format → Raxol

defmodule MyApp.BufferAdapter do
  @doc "Convert your buffer format to Raxol format"
  def to_raxol(your_buffer) do
    # Create blank Raxol buffer
    raxol_buffer = Raxol.Core.Buffer.create_blank_buffer(
      your_buffer.width,
      your_buffer.height
    )

    # Transfer cells
    Enum.reduce(your_buffer.cells, raxol_buffer, fn cell, buf ->
      style = convert_style(cell)
      Raxol.Core.Buffer.set_cell(buf, cell.x, cell.y, cell.char, style)
    end)
  end

  @doc "Convert your style format to Raxol style"
  defp convert_style(cell) do
    %{
      fg_color: cell.fg,
      bg_color: Map.get(cell, :bg),
      bold: Map.get(cell, :bold, false),
      italic: Map.get(cell, :italic, false),
      underline: Map.get(cell, :underline, false)
    }
  end
end

Adapter: Raxol → Your Format

defmodule MyApp.BufferAdapter do
  @doc "Convert Raxol buffer to your format (if needed)"
  def from_raxol(raxol_buffer) do
    cells =
      for {line, y} <- Enum.with_index(raxol_buffer.lines),
          {cell, x} <- Enum.with_index(line.cells) do
        %{
          x: x,
          y: y,
          char: cell.char,
          fg: cell.style[:fg_color],
          bg: cell.style[:bg_color],
          bold: cell.style[:bold] || false
        }
      end

    %{
      width: raxol_buffer.width,
      height: raxol_buffer.height,
      cells: cells
    }
  end
end

Performance Consideration

Adapters add overhead. Benchmark both approaches:

# Benchmark: Direct Raxol
{time_raxol, _} = :timer.tc(fn ->
  Raxol.Core.Buffer.create_blank_buffer(80, 24)
  |> Raxol.Core.Buffer.write_at(10, 5, "Test")
end)

# Benchmark: Adapter path
{time_adapter, _} = :timer.tc(fn ->
  your_buffer = YourRenderer.create_buffer(80, 24)
  raxol_buffer = BufferAdapter.to_raxol(your_buffer)
end)

IO.puts("Direct: #{time_raxol}μs, Adapter: #{time_adapter}μs")
# If adapter is > 2x slower, consider Strategy 3 or 4

Incremental Migration

Step-by-step migration plan.

Phase 1: Add Raxol Dependency (Week 1)

# mix.exs
def deps do
  [
    {:raxol_core, "~> 2.0"},  # Start with just core
    # ... your other deps
  ]
end

Run tests, ensure no conflicts.

Phase 2: Create Adapters (Week 1-2)

# test/support/buffer_adapter_test.exs
defmodule MyApp.BufferAdapterTest do
  use ExUnit.Case

  test "converts your buffer to Raxol" do
    your_buffer = YourRenderer.create_buffer(10, 5)
    your_buffer = YourRenderer.write_at(your_buffer, 2, 3, "Hi")

    raxol_buffer = BufferAdapter.to_raxol(your_buffer)

    cell = Raxol.Core.Buffer.get_cell(raxol_buffer, 2, 3)
    assert cell.char == "H"
  end

  test "round-trip conversion preserves data" do
    original = YourRenderer.create_buffer(10, 5)
    original = YourRenderer.write_at(original, 2, 3, "Test")

    raxol = BufferAdapter.to_raxol(original)
    back = BufferAdapter.from_raxol(raxol)

    assert buffer_equal?(original, back)
  end
end

Phase 3: Add LiveView Support (Week 2-3)

If you're using Phoenix:

# mix.exs
def deps do
  [
    {:raxol_core, "~> 2.0"},
    {:raxol_liveview, "~> 2.0"},  # Add LiveView support
    # ...
  ]
end
# lib/my_app_web/live/terminal_live.ex
defmodule MyAppWeb.TerminalLive do
  use MyAppWeb, :live_view

  def render(assigns) do
    ~H"""
    <.live_component
      module={Raxol.LiveView.TerminalComponent}
      id="terminal"
      buffer={@raxol_buffer}
      theme={:nord}
    />
    """
  end

  def handle_info(:update, socket) do
    # Your existing logic
    your_buffer = YourRenderer.update(socket.assigns.your_buffer)

    # Convert to Raxol for rendering
    raxol_buffer = BufferAdapter.to_raxol(your_buffer)

    {:noreply, assign(socket,
      your_buffer: your_buffer,
      raxol_buffer: raxol_buffer
    )}
  end
end

Phase 4: Replace Components (Week 3-6)

Gradually replace custom components:

# Week 3: Replace box drawing
# Before:
your_buffer = YourRenderer.draw_box(buffer, 0, 0, 10, 5)

# After:
raxol_buffer = Raxol.Core.Box.draw_box(buffer, 0, 0, 10, 5, :single)

# Week 4: Replace text rendering
# Before:
your_buffer = YourRenderer.write_colored(buffer, 5, 3, "Text", :cyan)

# After:
raxol_buffer = Raxol.Core.Buffer.write_at(buffer, 5, 3, "Text", %{fg_color: :cyan})

# Week 5: Replace diffing
# Before:
diff = YourRenderer.calculate_diff(old, new)

# After:
diff = Raxol.Core.Renderer.render_diff(old, new)

Phase 5: Remove Old Code (Week 6+)

Once confidence is high:

# Archive old code (don't delete yet)
git mv lib/my_app/old_renderer.ex lib/my_app/archived/

# Update imports throughout codebase
# YourRenderer -> Raxol.Core.Buffer
# YourRenderer.Box -> Raxol.Core.Box

# Remove adapters (no longer needed)
git rm lib/my_app/buffer_adapter.ex

Phase 6: Monitor & Optimize (Ongoing)

# Add performance tracking
defmodule MyApp.RenderMetrics do
  def track_render(fun) do
    {time, result} = :timer.tc(fun)

    MyApp.Metrics.histogram("terminal.render_time_us", time)

    if time > 16_000 do
      Logger.warn("Slow render: #{time}μs")
    end

    result
  end
end

Feature Parity Checklist

Ensure Raxol can do everything your code does.

Buffer Operations

  • [ ] Create buffer with dimensions
  • [ ] Write text at coordinates
  • [ ] Read cell at coordinates
  • [ ] Clear buffer
  • [ ] Resize buffer
  • [ ] Fill rectangular area
  • [ ] Copy region to another buffer

Styling

  • [ ] Foreground colors (16 basic)
  • [ ] Background colors (16 basic)
  • [ ] 256-color palette support
  • [ ] RGB true color support
  • [ ] Bold text
  • [ ] Italic text
  • [ ] Underline
  • [ ] Strikethrough
  • [ ] Reverse video
  • [ ] Custom attributes

Box Drawing

  • [ ] Single-line boxes
  • [ ] Double-line boxes
  • [ ] Rounded corners
  • [ ] Horizontal lines
  • [ ] Vertical lines
  • [ ] Custom line characters

Rendering

  • [ ] Full buffer render to string
  • [ ] Diff rendering (only changed cells)
  • [ ] ANSI escape code generation
  • [ ] HTML output (for web)
  • [ ] Cursor positioning

Advanced Features

  • [ ] Unicode support (grapheme clusters)
  • [ ] Wide characters (CJK)
  • [ ] Zero-width characters (combining diacritics)
  • [ ] Emoji support
  • [ ] Sixel graphics (if applicable)
  • [ ] Custom rendering backends

Performance

  • [ ] < 1ms buffer operations
  • [ ] < 16ms full renders (60fps)
  • [ ] Memory efficient (< 100KB per buffer)
  • [ ] Diff calculation < 2ms

If any features are missing: Open a GitHub issue! We'll add them or help you extend Raxol.


Case Study: droodotfoo

Real-world example of DIY → Raxol migration.

Their Setup (Before)

# lib/droodotfoo/terminal_bridge.ex
defmodule Droodotfoo.TerminalBridge do
  use GenServer

  # Custom buffer format
  defstruct [:width, :height, :cells, :cache]

  # Custom HTML rendering
  def buffer_to_html(buffer) do
    # ~300 lines of conversion logic
  end

  # Custom diffing
  def calculate_diff(old, new) do
    # ~100 lines of diff algorithm
  end
end

Pain Points

  1. Maintenance burden - 500+ lines of buffer code to maintain
  2. No testing utilities - Hard to test rendering logic
  3. Performance unknowns - No benchmarking infrastructure
  4. Missing features - Wanted more themes, better diffing

Phase 1: LiveView Integration Only

# mix.exs
def deps do
  [
    {:raxol_liveview, "~> 2.0"},  # Just for HTML rendering
    # Keep their buffer code for now
  ]
end

Phase 2: Adapter

defmodule Droodotfoo.RaxolAdapter do
  def to_raxol(droodotfoo_buffer) do
    # Convert their format to Raxol
    Raxol.Core.Buffer.create_blank_buffer(
      droodotfoo_buffer.width,
      droodotfoo_buffer.height
    )
    |> populate_from_droodotfoo(droodotfoo_buffer)
  end

  defp populate_from_droodotfoo(raxol_buffer, droodotfoo_buffer) do
    Enum.reduce(droodotfoo_buffer.cells, raxol_buffer, fn {coord, cell}, buf ->
      {x, y} = coord
      style = %{
        fg_color: cell.fg_color,
        bg_color: cell.bg_color,
        bold: cell.bold
      }
      Raxol.Core.Buffer.set_cell(buf, x, y, cell.char, style)
    end)
  end
end

Phase 3: Replace HTML Rendering

# Before: 300 lines of custom code
Droodotfoo.TerminalBridge.buffer_to_html(buffer)

# After: One line
buffer
|> Droodotfoo.RaxolAdapter.to_raxol()
|> Raxol.LiveView.TerminalBridge.buffer_to_html()

Result:

  • 300 lines deleted
  • 5 built-in themes (vs 1 custom)
  • Better performance (1.2ms vs 8ms avg)
  • Easier testing

Phase 4: (Optional) Full Migration

Replace buffer implementation:

# Delete custom buffer code (~200 lines)
# Use Raxol.Core.Buffer directly
buffer = Raxol.Core.Buffer.create_blank_buffer(80, 24)

Lessons Learned

  1. Start with LiveView - Biggest immediate value
  2. Keep adapters simple - Don't optimize prematurely
  3. Measure everything - Benchmark before/after
  4. Gradual rollout - Feature flags in production
  5. Document differences - Note any behavior changes

Getting Help

Common Questions

Q: Will this break my existing code?

A: No. Raxol can run alongside your code. Use adapters for gradual migration.

Q: What if Raxol is missing a feature I need?

A: Three options:

  1. Keep that part of your code (use Raxol for other parts)
  2. Extend Raxol with a plugin
  3. Open a GitHub issue (we'll help add it)

Q: How long does migration typically take?

A: Depends on strategy:

  • LiveView only: 1-2 days
  • Partial migration: 2-4 weeks
  • Full migration: 4-8 weeks

Q: Can I contribute my adapter back to Raxol?

A: Yes! We'd love to see it. Open a PR with your adapter in lib/raxol/adapters/.

Resources

Community Support

  • Post in GitHub Discussions
  • Join our Discord (coming soon)
  • Tag @Hydepwns on Twitter/X

Ready to migrate? Start with Strategy 1 (side-by-side) to validate, then choose your path.

Good luck! We're here to help.