Sunday, May 19, 2024
HomeGolangIntroducing lazyasdf: An Elixir-based TUI for the asdf model supervisor

Introducing lazyasdf: An Elixir-based TUI for the asdf model supervisor


lazyasdf
lazyasdf

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.

Tuido
Tuido
#!/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.



RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments