Skip to content

Porting from termbox2

rizukirr edited this page Jun 13, 2026 · 1 revision

Porting from termbox2

libterm is termbox2 with every symbol renamed (tb_lt_, TB_LT_). If you have existing termbox2 code, you have two ways to move it onto libterm:

  1. The compat header — change one #include and keep your tb_/TB_ code. Fastest path; covered below.
  2. A full rename — switch to the lt_/LT_ names directly. Best long-term; the rest of this wiki documents that API.

This page covers the compat header and the one behavior difference you'll hit in practice: Ctrl+letter keys.

The drop-in compat header

compat/termbox2.h (in the libterm repo) maps the supported termbox2 API onto libterm. It's a single self-contained header — put it on your include path and replace your include:

/* was: #include "termbox2.h" */
#include "termbox2.h"   /* now resolved to libterm's compat/termbox2.h */
cc myapp.c -I path/to/libterm/compat -lterm -o myapp

It includes <libterm/libterm.h>, so it works against the normally-installed library. Your existing calls — tb_init, tb_present, tb_poll_event, tb_set_cell, TB_KEY_ENTER, struct tb_event, uintattr_t, … — compile and link unchanged.

What's supported

  • Every termbox2 function that has a libterm equivalent — lifecycle, rendering, the print/send helpers, input, output modes, the UTF-8 helpers, introspection.
  • All TB_KEY_* and every TB_* constant — keys, modifiers, events, output/input modes, colors, attributes, return codes.
  • struct tb_cell, struct tb_event, uintattr_t — aliased to libterm's types. (tb_cell is libterm's fixed 16-byte cell; the per-cell ech/nech/cech grapheme pointers from termbox2 are not present — use tb_set_cell_ex / tb_extend_cell for grapheme clusters.)

What's adapted

  • tb_get_cell — libterm copies a cell into a struct rather than handing back a live buffer pointer. The compat wrapper returns a pointer to an internal snapshot, valid until your next tb_get_cell call, and reads the back buffer only (back == 0, the front buffer, returns LT_ERR).

What's unsupported

These have no libterm equivalent; using one is a compile error whose message names the replacement:

termbox2 use instead
tb_init_rwfd lt_init_fd / lt_init_file (libterm is single-fd by design)
tb_has_truecolor lt_detect_color_depth
tb_cell_buffer tb_get_cell (no raw back-buffer pointer)
tb_set_func — (a custom parser hook; deprecated upstream)
tb_key_i — (no terminfo capability table)

The one gotcha: Ctrl+letter keys

This is the difference that trips up almost every port. Ctrl-S, Ctrl-Q, Ctrl-A … may appear to "do nothing" after you switch to the compat header.

Why. libterm defaults to a modern keyboard model. In it, Ctrl+letter is reported as the letter in ev.ch plus the LT_MOD_CTRL modifier, with ev.key == 0:

press Ctrl-S  →  ev.key = 0,  ev.ch = 's',  ev.mod = LT_MOD_CTRL

termbox2 (and most ported code) instead checks the legacy control byte in ev.key:

if (ev.key == TB_KEY_CTRL_S) save();   /* never true under the modern model */

TB_KEY_CTRL_S is 0x13, but ev.key is 0 — so the check never fires, and the 's' falls through to your text-insert path. (This is not terminal flow control: libterm's raw mode disables IXON, so Ctrl-S/Ctrl-Q do reach your program.)

You can fix this two ways.

Fix A — keep termbox2 semantics (one line)

Ask libterm for termbox2's control-byte model right after init:

tb_init();
tb_set_input_mode(LT_INPUT_COMPAT);   /* Ctrl+letter -> ev.key = the control byte */

Now ev.key == TB_KEY_CTRL_S works exactly as it did under termbox2, and the rest of your tb_/TB_ code is untouched. This is the fastest, most faithful port — pick it if you just want your program running.

LT_INPUT_COMPAT also restores termbox2's other legacy behaviors: a lone ESC and an Alt-combo as two events (tunable with LT_INPUT_ESC / LT_INPUT_ALT), Shift+letter as a bare uppercase ch, and kitty negotiation off.

Fix B — adopt the modern model (recommended for code you'll keep evolving)

Leave libterm in its default (don't set LT_INPUT_COMPAT) and update your Ctrl+letter checks to read ch + mod instead of key:

struct tb_event ev;
tb_poll_event(&ev);

if (ev.type == TB_EVENT_KEY) {
  if ((ev.mod & TB_MOD_CTRL) && ev.ch == 's') { save(); }
  else if ((ev.mod & TB_MOD_CTRL) && ev.ch == 'q') { quit(); }
  /* arrows, Enter, Backspace, Esc are UNCHANGED — still named keys in ev.key */
  else if (ev.key == TB_KEY_ARROW_UP) { /* ... */ }
  else if (ev.key == TB_KEY_ENTER)    { /* ... */ }
  else if (ev.ch != 0)                { insert((char)ev.ch); }
}

(TB_MOD_CTRL is the same value as LT_MOD_CTRL; Ctrl+letter always reports the lowercase letter in the modern model.)

Only Ctrl+letter and Shift+letter handling changes — named keys (arrows, F-keys, Enter, Backspace, Esc, nav) arrive in ev.key in both models, so those branches stay as they are.

Why prefer the modern model? On a kitty-capable terminal (negotiated automatically) or on Windows it is strictly more capable:

  • Ctrl-I stays distinct from Tab, Ctrl-M from Enter, Ctrl-H from Backspace — the legacy byte model collapses these.
  • Modifiers arrive on every key, including Shift+letter; the typed character is layout-translated into ch.
  • Bare modifier presses, key-release, and key-repeat become available (ev.action; releases with LT_INPUT_RELEASE).

The trade-off: on a legacy POSIX terminal, Shift+letter / bare modifiers / releases simply aren't on the wire, so they're unavailable there regardless — a terminal limitation libterm can't paper over. See Input & Events for the full model.

Which should I pick?

Fix A — LT_INPUT_COMPAT Fix B — modern model
Change to your code one line rewrite Ctrl+letter checks
Keeps ev.key == TB_KEY_CTRL_* yes no (use ev.ch + ev.mod)
Ctrl-I vs Tab disambiguation no yes (capable terminals)
Modifiers on all keys, release/repeat no yes (capable terminals)
Best for a quick, faithful drop-in code you'll keep growing

A worked example

examples/editor.c in the repo is a small text editor written entirely against the compat header (a termbox2-idiom program). It uses Fix A — a single tb_set_input_mode(LT_INPUT_COMPAT) after tb_init — so its Ctrl-S (save) and Ctrl-Q (quit) bindings work. It's the most complete reference for what a real drop-in looks like.

See also

  • Input & Events — the event struct, the modern vs. compat models in full, modifiers, Windows specifics.
  • API Reference — the lt_ API the compat names map onto.
  • Roadmap — per-symbol termbox2 parity status.

Clone this wiki locally