
lazyasdf is my first actual* try at making a TUI with Elixir!
asdf is generally used by way of a command line interface (CLI), lazyasdf
presents you with a terminal person interface (TUI) for working with asdf
.
I not too long ago fell in love with lazygit and have since dreamed of writing my very own TUI packages, however with Elixir.
The TUI supplies a fast and intuitive interface for these acquainted with the terminal and for individuals who choose a graphical utility, however the TUI is a lot extra approachable in my humble opinion in terms of making your individual 😄.
Whereas I discover lazyasdf
to be an incredible achievement for myself, it isn’t tremendous fascinating by itself. Let’s dive into the specifics of how I used to be in a position to construct and distribute a TUI utility with Elixir.
Ratatouille
None of this might be potential if it weren’t for the library ratatouille by Nick Reynolds.
I’m not some genius in terms of terminals or laying out textual content, this all comes from Ratatouille, which builds off of termbox, which is a [n]curses various.
Ratatouille leverages the Elm Structure of which many people have grown acquainted. Let’s check out a small Ratatouille program that showcases most of its options.

#!/usr/bin/env elixir
Combine.set up([:ratatouille])
defmodule Todos do
@behaviour Ratatouille.App
import Ratatouille.View
import Ratatouille.Constants, solely: [color: 1, key: 1]
@style_selected [
color: color(:black),
background: color(:white)
]
@area key(:area)
@impl true
def init(_) do
%{
todo: %{
objects: %{"purchase eggs" => false , "mow the garden" => true, "get a haircut" => false},
cursor_y: 0
}
}
finish
@impl true
def replace(mannequin, msg) do
case msg do
{:occasion, %{key: @area}} ->
{todo, _done} = Enum.at(mannequin.todo.objects, mannequin.todo.cursor_y)
update_in(mannequin.todo.objects[todo], & !&1)
{:occasion, %{ch: ?j}} ->
update_in(mannequin.todo.cursor_y, &cursor_down(&1, mannequin.todo.objects))
{:occasion, %{ch: ?okay}} ->
update_in(mannequin.todo.cursor_y, &cursor_up/1)
_ ->
mannequin
finish
finish
defp cursor_down(cursor, rows) do
min(cursor + 1, Enum.rely(rows) - 1)
finish
defp cursor_up(cursor) do
max(cursor - 1, 0)
finish
@impl true
def render(mannequin) do
view do
panel title: "TODO" do
for { {t, finished}, idx } <- Enum.with_index(mannequin.todo.objects) do
row do
column dimension: 12 do
label if(idx == mannequin.todo.cursor_y, do: @style_selected, else: []) do
textual content content material: "- ["
done(done)
text content: "] #{t}"
finish
finish
finish
finish
finish
finish
finish
defp finished(true), do: textual content(content material: "x")
defp finished(false), do: textual content(content material: " ")
finish
Ratatouille.run(Todos)
You must be capable of copy the above snippet right into a file, make it executable (chmod + x
) and run it!
Ratatouille requires 3 callbacks in your TUI program, init/1
, replace/2
, and render/1
.
-
When this system boots up, the
init/1
callback is known as and the return worth turns into your preliminary mannequin state. -
At any time when the TUI receives person enter, the
replace/2
callback is executed with the message and your present mannequin state. -
When that returns, the runtime will name the
render/1
callback with the brand new mannequin state.The
render/1
callback is filled with macros which translate to aspect structs, so it’s simply an ergonomic DSL. Typing out many structs by hand can be a PITA!
Notes
You’ve in all probability noticed that, whereas it’s excessive degree in comparison with uncooked termbox
, Ratatouille remains to be kind of “low degree” as an utility framework.
We nonetheless need to manually observe and transfer our cursor place, in addition to index into our information constructions to tug out the best information for that place.
Burrito
Now the conventional downside with Elixir apps is that it’s important to have Elixir and Erlang in your machine to run them, in addition to preserve observe of the model of them you could have put in to ensure they’re suitable, in addition to write aliases to run escripts and Combine duties, yada yada.
That is the place Burrito is available in!
Burrito makes use of Zig to bundle up your utility, the BEAM, and the Runtime all into one tidy executable that you may distribute at your leisure!
In the long run, as soon as we run MIX_ENV=prod combine launch
, Burrito will create binaries for every of our specified goal platforms, and you’ll simply copy these onto your pc and run them
The Burrito undertaking is lead by Digit.
Homebrew
To make any program helpful, it’s assist to have the ability to set up it simply.
Homebrew is the first means of engaging in this on MacOS (my most popular working system) and you’ll simply host your individual assortment of Homebrew packages with your individual Faucet!
Since lazyasdf
has some quirky dependencies, the formulation (what Homebrew calls a bundle definition) is a bit fascinating.
class Lazyasdf < Method
desc "TUI for the asdf model supervisor"
homepage "https://github.com/mhanberg/lazyasdf"
url "https://github.com/mhanberg/lazyasdf/archive/refs/tags/v0.1.1.tar.gz"
sha256 "787da19809ed714c569c8bd7df58d55d7389b69efdf1859e57f713d18e3d2d05"
license "MIT"
bottle do
root_url "https://github.com/mhanberg/homebrew-tap/releases/obtain/lazyasdf-0.1.1"
sha256 cellar: :any_skip_relocation, monterey: "f489e328c19954d62284a7154fbc8da4e7a1df61dc963930d291361a7b2ca751"
finish
depends_on "elixir" => :construct
depends_on "erlang" => :construct
depends_on "gcc" => :construct
depends_on "make" => :construct
depends_on "python@3.9" => :construct
depends_on "xz" => :construct
depends_on "asdf"
on_macos do
on_arm do
useful resource "zig" do
url "https://ziglang.org/obtain/0.10.0/zig-macos-aarch64-0.10.0.tar.xz"
sha256 "02f7a7839b6a1e127eeae22ea72c87603fb7298c58bc35822a951479d53c7557"
finish
finish
on_intel do
useful resource "zig" do
url "https://ziglang.org/obtain/0.10.0/zig-macos-x86_64-0.10.0.tar.xz"
sha256 "3a22cb6c4749884156a94ea9b60f3a28cf4e098a69f08c18fbca81c733ebfeda"
finish
finish
finish
def set up
zig_install_dir = buildpath/"zig"
mkdir zig_install_dir
assets.every do |r|
r.fetch
system "tar", "xvC", zig_install_dir, "-f", r.cached_download
zig_dir =
if {Hardware}::CPU.arm?
zig_install_dir/"zig-macos-aarch64-0.10.0"
else
zig_install_dir/"zig-macos-x86_64-0.10.0"
finish
ENV["PATH"] = "#{zig_dir}:" + ENV["PATH"]
finish
ENV["PATH"] = (Method["python@3.9"].opt_libexec/"bin:") + ENV["PATH"]
system "combine", "native.hex", "--force"
system "combine", "native.rebar", "--force"
ENV["BURRITO_TARGET"] = if {Hardware}::CPU.arm?
"macos_m1"
else
"macos"
finish
ENV["MIX_ENV"] = "prod"
system "combine", "deps.get"
system "combine", "launch"
if OS.mac?
if {Hardware}::CPU.arm?
bin.set up "burrito_out/lazyasdf_macos_m1" => "lazyasdf"
else
bin.set up "burrito_out/lazyasdf_macos" => "lazyasdf"
finish
finish
finish
take a look at do
# that is required for homebrew-core
system "true"
finish
finish
Right here we will see all of lazyasdf
’s dependencies.
It requires
- Elixir/Erlang: self-explanatory
- asdf: self-explanatory
- gcc,make: used to compile the termbox NIF bindings
- Python 3.9: The termbox NIF makes use of a python script. For some cause it really works with 3.9 and never 3.11, so I pinned it at 3.9 🤷♂️.
-
zig,xz: Burrito makes use of these two.
Burrito particularly makes use of Zig 0.10.0, not 0.10.1, so we’ve to specify it as a useful resource and obtain it from the Zig web site. Fortunately, they supply pre-compiled binaries for each our our goal platforms, so we will simply obtain, untar, and add them to our PATH!
The Python dependency is much more quirky. The termbox scripts use the unversioned python
executable, however Homebrew doesn’t hyperlink these by default, so we’ve to manually add the unversioned one to our PATH for it to work.
Voilà!
Notes
Since this can be a third social gathering Faucet, the bottles which can be generated are for an older model of Intel Mac, so these gained’t be very helpful to anyone.
But when I had been to merge this formulation into homebrew-core, they might be bottled utilizing the key Homebrew GitHub Actions runners that may bottle it for all of the OS’s and architectures.
The Future
Ratatouille is unimaginable as it’s at this time, however there’s a whole lot of room for enchancment.
As time permits, I hope to:
- Contribute to Ratatouille to permit extra advanced UI options like scrollbars and dynamic dimension info for parts.
- Create bindings for termbox2 (the following iteration of termbox).
- Create the next degree toolkit for constructing TUIs with Ratatouille, together with menus, inputs, dialogs, and many others.
Thanks for studying!
* Beforehand, I’ve made a fzf clone utilizing Ratatouille. You could find it in my dotfiles.