Raxol Revived ExTermbox

View Source

Hex.pm Hexdocs.pm

An Elixir library for interacting with the terminal via the termbox2 C library, a maintained fork of the original termbox.

Starting with version 2.0.0, this library uses Elixir Native Implemented Functions (NIFs) provided by the termbox2 Hex package (and its associated NIF bindings). This replaces the previous Port/Unix Domain Socket architecture, leveraging a maintained C library and simplifying the build process.

For high-level, declarative terminal UIs in Elixir, see raxol or its predecessor Ratatouille, which build on top of this library.

For the API Reference, see the ExTermbox module: https://hexdocs.pm/rrex_termbox/ExTermbox.html.

Getting Started

Architecture (NIF Based)

Note: If you previously used versions prior to 2.0.0, be aware that the underlying communication mechanism has changed significantly from a Port/UDS system back to NIFs, leveraging the termbox2 dependency. See the Changelog for details.

ExTermbox now interacts directly with the termbox2 C library through NIFs provided by the termbox2 Hex dependency.

  1. Initialization: ExTermbox.init/1 starts a GenServer (ExTermbox.Server) which calls the tb_init() NIF function. This server manages the termbox state and handles API calls.
  2. API Calls: Public functions in the ExTermbox module (e.g., ExTermbox.print/5, ExTermbox.clear/0, ExTermbox.present/0) communicate with the ExTermbox.Server via GenServer calls/casts. The server then invokes the corresponding termbox2 NIF function (e.g., tb_print, tb_clear, tb_present).
  3. Event Handling: The ExTermbox.Server periodically polls for terminal events (like key presses, mouse events, or resizes) using the tb_peek_event() NIF. When an event occurs, it's translated into an ExTermbox.Event struct and sent as a standard Elixir message ({:termbox_event, event}) to the process that originally called ExTermbox.init/1 (the "owner" process).

The public API is exposed primarily through the ExTermbox module.

Hello World

Let's go through a simple example.

Create an Elixir script (e.g., hello.exs) in any Mix project that includes rrex_termbox in its dependencies (see Installation below).

# hello.exs
defmodule HelloWorld do
  use GenServer
  alias ExTermbox

  def start_link(_opts) do
    # Start our process that will own the termbox session
    GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
  end

  @impl true
  def init(:ok) do
    # Initialize ExTermbox, registering this GenServer process as the owner
    # The owner process will receive {:termbox_event, event} messages
    case ExTermbox.init(self()) do
      :ok ->
        IO.puts("ExTermbox initialized successfully.")
        # Send ourselves a message to trigger drawing the initial screen
        send(self(), :draw)
        {:ok, %{}} # Initial state for our GenServer

      {:error, reason} ->
        IO.inspect(reason, label: "Error initializing ExTermbox")
        {:stop, :init_failed}
    end
  end

  @impl true
  def handle_info(:draw, state) do
    # Clear the screen
    :ok = ExTermbox.clear()

    # Print "Hello, World!" at (0, 0) with default colors
    :ok = ExTermbox.print(0, 0, :default, :default, "Hello, World!")

    # Print "(Press <q> to quit)" at (0, 2)
    :ok = ExTermbox.print(0, 2, :default, :default, "(Press <q> to quit)")

    # Render the changes to the terminal
    :ok = ExTermbox.present()
    {:noreply, state}
  end

  # Handle events sent from ExTermbox.Server
  @impl true
  def handle_info({:termbox_event, %ExTermbox.Event{type: :key, key: :q}}, state) do
    IO.puts("Quit event received.")
    # Trigger shutdown before stopping
    ExTermbox.shutdown()
    {:stop, :normal, state}
  end

  @impl true
  def handle_info({:termbox_event, event}, state) do
    # Optional: Log other events
    # IO.inspect(event, label: "Received event")
    {:noreply, state}
  end

  # Handle other messages if needed
  @impl true
  def handle_info(msg, state) do
    # IO.inspect(msg, label: "Received other message")
    {:noreply, state}
  end

  @impl true
  def terminate(reason, _state) do
    IO.puts("HelloWorld GenServer terminating: #{inspect(reason)}")
    # Ensure termbox is shut down if termination wasn't triggered by :q
    # (This might be redundant if ExTermbox.Server links/monitors)
    ExTermbox.shutdown()
    :ok
  end

  # Helper to run the example
  def run do
    # Ensure the app is started if running as a script
    {:ok, _} = Application.ensure_all_started(:rrex_termbox)

    {:ok, pid} = start_link([])
    # Keep the script alive until the GenServer terminates
    Process.monitor(pid)
    receive do
      {:DOWN, _, :process, ^pid, reason} ->
        IO.puts("HelloWorld process finished: #{inspect(reason)}")
    end
  end
end

HelloWorld.run()

In this example, we use a GenServer to manage the application's lifecycle and handle the asynchronous {:termbox_event, ...} messages.

Finally, run the example like this (assuming you have rrex_termbox added to a Mix project):

mix run hello.exs

You should see the text we rendered and be able to quit with 'q'.

Installation

Add rrex_termbox as a dependency in your project's mix.exs.

Important: This library currently relies on a fork of the termbox2 NIF wrapper to include necessary fixes and features. Point your dependency directly to the GitHub repository:

def deps do
  [
    # {:rrex_termbox, "~> 2.0.0"}, # Use this once published to Hex
    {:rrex_termbox, git: "https://github.com/Hydepwns/rrex_termbox.git", tag: "v2.0.0-alpha.2"} # Or branch: "main"

    # The underlying NIF library (currently points to a fork)
    # rrex_termbox depends on this, so it's usually fetched automatically,
    # but explicitly listing it might be needed for overrides.
    # {:termbox2, github: "Hydepwns/termbox2-nif", tag: "0.1.1-hydepwns-fix1", submodules: true}
  ]
end

(Note: Once rrex_termbox v2.0.0 (or later) is published on Hex.pm and the underlying termbox2 dependency issues are resolved upstream or the fork is published, the dependency specification can likely be simplified back to {:rrex_termbox, "~> 2.0.0"}.)

You will need standard C build tools (like gcc or clang, often part of build-essential or Xcode Command Line Tools) installed on your system for the termbox2 NIF dependency to compile.

Mix should handle fetching the dependency and compiling the NIFs automatically when you run mix deps.get and mix compile.

If you encounter build issues, ensure your build tools are installed and check the termbox2 dependency's documentation or repository for any specific requirements.

Distribution

Building standalone releases for applications using rrex_termbox (and its underlying NIF dependency) should work with standard Elixir Releases. The build process compiles the NIFs into a shared object file (.so or .dylib) located in the priv/ directory of the dependency (termbox2). Releases are designed to package these priv/ artifacts correctly.

Ensure your release configuration properly includes the rrex_termbox and termbox2 applications. Consult the Elixir Releases documentation for details.