Packages
Multi-surface application runtime for Elixir. One TEA module renders to terminal, browser (LiveView), SSH, and MCP (agents). 30+ widgets, flexbox + CSS grid, AI agent runtime, distributed swarm with CRDTs, time-travel debugging, session recording, sandboxed REPL, and agentic commerce.
Current section
Files
Jump to
Current section
Files
lib/raxol/terminal/rendering/optimized_style_renderer.ex
defmodule Raxol.Terminal.Rendering.OptimizedStyleRenderer do
@moduledoc """
Phase 3 optimized terminal renderer with efficient style handling.
Key optimizations:
1. Pre-compiled style patterns at compile time
2. Direct pattern matching instead of cache lookups
3. Minimal memory allocations
4. No process dictionary usage
5. Efficient string building with iodata
Target: <500μs render time (from current 1200-2600μs)
"""
alias Raxol.Terminal.ScreenBuffer
# alias Raxol.Terminal.Cell # Not used currently
alias Raxol.Terminal.ANSI.TextFormatting
# Pre-compiled at compile time for zero runtime cost
# @default_style %TextFormatting{} # Not used currently
# Common style patterns pre-compiled as binaries
@style_patterns %{
# Empty/default style
{nil, nil, false, false, false} => "",
# Basic colors (ANSI 16)
{:black, nil, false, false, false} => "color:#000000",
{:red, nil, false, false, false} => "color:#cc0000",
{:green, nil, false, false, false} => "color:#4e9a06",
{:yellow, nil, false, false, false} => "color:#c4a000",
{:blue, nil, false, false, false} => "color:#3465a4",
{:magenta, nil, false, false, false} => "color:#75507b",
{:cyan, nil, false, false, false} => "color:#06989a",
{:white, nil, false, false, false} => "color:#d3d7cf",
# Bright colors
{:bright_black, nil, false, false, false} => "color:#555753",
{:bright_red, nil, false, false, false} => "color:#ef2929",
{:bright_green, nil, false, false, false} => "color:#8ae234",
{:bright_yellow, nil, false, false, false} => "color:#fce94f",
{:bright_blue, nil, false, false, false} => "color:#729fcf",
{:bright_magenta, nil, false, false, false} => "color:#ad7fa8",
{:bright_cyan, nil, false, false, false} => "color:#34e2e2",
{:bright_white, nil, false, false, false} => "color:#eeeeec",
# Bold variants
{nil, nil, true, false, false} => "font-weight:bold",
{:red, nil, true, false, false} => "color:#cc0000;font-weight:bold",
{:green, nil, true, false, false} => "color:#4e9a06;font-weight:bold",
{:blue, nil, true, false, false} => "color:#3465a4;font-weight:bold",
# Underline variants
{nil, nil, false, false, true} => "text-decoration:underline",
{:red, nil, false, false, true} =>
"color:#cc0000;text-decoration:underline",
# Italic variants
{nil, nil, false, true, false} => "font-style:italic"
}
@doc """
Renders the screen buffer to HTML with optimized style handling.
"""
def render(%ScreenBuffer{} = buffer, options \\ %{}) do
cursor_pos = Map.get(options, :cursor, nil)
buffer.cells
|> render_cells_to_iodata()
|> wrap_with_container()
|> maybe_add_cursor(cursor_pos, buffer.width)
|> IO.iodata_to_binary()
end
# Use iodata for efficient string building
defp render_cells_to_iodata(cells) do
cells
|> Enum.with_index()
|> Enum.map(fn {row, _y} ->
render_row_optimized(row)
end)
|> Enum.intersperse("\n")
end
defp render_row_optimized(row) do
# Group consecutive cells with same style
row
|> group_by_style()
|> Enum.map(&render_styled_span/1)
end
defp group_by_style(row) do
row
|> Enum.chunk_by(& &1.style)
|> Enum.map(fn cells ->
style = hd(cells).style
text = cells |> Enum.map(& &1.char) |> IO.iodata_to_binary()
{style, text}
end)
end
defp render_styled_span({style, text}) do
case get_style_string_fast(style) do
# No style needed
"" -> text
style_str -> ["<span style=\"", style_str, "\">", text, "</span>"]
end
end
# Direct pattern matching for maximum performance
defp get_style_string_fast(
%TextFormatting{
foreground: fg,
background: bg,
bold: bold,
italic: italic,
underline: underline
} = style
) do
# Try pre-compiled patterns first (most common cases)
key = {fg, bg, bold, italic, underline}
case Map.get(@style_patterns, key) do
nil ->
# Fall back to building style string for uncommon combinations
build_style_string_minimal(style)
style_str ->
style_str
end
end
# Minimal style string builder for uncommon cases
defp build_style_string_minimal(%TextFormatting{} = style) do
parts = []
parts =
case style.foreground do
nil -> parts
color -> [color_to_css(color, "color") | parts]
end
parts =
case style.background do
nil -> parts
color -> [color_to_css(color, "background-color") | parts]
end
parts = if style.bold, do: ["font-weight:bold" | parts], else: parts
parts = if style.italic, do: ["font-style:italic" | parts], else: parts
parts =
if style.underline, do: ["text-decoration:underline" | parts], else: parts
parts =
if style.strikethrough,
do: ["text-decoration:line-through" | parts],
else: parts
Enum.join(parts, ";")
end
# Efficient color conversion with pattern matching
defp color_to_css(color, property) when is_atom(color) do
hex =
case color do
:black -> "#000000"
:red -> "#cc0000"
:green -> "#4e9a06"
:yellow -> "#c4a000"
:blue -> "#3465a4"
:magenta -> "#75507b"
:cyan -> "#06989a"
:white -> "#d3d7cf"
:bright_black -> "#555753"
:bright_red -> "#ef2929"
:bright_green -> "#8ae234"
:bright_yellow -> "#fce94f"
:bright_blue -> "#729fcf"
:bright_magenta -> "#ad7fa8"
:bright_cyan -> "#34e2e2"
:bright_white -> "#eeeeec"
_ -> "#ffffff"
end
"#{property}:#{hex}"
end
defp color_to_css({:rgb, r, g, b}, property) do
"#{property}:rgb(#{r},#{g},#{b})"
end
defp color_to_css({:indexed, idx}, property) when idx in 0..255 do
# Convert 256-color index to RGB
hex = indexed_to_hex(idx)
"#{property}:#{hex}"
end
defp color_to_css(_, property) do
"#{property}:#ffffff"
end
# Pre-computed 256-color palette (first 16 entries shown, rest computed)
defp indexed_to_hex(idx) when idx < 16 do
# Standard ANSI colors
case idx do
0 -> "#000000"
1 -> "#cc0000"
2 -> "#4e9a06"
3 -> "#c4a000"
4 -> "#3465a4"
5 -> "#75507b"
6 -> "#06989a"
7 -> "#d3d7cf"
8 -> "#555753"
9 -> "#ef2929"
10 -> "#8ae234"
11 -> "#fce94f"
12 -> "#729fcf"
13 -> "#ad7fa8"
14 -> "#34e2e2"
15 -> "#eeeeec"
end
end
defp indexed_to_hex(idx) when idx < 232 do
# 216-color cube (6x6x6)
idx_offset = idx - 16
r = div(idx_offset, 36) * 51
g = rem(div(idx_offset, 6), 6) * 51
b = rem(idx_offset, 6) * 51
"##{to_hex(r)}#{to_hex(g)}#{to_hex(b)}"
end
defp indexed_to_hex(idx) do
# Grayscale ramp
gray = 8 + (idx - 232) * 10
"##{to_hex(gray)}#{to_hex(gray)}#{to_hex(gray)}"
end
defp to_hex(n) do
Integer.to_string(n, 16) |> String.pad_leading(2, "0")
end
defp wrap_with_container(content) do
["<pre class=\"terminal-output\">", content, "</pre>"]
end
defp maybe_add_cursor(content, nil, _width), do: content
defp maybe_add_cursor(content, {y, x}, _width) do
# Add cursor overlay element
cursor_style =
"position:absolute;left:#{x}ch;top:#{y}lh;width:1ch;height:1lh;background:rgba(255,255,255,0.5)"
[content, "<div class=\"cursor\" style=\"", cursor_style, "\"></div>"]
end
end