Raxol Revived ExTermbox
View SourceAn 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.
- Initialization:
ExTermbox.init/1
starts aGenServer
(ExTermbox.Server
) which calls thetb_init()
NIF function. This server manages the termbox state and handles API calls. - API Calls: Public functions in the
ExTermbox
module (e.g.,ExTermbox.print/5
,ExTermbox.clear/0
,ExTermbox.present/0
) communicate with theExTermbox.Server
viaGenServer
calls/casts. The server then invokes the correspondingtermbox2
NIF function (e.g.,tb_print
,tb_clear
,tb_present
). - Event Handling: The
ExTermbox.Server
periodically polls for terminal events (like key presses, mouse events, or resizes) using thetb_peek_event()
NIF. When an event occurs, it's translated into anExTermbox.Event
struct and sent as a standard Elixir message ({:termbox_event, event}
) to the process that originally calledExTermbox.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.