6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2022-01-02

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

Elixirのライブラリ作ってみたいなぁ \

  • 日常的に使えるものだと愛着湧くなぁ \
  • 最近Earthlyというmakeに関連した技術が良かったなぁ、 \
  • (Elixir...魔法の薬、、、魔法、、、 + make...)
    = makeを叩く時に魔方陣が表示されるCLIでも作ってみよう!!

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

demo

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

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責務や設計が把握出来ます。

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でのこちらの部分です

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

実装はこちら

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でのこちらの部分です

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

実装はこちら

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変数として定義しておきます。
なぜ先んじて読み込んでおく必要があるかというのは、こちらでまとめておりますので、興味がある方はご閲覧ください。

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

MagicalMake.Makeの解説

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

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

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

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 を用いてmake comamndを叩くことができるかの確認。不可だった場合 MagicalMake.MakefileMissing の例外を発火します。

exec_make/1

MagicalMake.SystemCommand に対して指定の引数でmake commandの発火を促します。

draw_make/3

make commandをChiselを用いてアスキーアートに変換。
それらを魔法陣の真ん中に描画出来るよう加工、背面が魔法陣、前面がmake commandのアスキーアートとして、指定インターバルで描画します。
create_make_artに関しては割愛しつつも流れ自体にコメントを記述しておきました。
@circle_x_center 75, @ciercle_y_make_start 17 といった変数の計算を動的ではなく実装してしまったため、事前に用意してあるアスキーアートへの依存度が高くなってしまっており、この部分に関しては、後に改善したいポイントです。
この辺りのロジックが一番複雑度が高い部分になってるかと思います。

MagicalMake.Painter, MagicalMake.SystemCommandの補足

MagicalMake.PainterとMagicalMake.SystemCommandの補足をさせていただきます。

MagicalMake.Painter
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
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を楽しく表示出来るだけですが、ちょっとだけ気分が変わります。

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

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

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

6
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?