Packages
Terminal emulation and driver infrastructure for Raxol. ANSI parsing, screen buffers, command processing, cursor management, input handling, session management, and termbox2 NIF integration.
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