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/ligature_renderer.ex
defmodule Raxol.Terminal.Rendering.LigatureRenderer do
@moduledoc """
Programming font ligature rendering system for Raxol terminals.
This module provides comprehensive support for programming font ligatures with:
- Multi-character ligature detection and replacement
- Font-specific ligature mapping (FiraCode, JetBrains Mono, Cascadia Code, etc.)
- Unicode rendering with proper character width calculation
- Performance-optimized ligature processing
- Customizable ligature sets and user preferences
- Fallback handling for non-ligature fonts
## Supported Ligatures
### Arrows and Flow
- `->`, `<-`, `=>`, `<=`, `>=`, `!=`, `==`, `===`
- `|>`, `<|`, `>>`, `<<`, `<>`, `<=>`, `<->`
- `~>`, `<~`, `~>>`, `<<~`, `<~~`, `~~>`
### Programming Symbols
- `++`, `--`, `**`, `//`, `::`, `;;`, `??`, `!!`
- `&&`, `||`, `&&&`, `|||`
- `#=`, `#!`, `#?`, `#_`, `##`
- `/*`, `*/`, `/**`, `**/`
### Mathematical
- `<=`, `>=`, `!=`, `==`, `===`, `!==`
- `<<=`, `>>=`, `<=>`, `<->`
- `+-`, `-+`, `*=`, `/=`, `%=`, `^=`
## Usage
# Configure ligature rendering
config = LigatureRenderer.config(
font: :fira_code,
enabled_sets: [:arrows, :programming, :math],
disabled_ligatures: ["->"] # Disable specific ligatures
)
# Render text with ligatures
text = "const arrow = (x) => x + 1; // Lambda function"
rendered = LigatureRenderer.render(text, config)
# Check if text contains ligatures
has_ligatures? = LigatureRenderer.contains_ligatures?(text, config)
"""
require Logger
defstruct [
:font,
:enabled_sets,
:disabled_ligatures,
:custom_ligatures,
:performance_mode,
:fallback_enabled
]
@type ligature_set ::
:arrows | :programming | :math | :brackets | :comments | :operators
@type font_family ::
:fira_code
| :jetbrains_mono
| :cascadia_code
| :iosevka
| :hack
| :custom
@type unicode_point :: 0..0x10FFFF
@type ligature_config :: %__MODULE__{
font: font_family(),
enabled_sets: [ligature_set()],
disabled_ligatures: [String.t()],
custom_ligatures: %{String.t() => unicode_point()},
performance_mode: boolean(),
fallback_enabled: boolean()
}
# Unicode Private Use Area for ligatures (varies by font)
@fira_code_base 0xE100
@jetbrains_mono_base 0xE200
@cascadia_code_base 0xE300
@iosevka_base 0xE400
# Ligature definitions by category
@arrows_ligatures %{
"->" => 0xE100,
"<-" => 0xE101,
"=>" => 0xE102,
"<=" => 0xE103,
">=" => 0xE104,
"!=" => 0xE105,
"==" => 0xE106,
"===" => 0xE107,
"!===" => 0xE108,
"|>" => 0xE109,
"<|" => 0xE10A,
">>" => 0xE10B,
"<<" => 0xE10C,
"<>" => 0xE10D,
"<=>" => 0xE10E,
"<->" => 0xE10F,
"~>" => 0xE110,
"<~" => 0xE111,
"~>>" => 0xE112,
"<<~" => 0xE113,
"<~~" => 0xE114,
"~~>" => 0xE115,
">>>" => 0xE116,
"<<<" => 0xE117
}
@programming_ligatures %{
"++" => 0xE120,
"--" => 0xE121,
"**" => 0xE122,
"//" => 0xE123,
"::" => 0xE124,
";;" => 0xE125,
"??" => 0xE126,
"!!" => 0xE127,
"&&" => 0xE128,
"||" => 0xE129,
"&&&" => 0xE12A,
"|||" => 0xE12B,
"#=" => 0xE12C,
"#!" => 0xE12D,
"#?" => 0xE12E,
"#_" => 0xE12F,
"##" => 0xE130,
"###" => 0xE131,
"####" => 0xE132,
".." => 0xE133,
"..." => 0xE134,
"..<" => 0xE135,
".=" => 0xE136
}
@math_ligatures %{
"+-" => 0xE140,
"-+" => 0xE141,
"*=" => 0xE142,
"/=" => 0xE143,
"%=" => 0xE144,
"^=" => 0xE145,
"<<=" => 0xE146,
">>=" => 0xE147,
"***" => 0xE148,
"///" => 0xE149,
":::" => 0xE14A,
"=:=" => 0xE14B,
"=/=" => 0xE14C,
"<|>" => 0xE14D,
"<||>" => 0xE14E
}
@brackets_ligatures %{
"[[" => 0xE160,
"]]" => 0xE161,
"{|" => 0xE162,
"|}" => 0xE163,
"[|" => 0xE164,
"|]" => 0xE165,
"((" => 0xE166,
"))" => 0xE167,
"{{" => 0xE168,
"}}" => 0xE169
}
@comments_ligatures %{
"/*" => 0xE180,
"*/" => 0xE181,
"/**" => 0xE182,
"**/" => 0xE183,
"/*/" => 0xE184,
"/***" => 0xE185,
"***/" => 0xE186,
"///" => 0xE187,
"////" => 0xE188,
"<!--" => 0xE189,
"-->" => 0xE18A
}
@operators_ligatures %{
"<:" => 0xE1A0,
":>" => 0xE1A1,
"<:>" => 0xE1A2,
"=<<" => 0xE1A3,
">>=" => 0xE1A4,
"<<>>" => 0xE1A5,
"<><>" => 0xE1A6,
"++" => 0xE1A7,
"--" => 0xE1A8,
"**" => 0xE1A9,
"//" => 0xE1AA,
"%%" => 0xE1AB,
"@@" => 0xE1AC,
"$$" => 0xE1AD
}
@all_ligature_sets %{
arrows: @arrows_ligatures,
programming: @programming_ligatures,
math: @math_ligatures,
brackets: @brackets_ligatures,
comments: @comments_ligatures,
operators: @operators_ligatures
}
def default_config do
%__MODULE__{
font: :fira_code,
enabled_sets: [:arrows, :programming, :math],
disabled_ligatures: [],
custom_ligatures: %{},
performance_mode: false,
fallback_enabled: true
}
end
## Public API
@doc """
Creates a ligature rendering configuration.
## Examples
config = LigatureRenderer.config(
font: :fira_code,
enabled_sets: [:arrows, :programming],
disabled_ligatures: ["->", "<="]
)
"""
def config(opts \\ []) do
Enum.reduce(opts, default_config(), fn {key, value}, acc ->
Map.put(acc, key, value)
end)
end
@doc """
Renders text with ligatures applied.
## Examples
config = LigatureRenderer.config(font: :fira_code)
text = "const sum = (a, b) => a + b;"
rendered = LigatureRenderer.render(text, config)
"""
def render(text, config \\ nil) when is_binary(text) do
config = config || default_config()
apply_rendering_mode(config.performance_mode, text, config)
end
defp apply_rendering_mode(true, text, config),
do: render_optimized(text, config)
defp apply_rendering_mode(false, text, config),
do: render_standard(text, config)
@doc """
Checks if text contains any ligatures that would be rendered.
## Examples
config = LigatureRenderer.config()
LigatureRenderer.contains_ligatures?("hello -> world", config)
# true
LigatureRenderer.contains_ligatures?("hello world", config)
# false
"""
def contains_ligatures?(text, config \\ nil) do
config = config || default_config()
ligature_map = build_ligature_map(config)
ligature_map
|> Map.keys()
|> Enum.any?(&String.contains?(text, &1))
end
@doc """
Gets all available ligatures for a specific font and configuration.
## Examples
config = LigatureRenderer.config(font: :fira_code)
ligatures = LigatureRenderer.available_ligatures(config)
"""
def available_ligatures(config \\ nil) do
config = config || default_config()
build_ligature_map(config)
end
@doc """
Calculates the visual width of text after ligature rendering.
Some ligatures reduce the visual width (e.g., "->" becomes one character),
which is important for proper text alignment and cursor positioning.
"""
def visual_width(text, config \\ nil) do
config = config || default_config()
ligature_map = build_ligature_map(config)
calculate_visual_width(text, ligature_map)
end
@doc """
Converts ligature unicode points back to original text sequences.
Useful for editing operations where you need the original text.
"""
def ligatures_to_text(rendered_text, config \\ nil) do
config = config || default_config()
reverse_map =
config
|> build_ligature_map()
|> Enum.map(fn {text, unicode} -> {unicode, text} end)
|> Map.new()
rendered_text
|> String.to_charlist()
|> Enum.map(fn char ->
case Map.get(reverse_map, char) do
nil -> <<char::utf8>>
text -> text
end
end)
|> Enum.join()
end
@doc """
Gets cursor position mapping after ligature rendering.
When ligatures are rendered, cursor positions need to be adjusted
because multiple characters may render as one.
"""
def cursor_position_map(text, config \\ nil) do
config = config || default_config()
ligature_map = build_ligature_map(config)
build_cursor_map(text, ligature_map)
end
@doc """
Detects font ligature capabilities.
Attempts to determine if the current terminal font supports ligatures.
"""
def detect_font_ligatures do
# This is a simplified detection - in practice would need more sophisticated detection
case System.get_env("TERM_PROGRAM") do
"iTerm.app" ->
:likely_supported
"WezTerm" ->
:supported
"Alacritty" ->
:likely_supported
"kitty" ->
:supported
_ ->
case System.get_env("FONT_FAMILY") do
font when font in ["Fira Code", "JetBrains Mono", "Cascadia Code"] ->
:likely_supported
_ ->
:unknown
end
end
end
@doc """
Optimizes ligature configuration for performance.
Analyzes text patterns to suggest optimal ligature sets.
"""
def optimize_config(text_samples, base_config \\ nil)
when is_list(text_samples) do
base_config = base_config || default_config()
# Analyze which ligature sets are actually used
all_text = Enum.join(text_samples, " ")
used_sets =
@all_ligature_sets
|> Enum.filter(fn {_set_name, ligatures} ->
ligatures
|> Map.keys()
|> Enum.any?(&String.contains?(all_text, &1))
end)
|> Enum.map(&elem(&1, 0))
frequency_map = calculate_ligature_frequency(all_text, base_config)
# Suggest optimizations
suggestions = %{
recommended_sets: used_sets,
unused_sets:
Enum.filter(
[:arrows, :programming, :math, :brackets, :comments, :operators],
&(&1 not in used_sets)
),
frequently_used:
frequency_map |> Enum.sort_by(&elem(&1, 1), :desc) |> Enum.take(10),
performance_mode:
length(text_samples) > 100 or String.length(all_text) > 10_000
}
optimized_config = %{
base_config
| enabled_sets: used_sets,
performance_mode: suggestions.performance_mode
}
{optimized_config, suggestions}
end
## Private Implementation
defp render_standard(text, config) do
ligature_map = build_ligature_map(config)
# Sort by length (longest first) to handle overlapping ligatures correctly
sorted_ligatures =
ligature_map
|> Enum.sort_by(fn {text, _} -> -String.length(text) end)
Enum.reduce(sorted_ligatures, text, fn {pattern, unicode_point}, acc ->
apply_ligature_replacement(
String.contains?(acc, pattern),
acc,
pattern,
unicode_point
)
end)
end
defp apply_ligature_replacement(false, acc, _pattern, _unicode_point), do: acc
defp apply_ligature_replacement(true, acc, pattern, unicode_point) do
String.replace(acc, pattern, <<unicode_point::utf8>>)
end
defp render_optimized(text, config) do
# Performance-optimized version with early exits and caching
ligature_map = build_ligature_map(config)
# Pre-filter ligatures that might be in the text
candidate_ligatures =
ligature_map
|> Enum.filter(fn {pattern, _} -> String.contains?(text, pattern) end)
|> Enum.sort_by(fn {text, _} -> -String.length(text) end)
process_candidate_ligatures(
Enum.empty?(candidate_ligatures),
text,
candidate_ligatures
)
end
defp process_candidate_ligatures(true, text, _candidate_ligatures), do: text
defp process_candidate_ligatures(false, text, candidate_ligatures) do
Enum.reduce(candidate_ligatures, text, fn {pattern, unicode_point}, acc ->
String.replace(acc, pattern, <<unicode_point::utf8>>)
end)
end
defp build_ligature_map(config) do
# Build the complete ligature map based on configuration
base_map =
config.enabled_sets
|> Enum.reduce(%{}, fn set_name, acc ->
set_ligatures = Map.get(@all_ligature_sets, set_name, %{})
Map.merge(acc, set_ligatures)
end)
# Apply font-specific adjustments
font_adjusted_map = apply_font_adjustments(base_map, config.font)
# Remove disabled ligatures
filtered_map =
Enum.reduce(config.disabled_ligatures, font_adjusted_map, fn disabled,
acc ->
Map.delete(acc, disabled)
end)
# Add custom ligatures
Map.merge(filtered_map, config.custom_ligatures)
end
defp apply_font_adjustments(ligature_map, font) do
# Adjust unicode points based on font
_base_offset =
case font do
:fira_code -> @fira_code_base
:jetbrains_mono -> @jetbrains_mono_base
:cascadia_code -> @cascadia_code_base
:iosevka -> @iosevka_base
# Default
_ -> @fira_code_base
end
# For now, just use the default mapping
# In a real implementation, this would adjust unicode points per font
ligature_map
end
defp calculate_visual_width(text, ligature_map) do
# Calculate how much the visual width changes due to ligatures
original_width = String.length(text)
width_reduction =
ligature_map
|> Enum.reduce(0, fn {pattern, _unicode}, acc ->
pattern_count = text |> String.split(pattern) |> length() |> Kernel.-(1)
pattern_reduction = (String.length(pattern) - 1) * pattern_count
acc + pattern_reduction
end)
original_width - width_reduction
end
defp build_cursor_map(text, ligature_map) do
# Build a mapping of original cursor positions to visual positions
char_list = String.graphemes(text)
{_final_text, cursor_map} =
char_list
|> Enum.with_index()
|> Enum.reduce({[], %{}}, fn {char, original_pos}, {acc_chars, acc_map} ->
visual_pos = length(acc_chars)
# Check if we're starting a ligature
remaining_text = Enum.drop(char_list, original_pos) |> Enum.join()
case find_ligature_at_position(remaining_text, ligature_map) do
{ligature_text, _unicode, ligature_length} ->
# This position starts a ligature
new_acc_chars = acc_chars ++ [ligature_text]
# Map all positions in the ligature to the same visual position
ligature_positions =
original_pos..(original_pos + ligature_length - 1)
ligature_map_entries =
Map.new(ligature_positions, &{&1, visual_pos})
{new_acc_chars, Map.merge(acc_map, ligature_map_entries)}
nil ->
# Regular character
new_acc_chars = acc_chars ++ [char]
new_acc_map = Map.put(acc_map, original_pos, visual_pos)
{new_acc_chars, new_acc_map}
end
end)
cursor_map
end
defp find_ligature_at_position(text, ligature_map) do
ligature_map
|> Enum.sort_by(fn {pattern, _} -> -String.length(pattern) end)
|> Enum.find_value(fn {pattern, unicode} ->
check_ligature_match(String.starts_with?(text, pattern), pattern, unicode)
end)
end
defp check_ligature_match(false, _pattern, _unicode), do: nil
defp check_ligature_match(true, pattern, unicode) do
{pattern, unicode, String.length(pattern)}
end
defp calculate_ligature_frequency(text, config) do
ligature_map = build_ligature_map(config)
ligature_map
|> Enum.map(fn {pattern, _unicode} ->
count = text |> String.split(pattern) |> length() |> Kernel.-(1)
{pattern, count}
end)
|> Enum.filter(fn {_pattern, count} -> count > 0 end)
end
## Utility Functions
@doc """
Converts text to a ligature-aware representation for processing.
This creates a structure that maintains both original text and
ligature information for complex text operations.
"""
def to_ligature_structure(text, config \\ nil) do
config = config || default_config()
ligature_map = build_ligature_map(config)
char_list = String.graphemes(text)
{result, _pos} =
Enum.reduce(char_list, {[], 0}, fn char, {acc, pos} ->
remaining_text = Enum.drop(char_list, pos) |> Enum.join()
case find_ligature_at_position(remaining_text, ligature_map) do
{ligature_text, unicode, length} ->
ligature_info = %{
type: :ligature,
original: ligature_text,
rendered: <<unicode::utf8>>,
length: length,
position: pos
}
{acc ++ [ligature_info], pos + length}
nil ->
char_info = %{
type: :character,
original: char,
rendered: char,
length: 1,
position: pos
}
{acc ++ [char_info], pos + 1}
end
end)
result
end
@doc """
Validates ligature configuration.
"""
def validate_config(%__MODULE__{} = config) do
errors = []
# Check enabled sets
errors =
validate_enabled_sets(
Enum.all?(config.enabled_sets, &Map.has_key?(@all_ligature_sets, &1)),
errors,
config.enabled_sets
)
# Check custom ligatures
valid_custom =
Enum.all?(config.custom_ligatures, fn {k, v} ->
is_binary(k) and is_integer(v)
end)
errors = validate_custom_ligatures(valid_custom, errors)
return_validation_result(Enum.empty?(errors), errors)
end
defp validate_enabled_sets(true, errors, _enabled_sets), do: errors
defp validate_enabled_sets(false, errors, enabled_sets) do
invalid_sets =
Enum.reject(enabled_sets, &Map.has_key?(@all_ligature_sets, &1))
["Invalid ligature sets: #{inspect(invalid_sets)}" | errors]
end
defp validate_custom_ligatures(true, errors), do: errors
defp validate_custom_ligatures(false, errors) do
["Custom ligatures must be %{String.t() => integer()}" | errors]
end
defp return_validation_result(true, _errors), do: :ok
defp return_validation_result(false, errors), do: {:error, errors}
@doc """
Gets performance statistics for ligature rendering.
"""
def performance_stats(text, config \\ nil) do
config = config || default_config()
ligature_map = build_ligature_map(config)
%{
text_length: String.length(text),
available_ligatures: map_size(ligature_map),
enabled_sets: length(config.enabled_sets),
disabled_ligatures: length(config.disabled_ligatures),
custom_ligatures: map_size(config.custom_ligatures),
contains_ligatures: contains_ligatures?(text, config),
performance_mode: config.performance_mode
}
end
end