Current section
Files
Jump to
Current section
Files
lib/identicon.ex
# ┌────────────────────────────────────────────────────────────────┐
# │ Based on the course "The Complete Elixir and Phoenix Bootcamp" │
# │ by Stephen Grider. │
# └────────────────────────────────────────────────────────────────┘
defmodule Identicon do
@moduledoc """
Opens a PNG file populated with an identicon derived from an input string,
a dimension and a size.
##### Based on the course [The Complete Elixir and Phoenix Bootcamp](https://www.udemy.com/the-complete-elixir-and-phoenix-bootcamp-and-tutorial/) by Stephen Grider.
"""
use PersistConfig
alias __MODULE__.{Drawer, Help, Image, Log}
@default_dimension get_env(:default_dimension)
@default_switches get_env(:default_switches)
@default_bell? @default_switches.bell
@default_duration @default_switches.duration
@default_size @default_switches.size
@directory get_env(:directory)
@valid_dimensions get_env(:valid_dimensions)
@valid_durations get_env(:valid_durations)
@valid_sizes get_env(:valid_sizes)
@typedoc "Identicon"
@type t :: binary
defguardp valid?(input, dimension, size, duration)
when is_binary(input) and dimension in @valid_dimensions and
size in @valid_sizes and duration in @valid_durations
@doc """
Opens a PNG file populated with an identicon derived from `input` and
`options`.
## Options
* `:dimension` - number of squares across and down in the identicon.
Can be `5` (default), `6`, `7`, `8`, `9` or `10`.
* `:size` - width and height of the identicon in pixels.
Can be `250` (default), `300` or `350`.
* `:duration` - number of seconds to display the identicon.
Can be `3` (default), `4`, `5`, `6`, `7`, `8` or `9`.
* `:bell` - whether to ring the bell. Defaults to `false`.
## Examples
# Creates file "olive 250px 5x5.png" and opens it for 3 seconds.
Identicon.display("olive")
# Creates file "grape 300px 7x7.png" and opens it for 5 seconds.
Identicon.display("grape", dimension: 7, size: 300, duration: 5)
"""
@spec display(String.t(), keyword) :: :ok
def display(input, options \\ []) do
dimension = Keyword.get(options, :dimension, @default_dimension)
size = Keyword.get(options, :size, @default_size)
duration = Keyword.get(options, :duration, @default_duration)
bell? = Keyword.get(options, :bell, @default_bell?)
:ok = _display(input, dimension, size, duration, bell?)
end
## Private functions
@spec _display(String.t(), pos_integer, pos_integer, pos_integer, any) :: :ok
defp _display(input, dimension, size, duration, bell?)
when valid?(input, dimension, size, duration) do
base_name = "#{input} #{size}px #{dimension}x#{dimension}.png"
file_path = String.replace("#{@directory}/#{base_name}", "/", separator())
open_with = open_with()
close_cmd = close_cmd()
identicon = Image.new(input, dimension, size) |> Drawer.render()
case File.write(file_path, identicon) do
:ok ->
info = self() |> Process.info()
iex? = info[:dictionary][:iex_server]
IO.write(if !iex? && bell?, do: "\a", else: "")
_open_pid = spawn_link(fn -> open(open_with, file_path) end)
closetask = Task.async(fn -> close(close_cmd, duration) end)
await_max = :timer.seconds(duration + 1)
_charlist = Task.await(closetask, await_max)
args = {base_name, @directory, open_with, duration, __ENV__}
:ok = Log.info(:identicon_displayed, args)
{:error, reason} ->
:ok = Log.error(:cannot_write, {base_name, @directory, reason, __ENV__})
end
end
defp _display(_input, _dimension, _size, _duration, _bell?) do
:ok = Help.print_help()
end
@spec open_with :: binary
defp open_with, do: get_env(:open_with)
@spec separator :: binary
defp separator, do: get_env(:separator)
@spec close_cmd :: charlist
defp close_cmd, do: get_env(:close_cmd)
@dialyzer {:no_unused, [open: 2]}
@spec open(binary, binary) :: charlist
defp open(open_with, file_path) do
:os.cmd(~c[#{open_with} "#{file_path}"])
end
@dialyzer {:no_unused, [close: 2]}
@spec close(charlist, pos_integer) :: charlist
defp close(close_cmd, duration) do
:timer.seconds(duration) |> Process.sleep()
:os.cmd(close_cmd)
end
end