Migration Guide: DIY to Raxol
View SourceAlready 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?
- Migration Strategies
- Adapting Your Buffer Format
- Incremental Migration
- Feature Parity Checklist
- Case Study: droodotfoo
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 structure3. 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
endPros:
- 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
endPros:
- 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
endPros:
- 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
endAdapter: 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
endPerformance 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 4Incremental 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
]
endRun 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
endPhase 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
endPhase 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
endFeature 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
endPain Points
- Maintenance burden - 500+ lines of buffer code to maintain
- No testing utilities - Hard to test rendering logic
- Performance unknowns - No benchmarking infrastructure
- Missing features - Wanted more themes, better diffing
Migration Approach (Recommended)
Phase 1: LiveView Integration Only
# mix.exs
def deps do
[
{:raxol_liveview, "~> 2.0"}, # Just for HTML rendering
# Keep their buffer code for now
]
endPhase 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
endPhase 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
- Start with LiveView - Biggest immediate value
- Keep adapters simple - Don't optimize prematurely
- Measure everything - Benchmark before/after
- Gradual rollout - Feature flags in production
- 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:
- Keep that part of your code (use Raxol for other parts)
- Extend Raxol with a plugin
- 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
- Quickstart - Get started quickly
- Core Concepts - Understand architecture
- API Reference - Complete function docs
- Cookbook - Practical recipes
- GitHub Issues - Ask questions, request features
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.