Raxol Revived ExTermbox

View Source

Hex.pm Hexdocs.pm

An Elixir library for interacting with the terminal via the termbox C library.

Starting with version 1.1.0, this library no longer uses Elixir Native Implemented Functions (NIFs). Instead, it manages a separate C helper process (termbox_port) and communicates with it using an Elixir Port for initialization and Unix Domain Sockets (UDS) for runtime commands and events. This approach provides enhanced stability and avoids potential NIF-related pitfalls.

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 (Port/UDS Based)

Note: If you previously used versions prior to 1.1.0, be aware that the underlying communication mechanism has changed significantly from NIFs to a Port/UDS system. The ExTermbox.Bindings module and NIF-based event polling have been removed. See the Changelog for details.

ExTermbox starts and manages a C helper program (termbox_port). Communication happens as follows:

  1. Initialization: An Elixir Port is used briefly to exchange the path for a Unix Domain Socket (UDS).
  2. Runtime: All subsequent commands (like printing, setting cursor, changing cells) and events (like key presses, resizes) are sent over the UDS connection using a simple text-based protocol.

The public API is exposed primarily through the ExTermbox module.

Hello World

Let's go through a simple example. To follow along, clone this repo and save the code below as an .exs file (e.g., hello.exs).

This repository makes use of Git submodules, so make sure you include them in your clone. In recent versions of git, this can be accomplished by including the --recursive flag, e.g.

# Make sure to clone *this* repository recursively to include submodules
git clone --recursive https://github.com/Hydepwns/rrex_termbox.git

When the clone is complete, the c_src/termbox/ directory should have files in it.

You can also create an Elixir script in any Mix project with rrex_termbox in the dependencies list. Later, we'll run the example with mix run <file>.

# hello.exs
defmodule HelloWorld do
  alias ExTermbox

  def run do
    # Start ExTermbox, registering the current process to receive events
    case ExTermbox.init(self()) do
      :ok ->
        IO.puts("ExTermbox initialized successfully.")
        # 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()

        # Wait for the 'q' key event
        wait_for_quit()

        # Shut down ExTermbox
        :ok = ExTermbox.shutdown()
        IO.puts("ExTermbox shut down.")

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

  defp wait_for_quit do
    receive do
      # Events are sent as messages to the registered process
      {:termbox_event, %{type: :key, key: :q}} ->
        :quit
      {:termbox_event, event} ->
        # IO.inspect(event, label: "Received event") # Uncomment to see other events
        wait_for_quit() # Wait for the next event
      _other_message ->
        # IO.inspect(other_message, label: "Received other message")
        wait_for_quit() # Wait for the next event
    after
      10_000 -> IO.puts("Timeout waiting for 'q' key.") # Add a timeout for safety
    end
  end
end

HelloWorld.run()

In a real application, you'll likely want to integrate ExTermbox into an OTP application with a proper supervisor.

The ExTermbox.print/5 function provides a simple way to display strings. For more control over individual cells (characters, foreground/background colors, attributes), use ExTermbox.change_cell/5. The ExTermbox.width/0 and ExTermbox.height/0 functions can be used to get the terminal dimensions.

Finally, run the example like this:

mix run hello.exs

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

Python Build Compatibility (Python 3.12+)

The version of the termbox C library bundled with :rrex_termbox uses an older version of the waf build system. This version of waf contained code (import imp) that is incompatible with Python 3.12 and newer, causing the C helper compilation to fail if a modern Python version is your system default.

This fork includes a small patch directly within the bundled waf scripts (c_src/termbox/.waf3-2.0.14-e67604cd8962dbdaf7c93e0d7470ef5b/waflib/Context.py) to replace the incompatible code with its modern equivalent (importlib).

With this patch, :rrex_termbox should compile successfully using Python 3.12+ without requiring manual intervention or downgrading Python.

If you encounter build issues related to Python or waf, please ensure you are using a version of this library that includes this fix.

Installation

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

def deps do
  [
    {:rrex_termbox, "~> 1.1.0"}
  ]
end

The Hex package bundles a compatible version of termbox. 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.

Mix compile hooks automatically build the termbox_port C helper executable needed by the library. This should happen the first time you build :rrex_termbox (e.g., via mix deps.get followed by mix deps.compile or simply mix compile).

The build has been tested on macOS and some Linux distributions. Please open an issue if you encounter build problems.

Using the Source Directly

To try out the master branch, first clone the repo:

# Make sure to clone *this* repository recursively to include submodules
git clone --recurse-submodules <your-fork-url>
cd rrex_termbox # Assuming the directory name matches the repo

The --recurse-submodules flag (--recursive before Git 2.13) is necessary in order to additionally clone the termbox source code, which is required to build the C helper program.

Next, fetch the deps:

mix deps.get

Finally, try out the included event viewer application:

mix run examples/event_viewer.exs

If you see the application drawn and can trigger events, you're good to go. Use 'q' to quit the examples.

Distribution

Building a standalone executable for applications using :rrex_termbox requires some special consideration due to the included C helper program (termbox_port) which needs to be packaged alongside your Elixir application.

Standard Elixir tools like escript are not suitable because they do not correctly package the necessary helper executable located in the priv/ directory after compilation.

The recommended approach for creating distributable releases is to use Distillery or the built-in Elixir Releases. These tools are designed to handle external programs and assets correctly. They will package your application, the Erlang Runtime System (ERTS), and the compiled termbox_port helper into a self-contained bundle.

Consult the documentation for Distillery or Elixir Releases for specific instructions on configuring your project for release builds, ensuring the priv/termbox_port executable is included in the release package.