defmodule Cowrie do
@moduledoc """
`Cowrie` helps you print beautiful and consistent Terminal output to the Shell
using familiar functions inspired by HTML.
The output of each function can be styled via configuration (see the page on
configuration) or by overriding any configured styles by passing relevant
options as the second argument. This is analogous to cascading style sheets (CSS).
"""
require Logger
# By default, these codes are added after any formatted text
@default_formatting_reset IO.ANSI.default_background() <> IO.ANSI.normal() <> IO.ANSI.reset()
@red IO.ANSI.color(162)
@blue IO.ANSI.color(57)
@green IO.ANSI.color(42)
@yellow IO.ANSI.color(190)
@grey IO.ANSI.color(246)
@bold IO.ANSI.bright()
@default_styles %{
# For alert/2
alert_danger: @red,
alert_info: @blue,
alert_success: @green,
alert_warning: @yellow,
box_chars: %{
t: "═",
tr: "╗",
r: "║",
br: "╝",
b: "═",
bl: "╚",
l: "║",
tl: "╔"
},
box_padding: 1,
br: "",
cols: 80,
comment: @grey,
error: "",
dt: @blue,
dd: " : " <> @grey,
h1: @bold <> IO.ANSI.white_background() <> IO.ANSI.black(),
h1_transform: &Cowrie.Transforms.upcase/2,
h2: @bold,
info: @green,
hr: IO.ANSI.color(236),
hr_char: "─",
hr_padding: 4,
li: "",
li_bullet: "-",
line: "",
ol_start: 1,
prompt: "",
secret: "",
th: IO.ANSI.bright(),
td: "",
warning: @yellow,
yes?: "",
# The post_format map can define custom formatting resets.
post_format: %{
error: ""
},
# The pre_transforms map can define callbacks to functions that modify the text
# BEFORE formatting it.
pre_transforms: %{
alert_danger: &Cowrie.Transforms.alert_danger/2,
alert_info: &Cowrie.Transforms.alert_info/2,
alert_success: &Cowrie.Transforms.alert_success/2,
alert_warning: &Cowrie.Transforms.alert_warning/2,
h1: &Cowrie.Transforms.center/2,
hr: &Cowrie.Transforms.hr/2,
ol_li: &Cowrie.Transforms.ol_li/2,
ul_li: &Cowrie.Transforms.ul_li/2
},
# The post_transforms map can define callbacks to functions that modify the text
# AFTER formatting it.
post_transforms: %{
h1: [&Cowrie.Transforms.prepend_newline/2, &Cowrie.Transforms.append_newline/2],
h2: &Cowrie.Transforms.append_newline/2,
hr: &Cowrie.Transforms.append_newline/2
},
# Must implement the `Cowrie.SpinnerBehaviour` behaviour
spinner_module: Cowrie.TimerSpinner
}
# See scripts/color_map.exs
@color_map [
%{index: 0, rgb: nil},
%{index: 1, rgb: nil},
%{index: 2, rgb: nil},
%{index: 3, rgb: nil},
%{index: 4, rgb: nil},
%{index: 5, rgb: nil},
%{index: 6, rgb: nil},
%{index: 7, rgb: nil},
%{index: 8, rgb: nil},
%{index: 9, rgb: nil},
%{index: 10, rgb: nil},
%{index: 11, rgb: nil},
%{index: 12, rgb: nil},
%{index: 13, rgb: nil},
%{index: 14, rgb: nil},
%{index: 15, rgb: nil},
%{index: 16, rgb: {0, 0, 0}},
%{index: 17, rgb: {0, 0, 1}},
%{index: 18, rgb: {0, 0, 2}},
%{index: 19, rgb: {0, 0, 3}},
%{index: 20, rgb: {0, 0, 4}},
%{index: 21, rgb: {0, 0, 5}},
%{index: 22, rgb: {0, 1, 0}},
%{index: 23, rgb: {0, 1, 1}},
%{index: 24, rgb: {0, 1, 2}},
%{index: 25, rgb: {0, 1, 3}},
%{index: 26, rgb: {0, 1, 4}},
%{index: 27, rgb: {0, 1, 5}},
%{index: 28, rgb: {0, 2, 0}},
%{index: 29, rgb: {0, 2, 1}},
%{index: 30, rgb: {0, 2, 2}},
%{index: 31, rgb: {0, 2, 3}},
%{index: 32, rgb: {0, 2, 4}},
%{index: 33, rgb: {0, 2, 5}},
%{index: 34, rgb: {0, 3, 0}},
%{index: 35, rgb: {0, 3, 1}},
%{index: 36, rgb: {0, 3, 2}},
%{index: 37, rgb: {0, 3, 3}},
%{index: 38, rgb: {0, 3, 4}},
%{index: 39, rgb: {0, 3, 5}},
%{index: 40, rgb: {0, 4, 0}},
%{index: 41, rgb: {0, 4, 1}},
%{index: 42, rgb: {0, 4, 2}},
%{index: 43, rgb: {0, 4, 3}},
%{index: 44, rgb: {0, 4, 4}},
%{index: 45, rgb: {0, 4, 5}},
%{index: 46, rgb: {0, 5, 0}},
%{index: 47, rgb: {0, 5, 1}},
%{index: 48, rgb: {0, 5, 2}},
%{index: 49, rgb: {0, 5, 3}},
%{index: 50, rgb: {0, 5, 4}},
%{index: 51, rgb: {0, 5, 5}},
%{index: 52, rgb: {1, 0, 0}},
%{index: 53, rgb: {1, 0, 1}},
%{index: 54, rgb: {1, 0, 2}},
%{index: 55, rgb: {1, 0, 3}},
%{index: 56, rgb: {1, 0, 4}},
%{index: 57, rgb: {1, 0, 5}},
%{index: 58, rgb: {1, 1, 0}},
%{index: 59, rgb: {1, 1, 1}},
%{index: 60, rgb: {1, 1, 2}},
%{index: 61, rgb: {1, 1, 3}},
%{index: 62, rgb: {1, 1, 4}},
%{index: 63, rgb: {1, 1, 5}},
%{index: 64, rgb: {1, 2, 0}},
%{index: 65, rgb: {1, 2, 1}},
%{index: 66, rgb: {1, 2, 2}},
%{index: 67, rgb: {1, 2, 3}},
%{index: 68, rgb: {1, 2, 4}},
%{index: 69, rgb: {1, 2, 5}},
%{index: 70, rgb: {1, 3, 0}},
%{index: 71, rgb: {1, 3, 1}},
%{index: 72, rgb: {1, 3, 2}},
%{index: 73, rgb: {1, 3, 3}},
%{index: 74, rgb: {1, 3, 4}},
%{index: 75, rgb: {1, 3, 5}},
%{index: 76, rgb: {1, 4, 0}},
%{index: 77, rgb: {1, 4, 1}},
%{index: 78, rgb: {1, 4, 2}},
%{index: 79, rgb: {1, 4, 3}},
%{index: 80, rgb: {1, 4, 4}},
%{index: 81, rgb: {1, 4, 5}},
%{index: 82, rgb: {1, 5, 0}},
%{index: 83, rgb: {1, 5, 1}},
%{index: 84, rgb: {1, 5, 2}},
%{index: 85, rgb: {1, 5, 3}},
%{index: 86, rgb: {1, 5, 4}},
%{index: 87, rgb: {1, 5, 5}},
%{index: 88, rgb: {2, 0, 0}},
%{index: 89, rgb: {2, 0, 1}},
%{index: 90, rgb: {2, 0, 2}},
%{index: 91, rgb: {2, 0, 3}},
%{index: 92, rgb: {2, 0, 4}},
%{index: 93, rgb: {2, 0, 5}},
%{index: 94, rgb: {2, 1, 0}},
%{index: 95, rgb: {2, 1, 1}},
%{index: 96, rgb: {2, 1, 2}},
%{index: 97, rgb: {2, 1, 3}},
%{index: 98, rgb: {2, 1, 4}},
%{index: 99, rgb: {2, 1, 5}},
%{index: 100, rgb: {2, 2, 0}},
%{index: 101, rgb: {2, 2, 1}},
%{index: 102, rgb: {2, 2, 2}},
%{index: 103, rgb: {2, 2, 3}},
%{index: 104, rgb: {2, 2, 4}},
%{index: 105, rgb: {2, 2, 5}},
%{index: 106, rgb: {2, 3, 0}},
%{index: 107, rgb: {2, 3, 1}},
%{index: 108, rgb: {2, 3, 2}},
%{index: 109, rgb: {2, 3, 3}},
%{index: 110, rgb: {2, 3, 4}},
%{index: 111, rgb: {2, 3, 5}},
%{index: 112, rgb: {2, 4, 0}},
%{index: 113, rgb: {2, 4, 1}},
%{index: 114, rgb: {2, 4, 2}},
%{index: 115, rgb: {2, 4, 3}},
%{index: 116, rgb: {2, 4, 4}},
%{index: 117, rgb: {2, 4, 5}},
%{index: 118, rgb: {2, 5, 0}},
%{index: 119, rgb: {2, 5, 1}},
%{index: 120, rgb: {2, 5, 2}},
%{index: 121, rgb: {2, 5, 3}},
%{index: 122, rgb: {2, 5, 4}},
%{index: 123, rgb: {2, 5, 5}},
%{index: 124, rgb: {3, 0, 0}},
%{index: 125, rgb: {3, 0, 1}},
%{index: 126, rgb: {3, 0, 2}},
%{index: 127, rgb: {3, 0, 3}},
%{index: 128, rgb: {3, 0, 4}},
%{index: 129, rgb: {3, 0, 5}},
%{index: 130, rgb: {3, 1, 0}},
%{index: 131, rgb: {3, 1, 1}},
%{index: 132, rgb: {3, 1, 2}},
%{index: 133, rgb: {3, 1, 3}},
%{index: 134, rgb: {3, 1, 4}},
%{index: 135, rgb: {3, 1, 5}},
%{index: 136, rgb: {3, 2, 0}},
%{index: 137, rgb: {3, 2, 1}},
%{index: 138, rgb: {3, 2, 2}},
%{index: 139, rgb: {3, 2, 3}},
%{index: 140, rgb: {3, 2, 4}},
%{index: 141, rgb: {3, 2, 5}},
%{index: 142, rgb: {3, 3, 0}},
%{index: 143, rgb: {3, 3, 1}},
%{index: 144, rgb: {3, 3, 2}},
%{index: 145, rgb: {3, 3, 3}},
%{index: 146, rgb: {3, 3, 4}},
%{index: 147, rgb: {3, 3, 5}},
%{index: 148, rgb: {3, 4, 0}},
%{index: 149, rgb: {3, 4, 1}},
%{index: 150, rgb: {3, 4, 2}},
%{index: 151, rgb: {3, 4, 3}},
%{index: 152, rgb: {3, 4, 4}},
%{index: 153, rgb: {3, 4, 5}},
%{index: 154, rgb: {3, 5, 0}},
%{index: 155, rgb: {3, 5, 1}},
%{index: 156, rgb: {3, 5, 2}},
%{index: 157, rgb: {3, 5, 3}},
%{index: 158, rgb: {3, 5, 4}},
%{index: 159, rgb: {3, 5, 5}},
%{index: 160, rgb: {4, 0, 0}},
%{index: 161, rgb: {4, 0, 1}},
%{index: 162, rgb: {4, 0, 2}},
%{index: 163, rgb: {4, 0, 3}},
%{index: 164, rgb: {4, 0, 4}},
%{index: 165, rgb: {4, 0, 5}},
%{index: 166, rgb: {4, 1, 0}},
%{index: 167, rgb: {4, 1, 1}},
%{index: 168, rgb: {4, 1, 2}},
%{index: 169, rgb: {4, 1, 3}},
%{index: 170, rgb: {4, 1, 4}},
%{index: 171, rgb: {4, 1, 5}},
%{index: 172, rgb: {4, 2, 0}},
%{index: 173, rgb: {4, 2, 1}},
%{index: 174, rgb: {4, 2, 2}},
%{index: 175, rgb: {4, 2, 3}},
%{index: 176, rgb: {4, 2, 4}},
%{index: 177, rgb: {4, 2, 5}},
%{index: 178, rgb: {4, 3, 0}},
%{index: 179, rgb: {4, 3, 1}},
%{index: 180, rgb: {4, 3, 2}},
%{index: 181, rgb: {4, 3, 3}},
%{index: 182, rgb: {4, 3, 4}},
%{index: 183, rgb: {4, 3, 5}},
%{index: 184, rgb: {4, 4, 0}},
%{index: 185, rgb: {4, 4, 1}},
%{index: 186, rgb: {4, 4, 2}},
%{index: 187, rgb: {4, 4, 3}},
%{index: 188, rgb: {4, 4, 4}},
%{index: 189, rgb: {4, 4, 5}},
%{index: 190, rgb: {4, 5, 0}},
%{index: 191, rgb: {4, 5, 1}},
%{index: 192, rgb: {4, 5, 2}},
%{index: 193, rgb: {4, 5, 3}},
%{index: 194, rgb: {4, 5, 4}},
%{index: 195, rgb: {4, 5, 5}},
%{index: 196, rgb: {5, 0, 0}},
%{index: 197, rgb: {5, 0, 1}},
%{index: 198, rgb: {5, 0, 2}},
%{index: 199, rgb: {5, 0, 3}},
%{index: 200, rgb: {5, 0, 4}},
%{index: 201, rgb: {5, 0, 5}},
%{index: 202, rgb: {5, 1, 0}},
%{index: 203, rgb: {5, 1, 1}},
%{index: 204, rgb: {5, 1, 2}},
%{index: 205, rgb: {5, 1, 3}},
%{index: 206, rgb: {5, 1, 4}},
%{index: 207, rgb: {5, 1, 5}},
%{index: 208, rgb: {5, 2, 0}},
%{index: 209, rgb: {5, 2, 1}},
%{index: 210, rgb: {5, 2, 2}},
%{index: 211, rgb: {5, 2, 3}},
%{index: 212, rgb: {5, 2, 4}},
%{index: 213, rgb: {5, 2, 5}},
%{index: 214, rgb: {5, 3, 0}},
%{index: 215, rgb: {5, 3, 1}},
%{index: 216, rgb: {5, 3, 2}},
%{index: 217, rgb: {5, 3, 3}},
%{index: 218, rgb: {5, 3, 4}},
%{index: 219, rgb: {5, 3, 5}},
%{index: 220, rgb: {5, 4, 0}},
%{index: 221, rgb: {5, 4, 1}},
%{index: 222, rgb: {5, 4, 2}},
%{index: 223, rgb: {5, 4, 3}},
%{index: 224, rgb: {5, 4, 4}},
%{index: 225, rgb: {5, 4, 5}},
%{index: 226, rgb: {5, 5, 0}},
%{index: 227, rgb: {5, 5, 1}},
%{index: 228, rgb: {5, 5, 2}},
%{index: 229, rgb: {5, 5, 3}},
%{index: 230, rgb: {5, 5, 4}},
%{index: 231, rgb: {5, 5, 5}},
%{index: 232, rgb: nil},
%{index: 233, rgb: nil},
%{index: 234, rgb: nil},
%{index: 235, rgb: nil},
%{index: 236, rgb: nil},
%{index: 237, rgb: nil},
%{index: 238, rgb: nil},
%{index: 239, rgb: nil},
%{index: 240, rgb: nil},
%{index: 241, rgb: nil},
%{index: 242, rgb: nil},
%{index: 243, rgb: nil},
%{index: 244, rgb: nil},
%{index: 245, rgb: nil},
%{index: 246, rgb: nil},
%{index: 247, rgb: nil},
%{index: 248, rgb: nil},
%{index: 249, rgb: nil},
%{index: 250, rgb: nil},
%{index: 251, rgb: nil},
%{index: 252, rgb: nil},
%{index: 253, rgb: nil},
%{index: 254, rgb: nil},
%{index: 255, rgb: nil}
]
@doc """
Prints an alert box of the specified type to STDOUT
(think [Bootstrap alert boxes](https://getbootstrap.com/docs/4.0/components/alerts/)).
Options:
- `:type` indicates the type of alert, one of `:danger`, `:info`, `:success`, `:warning`. default: `:warning`
Formatting and transform options correspond to the given alert `type`:
- `:alert_danger` for type `:danger`
- `:alert_info` for type `:info`
- `:alert_success` for type `:success`
- `:alert_warning` for type `:warning`
## Examples
iex(2)> alert("The answer is 42", type: :info)
╔═══════════════════════════╗
║ ℹ️ Info:️ The answer is 42 ║
╚═══════════════════════════╝
"""
def alert(text, opts \\ []) when is_binary(text) do
type = Keyword.get(opts, :type, :warning)
# We have to translate the config key so we don't collide with the keys defined
# for info/2, info/2, and warning/2
key =
Map.get(
%{
danger: :alert_danger,
info: :alert_info,
success: :alert_success,
warning: :alert_warning
},
type
)
transform_print(text, opts, key)
end
@doc """
Prints a line break (an empty line) to STDOUT.
Formatting and transforms options: `:br`
## Examples
iex> br
"""
def br, do: transform_print("", [], :br)
@doc """
Outputs available ANSI colors, useful when you want to choose a color visually.
## Examples
iex> colors
"""
def colors do
@color_map
|> Enum.each(fn %{index: i, rgb: rgb} -> Mix.shell().info(color_swatch_text(i, rgb)) end)
end
@doc """
Prints a comment to STDOUT, i.e. supplemental text regarded as non-essential.
Formatting and transforms options: `:comment`
## Examples
iex> comment("Objects are closer than they appear")
Objects are closer than they appear
"""
def comment(text, opts \\ []) when is_binary(text), do: transform_print(text, opts, :comment)
@doc """
Prints a dictionary term (dt) to STDOUT
Formatting and transforms options: `:dt`
## Examples
iex(5)> dt("Thonking", dt: ">>> " <> IO.ANSI.bright())
>>> Thonking
"""
def dt(text, opts \\ []) when is_binary(text), do: transform_print(text, opts, :dt)
@doc """
Prints a dictionary definition (dd) to STDOUT
Formatting and transforms options: `:dd`
"""
def dd(text, opts \\ []) when is_binary(text), do: transform_print(text, opts, :dd)
@doc """
Prints a demonstration of all output functions for visual inspection, useful when you
are tweaking your styling.
## Examples
iex> demo
Cowrie Formatting h1/2
Common Outputs h2/2)
This is a line of text line/2
This is an informational message info/2
This is a warning/2
Here comes an hr/2:
This is an error/2
────────────────────────────────────────────────────────────────────────
Alerts h2/2
╔════════════════════════════════╗
║ ℹ️ Info:️ This is an info alert ║
╚════════════════════════════════╝
# ... etc ...
"""
def demo do
h1("Cowrie Formatting h1/2")
h2("Common Outputs h2/2)")
line("This is a line of text line/2")
info("This is an informational message info/2")
warning("This is a warning/2")
error("This is an error/2")
line("Here comes an hr/2:")
hr()
h2("Alerts h2/2")
alert("This is an info alert", type: :info)
alert("This is a warning alert", type: :warning)
alert("This is a danger alert", type: :danger)
alert("This is a success alert", type: :success)
hr()
h2("Lists h2/2")
ul(["This is", "an unordered", "list via ul/2"])
br()
ol(["This is", "an ordered", "list via ol/2"])
hr()
h2("Tables h2/2")
table([["Max", "Mao", "Brutus"], ["Fido", "Whiskers", "Wilbur"]], ["Dogs", "Cats", "Pigs"])
hr()
h2("Definitions h2/2")
dt("This is a term dt/2")
dd("This is a term definition dd/2")
hr()
h2("Prompts h2/2")
# We simulate these so we can inspect the formatting by using STDOUT instead of the various input streams
transform_print("prompt/2 What's your name?", [], :prompt)
transform_print("yes?/2 Ok to continue?", [], :yes?)
transform_print("secret/2 Password:", [], :secret)
hr()
h2("Spinners h2/2")
spinner(fn -> Process.sleep(:timer.seconds(5)) end)
br()
end
@doc """
Prints an error message to STDERR.
Formatting and transforms options: `:error`
Your terminal may already provide formatting for messages sent to STDERR; be careful
about formatting these messages because you are more likely to have tests that assert
for specific error messages being logged, and adding ANSI formatting codes to the output
can make it a bit more difficult to verify the exact text.
"""
def error(text, opts \\ []) when is_binary(text),
do: transform_print(text, opts, :error, :stderr)
@doc """
Prints a primary heading (h1) to STDOUT.
Formatting and transforms options: `:h1`
## Examples
Normally, you would put your pre- and post-transformation functions in your config, but you
can pass them as arguments if you really want to:
iex> h1("This is awesome!", pre_transforms: %{h1: &Cowrie.Transforms.upcase/2})
THIS IS AWESOME!
"""
def h1(text, opts \\ []) when is_binary(text), do: transform_print(text, opts, :h1)
@doc """
Prints a secondary heading (h2) to STDOUT.
Formatting and transforms options: `:h2`
"""
def h2(text, opts \\ []) when is_binary(text), do: transform_print(text, opts, :h2)
@doc """
Prints a horizontal rule (hr) to STDOUT.
Formatting options:
- `:hr` for the line as a whole
- `:hr_char` specifies the character(s) to be repeated to form the line.
- `:hr_padding` number of whitespace columns
Transform key: `:hr`
## Examples
iex> hr(hr_char: "~", hr_padding: 0)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
"""
def hr(opts \\ []) do
opts = Keyword.put(opts, :hr_padding, config(opts, :hr_padding))
opts
|> config(:hr_char)
|> pre_transform(opts, :hr)
|> format(opts, :hr)
|> post_format(opts, :hr)
|> post_transform(opts, :hr)
|> out(:stdout)
end
@doc """
Prints informational text to STDOUT.
Formatting and transforms options: `:info`
"""
def info(text, opts \\ []) when is_binary(text), do: transform_print(text, opts, :info)
@doc """
Prints a list item (unordered) to STDOUT.
This function is useful if don't have a list to pass to `ul/2`.
Formatting option: `:li`; transform options: `:ul_li`.
`:li_bullet` changes the character(s) used for the list bullet.
## Examples
iex> li("Bullet me", li_bullet: "~>")
~> Bullet me
"""
def li(text, opts \\ []) when is_binary(text) do
# Pass our config along to the transform function
opts = Keyword.put(opts, :li_bullet, config(opts, :li_bullet))
text
|> pre_transform(opts, :ul_li)
|> format(opts, :li)
|> post_format(opts, :li)
|> post_transform(opts, :ul_li)
|> out(:stdout)
end
@doc """
Prints a line of "unformatted" text to STDOUT.
Formatting and transforms options: `:line`
"""
def line(text, opts \\ []) when is_binary(text), do: transform_print(text, opts, :line)
@doc """
Prints an ordered list of items (ul) to STDOUT.
- `:ol_start` the integer used to start the lists. Default: 1
- `:li` for formatting the individual list items.
Pre- and post-transform key: `:ol_li`
## Examples
iex> ol(["do", "re", "mi"])
1. do
2. re
3. mi
"""
def ol(items, opts \\ []) when is_list(items) do
items
|> Enum.with_index(config(opts, :ol_start))
|> Enum.each(fn {li, index} ->
opts = Keyword.put(opts, :n, index)
li
|> pre_transform(opts, :ol_li)
|> format(opts, :li)
|> post_format(opts, :li)
|> post_transform(opts, :ol_li)
|> out(:stdout)
end)
end
@doc """
Prompts the user for input. Input will be consumed until Enter is pressed.
*Careful:* the returned value will include a newline (`\\n`)!
Formatting and transforms options: `:prompt`
"""
@spec prompt(text :: binary, opts :: keyword) :: binary
def prompt(text, opts \\ []) when is_binary(text),
do: transform_print(text, opts, :prompt, :prompt)
@doc """
The user's input will not be visible to them as they type in the console.
This method is useful when asking for sensitive information such as a password.
Formatting and transforms options: `:secret`
"""
@spec secret(text :: binary, opts :: keyword) :: binary
def secret(text, opts \\ []) when is_binary(text) do
transform_print(text, opts, :secret, :secret)
# This erlang library returns a charlist
:io.get_password()
|> List.to_string()
end
@doc """
Displays a spinner animation which offers visual feedback appropriate for long-
running tasks. Displaying a spinner animation is most appropriate for tasks which
do not provide their own console messaging or when you wish to silence the task's
own messaging.
The callback function must be a zero-arity anonymous function. The return value
of the spinner function will the return value of this function.
Note: the task being executed should avoid any use of `IO.puts` (or other functions
that write to STDOUT) and instead rely on `Mix.shell().info`; all console messages
logged by the callback will be silenced by temporarily setting the `Mix.Shell` to
`Mix.Shell.Quiet` for the duration of the executed function.
## Examples
Using the function capture notation `&`:
iex> spinner(&Heavy.work/0)
Using an anonymous function:
iex> spinner(fn -> Heavy.work() end)
As used in the demo:
iex> spinner(fn -> Process.sleep(:timer.seconds(5)) end)
Demonstrating a return value:
iex> spinner(fn ->
...> Process.sleep(:timer.seconds(5))
...> {:ok, "Task complete!"}
...> end)
{:ok, "Task complete!"}
Specifying a custom spinner module:
iex> spinner(fn ->
...> Process.sleep(:timer.seconds(5))
...> {:ok, "Task complete!"}
...> end, spinner_module: Tacocat.Cat)
{:ok, "Task complete!"}
"""
def spinner(callback, opts \\ []) when is_function(callback) do
spinner_module = config(opts, :spinner_module)
cols = config(opts, :cols)
timeout = config(opts, :timeout, :infinity)
pid = spawn(fn -> spinner_module.start(cols: cols) end)
task =
Task.async(fn ->
Application.ensure_all_started(:gettext)
shell = Mix.shell()
Mix.shell(Mix.Shell.Quiet)
returned_value = callback.()
Mix.shell(shell)
returned_value
end)
returned_value = Task.await(task, timeout)
Process.exit(pid, :kill)
# Add a new line b/c the spinner itself cannot
Mix.shell().info("")
returned_value
end
@doc """
Prints a table with the given rows to STDOUT (headers are optional).
The first argument should be a list of lists, where each internal list represents a row of data.
The second argument should be a list of headers -- the length of this list
should match the length of the lists passed as rows.
Formatting options:
- `:th` for header cells
- `:td` for data cells
Transforms:
- `:th` for header cells
- `:td` for data cells
## Examples:
iex(12)> table([["Max", "Mao", "Brutus"], ["Fido", "Whiskers", "Wilbur"]], ["Dogs", "Cats", "Pigs"])
+------+----------+--------+
| Dogs | Cats | Pigs |
+------+----------+--------+
| Max | Mao | Brutus |
| Fido | Whiskers | Wilbur |
+------+----------+--------+
"""
def table(rows, headers \\ [], opts \\ []) when is_list(rows) and is_list(headers) do
headers =
headers
|> Enum.map(fn th ->
th
|> pre_transform(opts, :th)
|> format(opts, :th)
|> post_format(opts, :th)
end)
rows
|> Enum.map(fn tr ->
tr
|> Enum.map(fn td ->
td
|> pre_transform(opts, :td)
|> format(opts, :td)
|> post_format(opts, :td)
end)
end)
|> TableRex.quick_render!(headers)
|> out(:stdout)
end
@doc """
Prints an unordered list of items (ul) to STDOUT.
- `:li_bullet` the character used to denote each list item. Default: `-`
- `:li` for formatting the individual list items.
Transform key for each list item: `:ul_li`
## Examples
iex(13)> ul(["Sunlight", "Water", "Soil"])
- Sunlight
- Water
- Soil
iex(14)> ul(["Sunlight", "Water", "Soil"], li_bullet: ">")
> Sunlight
> Water
> Soil
"""
def ul(items, opts \\ []) when is_list(items) do
# Pass our config along to the transform function
opts = Keyword.put(opts, :li_bullet, config(opts, :li_bullet))
Enum.each(
items,
fn li ->
li
|> pre_transform(opts, :ul_li)
|> format(opts, :li)
|> post_format(opts, :li)
|> post_transform(opts, :ul_li)
|> out(:stdout)
end
)
end
@doc """
Prints a warning to STDOUT.
Formatting and transforms options: `:warning`
"""
def warning(text, opts \\ []) when is_binary(text), do: transform_print(text, opts, :warning)
@doc """
Prompts the user to continue with a simple `Yes` or `No`.
"""
@spec yes?(text :: binary, opts :: keyword) :: boolean()
def yes?(text, opts \\ []) when is_binary(text), do: transform_print(text, opts, :yes?, :yes?)
################################################
defp color_swatch_text(index, nil) do
IO.ANSI.color(index) <>
"████████████████████ Sample Text #{@default_formatting_reset} IO.ANSI.color(#{index}) -- no RBB alternative --"
end
defp color_swatch_text(index, {r, g, b}) do
IO.ANSI.color(index) <>
"████████████████████ Sample Text #{@default_formatting_reset} IO.ANSI.color(#{index}) or IO.ANSI.color(#{r}, #{g}, #{b})"
end
# Read from config, defer to opts
defp config(opts, key, default \\ "") do
Keyword.get(
opts,
key,
Application.get_env(:cowrie, key, Map.get(@default_styles, key, default))
)
end
# Applies any configured ANSI formatting to text with formatting reset
defp format(text, opts, key) do
"#{config(opts, key)}#{text}"
end
# Applies any configured ANSI formatting reset. This is important so we don't
# end up with ANSI codes polluting our messages (even if they are only reset codes).
defp post_format(text, opts, key) do
reset =
opts
|> config(:post_format, %{})
|> Map.get(key, @default_formatting_reset)
"#{text}#{reset}"
end
# Applies the transformation(s) to the text
defp pre_transform(text, opts, key) do
opts
|> config(:pre_transforms)
|> Map.get(key, nil)
|> List.wrap()
|> Enum.reduce(text, fn
callback, x when is_function(callback) ->
apply(callback, [x, opts])
nil, x ->
x
_, x ->
Logger.error("Pre-transform for key #{key} must be a function.")
x
end)
end
defp post_transform(text, opts, key) do
opts
|> config(:post_transforms)
|> Map.get(key, nil)
|> List.wrap()
|> Enum.reduce(text, fn
callback, x when is_function(callback) ->
apply(callback, [x, opts])
nil, x ->
x
_, x ->
Logger.error("Post-transform for key #{key} must be a function.")
x
end)
end
# The base functions to route to STDOUT, STDERR, and some other Mix.Shell callbacks
defp out(text, :stdout), do: Mix.shell().info("#{text}")
defp out(text, :stderr), do: Mix.shell().error("#{text}")
defp out(text, :prompt), do: Mix.shell().prompt("#{text}")
defp out(text, :yes?), do: Mix.shell().yes?("#{text}")
defp out(text, :secret), do: IO.write("#{text}")
# Applies a transformation, applies formatting, then prints to STDOUT
# Most functions will use the same key for formatting and transformations.
defp transform_print(text, opts, key, stream \\ :stdout) do
text
|> pre_transform(opts, key)
|> format(opts, key)
|> post_format(opts, key)
|> post_transform(opts, key)
|> out(stream)
end
end