Elixir + Make command = MagicalMake ~ 魔法陣を描きながらmakeを叩くCLIをElixirで作ってみました ~ 

# 魔法陣を描きながらmakeを叩くCLIをElixirで作ってみました

Elixirのライブラリ作ってみたいなぁ \
+ 日常的に使えるものだと愛着湧くなぁ \
+ 最近[Earthly](https://earthly.dev/)というmakeに関連した技術が良かったなぁ、 \
+ （Elixir...魔法の薬、、、魔法、、、 + make...） \
= **makeを叩く時に魔方陣が表示されるCLIでも作ってみよう！！**

という、安直な動機から、こちらのCLIを作成致しました。
実装に関して、ライブラリの紹介を兼ねて、設計諸々もご紹介させていただけたらとおもいます。
参考になりましたら幸いです。

![demo](https://user-images.githubusercontent.com/33741858/147806471-13b68d26-21e7-4e05-b831-45bfad37e277.gif)

https://github.com/tashirosota/magical_make

https://hexdocs.pm/magical_make/api-reference.html


CLIの作り方に関してはこちらの記事でご紹介していますので、**「Elixir盛り上げちゃうぞ、CLI作っちゃうぞ」** という方はぜひ見てやってください。

https://qiita.com/sotashiro/private/f7e59abd97254dbb12dd

## magical_makeの実装解説
以下要件

 - make commandの資産をそのまま使うことが出来る
 - 魔法陣のアスキーアートを**イイカンジ**に描画することが出来る
 - 叩くmake command自体のアスキーアートを出力することが出来る
 - 魔法陣のアスキーアートやmake commandは色付けして出力することが出来る
 - CLIを用いて上記操作を行うことが出来る

### 全体像

以下、各要件とそれぞれに該当する関数の一覧です。

| | MagicalMake.Circle | MagicalMake.CLI | MagicalMake.Font | MagicalMake.Make | MagicalMake.SystemCommand | MagicalMake.Painter |
| ---- | ---- | ---- | ---- | ---- | ---- | ---- |
| make command | - | - | - | exec_make | exec | - |
| 魔法陣のアスキーアート | draw_circle | - | - | - | - | draw |
| make commandのアスキーアート | - | - | - | draw_make | - | draw |
| 色付け | - | - | create_decoration | - | - | - |
| CLI | - | main | - | - | - | - |

さっぱりしてますね、execだったりexecuteだったりは多少気の迷いが感じられます。

### MagicalMake.executeの流れ
`MagicalMake.execute`が全体の処理をまとめたmoduleとなっています。
この処理を追っていけば実装の内容や各module責務や設計が把握出来ます。

```elixir
def execute(make_command) do
  make_check!() # ① makeが実行可能な事を確認
  font_decoration = create_decoration() # ② 色付け情報の作成
  draw_circle(@interval, font_decoration) # ③ 魔法陣の描画
  |> draw_make(make_command, @final_interval) # ④ 魔法陣の前面にmakeのアスキーアートを描画
  exec_make(make_command) # ⑤ make commandの実行
  :ok
end
```
こちらの順番に沿ってそれぞれ関数の解説を以下記載します。

#### MagicalMake.Fontの解説

MagicalMake.executeでのこちらの部分です

```elixir
def execute(make_command) do
  ...
  font_decoration = create_decoration() # ② 色付け情報の作成
  ...
end
```

実装はこちら

```elixir
defmodule MagicalMake.Font do
  @esc "\e"
  @colors [
    black: "#{@esc}[30m",
    red: "#{@esc}[31m",
    green: "#{@esc}[32m",
    yellow: "#{@esc}[33m",
    blue: "#{@esc}[34m",
    magenta: "#{@esc}[35m",
    cyan: "#{@esc}[36m"
  ]

  def create_decoration() do
    @colors |> Keyword.values() |> Enum.random()
  end
end

```
##### create_decoration/0
予めstdoutへの色付け情報をKeywordListとして保持しておき、それらをランダムで取得するだけというシンプルな実装です。
今後色味の指定が出来るようにしたり、それらを用いてトリッキーな絵柄を作ったりと、拡張する可能性が高いmoduleとなっております。@escはわざわざmodule変数に分けなくても良かったかも。。。

#### MagicalMake.Circleの解説

MagicalMake.executeでのこちらの部分です

```elixir
def execute(make_command) do
  ...
  draw_circle(@interval, font_decoration) # ③ 魔法陣の描画
  ...
end
```

実装はこちら

```elixir
defmodule MagicalMake.Circle do
  @last_word "M"
  @gsub_words [
    ".",
    ";",
    "i",
    "I",
    @last_word
  ]
  @assets_path "priv/assets/circles/"
  @circle_texts [
    elem(File.read(@assets_path <> "1.txt"), 1),
    elem(File.read(@assets_path <> "2.txt"), 1),
    elem(File.read(@assets_path <> "3.txt"), 1),
    elem(File.read(@assets_path <> "4.txt"), 1),
    elem(File.read(@assets_path <> "5.txt"), 1)
  ]

  def draw_circle(interval, font_decoration) do
    circle_txt = @circle_texts |> Enum.random()

    Enum.each(@gsub_words ++ [], fn word ->
      (font_decoration <> String.replace(circle_txt, ~r/\S/, word))
      |> MagicalMake.Painter.draw(interval)
    end)

    font_decoration <> String.replace(circle_txt, ~r/\S/, @last_word)
  end
end
```
##### draw_circle/2
事前に用意しておいたアスキーアートを `priv/assets/circles/` に配置しておき、module変数として定義しておきます。
なぜ先んじて読み込んでおく必要があるかというのは、こちらでまとめておりますので、興味がある方はご閲覧ください。

https://qiita.com/sotashiro/items/4f39b07260dc3af429b2

それらをランダムに取得して、`@gsub_words`の順に `MagicalMake.Painter.draw` を用いて徐々に描画していきます。
文字列を`@gsub_words`の順に置き換えることにより、アスキーアートのデータに依存せず、表示を安定させています。
また、最後に描画される文字列を返却して、後述のmakeのアスキーアートの背面に指定できるような設計となっております。
アスキーアート自体の種類に関しては `@circle_texts`、アニメーションに関しては `@gsub_words`で担保しているため、変更のしやすいmoduleとなってます。
ロジック的にはシンプルですので、これらを他言語で実装することも容易いかと思います。

#### MagicalMake.Makeの解説

MagicalMake.executeでのこちらの部分です

```elixir
def execute(make_command) do
  make_check!() # ① makeが実行可能な事を確認
  ...
  |> draw_make(make_command, @final_interval) # ④ 魔法陣の前面にmakeのアスキーアートを描画
  exec_make(make_command) # ⑤ make commandの実行
end
```

実装はこちら
すこし長くなってしまったため、private functionに関しては割愛させていただきます。

```elixir
defmodule MagicalMake.Make do
  @make "make"
  @asc_word "※"
  @pin_word "@"
  @make_y_range 1..8
  @make_x_range 0..100
  @circle_x_center 75
  @ciercle_y_make_start 17
  @font elem(Chisel.Font.load("priv/assets/5x8.bdf"), 1)

  def make_check! do
    System.find_executable(@make) ||
      raise MagicalMake.MakefileMissing,
        message: "Makefile does not exist. Must exist in current directory."
    :ok
  end

  def exec_make(command) do
    MagicalMake.SystemCommand.exec(
      @make,
      [command |> to_string()],
      into: IO.stream(:stdio, :line)
    )
  end

  def draw_make(circle_text, make_command, interval) do
    create_make_art(circle_text, make_command)
    |> MagicalMake.Painter.draw(interval)
  end

  defp create_make_art(circle_text, make_command) do
    make_texts = create_make_texts(make_command)

    circle_text
    |> String.split("\n")
    |> Enum.with_index(fn text, index ->
      if index >= @ciercle_y_make_start && index < @ciercle_y_make_start + Enum.count(make_texts) do
        make_texts |> Enum.at(index - @ciercle_y_make_start)
      else
        text
      end
    end)
    |> Enum.join("\n")
  end

  defp create_make_texts(make_command) do
    ...
    make_texts = ... # Chisel.Renderer.reduce_draw_textを用いてmake commandをアスキーアートに変換

    space_count = @circle_x_center - max_x_length(make_texts) |> div(2)
    spaces = Enum.map(0..(space_count - 1), fn _ -> " " end)
    # 魔法陣の真ん中に表示されるようにmake commandアスキーアートの開始位置を整えていく
    fin_make_texts = make_texts |> Enum.map(fn words -> spaces ++ words end)
    # 境界線を含むmake commandアスキーアートを作成
    (create_partition() ++ fin_make_texts ++ create_partition())
  end

  defp create_partition() do
    # 魔法陣とmake commandのアスキーアートの境界線を作成
  end

  defp max_x_length(make_text) do
    # make commandのアスキーアートでの最大の長さをゴニョゴニョして取得する
    ...
  end
end

```
##### make_check!/0
[System.find_executable](https://hexdocs.pm/elixir/1.13.1/System.html#find_executable/1) を用いてmake comamndを叩くことができるかの確認。不可だった場合 `MagicalMake.MakefileMissing` の例外を発火します。
##### exec_make/1
`MagicalMake.SystemCommand` に対して指定の引数でmake commandの発火を促します。
##### draw_make/3
make commandを[Chisel](https://hexdocs.pm/chisel/readme.html)を用いてアスキーアートに変換。
それらを魔法陣の真ん中に描画出来るよう加工、背面が魔法陣、前面がmake commandのアスキーアートとして、指定インターバルで描画します。
`create_make_art`に関しては割愛しつつも流れ自体にコメントを記述しておきました。
`@circle_x_center 75, @ciercle_y_make_start 17` といった変数の計算を動的ではなく実装してしまったため、事前に用意してあるアスキーアートへの依存度が高くなってしまっており、この部分に関しては、後に改善したいポイントです。
この辺りのロジックが一番複雑度が高い部分になってるかと思います。
#### MagicalMake.Painter, MagicalMake.SystemCommandの補足
MagicalMake.PainterとMagicalMake.SystemCommandの補足をさせていただきます。

##### MagicalMake.Painter
```elixir
defmodule MagicalMake.Painter do
  def draw(text, interval) do
    IO.puts(text)
    Process.sleep(interval)
    refresh!()
    :ok
  end

  def refresh! do
    :font_reset |> sys_cmd()
    :clear |> sys_cmd()
    :ok
  end
end
```
###### draw
描画に関してはシンプルで、必要な以下処理を順次行うのみの実装です。

 - `IO.puts`　での表示
 - sleepによるintervalの実現
 - refresh!()による画面クリアとfont情報のreset

###### refresh!
SystemCommand.sys_cmd()で定義されている画面クリアとfont情報のresetを呼び出しを行っております。

##### MagicalMake.SystemCommand
```elixir
defmodule MagicalMake.SystemCommand do
  @commands [
    clear: %{
      command: "printf",
      args: ["'\e[2J\e[3J\e[H'"],
      opts: [into: IO.stream(:stdio, :line)]
    },
    font_reset: %{
      command: "tput",
      args: ["sgr0"],
      opts: [into: IO.stream(:stdio, :line)]
    }
  ]

  def sys_cmd(command) do
    command_map = Keyword.fetch!(@commands, command)
    exec(
      command_map.command,
      command_map.args,
      command_map.opts
    )
  end

  def exec(command, args, opts) do
    System.cmd(
      command,
      args,
      opts
    )
    :ok
  end
end
```

###### sys_cmd
MagicalMake内で利用されるコマンドを事前に変数に入れておき、それらをatomを用いて呼び出すだけのシンプルなfunctionです。
###### exec
System.cmdのwrapper functionです。わざわざ分けずともいいくらいの実装になってます。

## おわりに
今回Elixirを用いたCLIの作成では、初めてなことが多く、とても勉強になりました。
magical_make自体は副作用も少ない軽量なCLIです。
是非、入れて遊んでみていただきたいです。make commandを楽しく表示出来るだけですが、ちょっとだけ気分が変わります。

https://github.com/tashirosota/magical_make

hexへの登録に辺り、module docの記述はspec, unittest, actionsの整備等も経験する事ができたので、ライブラリの作成はElixirコトハジメにはベストなのでは？と感じました。

幸いなことに、アイディアはまだありますので、今後またElixir開発に役立つようなものをなにか作ってみたいと思っております。

2022はガッツリElixirをしていきたいと思います。
引き続きよろしくおねがいします。