defmodule Owl.Box do
@moduledoc """
Allows wrapping data to boxes.
"""
@border_styles %{
none: %{
top_left: "",
top: "",
top_right: "",
right: "",
left: "",
bottom_left: "",
bottom: "",
bottom_right: ""
},
solid: %{
top_left: "┌",
top: "─",
top_right: "┐",
right: "│",
left: "│",
bottom_left: "└",
bottom: "─",
bottom_right: "┘"
},
solid_rounded: %{
top_left: "╭",
top: "─",
top_right: "╮",
right: "│",
left: "│",
bottom_left: "╰",
bottom: "─",
bottom_right: "╯"
},
double: %{
top_left: "╔",
top: "═",
top_right: "╗",
right: "║",
left: "║",
bottom_left: "╚",
bottom: "═",
bottom_right: "╝"
}
}
@title_padding_left 1
@title_padding_right 4
@doc """
Wraps data into a box.
## Options
* `:padding` - sets the padding area for all four sides at once. Defaults to 0.
* `:padding_x` - sets `:padding_right` and `:padding_left` at once. Overrides value set by `:padding`. Defaults to 0.
* `:padding_y` - sets `:padding_top` and `:padding_bottom` at once. Overrides value set by `:padding`. Defaults to 0.
* `:padding_top` - sets the padding area for top side. Overrides value set by `:padding_y` or `:padding`. Defaults to 0.
* `:padding_bottom` - sets the padding area for bottom side. Overrides value set by `:padding_y` or `:padding`. Defaults to 0.
* `:padding_right` - sets the padding area for right side. Overrides value set by `:padding_x` or `:padding`. Defaults to 0.
* `:padding_left` - sets the padding area for left side. Overrides value set by `:padding_x` or `:padding`. Defaults to 0.
* `:min_height` - sets the minimum height of the box, including paddings and size of the borders. Defaults to 0.
* `:min_width` - sets the minimum width of the box, including paddings and size of the borders. Defaults to 0.
* `:max_width` - sets the maximum width of the box, including paddings and size of the borders. Defaults to width of the terminal, if available, `:infinity` otherwise.
* `:horizontal_align` - sets the horizontal alignment of the content inside a box. Defaults to `:right`.
* `:vertical_align` - sets the vertical alignment of the content inside a box. Defaults to `:top`.
* `:border_style` - sets the border style. Defaults to `:solid`.
* `:title` - sets a title that is displayed in a top border. Ignored if `:border_style` is `:none`. Defaults to `nil`.
## Examples
iex> "Owl" |> Owl.Box.new() |> to_string()
\"""
┌───┐
│Owl│
└───┘
\""" |> String.trim_trailing()
iex> "Owl" |> Owl.Box.new(padding_x: 4) |> to_string()
\"""
┌───────────┐
│ Owl │
└───────────┘
\""" |> String.trim_trailing()
iex> "Hello\\nworld!"
...> |> Owl.Box.new(
...> title: "Greeting!",
...> min_width: 20,
...> horizontal_align: :center,
...> border_style: :double
...> )
...> |> to_string()
\"""
╔═Greeting!════════╗
║ Hello ║
║ world! ║
╚══════════════════╝
\""" |> String.trim_trailing()
iex> "Success"
...> |> Owl.Box.new(
...> min_width: 20,
...> min_height: 3,
...> border_style: :none,
...> horizontal_align: :right,
...> vertical_align: :bottom
...> )
...> |> to_string()
\"""
Success
\""" |> String.trim_trailing()
iex> "OK"
...> |> Owl.Box.new(min_height: 5, vertical_align: :middle)
...> |> to_string()
\"""
┌──┐
│ │
│OK│
│ │
└──┘
\""" |> String.trim_trailing()
iex> "VeryLongLine" |> Owl.Box.new(max_width: 6) |> to_string()
\"""
┌────┐
│Very│
│Long│
│Line│
└────┘
\""" |> String.trim_trailing()
iex> "VeryLongLine" |> Owl.Box.new(max_width: 4, border_style: :none) |> to_string()
\"""
Very
Long
Line
\""" |> String.trim_trailing()
iex> "Green!"
...> |> Owl.Data.tag(:green)
...> |> Owl.Box.new(title: Owl.Data.tag("Red!", :red))
...> |> Owl.Data.tag(:cyan)
...> |> Owl.Data.to_ansidata()
...> |> to_string()
\"""
\e[36m┌─\e[31mRed!\e[36m────┐\e[39m
\e[36m│\e[32mGreen!\e[36m │\e[39m
\e[36m└─────────┘\e[39m\e[0m
\""" |> String.trim_trailing()
"""
@spec new(Owl.Data.t(),
padding: non_neg_integer(),
padding_x: non_neg_integer(),
padding_y: non_neg_integer(),
padding_top: non_neg_integer(),
padding_bottom: non_neg_integer(),
padding_right: non_neg_integer(),
padding_left: non_neg_integer(),
min_height: non_neg_integer(),
min_width: non_neg_integer(),
max_width: non_neg_integer() | :infinity,
horizontal_align: :left | :center | :right,
vertical_align: :top | :middle | :bottom,
border_style: :solid | :solid_rounded | :double | :none,
title: nil | Owl.Data.t()
) :: Owl.Data.t()
def new(data, opts \\ []) do
padding = Keyword.get(opts, :padding, 0)
padding_x = Keyword.get(opts, :padding_x, padding)
padding_y = Keyword.get(opts, :padding_y, padding)
padding_top = Keyword.get(opts, :padding_top, padding_y)
padding_bottom = Keyword.get(opts, :padding_bottom, padding_y)
padding_left = Keyword.get(opts, :padding_left, padding_x)
padding_right = Keyword.get(opts, :padding_right, padding_x)
min_width = Keyword.get(opts, :min_width, 0)
min_height = Keyword.get(opts, :min_height, 0)
horizontal_align = Keyword.get(opts, :horizontal_align, :left)
vertical_align = Keyword.get(opts, :vertical_align, :top)
border_style = Keyword.get(opts, :border_style, :solid)
border_symbols = Map.fetch!(@border_styles, border_style)
title = Keyword.get(opts, :title)
max_width = opts[:max_width] || Owl.IO.columns() || :infinity
max_width =
if is_integer(max_width) and max_width < min_width do
min_width
else
max_width
end
max_inner_width =
case max_width do
:infinity -> :infinity
width -> width - borders_size(border_style) - padding_right - padding_left
end
lines = Owl.Data.lines(data)
lines =
case max_inner_width do
:infinity -> lines
max_width -> Enum.flat_map(lines, fn line -> Owl.Data.chunk_every(line, max_width) end)
end
data_height = length(lines)
inner_height =
max(
data_height,
min_height - borders_size(border_style) - padding_bottom - padding_top
)
{padding_before, padding_after} =
case vertical_align do
:top ->
{padding_top, padding_bottom + inner_height - data_height}
:middle ->
to_center = div(inner_height - data_height, 2)
{padding_top + to_center, inner_height - data_height - to_center + padding_bottom}
:bottom ->
{padding_bottom + inner_height - data_height, padding_top}
end
lines =
List.duplicate({[], 0}, padding_before) ++
Enum.map(lines, fn line ->
{line, Owl.Data.length(line)}
end) ++ List.duplicate({[], 0}, padding_after)
min_width_required_by_title =
if is_nil(title) do
0
else
Owl.Data.length(title) + @title_padding_left + @title_padding_right +
borders_size(border_style)
end
if is_integer(max_width) and min_width_required_by_title > max_width do
raise ArgumentError, "`:title` is too big for given `:max_width`"
end
inner_width =
Enum.max([
min_width - padding_right - padding_left - borders_size(border_style),
min_width_required_by_title - padding_right - padding_left - borders_size(border_style)
| Enum.map(lines, fn {_line, line_length} -> line_length end)
])
top_border =
case border_style do
:none ->
[]
_ ->
[
border_symbols.top_left,
if is_nil(title) do
String.duplicate(border_symbols.top, inner_width + padding_left + padding_right)
else
[
String.duplicate(border_symbols.top, @title_padding_left),
title,
String.duplicate(
border_symbols.top,
inner_width - (min_width_required_by_title - borders_size(border_style)) +
padding_left + padding_right
),
String.duplicate(border_symbols.top, @title_padding_right)
]
end,
border_symbols.top_right,
"\n"
]
end
bottom_border =
case border_style do
:none ->
[]
_ ->
[
if(inner_height > 0, do: "\n", else: []),
border_symbols.bottom_left,
String.duplicate(border_symbols.bottom, inner_width + padding_left + padding_right),
border_symbols.bottom_right
]
end
[
top_border,
lines
|> Enum.map(fn {line, length} ->
{padding_before, padding_after} =
case horizontal_align do
:left ->
{padding_left, inner_width - length + padding_right}
:right ->
{inner_width - length + padding_left, padding_right}
:center ->
to_center = div(inner_width - length, 2)
{padding_left + to_center, inner_width - length - to_center + padding_right}
end
[
border_symbols.left,
String.duplicate(" ", padding_before),
line,
String.duplicate(" ", padding_after),
border_symbols.right
]
end)
|> Owl.Data.unlines(),
bottom_border
]
end
defp borders_size(:none = _border_style), do: 0
defp borders_size(_border_style), do: 2
end