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/buffer/writer.ex
defmodule Raxol.Terminal.Buffer.Writer do
@moduledoc """
Handles writing characters and strings to the Raxol.Terminal.ScreenBuffer.
Responsible for character width, bidirectional text segmentation, and cell creation.
"""
alias Raxol.Terminal.ANSI.TextFormatting
alias Raxol.Terminal.Cell
alias Raxol.Terminal.ScreenBuffer
@doc """
Writes a character to the buffer at the specified position.
Handles wide characters by taking up two cells when necessary.
Accepts an optional style to apply to the cell.
"""
@dialyzer {:nowarn_function, write_char: 5}
@spec write_char(
ScreenBuffer.t() | map(),
non_neg_integer(),
non_neg_integer(),
String.t(),
TextFormatting.text_style() | nil
) :: ScreenBuffer.t() | map()
def write_char(buffer, x, y, char, style \\ nil)
when x >= 0 and y >= 0 and is_map(buffer) do
case within_bounds?(y, x, buffer.height, buffer.width) do
true ->
codepoint = hd(String.to_charlist(char))
width = Raxol.Terminal.CharacterHandling.get_char_width(codepoint)
cell_style = create_cell_style(style)
log_char_write(char, x, y, cell_style)
cells = update_cells(buffer, x, y, char, cell_style, width)
%{buffer | cells: cells}
false ->
buffer
end
end
@doc """
Creates a cell style by merging the provided style with default formatting.
## Parameters
* `style` - The style to merge with default formatting, or nil for default style
## Returns
A map containing the merged text formatting style.
## Examples
iex> Writer.create_cell_style(%{fg: :red})
%{fg: :red, bg: :default, bold: false, ...}
"""
@spec create_cell_style(TextFormatting.text_style() | nil) ::
TextFormatting.t()
def create_cell_style(nil), do: TextFormatting.new()
def create_cell_style(style) when is_map(style) do
style =
case is_struct(style) do
true -> Map.from_struct(style)
false -> style
end
|> Map.new(fn {k, v} ->
case k do
:fg -> {:foreground, v}
:bg -> {:background, v}
_ -> {k, v}
end
end)
Map.merge(TextFormatting.new(), style)
end
def create_cell_style(_), do: TextFormatting.new()
@doc """
Logs character write operations for debugging purposes.
## Parameters
* `char` - The character being written
* `x` - The x-coordinate where the character is being written
* `y` - The y-coordinate where the character is being written
* `cell_style` - The style being applied to the cell
## Returns
:ok
## Examples
iex> Writer.log_char_write("A", 0, 0, %{fg: :red})
:ok
"""
@spec log_char_write(
String.t(),
non_neg_integer(),
non_neg_integer(),
TextFormatting.text_style()
) :: :ok
def log_char_write(_char, _x, _y, _cell_style) do
require Raxol.Core.Runtime.Log
Raxol.Core.Runtime.Log.debug(
# {char}" at {#{x}, #{y}} with style: #{inspect(cell_style)}"
"[Buffer.Writer] Writing char "
)
end
@doc """
Updates cells in the buffer at the specified position.
## Parameters
* `buffer` - The screen buffer to update
* `x` - The x-coordinate to update
* `y` - The y-coordinate to update
* `char` - The character to write
* `cell_style` - The style to apply
* `width` - The width of the character (1 or 2 for wide characters)
## Returns
The updated list of cells.
## Examples
iex> buffer = ScreenBuffer.new(80, 24)
iex> Writer.update_cells(buffer, 0, 0, "A", %{fg: :red}, 1)
[%Cell{char: "A", style: %{fg: :red}}, ...]
"""
@spec update_cells(
ScreenBuffer.t() | map(),
non_neg_integer(),
non_neg_integer(),
String.t(),
TextFormatting.text_style(),
1..2
) :: list(list(Cell.t()))
def update_cells(buffer, x, y, char, cell_style, width) do
List.update_at(
buffer.cells,
y,
&update_row(&1, x, char, cell_style, width, buffer.width)
)
end
@doc """
Updates a row in the buffer at the specified position.
## Parameters
* `row` - The row to update
* `x` - The x-coordinate to update
* `char` - The character to write
* `cell_style` - The style to apply
* `width` - The width of the character (1 or 2 for wide characters)
* `buffer_width` - The total width of the buffer
## Returns
The updated row of cells.
## Examples
iex> row = List.duplicate(Cell.new(), 80)
iex> Writer.update_row(row, 0, "A", %{fg: :red}, 1, 80)
[%Cell{char: "A", style: %{fg: :red}}, ...]
"""
@spec update_row(
list(Cell.t()),
non_neg_integer(),
String.t(),
TextFormatting.text_style(),
1..2,
non_neg_integer()
) :: list(Cell.t())
def update_row(row, x, char, cell_style, width, buffer_width) do
new_cell = Cell.new(char, cell_style)
case {width == 2, x + 1 < buffer_width} do
{true, true} ->
row
|> List.update_at(x, fn _ -> new_cell end)
|> List.update_at(x + 1, fn _ ->
Cell.new_wide_placeholder(cell_style)
end)
_ ->
List.update_at(row, x, fn _ -> new_cell end)
end
end
@doc """
Writes a string to the buffer at the specified position.
Handles wide characters and bidirectional text.
"""
@spec write_string(
ScreenBuffer.t() | map(),
non_neg_integer(),
non_neg_integer(),
String.t(),
TextFormatting.text_style() | nil
) :: ScreenBuffer.t() | map()
def write_string(buffer, x, y, string, style \\ nil)
when x >= 0 and y >= 0 and is_map(buffer) do
case within_bounds?(y, x, buffer.height, buffer.width) do
true ->
segments = Raxol.Terminal.CharacterHandling.process_bidi_text(string)
Enum.reduce(segments, {buffer, x}, fn {_type, segment}, {acc_buffer, acc_x} ->
{new_buffer, new_x} =
write_segment(acc_buffer, acc_x, y, segment, style)
{new_buffer, new_x}
end)
|> elem(0)
false ->
buffer
end
end
@doc """
Writes a segment of text to the buffer.
## Parameters
* `buffer` - The screen buffer to write to
* `x` - The x-coordinate to start writing at
* `y` - The y-coordinate to write at
* `segment` - The text segment to write
## Returns
A tuple containing the updated buffer and the new x-coordinate.
## Examples
iex> buffer = ScreenBuffer.new(80, 24)
iex> {new_buffer, new_x} = Writer.write_segment(buffer, 0, 0, "Hello")
iex> new_x
5
"""
@spec write_segment(
ScreenBuffer.t() | map(),
non_neg_integer(),
non_neg_integer(),
String.t(),
TextFormatting.text_style() | nil
) ::
{ScreenBuffer.t() | map(), non_neg_integer()}
def write_segment(buffer, x, y, segment, style \\ nil) do
Enum.reduce(String.graphemes(segment), {buffer, x}, fn char, {acc_buffer, acc_x} ->
codepoint = hd(String.to_charlist(char))
width = Raxol.Terminal.CharacterHandling.get_char_width(codepoint)
{write_char(acc_buffer, acc_x, y, char, style), acc_x + width}
end)
end
defp within_bounds?(y, x, height, width) do
y < height and x < width
end
end