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
raxol_terminal lib raxol terminal format.ex
Raw

lib/raxol/terminal/format.ex

defmodule Raxol.Terminal.Format do
@moduledoc """
Unified terminal text formatting and styling operations.
This module manages formatting state (bold, italic, colors, etc.) and provides
functions for applying ANSI escape codes to text. It consolidates the
previously separate FormattingManager and Formatting.Manager modules.
## Usage
iex> format = Format.new()
iex> format = Format.set_foreground(format, 196)
iex> format = Format.toggle_bold(format)
iex> Format.apply_formatting(format, "Hello")
"\e[1m\e[38;5;196mHello\e[39m\e[22m"
"""
@typedoc "Terminal text attributes"
@type format :: %{
bold: boolean(),
faint: boolean(),
italic: boolean(),
underline: boolean(),
blink: boolean(),
reverse: boolean(),
conceal: boolean(),
strikethrough: boolean(),
foreground: term() | nil,
background: term() | nil,
font: non_neg_integer()
}
@typedoc "Format state with current and saved formats"
@type t :: %__MODULE__{
current_format: format(),
saved_format: format() | nil
}
defstruct current_format: %{
bold: false,
faint: false,
italic: false,
underline: false,
blink: false,
reverse: false,
conceal: false,
strikethrough: false,
foreground: nil,
background: nil,
font: 0
},
saved_format: nil
@default_format %{
bold: false,
faint: false,
italic: false,
underline: false,
blink: false,
reverse: false,
conceal: false,
strikethrough: false,
foreground: nil,
background: nil,
font: 0
}
# --- Constructor ---
@doc """
Creates a new formatting state with default values.
"""
@spec new() :: %__MODULE__{
current_format: %{
bold: false,
faint: false,
italic: false,
underline: false,
blink: false,
reverse: false,
conceal: false,
strikethrough: false,
foreground: nil,
background: nil,
font: 0
},
saved_format: nil
}
def new do
%__MODULE__{}
end
# --- Format Access ---
@doc """
Gets the current formatting state.
"""
@spec get_format(t()) :: format()
def get_format(%__MODULE__{} = state) do
state.current_format
end
@doc """
Applies a map of format updates to the current state.
"""
@spec apply_format(t(), map()) :: t()
def apply_format(%__MODULE__{} = state, format) when is_map(format) do
new_format = Map.merge(state.current_format, format)
%{state | current_format: new_format}
end
@doc """
Resets the current format to default values.
"""
@spec reset_format(t()) :: t()
def reset_format(%__MODULE__{} = state) do
%{state | current_format: @default_format}
end
# --- Save/Restore ---
@doc """
Saves the current format state for later restoration.
"""
@spec save_format(t()) :: t()
def save_format(%__MODULE__{} = state) do
%{state | saved_format: state.current_format}
end
@doc """
Restores the previously saved format state.
Returns unchanged state if no format was saved.
"""
@spec restore_format(t()) :: t()
def restore_format(%__MODULE__{saved_format: nil} = state), do: state
def restore_format(%__MODULE__{saved_format: format} = state) do
%{state | current_format: format}
end
# --- Color Operations ---
@doc """
Sets the foreground color.
Color can be an 8-bit color code (0-255) or nil for default.
"""
@spec set_foreground(t(), term() | nil) :: t()
def set_foreground(%__MODULE__{} = state, color) do
update_format(state, :foreground, color)
end
@doc """
Sets the background color.
Color can be an 8-bit color code (0-255) or nil for default.
"""
@spec set_background(t(), term() | nil) :: t()
def set_background(%__MODULE__{} = state, color) do
update_format(state, :background, color)
end
@doc """
Gets the current foreground color.
"""
@spec get_foreground(t()) :: term() | nil
def get_foreground(%__MODULE__{} = state) do
state.current_format.foreground
end
@doc """
Gets the current background color.
"""
@spec get_background(t()) :: term() | nil
def get_background(%__MODULE__{} = state) do
state.current_format.background
end
# --- Attribute Operations ---
@doc """
Sets the specified attribute to true.
"""
@spec set_attribute(t(), atom()) :: t()
def set_attribute(%__MODULE__{} = state, attribute) when is_atom(attribute) do
update_format(state, attribute, true)
end
@doc """
Resets the specified attribute to false.
"""
@spec reset_attribute(t(), atom()) :: t()
def reset_attribute(%__MODULE__{} = state, attribute)
when is_atom(attribute) do
update_format(state, attribute, false)
end
@doc """
Checks if the specified attribute is set.
"""
@spec attribute_set?(t(), atom()) :: boolean()
def attribute_set?(%__MODULE__{} = state, attribute) do
Map.get(state.current_format, attribute, false) == true
end
@doc """
Returns a list of all attributes that are currently set to true.
"""
@spec get_set_attributes(t()) :: [{atom(), true}]
def get_set_attributes(%__MODULE__{} = state) do
state.current_format
|> Enum.filter(fn {_key, value} -> value == true end)
end
# --- Toggle Operations ---
@doc "Toggles bold formatting."
@spec toggle_bold(t()) :: t()
def toggle_bold(%__MODULE__{} = state), do: toggle_attribute(state, :bold)
@doc "Toggles faint formatting."
@spec toggle_faint(t()) :: t()
def toggle_faint(%__MODULE__{} = state), do: toggle_attribute(state, :faint)
@doc "Toggles italic formatting."
@spec toggle_italic(t()) :: t()
def toggle_italic(%__MODULE__{} = state), do: toggle_attribute(state, :italic)
@doc "Toggles underline formatting."
@spec toggle_underline(t()) :: t()
def toggle_underline(%__MODULE__{} = state),
do: toggle_attribute(state, :underline)
@doc "Toggles blink formatting."
@spec toggle_blink(t()) :: t()
def toggle_blink(%__MODULE__{} = state), do: toggle_attribute(state, :blink)
@doc "Toggles reverse video formatting."
@spec toggle_reverse(t()) :: t()
def toggle_reverse(%__MODULE__{} = state),
do: toggle_attribute(state, :reverse)
@doc "Toggles conceal formatting."
@spec toggle_conceal(t()) :: t()
def toggle_conceal(%__MODULE__{} = state),
do: toggle_attribute(state, :conceal)
@doc "Toggles strikethrough formatting."
@spec toggle_strikethrough(t()) :: t()
def toggle_strikethrough(%__MODULE__{} = state),
do: toggle_attribute(state, :strikethrough)
# --- Font ---
@doc """
Sets the font number (0-9 for standard ANSI fonts).
"""
@spec set_font(t(), non_neg_integer()) :: t()
def set_font(%__MODULE__{} = state, font)
when is_integer(font) and font >= 0 do
update_format(state, :font, font)
end
# --- ANSI Application ---
@doc """
Applies the current formatting to a string, wrapping it with ANSI escape codes.
Each attribute that is enabled will add the appropriate SGR codes around the text.
"""
@spec apply_formatting(t(), String.t()) :: String.t()
def apply_formatting(%__MODULE__{} = state, text) when is_binary(text) do
format = state.current_format
text
|> maybe_apply_bold(format.bold)
|> maybe_apply_faint(format.faint)
|> maybe_apply_italic(format.italic)
|> maybe_apply_underline(format.underline)
|> maybe_apply_blink(format.blink)
|> maybe_apply_reverse(format.reverse)
|> maybe_apply_conceal(format.conceal)
|> maybe_apply_strikethrough(format.strikethrough)
|> maybe_apply_foreground(format.foreground)
|> maybe_apply_background(format.background)
end
@doc """
Generates the ANSI escape sequence for the current format without text.
Returns a tuple of {start_sequence, end_sequence} that can be used to wrap text.
"""
@spec to_ansi_sequences(t()) :: {String.t(), String.t()}
def to_ansi_sequences(%__MODULE__{} = state) do
format = state.current_format
start_codes = []
end_codes = []
{start_codes, end_codes} =
if format.bold,
do: {[1 | start_codes], [22 | end_codes]},
else: {start_codes, end_codes}
{start_codes, end_codes} =
if format.faint,
do: {[2 | start_codes], [22 | end_codes]},
else: {start_codes, end_codes}
{start_codes, end_codes} =
if format.italic,
do: {[3 | start_codes], [23 | end_codes]},
else: {start_codes, end_codes}
{start_codes, end_codes} =
if format.underline,
do: {[4 | start_codes], [24 | end_codes]},
else: {start_codes, end_codes}
{start_codes, end_codes} =
if format.blink,
do: {[5 | start_codes], [25 | end_codes]},
else: {start_codes, end_codes}
{start_codes, end_codes} =
if format.reverse,
do: {[7 | start_codes], [27 | end_codes]},
else: {start_codes, end_codes}
{start_codes, end_codes} =
if format.conceal,
do: {[8 | start_codes], [28 | end_codes]},
else: {start_codes, end_codes}
{start_codes, end_codes} =
if format.strikethrough,
do: {[9 | start_codes], [29 | end_codes]},
else: {start_codes, end_codes}
{start_codes, end_codes} =
case format.foreground do
nil -> {start_codes, end_codes}
color -> {[{38, 5, color} | start_codes], [39 | end_codes]}
end
{start_codes, end_codes} =
case format.background do
nil -> {start_codes, end_codes}
color -> {[{48, 5, color} | start_codes], [49 | end_codes]}
end
start_seq = build_sgr_sequence(start_codes)
end_seq = build_sgr_sequence(end_codes)
{start_seq, end_seq}
end
# --- Private Helpers ---
defp update_format(%__MODULE__{} = state, key, value) do
%{state | current_format: Map.put(state.current_format, key, value)}
end
defp toggle_attribute(%__MODULE__{} = state, attribute) do
%{
state
| current_format: Map.update!(state.current_format, attribute, &(!&1))
}
end
defp build_sgr_sequence([]), do: ""
defp build_sgr_sequence(codes) do
params =
codes
|> Enum.reverse()
|> Enum.map_join(";", fn
{a, b, c} -> "#{a};#{b};#{c}"
code -> to_string(code)
end)
"\e[#{params}m"
end
# ANSI application helpers
defp maybe_apply_bold(text, true), do: "\e[1m#{text}\e[22m"
defp maybe_apply_bold(text, false), do: text
defp maybe_apply_faint(text, true), do: "\e[2m#{text}\e[22m"
defp maybe_apply_faint(text, false), do: text
defp maybe_apply_italic(text, true), do: "\e[3m#{text}\e[23m"
defp maybe_apply_italic(text, false), do: text
defp maybe_apply_underline(text, true), do: "\e[4m#{text}\e[24m"
defp maybe_apply_underline(text, false), do: text
defp maybe_apply_blink(text, true), do: "\e[5m#{text}\e[25m"
defp maybe_apply_blink(text, false), do: text
defp maybe_apply_reverse(text, true), do: "\e[7m#{text}\e[27m"
defp maybe_apply_reverse(text, false), do: text
defp maybe_apply_conceal(text, true), do: "\e[8m#{text}\e[28m"
defp maybe_apply_conceal(text, false), do: text
defp maybe_apply_strikethrough(text, true), do: "\e[9m#{text}\e[29m"
defp maybe_apply_strikethrough(text, false), do: text
defp maybe_apply_foreground(text, nil), do: text
defp maybe_apply_foreground(text, color), do: "\e[38;5;#{color}m#{text}\e[39m"
defp maybe_apply_background(text, nil), do: text
defp maybe_apply_background(text, color), do: "\e[48;5;#{color}m#{text}\e[49m"
end