Skip to content

♻️ Make termbox2 muliplatform with wasm support.#129

Draft
cowboyd wants to merge 2 commits intotermbox:masterfrom
cowboyd:multiplatform-with-wasm
Draft

♻️ Make termbox2 muliplatform with wasm support.#129
cowboyd wants to merge 2 commits intotermbox:masterfrom
cowboyd:multiplatform-with-wasm

Conversation

@cowboyd
Copy link
Copy Markdown

@cowboyd cowboyd commented Feb 18, 2026

Motivation

This is a proof of concept to resolve #122.

WASM and Windows do not have termios support and will require much different ways of interacting with the terminal.

This extracts a "platform" API that termbox can use so that all platform specific details can be hidden behind a swappable facade.

The tricky part is that we need to preserve these key properties of termbox2:

  1. termbox2.h is a single header file that can be included into any project as a unit. (no platform specific includes)
  2. the individual source .h files and .c files can be worked upon using standard IDE tools and language servers to check syntax, navigate, and resolve symbols.

Ultimately, the consumer experience must be unchanged:

#define TB_IMPL + #include <termbox2.h> in one translation unit. The assembled header is functionally identical to the original — same API, same behavior, same single-file usage.

Approach

Note: this is my best guess. I was not exactly sure what the "thinnest" possible platform API should be, but the main thing here is to provide a strategy for building termbox2.h from different platform component parts, not necessarily that those parts are optimally specified.

To accomplish this we re-organize the way that the header file is built. termbox2.h is now created entirely from a template called termbox2.h.in

New source file layout:

  termbox2.h.in   — assembly template with BEGIN/END markers
  tb_api.h        — public API + implementation internals (#ifdef TB_IMPL)
  tb_platform.h   — platform interface declarations. Each platform
                    must implement these functions
  termbox2.c      — core logic
  tb_posix.c      — POSIX backend (termios, pipes, signals, terminfo)
  tb_wasm.c       — WASM backend (delegates to __tb_host_* imports)
  assemble.awk    — assembles termbox2.h from the above

Every platform now as a chance at the very beginning to allocate a data structure where all its state will be stored. This state is treated as fully opaque by termbox and is stored at global.platform. It is passed as the first argument to each platform call. For example, in tb_posix.c the struct looks like:

struct tb_posix {
    int ttyfd;
    int rfd;
    int wfd;
    int ttyfd_open;
    int resize_pipefd[2];
    struct termios orig_tios;
    int has_orig_tios;
    char *terminfo;
    size_t nterminfo;
};

The .c files include headers directly and compile standalone. This is so that LSP/editor support (clangd, jump-to-definition, diagnostics) works out of the box, but local include directives are stripped during assembly.

WASM Support

In order to make wasm actually work, the embedder will still need to provide four more functions:

int __tb_host_write(const char *buf, int len);
int __tb_host_read(char *buf, int len);
int __tb_host_wait(int timeout_ms); /* returns bitmask: 1=input, 2=resize */
void __tb_host_get_size(int *w, int *h);

These have to be provided by the JavaScript runtime and will be different depending on whether this is in the browser, or this is in Node, or Deno.

DX

  • make termbox2.h builds termbox2.h from all its component parts.
  • make terminfo regenerates terminfo tables directly in tb_api.h, replacing content between codegen markers.

WASM and Windows do not have termios support and will require much
different ways of interacting with the terminal.

This extracts a "platform " API that termbox can use so that all
platform specific details can be hidden behind a swappable facade.

The tricky part is that we need to preserve these key properties.

1. `termbox2.h` is a single header file that can be included into any
project as a unit. (no platform specific includes)
2. the individual source `.h` files and `.c` files can be worked upon
using standard IDE tools and language servers to check syntax,
navigate, and resolev symbols.

Ultimately, the consumer experience must be unchanged:

`#define TB_IMPL` + `#include <termbox2.h>` in one translation unit.
The assembled header is functionally identical to the original — same
API, same behavior, same single-file usage.

To accomplish this we re-organize the way that the header file is
built. `termbox2.h` is now created entirely from a template called `termbox2.h.in`

New source file layout:

  termbox2.h.in   — assembly template with BEGIN/END markers
  tb_api.h        — public API + implementation internals (#ifdef TB_IMPL)
  tb_platform.h   — platform interface declarations. Each platform
                    must implement these functions
  termbox2.c      — core logic
  tb_posix.c      — POSIX backend (termios, pipes, signals, terminfo)
  tb_wasm.c       — WASM backend (delegates to __tb_host_* imports)
  assemble.awk    — assembles termbox2.h from the above

Every platform now as a chance at the very beginning to allocate a
data structure where all its state will be stored. This state is treated as fully opaque by
termbox and is stored at `global.platform`. It is passed as the first
argument to each platform call. For example, in `tb_posix.c` the
struct looks like:

```c
struct tb_posix {
    int ttyfd;
    int rfd;
    int wfd;
    int ttyfd_open;
    int resize_pipefd[2];
    struct termios orig_tios;
    int has_orig_tios;
    char *terminfo;
    size_t nterminfo;
};
```

The .c files include headers directly and compile standalone. This is
so that LSP/editor support (clangd, jump-to-definition, diagnostics)
works out of the box, but local include directives are stripped during assembly.

`make termbox2.h` reassembles the single-header deliverable. The awk script
inlines headers (stripping include guards), injects .c contents (stripping
preambles), and wraps everything in BEGIN/END markers for traceability.

`make terminfo` regenerates terminfo tables directly in tb_api.h, replacing
content between codegen markers.
@adsr
Copy link
Copy Markdown
Contributor

adsr commented Feb 25, 2026

Ack-ing your PR. Haven't had a chance to review yet.

@cowboyd
Copy link
Copy Markdown
Author

cowboyd commented Mar 2, 2026

Here is a proof of concept running the keyboard demo in NodeJS

  • no native extensions,
  • no FFI,
  • just a single typescript file

2026-03-02 11 55 48

@cowboyd
Copy link
Copy Markdown
Author

cowboyd commented Mar 2, 2026

Also, was able to get this working in the browser using xterm.js

2026-03-02 12 54 01

@adsr
Copy link
Copy Markdown
Contributor

adsr commented Mar 20, 2026

Cool to see it running in a browser. I still haven't had a chance to review this but haven't forgotten.

Last I recall, I had played around with some WASM technologies you mentioned a few months ago, and I was walking through the various approaches people brought up in #123. All of that's been flushed out of memory for me so I'll be coming at this with fresh eyes when I get a chance.

The .c files include headers directly and compile standalone. This is so that LSP/editor support (clangd, jump-to-definition, diagnostics) works out of the box, but local include directives are stripped during assembly.

Right now this sounds pretty good to me.

@adsr
Copy link
Copy Markdown
Contributor

adsr commented Mar 20, 2026

Tagging @txgk @sewbacca if you'd like to review. You both had opinions in #123.

@txgk
Copy link
Copy Markdown
Contributor

txgk commented Mar 20, 2026

Tagging @txgk @sewbacca if you'd like to review. You both had opinions in #123.

I pretty much stay by my words in #123 (comment) i.e. I want to see only one file changed in WASM support patch - termbox2.h

It's great to see approach taken in this PR in a working state. The proof of concept is undoubtedly successful, altough I haven't tested it myself and I'd nack the architecture which have formed here in this patch, because it's a mess to work with compared to how termbox/termbox2 is used to be used in general.

@rofl0r
Copy link
Copy Markdown

rofl0r commented Mar 20, 2026

apart from the usability issues of the multi-step build process, the sheer size of the changes (especially additions) also raises eyebrows. the existing code is 4KLOC, after this PR it's more than double.

@cowboyd
Copy link
Copy Markdown
Author

cowboyd commented Mar 25, 2026

@rofl0r It's worth noting that the actual termbox2.h file https://github.com/termbox/termbox2/pull/129/changes#diff-4cc2fac8c0b525fa9fe2f1795c919f9a7f42e6e090f4de0e6c345afeedd25cd4 is 1200L vs 1000L before, so the delta is only about +200

Which is because there is an ifdef where both the wasm, and the posix are included, so in terms of your compiled binary it's pretty much the same.

But this is definitely a heavier setup for development. I'm not happy about it, but I'm not sure what the better way would be.

From the user's perspective, the experience is still just #include "termbox2.h"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

WASM support

4 participants