Your First Day with Elixir: Building Something Beautiful from Scratch

I’ve been a Ruby on Rails developer for most of my career. Over the years I’ve heard of the benefits of functional programming, but when I looked into it the syntax and approach seemed out of touch with the ergonomics that Ruby provides. Elixir helps to bridge that gap.

LazyGit and LazyDocker are growing in popularity so let’s build our own TUI, but instead of wiring it up to git or docker, we’re going to build a retro game inspired by the classic Drug Wars. The first part of this tutorial is about laying the groundwork you’ll need to continue to learn elixir with me.

Before We Begin

This tutorial assumes you have Elixir installed on your machine. If you haven’t set it up yet, head over to the official Elixir installation guide and follow the instructions for your operating system. Once you’re set up, come back and we’ll work through it together.

What’s Mix and Why Should You Care?

If you’ve used npm, pip, or cargo, you already understand Mix conceptually. It’s Elixir’s Swiss Army knife. It creates projects, manages dependencies, runs tests, and compiles your code. The difference is that Mix is built into Elixir itself, not a separate tool you need to install.

Let’s create our project:

mix new herb_wars
cd herb_wars

That’s it. Mix just created a complete project structure for you. Take a peek inside:

herb_wars/
├── lib/                    # Your code lives here
│   └── herb_wars.ex        # Main module
├── test/                   # Tests go here
├── mix.exs                 # Project configuration
└── README.md

The mix.exs file is particularly important. It’s where you define your project’s name, version, and dependencies. Think of it as package.json or Cargo.toml for Elixir.

Adding Our First Dependency

Plain terminal output is boring. Let’s add the Owl library to create beautiful boxes and colors. Open mix.exs and find the deps function:

defp deps do
  [
    {:owl, "~> 0.13"}
  ]
end

The ~> operator means “compatible version.” It will accept any version from 0.13.0 up to (but not including) 0.14.0. This keeps your dependencies stable while still getting bug fixes.

Now fetch it:

mix deps.get

Elixir just downloaded Owl from Hex.pm (Elixir’s package registry) and made it available to your project.

Understanding Modules: Your Code’s Home

Open lib/herb_wars.ex. You’ll see the boilerplate that Mix generated:

defmodule HerbWars do
  @moduledoc """
  Documentation for `HerbWars`.
  """

  @doc """
  Hello world.

  ## Examples

      iex> HerbWars.hello()
      :world

  """
  def hello do
    :world
  end
end

There’s a lot going on here already. In Elixir, all code lives inside modules. A module is just a container for related functions. The naming convention is important: your file is herb_wars.ex, so your module is HerbWars (PascalCase).

The @moduledoc and @doc attributes are documentation strings – Elixir treats documentation as a first-class feature, not an afterthought. The ## Examples block inside @doc is even more interesting: those are doctests. When you run mix test, Elixir actually executes HerbWars.hello() and verifies it returns :world. You get tests for free just by documenting your code.

Let’s clean up the placeholder and give the module a proper description:

defmodule HerbWars do
  @moduledoc """
  HerbWars - A terminal-based trading game.
  """
end

We’ll add game logic to this module in later chapters. For now, let’s build something you can actually see. Create a new file at lib/herb_wars/display.ex:

defmodule HerbWars.Display do
  @moduledoc """
  Display functions for HerbWars.
  """

  def display_banner do
    IO.write(IO.ANSI.clear() <> IO.ANSI.home())

    message = [
      "Welcome to ",
      Owl.Data.tag("HerbWars", :green),
      "!\n\n",
      "You have 30 days to maximize your profit buying and selling herbs.\n\n",
      "Press ",
      Owl.Data.tag("ENTER", :cyan),
      " to start your journey..."
    ]

    box = Owl.Box.new(message,
      min_width: 60,
      padding: 1,
      border_style: :solid
    )

    Owl.IO.puts(box)

    IO.gets("")
    :ok
  end
end

Notice the module name: HerbWars.Display. Elixir maps module names to file paths — HerbWars.Display lives in lib/herb_wars/display.ex. The dot becomes a directory separator. Mix created the lib/herb_wars/ directory for us, so this is where we’ll put all our sub-modules as the project grows.

There’s a lot happening here, so let’s break it down piece by piece.

Clearing the Screen

The first line of our display_banner function is:

IO.write(IO.ANSI.clear() <> IO.ANSI.home())

The <> operator concatenates two strings together. IO.ANSI.clear() returns a special string that tells the terminal to clear the screen, and IO.ANSI.home() returns one that moves the cursor to the top-left corner. We join them with <> and write the combined string to the terminal in one shot.

The Pipe Operator: Elixir’s Secret Weapon

Look at these two lines in our display_banner function:

box = Owl.Box.new(message, ...)
Owl.IO.puts(box)

We created a box variable just to pass it to the next function. That works fine, but Elixir has a better way. The pipe operator (|>) takes the result of the left side and passes it as the first argument to the right side:

Owl.Box.new(message, ...)
|> Owl.IO.puts()

No intermediate variable needed. It reads top-to-bottom, like a recipe: first, create the box. Then, print it. Let’s update our display_banner function to use it:

    Owl.Box.new(message,
      min_width: 60,
      padding: 1,
      border_style: :solid
    )
    |> Owl.IO.puts()

This is a pattern you’ll see everywhere in Elixir. Whenever you find yourself creating a variable just to pass it to the next function, reach for the pipe instead.

Adding Color to Your Terminal

There are two coloring tools at work here, each for a different job.

IO.ANSI is Elixir’s built-in module for terminal control. We use it for screen operations:

IO.ANSI.clear()    # Clear the screen
IO.ANSI.home()     # Move cursor to top-left

For colored text inside Owl components like boxes, we use Owl.Data.tag instead:

Owl.Data.tag("HerbWars", :green)    # Green text
Owl.Data.tag("ENTER", :cyan)        # Cyan text

Why not use IO.ANSI for everything? Owl needs to know the visual width of your text to draw borders correctly. Raw ANSI escape codes are invisible characters that throw off the width calculation, pushing the right border out of alignment. Owl.Data.tag lets Owl handle the coloring itself, so it measures your text accurately.

Running Your Code

Time for the exciting part! Run your banner directly from the terminal:

mix run -e "HerbWars.Display.display_banner()"

This compiles your project and executes the function. You should see a beautiful bordered box with your welcome message, complete with colors!

Exploring with IEx

You can also use Elixir’s interactive shell to call functions and experiment:

iex -S mix

iex is Elixir’s REPL (Read-Eval-Print Loop). The -S mix flag tells it to load your project. Try calling our function from here:

iex> HerbWars.Display.display_banner()

Some handy IEx tricks:

Writing Your First Test

Testing in Elixir is first-class. Open test/herb_wars_test.exs. You’ll see a default test that Mix generated:

defmodule HerbWarsTest do
  use ExUnit.Case
  doctest HerbWars

  describe "hello/0" do
    test "returns :world atom" do
      assert HerbWars.hello() == :world
    end
  end
end

This test checks hello/0, but we already removed that function. If you run mix test now, it will fail. Let’s update the test file to match our new code. Since display_banner/0 is interactive (it waits for keyboard input), we can’t easily test it automatically yet. For now, let’s clean up the test file and verify everything compiles:

defmodule HerbWarsTest do
  use ExUnit.Case

  test "module exists" do
    assert Code.ensure_loaded?(HerbWars)
  end
end

The assert macro checks if something is true. We’ll write more meaningful tests starting in the next tutorial when we build testable, non-interactive functions. Run your tests:

mix test

You should see 1 test, 0 failures.

References

What’s Next?

In the next post, we’ll create our first data structures using structs and maps. You’ll learn how Elixir handles immutable data (spoiler: it’s not as scary as it sounds), and we’ll start building the Player module that will track money, inventory, and upgrades.

Until then, experiment! Try different Owl box styles (:double, :rounded), play with more Owl.Data.tag colors (:red, :yellow, :magenta), or add a display_goodbye/0 function to the Display module with its own styled message.