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/cell.ex
defmodule Raxol.Terminal.Cell do
@moduledoc """
Terminal character cell module.
This module handles the representation and manipulation of individual
character cells in the terminal screen buffer, including:
- Character content
- Text attributes (color, style)
- Cell state
"""
alias Raxol.Terminal.ANSI.TextFormatting
@typedoc """
Text style for a terminal cell. See `Raxol.Terminal.ANSI.TextFormatting.text_style()` type for details.
"""
@type style :: TextFormatting.text_style()
@type t :: %__MODULE__{
char: String.t() | nil,
style: TextFormatting.text_style() | nil,
dirty: boolean(),
wide_placeholder: boolean(),
sixel: boolean()
}
defstruct [
:char,
:style,
:dirty,
wide_placeholder: false,
sixel: false
]
@doc """
Creates a new cell with optional character and style.
## Examples
iex> cell = Cell.new()
iex> Cell.empty?(cell)
true
iex> cell = Cell.new("A")
iex> Cell.get_char(cell)
"A"
iex> cell = Cell.new("A", %{foreground: :red})
iex> Cell.get_char(cell)
"A"
iex> Cell.get_style(cell)
%{foreground: :red}
"""
@spec new(
String.t() | nil,
TextFormatting.t() | TextFormatting.text_style() | nil
) :: t()
def new(char \\ " ", style \\ TextFormatting.new()) do
%__MODULE__{
char: char || " ",
style: style,
dirty: false,
wide_placeholder: false,
sixel: false
}
end
@doc """
Creates a new cell representing the second half of a wide character.
Inherits the style from the primary cell.
"""
def new_wide_placeholder(style) do
%__MODULE__{
# Placeholder has no visible char
char: " ",
style: style,
dirty: true,
wide_placeholder: true,
sixel: false
}
end
@doc """
Creates a new cell representing a sixel graphics pixel.
"""
def new_sixel(char \\ " ", style \\ TextFormatting.new()) do
%__MODULE__{
char: char || " ",
style: style,
dirty: true,
wide_placeholder: false,
sixel: true
}
end
@doc """
Returns the character of the cell.
"""
@spec get_char(t()) :: String.t() | char()
def get_char(%__MODULE__{char: char}), do: char
@doc """
Gets the text style of the cell.
## Examples
iex> cell = Cell.new("A", %{foreground: :red})
iex> Cell.get_style(cell)
%{foreground: :red}
"""
@spec get_style(t()) :: TextFormatting.text_style() | nil
def get_style(%__MODULE__{style: style}), do: style
@doc """
Gets the cell's foreground color (compatibility function).
"""
def fg(%__MODULE__{style: style}) do
style.foreground
end
@doc """
Gets the cell's background color (compatibility function).
"""
def bg(%__MODULE__{style: style}) do
style.background
end
@doc """
Sets the character content of a cell.
## Examples
iex> cell = Cell.new()
iex> cell = Cell.set_char(cell, "A")
iex> Cell.get_char(cell)
"A"
"""
@spec set_char(t(), String.t()) :: t()
def set_char(%__MODULE__{} = cell, char) do
%{cell | char: char}
end
@doc """
Sets the text style of the cell.
## Examples
iex> cell = Cell.new("A")
iex> cell = Cell.set_style(cell, %{foreground: :red})
iex> Cell.get_style(cell)
%{foreground: :red}
"""
@spec set_style(t(), TextFormatting.text_style() | nil) :: t()
def set_style(%__MODULE__{} = cell, style) do
%{cell | style: style}
end
@doc """
Merges a given style map into the cell's style.
Only non-default attributes from the `style` map will overwrite existing attributes
in the cell's style. This prevents merging default values (like `bold: false`)
and unintentionally removing existing attributes.
## Examples
iex> initial_style = TextFormatting.new() |> TextFormatting.apply_attribute(:bold) # %{bold: true, ...}
iex> merge_style = TextFormatting.new() |> TextFormatting.apply_attribute(:underline) # %{underline: true, bold: false, ...}
iex> cell = Cell.new("A", initial_style)
iex> merged_cell = Cell.merge_style(cell, merge_style)
iex> Cell.get_style(merged_cell)
%{bold: true, underline: true} # Note: :bold remains, :underline added
"""
def merge_style(%__MODULE__{} = cell, style_to_merge)
when is_struct(style_to_merge) do
default_style = TextFormatting.new()
# Convert both styles to maps for easier manipulation
cell_style_map = Map.from_struct(cell.style)
merge_style_map = Map.from_struct(style_to_merge)
default_style_map = Map.from_struct(default_style)
# Iterate through the style map we want to merge in.
# Only apply the attribute if its value is different from the default.
final_style_map =
Enum.reduce(merge_style_map, cell_style_map, fn {key, value}, acc_style ->
case Map.get(default_style_map, key) != value do
true -> Map.put(acc_style, key, value)
false -> acc_style
end
end)
# Convert back to struct by starting with cell.style's module
final_style = struct(cell.style.__struct__, final_style_map)
%{cell | style: final_style}
end
def merge_style(%__MODULE__{} = cell, style_to_merge)
when is_map(style_to_merge) do
# Handle plain maps by converting to TextFormatting struct first
style_struct = TextFormatting.new()
# Apply the attributes from the map
style_struct = struct(style_struct, style_to_merge)
merge_style(cell, style_struct)
end
@doc """
Checks if the cell has a specific attribute.
## Examples
iex> cell = Cell.new("A", %{foreground: :red})
iex> Cell.has_attribute?(cell, :foreground)
true
"""
def has_attribute?(%__MODULE__{style: style}, attribute) do
Map.get(Map.from_struct(style), attribute, false)
end
@doc """
Checks if the cell has a specific decoration.
## Examples
iex> cell = Cell.new("A", %{foreground: :red})
iex> Cell.has_decoration?(cell, :bold)
false
"""
def has_decoration?(%__MODULE__{style: style}, decoration) do
Map.get(Map.from_struct(style), decoration, false)
end
@doc """
Checks if the cell is in double-width mode.
## Examples
iex> cell = Cell.new("A", %{foreground: :red})
iex> Cell.double_width?(cell)
false
"""
def double_width?(%__MODULE__{style: style}), do: style.double_width
@doc """
Checks if the cell is in double-height mode.
## Examples
iex> cell = Cell.new("A", %{foreground: :red})
iex> Cell.double_height?(cell)
false
"""
def double_height?(%__MODULE__{style: style}),
do: style.double_height != :none
@doc """
Checks if the cell is empty.
## Examples
iex> cell = Cell.new()
iex> Cell.empty?(cell)
true
iex> cell = Cell.new("A")
iex> Cell.empty?(cell)
false
"""
@spec empty?(t()) :: boolean()
def empty?(%__MODULE__{char: char, style: style}) do
(char == nil or char == "" or char == " ") and style == TextFormatting.new()
end
@doc """
Creates a copy of a cell with new attributes applied.
Accepts a map of attributes or a list of attribute atoms.
If a list is provided, the attributes are applied sequentially, starting from the cell's *existing* style.
## Examples
iex> cell = Cell.new("A", %{bold: true})
iex> new_cell = Cell.with_attributes(cell, %{underline: true}) # Using a map
iex> Cell.get_style(new_cell)
%{bold: true, underline: true} # Merged
iex> cell = Cell.new("B", %{bold: true})
iex> new_cell = Cell.with_attributes(cell, [:underline, :reverse]) # Using a list
iex> Cell.get_style(new_cell)
%{bold: true, underline: true, reverse: true} # Original bold + list applied
"""
def with_attributes(%__MODULE__{} = cell, attributes)
when is_list(attributes) do
# Apply each attribute to the cell's existing style
new_style =
Enum.reduce(attributes, cell.style, fn attribute, acc_style ->
TextFormatting.Attributes.apply_attribute(acc_style, attribute)
end)
%{cell | style: new_style}
end
def with_attributes(%__MODULE__{} = cell, attributes)
when is_map(attributes) do
# When merging a map, use the refined merge_style logic
merge_style(cell, attributes)
end
@doc """
Creates a copy of a cell with a new character.
## Examples
iex> cell = Cell.new("A", %{foreground: :red})
iex> new_cell = Cell.with_char(cell, "B")
iex> Cell.get_char(new_cell)
"B"
iex> Cell.get_style(new_cell)
%{foreground: :red}
"""
def with_char(%__MODULE__{} = cell, char) do
%{cell | char: char}
end
@doc """
Creates a deep copy of a cell.
## Examples
iex> cell = Cell.new("A", %{foreground: :red})
iex> copy = Cell.copy(cell)
iex> Cell.get_char(copy)
"A"
iex> Cell.get_style(copy)
%{foreground: :red}
"""
def copy(%__MODULE__{} = cell) do
%__MODULE__{
char: cell.char,
style: cell.style,
dirty: cell.dirty,
wide_placeholder: cell.wide_placeholder
}
end
@doc """
Compares two cells for equality.
Cells are considered equal if they have the same character and the same style map.
Handles comparison with `nil`.
## Examples
iex> style1 = TextFormatting.new() |> TextFormatting.apply_attribute(:bold)
iex> style2 = TextFormatting.new() |> TextFormatting.apply_attribute(:bold)
iex> style3 = TextFormatting.new() |> TextFormatting.apply_attribute(:underline)
iex> cell1 = Cell.new("A", style1)
iex> cell2 = Cell.new("A", style2) # Same char and style attributes
iex> cell3 = Cell.new("B", style1) # Different char
iex> cell4 = Cell.new("A", style3) # Different style
iex> Cell.equals?(cell1, cell2)
true
iex> Cell.equals?(cell1, cell3)
false
iex> Cell.equals?(cell1, cell4)
false
iex> Cell.equals?(cell1, nil)
false
iex> Cell.equals?(nil, cell1)
false
iex> Cell.equals?(nil, nil)
true
"""
def equals?(%__MODULE__{} = cell1, %__MODULE__{} = cell2) do
cell1.char == cell2.char && cell1.style == cell2.style &&
cell1.wide_placeholder == cell2.wide_placeholder
end
def equals?(nil, nil), do: true
def equals?(_, nil), do: false
def equals?(nil, _), do: false
@doc """
Creates a Cell struct from a map representation, typically from rendering.
Expects a map like %{char: integer_codepoint, style: map, wide_placeholder: boolean | nil}.
Returns nil if the map is invalid.
"""
@spec from_map(map()) :: t() | nil
def from_map(%{char: char_code, style: style} = map)
when is_integer(char_code) and is_map(style) do
# Convert integer code point back to string for storing in the struct
char_str = <<char_code::utf8>>
wide_placeholder = Map.get(map, :wide_placeholder, false)
%__MODULE__{
char: char_str,
style: style,
dirty: true,
wide_placeholder: wide_placeholder
}
end
def from_map(_other_map) do
# Log warning or handle error? For now, return nil.
nil
end
@doc """
Creates an empty cell.
"""
@spec empty() :: t()
def empty, do: new()
end