From 2e31cbeb70d93bb0f5512064f181990db189d1f6 Mon Sep 17 00:00:00 2001 From: Sebastiano Tronto Date: Sun, 4 Oct 2026 20:02:15 +0200 Subject: Initial commit --- termbox2/termbox2.ha | 605 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 605 insertions(+) create mode 100644 termbox2/termbox2.ha (limited to 'termbox2/termbox2.ha') diff --git a/termbox2/termbox2.ha b/termbox2/termbox2.ha new file mode 100644 index 0000000..f8476ee --- /dev/null +++ b/termbox2/termbox2.ha @@ -0,0 +1,605 @@ +use encoding::utf8; +use fmt; +use types::c; + +// Type for key values, see [[termbox2::KEY]]. +export type key = u16; + +// ASCII constants, see [[termbox2::key_event]].key. +export def KEY = struct { + CTRL_TILDE: key = 0x00, + CTRL_2: key = 0x00, // clash with CTRL_TILDE + CTRL_A: key = 0x01, + CTRL_B: key = 0x02, + CTRL_C: key = 0x03, + CTRL_D: key = 0x04, + CTRL_E: key = 0x05, + CTRL_F: key = 0x06, + CTRL_G: key = 0x07, + BACKSPACE: key = 0x08, + CTRL_H: key = 0x08, // clash with CTRL_BACKSPACE + TAB: key = 0x09, + CTRL_I: key = 0x09, // clash with TAB + CTRL_J: key = 0x0a, + CTRL_K: key = 0x0b, + CTRL_L: key = 0x0c, + ENTER: key = 0x0d, + CTRL_M: key = 0x0d, // clash with ENTER + CTRL_N: key = 0x0e, + CTRL_O: key = 0x0f, + CTRL_P: key = 0x10, + CTRL_Q: key = 0x11, + CTRL_R: key = 0x12, + CTRL_S: key = 0x13, + CTRL_T: key = 0x14, + CTRL_U: key = 0x15, + CTRL_V: key = 0x16, + CTRL_W: key = 0x17, + CTRL_X: key = 0x18, + CTRL_Y: key = 0x19, + CTRL_Z: key = 0x1a, + ESC: key = 0x1b, + CTRL_LSQ_BRACKET: key = 0x1b, // clash with 'ESC' + CTRL_3: key = 0x1b, // clash with 'ESC' + CTRL_4: key = 0x1c, + CTRL_BACKSLASH: key = 0x1c, // clash with 'CTRL_4' + CTRL_5: key = 0x1d, + CTRL_RSQ_BRACKET: key = 0x1d, // clash with 'CTRL_5' + CTRL_6: key = 0x1e, + CTRL_7: key = 0x1f, + CTRL_SLASH: key = 0x1f, // clash with 'CTRL_7' + CTRL_UNDERSCORE: key = 0x1f, // clash with 'CTRL_7' + SPACE: key = 0x20, + BACKSPACE2: key = 0x7f, + CTRL_8: key = 0x7f, // clash with 'BACKSPACE2' + F1: key = (0xffff - 0), + F2: key = (0xffff - 1), + F3: key = (0xffff - 2), + F4: key = (0xffff - 3), + F5: key = (0xffff - 4), + F6: key = (0xffff - 5), + F7: key = (0xffff - 6), + F8: key = (0xffff - 7), + F9: key = (0xffff - 8), + F10: key = (0xffff - 9), + F11: key = (0xffff - 10), + F12: key = (0xffff - 11), + INSERT: key = (0xffff - 12), + DELETE: key = (0xffff - 13), + HOME: key = (0xffff - 14), + END: key = (0xffff - 15), + PGUP: key = (0xffff - 16), + PGDN: key = (0xffff - 17), + ARROW_UP: key = (0xffff - 18), + ARROW_DOWN: key = (0xffff - 19), + ARROW_LEFT: key = (0xffff - 20), + ARROW_RIGHT: key = (0xffff - 21), + BACK_TAB: key = (0xffff - 22), + MOUSE_LEFT: key = (0xffff - 23), + MOUSE_RIGHT: key = (0xffff - 24), + MOUSE_MIDDLE: key = (0xffff - 25), + MOUSE_RELEASE: key = (0xffff - 26), + MOUSE_WHEEL_UP: key = (0xffff - 27), + MOUSE_WHEEL_DOWN: key = (0xffff - 28), +}; + +// Type for key modifiers, see [[termbox2::MOD]]. +export type mod = u8; + +// Key modifiers (bitwise), see [[termbox2::event]].mod. +export def MOD = struct { + ALT: mod = 1, + CTRL: mod = 2, + SHIFT: mod = 4, + MOTION: mod = 8, +}; + +// Type for input modes, see [[termbox2::INPUT]]. +export type input_mode = int; + +// Input modes (bitwise), see [[termbox2::set_input_mode]]. +export def INPUT = struct { + CURRENT: input_mode = 0, + ESC: input_mode = 1, + ALT: input_mode = 2, + MOUSE: input_mode = 4, +}; + +// Type for output modes, see [[termbox2::OUTPUT]]. +export type output_mode = int; + +// Output modes, see [[termbox2::set_output_mode]]. +export def OUTPUT = struct { + CURRENT: output_mode = 0, + NORMAL: output_mode = 1, + _256: output_mode = 2, + _216: output_mode = 3, + GRAYSCALE: output_mode = 4, + TRUECOLOR: output_mode = 5, // only available if OPT_ATTR_W >= 32 +}; + +// Type for error return values, see [[termbox2::ERR]]. +export type err = !int; + +// Common error return values. +// +// Library behavior is undefined after receiving ERR.MEM. +// Callers may attempt reinitializing by freeing memory, invoking +// [[termbox2::shutdown]], then [[termbox2::init]]. +export def ERR = struct { + ERR: err = -1, + NEED_MORE: err = -2, + INIT_ALREADY: err = -3, + INIT_OPEN: err = -4, + MEM: err = -5, + NO_EVENT: err = -6, + NO_TERM: err = -7, + NOT_INIT: err = -8, + OUT_OF_BOUNDS: err = -9, + READ: err = -10, + RESIZE_IOCTL: err = -11, + RESIZE_PIPE: err = -12, + RESIZE_SIGACTION: err = -13, + POLL: err = -14, + SELECT: err = -14, + TCGETATTR: err = -15, + TCSETATTR: err = -16, + UNSUPPORTED_TERM: err = -17, + RESIZE_WRITE: err = -18, + RESIZE_POLL: err = -19, + RESIZE_SELECT: err = -19, + RESIZE_READ: err = -20, + RESIZE_SSCANF: err = -21, + CAP_COLLISION: err = -22, + HARE_BUILD: err = -9990, // Additional error type + HARE_INVALID_UTF8: err = -9991, // Additional error type + HARE_OTHER: err = -9992, // Additional error type +}; + +// An incoming key press event. Note that there is overlap between +// MOD.CTRL and KEY.CTRL_* keys. MOD.CTRL and MOD.SHIFT are only set +// as modifiers to KEY.ARROW_*. +export type key_event = struct { + mod: mod, // Modifier + key_or_ch: (key | rune), // A [[termbox2::KEY]] or a Unicode code point +}; + +// An incoming resize event. +export type resize_event = struct { + w: i32, // resize width + h: i32, // resize height +}; + +// An incoming mouse event. +export type mouse_event = struct { + key: key, // Mouse button, e.g. termbox2::KEY.MOUSE_LEFT + x: i32, + y: i32, +}; + +// The C data structure for an event, for compatibility. +type tb_event = struct { + t: u8, + mod: u8, + key: u16, + ch: u32, + w: i32, + h: i32, + x: i32, + y: i32, +}; + +def TB_EVENT_KEY: u8 = 1; +def TB_EVENT_RESIZE: u8 = 2; +def TB_EVENT_MOUSE: u8 = 3; +fn tb_event_to_event(e: tb_event) (event | err) = switch (e.t) { + case TB_EVENT_KEY => yield key_event { + mod = e.mod, + key_or_ch = if (e.key != 0) e.key: key else e.ch: rune, + }; + case TB_EVENT_RESIZE => yield resize_event { + w = e.w, + h = e.h, + }; + case TB_EVENT_MOUSE => yield mouse_event { + key = e.key, + x = e.x, + y = e.y, + }; + case => yield ERR.HARE_OTHER; + }; + +// Any incoming event from the tty. +export type event = (key_event | resize_event | mouse_event); + +fn build_opts_match() bool = + (has_egc() == OPT_EGC) && + (attr_width() == OPT_ATTR_W) && + (has_truecolor() == (OPT_ATTR_W == 32 || OPT_ATTR_W == 64)); + +fn void_or_err(r: int) (void | err) = if (r < 0) r: err; +fn int_or_err(r: int) (int | err) = if (r < 0) r: err else r; +fn size_or_err(r: int) (size | err) = if (r < 0) r: err else r: size; +fn str_or_err(cstr: *c::char) (str | err) = match(c::tostr(cstr)) { + case utf8::invalid => yield ERR.HARE_INVALID_UTF8; + case let s: str => yield s; +}; + +// Initialize the termbox library. This function should be called before +// any other functions. After successful initialization, the library must +// be finalized using [[termbox2::shutdown]]. +export fn init() (void | err) = + if (!build_opts_match()) + ERR.HARE_BUILD + else + void_or_err(tb_init()); +@symbol("tb_init") fn tb_init() int; + +// Same as [[termbox2::init]], but allows to specify a file path in place +// of the default /dev/tty. +export fn init_file(path: str) (void | err) = { + let c_path = c::fromstr(path)!; + defer free(c_path); + return if (!build_opts_match()) + ERR.HARE_BUILD + else + void_or_err(tb_init_file(c_path)); +}; +@symbol("tb_init_file") fn tb_init_file(cstr: *c::char) int; + +// Same as [[termbox2::init]], but allows to specify an output +// file descriptor. +export fn init_fd(ttyfd: int) (void | err) = + if (!build_opts_match()) + ERR.HARE_BUILD + else + void_or_err(tb_init_fd(ttyfd)); +@symbol("tb_init_fd") fn tb_init_fd(fd: int) int; + +// Same as [[termbox2::init]], but allows to specify input and output +// file descriptors. +export fn init_rwfd(rfd: int, wfd: int) (void | err) = + if (!build_opts_match()) + ERR.HARE_BUILD + else + void_or_err(tb_init_rwfd(rfd, wfd)); +@symbol("tb_init_rwfd") fn tb_init_rwfd(rfd: int, wfd: int) int; + +// Finalizes the library. +export fn shutdown() (void | err) = void_or_err(tb_shutdown()); +@symbol("tb_shutdown") fn tb_shutdown() int; + +// Return the width of the internal back buffer (which is the same as the +// terminal's window width in columns). The internal buffer can be resized +// after [[termbox2::clear]] or [[termbox2::present]] calls. Returns ERR.ERR +// when called before [[termbox2::init]] or after [[termbox2::shutdown]]. +export fn width() (size | err) = size_or_err(tb_width()); +@symbol("tb_width") fn tb_width() int; + +// Return the height of the internal back buffer (which is the same as the +// terminal's window width in rows). The internal buffer can be resized +// after [[termbox2::clear]] or [[termbox2::present]] calls. Returns ERR.ERR +// when called before [[termbox2::init]] or after [[termbox2::shutdown]]. +export fn height() (size | err) = size_or_err(tb_height()); +@symbol("tb_height") fn tb_height() int; + +// Clear the internal back buffer using [[termbox2::ATTRIBUTE]].DEFAULT. +export fn clear() (void | err) = void_or_err(tb_clear()); +@symbol("tb_clear") fn tb_clear() int; + +// Clear the internal back buffer using the specified attributes. +export fn set_clear_attrs(fg: uattr, bg: uattr) (void | err) = + void_or_err(tb_set_clear_attrs(fg, bg)); +@symbol("tb_set_clear_attrs") fn tb_set_clear_attrs(fg: uattr, bg: uattr) int; + +// Synchronize the internal back buffer with the terminal by +// writing to tty. +export fn present() (void | err) = void_or_err(tb_present()); +@symbol("tb_present") fn tb_present() int; + +// Clear the internal front buffer effectively forcing a complete re-render +// of the back buffer to the tty. It is not necessary to call this under +// normal circumstances. +export fn invalidate() (void | err) = void_or_err(tb_invalidate()); +@symbol("tb_invalidate") fn tb_invalidate() int; + +// Set the position of the cursor. Upper-left cell is (0, 0). +export fn set_cursor(cx: int, cy: int) (void | err) = + void_or_err(tb_set_cursor(cx, cy)); +@symbol("tb_set_cursor") fn tb_set_cursor(x: int, y: int) int; + +// Hide the cursor. +export fn hide_cursor() (void | err) = void_or_err(tb_hide_cursor()); +@symbol("tb_hide_cursor") fn tb_hide_cursor() int; + +// Set cell contents in the internal back buffer at the specified position. +// Non-printable runes are replaced with U+FFFD at render time. +export fn set_cell( + x: int, + y: int, + ch: rune, + fg: uattr, + bg: uattr, +) (void | err) = void_or_err(tb_set_cell(x, y, ch: u32, fg, bg)); +@symbol("tb_set_cell") fn tb_set_cell( + x: int, + y: int, + ch: u32, + fg: uattr, + bg: uattr, +) int; + +// Set extended cell contents. Only available when OPT_ATTR_W >= 32. +// See also [[termbox2::set_cell]] and [[termbox2::cell]]. +export fn set_cell_ex( + x: int, + y: int, + ch: []rune, + fg: uattr, + bg: uattr, +) (void | err) = + void_or_err(tb_set_cell_ex(x, y, &ch[0]: *u32, len(ch), fg, bg)); +@symbol("tb_set_cell_ex") fn tb_set_cell_ex( + x: int, + y: int, + ch: *u32, + n: size, + fg: uattr, + bg: uattr, +) int; + +// Add one character to the cell's ech. Only available when OPT_ATTR_W >= 32. +// See also [[termbox2::set_cell_ex]] and [[termbox2::cell]]. +export fn extend_cell(x: int, y: int, ch: rune) (void | err) = + void_or_err(tb_extend_cell(x, y, ch: u32)); +@symbol("tb_extend_cell") fn tb_extend_cell(x: int, y: int, ch: u32) int; + +// Set the input mode, see [[termbox2::INPUT]]. +// +// Termbox has two input modes: +// - INPUT.ESC When escape (\x1b) is in the buffer and there's no match +// for an escape sequence, a key event for [[termbox2::KEY]].ESC is returned. +// - INPUT.ALT When escape (\x1b) is in the buffer and there's no match +// for an an escape sequence, the next keyboard event is returned with a +// [[termbox2::MOD]].ALT modifier. +// +// You can also apply termbox2::INPUT.MOUSE via bitwise OR operation to either +// of the modes (e.g., INPUT.ALT | INPUT.MOUSE) to receive +// [[termbox2::mouse_event]] events. If none of the main two modes were +// set, but the mouse mode was, INPUT.ESC is used. If for some reason you've +// decided to use INPUT.ESC | INPUT.ALT, it will behave as if only INPUT.ESC +// was selected. +// +// If mode is INPUT.CURRENT, return the current input mode. +// +// The default input mode is INPUT.ESC. +export fn set_input_mode(mode: input_mode) (int | err) = + int_or_err(tb_set_input_mode(mode: int)); +@symbol("tb_set_input_mode") fn tb_set_input_mode(mode: int) int; + +// Set the output mode, see [[termbox2::OUTPUT]]. +// +// Termbox has multiple output modes: +// +// 1. OUTPUT.NORMAL => [0..8] +// +// This mode provides 8 different colors (see [[termbox2::COLOR]]), plus +// COLOR.DEFAULT which skips sending a color code (i.e., uses the terminal's +// default color). Colors may be bitwise OR'd with style attributes, see +// [[termbox2::ATTRIBUTE]]. Extra attributes are available if OPT_ATTR_W == 64. +// +// Some notes: ATTRIBUTE.REVERSE and ATTRIBUTE.BRIGHT can be applied as either +// fg or bg attributes for the same effect. The rest of the attributes apply to +// fg only and are ignored as bg attributes. +// +// Example usage: +// +// let fg = termbox2::COLOR.BLACK | termbox2::ATTRIBUTE.BOLD; +// let bg = termbox2::COLOR.RED; +// termbox2::set_cell(x, y, '@', fg, bg); +// +// 2. OUTPUT._256 => [0..255] + ATTRIBUTE.HI_BLACK +// +// In this mode you get 256 distinct colors (plus default): +// - 0x00 (1): ATTRIBUTE.DEFAULT +// - ATTRIBUTE.HI_BLACK (1): COLOR.BLACK in OUTPUT.NORMAL +// - 0x01..0x07 (7): the next 7 colors as in OUTPUT.NORMAL +// - 0x08..0x0f (8): bright versions of the above +// - 0x10..0xe7 (216): 216 different colors +// - 0xe8..0xff (24): 24 different shades of gray +// +// All style attributes except ATTRIBUTE.BRIGHT may be bitwise OR'd as in +// OUTPUT.NORMAL. +// +// Note COLOR.BLACK must be used for black, as 0x00 represents default. +// +// 3. OUTPUT._216 => [0..216] +// +// This mode supports the 216-color range of OUTPUT._256 only, but you +// don't need to provide an offset: +// - 0x00 (1): COLOR.DEFAULT +// - 0x01..0xd8 (216): 216 different colors +// +// 4. OUTPUT.GRAYSCALE => [0..24] +// +// This mode supports the 24-color range of OUTPUT._256 only, but you +// don't need to provide an offset: +// - 0x00 (1): COLOR.DEFAULT +// - 0x01..0x18 (24): 24 different shades of gray +// +// 5. OUTPUT.TRUECOLOR => [0x000000..0xffffff] + ATTRIBUTE.HI_BLACK +// +// This mode provides 24-bit color on supported terminals. +// The format is 0xRRGGBB. +// +// All style attributes except ATTRIBUTE.BRIGHT may be bitwise OR'd as in +// OUTPUT.NORMAL. +// +// Note ATTRIBUTE.HI_BLACK must be used for black, as 0x000000 represents +// default. +// +// To use the terminal default color (i.e., to not send an escape code), pass +// COLOR.DEFAULT. For convenience, the value 0 is interpreted as COLOR.DEFAULT +// in all modes. +// +// Note, cell attributes persist after switching output modes. Any translation +// between, for example, OUTPUT.NORMAL's COLOR.RED and OUTPUT.TRUECOLOR's +// 0xff0000 must be performed by the caller. Also note that cells previously +// rendered in one mode may persist unchanged until the front buffer is cleared +// (such as after a resize event) at which point it will be re-interpreted and +// flushed according to the current mode. Callers may invoke +// [[termbox2::invalidate]] if it is desirable to immediately re-interpret +// and flush the entire screen according to the current mode. +// +// Note, not all terminals support all output modes, especially beyond +// OUTPUT.NORMAL. There is also no very reliable way to determine color +// support dynamically. If portability is desired, callers are recommended to +// use OUTPUT.NORMAL or make output mode end-user configurable. The same +// advice applies to style attributes. +// +// If mode is OUTPUT.CURRENT, return the current output mode. +// +// The default output mode is OUTPUT.NORMAL. +export fn set_output_mode(mode: output_mode) (int | err) = + int_or_err(tb_set_output_mode(mode: int)); +@symbol("tb_set_output_mode") fn tb_set_output_mode(mode: int) int; + +// Wait for an event up to timeout_ms milliseconds and return it. If no event +// is available within the timeout period, ERR.NO_EVENT is returned. On a resize +// event, the underlying select(2) call may be interrupted, yielding a return +// code of ERR.POLL. +export fn peek_event(timeout_ms: size) (event | err) = { + let e = tb_event { + t = 0, + mod = 0, + key = 0, + ch = 0, + w = 0, + h = 0, + x = 0, + y = 0, + }; + let r = tb_peek_event(&e, timeout_ms: int); + return if (r < 0) r: err else tb_event_to_event(e); +}; +@symbol("tb_peek_event") fn tb_peek_event(ev: *tb_event, t: int) int; + +// Same as [[termbox2::peek_event]] except no timeout. +export fn poll_event() (event | err) = { + let e = tb_event { + t = 0, + mod = 0, + key = 0, + ch = 0, + w = 0, + h = 0, + x = 0, + y = 0, + }; + let r = tb_poll_event(&e); + return if (r < 0) r: err else tb_event_to_event(e); +}; +@symbol("tb_poll_event") fn tb_poll_event(ev: *tb_event) int; + +// Internal termbox fds that can be used with poll(2), select(2), etc. +// externally. Callers must invoke [[termbox2::poll_event]] or +// [[termbox2::peek_event]] if fds become readable. +export fn get_fds() ((int, int) | err) = { + let ttyfd: int = 0; + let resizefd: int = 0; + let r = tb_get_fds(&ttyfd, &resizefd); + return if (r < 0) r: err else (ttyfd, resizefd); +}; +@symbol("tb_get_fds") fn tb_get_fds(ttyfd: *int, resizefd: *int) int; + +// Print function. On success, it returns the width of the printed string. +// Strings are interpreted as UTF-8. +// +// No attempt is made to do proper grapheme cluster parsing. With +// [[termbox2::OPT_EGC]] enabled, as a very coarse approximation, codepoints +// of width 0 are tacked on to the previous codepoint via +// [[termbox2::extend_cell]]. +// +// Non-printable characters (iswprint(3)) and truncated UTF-8 byte sequences +// are replaced with U+FFFD. +// +// Newlines (\n) are supported with the caveat that the return value will be +// the width of the string as if it were on a single line. +// +// If the starting coordinate is out of bounds, ERR.OUT_OF_BOUNDS is returned. +// If the starting coordinates is in bounds, but goes out of bounds, then the +// out-of-bounds portions of the string are ignored. +// +// For finer control, use [[termbox2::set_cell]]. +export fn print( + x: int, + y: int, + fg: uattr, + bg: uattr, + fmt_str: str, + args: fmt::field... +) (size | err) = { + let s = fmt::asprintf(fmt_str, args...)!; + defer free(s); + let cstr = c::fromstr(s)!; + defer free(cstr); + let outw: size = 0; + let e = tb_print_ex(x, y, fg, bg, &outw, cstr); + return if (e < 0) e: err else outw; +}; +@symbol("tb_print_ex") fn tb_print_ex( + x: int, + y: int, + fg: uattr, + bg: uattr, + out_w: *size, + cstr: const *c::char, +) int; + +// Send raw bytes to the terminal. +export fn send(buf: []u8) (void | err) = + void_or_err(tb_send(&buf[0]: *c::char, len(buf))); +@symbol("tb_send") fn tb_send(buf: const *c::char, nbuf: size) int; + +// Returns a string explaining the given error code. The caller must free +// the return value. +export fn strerror(e: err) str = switch (e) { + case ERR.HARE_BUILD => + yield fmt::asprintf( + "Cannot initialize Termbox because of a mismatch " + "between the C and Hare build options.\n" + "Hare OPT_EGC is {} and C TB_OPT_EGC is {}\n" + "Hare OPT_ATTR_W {} and C TB_OPT_ATTR_W is {}", + OPT_EGC, has_egc(), OPT_ATTR_W, attr_width() + )!; + case ERR.HARE_INVALID_UTF8 => + yield fmt::asprintf("Invalid UTF8 sequence recieved from tb")!; + case ERR.HARE_OTHER => + yield fmt::asprintf("Unknown error in the Hare bindings")!; + case => yield match (str_or_err(tb_strerror(e: int))) { + case err => yield fmt::asprintf( + "Unexpected error while interpreting previous error" + )!; + case let estr: str => yield estr; + }; +}; +@symbol("tb_strerror") fn tb_strerror(e: int) const *c::char; + +// Returns true if the the C termbox2 library was compiled with truecolor +// support (i.e., if [[termbox2::OPT_ATTR_W]] is 32 or 64). +export fn has_truecolor() bool = tb_has_truecolor() != 0; +@symbol("tb_has_truecolor") fn tb_has_truecolor() int; + +// Returns true if the the C termbox2 library was compiled with EGC +// support (i.e., if [[termbox2::OPT_EGC]] is true). +export fn has_egc() bool = tb_has_egc() != 0; +@symbol("tb_has_egc") fn tb_has_egc() int; + +// Returns the value of the TB_OPT_ATTR_W compile time option of the +// C termbox2 library. +export fn attr_width() size = tb_attr_width(): size; +@symbol("tb_attr_width") fn tb_attr_width() int; + +// Returns the string version of the C termbox2 library. +export fn version() (str | err) = str_or_err(tb_version()); +@symbol("tb_version") fn tb_version() const *c::char; -- cgit v1.3