Packages

Declarative terminal styling for Elixir. Colors, borders, padding, layout, tables, trees, spinners, and interactive select menus.

Current section

Files

Jump to
esc lib esc list.ex
Raw

lib/esc/list.ex

defmodule Esc.List do
@moduledoc """
Styled hierarchical lists for terminal output.
Lists support various enumerator styles and can be nested.
## Example
List.new(["First item", "Second item", "Third item"])
|> List.enumerator(:arabic)
|> List.render()
## Enumerators
Available built-in enumerators:
- `:bullet` - Bullet points (•)
- `:dash` - Dashes (-)
- `:arabic` - Arabic numerals (1., 2., 3.)
- `:roman` - Roman numerals (i., ii., iii.)
- `:alphabet` - Alphabetic (a., b., c.)
Custom enumerators can be functions that take an index and return a string.
## Nesting
Lists can contain other lists for nested structures:
nested = List.new(["Sub-item 1", "Sub-item 2"])
List.new(["Main item", nested])
|> List.render()
## Theme Integration
When a global theme is set (via `Esc.set_theme/1`) and `use_theme` is enabled (default),
the list automatically uses theme colors:
- Enumerators: theme `:muted` color
Explicit styles override theme colors. Use `use_theme(list, false)` to disable.
"""
defstruct items: [],
enumerator: :bullet,
enumerator_style: nil,
item_style: nil,
indent: 0,
use_theme: true
@type item :: String.t() | t()
@type t :: %__MODULE__{
items: [item()],
enumerator: atom() | (non_neg_integer() -> String.t()),
enumerator_style: Esc.Style.t() | nil,
item_style: Esc.Style.t() | nil,
indent: non_neg_integer(),
use_theme: boolean()
}
@doc """
Creates a new list with the given items.
"""
@spec new([item()]) :: t()
def new(items \\ []) when is_list(items) do
%__MODULE__{items: items}
end
@doc """
Adds an item to the list.
"""
@spec item(t(), item()) :: t()
def item(%__MODULE__{} = list, item) do
%{list | items: list.items ++ [item]}
end
@doc """
Sets the enumerator style.
Built-in options: `:bullet`, `:dash`, `:arabic`, `:roman`, `:alphabet`
Can also be a function that takes an index and returns a string.
"""
@spec enumerator(t(), atom() | (non_neg_integer() -> String.t())) :: t()
def enumerator(%__MODULE__{} = list, enum) do
%{list | enumerator: enum}
end
@doc """
Sets the style for enumerators.
"""
@spec enumerator_style(t(), Esc.Style.t()) :: t()
def enumerator_style(%__MODULE__{} = list, style) do
%{list | enumerator_style: style}
end
@doc """
Sets the style for list items.
"""
@spec item_style(t(), Esc.Style.t()) :: t()
def item_style(%__MODULE__{} = list, style) do
%{list | item_style: style}
end
@doc """
Sets the base indentation level.
"""
@spec indent(t(), non_neg_integer()) :: t()
def indent(%__MODULE__{} = list, spaces) when is_integer(spaces) and spaces >= 0 do
%{list | indent: spaces}
end
@doc """
Enables or disables automatic theme colors.
When enabled (default), the list uses theme colors for:
- Enumerators (`:muted` color)
Explicit styles (via `enumerator_style/2`) override theme colors.
## Examples
# Disable theme colors
List.new(["Item 1", "Item 2"]) |> List.use_theme(false)
"""
@spec use_theme(t(), boolean()) :: t()
def use_theme(%__MODULE__{} = list, enabled) when is_boolean(enabled) do
%{list | use_theme: enabled}
end
@doc """
Renders the list to a string.
"""
@spec render(t()) :: String.t()
def render(%__MODULE__{items: []}), do: ""
def render(%__MODULE__{} = list) do
render_items(list.items, list, 0)
|> Enum.join("\n")
end
defp render_items(items, list, depth) do
base_indent = String.duplicate(" ", list.indent + depth * 2)
enumerator_style = get_effective_enumerator_style(list)
{lines, _} =
Enum.reduce(items, {[], 0}, fn item, {acc, idx} ->
case item do
%__MODULE__{} = nested ->
# Nested list - render with increased depth (doesn't increment parent index)
nested_lines = render_items(nested.items, merge_styles(nested, list), depth + 1)
{acc ++ nested_lines, idx}
text when is_binary(text) ->
enum_text = get_enumerator(list.enumerator, idx)
styled_enum =
if enumerator_style do
Esc.render(enumerator_style, enum_text)
else
enum_text
end
styled_item =
if list.item_style do
Esc.render(list.item_style, text)
else
text
end
{acc ++ [base_indent <> styled_enum <> styled_item], idx + 1}
end
end)
lines
end
defp merge_styles(nested, parent) do
%{nested |
enumerator_style: nested.enumerator_style || parent.enumerator_style,
item_style: nested.item_style || parent.item_style,
indent: parent.indent,
use_theme: nested.use_theme && parent.use_theme
}
end
defp get_enumerator(:bullet, _idx), do: "• "
defp get_enumerator(:dash, _idx), do: "- "
defp get_enumerator(:arabic, idx), do: "#{idx + 1}. "
defp get_enumerator(:roman, idx), do: "#{to_roman(idx + 1)}. "
defp get_enumerator(:alphabet, idx), do: "#{to_alphabet(idx)}. "
defp get_enumerator(func, idx) when is_function(func, 1), do: func.(idx)
defp to_roman(n) when n <= 0, do: ""
defp to_roman(n) when n >= 1000, do: "m" <> to_roman(n - 1000)
defp to_roman(n) when n >= 900, do: "cm" <> to_roman(n - 900)
defp to_roman(n) when n >= 500, do: "d" <> to_roman(n - 500)
defp to_roman(n) when n >= 400, do: "cd" <> to_roman(n - 400)
defp to_roman(n) when n >= 100, do: "c" <> to_roman(n - 100)
defp to_roman(n) when n >= 90, do: "xc" <> to_roman(n - 90)
defp to_roman(n) when n >= 50, do: "l" <> to_roman(n - 50)
defp to_roman(n) when n >= 40, do: "xl" <> to_roman(n - 40)
defp to_roman(n) when n >= 10, do: "x" <> to_roman(n - 10)
defp to_roman(n) when n >= 9, do: "ix" <> to_roman(n - 9)
defp to_roman(n) when n >= 5, do: "v" <> to_roman(n - 5)
defp to_roman(n) when n >= 4, do: "iv" <> to_roman(n - 4)
defp to_roman(n) when n >= 1, do: "i" <> to_roman(n - 1)
defp to_alphabet(idx) do
<<97 + rem(idx, 26)>>
end
# Theme-aware style resolution
# Gets effective enumerator style: explicit style > theme style > nil
defp get_effective_enumerator_style(list) do
case {list.enumerator_style, list.use_theme, Esc.get_theme()} do
{style, _, _} when not is_nil(style) ->
# Explicit style takes precedence
style
{nil, true, theme} when not is_nil(theme) ->
# Use theme colors
Esc.style()
|> Esc.foreground(Esc.Theme.color(theme, :muted))
_ ->
nil
end
end
end