Getting Started with Raxol.Core
View SourceQuick 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:
- Persistent UI - Draw boxes and labels once
- Efficient updates - Only redraw changed parts
- Diff rendering -
render_diff/2calculates 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:
- Style composition - Build reusable style maps
- Color coding - Visual hierarchy with colors
- Component patterns - Reusable
draw_panel/6function - 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
endPattern 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})
endPattern 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
endPattern 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, %{})
endPerformance 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})
endDebugging 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
endMix 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
endCommon 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)
end3. 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
- API Reference: See BUFFER_API.md for complete function documentation
- Architecture: Read ARCHITECTURE.md to understand design decisions
- Examples: Explore examples/core/README.md for more patterns
- Benchmarks: Check benchmarks for performance data
Getting Help
- GitHub Issues: Report bugs or request features
- Documentation: Browse the docs directory
- Examples: Working code in the examples directory
Happy terminal hacking!