Packages

Create boxes in the terminal

Current section

Files

Jump to
exbox lib exbox.ex
Raw

lib/exbox.ex

defmodule ExBox do
@moduledoc """
Provides a simple interface to text boxes.
"""
@nl "\n"
@pad " "
alias ExBox.Boxes
alias ExBox.ANSI
alias ExBox.Width
@doc """
Returns a text box with the given text and style options. Text boxes are returned
in List form, which can be printed.
To convert to a string, use `IO.iodata_to_binary`.
**Options**
## `border_color`
Color of the box border.
Values: `:black :red :green :yellow :blue :magenta :cyan :white :gray :grey`
## `background_color`
Color of the background.
Values: `:black :red :green :yellow :blue :magenta :cyan :white`
## `border_style`
Style of the box border.
Default: `:single`
Values: `:single :double :round :single_double :double_single :classic`
## `padding`
Space between the text and box border.
Default: `0`
Accepts a number or an Keyword List with any of the `top`, `right`, `bottom`, `left` properties.
When a number is specified, the left/right padding is 3 times the top/bottom to make it look nice.
## `margin`
Space around the box.
Default: `0`
Accepts a number or an Keyword List with any of the `top`, `right`, `bottom`, `left` properties.
When a number is specified, the left/right padding is 3 times the top/bottom to make it look nice.
## `align`
Align the text in the box based on the widest line.
Default: `:left`
Values: `:left :center :right`
## `float`
Float the box on the available terminal screen space.
Default: `:left`
Values: `:right :center :left`
Examples:
`"hello" |> ExBox.get`
`"hello" |> ExBox.get(padding: 5, border_color: :red)`
"""
@spec get(String.t, Keyword.t) :: List.t
def get(text, opts \\ [])
def get(text, opts) when is_list(text) do
text
|> IO.iodata_to_binary
|> get(opts)
end
def get(text, opts) when is_binary(text) do
default = [
padding: 0,
margin: 0,
border_style: :single,
dim_border: false,
align: :left,
float: :left
]
# Merge given options with default, preferring the given
opts = Keyword.merge(default, opts)
{cols, _} = TermSize.get
chars = get_border_chars(opts[:border_style])
padding = get_list(opts[:padding])
margin = get_list(opts[:margin])
# Align the text, handling possible ANSI codes
text = ANSI.align(text, [align: opts[:align]])
# Split the lines and add given padding to the top or bottom
lines = text |> String.split("\n") |> fill_lines(padding)
# Get the width of the widest line and add the left and right padding
content_width = Width.widest_line(text) + padding[:left] + padding[:right]
padding_left = duplicate(@pad, padding[:left])
# Overwrite left margin depending on given float options (center and right floats need specific numbers)
margin_left = align_left(margin[:left], opts[:float], content_width, cols)
# These lines take care of the borders, sizing them to fit the given text
horizontal = duplicate(chars.horizontal, content_width)
top = colorize_border([duplicate(@nl, margin[:top]), margin_left, chars.top_left, horizontal, chars.top_right], opts)
bottom = colorize_border([margin_left, chars.bottom_left, horizontal, chars.bottom_right, duplicate(@nl, margin[:bottom])], opts)
side = colorize_border(chars.vertical, opts)
# For each line, add left margin, the side border, padding, text, and the ending side border
middle =
lines
|> Enum.map(
fn(line) ->
padding_right = duplicate(@pad, content_width - Width.string(line) - padding[:left])
[margin_left, side, colorize_content([padding_left, line, padding_right], opts), side]
end)
|> Enum.join(@nl)
[top, @nl, middle, @nl, bottom]
end
defp get_list(detail) when is_number(detail) do
# If padding or margin was given as a number, return a list with some default values
[top: detail, right: detail * 3, bottom: detail, left: detail * 3]
end
defp get_list(detail) when is_list(detail) do
# If padding or margin was given as a list, merge it with some default values
default = [top: 0, right: 0, bottom: 0, left: 0]
Keyword.merge(default, detail)
end
defp get_border_chars(style) when is_atom(style) do
# Return the box style by calling the given function in the `Boxes` module
# For example, an argument of `:classic` will return the call to `Boxes.classic`
apply(Boxes, style, [])
end
defp colorize_border(x, opts) do
case Keyword.fetch(opts, :border_color) do
{:ok, c} ->
# Apply the given color `c` to the string representation of the border characters
apply(ExChalk, c, [IO.iodata_to_binary(x)])
:error ->
x
end
end
defp colorize_content(x, opts) do
case Keyword.fetch(opts, :bg_color) do
{:ok, c} ->
# Apply the given color `c` to the background of the content
color =
"bg_"
|> String.to_charlist
|> Enum.concat(Atom.to_charlist(c))
|> List.to_atom
apply(ExChalk, color, [IO.iodata_to_binary(x)])
:error ->
x
end
end
defp duplicate(x, n) do
# Wrapper over `duplicate` that accepts a string and puts it in a List
# This is done because we need the result to be a List
List.duplicate([x], n)
end
defp fill_lines(lines, padding) do
top = List.duplicate("", padding[:top])
bottom = List.duplicate("", padding[:bottom])
top ++ lines ++ bottom
end
defp align_left(margin_left, float, content_width, cols) do
# Returns a new left margin depending on arguments given
# Center and right floats require the correct amount of margin
case float do
:center ->
pad_width = div((cols - content_width), 2)
List.duplicate([@pad], pad_width)
:right ->
pad_width = Enum.max([cols - content_width - 3, 0])
if pad_width < 0 do
List.duplicate([@pad], 0)
else
List.duplicate([@pad], pad_width)
end
_ ->
List.duplicate([@pad], margin_left)
end
end
end