Skip to content

Commit aaa2386

Browse files
committed
Initial commit
0 parents  commit aaa2386

File tree

12 files changed

+597
-0
lines changed

12 files changed

+597
-0
lines changed

.formatter.exs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Used by "mix format"
2+
[
3+
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4+
]

.gitignore

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# The directory Mix will write compiled artifacts to.
2+
/_build/
3+
4+
# If you run "mix test --cover", coverage assets end up here.
5+
/cover/
6+
7+
# The directory Mix downloads your dependencies sources to.
8+
/deps/
9+
10+
# Where third-party dependencies like ExDoc output generated docs.
11+
/doc/
12+
13+
# Ignore .fetch files in case you like to edit your project deps locally.
14+
/.fetch
15+
16+
# If the VM crashes, it generates a dump, let's ignore it too.
17+
erl_crash.dump
18+
19+
# Also ignore archive artifacts (built via "mix archive.build").
20+
*.ez
21+
22+
# Ignore package tarball (built via "mix hex.build").
23+
owl-*.tar
24+
25+
# Temporary files, for example, from tests.
26+
/tmp/

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Owl
2+
```
3+
,_,
4+
{o,o}
5+
/) )
6+
---"-"--
7+
```
8+
9+
**TODO: Add description**
10+
11+
## Installation
12+
13+
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
14+
by adding `owl` to your list of dependencies in `mix.exs`:
15+
16+
```elixir
17+
def deps do
18+
[
19+
{:owl, "~> 0.1.0"}
20+
]
21+
end
22+
```
23+
24+
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
25+
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
26+
be found at [https://hexdocs.pm/owl](https://hexdocs.pm/owl).
27+

lib/owl.ex

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
defmodule Owl do
2+
def input(type, opts \\ []) do
3+
value =
4+
case String.trim(IO.gets(Keyword.fetch!(opts, :prompt) <> "\n")) do
5+
"" -> nil
6+
string -> string
7+
end
8+
9+
cond do
10+
not Keyword.get(opts, :allow_blank, false) and is_nil(value) ->
11+
puts(Owl.Data.tag(:red, "Cannot be blank"))
12+
input(type, opts)
13+
14+
true ->
15+
value
16+
end
17+
end
18+
19+
def cmd(command, args, opts \\ []) do
20+
log_shell_command(command, args)
21+
System.cmd(command, args, opts)
22+
end
23+
24+
def log_shell_command(command, args) do
25+
command =
26+
case args do
27+
[] -> command
28+
args -> "#{command} #{Enum.join(args, " ")}"
29+
end
30+
31+
command = sanitize_passwords_in_urls(command)
32+
33+
puts(Owl.Data.tag(:light_black, "$ #{command}"))
34+
end
35+
36+
def puts(content) do
37+
content
38+
|> Owl.Data.to_iodata()
39+
|> IO.puts()
40+
end
41+
42+
defp sanitize_passwords_in_urls(text) do
43+
Regex.replace(~r/\w+:\/\/[^ ]+/, text, fn value ->
44+
uri = URI.parse(value)
45+
46+
case uri.userinfo do
47+
nil ->
48+
value
49+
50+
userinfo ->
51+
[username, _password] = String.split(userinfo, ":")
52+
to_string(%{uri | userinfo: username <> ":********"})
53+
end
54+
end)
55+
end
56+
end

lib/owl/data.ex

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
defmodule Owl.Data do
2+
def tag(sequences, data) do
3+
Owl.Data.Tag.new(sequences, data)
4+
end
5+
6+
@box_symbols %{
7+
top_left: "┌",
8+
top: "─",
9+
top_right: "┐",
10+
right: "│",
11+
left: "│",
12+
bottom_left: "└",
13+
bottom: "─",
14+
bottom_right: "┘"
15+
}
16+
def box(data, opts \\ []) do
17+
padding_top = Keyword.get(opts, :padding_top, 0)
18+
padding_bottom = Keyword.get(opts, :padding_bottom, 0)
19+
padding_left = Keyword.get(opts, :padding_left, 2)
20+
padding_right = Keyword.get(opts, :padding_right, 2)
21+
22+
lines =
23+
(List.duplicate([], padding_top) ++ split(data, "\n") ++ List.duplicate([], padding_bottom))
24+
|> Enum.map(&{&1, Owl.Data.length(&1)})
25+
26+
max_line_length = lines |> Enum.map(&elem(&1, 1)) |> Enum.max()
27+
28+
[
29+
@box_symbols.top_left,
30+
String.duplicate(@box_symbols.top, max_line_length + padding_left + padding_right),
31+
@box_symbols.top_right,
32+
"\n",
33+
lines
34+
|> Enum.map(fn {line, length} ->
35+
[
36+
@box_symbols.left,
37+
String.duplicate(" ", padding_left),
38+
line,
39+
String.duplicate(" ", max_line_length - length + padding_right),
40+
@box_symbols.right
41+
]
42+
end)
43+
|> Enum.intersperse("\n"),
44+
"\n",
45+
@box_symbols.bottom_left,
46+
String.duplicate(@box_symbols.bottom, max_line_length + padding_left + padding_right),
47+
@box_symbols.bottom_right
48+
]
49+
end
50+
51+
def length(data) when is_binary(data) do
52+
String.length(data)
53+
end
54+
55+
def length(data) when is_list(data) do
56+
Enum.reduce(data, 0, fn item, acc -> Owl.Data.length(item) + acc end)
57+
end
58+
59+
def length(%Owl.Data.Tag{data: data}) do
60+
Owl.Data.length(data)
61+
end
62+
63+
def add_prefix(data, prefix) do
64+
data
65+
|> split("\n")
66+
|> Enum.map(fn line ->
67+
[prefix, line]
68+
end)
69+
|> Enum.intersperse("\n")
70+
end
71+
72+
def to_iodata(data) do
73+
data
74+
|> split("\n")
75+
|> Enum.intersperse("\n")
76+
|> do_to_iodata(%{foreground: :default_color, background: :default_background})
77+
|> IO.ANSI.format()
78+
end
79+
80+
defp do_to_iodata(
81+
%Owl.Data.Tag{sequences: sequences, data: data},
82+
%{foreground: fg, background: bg}
83+
) do
84+
parent_sequences = Enum.reject([fg, bg], &is_nil/1)
85+
86+
[
87+
sequences,
88+
do_to_iodata(data, sequences_to_state(parent_sequences ++ sequences)),
89+
parent_sequences
90+
]
91+
end
92+
93+
defp do_to_iodata([head | tail], state) do
94+
[do_to_iodata(head, state) | do_to_iodata(tail, state)]
95+
end
96+
97+
defp do_to_iodata(term, _state), do: term
98+
99+
defp maybe_wrap_to_tag([], [element]), do: element
100+
defp maybe_wrap_to_tag([], data), do: data
101+
102+
defp maybe_wrap_to_tag(sequences1, [%Owl.Data.Tag{sequences: sequences2, data: data}]) do
103+
Owl.Data.Tag.new(collapse_sequences(sequences1 ++ sequences2), data)
104+
end
105+
106+
defp maybe_wrap_to_tag(sequences, data) do
107+
Owl.Data.Tag.new(collapse_sequences(sequences), data)
108+
end
109+
110+
defp reverse_and_tag(sequences, [%Owl.Data.Tag{sequences: last_sequences} | _] = data) do
111+
maybe_wrap_to_tag(sequences -- last_sequences, Enum.reverse(data))
112+
end
113+
114+
defp reverse_and_tag(sequences, data) do
115+
maybe_wrap_to_tag(sequences, Enum.reverse(data))
116+
end
117+
118+
# last write wins
119+
defp collapse_sequences(sequences) do
120+
sequences
121+
|> sequences_to_state()
122+
|> Map.values()
123+
|> Enum.reject(&is_nil/1)
124+
end
125+
126+
def split(data, pattern), do: split(data, pattern, [])
127+
defp split([], _pattern, _acc_sequences), do: []
128+
129+
defp split(data, pattern, acc_sequences) do
130+
case do_split(data, pattern, [], acc_sequences) do
131+
{before_pattern, after_pattern, []} ->
132+
[
133+
reverse_and_tag(acc_sequences, before_pattern)
134+
| split(after_pattern, pattern, acc_sequences)
135+
]
136+
137+
{before_pattern, after_pattern, next_acc_sequences} ->
138+
[
139+
reverse_and_tag(acc_sequences ++ next_acc_sequences, before_pattern)
140+
| split(after_pattern, pattern, next_acc_sequences)
141+
]
142+
end
143+
end
144+
145+
defp do_split([head | tail], pattern, acc, acc_sequences) do
146+
case do_split(head, pattern, acc, acc_sequences) do
147+
{new_head, [], new_acc_sequences} ->
148+
do_split(tail, pattern, new_head, new_acc_sequences)
149+
150+
{new_head, new_tail, new_acc_sequences} ->
151+
new_tail = maybe_wrap_to_tag(new_acc_sequences -- acc_sequences, new_tail)
152+
153+
new_acc_sequences =
154+
case new_head do
155+
[%Owl.Data.Tag{sequences: sequences} | _] -> new_acc_sequences -- sequences
156+
_ -> new_acc_sequences
157+
end
158+
159+
new_head =
160+
case new_head do
161+
[%Owl.Data.Tag{data: []} | rest] -> rest
162+
list -> list
163+
end
164+
165+
{new_head, [new_tail | tail], new_acc_sequences}
166+
end
167+
end
168+
169+
defp do_split([], _pattern, acc, acc_sequences) do
170+
{acc, [], acc_sequences}
171+
end
172+
173+
defp do_split(
174+
%Owl.Data.Tag{sequences: sequences, data: data},
175+
pattern,
176+
acc,
177+
acc_sequences
178+
) do
179+
{before_pattern, after_pattern, next_acc_sequences} =
180+
do_split(data, pattern, [], acc_sequences ++ sequences)
181+
182+
before_pattern = reverse_and_tag(sequences, before_pattern)
183+
184+
next_acc_sequences =
185+
case {before_pattern, after_pattern} do
186+
{%Owl.Data.Tag{sequences: sequences}, []} -> next_acc_sequences -- sequences
187+
{_, []} -> acc_sequences
188+
{_, _} -> next_acc_sequences
189+
end
190+
191+
{[before_pattern | acc], after_pattern, next_acc_sequences}
192+
end
193+
194+
defp do_split(value, pattern, acc, acc_sequences) when is_binary(value) do
195+
[head | tail] = String.split(value, pattern, parts: 2)
196+
197+
{
198+
case head do
199+
"" -> acc
200+
value -> [value | acc]
201+
end,
202+
tail,
203+
acc_sequences
204+
}
205+
end
206+
207+
defp sequences_to_state(sequences) do
208+
Enum.reduce(sequences, %{foreground: nil, background: nil}, fn sequence, acc ->
209+
Map.put(acc, sequence_type(sequence), sequence)
210+
end)
211+
end
212+
213+
for color <- [:black, :red, :green, :yellow, :blue, :magenta, :cyan, :white] do
214+
defp sequence_type(unquote(color)), do: :foreground
215+
defp sequence_type(unquote(:"light_#{color}")), do: :foreground
216+
defp sequence_type(unquote(:"#{color}_background")), do: :background
217+
defp sequence_type(unquote(:"light_#{color}_background")), do: :background
218+
end
219+
220+
defp sequence_type(:default_color), do: :foreground
221+
defp sequence_type(:default_background), do: :background
222+
223+
# https://github.com/elixir-lang/elixir/blob/74bfab8ee271e53d24cb0012b5db1e2a931e0470/lib/elixir/lib/io/ansi.ex#L73
224+
defp sequence_type("\e[38;5;" <> _), do: :foreground
225+
226+
# https://github.com/elixir-lang/elixir/blob/74bfab8ee271e53d24cb0012b5db1e2a931e0470/lib/elixir/lib/io/ansi.ex#L87
227+
defp sequence_type("\e[48;5;" <> _), do: :background
228+
end

lib/owl/data/tag.ex

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
defmodule Owl.Data.Tag do
2+
defstruct sequences: [], data: []
3+
4+
@doc false
5+
def new(sequences, data) do
6+
%__MODULE__{
7+
sequences: List.wrap(sequences),
8+
data: data
9+
}
10+
end
11+
end

lib/owl/io.ex

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
defmodule Owl.IO do
2+
def inspect(data) do
3+
["\n", data, "\n"]
4+
|> Owl.Data.to_iodata()
5+
|> Owl.puts()
6+
7+
data
8+
end
9+
end

mix.exs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
defmodule Owl.MixProject do
2+
use Mix.Project
3+
4+
def project do
5+
[
6+
app: :owl,
7+
version: "0.1.0",
8+
elixir: "~> 1.12",
9+
start_permanent: Mix.env() == :prod,
10+
deps: deps()
11+
]
12+
end
13+
14+
# Run "mix help compile.app" to learn about applications.
15+
def application do
16+
[
17+
extra_applications: [:logger]
18+
]
19+
end
20+
21+
# Run "mix help deps" to learn about dependencies.
22+
defp deps do
23+
[
24+
{:ex_doc, "~> 0.24", only: :dev, runtime: false}
25+
]
26+
end
27+
end

0 commit comments

Comments
 (0)