diff options
Diffstat (limited to 'v2')
| -rw-r--r-- | v2/.gitignore | 3 | ||||
| -rw-r--r-- | v2/README.md | 52 | ||||
| -rw-r--r-- | v2/arena.h | 109 | ||||
| -rwxr-xr-x | v2/build.sh | 16 | ||||
| -rw-r--r-- | v2/chatty.c | 260 | ||||
| -rw-r--r-- | v2/common.h | 55 | ||||
| -rw-r--r-- | v2/send.c | 61 | ||||
| -rw-r--r-- | v2/server.c | 149 | ||||
| -rw-r--r-- | v2/termbox2.h | 3517 | 
9 files changed, 4222 insertions, 0 deletions
| diff --git a/v2/.gitignore b/v2/.gitignore new file mode 100644 index 0000000..7b92c6b --- /dev/null +++ b/v2/.gitignore @@ -0,0 +1,3 @@ +chatty +send +server diff --git a/v2/README.md b/v2/README.md new file mode 100644 index 0000000..17f3be5 --- /dev/null +++ b/v2/README.md @@ -0,0 +1,52 @@ +# Chatty +The idea is the following: +- tcp server that you can send messages to +- history upon connecting +- date of messages sent +- authentication +- encrypted communication (tls?) +- client for reading the messages and sending them at the same time + +- rooms +- encryption +- authentication + +## client +- wrapping messages +- prompt +- sending message + +## server +- log messages +- check if when sending and the client is offline (due to connection loss) what happens +- timeout on recv? + +## common +- handle messages that are too large +- connect/disconnections messages +- use IP address / domain +- chat history + +## Protocol +For now the protocol consists of sending Message type over the network, but in the future something +more flexible might be required.  Because it will make it easier to do things like: +- request chat logs up to a certain point +- connect to a specific room +- connect/disconnect messages + +- The null terminator must be sent with the string. +- The text can be arbitrary length +- [ ] use char text[]; instead of char* + +- todo: compression? + +## Arena's +1. There is an arena for the messages' texts (`msgTextArena`) and an arena for the messages +   (`msgsArena`). +2. The `Message.text` pointer will point to a text buffer entry in `msgTextArena` +3. Good way to do this, if you have message `M`. +```c +M.text = ArenaPush(msgTextArena, M.text_len); +``` +Notice, that this depends on knowing the text's length before allocating the memory. + diff --git a/v2/arena.h b/v2/arena.h new file mode 100644 index 0000000..9327a5e --- /dev/null +++ b/v2/arena.h @@ -0,0 +1,109 @@ +#ifndef ARENA_IMPL +#define ARENA_IMPL + +#include "common.h" + +#include <fcntl.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> +#include <sys/mman.h> +#include <unistd.h> + +#define PAGESIZE 4096 + +#ifndef ARENA_MEMORY +#define ARENA_MEMORY PAGESIZE +#endif + +struct Arena { +    void *memory; +    u64 size; +    u64 pos; +} typedef Arena; + +// Create an arena +Arena *ArenaAlloc(void); +// Destroy an arena +void ArenaRelease(Arena *arena); + +// Push bytes on to the arena | allocating +void *ArenaPush(Arena *arena, u64 size); +void *ArenaPushZero(Arena *arena, u64 size); + +#define PushArray(arena, type, count) (type *)ArenaPush((arena), sizeof(type)*(count)) +#define PushArrayZero(arena, type, count) (type *)ArenaPushZero((arena), sizeof(type) * (count)) +#define PushStruct(arena, type) PushArray((arena), (type), 1) +#define PushStructZero(arena, type) PushArrayZero((arena), (type), 1) + +// Free some bytes by popping the stack +void ArenaPop(Arena *arena, u64 size); +// Get the number of bytes allocated +u64 ArenaGetPos(Arena *arena); + +void ArenaSetPosBack(Arena *arena, u64 pos); +void ArenaClear(Arena *arena); + +Arena *ArenaAlloc(void) +{ +    // NOTE: If the arena is created here the pointer to the memory get's overwritten with size in +    // ArenaPush, so we are forced to use malloc +    Arena *arena = malloc(sizeof(Arena)); + +    arena->memory = mmap(NULL, ARENA_MEMORY, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); +    if (arena->memory == MAP_FAILED) +        return NULL; + +    arena->pos = 0; +    arena->size = ARENA_MEMORY; + +    return arena; +} + +void ArenaRelease(Arena *arena) +{ +    munmap(arena->memory, ARENA_MEMORY); +    free(arena); +} + +void *ArenaPush(Arena *arena, u64 size) +{ +    u8 *mem; +    mem = (u8 *)arena->memory + arena->pos; +    arena->pos += size; +    return mem; +} + +void *ArenaPushZero(Arena *arena, u64 size) +{ +    u8 *mem; +    mem = (u8 *)arena->memory + arena->pos; +    bzero(mem, size); +    arena->pos += size; +    return mem; +} + +void ArenaPop(Arena *arena, u64 size) +{ +    arena->pos -= size; +} + +u64 ArenaGetPos(Arena *arena) +{ +    return arena->pos; +} + +void ArenaSetPosBack(Arena *arena, u64 pos) +{ +    arena->pos -= pos; +} + +void ArenaClear(Arena *arena) +{ +    bzero(arena->memory, arena->size); +    arena->pos = 0; +} + +#endif diff --git a/v2/build.sh b/v2/build.sh new file mode 100755 index 0000000..53e6e14 --- /dev/null +++ b/v2/build.sh @@ -0,0 +1,16 @@ +#!/bin/sh +build () { +    ( +        set -x +        gcc -ggdb -Wall -pedantic -std=c99 -o ${1%.c} $@ +    ) +} + +if [ "$1" ]; then +    build "$1" +    exit +fi + +build chatty.c +build server.c +build send.c diff --git a/v2/chatty.c b/v2/chatty.c new file mode 100644 index 0000000..256cc22 --- /dev/null +++ b/v2/chatty.c @@ -0,0 +1,260 @@ +#define TB_IMPL +#include "termbox2.h" + +#include "arena.h" +#include "common.h" + +#include <arpa/inet.h> +#include <assert.h> +#include <locale.h> +#include <poll.h> +#include <sys/socket.h> + +#define TIMEOUT_POLL 60 * 1000 +// time to reconnect in seconds +#define TIMEOUT_RECONNECT 1 + +// must be of AUTHOR_LEN -1 +static char username[AUTHOR_LEN] = "(null)"; + +enum { FDS_SERVER = 0, +       FDS_TTY, +       FDS_RESIZE, +       FDS_MAX }; + +// fill str array with char +void fillstr(u8 *str, u8 ch, u8 len) +{ +    for (int i = 0; i < len; i++) { +        str[i] = ch; +    } +} + +// home screen, the first screen the user sees +// it displays a prompt for user input and the received messages from msgsArena +void screen_home(Arena *msgsArena, wchar_t input[], u32 input_len) +{ +    Message *messages = msgsArena->memory; +    assert(messages != NULL); +    for (int i = 0; i < (msgsArena->pos / sizeof(Message)); i++) { +        // Color user's own messages +        u32 fg = 0; +        if (strncmp(username, (char *)messages[i].author, AUTHOR_LEN) == 0) { +            fg = TB_CYAN; +        } else { +            fg = TB_WHITE; +        } + +        // TODO: wrap when exceeding prompt size +        tb_printf(0, i, fg, 0, "%s [%s] %ls", messages[i].timestamp, messages[i].author, messages[i].text); +    } + +    int len = global.width * 80 / 100; +    wchar_t su[len + 2]; +    wchar_t sd[len + 2]; +    wchar_t lr = L'─', ur = L'╭', rd = L'╮', dr = L'╰', ru = L'╯', ud = L'│'; +    { +        // top bar for prompt +        su[0] = ur; +        for (int i = 1; i < len; i++) { +            su[i] = lr; +        } +        su[len] = rd; +        su[len + 1] = 0; + +        // bottom bar for prompt +        sd[0] = dr; +        for (int i = 1; i < len; i++) { +            sd[i] = lr; +        } +        sd[len] = ru; +        sd[len + 1] = 0; +    } + +    tb_printf(1, global.height - 3, 0, 0, "%ls", su); +    tb_printf(1, global.height - 2, 0, 0, "%lc", ud); +    global.cursor_x = 1 + 2 + input_len; +    global.cursor_y = global.height - 2; +    tb_printf(1 + 2, global.height - 2, 0, 0, "%ls", input); +    tb_printf(1 + len, global.height - 2, 0, 0, "%lc", ud); +    tb_printf(1, global.height - 1, 0, 0, "%ls", sd); +} + +int main(int argc, char **argv) +{ +    // Use first argument as username +    if (argc > 1) { +        u32 arg_len = strlen(argv[1]); +        assert(arg_len <= AUTHOR_LEN - 1); +        memcpy(username, argv[1], arg_len); +        username[arg_len] = '\0'; +    } + +    s32 err, serverfd, ttyfd, resizefd, nsend; +    setlocale(LC_ALL, ""); /* Fix unicode handling */ + +    const struct sockaddr_in address = { +        AF_INET, +        htons(PORT), +        {0}, +    }; + +    serverfd = socket(AF_INET, SOCK_STREAM, 0); +    assert(serverfd > 2); // greater than STDERR + +    err = connect(serverfd, (struct sockaddr *)&address, sizeof(address)); +    assert(err == 0); + +    tb_init(); +    bytebuf_puts(&global.out, global.caps[TB_CAP_SHOW_CURSOR]); +    tb_get_fds(&ttyfd, &resizefd); + +    struct pollfd fds[FDS_MAX] = { +        {serverfd, POLLIN, 0}, +        {   ttyfd, POLLIN, 0}, +        {resizefd, POLLIN, 0}, +    }; + +    Arena *msgsArena = ArenaAlloc(); +    // Message *messages = msgsArena->memory; // helper pointer, for indexing memory +    Arena *msgTextArena = ArenaAlloc(); +    u32 nrecv = 0; +    // buffer used for receiving and sending messages +    u8 buf[STREAM_BUF] = {0}; +    Message *mbuf = (Message *)buf; + +    wchar_t input[256] = {0}; +    u32 input_len = 0; +    struct tb_event ev; +    char *errmsg = NULL; + +    // Display loop +    screen_home(msgsArena, input, input_len); +    tb_present(); +    while (1) { +        err = poll(fds, FDS_MAX, TIMEOUT_POLL); +        // ignore resize events because we redraw the whole screen anyways. +        assert(err != -1 || errno == EINTR); + +        tb_clear(); + +        if (fds[FDS_SERVER].revents & POLLIN) { +            // got data from server +            u8 timestamp[TIMESTAMP_LEN]; +            message_timestamp(timestamp); + +            nrecv = recv(serverfd, buf, STREAM_LIMIT, 0); +            assert(nrecv != -1); +            if (nrecv == 0) { +                // TODO: Handle disconnection, aka wait for server to reconnect. +                // Try to reconnect with 1 second timeout +                // TODO: still listen for events +                // NOTE: we want to display a popup, but still give the user the chance to do +                // ctrl+c +                u8 once = 0; +                while (1) { +                    err = close(serverfd); +                    assert(err == 0); + +                    serverfd = socket(AF_INET, SOCK_STREAM, 0); +                    assert(serverfd > 2); // greater than STDERR + +                    err = connect(serverfd, (struct sockaddr *)&address, sizeof(address)); +                    if (err == 0) { +                        bytebuf_puts(&global.out, global.caps[TB_CAP_SHOW_CURSOR]); +                        tb_clear(); +                        break; +                    } +                    assert(errno == ECONNREFUSED); + +                    // Popup error message +                    if (!once) { +                        screen_home(msgsArena, input, input_len); +                        tb_hide_cursor(); +                        tb_print(global.width / 2 - 10, global.height / 2, TB_RED, TB_BLACK, "Server disconnected!"); +                        tb_present(); +                        once = 1; +                    } + +                    sleep(TIMEOUT_RECONNECT); +                } +            } else { + +                Message *buf_msg = (Message *)buf; // helper for indexing memory +                Message *recvmsg = ArenaPush(msgsArena, sizeof(Message)); +                // copy everything but the text +                memcpy(recvmsg, buf, AUTHOR_LEN + TIMESTAMP_LEN + sizeof(buf_msg->text_len)); +                // allocate memeory for text +                recvmsg->text = ArenaPush(msgTextArena, recvmsg->text_len * sizeof(wchar_t)); +                // copy the text to the allocated space +                memcpy(recvmsg->text, buf + TIMESTAMP_LEN + AUTHOR_LEN + sizeof(recvmsg->text_len), recvmsg->text_len * sizeof(wchar_t)); +            } +        } else if (fds[FDS_TTY].revents & POLLIN) { +            // got a key event +            tb_poll_event(&ev); + +            u8 exit = 0; +            switch (ev.key) { +            case TB_KEY_CTRL_D: +            case TB_KEY_CTRL_C: +                exit = 1; +                break; +            case TB_KEY_CTRL_M: // send message +                // null terminate +                input[input_len] = 0; +                input_len++; +                // TODO: check size does not exceed buffer + +                // add to msgsArena +                Message *sendmsg = ArenaPush(msgsArena, sizeof(Message)); +                memcpy(sendmsg->author, username, AUTHOR_LEN); +                message_timestamp(sendmsg->timestamp); +                sendmsg->text_len = input_len; +                sendmsg->text = ArenaPush(msgTextArena, input_len * sizeof(wchar_t)); +                // copy the text to the allocated space +                memcpy(sendmsg->text, input, input_len * sizeof(wchar_t)); + +                // Send the message +                // copy everything but the text +                memcpy(buf, sendmsg, AUTHOR_LEN + TIMESTAMP_LEN + sizeof(wchar_t)); +                memcpy(&mbuf->text, input, input_len * sizeof(wchar_t)); +                nsend = send(serverfd, buf, AUTHOR_LEN + TIMESTAMP_LEN + input_len * sizeof(wchar_t), 0); +                assert(nsend > 0); + +            case TB_KEY_CTRL_U: // clear input +                bzero(input, input_len * sizeof(wchar_t)); +                input_len = 0; +                break; +            default: +                assert(ev.ch >= 0); +                if (ev.ch == 0) +                    break; +                // append key to input buffer +                // TODO: check size does not exceed buffer +                input[input_len] = ev.ch; +                input_len++; + +                break; +            } +            if (exit) +                break; + +        } else if (fds[FDS_RESIZE].revents & POLLIN) { +            tb_poll_event(&ev); +        } + +        screen_home(msgsArena, input, input_len); + +        tb_present(); +    } + +    tb_shutdown(); + +    if (errmsg != NULL) +        printf("%s\n", errmsg); + +    ArenaRelease(msgTextArena); +    ArenaRelease(msgsArena); + +    return 0; +} diff --git a/v2/common.h b/v2/common.h new file mode 100644 index 0000000..0f14329 --- /dev/null +++ b/v2/common.h @@ -0,0 +1,55 @@ +#ifndef COMMON_H +#define COMMON_H + +#include <stdarg.h> +#include <stdio.h> +#include <time.h> +#include <unistd.h> +#include <stdint.h> +#include <stddef.h> +#include <wchar.h> + +#define AUTHOR_LEN 13 +#define TIMESTAMP_LEN 9 +// port to listen on +#define PORT 9983 +// buffer size for holding data received from recv() +// TODO: choose a good size +#define STREAM_BUF 256 +// max data received in one recv() call on serverfd +// TODO: choose a good size +#define STREAM_LIMIT 512 +// max message that can be displayed with writef() +#define WRITEF_MAX 256 + + +typedef uint8_t u8; +typedef uint16_t u16; +typedef uint32_t u32; +typedef uint64_t u64; +typedef int8_t s8; +typedef int16_t s16; +typedef int32_t s32; +typedef int64_t s64; + +struct Message { +    u8 author[AUTHOR_LEN]; +    u8 timestamp[TIMESTAMP_LEN]; +    // includes null terminator +    u16 text_len; +    wchar_t *text; +} typedef Message; + +#define MESSAGELEN(m) (AUTHOR_LEN + TIMESTAMP_LEN + sizeof(m.text_len)*sizeof(wchar_t) + m.text_len) +#define MESSAGELENP(m) (AUTHOR_LEN + TIMESTAMP_LEN + sizeof(m->text_len) + m->text_len*(sizeof(wchar_t))) + +void message_timestamp(u8 str[TIMESTAMP_LEN]) +{ +    time_t now; +    struct tm *ltime; +    time(&now); +    ltime = localtime(&now); +    strftime((char *)str, TIMESTAMP_LEN, "%H:%M:%S", ltime); +} + +#endif diff --git a/v2/send.c b/v2/send.c new file mode 100644 index 0000000..3caed15 --- /dev/null +++ b/v2/send.c @@ -0,0 +1,61 @@ +// minimal client implementation + +#include <arpa/inet.h> +#include <assert.h> +#include <stdarg.h> +#include <string.h> +#include <unistd.h> + +#include "arena.h" +#include "common.h" + +int main(int argc, char **argv) +{ +    if (argc < 3) { +        fprintf(stderr, "usage: send <author> <msg>\n"); +        return 1; +    } + +    u32 err, serverfd, nsend; + +    serverfd = socket(AF_INET, SOCK_STREAM, 0); +    assert(serverfd != -1); + +    const struct sockaddr_in address = { +        AF_INET, +        htons(PORT), +        {0}, +    }; +    err = connect(serverfd, (struct sockaddr *)&address, sizeof(address)); +    assert(err == 0); + +    { +        u32 author_len = strlen(argv[1]) + 1; // add 1 for null terminator +        assert(author_len <= AUTHOR_LEN); + +        // convert text to wide string +        u32 text_len = strlen(argv[2]) + 1; +        wchar_t text_wide[text_len]; +        u32 size = mbstowcs(text_wide, argv[2], text_len - 1); +        assert(size == text_len - 1); +        // null terminate +        text_wide[text_len - 1] = 0; + +        u8 buf[STREAM_BUF] = {0}; +        Message *m = (Message *)buf; + +        memcpy(m->author, argv[1], author_len - 1); +        message_timestamp(m->timestamp); +        m->text_len = text_len; +        memcpy(&m->text, text_wide, m->text_len * sizeof(wchar_t)); + +        nsend = send(serverfd, buf, MESSAGELENP(m), 0); + +        assert(nsend >= 0); + +        printf("text_len: %d\n", text_len); +        fprintf(stdout, "Sent %d bytes.\n", nsend); +    } + +    return 0; +} diff --git a/v2/server.c b/v2/server.c new file mode 100644 index 0000000..c86964e --- /dev/null +++ b/v2/server.c @@ -0,0 +1,149 @@ +#include "arena.h" +#include "common.h" + +#include <assert.h> +#include <netinet/in.h> +#include <poll.h> +#include <stdarg.h> +#include <sys/socket.h> + +// timeout on polling +#define TIMEOUT 60 * 1000 +// max pending connections +#define PENDING_MAX 16 + +// the size of pollfd element in the fdsArena +// note: clientsArena and pollfd_size must have been initialisezd +#define FDS_SIZE fdsArena->pos / pollfd_size + +// enum for indexing the fds array +enum { FDS_STDIN = 0, +       FDS_SERVER, +       FDS_CLIENTS }; + +int main(void) +{ +    u32 err, serverfd, clientfd; +    u16 nclient = 0; +    u32 on = 1; + +    // Start listening on the socket +    { +        serverfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP); +        assert(serverfd > 2); + +        err = setsockopt(serverfd, SOL_SOCKET, SO_REUSEADDR, (u8 *)&on, sizeof(on)); +        assert(err == 0); + +        const struct sockaddr_in address = { +            AF_INET, +            htons(PORT), +            {0}, +        }; + +        err = bind(serverfd, (const struct sockaddr *)&address, sizeof(address)); +        assert(err == 0); + +        err = listen(serverfd, PENDING_MAX); +        assert(err == 0); +    } + +    Arena *msgTextArena = ArenaAlloc(); // allocating text in messages that have a dynamic sized +    Message mrecv = {0};                // message used for receiving messages from clients +    u32 nrecv = 0;                      // number of bytes received +    u32 nsend = 0;                      // number of bytes sent +    u8 buf[STREAM_BUF] = {0};           // temporary buffer for received data, NOTE: this buffer +                                        // is also use for retransmitting received messages to other +                                        // clients. + +    Arena *fdsArena = ArenaAlloc(); +    struct pollfd *fds = fdsArena->memory; // helper for indexing memory +    struct pollfd c = {0, POLLIN, 0};      // helper client structure fore reusing +    struct pollfd *fdsAddr;                // used for copying clients +    const u64 pollfd_size = sizeof(struct pollfd); + +    // initialize fds structure +    // add stdin (c.fd == 0) +    fdsAddr = ArenaPush(fdsArena, pollfd_size); +    memcpy(fdsAddr, &c, pollfd_size); +    // add serverfd +    c.fd = serverfd; +    fdsAddr = ArenaPush(fdsArena, pollfd_size); +    memcpy(fdsAddr, &c, pollfd_size); + +    while (1) { +        err = poll(fds, FDS_SIZE, TIMEOUT); +        assert(err != -1); + +        if (fds[FDS_STDIN].revents & POLLIN) { +            // helps for testing and exiting gracefully +            break; +        } else if (fds[FDS_SERVER].revents & POLLIN) { +            clientfd = accept(serverfd, NULL, NULL); +            assert(clientfd != -1); +            assert(clientfd > serverfd); + +            // fill up a hole +            u8 found = 0; +            for (u32 i = FDS_CLIENTS; i < FDS_SIZE; i++) { +                if (fds[i].fd == -1) { +                    fds[i].fd = clientfd; +                    // note we do not have to reset .revents because poll will set it to 0 next time +                    found = 1; +                    break; +                } +            } + +            // allocate an extra client because there was no empty spot in the fds array +            if (!found) { +                // add client to arena +                fdsAddr = ArenaPush(fdsArena, pollfd_size); +                c.fd = clientfd; +                memcpy(fdsAddr, &c, pollfd_size); +            } + +            nclient++; +            fprintf(stdout, "connected(%d).\n", clientfd - serverfd); +        } + +        for (u32 i = FDS_CLIENTS; i < (FDS_SIZE); i++) { +            if (!(fds[i].revents & POLLIN)) +                continue; +            if (fds[i].fd == -1) +                continue; + +            nrecv = recv(fds[i].fd, buf, STREAM_LIMIT, 0); +            assert(nrecv >= 0); + +            if (nrecv == 0) { +                fprintf(stdout, "disconnected(%d). \n", fds[i].fd - serverfd); +                shutdown(fds[i].fd, SHUT_RDWR); +                close(fds[i].fd); // send close to client +                fds[i].fd = -1;   // ignore in the future +                continue; +            } + +            // TODO: Do not print the message in the logs +            fprintf(stdout, "message(%d): %d bytes.\n", fds[i].fd - serverfd, nrecv);  + +            for (u32 j = FDS_CLIENTS; j < (FDS_SIZE); j++) { +                if (j == i) +                    continue; +                if (fds[j].fd == -1) +                    continue; + +                nsend = send(fds[j].fd, buf, nrecv, 0); +                assert(nsend != 1); +                assert(nsend == nrecv); +                fprintf(stdout, "retransmitted(%d->%d).\n", fds[i].fd - serverfd, fds[j].fd - serverfd); +            } + +            ArenaPop(msgTextArena, mrecv.text_len); +        } +    } + +    ArenaRelease(fdsArena); +    ArenaRelease(msgTextArena); + +    return 0; +} diff --git a/v2/termbox2.h b/v2/termbox2.h new file mode 100644 index 0000000..265cdab --- /dev/null +++ b/v2/termbox2.h @@ -0,0 +1,3517 @@ +/* +MIT License + +Copyright (c) 2010-2020 nsf <no.smile.face@gmail.com> +              2015-2024 Adam Saponara <as@php.net> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef TERMBOX_H_INCL +#define TERMBOX_H_INCL + +#ifndef _XOPEN_SOURCE +#define _XOPEN_SOURCE +#endif + +#ifndef _DEFAULT_SOURCE +#define _DEFAULT_SOURCE +#endif + +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <signal.h> +#include <stdarg.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/ioctl.h> +#include <sys/select.h> +#include <sys/stat.h> +#include <sys/time.h> +#include <sys/types.h> +#include <termios.h> +#include <unistd.h> +#include <wchar.h> +#include <wctype.h> + +#ifdef PATH_MAX +#define TB_PATH_MAX PATH_MAX +#else +#define TB_PATH_MAX 4096 +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +// __ffi_start + +#define TB_VERSION_STR "2.5.0-dev" + +/* The following compile-time options are supported: + * + *     TB_OPT_ATTR_W: Integer width of fg and bg attributes. Valid values + *                    (assuming system support) are 16, 32, and 64. (See + *                    uintattr_t). 32 or 64 enables output mode + *                    TB_OUTPUT_TRUECOLOR. 64 enables additional style + *                    attributes. (See tb_set_output_mode.) Larger values + *                    consume more memory in exchange for more features. + *                    Defaults to 16. + * + *        TB_OPT_EGC: If set, enable extended grapheme cluster support + *                    (tb_extend_cell, tb_set_cell_ex). Consumes more memory. + *                    Defaults off. + * + * TB_OPT_PRINTF_BUF: Write buffer size for printf operations. Represents the + *                    largest string that can be sent in one call to tb_print* + *                    and tb_send* functions. Defaults to 4096. + * + *   TB_OPT_READ_BUF: Read buffer size for tty reads. Defaults to 64. + * + *  TB_OPT_TRUECOLOR: Deprecated. Sets TB_OPT_ATTR_W to 32 if not already set. + */ + +#if defined(TB_LIB_OPTS) || 0 // __tb_lib_opts +/* Ensure consistent compile-time options when using as a shared library */ +#undef TB_OPT_ATTR_W +#undef TB_OPT_EGC +#undef TB_OPT_PRINTF_BUF +#undef TB_OPT_READ_BUF +#define TB_OPT_ATTR_W 64 +#define TB_OPT_EGC +#endif + +/* Ensure sane `TB_OPT_ATTR_W` (16, 32, or 64) */ +#if defined TB_OPT_ATTR_W && TB_OPT_ATTR_W == 16 +#elif defined TB_OPT_ATTR_W && TB_OPT_ATTR_W == 32 +#elif defined TB_OPT_ATTR_W && TB_OPT_ATTR_W == 64 +#else +#undef TB_OPT_ATTR_W +#if defined TB_OPT_TRUECOLOR // Deprecated. Back-compat for old flag. +#define TB_OPT_ATTR_W 32 +#else +#define TB_OPT_ATTR_W 16 +#endif +#endif + +/* ASCII key constants (`tb_event.key`) */ +#define TB_KEY_CTRL_TILDE       0x00 +#define TB_KEY_CTRL_2           0x00 // clash with `CTRL_TILDE` +#define TB_KEY_CTRL_A           0x01 +#define TB_KEY_CTRL_B           0x02 +#define TB_KEY_CTRL_C           0x03 +#define TB_KEY_CTRL_D           0x04 +#define TB_KEY_CTRL_E           0x05 +#define TB_KEY_CTRL_F           0x06 +#define TB_KEY_CTRL_G           0x07 +#define TB_KEY_BACKSPACE        0x08 +#define TB_KEY_CTRL_H           0x08 // clash with `CTRL_BACKSPACE` +#define TB_KEY_TAB              0x09 +#define TB_KEY_CTRL_I           0x09 // clash with `TAB` +#define TB_KEY_CTRL_J           0x0a +#define TB_KEY_CTRL_K           0x0b +#define TB_KEY_CTRL_L           0x0c +#define TB_KEY_ENTER            0x0d +#define TB_KEY_CTRL_M           0x0d // clash with `ENTER` +#define TB_KEY_CTRL_N           0x0e +#define TB_KEY_CTRL_O           0x0f +#define TB_KEY_CTRL_P           0x10 +#define TB_KEY_CTRL_Q           0x11 +#define TB_KEY_CTRL_R           0x12 +#define TB_KEY_CTRL_S           0x13 +#define TB_KEY_CTRL_T           0x14 +#define TB_KEY_CTRL_U           0x15 +#define TB_KEY_CTRL_V           0x16 +#define TB_KEY_CTRL_W           0x17 +#define TB_KEY_CTRL_X           0x18 +#define TB_KEY_CTRL_Y           0x19 +#define TB_KEY_CTRL_Z           0x1a +#define TB_KEY_ESC              0x1b +#define TB_KEY_CTRL_LSQ_BRACKET 0x1b // clash with 'ESC' +#define TB_KEY_CTRL_3           0x1b // clash with 'ESC' +#define TB_KEY_CTRL_4           0x1c +#define TB_KEY_CTRL_BACKSLASH   0x1c // clash with 'CTRL_4' +#define TB_KEY_CTRL_5           0x1d +#define TB_KEY_CTRL_RSQ_BRACKET 0x1d // clash with 'CTRL_5' +#define TB_KEY_CTRL_6           0x1e +#define TB_KEY_CTRL_7           0x1f +#define TB_KEY_CTRL_SLASH       0x1f // clash with 'CTRL_7' +#define TB_KEY_CTRL_UNDERSCORE  0x1f // clash with 'CTRL_7' +#define TB_KEY_SPACE            0x20 +#define TB_KEY_BACKSPACE2       0x7f +#define TB_KEY_CTRL_8           0x7f // clash with 'BACKSPACE2' + +#define tb_key_i(i)             0xffff - (i) +/* Terminal-dependent key constants (`tb_event.key`) and terminfo caps */ +/* BEGIN codegen h */ +/* Produced by ./codegen.sh on Tue, 03 Sep 2024 04:17:47 +0000 */ +#define TB_KEY_F1               (0xffff - 0) +#define TB_KEY_F2               (0xffff - 1) +#define TB_KEY_F3               (0xffff - 2) +#define TB_KEY_F4               (0xffff - 3) +#define TB_KEY_F5               (0xffff - 4) +#define TB_KEY_F6               (0xffff - 5) +#define TB_KEY_F7               (0xffff - 6) +#define TB_KEY_F8               (0xffff - 7) +#define TB_KEY_F9               (0xffff - 8) +#define TB_KEY_F10              (0xffff - 9) +#define TB_KEY_F11              (0xffff - 10) +#define TB_KEY_F12              (0xffff - 11) +#define TB_KEY_INSERT           (0xffff - 12) +#define TB_KEY_DELETE           (0xffff - 13) +#define TB_KEY_HOME             (0xffff - 14) +#define TB_KEY_END              (0xffff - 15) +#define TB_KEY_PGUP             (0xffff - 16) +#define TB_KEY_PGDN             (0xffff - 17) +#define TB_KEY_ARROW_UP         (0xffff - 18) +#define TB_KEY_ARROW_DOWN       (0xffff - 19) +#define TB_KEY_ARROW_LEFT       (0xffff - 20) +#define TB_KEY_ARROW_RIGHT      (0xffff - 21) +#define TB_KEY_BACK_TAB         (0xffff - 22) +#define TB_KEY_MOUSE_LEFT       (0xffff - 23) +#define TB_KEY_MOUSE_RIGHT      (0xffff - 24) +#define TB_KEY_MOUSE_MIDDLE     (0xffff - 25) +#define TB_KEY_MOUSE_RELEASE    (0xffff - 26) +#define TB_KEY_MOUSE_WHEEL_UP   (0xffff - 27) +#define TB_KEY_MOUSE_WHEEL_DOWN (0xffff - 28) + +#define TB_CAP_F1               0 +#define TB_CAP_F2               1 +#define TB_CAP_F3               2 +#define TB_CAP_F4               3 +#define TB_CAP_F5               4 +#define TB_CAP_F6               5 +#define TB_CAP_F7               6 +#define TB_CAP_F8               7 +#define TB_CAP_F9               8 +#define TB_CAP_F10              9 +#define TB_CAP_F11              10 +#define TB_CAP_F12              11 +#define TB_CAP_INSERT           12 +#define TB_CAP_DELETE           13 +#define TB_CAP_HOME             14 +#define TB_CAP_END              15 +#define TB_CAP_PGUP             16 +#define TB_CAP_PGDN             17 +#define TB_CAP_ARROW_UP         18 +#define TB_CAP_ARROW_DOWN       19 +#define TB_CAP_ARROW_LEFT       20 +#define TB_CAP_ARROW_RIGHT      21 +#define TB_CAP_BACK_TAB         22 +#define TB_CAP__COUNT_KEYS      23 +#define TB_CAP_ENTER_CA         23 +#define TB_CAP_EXIT_CA          24 +#define TB_CAP_SHOW_CURSOR      25 +#define TB_CAP_HIDE_CURSOR      26 +#define TB_CAP_CLEAR_SCREEN     27 +#define TB_CAP_SGR0             28 +#define TB_CAP_UNDERLINE        29 +#define TB_CAP_BOLD             30 +#define TB_CAP_BLINK            31 +#define TB_CAP_ITALIC           32 +#define TB_CAP_REVERSE          33 +#define TB_CAP_ENTER_KEYPAD     34 +#define TB_CAP_EXIT_KEYPAD      35 +#define TB_CAP_DIM              36 +#define TB_CAP_INVISIBLE        37 +#define TB_CAP__COUNT           38 +/* END codegen h */ + +/* Some hard-coded caps */ +#define TB_HARDCAP_ENTER_MOUSE  "\x1b[?1000h\x1b[?1002h\x1b[?1015h\x1b[?1006h" +#define TB_HARDCAP_EXIT_MOUSE   "\x1b[?1006l\x1b[?1015l\x1b[?1002l\x1b[?1000l" +#define TB_HARDCAP_STRIKEOUT    "\x1b[9m" +#define TB_HARDCAP_UNDERLINE_2  "\x1b[21m" +#define TB_HARDCAP_OVERLINE     "\x1b[53m" + +/* Colors (numeric) and attributes (bitwise) (`tb_cell.fg`, `tb_cell.bg`) */ +#define TB_DEFAULT              0x0000 +#define TB_BLACK                0x0001 +#define TB_RED                  0x0002 +#define TB_GREEN                0x0003 +#define TB_YELLOW               0x0004 +#define TB_BLUE                 0x0005 +#define TB_MAGENTA              0x0006 +#define TB_CYAN                 0x0007 +#define TB_WHITE                0x0008 + +#if TB_OPT_ATTR_W == 16 +#define TB_BOLD      0x0100 +#define TB_UNDERLINE 0x0200 +#define TB_REVERSE   0x0400 +#define TB_ITALIC    0x0800 +#define TB_BLINK     0x1000 +#define TB_HI_BLACK  0x2000 +#define TB_BRIGHT    0x4000 +#define TB_DIM       0x8000 +#define TB_256_BLACK TB_HI_BLACK // `TB_256_BLACK` is deprecated +#else +// `TB_OPT_ATTR_W` is 32 or 64 +#define TB_BOLD                0x01000000 +#define TB_UNDERLINE           0x02000000 +#define TB_REVERSE             0x04000000 +#define TB_ITALIC              0x08000000 +#define TB_BLINK               0x10000000 +#define TB_HI_BLACK            0x20000000 +#define TB_BRIGHT              0x40000000 +#define TB_DIM                 0x80000000 +#define TB_TRUECOLOR_BOLD      TB_BOLD // `TB_TRUECOLOR_*` is deprecated +#define TB_TRUECOLOR_UNDERLINE TB_UNDERLINE +#define TB_TRUECOLOR_REVERSE   TB_REVERSE +#define TB_TRUECOLOR_ITALIC    TB_ITALIC +#define TB_TRUECOLOR_BLINK     TB_BLINK +#define TB_TRUECOLOR_BLACK     TB_HI_BLACK +#endif + +#if TB_OPT_ATTR_W == 64 +#define TB_STRIKEOUT   0x0000000100000000 +#define TB_UNDERLINE_2 0x0000000200000000 +#define TB_OVERLINE    0x0000000400000000 +#define TB_INVISIBLE   0x0000000800000000 +#endif + +/* Event types (`tb_event.type`) */ +#define TB_EVENT_KEY        1 +#define TB_EVENT_RESIZE     2 +#define TB_EVENT_MOUSE      3 + +/* Key modifiers (bitwise) (`tb_event.mod`) */ +#define TB_MOD_ALT          1 +#define TB_MOD_CTRL         2 +#define TB_MOD_SHIFT        4 +#define TB_MOD_MOTION       8 + +/* Input modes (bitwise) (`tb_set_input_mode`) */ +#define TB_INPUT_CURRENT    0 +#define TB_INPUT_ESC        1 +#define TB_INPUT_ALT        2 +#define TB_INPUT_MOUSE      4 + +/* Output modes (`tb_set_output_mode`) */ +#define TB_OUTPUT_CURRENT   0 +#define TB_OUTPUT_NORMAL    1 +#define TB_OUTPUT_256       2 +#define TB_OUTPUT_216       3 +#define TB_OUTPUT_GRAYSCALE 4 +#if TB_OPT_ATTR_W >= 32 +#define TB_OUTPUT_TRUECOLOR 5 +#endif + +/* Common function return values unless otherwise noted. + * + * Library behavior is undefined after receiving `TB_ERR_MEM`. Callers may + * attempt reinitializing by freeing memory, invoking `tb_shutdown`, then + * `tb_init`. + */ +#define TB_OK                   0 +#define TB_ERR                  -1 +#define TB_ERR_NEED_MORE        -2 +#define TB_ERR_INIT_ALREADY     -3 +#define TB_ERR_INIT_OPEN        -4 +#define TB_ERR_MEM              -5 +#define TB_ERR_NO_EVENT         -6 +#define TB_ERR_NO_TERM          -7 +#define TB_ERR_NOT_INIT         -8 +#define TB_ERR_OUT_OF_BOUNDS    -9 +#define TB_ERR_READ             -10 +#define TB_ERR_RESIZE_IOCTL     -11 +#define TB_ERR_RESIZE_PIPE      -12 +#define TB_ERR_RESIZE_SIGACTION -13 +#define TB_ERR_POLL             -14 +#define TB_ERR_TCGETATTR        -15 +#define TB_ERR_TCSETATTR        -16 +#define TB_ERR_UNSUPPORTED_TERM -17 +#define TB_ERR_RESIZE_WRITE     -18 +#define TB_ERR_RESIZE_POLL      -19 +#define TB_ERR_RESIZE_READ      -20 +#define TB_ERR_RESIZE_SSCANF    -21 +#define TB_ERR_CAP_COLLISION    -22 + +#define TB_ERR_SELECT           TB_ERR_POLL +#define TB_ERR_RESIZE_SELECT    TB_ERR_RESIZE_POLL + +/* Deprecated. Function types to be used with `tb_set_func`. */ +#define TB_FUNC_EXTRACT_PRE     0 +#define TB_FUNC_EXTRACT_POST    1 + +/* Define this to set the size of the buffer used in `tb_printf` + * and `tb_sendf` + */ +#ifndef TB_OPT_PRINTF_BUF +#define TB_OPT_PRINTF_BUF 4096 +#endif + +/* Define this to set the size of the read buffer used when reading + * from the tty + */ +#ifndef TB_OPT_READ_BUF +#define TB_OPT_READ_BUF 64 +#endif + +/* Define this for limited back compat with termbox v1 */ +#ifdef TB_OPT_V1_COMPAT +#define tb_change_cell          tb_set_cell +#define tb_put_cell(x, y, c)    tb_set_cell((x), (y), (c)->ch, (c)->fg, (c)->bg) +#define tb_set_clear_attributes tb_set_clear_attrs +#define tb_select_input_mode    tb_set_input_mode +#define tb_select_output_mode   tb_set_output_mode +#endif + +/* Define these to swap in a different allocator */ +#ifndef tb_malloc +#define tb_malloc  malloc +#define tb_realloc realloc +#define tb_free    free +#endif + +#if TB_OPT_ATTR_W == 64 +typedef uint64_t uintattr_t; +#elif TB_OPT_ATTR_W == 32 +typedef uint32_t uintattr_t; +#else // 16 +typedef uint16_t uintattr_t; +#endif + +/* A cell in a 2d grid representing the terminal screen. + * + * The terminal screen is represented as 2d array of cells. The structure is + * optimized for dealing with single-width (`wcwidth==1`) Unicode codepoints, + * however some support for grapheme clusters (e.g., combining diacritical + * marks) and wide codepoints (e.g., Hiragana) is provided through `ech`, + * `nech`, and `cech` via `tb_set_cell_ex`. `ech` is only valid when `nech>0`, + * otherwise `ch` is used. + * + * For non-single-width codepoints, given `N=wcwidth(ch)/wcswidth(ech)`: + * + * when `N==0`: termbox forces a single-width cell. Callers should avoid this + *              if aiming to render text accurately. Callers may use + *              `tb_set_cell_ex` or `tb_print*` to render `N==0` combining + *              characters. + * + *  when `N>1`: termbox zeroes out the following `N-1` cells and skips sending + *              them to the tty. So, e.g., if the caller sets `x=0,y=0` to an + *              `N==2` codepoint, the caller's next set should be at `x=2,y=0`. + *              Anything set at `x=1,y=0` will be ignored. If there are not + *              enough columns remaining on the line to render `N` width, spaces + *              are sent instead. + * + * See `tb_present` for implementation. + */ +struct tb_cell { +    uint32_t ch;   // a Unicode codepoint +    uintattr_t fg; // bitwise foreground attributes +    uintattr_t bg; // bitwise background attributes +#ifdef TB_OPT_EGC +    uint32_t *ech; // a grapheme cluster of Unicode codepoints, 0-terminated +    size_t nech;   // num elements in ech, 0 means use ch instead of ech +    size_t cech;   // num elements allocated for ech +#endif +}; + +/* An incoming event from the tty. + * + * Given the event type, the following fields are relevant: + * + *    when `TB_EVENT_KEY`: `key` xor `ch` (one will be zero) and `mod`. Note + *                         there is overlap between `TB_MOD_CTRL` and + *                         `TB_KEY_CTRL_*`. `TB_MOD_CTRL` and `TB_MOD_SHIFT` are + *                         only set as modifiers to `TB_KEY_ARROW_*`. + * + * when `TB_EVENT_RESIZE`: `w` and `h` + * + *  when `TB_EVENT_MOUSE`: `key` (`TB_KEY_MOUSE_*`), `x`, and `y` + */ +struct tb_event { +    uint8_t type; // one of `TB_EVENT_*` constants +    uint8_t mod;  // bitwise `TB_MOD_*` constants +    uint16_t key; // one of `TB_KEY_*` constants +    uint32_t ch;  // a Unicode codepoint +    int32_t w;    // resize width +    int32_t h;    // resize height +    int32_t x;    // mouse x +    int32_t y;    // mouse y +}; + +/* Initialize the termbox library. This function should be called before any + * other functions. `tb_init` is equivalent to `tb_init_file("/dev/tty")`. After + * successful initialization, the library must be finalized using `tb_shutdown`. + */ +int tb_init(void); +int tb_init_file(const char *path); +int tb_init_fd(int ttyfd); +int tb_init_rwfd(int rfd, int wfd); +int tb_shutdown(void); + +/* Return the size of the internal back buffer (which is the same as terminal's + * window size in rows and columns). The internal buffer can be resized after + * `tb_clear` or `tb_present` calls. Both dimensions have an unspecified + * negative value when called before `tb_init` or after `tb_shutdown`. + */ +int tb_width(void); +int tb_height(void); + +/* Clear the internal back buffer using `TB_DEFAULT` or the attributes set by + * `tb_set_clear_attrs`. + */ +int tb_clear(void); +int tb_set_clear_attrs(uintattr_t fg, uintattr_t bg); + +/* Synchronize the internal back buffer with the terminal by writing to tty. */ +int tb_present(void); + +/* 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. */ +int tb_invalidate(void); + +/* Set the position of the cursor. Upper-left cell is (0, 0). */ +int tb_set_cursor(int cx, int cy); +int tb_hide_cursor(void); + +/* Set cell contents in the internal back buffer at the specified position. + * + * Use `tb_set_cell_ex` for rendering grapheme clusters (e.g., combining + * diacritical marks). + * + * Calling `tb_set_cell(x, y, ch, fg, bg)` is equivalent to + * `tb_set_cell_ex(x, y, &ch, 1, fg, bg)`. + * + * `tb_extend_cell` is a shortcut for appending 1 codepoint to `tb_cell.ech`. + * + * Non-printable (`iswprint(3)`) codepoints are replaced with `U+FFFD` at render + * time. + */ +int tb_set_cell(int x, int y, uint32_t ch, uintattr_t fg, uintattr_t bg); +int tb_set_cell_ex(int x, int y, uint32_t *ch, size_t nch, uintattr_t fg, +    uintattr_t bg); +int tb_extend_cell(int x, int y, uint32_t ch); + +/* Set the input mode. Termbox has two input modes: + * + * 1. `TB_INPUT_ESC` + *    When escape (`\x1b`) is in the buffer and there's no match for an escape + *    sequence, a key event for `TB_KEY_ESC` is returned. + * + * 2. `TB_INPUT_ALT` + *    When escape (`\x1b`) is in the buffer and there's no match for an escape + *    sequence, the next keyboard event is returned with a `TB_MOD_ALT` + *    modifier. + * + * You can also apply `TB_INPUT_MOUSE` via bitwise OR operation to either of the + * modes (e.g., `TB_INPUT_ESC | TB_INPUT_MOUSE`) to receive `TB_EVENT_MOUSE` + * events. If none of the main two modes were set, but the mouse mode was, + * `TB_INPUT_ESC` is used. If for some reason you've decided to use + * `TB_INPUT_ESC | TB_INPUT_ALT`, it will behave as if only `TB_INPUT_ESC` was + * selected. + * + * If mode is `TB_INPUT_CURRENT`, return the current input mode. + * + * The default input mode is `TB_INPUT_ESC`. + */ +int tb_set_input_mode(int mode); + +/* Set the output mode. Termbox has multiple output modes: + * + * 1. `TB_OUTPUT_NORMAL`     => [0..8] + * + *    This mode provides 8 different colors: + *      `TB_BLACK`, `TB_RED`, `TB_GREEN`, `TB_YELLOW`, + *      `TB_BLUE`, `TB_MAGENTA`, `TB_CYAN`, `TB_WHITE` + * + *    Plus `TB_DEFAULT` which skips sending a color code (i.e., uses the + *    terminal's default color). + * + *    Colors (including `TB_DEFAULT`) may be bitwise OR'd with attributes: + *      `TB_BOLD`, `TB_UNDERLINE`, `TB_REVERSE`, `TB_ITALIC`, `TB_BLINK`, + *      `TB_BRIGHT`, `TB_DIM` + * + *    The following style attributes are also available if compiled with + *    `TB_OPT_ATTR_W` set to 64: + *      `TB_STRIKEOUT`, `TB_UNDERLINE_2`, `TB_OVERLINE`, `TB_INVISIBLE` + * + *    As in all modes, the value 0 is interpreted as `TB_DEFAULT` for + *    convenience. + * + *    Some notes: `TB_REVERSE` and `TB_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: `tb_set_cell(x, y, '@', TB_BLACK | TB_BOLD, TB_RED)` + * + * 2. `TB_OUTPUT_256`        => [0..255] + `TB_HI_BLACK` + * + *    In this mode you get 256 distinct colors (plus default): + *                0x00   (1): `TB_DEFAULT` + *       `TB_HI_BLACK`   (1): `TB_BLACK` in `TB_OUTPUT_NORMAL` + *          0x01..0x07   (7): the next 7 colors as in `TB_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 `TB_*` style attributes except `TB_BRIGHT` may be bitwise OR'd as in + *    `TB_OUTPUT_NORMAL`. + * + *    Note `TB_HI_BLACK` must be used for black, as 0x00 represents default. + * + * 3. `TB_OUTPUT_216`        => [0..216] + * + *    This mode supports the 216-color range of `TB_OUTPUT_256` only, but you + *    don't need to provide an offset: + *                0x00   (1): `TB_DEFAULT` + *          0x01..0xd8 (216): 216 different colors + * + * 4. `TB_OUTPUT_GRAYSCALE`  => [0..24] + * + *    This mode supports the 24-color range of `TB_OUTPUT_256` only, but you + *    don't need to provide an offset: + *                0x00   (1): `TB_DEFAULT` + *          0x01..0x18  (24): 24 different shades of gray + * + * 5. `TB_OUTPUT_TRUECOLOR`  => [0x000000..0xffffff] + `TB_HI_BLACK` + * + *    This mode provides 24-bit color on supported terminals. The format is + *    0xRRGGBB. + * + *    All `TB_*` style attributes except `TB_BRIGHT` may be bitwise OR'd as in + *    `TB_OUTPUT_NORMAL`. + * + *    Note `TB_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 + * `TB_DEFAULT`. For convenience, the value 0 is interpreted as `TB_DEFAULT` in + * all modes. + * + * Note, cell attributes persist after switching output modes. Any translation + * between, for example, `TB_OUTPUT_NORMAL`'s `TB_RED` and + * `TB_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 `tb_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 + * `TB_OUTPUT_NORMAL`. There is also no very reliable way to determine color + * support dynamically. If portability is desired, callers are recommended to + * use `TB_OUTPUT_NORMAL` or make output mode end-user configurable. The same + * advice applies to style attributes. + * + * If mode is `TB_OUTPUT_CURRENT`, return the current output mode. + * + * The default output mode is `TB_OUTPUT_NORMAL`. + */ +int tb_set_output_mode(int mode); + +/* Wait for an event up to `timeout_ms` milliseconds and populate `event` with + * it. If no event is available within the timeout period, `TB_ERR_NO_EVENT` + * is returned. On a resize event, the underlying `select(2)` call may be + * interrupted, yielding a return code of `TB_ERR_POLL`. In this case, you may + * check `errno` via `tb_last_errno`. If it's `EINTR`, you may elect to ignore + * that and call `tb_peek_event` again. + */ +int tb_peek_event(struct tb_event *event, int timeout_ms); + +/* Same as `tb_peek_event` except no timeout. */ +int tb_poll_event(struct tb_event *event); + +/* Internal termbox fds that can be used with `poll(2)`, `select(2)`, etc. + * externally. Callers must invoke `tb_poll_event` or `tb_peek_event` if + * fds become readable. */ +int tb_get_fds(int *ttyfd, int *resizefd); + +/* Print and printf functions. Specify param `out_w` to determine width of + * printed string. Strings are interpreted as UTF-8. + * + * Non-printable characters (`iswprint(3)`) and truncated UTF-8 byte sequences + * are replaced with U+FFFD. + * + * Newlines (`\n`) are supported with the caveat that `out_w` will return the + * width of the string as if it were on a single line. + * + * If the starting coordinate is out of bounds, `TB_ERR_OUT_OF_BOUNDS` is + * returned. If the starting coordinate is in bounds, but goes out of bounds, + * then the out-of-bounds portions of the string are ignored. + * + * For finer control, use `tb_set_cell`. + */ +int tb_print(int x, int y, uintattr_t fg, uintattr_t bg, const char *str); +int tb_printf(int x, int y, uintattr_t fg, uintattr_t bg, const char *fmt, ...); +int tb_print_ex(int x, int y, uintattr_t fg, uintattr_t bg, size_t *out_w, +    const char *str); +int tb_printf_ex(int x, int y, uintattr_t fg, uintattr_t bg, size_t *out_w, +    const char *fmt, ...); + +/* Send raw bytes to terminal. */ +int tb_send(const char *buf, size_t nbuf); +int tb_sendf(const char *fmt, ...); + +/* Deprecated. Set custom callbacks. `fn_type` is one of `TB_FUNC_*` constants, + * `fn` is a compatible function pointer, or NULL to clear. + * + * `TB_FUNC_EXTRACT_PRE`: + *   If specified, invoke this function BEFORE termbox tries to extract any + *   escape sequences from the input buffer. + * + * `TB_FUNC_EXTRACT_POST`: + *   If specified, invoke this function AFTER termbox tries (and fails) to + *   extract any escape sequences from the input buffer. + */ +int tb_set_func(int fn_type, int (*fn)(struct tb_event *, size_t *)); + +/* Return byte length of codepoint given first byte of UTF-8 sequence (1-6). */ +int tb_utf8_char_length(char c); + +/* Convert UTF-8 null-terminated byte sequence to UTF-32 codepoint. + * + * If `c` is an empty C string, return 0. `out` is left unchanged. + * + * If a null byte is encountered in the middle of the codepoint, return a + * negative number indicating how many bytes were processed. `out` is left + * unchanged. + * + * Otherwise, return byte length of codepoint (1-6). + */ +int tb_utf8_char_to_unicode(uint32_t *out, const char *c); + +/* Convert UTF-32 codepoint to UTF-8 null-terminated byte sequence. + * + * `out` must be char[7] or greater. Return byte length of codepoint (1-6). + */ +int tb_utf8_unicode_to_char(char *out, uint32_t c); + +/* Library utility functions */ +int tb_last_errno(void); +const char *tb_strerror(int err); +struct tb_cell *tb_cell_buffer(void); // Deprecated +int tb_has_truecolor(void); +int tb_has_egc(void); +int tb_attr_width(void); +const char *tb_version(void); + +/* Deprecation notice! + * + * The following will be removed in version 3.x (ABI version 3): + * + *   TB_256_BLACK           (use TB_HI_BLACK) + *   TB_OPT_TRUECOLOR       (use TB_OPT_ATTR_W) + *   TB_TRUECOLOR_BOLD      (use TB_BOLD) + *   TB_TRUECOLOR_UNDERLINE (use TB_UNDERLINE) + *   TB_TRUECOLOR_REVERSE   (use TB_REVERSE) + *   TB_TRUECOLOR_ITALIC    (use TB_ITALICe) + *   TB_TRUECOLOR_BLINK     (use TB_BLINK) + *   TB_TRUECOLOR_BLACK     (use TB_HI_BLACK) + *   tb_cell_buffer + *   tb_set_func + *   TB_FUNC_EXTRACT_PRE + *   TB_FUNC_EXTRACT_POST + */ + +#ifdef __cplusplus +} +#endif + +#endif // TERMBOX_H_INCL + +#ifdef TB_IMPL + +#define if_err_return(rv, expr)                                                \ +    if (((rv) = (expr)) != TB_OK) return (rv) +#define if_err_break(rv, expr)                                                 \ +    if (((rv) = (expr)) != TB_OK) break +#define if_ok_return(rv, expr)                                                 \ +    if (((rv) = (expr)) == TB_OK) return (rv) +#define if_ok_or_need_more_return(rv, expr)                                    \ +    if (((rv) = (expr)) == TB_OK || (rv) == TB_ERR_NEED_MORE) return (rv) + +#define send_literal(rv, a)                                                    \ +    if_err_return((rv), bytebuf_nputs(&global.out, (a), sizeof(a) - 1)) + +#define send_num(rv, nbuf, n)                                                  \ +    if_err_return((rv),                                                        \ +        bytebuf_nputs(&global.out, (nbuf), convert_num((n), (nbuf)))) + +#define snprintf_or_return(rv, str, sz, fmt, ...)                              \ +    do {                                                                       \ +        (rv) = snprintf((str), (sz), (fmt), __VA_ARGS__);                      \ +        if ((rv) < 0 || (rv) >= (int)(sz)) return TB_ERR;                      \ +    } while (0) + +#define if_not_init_return()                                                   \ +    if (!global.initialized) return TB_ERR_NOT_INIT + +struct bytebuf_t { +    char *buf; +    size_t len; +    size_t cap; +}; + +struct cellbuf_t { +    int width; +    int height; +    struct tb_cell *cells; +}; + +struct cap_trie_t { +    char c; +    struct cap_trie_t *children; +    size_t nchildren; +    int is_leaf; +    uint16_t key; +    uint8_t mod; +}; + +struct tb_global_t { +    int ttyfd; +    int rfd; +    int wfd; +    int ttyfd_open; +    int resize_pipefd[2]; +    int width; +    int height; +    int cursor_x; +    int cursor_y; +    int last_x; +    int last_y; +    uintattr_t fg; +    uintattr_t bg; +    uintattr_t last_fg; +    uintattr_t last_bg; +    int input_mode; +    int output_mode; +    char *terminfo; +    size_t nterminfo; +    const char *caps[TB_CAP__COUNT]; +    struct cap_trie_t cap_trie; +    struct bytebuf_t in; +    struct bytebuf_t out; +    struct cellbuf_t back; +    struct cellbuf_t front; +    struct termios orig_tios; +    int has_orig_tios; +    int last_errno; +    int initialized; +    int (*fn_extract_esc_pre)(struct tb_event *, size_t *); +    int (*fn_extract_esc_post)(struct tb_event *, size_t *); +    char errbuf[1024]; +}; + +static struct tb_global_t global = {0}; + +/* BEGIN codegen c */ +/* Produced by ./codegen.sh on Tue, 03 Sep 2024 04:17:48 +0000 */ + +static const int16_t terminfo_cap_indexes[] = { +    66,  // kf1 (TB_CAP_F1) +    68,  // kf2 (TB_CAP_F2) +    69,  // kf3 (TB_CAP_F3) +    70,  // kf4 (TB_CAP_F4) +    71,  // kf5 (TB_CAP_F5) +    72,  // kf6 (TB_CAP_F6) +    73,  // kf7 (TB_CAP_F7) +    74,  // kf8 (TB_CAP_F8) +    75,  // kf9 (TB_CAP_F9) +    67,  // kf10 (TB_CAP_F10) +    216, // kf11 (TB_CAP_F11) +    217, // kf12 (TB_CAP_F12) +    77,  // kich1 (TB_CAP_INSERT) +    59,  // kdch1 (TB_CAP_DELETE) +    76,  // khome (TB_CAP_HOME) +    164, // kend (TB_CAP_END) +    82,  // kpp (TB_CAP_PGUP) +    81,  // knp (TB_CAP_PGDN) +    87,  // kcuu1 (TB_CAP_ARROW_UP) +    61,  // kcud1 (TB_CAP_ARROW_DOWN) +    79,  // kcub1 (TB_CAP_ARROW_LEFT) +    83,  // kcuf1 (TB_CAP_ARROW_RIGHT) +    148, // kcbt (TB_CAP_BACK_TAB) +    28,  // smcup (TB_CAP_ENTER_CA) +    40,  // rmcup (TB_CAP_EXIT_CA) +    16,  // cnorm (TB_CAP_SHOW_CURSOR) +    13,  // civis (TB_CAP_HIDE_CURSOR) +    5,   // clear (TB_CAP_CLEAR_SCREEN) +    39,  // sgr0 (TB_CAP_SGR0) +    36,  // smul (TB_CAP_UNDERLINE) +    27,  // bold (TB_CAP_BOLD) +    26,  // blink (TB_CAP_BLINK) +    311, // sitm (TB_CAP_ITALIC) +    34,  // rev (TB_CAP_REVERSE) +    89,  // smkx (TB_CAP_ENTER_KEYPAD) +    88,  // rmkx (TB_CAP_EXIT_KEYPAD) +    30,  // dim (TB_CAP_DIM) +    32,  // invis (TB_CAP_INVISIBLE) +}; + +// xterm +static const char *xterm_caps[] = { +    "\033OP",                  // kf1 (TB_CAP_F1) +    "\033OQ",                  // kf2 (TB_CAP_F2) +    "\033OR",                  // kf3 (TB_CAP_F3) +    "\033OS",                  // kf4 (TB_CAP_F4) +    "\033[15~",                // kf5 (TB_CAP_F5) +    "\033[17~",                // kf6 (TB_CAP_F6) +    "\033[18~",                // kf7 (TB_CAP_F7) +    "\033[19~",                // kf8 (TB_CAP_F8) +    "\033[20~",                // kf9 (TB_CAP_F9) +    "\033[21~",                // kf10 (TB_CAP_F10) +    "\033[23~",                // kf11 (TB_CAP_F11) +    "\033[24~",                // kf12 (TB_CAP_F12) +    "\033[2~",                 // kich1 (TB_CAP_INSERT) +    "\033[3~",                 // kdch1 (TB_CAP_DELETE) +    "\033OH",                  // khome (TB_CAP_HOME) +    "\033OF",                  // kend (TB_CAP_END) +    "\033[5~",                 // kpp (TB_CAP_PGUP) +    "\033[6~",                 // knp (TB_CAP_PGDN) +    "\033OA",                  // kcuu1 (TB_CAP_ARROW_UP) +    "\033OB",                  // kcud1 (TB_CAP_ARROW_DOWN) +    "\033OD",                  // kcub1 (TB_CAP_ARROW_LEFT) +    "\033OC",                  // kcuf1 (TB_CAP_ARROW_RIGHT) +    "\033[Z",                  // kcbt (TB_CAP_BACK_TAB) +    "\033[?1049h\033[22;0;0t", // smcup (TB_CAP_ENTER_CA) +    "\033[?1049l\033[23;0;0t", // rmcup (TB_CAP_EXIT_CA) +    "\033[?12l\033[?25h",      // cnorm (TB_CAP_SHOW_CURSOR) +    "\033[?25l",               // civis (TB_CAP_HIDE_CURSOR) +    "\033[H\033[2J",           // clear (TB_CAP_CLEAR_SCREEN) +    "\033(B\033[m",            // sgr0 (TB_CAP_SGR0) +    "\033[4m",                 // smul (TB_CAP_UNDERLINE) +    "\033[1m",                 // bold (TB_CAP_BOLD) +    "\033[5m",                 // blink (TB_CAP_BLINK) +    "\033[3m",                 // sitm (TB_CAP_ITALIC) +    "\033[7m",                 // rev (TB_CAP_REVERSE) +    "\033[?1h\033=",           // smkx (TB_CAP_ENTER_KEYPAD) +    "\033[?1l\033>",           // rmkx (TB_CAP_EXIT_KEYPAD) +    "\033[2m",                 // dim (TB_CAP_DIM) +    "\033[8m",                 // invis (TB_CAP_INVISIBLE) +}; + +// linux +static const char *linux_caps[] = { +    "\033[[A",           // kf1 (TB_CAP_F1) +    "\033[[B",           // kf2 (TB_CAP_F2) +    "\033[[C",           // kf3 (TB_CAP_F3) +    "\033[[D",           // kf4 (TB_CAP_F4) +    "\033[[E",           // kf5 (TB_CAP_F5) +    "\033[17~",          // kf6 (TB_CAP_F6) +    "\033[18~",          // kf7 (TB_CAP_F7) +    "\033[19~",          // kf8 (TB_CAP_F8) +    "\033[20~",          // kf9 (TB_CAP_F9) +    "\033[21~",          // kf10 (TB_CAP_F10) +    "\033[23~",          // kf11 (TB_CAP_F11) +    "\033[24~",          // kf12 (TB_CAP_F12) +    "\033[2~",           // kich1 (TB_CAP_INSERT) +    "\033[3~",           // kdch1 (TB_CAP_DELETE) +    "\033[1~",           // khome (TB_CAP_HOME) +    "\033[4~",           // kend (TB_CAP_END) +    "\033[5~",           // kpp (TB_CAP_PGUP) +    "\033[6~",           // knp (TB_CAP_PGDN) +    "\033[A",            // kcuu1 (TB_CAP_ARROW_UP) +    "\033[B",            // kcud1 (TB_CAP_ARROW_DOWN) +    "\033[D",            // kcub1 (TB_CAP_ARROW_LEFT) +    "\033[C",            // kcuf1 (TB_CAP_ARROW_RIGHT) +    "\033\011",          // kcbt (TB_CAP_BACK_TAB) +    "",                  // smcup (TB_CAP_ENTER_CA) +    "",                  // rmcup (TB_CAP_EXIT_CA) +    "\033[?25h\033[?0c", // cnorm (TB_CAP_SHOW_CURSOR) +    "\033[?25l\033[?1c", // civis (TB_CAP_HIDE_CURSOR) +    "\033[H\033[J",      // clear (TB_CAP_CLEAR_SCREEN) +    "\033[m\017",        // sgr0 (TB_CAP_SGR0) +    "\033[4m",           // smul (TB_CAP_UNDERLINE) +    "\033[1m",           // bold (TB_CAP_BOLD) +    "\033[5m",           // blink (TB_CAP_BLINK) +    "",                  // sitm (TB_CAP_ITALIC) +    "\033[7m",           // rev (TB_CAP_REVERSE) +    "",                  // smkx (TB_CAP_ENTER_KEYPAD) +    "",                  // rmkx (TB_CAP_EXIT_KEYPAD) +    "\033[2m",           // dim (TB_CAP_DIM) +    "",                  // invis (TB_CAP_INVISIBLE) +}; + +// screen +static const char *screen_caps[] = { +    "\033OP",            // kf1 (TB_CAP_F1) +    "\033OQ",            // kf2 (TB_CAP_F2) +    "\033OR",            // kf3 (TB_CAP_F3) +    "\033OS",            // kf4 (TB_CAP_F4) +    "\033[15~",          // kf5 (TB_CAP_F5) +    "\033[17~",          // kf6 (TB_CAP_F6) +    "\033[18~",          // kf7 (TB_CAP_F7) +    "\033[19~",          // kf8 (TB_CAP_F8) +    "\033[20~",          // kf9 (TB_CAP_F9) +    "\033[21~",          // kf10 (TB_CAP_F10) +    "\033[23~",          // kf11 (TB_CAP_F11) +    "\033[24~",          // kf12 (TB_CAP_F12) +    "\033[2~",           // kich1 (TB_CAP_INSERT) +    "\033[3~",           // kdch1 (TB_CAP_DELETE) +    "\033[1~",           // khome (TB_CAP_HOME) +    "\033[4~",           // kend (TB_CAP_END) +    "\033[5~",           // kpp (TB_CAP_PGUP) +    "\033[6~",           // knp (TB_CAP_PGDN) +    "\033OA",            // kcuu1 (TB_CAP_ARROW_UP) +    "\033OB",            // kcud1 (TB_CAP_ARROW_DOWN) +    "\033OD",            // kcub1 (TB_CAP_ARROW_LEFT) +    "\033OC",            // kcuf1 (TB_CAP_ARROW_RIGHT) +    "\033[Z",            // kcbt (TB_CAP_BACK_TAB) +    "\033[?1049h",       // smcup (TB_CAP_ENTER_CA) +    "\033[?1049l",       // rmcup (TB_CAP_EXIT_CA) +    "\033[34h\033[?25h", // cnorm (TB_CAP_SHOW_CURSOR) +    "\033[?25l",         // civis (TB_CAP_HIDE_CURSOR) +    "\033[H\033[J",      // clear (TB_CAP_CLEAR_SCREEN) +    "\033[m\017",        // sgr0 (TB_CAP_SGR0) +    "\033[4m",           // smul (TB_CAP_UNDERLINE) +    "\033[1m",           // bold (TB_CAP_BOLD) +    "\033[5m",           // blink (TB_CAP_BLINK) +    "",                  // sitm (TB_CAP_ITALIC) +    "\033[7m",           // rev (TB_CAP_REVERSE) +    "\033[?1h\033=",     // smkx (TB_CAP_ENTER_KEYPAD) +    "\033[?1l\033>",     // rmkx (TB_CAP_EXIT_KEYPAD) +    "\033[2m",           // dim (TB_CAP_DIM) +    "",                  // invis (TB_CAP_INVISIBLE) +}; + +// rxvt-256color +static const char *rxvt_256color_caps[] = { +    "\033[11~",              // kf1 (TB_CAP_F1) +    "\033[12~",              // kf2 (TB_CAP_F2) +    "\033[13~",              // kf3 (TB_CAP_F3) +    "\033[14~",              // kf4 (TB_CAP_F4) +    "\033[15~",              // kf5 (TB_CAP_F5) +    "\033[17~",              // kf6 (TB_CAP_F6) +    "\033[18~",              // kf7 (TB_CAP_F7) +    "\033[19~",              // kf8 (TB_CAP_F8) +    "\033[20~",              // kf9 (TB_CAP_F9) +    "\033[21~",              // kf10 (TB_CAP_F10) +    "\033[23~",              // kf11 (TB_CAP_F11) +    "\033[24~",              // kf12 (TB_CAP_F12) +    "\033[2~",               // kich1 (TB_CAP_INSERT) +    "\033[3~",               // kdch1 (TB_CAP_DELETE) +    "\033[7~",               // khome (TB_CAP_HOME) +    "\033[8~",               // kend (TB_CAP_END) +    "\033[5~",               // kpp (TB_CAP_PGUP) +    "\033[6~",               // knp (TB_CAP_PGDN) +    "\033[A",                // kcuu1 (TB_CAP_ARROW_UP) +    "\033[B",                // kcud1 (TB_CAP_ARROW_DOWN) +    "\033[D",                // kcub1 (TB_CAP_ARROW_LEFT) +    "\033[C",                // kcuf1 (TB_CAP_ARROW_RIGHT) +    "\033[Z",                // kcbt (TB_CAP_BACK_TAB) +    "\0337\033[?47h",        // smcup (TB_CAP_ENTER_CA) +    "\033[2J\033[?47l\0338", // rmcup (TB_CAP_EXIT_CA) +    "\033[?25h",             // cnorm (TB_CAP_SHOW_CURSOR) +    "\033[?25l",             // civis (TB_CAP_HIDE_CURSOR) +    "\033[H\033[2J",         // clear (TB_CAP_CLEAR_SCREEN) +    "\033[m\017",            // sgr0 (TB_CAP_SGR0) +    "\033[4m",               // smul (TB_CAP_UNDERLINE) +    "\033[1m",               // bold (TB_CAP_BOLD) +    "\033[5m",               // blink (TB_CAP_BLINK) +    "",                      // sitm (TB_CAP_ITALIC) +    "\033[7m",               // rev (TB_CAP_REVERSE) +    "\033=",                 // smkx (TB_CAP_ENTER_KEYPAD) +    "\033>",                 // rmkx (TB_CAP_EXIT_KEYPAD) +    "",                      // dim (TB_CAP_DIM) +    "",                      // invis (TB_CAP_INVISIBLE) +}; + +// rxvt-unicode +static const char *rxvt_unicode_caps[] = { +    "\033[11~",           // kf1 (TB_CAP_F1) +    "\033[12~",           // kf2 (TB_CAP_F2) +    "\033[13~",           // kf3 (TB_CAP_F3) +    "\033[14~",           // kf4 (TB_CAP_F4) +    "\033[15~",           // kf5 (TB_CAP_F5) +    "\033[17~",           // kf6 (TB_CAP_F6) +    "\033[18~",           // kf7 (TB_CAP_F7) +    "\033[19~",           // kf8 (TB_CAP_F8) +    "\033[20~",           // kf9 (TB_CAP_F9) +    "\033[21~",           // kf10 (TB_CAP_F10) +    "\033[23~",           // kf11 (TB_CAP_F11) +    "\033[24~",           // kf12 (TB_CAP_F12) +    "\033[2~",            // kich1 (TB_CAP_INSERT) +    "\033[3~",            // kdch1 (TB_CAP_DELETE) +    "\033[7~",            // khome (TB_CAP_HOME) +    "\033[8~",            // kend (TB_CAP_END) +    "\033[5~",            // kpp (TB_CAP_PGUP) +    "\033[6~",            // knp (TB_CAP_PGDN) +    "\033[A",             // kcuu1 (TB_CAP_ARROW_UP) +    "\033[B",             // kcud1 (TB_CAP_ARROW_DOWN) +    "\033[D",             // kcub1 (TB_CAP_ARROW_LEFT) +    "\033[C",             // kcuf1 (TB_CAP_ARROW_RIGHT) +    "\033[Z",             // kcbt (TB_CAP_BACK_TAB) +    "\033[?1049h",        // smcup (TB_CAP_ENTER_CA) +    "\033[r\033[?1049l",  // rmcup (TB_CAP_EXIT_CA) +    "\033[?12l\033[?25h", // cnorm (TB_CAP_SHOW_CURSOR) +    "\033[?25l",          // civis (TB_CAP_HIDE_CURSOR) +    "\033[H\033[2J",      // clear (TB_CAP_CLEAR_SCREEN) +    "\033[m\033(B",       // sgr0 (TB_CAP_SGR0) +    "\033[4m",            // smul (TB_CAP_UNDERLINE) +    "\033[1m",            // bold (TB_CAP_BOLD) +    "\033[5m",            // blink (TB_CAP_BLINK) +    "\033[3m",            // sitm (TB_CAP_ITALIC) +    "\033[7m",            // rev (TB_CAP_REVERSE) +    "\033=",              // smkx (TB_CAP_ENTER_KEYPAD) +    "\033>",              // rmkx (TB_CAP_EXIT_KEYPAD) +    "",                   // dim (TB_CAP_DIM) +    "",                   // invis (TB_CAP_INVISIBLE) +}; + +// Eterm +static const char *eterm_caps[] = { +    "\033[11~",              // kf1 (TB_CAP_F1) +    "\033[12~",              // kf2 (TB_CAP_F2) +    "\033[13~",              // kf3 (TB_CAP_F3) +    "\033[14~",              // kf4 (TB_CAP_F4) +    "\033[15~",              // kf5 (TB_CAP_F5) +    "\033[17~",              // kf6 (TB_CAP_F6) +    "\033[18~",              // kf7 (TB_CAP_F7) +    "\033[19~",              // kf8 (TB_CAP_F8) +    "\033[20~",              // kf9 (TB_CAP_F9) +    "\033[21~",              // kf10 (TB_CAP_F10) +    "\033[23~",              // kf11 (TB_CAP_F11) +    "\033[24~",              // kf12 (TB_CAP_F12) +    "\033[2~",               // kich1 (TB_CAP_INSERT) +    "\033[3~",               // kdch1 (TB_CAP_DELETE) +    "\033[7~",               // khome (TB_CAP_HOME) +    "\033[8~",               // kend (TB_CAP_END) +    "\033[5~",               // kpp (TB_CAP_PGUP) +    "\033[6~",               // knp (TB_CAP_PGDN) +    "\033[A",                // kcuu1 (TB_CAP_ARROW_UP) +    "\033[B",                // kcud1 (TB_CAP_ARROW_DOWN) +    "\033[D",                // kcub1 (TB_CAP_ARROW_LEFT) +    "\033[C",                // kcuf1 (TB_CAP_ARROW_RIGHT) +    "",                      // kcbt (TB_CAP_BACK_TAB) +    "\0337\033[?47h",        // smcup (TB_CAP_ENTER_CA) +    "\033[2J\033[?47l\0338", // rmcup (TB_CAP_EXIT_CA) +    "\033[?25h",             // cnorm (TB_CAP_SHOW_CURSOR) +    "\033[?25l",             // civis (TB_CAP_HIDE_CURSOR) +    "\033[H\033[2J",         // clear (TB_CAP_CLEAR_SCREEN) +    "\033[m\017",            // sgr0 (TB_CAP_SGR0) +    "\033[4m",               // smul (TB_CAP_UNDERLINE) +    "\033[1m",               // bold (TB_CAP_BOLD) +    "\033[5m",               // blink (TB_CAP_BLINK) +    "",                      // sitm (TB_CAP_ITALIC) +    "\033[7m",               // rev (TB_CAP_REVERSE) +    "",                      // smkx (TB_CAP_ENTER_KEYPAD) +    "",                      // rmkx (TB_CAP_EXIT_KEYPAD) +    "",                      // dim (TB_CAP_DIM) +    "",                      // invis (TB_CAP_INVISIBLE) +}; + +static struct { +    const char *name; +    const char **caps; +    const char *alias; +} builtin_terms[] = { +    {"xterm",         xterm_caps,         ""    }, +    {"linux",         linux_caps,         ""    }, +    {"screen",        screen_caps,        "tmux"}, +    {"rxvt-256color", rxvt_256color_caps, ""    }, +    {"rxvt-unicode",  rxvt_unicode_caps,  "rxvt"}, +    {"Eterm",         eterm_caps,         ""    }, +    {NULL,            NULL,               NULL  }, +}; + +/* END codegen c */ + +static struct { +    const char *cap; +    const uint16_t key; +    const uint8_t mod; +} builtin_mod_caps[] = { +  // xterm arrows +    {"\x1b[1;2A",    TB_KEY_ARROW_UP,    TB_MOD_SHIFT                           }, +    {"\x1b[1;3A",    TB_KEY_ARROW_UP,    TB_MOD_ALT                             }, +    {"\x1b[1;4A",    TB_KEY_ARROW_UP,    TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[1;5A",    TB_KEY_ARROW_UP,    TB_MOD_CTRL                            }, +    {"\x1b[1;6A",    TB_KEY_ARROW_UP,    TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[1;7A",    TB_KEY_ARROW_UP,    TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b[1;8A",    TB_KEY_ARROW_UP,    TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, + +    {"\x1b[1;2B",    TB_KEY_ARROW_DOWN,  TB_MOD_SHIFT                           }, +    {"\x1b[1;3B",    TB_KEY_ARROW_DOWN,  TB_MOD_ALT                             }, +    {"\x1b[1;4B",    TB_KEY_ARROW_DOWN,  TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[1;5B",    TB_KEY_ARROW_DOWN,  TB_MOD_CTRL                            }, +    {"\x1b[1;6B",    TB_KEY_ARROW_DOWN,  TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[1;7B",    TB_KEY_ARROW_DOWN,  TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b[1;8B",    TB_KEY_ARROW_DOWN,  TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, + +    {"\x1b[1;2C",    TB_KEY_ARROW_RIGHT, TB_MOD_SHIFT                           }, +    {"\x1b[1;3C",    TB_KEY_ARROW_RIGHT, TB_MOD_ALT                             }, +    {"\x1b[1;4C",    TB_KEY_ARROW_RIGHT, TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[1;5C",    TB_KEY_ARROW_RIGHT, TB_MOD_CTRL                            }, +    {"\x1b[1;6C",    TB_KEY_ARROW_RIGHT, TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[1;7C",    TB_KEY_ARROW_RIGHT, TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b[1;8C",    TB_KEY_ARROW_RIGHT, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, + +    {"\x1b[1;2D",    TB_KEY_ARROW_LEFT,  TB_MOD_SHIFT                           }, +    {"\x1b[1;3D",    TB_KEY_ARROW_LEFT,  TB_MOD_ALT                             }, +    {"\x1b[1;4D",    TB_KEY_ARROW_LEFT,  TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[1;5D",    TB_KEY_ARROW_LEFT,  TB_MOD_CTRL                            }, +    {"\x1b[1;6D",    TB_KEY_ARROW_LEFT,  TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[1;7D",    TB_KEY_ARROW_LEFT,  TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b[1;8D",    TB_KEY_ARROW_LEFT,  TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, + + // xterm keys +    {"\x1b[1;2H",    TB_KEY_HOME,        TB_MOD_SHIFT                           }, +    {"\x1b[1;3H",    TB_KEY_HOME,        TB_MOD_ALT                             }, +    {"\x1b[1;4H",    TB_KEY_HOME,        TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[1;5H",    TB_KEY_HOME,        TB_MOD_CTRL                            }, +    {"\x1b[1;6H",    TB_KEY_HOME,        TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[1;7H",    TB_KEY_HOME,        TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b[1;8H",    TB_KEY_HOME,        TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, + +    {"\x1b[1;2F",    TB_KEY_END,         TB_MOD_SHIFT                           }, +    {"\x1b[1;3F",    TB_KEY_END,         TB_MOD_ALT                             }, +    {"\x1b[1;4F",    TB_KEY_END,         TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[1;5F",    TB_KEY_END,         TB_MOD_CTRL                            }, +    {"\x1b[1;6F",    TB_KEY_END,         TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[1;7F",    TB_KEY_END,         TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b[1;8F",    TB_KEY_END,         TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, + +    {"\x1b[2;2~",    TB_KEY_INSERT,      TB_MOD_SHIFT                           }, +    {"\x1b[2;3~",    TB_KEY_INSERT,      TB_MOD_ALT                             }, +    {"\x1b[2;4~",    TB_KEY_INSERT,      TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[2;5~",    TB_KEY_INSERT,      TB_MOD_CTRL                            }, +    {"\x1b[2;6~",    TB_KEY_INSERT,      TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[2;7~",    TB_KEY_INSERT,      TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b[2;8~",    TB_KEY_INSERT,      TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, + +    {"\x1b[3;2~",    TB_KEY_DELETE,      TB_MOD_SHIFT                           }, +    {"\x1b[3;3~",    TB_KEY_DELETE,      TB_MOD_ALT                             }, +    {"\x1b[3;4~",    TB_KEY_DELETE,      TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[3;5~",    TB_KEY_DELETE,      TB_MOD_CTRL                            }, +    {"\x1b[3;6~",    TB_KEY_DELETE,      TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[3;7~",    TB_KEY_DELETE,      TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b[3;8~",    TB_KEY_DELETE,      TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, + +    {"\x1b[5;2~",    TB_KEY_PGUP,        TB_MOD_SHIFT                           }, +    {"\x1b[5;3~",    TB_KEY_PGUP,        TB_MOD_ALT                             }, +    {"\x1b[5;4~",    TB_KEY_PGUP,        TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[5;5~",    TB_KEY_PGUP,        TB_MOD_CTRL                            }, +    {"\x1b[5;6~",    TB_KEY_PGUP,        TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[5;7~",    TB_KEY_PGUP,        TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b[5;8~",    TB_KEY_PGUP,        TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, + +    {"\x1b[6;2~",    TB_KEY_PGDN,        TB_MOD_SHIFT                           }, +    {"\x1b[6;3~",    TB_KEY_PGDN,        TB_MOD_ALT                             }, +    {"\x1b[6;4~",    TB_KEY_PGDN,        TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[6;5~",    TB_KEY_PGDN,        TB_MOD_CTRL                            }, +    {"\x1b[6;6~",    TB_KEY_PGDN,        TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[6;7~",    TB_KEY_PGDN,        TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b[6;8~",    TB_KEY_PGDN,        TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, + +    {"\x1b[1;2P",    TB_KEY_F1,          TB_MOD_SHIFT                           }, +    {"\x1b[1;3P",    TB_KEY_F1,          TB_MOD_ALT                             }, +    {"\x1b[1;4P",    TB_KEY_F1,          TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[1;5P",    TB_KEY_F1,          TB_MOD_CTRL                            }, +    {"\x1b[1;6P",    TB_KEY_F1,          TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[1;7P",    TB_KEY_F1,          TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b[1;8P",    TB_KEY_F1,          TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, + +    {"\x1b[1;2Q",    TB_KEY_F2,          TB_MOD_SHIFT                           }, +    {"\x1b[1;3Q",    TB_KEY_F2,          TB_MOD_ALT                             }, +    {"\x1b[1;4Q",    TB_KEY_F2,          TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[1;5Q",    TB_KEY_F2,          TB_MOD_CTRL                            }, +    {"\x1b[1;6Q",    TB_KEY_F2,          TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[1;7Q",    TB_KEY_F2,          TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b[1;8Q",    TB_KEY_F2,          TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, + +    {"\x1b[1;2R",    TB_KEY_F3,          TB_MOD_SHIFT                           }, +    {"\x1b[1;3R",    TB_KEY_F3,          TB_MOD_ALT                             }, +    {"\x1b[1;4R",    TB_KEY_F3,          TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[1;5R",    TB_KEY_F3,          TB_MOD_CTRL                            }, +    {"\x1b[1;6R",    TB_KEY_F3,          TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[1;7R",    TB_KEY_F3,          TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b[1;8R",    TB_KEY_F3,          TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, + +    {"\x1b[1;2S",    TB_KEY_F4,          TB_MOD_SHIFT                           }, +    {"\x1b[1;3S",    TB_KEY_F4,          TB_MOD_ALT                             }, +    {"\x1b[1;4S",    TB_KEY_F4,          TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[1;5S",    TB_KEY_F4,          TB_MOD_CTRL                            }, +    {"\x1b[1;6S",    TB_KEY_F4,          TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[1;7S",    TB_KEY_F4,          TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b[1;8S",    TB_KEY_F4,          TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, + +    {"\x1b[15;2~",   TB_KEY_F5,          TB_MOD_SHIFT                           }, +    {"\x1b[15;3~",   TB_KEY_F5,          TB_MOD_ALT                             }, +    {"\x1b[15;4~",   TB_KEY_F5,          TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[15;5~",   TB_KEY_F5,          TB_MOD_CTRL                            }, +    {"\x1b[15;6~",   TB_KEY_F5,          TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[15;7~",   TB_KEY_F5,          TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b[15;8~",   TB_KEY_F5,          TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, + +    {"\x1b[17;2~",   TB_KEY_F6,          TB_MOD_SHIFT                           }, +    {"\x1b[17;3~",   TB_KEY_F6,          TB_MOD_ALT                             }, +    {"\x1b[17;4~",   TB_KEY_F6,          TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[17;5~",   TB_KEY_F6,          TB_MOD_CTRL                            }, +    {"\x1b[17;6~",   TB_KEY_F6,          TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[17;7~",   TB_KEY_F6,          TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b[17;8~",   TB_KEY_F6,          TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, + +    {"\x1b[18;2~",   TB_KEY_F7,          TB_MOD_SHIFT                           }, +    {"\x1b[18;3~",   TB_KEY_F7,          TB_MOD_ALT                             }, +    {"\x1b[18;4~",   TB_KEY_F7,          TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[18;5~",   TB_KEY_F7,          TB_MOD_CTRL                            }, +    {"\x1b[18;6~",   TB_KEY_F7,          TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[18;7~",   TB_KEY_F7,          TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b[18;8~",   TB_KEY_F7,          TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, + +    {"\x1b[19;2~",   TB_KEY_F8,          TB_MOD_SHIFT                           }, +    {"\x1b[19;3~",   TB_KEY_F8,          TB_MOD_ALT                             }, +    {"\x1b[19;4~",   TB_KEY_F8,          TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[19;5~",   TB_KEY_F8,          TB_MOD_CTRL                            }, +    {"\x1b[19;6~",   TB_KEY_F8,          TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[19;7~",   TB_KEY_F8,          TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b[19;8~",   TB_KEY_F8,          TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, + +    {"\x1b[20;2~",   TB_KEY_F9,          TB_MOD_SHIFT                           }, +    {"\x1b[20;3~",   TB_KEY_F9,          TB_MOD_ALT                             }, +    {"\x1b[20;4~",   TB_KEY_F9,          TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[20;5~",   TB_KEY_F9,          TB_MOD_CTRL                            }, +    {"\x1b[20;6~",   TB_KEY_F9,          TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[20;7~",   TB_KEY_F9,          TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b[20;8~",   TB_KEY_F9,          TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, + +    {"\x1b[21;2~",   TB_KEY_F10,         TB_MOD_SHIFT                           }, +    {"\x1b[21;3~",   TB_KEY_F10,         TB_MOD_ALT                             }, +    {"\x1b[21;4~",   TB_KEY_F10,         TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[21;5~",   TB_KEY_F10,         TB_MOD_CTRL                            }, +    {"\x1b[21;6~",   TB_KEY_F10,         TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[21;7~",   TB_KEY_F10,         TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b[21;8~",   TB_KEY_F10,         TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, + +    {"\x1b[23;2~",   TB_KEY_F11,         TB_MOD_SHIFT                           }, +    {"\x1b[23;3~",   TB_KEY_F11,         TB_MOD_ALT                             }, +    {"\x1b[23;4~",   TB_KEY_F11,         TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[23;5~",   TB_KEY_F11,         TB_MOD_CTRL                            }, +    {"\x1b[23;6~",   TB_KEY_F11,         TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[23;7~",   TB_KEY_F11,         TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b[23;8~",   TB_KEY_F11,         TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, + +    {"\x1b[24;2~",   TB_KEY_F12,         TB_MOD_SHIFT                           }, +    {"\x1b[24;3~",   TB_KEY_F12,         TB_MOD_ALT                             }, +    {"\x1b[24;4~",   TB_KEY_F12,         TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[24;5~",   TB_KEY_F12,         TB_MOD_CTRL                            }, +    {"\x1b[24;6~",   TB_KEY_F12,         TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[24;7~",   TB_KEY_F12,         TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b[24;8~",   TB_KEY_F12,         TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, + + // rxvt arrows +    {"\x1b[a",       TB_KEY_ARROW_UP,    TB_MOD_SHIFT                           }, +    {"\x1b\x1b[A",   TB_KEY_ARROW_UP,    TB_MOD_ALT                             }, +    {"\x1b\x1b[a",   TB_KEY_ARROW_UP,    TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1bOa",       TB_KEY_ARROW_UP,    TB_MOD_CTRL                            }, +    {"\x1b\x1bOa",   TB_KEY_ARROW_UP,    TB_MOD_CTRL | TB_MOD_ALT               }, + +    {"\x1b[b",       TB_KEY_ARROW_DOWN,  TB_MOD_SHIFT                           }, +    {"\x1b\x1b[B",   TB_KEY_ARROW_DOWN,  TB_MOD_ALT                             }, +    {"\x1b\x1b[b",   TB_KEY_ARROW_DOWN,  TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1bOb",       TB_KEY_ARROW_DOWN,  TB_MOD_CTRL                            }, +    {"\x1b\x1bOb",   TB_KEY_ARROW_DOWN,  TB_MOD_CTRL | TB_MOD_ALT               }, + +    {"\x1b[c",       TB_KEY_ARROW_RIGHT, TB_MOD_SHIFT                           }, +    {"\x1b\x1b[C",   TB_KEY_ARROW_RIGHT, TB_MOD_ALT                             }, +    {"\x1b\x1b[c",   TB_KEY_ARROW_RIGHT, TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1bOc",       TB_KEY_ARROW_RIGHT, TB_MOD_CTRL                            }, +    {"\x1b\x1bOc",   TB_KEY_ARROW_RIGHT, TB_MOD_CTRL | TB_MOD_ALT               }, + +    {"\x1b[d",       TB_KEY_ARROW_LEFT,  TB_MOD_SHIFT                           }, +    {"\x1b\x1b[D",   TB_KEY_ARROW_LEFT,  TB_MOD_ALT                             }, +    {"\x1b\x1b[d",   TB_KEY_ARROW_LEFT,  TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1bOd",       TB_KEY_ARROW_LEFT,  TB_MOD_CTRL                            }, +    {"\x1b\x1bOd",   TB_KEY_ARROW_LEFT,  TB_MOD_CTRL | TB_MOD_ALT               }, + + // rxvt keys +    {"\x1b[7$",      TB_KEY_HOME,        TB_MOD_SHIFT                           }, +    {"\x1b\x1b[7~",  TB_KEY_HOME,        TB_MOD_ALT                             }, +    {"\x1b\x1b[7$",  TB_KEY_HOME,        TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[7^",      TB_KEY_HOME,        TB_MOD_CTRL                            }, +    {"\x1b[7@",      TB_KEY_HOME,        TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b\x1b[7^",  TB_KEY_HOME,        TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b\x1b[7@",  TB_KEY_HOME,        TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, + +    {"\x1b\x1b[8~",  TB_KEY_END,         TB_MOD_ALT                             }, +    {"\x1b\x1b[8$",  TB_KEY_END,         TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[8^",      TB_KEY_END,         TB_MOD_CTRL                            }, +    {"\x1b\x1b[8^",  TB_KEY_END,         TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b\x1b[8@",  TB_KEY_END,         TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, +    {"\x1b[8@",      TB_KEY_END,         TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[8$",      TB_KEY_END,         TB_MOD_SHIFT                           }, + +    {"\x1b\x1b[2~",  TB_KEY_INSERT,      TB_MOD_ALT                             }, +    {"\x1b\x1b[2$",  TB_KEY_INSERT,      TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[2^",      TB_KEY_INSERT,      TB_MOD_CTRL                            }, +    {"\x1b\x1b[2^",  TB_KEY_INSERT,      TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b\x1b[2@",  TB_KEY_INSERT,      TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, +    {"\x1b[2@",      TB_KEY_INSERT,      TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[2$",      TB_KEY_INSERT,      TB_MOD_SHIFT                           }, + +    {"\x1b\x1b[3~",  TB_KEY_DELETE,      TB_MOD_ALT                             }, +    {"\x1b\x1b[3$",  TB_KEY_DELETE,      TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[3^",      TB_KEY_DELETE,      TB_MOD_CTRL                            }, +    {"\x1b\x1b[3^",  TB_KEY_DELETE,      TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b\x1b[3@",  TB_KEY_DELETE,      TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, +    {"\x1b[3@",      TB_KEY_DELETE,      TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[3$",      TB_KEY_DELETE,      TB_MOD_SHIFT                           }, + +    {"\x1b\x1b[5~",  TB_KEY_PGUP,        TB_MOD_ALT                             }, +    {"\x1b\x1b[5$",  TB_KEY_PGUP,        TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[5^",      TB_KEY_PGUP,        TB_MOD_CTRL                            }, +    {"\x1b\x1b[5^",  TB_KEY_PGUP,        TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b\x1b[5@",  TB_KEY_PGUP,        TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, +    {"\x1b[5@",      TB_KEY_PGUP,        TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[5$",      TB_KEY_PGUP,        TB_MOD_SHIFT                           }, + +    {"\x1b\x1b[6~",  TB_KEY_PGDN,        TB_MOD_ALT                             }, +    {"\x1b\x1b[6$",  TB_KEY_PGDN,        TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[6^",      TB_KEY_PGDN,        TB_MOD_CTRL                            }, +    {"\x1b\x1b[6^",  TB_KEY_PGDN,        TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b\x1b[6@",  TB_KEY_PGDN,        TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, +    {"\x1b[6@",      TB_KEY_PGDN,        TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[6$",      TB_KEY_PGDN,        TB_MOD_SHIFT                           }, + +    {"\x1b\x1b[11~", TB_KEY_F1,          TB_MOD_ALT                             }, +    {"\x1b\x1b[23~", TB_KEY_F1,          TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[11^",     TB_KEY_F1,          TB_MOD_CTRL                            }, +    {"\x1b\x1b[11^", TB_KEY_F1,          TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b\x1b[23^", TB_KEY_F1,          TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, +    {"\x1b[23^",     TB_KEY_F1,          TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[23~",     TB_KEY_F1,          TB_MOD_SHIFT                           }, + +    {"\x1b\x1b[12~", TB_KEY_F2,          TB_MOD_ALT                             }, +    {"\x1b\x1b[24~", TB_KEY_F2,          TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[12^",     TB_KEY_F2,          TB_MOD_CTRL                            }, +    {"\x1b\x1b[12^", TB_KEY_F2,          TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b\x1b[24^", TB_KEY_F2,          TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, +    {"\x1b[24^",     TB_KEY_F2,          TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[24~",     TB_KEY_F2,          TB_MOD_SHIFT                           }, + +    {"\x1b\x1b[13~", TB_KEY_F3,          TB_MOD_ALT                             }, +    {"\x1b\x1b[25~", TB_KEY_F3,          TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[13^",     TB_KEY_F3,          TB_MOD_CTRL                            }, +    {"\x1b\x1b[13^", TB_KEY_F3,          TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b\x1b[25^", TB_KEY_F3,          TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, +    {"\x1b[25^",     TB_KEY_F3,          TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[25~",     TB_KEY_F3,          TB_MOD_SHIFT                           }, + +    {"\x1b\x1b[14~", TB_KEY_F4,          TB_MOD_ALT                             }, +    {"\x1b\x1b[26~", TB_KEY_F4,          TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[14^",     TB_KEY_F4,          TB_MOD_CTRL                            }, +    {"\x1b\x1b[14^", TB_KEY_F4,          TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b\x1b[26^", TB_KEY_F4,          TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, +    {"\x1b[26^",     TB_KEY_F4,          TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[26~",     TB_KEY_F4,          TB_MOD_SHIFT                           }, + +    {"\x1b\x1b[15~", TB_KEY_F5,          TB_MOD_ALT                             }, +    {"\x1b\x1b[28~", TB_KEY_F5,          TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[15^",     TB_KEY_F5,          TB_MOD_CTRL                            }, +    {"\x1b\x1b[15^", TB_KEY_F5,          TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b\x1b[28^", TB_KEY_F5,          TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, +    {"\x1b[28^",     TB_KEY_F5,          TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[28~",     TB_KEY_F5,          TB_MOD_SHIFT                           }, + +    {"\x1b\x1b[17~", TB_KEY_F6,          TB_MOD_ALT                             }, +    {"\x1b\x1b[29~", TB_KEY_F6,          TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[17^",     TB_KEY_F6,          TB_MOD_CTRL                            }, +    {"\x1b\x1b[17^", TB_KEY_F6,          TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b\x1b[29^", TB_KEY_F6,          TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, +    {"\x1b[29^",     TB_KEY_F6,          TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[29~",     TB_KEY_F6,          TB_MOD_SHIFT                           }, + +    {"\x1b\x1b[18~", TB_KEY_F7,          TB_MOD_ALT                             }, +    {"\x1b\x1b[31~", TB_KEY_F7,          TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[18^",     TB_KEY_F7,          TB_MOD_CTRL                            }, +    {"\x1b\x1b[18^", TB_KEY_F7,          TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b\x1b[31^", TB_KEY_F7,          TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, +    {"\x1b[31^",     TB_KEY_F7,          TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[31~",     TB_KEY_F7,          TB_MOD_SHIFT                           }, + +    {"\x1b\x1b[19~", TB_KEY_F8,          TB_MOD_ALT                             }, +    {"\x1b\x1b[32~", TB_KEY_F8,          TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[19^",     TB_KEY_F8,          TB_MOD_CTRL                            }, +    {"\x1b\x1b[19^", TB_KEY_F8,          TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b\x1b[32^", TB_KEY_F8,          TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, +    {"\x1b[32^",     TB_KEY_F8,          TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[32~",     TB_KEY_F8,          TB_MOD_SHIFT                           }, + +    {"\x1b\x1b[20~", TB_KEY_F9,          TB_MOD_ALT                             }, +    {"\x1b\x1b[33~", TB_KEY_F9,          TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[20^",     TB_KEY_F9,          TB_MOD_CTRL                            }, +    {"\x1b\x1b[20^", TB_KEY_F9,          TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b\x1b[33^", TB_KEY_F9,          TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, +    {"\x1b[33^",     TB_KEY_F9,          TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[33~",     TB_KEY_F9,          TB_MOD_SHIFT                           }, + +    {"\x1b\x1b[21~", TB_KEY_F10,         TB_MOD_ALT                             }, +    {"\x1b\x1b[34~", TB_KEY_F10,         TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[21^",     TB_KEY_F10,         TB_MOD_CTRL                            }, +    {"\x1b\x1b[21^", TB_KEY_F10,         TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b\x1b[34^", TB_KEY_F10,         TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, +    {"\x1b[34^",     TB_KEY_F10,         TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[34~",     TB_KEY_F10,         TB_MOD_SHIFT                           }, + +    {"\x1b\x1b[23~", TB_KEY_F11,         TB_MOD_ALT                             }, +    {"\x1b\x1b[23$", TB_KEY_F11,         TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[23^",     TB_KEY_F11,         TB_MOD_CTRL                            }, +    {"\x1b\x1b[23^", TB_KEY_F11,         TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b\x1b[23@", TB_KEY_F11,         TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, +    {"\x1b[23@",     TB_KEY_F11,         TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[23$",     TB_KEY_F11,         TB_MOD_SHIFT                           }, + +    {"\x1b\x1b[24~", TB_KEY_F12,         TB_MOD_ALT                             }, +    {"\x1b\x1b[24$", TB_KEY_F12,         TB_MOD_ALT | TB_MOD_SHIFT              }, +    {"\x1b[24^",     TB_KEY_F12,         TB_MOD_CTRL                            }, +    {"\x1b\x1b[24^", TB_KEY_F12,         TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1b\x1b[24@", TB_KEY_F12,         TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, +    {"\x1b[24@",     TB_KEY_F12,         TB_MOD_CTRL | TB_MOD_SHIFT             }, +    {"\x1b[24$",     TB_KEY_F12,         TB_MOD_SHIFT                           }, + + // linux console/putty arrows +    {"\x1b[A",       TB_KEY_ARROW_UP,    TB_MOD_SHIFT                           }, +    {"\x1b[B",       TB_KEY_ARROW_DOWN,  TB_MOD_SHIFT                           }, +    {"\x1b[C",       TB_KEY_ARROW_RIGHT, TB_MOD_SHIFT                           }, +    {"\x1b[D",       TB_KEY_ARROW_LEFT,  TB_MOD_SHIFT                           }, + + // more putty arrows +    {"\x1bOA",       TB_KEY_ARROW_UP,    TB_MOD_CTRL                            }, +    {"\x1b\x1bOA",   TB_KEY_ARROW_UP,    TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1bOB",       TB_KEY_ARROW_DOWN,  TB_MOD_CTRL                            }, +    {"\x1b\x1bOB",   TB_KEY_ARROW_DOWN,  TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1bOC",       TB_KEY_ARROW_RIGHT, TB_MOD_CTRL                            }, +    {"\x1b\x1bOC",   TB_KEY_ARROW_RIGHT, TB_MOD_CTRL | TB_MOD_ALT               }, +    {"\x1bOD",       TB_KEY_ARROW_LEFT,  TB_MOD_CTRL                            }, +    {"\x1b\x1bOD",   TB_KEY_ARROW_LEFT,  TB_MOD_CTRL | TB_MOD_ALT               }, + +    {NULL,           0,                  0                                      }, +}; + +static const unsigned char utf8_length[256] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, +    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, +    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, +    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, +    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, +    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, +    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, +    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, +    1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, +    2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, +    3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 1, 1}; + +static const unsigned char utf8_mask[6] = {0x7f, 0x1f, 0x0f, 0x07, 0x03, 0x01}; + +static int tb_reset(void); +static int tb_printf_inner(int x, int y, uintattr_t fg, uintattr_t bg, +    size_t *out_w, const char *fmt, va_list vl); +static int init_term_attrs(void); +static int init_term_caps(void); +static int init_cap_trie(void); +static int cap_trie_add(const char *cap, uint16_t key, uint8_t mod); +static int cap_trie_find(const char *buf, size_t nbuf, struct cap_trie_t **last, +    size_t *depth); +static int cap_trie_deinit(struct cap_trie_t *node); +static int init_resize_handler(void); +static int send_init_escape_codes(void); +static int send_clear(void); +static int update_term_size(void); +static int update_term_size_via_esc(void); +static int init_cellbuf(void); +static int tb_deinit(void); +static int load_terminfo(void); +static int load_terminfo_from_path(const char *path, const char *term); +static int read_terminfo_path(const char *path); +static int parse_terminfo_caps(void); +static int load_builtin_caps(void); +static const char *get_terminfo_string(int16_t str_offsets_pos, +    int16_t str_offsets_len, int16_t str_table_pos, int16_t str_table_len, +    int16_t str_index); +static int wait_event(struct tb_event *event, int timeout); +static int extract_event(struct tb_event *event); +static int extract_esc(struct tb_event *event); +static int extract_esc_user(struct tb_event *event, int is_post); +static int extract_esc_cap(struct tb_event *event); +static int extract_esc_mouse(struct tb_event *event); +static int resize_cellbufs(void); +static void handle_resize(int sig); +static int send_attr(uintattr_t fg, uintattr_t bg); +static int send_sgr(uint32_t fg, uint32_t bg, int fg_is_default, +    int bg_is_default); +static int send_cursor_if(int x, int y); +static int send_char(int x, int y, uint32_t ch); +static int send_cluster(int x, int y, uint32_t *ch, size_t nch); +static int convert_num(uint32_t num, char *buf); +static int cell_cmp(struct tb_cell *a, struct tb_cell *b); +static int cell_copy(struct tb_cell *dst, struct tb_cell *src); +static int cell_set(struct tb_cell *cell, uint32_t *ch, size_t nch, +    uintattr_t fg, uintattr_t bg); +static int cell_reserve_ech(struct tb_cell *cell, size_t n); +static int cell_free(struct tb_cell *cell); +static int cellbuf_init(struct cellbuf_t *c, int w, int h); +static int cellbuf_free(struct cellbuf_t *c); +static int cellbuf_clear(struct cellbuf_t *c); +static int cellbuf_get(struct cellbuf_t *c, int x, int y, struct tb_cell **out); +static int cellbuf_in_bounds(struct cellbuf_t *c, int x, int y); +static int cellbuf_resize(struct cellbuf_t *c, int w, int h); +static int bytebuf_puts(struct bytebuf_t *b, const char *str); +static int bytebuf_nputs(struct bytebuf_t *b, const char *str, size_t nstr); +static int bytebuf_shift(struct bytebuf_t *b, size_t n); +static int bytebuf_flush(struct bytebuf_t *b, int fd); +static int bytebuf_reserve(struct bytebuf_t *b, size_t sz); +static int bytebuf_free(struct bytebuf_t *b); + +int tb_init(void) { +    return tb_init_file("/dev/tty"); +} + +int tb_init_file(const char *path) { +    if (global.initialized) return TB_ERR_INIT_ALREADY; +    int ttyfd = open(path, O_RDWR); +    if (ttyfd < 0) { +        global.last_errno = errno; +        return TB_ERR_INIT_OPEN; +    } +    global.ttyfd_open = 1; +    return tb_init_fd(ttyfd); +} + +int tb_init_fd(int ttyfd) { +    return tb_init_rwfd(ttyfd, ttyfd); +} + +int tb_init_rwfd(int rfd, int wfd) { +    int rv; + +    tb_reset(); +    global.ttyfd = rfd == wfd && isatty(rfd) ? rfd : -1; +    global.rfd = rfd; +    global.wfd = wfd; + +    do { +        if_err_break(rv, init_term_attrs()); +        if_err_break(rv, init_term_caps()); +        if_err_break(rv, init_cap_trie()); +        if_err_break(rv, init_resize_handler()); +        if_err_break(rv, send_init_escape_codes()); +        if_err_break(rv, send_clear()); +        if_err_break(rv, update_term_size()); +        if_err_break(rv, init_cellbuf()); +        global.initialized = 1; +    } while (0); + +    if (rv != TB_OK) { +        tb_deinit(); +    } + +    return rv; +} + +int tb_shutdown(void) { +    if_not_init_return(); +    tb_deinit(); +    return TB_OK; +} + +int tb_width(void) { +    if_not_init_return(); +    return global.width; +} + +int tb_height(void) { +    if_not_init_return(); +    return global.height; +} + +int tb_clear(void) { +    if_not_init_return(); +    return cellbuf_clear(&global.back); +} + +int tb_set_clear_attrs(uintattr_t fg, uintattr_t bg) { +    if_not_init_return(); +    global.fg = fg; +    global.bg = bg; +    return TB_OK; +} + +int tb_present(void) { +    if_not_init_return(); + +    int rv; + +    // TODO: Assert global.back.(width,height) == global.front.(width,height) + +    global.last_x = -1; +    global.last_y = -1; + +    int x, y, i; +    for (y = 0; y < global.front.height; y++) { +        for (x = 0; x < global.front.width;) { +            struct tb_cell *back, *front; +            if_err_return(rv, cellbuf_get(&global.back, x, y, &back)); +            if_err_return(rv, cellbuf_get(&global.front, x, y, &front)); + +            int w; +            { +#ifdef TB_OPT_EGC +                if (back->nech > 0) +                    w = wcswidth((wchar_t *)back->ech, back->nech); +                else +#endif +                    // wcwidth simply returns -1 on overflow of wchar_t +                    w = wcwidth((wchar_t)back->ch); +            } +            if (w < 1) w = 1; + +            if (cell_cmp(back, front) != 0) { +                cell_copy(front, back); + +                send_attr(back->fg, back->bg); +                if (w > 1 && x >= global.front.width - (w - 1)) { +                    // Not enough room for wide char, send spaces +                    for (i = x; i < global.front.width; i++) { +                        send_char(i, y, ' '); +                    } +                } else { +                    { +#ifdef TB_OPT_EGC +                        if (back->nech > 0) +                            send_cluster(x, y, back->ech, back->nech); +                        else +#endif +                            send_char(x, y, back->ch); +                    } + +                    // When wcwidth>1, we need to advance the cursor by more +                    // than 1, thereby skipping some cells. Set these skipped +                    // cells to an invalid codepoint in the front buffer, so +                    // that if this cell is later replaced by a wcwidth==1 char, +                    // we'll get a cell_cmp diff for the skipped cells and +                    // properly re-render. +                    for (i = 1; i < w; i++) { +                        struct tb_cell *front_wide; +                        uint32_t invalid = -1; +                        if_err_return(rv, +                            cellbuf_get(&global.front, x + i, y, &front_wide)); +                        if_err_return(rv, +                            cell_set(front_wide, &invalid, 1, -1, -1)); +                    } +                } +            } +            x += w; +        } +    } + +    if_err_return(rv, send_cursor_if(global.cursor_x, global.cursor_y)); +    if_err_return(rv, bytebuf_flush(&global.out, global.wfd)); + +    return TB_OK; +} + +int tb_invalidate(void) { +    int rv; +    if_not_init_return(); +    if_err_return(rv, resize_cellbufs()); +    return TB_OK; +} + +int tb_set_cursor(int cx, int cy) { +    if_not_init_return(); +    int rv; +    if (cx < 0) cx = 0; +    if (cy < 0) cy = 0; +    if (global.cursor_x == -1) { +        if_err_return(rv, +            bytebuf_puts(&global.out, global.caps[TB_CAP_SHOW_CURSOR])); +    } +    if_err_return(rv, send_cursor_if(cx, cy)); +    global.cursor_x = cx; +    global.cursor_y = cy; +    return TB_OK; +} + +int tb_hide_cursor(void) { +    if_not_init_return(); +    int rv; +    if (global.cursor_x >= 0) { +        if_err_return(rv, +            bytebuf_puts(&global.out, global.caps[TB_CAP_HIDE_CURSOR])); +    } +    global.cursor_x = -1; +    global.cursor_y = -1; +    return TB_OK; +} + +int tb_set_cell(int x, int y, uint32_t ch, uintattr_t fg, uintattr_t bg) { +    return tb_set_cell_ex(x, y, &ch, 1, fg, bg); +} + +int tb_set_cell_ex(int x, int y, uint32_t *ch, size_t nch, uintattr_t fg, +    uintattr_t bg) { +    if_not_init_return(); +    int rv; +    struct tb_cell *cell; +    if_err_return(rv, cellbuf_get(&global.back, x, y, &cell)); +    if_err_return(rv, cell_set(cell, ch, nch, fg, bg)); +    return TB_OK; +} + +int tb_extend_cell(int x, int y, uint32_t ch) { +    if_not_init_return(); +#ifdef TB_OPT_EGC +    int rv; +    struct tb_cell *cell; +    size_t nech; +    if_err_return(rv, cellbuf_get(&global.back, x, y, &cell)); +    if (cell->nech > 0) { // append to ech +        nech = cell->nech + 1; +        if_err_return(rv, cell_reserve_ech(cell, nech)); +        cell->ech[nech - 1] = ch; +    } else { // make new ech +        nech = 2; +        if_err_return(rv, cell_reserve_ech(cell, nech)); +        cell->ech[0] = cell->ch; +        cell->ech[1] = ch; +    } +    cell->ech[nech] = '\0'; +    cell->nech = nech; +    return TB_OK; +#else +    (void)x; +    (void)y; +    (void)ch; +    return TB_ERR; +#endif +} + +int tb_set_input_mode(int mode) { +    if_not_init_return(); +    if (mode == TB_INPUT_CURRENT) { +        return global.input_mode; +    } + +    if ((mode & (TB_INPUT_ESC | TB_INPUT_ALT)) == 0) { +        mode |= TB_INPUT_ESC; +    } + +    if ((mode & (TB_INPUT_ESC | TB_INPUT_ALT)) == (TB_INPUT_ESC | TB_INPUT_ALT)) +    { +        mode &= ~TB_INPUT_ALT; +    } + +    if (mode & TB_INPUT_MOUSE) { +        bytebuf_puts(&global.out, TB_HARDCAP_ENTER_MOUSE); +        bytebuf_flush(&global.out, global.wfd); +    } else { +        bytebuf_puts(&global.out, TB_HARDCAP_EXIT_MOUSE); +        bytebuf_flush(&global.out, global.wfd); +    } + +    global.input_mode = mode; +    return TB_OK; +} + +int tb_set_output_mode(int mode) { +    if_not_init_return(); +    switch (mode) { +        case TB_OUTPUT_CURRENT: +            return global.output_mode; +        case TB_OUTPUT_NORMAL: +        case TB_OUTPUT_256: +        case TB_OUTPUT_216: +        case TB_OUTPUT_GRAYSCALE: +#if TB_OPT_ATTR_W >= 32 +        case TB_OUTPUT_TRUECOLOR: +#endif +            global.last_fg = ~global.fg; +            global.last_bg = ~global.bg; +            global.output_mode = mode; +            return TB_OK; +    } +    return TB_ERR; +} + +int tb_peek_event(struct tb_event *event, int timeout_ms) { +    if_not_init_return(); +    return wait_event(event, timeout_ms); +} + +int tb_poll_event(struct tb_event *event) { +    if_not_init_return(); +    return wait_event(event, -1); +} + +int tb_get_fds(int *ttyfd, int *resizefd) { +    if_not_init_return(); + +    *ttyfd = global.rfd; +    *resizefd = global.resize_pipefd[0]; + +    return TB_OK; +} + +int tb_print(int x, int y, uintattr_t fg, uintattr_t bg, const char *str) { +    return tb_print_ex(x, y, fg, bg, NULL, str); +} + +int tb_print_ex(int x, int y, uintattr_t fg, uintattr_t bg, size_t *out_w, +    const char *str) { +    int rv, w, ix; +    uint32_t uni; + +    if_not_init_return(); + +    if (!cellbuf_in_bounds(&global.back, x, y)) { +        return TB_ERR_OUT_OF_BOUNDS; +    } + +    ix = x; +    if (out_w) *out_w = 0; + +    while (*str) { +        rv = tb_utf8_char_to_unicode(&uni, str); + +        if (rv < 0) { +            uni = 0xfffd; // replace invalid UTF-8 char with U+FFFD +            str += rv * -1; +        } else if (rv > 0) { +            str += rv; +        } else { +            break; // shouldn't get here +        } + +        if (uni == '\n') { // TODO: \r, \t, \v, \f, etc? +            x = ix; +            y += 1; +            continue; +        } else if (!iswprint((wint_t)uni)) { +            uni = 0xfffd; // replace non-printable with U+FFFD +        } + +        w = wcwidth((wchar_t)uni); +        if (w < 0) { +            return TB_ERR;   // shouldn't happen if iswprint +        } else if (w == 0) { // combining character +            if (cellbuf_in_bounds(&global.back, x - 1, y)) { +                if_err_return(rv, tb_extend_cell(x - 1, y, uni)); +            } +        } else { +            if (cellbuf_in_bounds(&global.back, x, y)) { +                if_err_return(rv, tb_set_cell(x, y, uni, fg, bg)); +            } +        } + +        x += w; +        if (out_w) *out_w += w; +    } + +    return TB_OK; +} + +int tb_printf(int x, int y, uintattr_t fg, uintattr_t bg, const char *fmt, +    ...) { +    int rv; +    va_list vl; +    va_start(vl, fmt); +    rv = tb_printf_inner(x, y, fg, bg, NULL, fmt, vl); +    va_end(vl); +    return rv; +} + +int tb_printf_ex(int x, int y, uintattr_t fg, uintattr_t bg, size_t *out_w, +    const char *fmt, ...) { +    int rv; +    va_list vl; +    va_start(vl, fmt); +    rv = tb_printf_inner(x, y, fg, bg, out_w, fmt, vl); +    va_end(vl); +    return rv; +} + +int tb_send(const char *buf, size_t nbuf) { +    return bytebuf_nputs(&global.out, buf, nbuf); +} + +int tb_sendf(const char *fmt, ...) { +    int rv; +    char buf[TB_OPT_PRINTF_BUF]; +    va_list vl; +    va_start(vl, fmt); +    rv = vsnprintf(buf, sizeof(buf), fmt, vl); +    va_end(vl); +    if (rv < 0 || rv >= (int)sizeof(buf)) { +        return TB_ERR; +    } +    return tb_send(buf, (size_t)rv); +} + +int tb_set_func(int fn_type, int (*fn)(struct tb_event *, size_t *)) { +    switch (fn_type) { +        case TB_FUNC_EXTRACT_PRE: +            global.fn_extract_esc_pre = fn; +            return TB_OK; +        case TB_FUNC_EXTRACT_POST: +            global.fn_extract_esc_post = fn; +            return TB_OK; +    } +    return TB_ERR; +} + +struct tb_cell *tb_cell_buffer(void) { +    if (!global.initialized) return NULL; +    return global.back.cells; +} + +int tb_utf8_char_length(char c) { +    return utf8_length[(unsigned char)c]; +} + +int tb_utf8_char_to_unicode(uint32_t *out, const char *c) { +    if (*c == '\0') return 0; + +    int i; +    unsigned char len = tb_utf8_char_length(*c); +    unsigned char mask = utf8_mask[len - 1]; +    uint32_t result = c[0] & mask; +    for (i = 1; i < len && c[i] != '\0'; ++i) { +        result <<= 6; +        result |= c[i] & 0x3f; +    } + +    if (i != len) return i * -1; + +    *out = result; +    return (int)len; +} + +int tb_utf8_unicode_to_char(char *out, uint32_t c) { +    int len = 0; +    int first; +    int i; + +    if (c < 0x80) { +        first = 0; +        len = 1; +    } else if (c < 0x800) { +        first = 0xc0; +        len = 2; +    } else if (c < 0x10000) { +        first = 0xe0; +        len = 3; +    } else if (c < 0x200000) { +        first = 0xf0; +        len = 4; +    } else if (c < 0x4000000) { +        first = 0xf8; +        len = 5; +    } else { +        first = 0xfc; +        len = 6; +    } + +    for (i = len - 1; i > 0; --i) { +        out[i] = (c & 0x3f) | 0x80; +        c >>= 6; +    } +    out[0] = c | first; +    out[len] = '\0'; + +    return len; +} + +int tb_last_errno(void) { +    return global.last_errno; +} + +const char *tb_strerror(int err) { +    switch (err) { +        case TB_OK: +            return "Success"; +        case TB_ERR_NEED_MORE: +            return "Not enough input"; +        case TB_ERR_INIT_ALREADY: +            return "Termbox initialized already"; +        case TB_ERR_MEM: +            return "Out of memory"; +        case TB_ERR_NO_EVENT: +            return "No event"; +        case TB_ERR_NO_TERM: +            return "No TERM in environment"; +        case TB_ERR_NOT_INIT: +            return "Termbox not initialized"; +        case TB_ERR_OUT_OF_BOUNDS: +            return "Out of bounds"; +        case TB_ERR_UNSUPPORTED_TERM: +            return "Unsupported terminal"; +        case TB_ERR_CAP_COLLISION: +            return "Termcaps collision"; +        case TB_ERR_RESIZE_SSCANF: +            return "Terminal width/height not received by sscanf() after " +                   "resize"; +        case TB_ERR: +        case TB_ERR_INIT_OPEN: +        case TB_ERR_READ: +        case TB_ERR_RESIZE_IOCTL: +        case TB_ERR_RESIZE_PIPE: +        case TB_ERR_RESIZE_SIGACTION: +        case TB_ERR_POLL: +        case TB_ERR_TCGETATTR: +        case TB_ERR_TCSETATTR: +        case TB_ERR_RESIZE_WRITE: +        case TB_ERR_RESIZE_POLL: +        case TB_ERR_RESIZE_READ: +        default: +            strerror_r(global.last_errno, global.errbuf, sizeof(global.errbuf)); +            return (const char *)global.errbuf; +    } +} + +int tb_has_truecolor(void) { +#if TB_OPT_ATTR_W >= 32 +    return 1; +#else +    return 0; +#endif +} + +int tb_has_egc(void) { +#ifdef TB_OPT_EGC +    return 1; +#else +    return 0; +#endif +} + +int tb_attr_width(void) { +    return TB_OPT_ATTR_W; +} + +const char *tb_version(void) { +    return TB_VERSION_STR; +} + +static int tb_reset(void) { +    int ttyfd_open = global.ttyfd_open; +    memset(&global, 0, sizeof(global)); +    global.ttyfd = -1; +    global.rfd = -1; +    global.wfd = -1; +    global.ttyfd_open = ttyfd_open; +    global.resize_pipefd[0] = -1; +    global.resize_pipefd[1] = -1; +    global.width = -1; +    global.height = -1; +    global.cursor_x = -1; +    global.cursor_y = -1; +    global.last_x = -1; +    global.last_y = -1; +    global.fg = TB_DEFAULT; +    global.bg = TB_DEFAULT; +    global.last_fg = ~global.fg; +    global.last_bg = ~global.bg; +    global.input_mode = TB_INPUT_ESC; +    global.output_mode = TB_OUTPUT_NORMAL; +    return TB_OK; +} + +static int init_term_attrs(void) { +    if (global.ttyfd < 0) { +        return TB_OK; +    } + +    if (tcgetattr(global.ttyfd, &global.orig_tios) != 0) { +        global.last_errno = errno; +        return TB_ERR_TCGETATTR; +    } + +    struct termios tios; +    memcpy(&tios, &global.orig_tios, sizeof(tios)); +    global.has_orig_tios = 1; + +    cfmakeraw(&tios); +    tios.c_cc[VMIN] = 1; +    tios.c_cc[VTIME] = 0; + +    if (tcsetattr(global.ttyfd, TCSAFLUSH, &tios) != 0) { +        global.last_errno = errno; +        return TB_ERR_TCSETATTR; +    } + +    return TB_OK; +} + +int tb_printf_inner(int x, int y, uintattr_t fg, uintattr_t bg, size_t *out_w, +    const char *fmt, va_list vl) { +    int rv; +    char buf[TB_OPT_PRINTF_BUF]; +    rv = vsnprintf(buf, sizeof(buf), fmt, vl); +    if (rv < 0 || rv >= (int)sizeof(buf)) { +        return TB_ERR; +    } +    return tb_print_ex(x, y, fg, bg, out_w, buf); +} + +static int init_term_caps(void) { +    if (load_terminfo() == TB_OK) { +        return parse_terminfo_caps(); +    } +    return load_builtin_caps(); +} + +static int init_cap_trie(void) { +    int rv, i; + +    // Add caps from terminfo or built-in +    // +    // Collisions are expected as some terminfo entries have dupes. (For +    // example, att605-pc collides on TB_CAP_F4 and TB_CAP_DELETE.) First cap +    // in TB_CAP_* index order will win. +    // +    // TODO: Reorder TB_CAP_* so more critical caps come first. +    for (i = 0; i < TB_CAP__COUNT_KEYS; i++) { +        rv = cap_trie_add(global.caps[i], tb_key_i(i), 0); +        if (rv != TB_OK && rv != TB_ERR_CAP_COLLISION) return rv; +    } + +    // Add built-in mod caps +    // +    // Collisions are OK here as well. This can happen if global.caps collides +    // with builtin_mod_caps. It is desirable to give precedence to global.caps +    // here. +    for (i = 0; builtin_mod_caps[i].cap != NULL; i++) { +        rv = cap_trie_add(builtin_mod_caps[i].cap, builtin_mod_caps[i].key, +            builtin_mod_caps[i].mod); +        if (rv != TB_OK && rv != TB_ERR_CAP_COLLISION) return rv; +    } + +    return TB_OK; +} + +static int cap_trie_add(const char *cap, uint16_t key, uint8_t mod) { +    struct cap_trie_t *next, *node = &global.cap_trie; +    size_t i, j; + +    if (!cap || strlen(cap) <= 0) return TB_OK; // Nothing to do for empty caps + +    for (i = 0; cap[i] != '\0'; i++) { +        char c = cap[i]; +        next = NULL; + +        // Check if c is already a child of node +        for (j = 0; j < node->nchildren; j++) { +            if (node->children[j].c == c) { +                next = &node->children[j]; +                break; +            } +        } +        if (!next) { +            // We need to add a new child to node +            node->nchildren += 1; +            node->children = (struct cap_trie_t *)tb_realloc(node->children, +                sizeof(*node) * node->nchildren); +            if (!node->children) { +                return TB_ERR_MEM; +            } +            next = &node->children[node->nchildren - 1]; +            memset(next, 0, sizeof(*next)); +            next->c = c; +        } + +        // Continue +        node = next; +    } + +    if (node->is_leaf) { +        // Already a leaf here +        return TB_ERR_CAP_COLLISION; +    } + +    node->is_leaf = 1; +    node->key = key; +    node->mod = mod; +    return TB_OK; +} + +static int cap_trie_find(const char *buf, size_t nbuf, struct cap_trie_t **last, +    size_t *depth) { +    struct cap_trie_t *next, *node = &global.cap_trie; +    size_t i, j; +    *last = node; +    *depth = 0; +    for (i = 0; i < nbuf; i++) { +        char c = buf[i]; +        next = NULL; + +        // Find c in node.children +        for (j = 0; j < node->nchildren; j++) { +            if (node->children[j].c == c) { +                next = &node->children[j]; +                break; +            } +        } +        if (!next) { +            // Not found +            return TB_OK; +        } +        node = next; +        *last = node; +        *depth += 1; +        if (node->is_leaf && node->nchildren < 1) { +            break; +        } +    } +    return TB_OK; +} + +static int cap_trie_deinit(struct cap_trie_t *node) { +    size_t j; +    for (j = 0; j < node->nchildren; j++) { +        cap_trie_deinit(&node->children[j]); +    } +    if (node->children) { +        tb_free(node->children); +    } +    memset(node, 0, sizeof(*node)); +    return TB_OK; +} + +static int init_resize_handler(void) { +    if (pipe(global.resize_pipefd) != 0) { +        global.last_errno = errno; +        return TB_ERR_RESIZE_PIPE; +    } + +    struct sigaction sa; +    memset(&sa, 0, sizeof(sa)); +    sa.sa_handler = handle_resize; +    if (sigaction(SIGWINCH, &sa, NULL) != 0) { +        global.last_errno = errno; +        return TB_ERR_RESIZE_SIGACTION; +    } + +    return TB_OK; +} + +static int send_init_escape_codes(void) { +    int rv; +    if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_ENTER_CA])); +    if_err_return(rv, +        bytebuf_puts(&global.out, global.caps[TB_CAP_ENTER_KEYPAD])); +    if_err_return(rv, +        bytebuf_puts(&global.out, global.caps[TB_CAP_HIDE_CURSOR])); +    return TB_OK; +} + +static int send_clear(void) { +    int rv; + +    if_err_return(rv, send_attr(global.fg, global.bg)); +    if_err_return(rv, +        bytebuf_puts(&global.out, global.caps[TB_CAP_CLEAR_SCREEN])); + +    if_err_return(rv, send_cursor_if(global.cursor_x, global.cursor_y)); +    if_err_return(rv, bytebuf_flush(&global.out, global.wfd)); + +    global.last_x = -1; +    global.last_y = -1; + +    return TB_OK; +} + +static int update_term_size(void) { +    int rv, ioctl_errno; + +    if (global.ttyfd < 0) { +        return TB_OK; +    } + +    struct winsize sz; +    memset(&sz, 0, sizeof(sz)); + +    // Try ioctl TIOCGWINSZ +    if (ioctl(global.ttyfd, TIOCGWINSZ, &sz) == 0) { +        global.width = sz.ws_col; +        global.height = sz.ws_row; +        return TB_OK; +    } +    ioctl_errno = errno; + +    // Try >cursor(9999,9999), >u7, <u6 +    if_ok_return(rv, update_term_size_via_esc()); + +    global.last_errno = ioctl_errno; +    return TB_ERR_RESIZE_IOCTL; +} + +static int update_term_size_via_esc(void) { +#ifndef TB_RESIZE_FALLBACK_MS +#define TB_RESIZE_FALLBACK_MS 1000 +#endif + +    char move_and_report[] = "\x1b[9999;9999H\x1b[6n"; +    ssize_t write_rv = +        write(global.wfd, move_and_report, strlen(move_and_report)); +    if (write_rv != (ssize_t)strlen(move_and_report)) { +        return TB_ERR_RESIZE_WRITE; +    } + +    fd_set fds; +    FD_ZERO(&fds); +    FD_SET(global.rfd, &fds); + +    struct timeval timeout; +    timeout.tv_sec = 0; +    timeout.tv_usec = TB_RESIZE_FALLBACK_MS * 1000; + +    int select_rv = select(global.rfd + 1, &fds, NULL, NULL, &timeout); + +    if (select_rv != 1) { +        global.last_errno = errno; +        return TB_ERR_RESIZE_POLL; +    } + +    char buf[TB_OPT_READ_BUF]; +    ssize_t read_rv = read(global.rfd, buf, sizeof(buf) - 1); +    if (read_rv < 1) { +        global.last_errno = errno; +        return TB_ERR_RESIZE_READ; +    } +    buf[read_rv] = '\0'; + +    int rw, rh; +    if (sscanf(buf, "\x1b[%d;%dR", &rh, &rw) != 2) { +        return TB_ERR_RESIZE_SSCANF; +    } + +    global.width = rw; +    global.height = rh; +    return TB_OK; +} + +static int init_cellbuf(void) { +    int rv; +    if_err_return(rv, cellbuf_init(&global.back, global.width, global.height)); +    if_err_return(rv, cellbuf_init(&global.front, global.width, global.height)); +    if_err_return(rv, cellbuf_clear(&global.back)); +    if_err_return(rv, cellbuf_clear(&global.front)); +    return TB_OK; +} + +static int tb_deinit(void) { +    if (global.caps[0] != NULL && global.wfd >= 0) { +        bytebuf_puts(&global.out, global.caps[TB_CAP_SHOW_CURSOR]); +        bytebuf_puts(&global.out, global.caps[TB_CAP_SGR0]); +        bytebuf_puts(&global.out, global.caps[TB_CAP_CLEAR_SCREEN]); +        bytebuf_puts(&global.out, global.caps[TB_CAP_EXIT_CA]); +        bytebuf_puts(&global.out, global.caps[TB_CAP_EXIT_KEYPAD]); +        bytebuf_puts(&global.out, TB_HARDCAP_EXIT_MOUSE); +        bytebuf_flush(&global.out, global.wfd); +    } +    if (global.ttyfd >= 0) { +        if (global.has_orig_tios) { +            tcsetattr(global.ttyfd, TCSAFLUSH, &global.orig_tios); +        } +        if (global.ttyfd_open) { +            close(global.ttyfd); +            global.ttyfd_open = 0; +        } +    } + +    struct sigaction sa; +    memset(&sa, 0, sizeof(sa)); +    sa.sa_handler = SIG_DFL; +    sigaction(SIGWINCH, &sa, NULL); +    if (global.resize_pipefd[0] >= 0) close(global.resize_pipefd[0]); +    if (global.resize_pipefd[1] >= 0) close(global.resize_pipefd[1]); + +    cellbuf_free(&global.back); +    cellbuf_free(&global.front); +    bytebuf_free(&global.in); +    bytebuf_free(&global.out); + +    if (global.terminfo) tb_free(global.terminfo); + +    cap_trie_deinit(&global.cap_trie); + +    tb_reset(); +    return TB_OK; +} + +static int load_terminfo(void) { +    int rv; +    char tmp[TB_PATH_MAX]; + +    // See terminfo(5) "Fetching Compiled Descriptions" for a description of +    // this behavior. Some of these paths are compile-time ncurses options, so +    // best guesses are used here. +    const char *term = getenv("TERM"); +    if (!term) { +        return TB_ERR; +    } + +    // If TERMINFO is set, try that directory and stop +    const char *terminfo = getenv("TERMINFO"); +    if (terminfo) { +        return load_terminfo_from_path(terminfo, term); +    } + +    // Next try ~/.terminfo +    const char *home = getenv("HOME"); +    if (home) { +        snprintf_or_return(rv, tmp, sizeof(tmp), "%s/.terminfo", home); +        if_ok_return(rv, load_terminfo_from_path(tmp, term)); +    } + +    // Next try TERMINFO_DIRS +    // +    // Note, empty entries are supposed to be interpretted as the "compiled-in +    // default", which is of course system-dependent. Previously /etc/terminfo +    // was used here. Let's skip empty entries altogether rather than give +    // precedence to a guess, and check common paths after this loop. +    const char *dirs = getenv("TERMINFO_DIRS"); +    if (dirs) { +        snprintf_or_return(rv, tmp, sizeof(tmp), "%s", dirs); +        char *dir = strtok(tmp, ":"); +        while (dir) { +            const char *cdir = dir; +            if (*cdir != '\0') { +                if_ok_return(rv, load_terminfo_from_path(cdir, term)); +            } +            dir = strtok(NULL, ":"); +        } +    } + +#ifdef TB_TERMINFO_DIR +    if_ok_return(rv, load_terminfo_from_path(TB_TERMINFO_DIR, term)); +#endif +    if_ok_return(rv, load_terminfo_from_path("/usr/local/etc/terminfo", term)); +    if_ok_return(rv, +        load_terminfo_from_path("/usr/local/share/terminfo", term)); +    if_ok_return(rv, load_terminfo_from_path("/usr/local/lib/terminfo", term)); +    if_ok_return(rv, load_terminfo_from_path("/etc/terminfo", term)); +    if_ok_return(rv, load_terminfo_from_path("/usr/share/terminfo", term)); +    if_ok_return(rv, load_terminfo_from_path("/usr/lib/terminfo", term)); +    if_ok_return(rv, load_terminfo_from_path("/usr/share/lib/terminfo", term)); +    if_ok_return(rv, load_terminfo_from_path("/lib/terminfo", term)); + +    return TB_ERR; +} + +static int load_terminfo_from_path(const char *path, const char *term) { +    int rv; +    char tmp[TB_PATH_MAX]; + +    // Look for term at this terminfo location, e.g., <terminfo>/x/xterm +    snprintf_or_return(rv, tmp, sizeof(tmp), "%s/%c/%s", path, term[0], term); +    if_ok_return(rv, read_terminfo_path(tmp)); + +#ifdef __APPLE__ +    // Try the Darwin equivalent path, e.g., <terminfo>/78/xterm +    snprintf_or_return(rv, tmp, sizeof(tmp), "%s/%x/%s", path, term[0], term); +    return read_terminfo_path(tmp); +#endif + +    return TB_ERR; +} + +static int read_terminfo_path(const char *path) { +    FILE *fp = fopen(path, "rb"); +    if (!fp) { +        return TB_ERR; +    } + +    struct stat st; +    if (fstat(fileno(fp), &st) != 0) { +        fclose(fp); +        return TB_ERR; +    } + +    size_t fsize = st.st_size; +    char *data = (char *)tb_malloc(fsize); +    if (!data) { +        fclose(fp); +        return TB_ERR; +    } + +    if (fread(data, 1, fsize, fp) != fsize) { +        fclose(fp); +        tb_free(data); +        return TB_ERR; +    } + +    global.terminfo = data; +    global.nterminfo = fsize; + +    fclose(fp); +    return TB_OK; +} + +static int parse_terminfo_caps(void) { +    // See term(5) "LEGACY STORAGE FORMAT" and "EXTENDED STORAGE FORMAT" for a +    // description of this behavior. + +    // Ensure there's at least a header's worth of data +    if (global.nterminfo < 6) { +        return TB_ERR; +    } + +    int16_t *header = (int16_t *)global.terminfo; +    // header[0] the magic number (octal 0432 or 01036) +    // header[1] the size, in bytes, of the names section +    // header[2] the number of bytes in the boolean section +    // header[3] the number of short integers in the numbers section +    // header[4] the number of offsets (short integers) in the strings section +    // header[5] the size, in bytes, of the string table + +    // Legacy ints are 16-bit, extended ints are 32-bit +    const int bytes_per_int = header[0] == 01036 ? 4  // 32-bit +                                                 : 2; // 16-bit + +    // > Between the boolean section and the number section, a null byte will be +    // > inserted, if necessary, to ensure that the number section begins on an +    // > even byte +    const int align_offset = (header[1] + header[2]) % 2 != 0 ? 1 : 0; + +    const int pos_str_offsets = +        (6 * sizeof(int16_t)) // header (12 bytes) +        + header[1]           // length of names section +        + header[2]           // length of boolean section +        + align_offset + +        (header[3] * bytes_per_int); // length of numbers section + +    const int pos_str_table = +        pos_str_offsets + +        (header[4] * sizeof(int16_t)); // length of string offsets table + +    // Load caps +    int i; +    for (i = 0; i < TB_CAP__COUNT; i++) { +        const char *cap = get_terminfo_string(pos_str_offsets, header[4], +            pos_str_table, header[5], terminfo_cap_indexes[i]); +        if (!cap) { +            // Something is not right +            return TB_ERR; +        } +        global.caps[i] = cap; +    } + +    return TB_OK; +} + +static int load_builtin_caps(void) { +    int i, j; +    const char *term = getenv("TERM"); + +    if (!term) { +        return TB_ERR_NO_TERM; +    } + +    // Check for exact TERM match +    for (i = 0; builtin_terms[i].name != NULL; i++) { +        if (strcmp(term, builtin_terms[i].name) == 0) { +            for (j = 0; j < TB_CAP__COUNT; j++) { +                global.caps[j] = builtin_terms[i].caps[j]; +            } +            return TB_OK; +        } +    } + +    // Check for partial TERM or alias match +    for (i = 0; builtin_terms[i].name != NULL; i++) { +        if (strstr(term, builtin_terms[i].name) != NULL || +            (*(builtin_terms[i].alias) != '\0' && +                strstr(term, builtin_terms[i].alias) != NULL)) +        { +            for (j = 0; j < TB_CAP__COUNT; j++) { +                global.caps[j] = builtin_terms[i].caps[j]; +            } +            return TB_OK; +        } +    } + +    return TB_ERR_UNSUPPORTED_TERM; +} + +static const char *get_terminfo_string(int16_t str_offsets_pos, +    int16_t str_offsets_len, int16_t str_table_pos, int16_t str_table_len, +    int16_t str_index) { +    const int str_byte_index = (int)str_index * (int)sizeof(int16_t); +    if (str_byte_index >= (int)str_offsets_len * (int)sizeof(int16_t)) { +        // An offset beyond the table indicates absent +        // See `convert_strings` in tinfo `read_entry.c` +        return ""; +    } +    const int16_t *str_offset = +        (int16_t *)(global.terminfo + (int)str_offsets_pos + str_byte_index); +    if ((char *)str_offset >= global.terminfo + global.nterminfo) { +        // str_offset points beyond end of entry +        // Truncated/corrupt terminfo entry? +        return NULL; +    } +    if (*str_offset < 0 || *str_offset >= str_table_len) { +        // A negative offset indicates absent +        // An offset beyond the table indicates absent +        // See `convert_strings` in tinfo `read_entry.c` +        return ""; +    } +    if (((size_t)((int)str_table_pos + (int)*str_offset)) >= global.nterminfo) { +        // string points beyond end of entry +        // Truncated/corrupt terminfo entry? +        return NULL; +    } +    return ( +        const char *)(global.terminfo + (int)str_table_pos + (int)*str_offset); +} + +static int wait_event(struct tb_event *event, int timeout) { +    int rv; +    char buf[TB_OPT_READ_BUF]; + +    memset(event, 0, sizeof(*event)); +    if_ok_return(rv, extract_event(event)); + +    fd_set fds; +    struct timeval tv; +    tv.tv_sec = timeout / 1000; +    tv.tv_usec = (timeout - (tv.tv_sec * 1000)) * 1000; + +    do { +        FD_ZERO(&fds); +        FD_SET(global.rfd, &fds); +        FD_SET(global.resize_pipefd[0], &fds); + +        int maxfd = global.resize_pipefd[0] > global.rfd +                        ? global.resize_pipefd[0] +                        : global.rfd; + +        int select_rv = +            select(maxfd + 1, &fds, NULL, NULL, (timeout < 0) ? NULL : &tv); + +        if (select_rv < 0) { +            // Let EINTR/EAGAIN bubble up +            global.last_errno = errno; +            return TB_ERR_POLL; +        } else if (select_rv == 0) { +            return TB_ERR_NO_EVENT; +        } + +        int tty_has_events = (FD_ISSET(global.rfd, &fds)); +        int resize_has_events = (FD_ISSET(global.resize_pipefd[0], &fds)); + +        if (tty_has_events) { +            ssize_t read_rv = read(global.rfd, buf, sizeof(buf)); +            if (read_rv < 0) { +                global.last_errno = errno; +                return TB_ERR_READ; +            } else if (read_rv > 0) { +                bytebuf_nputs(&global.in, buf, read_rv); +            } +        } + +        if (resize_has_events) { +            int ignore = 0; +            read(global.resize_pipefd[0], &ignore, sizeof(ignore)); +            // TODO: Harden against errors encountered mid-resize +            if_err_return(rv, update_term_size()); +            if_err_return(rv, resize_cellbufs()); +            event->type = TB_EVENT_RESIZE; +            event->w = global.width; +            event->h = global.height; +            return TB_OK; +        } + +        memset(event, 0, sizeof(*event)); +        if_ok_return(rv, extract_event(event)); +    } while (timeout == -1); + +    return rv; +} + +static int extract_event(struct tb_event *event) { +    int rv; +    struct bytebuf_t *in = &global.in; + +    if (in->len == 0) { +        return TB_ERR; +    } + +    if (in->buf[0] == '\x1b') { +        // Escape sequence? +        // In TB_INPUT_ESC, skip if the buffer is a single escape char +        if (!((global.input_mode & TB_INPUT_ESC) && in->len == 1)) { +            if_ok_or_need_more_return(rv, extract_esc(event)); +        } + +        // Escape key? +        if (global.input_mode & TB_INPUT_ESC) { +            event->type = TB_EVENT_KEY; +            event->ch = 0; +            event->key = TB_KEY_ESC; +            event->mod = 0; +            bytebuf_shift(in, 1); +            return TB_OK; +        } + +        // Recurse for alt key +        event->mod |= TB_MOD_ALT; +        bytebuf_shift(in, 1); +        return extract_event(event); +    } + +    // ASCII control key? +    if ((uint16_t)in->buf[0] < TB_KEY_SPACE || in->buf[0] == TB_KEY_BACKSPACE2) +    { +        event->type = TB_EVENT_KEY; +        event->ch = 0; +        event->key = (uint16_t)in->buf[0]; +        event->mod |= TB_MOD_CTRL; +        bytebuf_shift(in, 1); +        return TB_OK; +    } + +    // UTF-8? +    if (in->len >= (size_t)tb_utf8_char_length(in->buf[0])) { +        event->type = TB_EVENT_KEY; +        tb_utf8_char_to_unicode(&event->ch, in->buf); +        event->key = 0; +        bytebuf_shift(in, tb_utf8_char_length(in->buf[0])); +        return TB_OK; +    } + +    // Need more input +    return TB_ERR; +} + +static int extract_esc(struct tb_event *event) { +    int rv; +    if_ok_or_need_more_return(rv, extract_esc_user(event, 0)); +    if_ok_or_need_more_return(rv, extract_esc_cap(event)); +    if_ok_or_need_more_return(rv, extract_esc_mouse(event)); +    if_ok_or_need_more_return(rv, extract_esc_user(event, 1)); +    return TB_ERR; +} + +static int extract_esc_user(struct tb_event *event, int is_post) { +    int rv; +    size_t consumed = 0; +    struct bytebuf_t *in = &global.in; +    int (*fn)(struct tb_event *, size_t *); + +    fn = is_post ? global.fn_extract_esc_post : global.fn_extract_esc_pre; + +    if (!fn) { +        return TB_ERR; +    } + +    rv = fn(event, &consumed); +    if (rv == TB_OK) { +        bytebuf_shift(in, consumed); +    } + +    if_ok_or_need_more_return(rv, rv); +    return TB_ERR; +} + +static int extract_esc_cap(struct tb_event *event) { +    int rv; +    struct bytebuf_t *in = &global.in; +    struct cap_trie_t *node; +    size_t depth; + +    if_err_return(rv, cap_trie_find(in->buf, in->len, &node, &depth)); +    if (node->is_leaf) { +        // Found a leaf node +        event->type = TB_EVENT_KEY; +        event->ch = 0; +        event->key = node->key; +        event->mod = node->mod; +        bytebuf_shift(in, depth); +        return TB_OK; +    } else if (node->nchildren > 0 && in->len <= depth) { +        // Found a branch node (not enough input) +        return TB_ERR_NEED_MORE; +    } + +    return TB_ERR; +} + +static int extract_esc_mouse(struct tb_event *event) { +    struct bytebuf_t *in = &global.in; + +    enum { TYPE_VT200 = 0, TYPE_1006, TYPE_1015, TYPE_MAX }; + +    const char *cmp[TYPE_MAX] = {// +        // X10 mouse encoding, the simplest one +        // \x1b [ M Cb Cx Cy +        [TYPE_VT200] = "\x1b[M", +        // xterm 1006 extended mode or urxvt 1015 extended mode +        // xterm: \x1b [ < Cb ; Cx ; Cy (M or m) +        [TYPE_1006] = "\x1b[<", +        // urxvt: \x1b [ Cb ; Cx ; Cy M +        [TYPE_1015] = "\x1b["}; + +    int type = 0; +    int ret = TB_ERR; + +    // Unrolled at compile-time (probably) +    for (; type < TYPE_MAX; type++) { +        size_t size = strlen(cmp[type]); + +        if (in->len >= size && (strncmp(cmp[type], in->buf, size)) == 0) { +            break; +        } +    } + +    if (type == TYPE_MAX) { +        ret = TB_ERR; // No match +        return ret; +    } + +    size_t buf_shift = 0; + +    switch (type) { +        case TYPE_VT200: +            if (in->len >= 6) { +                int b = in->buf[3] - 0x20; +                int fail = 0; + +                switch (b & 3) { +                    case 0: +                        event->key = ((b & 64) != 0) ? TB_KEY_MOUSE_WHEEL_UP +                                                     : TB_KEY_MOUSE_LEFT; +                        break; +                    case 1: +                        event->key = ((b & 64) != 0) ? TB_KEY_MOUSE_WHEEL_DOWN +                                                     : TB_KEY_MOUSE_MIDDLE; +                        break; +                    case 2: +                        event->key = TB_KEY_MOUSE_RIGHT; +                        break; +                    case 3: +                        event->key = TB_KEY_MOUSE_RELEASE; +                        break; +                    default: +                        ret = TB_ERR; +                        fail = 1; +                        break; +                } + +                if (!fail) { +                    if ((b & 32) != 0) { +                        event->mod |= TB_MOD_MOTION; +                    } + +                    // the coord is 1,1 for upper left +                    event->x = ((uint8_t)in->buf[4]) - 0x21; +                    event->y = ((uint8_t)in->buf[5]) - 0x21; + +                    ret = TB_OK; +                } + +                buf_shift = 6; +            } +            break; +        case TYPE_1006: +            // fallthrough +        case TYPE_1015: { +            size_t index_fail = (size_t)-1; + +            enum { +                FIRST_M = 0, +                FIRST_SEMICOLON, +                LAST_SEMICOLON, +                FIRST_LAST_MAX +            }; + +            size_t indices[FIRST_LAST_MAX] = {index_fail, index_fail, +                index_fail}; +            int m_is_capital = 0; + +            for (size_t i = 0; i < in->len; i++) { +                if (in->buf[i] == ';') { +                    if (indices[FIRST_SEMICOLON] == index_fail) { +                        indices[FIRST_SEMICOLON] = i; +                    } else { +                        indices[LAST_SEMICOLON] = i; +                    } +                } else if (indices[FIRST_M] == index_fail) { +                    if (in->buf[i] == 'm' || in->buf[i] == 'M') { +                        m_is_capital = (in->buf[i] == 'M'); +                        indices[FIRST_M] = i; +                    } +                } +            } + +            if (indices[FIRST_M] == index_fail || +                indices[FIRST_SEMICOLON] == index_fail || +                indices[LAST_SEMICOLON] == index_fail) +            { +                ret = TB_ERR; +            } else { +                int start = (type == TYPE_1015 ? 2 : 3); + +                unsigned n1 = strtoul(&in->buf[start], NULL, 10); +                unsigned n2 = +                    strtoul(&in->buf[indices[FIRST_SEMICOLON] + 1], NULL, 10); +                unsigned n3 = +                    strtoul(&in->buf[indices[LAST_SEMICOLON] + 1], NULL, 10); + +                if (type == TYPE_1015) { +                    n1 -= 0x20; +                } + +                int fail = 0; + +                switch (n1 & 3) { +                    case 0: +                        event->key = ((n1 & 64) != 0) ? TB_KEY_MOUSE_WHEEL_UP +                                                      : TB_KEY_MOUSE_LEFT; +                        break; +                    case 1: +                        event->key = ((n1 & 64) != 0) ? TB_KEY_MOUSE_WHEEL_DOWN +                                                      : TB_KEY_MOUSE_MIDDLE; +                        break; +                    case 2: +                        event->key = TB_KEY_MOUSE_RIGHT; +                        break; +                    case 3: +                        event->key = TB_KEY_MOUSE_RELEASE; +                        break; +                    default: +                        ret = TB_ERR; +                        fail = 1; +                        break; +                } + +                buf_shift = in->len; + +                if (!fail) { +                    if (!m_is_capital) { +                        // on xterm mouse release is signaled by lowercase m +                        event->key = TB_KEY_MOUSE_RELEASE; +                    } + +                    if ((n1 & 32) != 0) { +                        event->mod |= TB_MOD_MOTION; +                    } + +                    event->x = ((uint8_t)n2) - 1; +                    event->y = ((uint8_t)n3) - 1; + +                    ret = TB_OK; +                } +            } +        } break; +        case TYPE_MAX: +            ret = TB_ERR; +    } + +    if (buf_shift > 0) { +        bytebuf_shift(in, buf_shift); +    } + +    if (ret == TB_OK) { +        event->type = TB_EVENT_MOUSE; +    } + +    return ret; +} + +static int resize_cellbufs(void) { +    int rv; +    if_err_return(rv, +        cellbuf_resize(&global.back, global.width, global.height)); +    if_err_return(rv, +        cellbuf_resize(&global.front, global.width, global.height)); +    if_err_return(rv, cellbuf_clear(&global.front)); +    if_err_return(rv, send_clear()); +    return TB_OK; +} + +static void handle_resize(int sig) { +    int errno_copy = errno; +    write(global.resize_pipefd[1], &sig, sizeof(sig)); +    errno = errno_copy; +} + +static int send_attr(uintattr_t fg, uintattr_t bg) { +    int rv; + +    if (fg == global.last_fg && bg == global.last_bg) { +        return TB_OK; +    } + +    if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_SGR0])); + +    uint32_t cfg, cbg; +    switch (global.output_mode) { +        default: +        case TB_OUTPUT_NORMAL: +            // The minus 1 below is because our colors are 1-indexed starting +            // from black. Black is represented by a 30, 40, 90, or 100 for fg, +            // bg, bright fg, or bright bg respectively. Red is 31, 41, 91, +            // 101, etc. +            cfg = (fg & TB_BRIGHT ? 90 : 30) + (fg & 0x0f) - 1; +            cbg = (bg & TB_BRIGHT ? 100 : 40) + (bg & 0x0f) - 1; +            break; + +        case TB_OUTPUT_256: +            cfg = fg & 0xff; +            cbg = bg & 0xff; +            if (fg & TB_HI_BLACK) cfg = 0; +            if (bg & TB_HI_BLACK) cbg = 0; +            break; + +        case TB_OUTPUT_216: +            cfg = fg & 0xff; +            cbg = bg & 0xff; +            if (cfg > 216) cfg = 216; +            if (cbg > 216) cbg = 216; +            cfg += 0x0f; +            cbg += 0x0f; +            break; + +        case TB_OUTPUT_GRAYSCALE: +            cfg = fg & 0xff; +            cbg = bg & 0xff; +            if (cfg > 24) cfg = 24; +            if (cbg > 24) cbg = 24; +            cfg += 0xe7; +            cbg += 0xe7; +            break; + +#if TB_OPT_ATTR_W >= 32 +        case TB_OUTPUT_TRUECOLOR: +            cfg = fg & 0xffffff; +            cbg = bg & 0xffffff; +            if (fg & TB_HI_BLACK) cfg = 0; +            if (bg & TB_HI_BLACK) cbg = 0; +            break; +#endif +    } + +    if (fg & TB_BOLD) +        if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_BOLD])); + +    if (fg & TB_BLINK) +        if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_BLINK])); + +    if (fg & TB_UNDERLINE) +        if_err_return(rv, +            bytebuf_puts(&global.out, global.caps[TB_CAP_UNDERLINE])); + +    if (fg & TB_ITALIC) +        if_err_return(rv, +            bytebuf_puts(&global.out, global.caps[TB_CAP_ITALIC])); + +    if (fg & TB_DIM) +        if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_DIM])); + +#if TB_OPT_ATTR_W == 64 +    if (fg & TB_STRIKEOUT) +        if_err_return(rv, bytebuf_puts(&global.out, TB_HARDCAP_STRIKEOUT)); + +    if (fg & TB_UNDERLINE_2) +        if_err_return(rv, bytebuf_puts(&global.out, TB_HARDCAP_UNDERLINE_2)); + +    if (fg & TB_OVERLINE) +        if_err_return(rv, bytebuf_puts(&global.out, TB_HARDCAP_OVERLINE)); + +    if (fg & TB_INVISIBLE) +        if_err_return(rv, +            bytebuf_puts(&global.out, global.caps[TB_CAP_INVISIBLE])); +#endif + +    if ((fg & TB_REVERSE) || (bg & TB_REVERSE)) +        if_err_return(rv, +            bytebuf_puts(&global.out, global.caps[TB_CAP_REVERSE])); + +    int fg_is_default = (fg & 0xff) == 0; +    int bg_is_default = (bg & 0xff) == 0; +    if (global.output_mode == TB_OUTPUT_256) { +        if (fg & TB_HI_BLACK) fg_is_default = 0; +        if (bg & TB_HI_BLACK) bg_is_default = 0; +    } +#if TB_OPT_ATTR_W >= 32 +    if (global.output_mode == TB_OUTPUT_TRUECOLOR) { +        fg_is_default = ((fg & 0xffffff) == 0) && ((fg & TB_HI_BLACK) == 0); +        bg_is_default = ((bg & 0xffffff) == 0) && ((bg & TB_HI_BLACK) == 0); +    } +#endif + +    if_err_return(rv, send_sgr(cfg, cbg, fg_is_default, bg_is_default)); + +    global.last_fg = fg; +    global.last_bg = bg; + +    return TB_OK; +} + +static int send_sgr(uint32_t cfg, uint32_t cbg, int fg_is_default, +    int bg_is_default) { +    int rv; +    char nbuf[32]; + +    if (fg_is_default && bg_is_default) { +        return TB_OK; +    } + +    switch (global.output_mode) { +        default: +        case TB_OUTPUT_NORMAL: +            send_literal(rv, "\x1b["); +            if (!fg_is_default) { +                send_num(rv, nbuf, cfg); +                if (!bg_is_default) { +                    send_literal(rv, ";"); +                } +            } +            if (!bg_is_default) { +                send_num(rv, nbuf, cbg); +            } +            send_literal(rv, "m"); +            break; + +        case TB_OUTPUT_256: +        case TB_OUTPUT_216: +        case TB_OUTPUT_GRAYSCALE: +            send_literal(rv, "\x1b["); +            if (!fg_is_default) { +                send_literal(rv, "38;5;"); +                send_num(rv, nbuf, cfg); +                if (!bg_is_default) { +                    send_literal(rv, ";"); +                } +            } +            if (!bg_is_default) { +                send_literal(rv, "48;5;"); +                send_num(rv, nbuf, cbg); +            } +            send_literal(rv, "m"); +            break; + +#if TB_OPT_ATTR_W >= 32 +        case TB_OUTPUT_TRUECOLOR: +            send_literal(rv, "\x1b["); +            if (!fg_is_default) { +                send_literal(rv, "38;2;"); +                send_num(rv, nbuf, (cfg >> 16) & 0xff); +                send_literal(rv, ";"); +                send_num(rv, nbuf, (cfg >> 8) & 0xff); +                send_literal(rv, ";"); +                send_num(rv, nbuf, cfg & 0xff); +                if (!bg_is_default) { +                    send_literal(rv, ";"); +                } +            } +            if (!bg_is_default) { +                send_literal(rv, "48;2;"); +                send_num(rv, nbuf, (cbg >> 16) & 0xff); +                send_literal(rv, ";"); +                send_num(rv, nbuf, (cbg >> 8) & 0xff); +                send_literal(rv, ";"); +                send_num(rv, nbuf, cbg & 0xff); +            } +            send_literal(rv, "m"); +            break; +#endif +    } +    return TB_OK; +} + +static int send_cursor_if(int x, int y) { +    int rv; +    char nbuf[32]; +    if (x < 0 || y < 0) { +        return TB_OK; +    } +    send_literal(rv, "\x1b["); +    send_num(rv, nbuf, y + 1); +    send_literal(rv, ";"); +    send_num(rv, nbuf, x + 1); +    send_literal(rv, "H"); +    return TB_OK; +} + +static int send_char(int x, int y, uint32_t ch) { +    return send_cluster(x, y, &ch, 1); +} + +static int send_cluster(int x, int y, uint32_t *ch, size_t nch) { +    int rv; +    char chu8[8]; + +    if (global.last_x != x - 1 || global.last_y != y) { +        if_err_return(rv, send_cursor_if(x, y)); +    } +    global.last_x = x; +    global.last_y = y; + +    int i; +    for (i = 0; i < (int)nch; i++) { +        uint32_t ch32 = *(ch + i); +        if (!iswprint((wint_t)ch32)) { +            ch32 = 0xfffd; // replace non-printable codepoints with U+FFFD +        } +        int chu8_len = tb_utf8_unicode_to_char(chu8, ch32); +        if_err_return(rv, bytebuf_nputs(&global.out, chu8, (size_t)chu8_len)); +    } + +    return TB_OK; +} + +static int convert_num(uint32_t num, char *buf) { +    int i, l = 0; +    char ch; +    do { +        buf[l++] = (char)('0' + (num % 10)); +        num /= 10; +    } while (num); +    for (i = 0; i < l / 2; i++) { +        ch = buf[i]; +        buf[i] = buf[l - 1 - i]; +        buf[l - 1 - i] = ch; +    } +    return l; +} + +static int cell_cmp(struct tb_cell *a, struct tb_cell *b) { +    if (a->ch != b->ch || a->fg != b->fg || a->bg != b->bg) { +        return 1; +    } +#ifdef TB_OPT_EGC +    if (a->nech != b->nech) { +        return 1; +    } else if (a->nech > 0) { // a->nech == b->nech +        return memcmp(a->ech, b->ech, a->nech); +    } +#endif +    return 0; +} + +static int cell_copy(struct tb_cell *dst, struct tb_cell *src) { +#ifdef TB_OPT_EGC +    if (src->nech > 0) { +        return cell_set(dst, src->ech, src->nech, src->fg, src->bg); +    } +#endif +    return cell_set(dst, &src->ch, 1, src->fg, src->bg); +} + +static int cell_set(struct tb_cell *cell, uint32_t *ch, size_t nch, +    uintattr_t fg, uintattr_t bg) { +    cell->ch = ch ? *ch : 0; +    cell->fg = fg; +    cell->bg = bg; +#ifdef TB_OPT_EGC +    if (nch <= 1) { +        cell->nech = 0; +    } else { +        int rv; +        if_err_return(rv, cell_reserve_ech(cell, nch + 1)); +        memcpy(cell->ech, ch, sizeof(ch) * nch); +        cell->ech[nch] = '\0'; +        cell->nech = nch; +    } +#else +    (void)nch; +    (void)cell_reserve_ech; +#endif +    return TB_OK; +} + +static int cell_reserve_ech(struct tb_cell *cell, size_t n) { +#ifdef TB_OPT_EGC +    if (cell->cech >= n) { +        return TB_OK; +    } +    if (!(cell->ech = tb_realloc(cell->ech, n * sizeof(cell->ch)))) { +        return TB_ERR_MEM; +    } +    cell->cech = n; +    return TB_OK; +#else +    (void)cell; +    (void)n; +    return TB_ERR; +#endif +} + +static int cell_free(struct tb_cell *cell) { +#ifdef TB_OPT_EGC +    if (cell->ech) { +        tb_free(cell->ech); +    } +#endif +    memset(cell, 0, sizeof(*cell)); +    return TB_OK; +} + +static int cellbuf_init(struct cellbuf_t *c, int w, int h) { +    c->cells = (struct tb_cell *)tb_malloc(sizeof(struct tb_cell) * w * h); +    if (!c->cells) { +        return TB_ERR_MEM; +    } +    memset(c->cells, 0, sizeof(struct tb_cell) * w * h); +    c->width = w; +    c->height = h; +    return TB_OK; +} + +static int cellbuf_free(struct cellbuf_t *c) { +    if (c->cells) { +        int i; +        for (i = 0; i < c->width * c->height; i++) { +            cell_free(&c->cells[i]); +        } +        tb_free(c->cells); +    } +    memset(c, 0, sizeof(*c)); +    return TB_OK; +} + +static int cellbuf_clear(struct cellbuf_t *c) { +    int rv, i; +    uint32_t space = (uint32_t)' '; +    for (i = 0; i < c->width * c->height; i++) { +        if_err_return(rv, +            cell_set(&c->cells[i], &space, 1, global.fg, global.bg)); +    } +    return TB_OK; +} + +static int cellbuf_get(struct cellbuf_t *c, int x, int y, +    struct tb_cell **out) { +    if (!cellbuf_in_bounds(c, x, y)) { +        *out = NULL; +        return TB_ERR_OUT_OF_BOUNDS; +    } +    *out = &c->cells[(y * c->width) + x]; +    return TB_OK; +} + +static int cellbuf_in_bounds(struct cellbuf_t *c, int x, int y) { +    if (x < 0 || x >= c->width || y < 0 || y >= c->height) { +        return 0; +    } +    return 1; +} + +static int cellbuf_resize(struct cellbuf_t *c, int w, int h) { +    int rv; + +    int ow = c->width; +    int oh = c->height; + +    if (ow == w && oh == h) { +        return TB_OK; +    } + +    w = w < 1 ? 1 : w; +    h = h < 1 ? 1 : h; + +    int minw = (w < ow) ? w : ow; +    int minh = (h < oh) ? h : oh; + +    struct tb_cell *prev = c->cells; + +    if_err_return(rv, cellbuf_init(c, w, h)); +    if_err_return(rv, cellbuf_clear(c)); + +    int x, y; +    for (x = 0; x < minw; x++) { +        for (y = 0; y < minh; y++) { +            struct tb_cell *src, *dst; +            src = &prev[(y * ow) + x]; +            if_err_return(rv, cellbuf_get(c, x, y, &dst)); +            if_err_return(rv, cell_copy(dst, src)); +        } +    } + +    tb_free(prev); + +    return TB_OK; +} + +static int bytebuf_puts(struct bytebuf_t *b, const char *str) { +    if (!str || strlen(str) <= 0) return TB_OK; // Nothing to do for empty caps +    return bytebuf_nputs(b, str, (size_t)strlen(str)); +} + +static int bytebuf_nputs(struct bytebuf_t *b, const char *str, size_t nstr) { +    int rv; +    if_err_return(rv, bytebuf_reserve(b, b->len + nstr + 1)); +    memcpy(b->buf + b->len, str, nstr); +    b->len += nstr; +    b->buf[b->len] = '\0'; +    return TB_OK; +} + +static int bytebuf_shift(struct bytebuf_t *b, size_t n) { +    if (n > b->len) { +        n = b->len; +    } +    size_t nmove = b->len - n; +    memmove(b->buf, b->buf + n, nmove); +    b->len -= n; +    return TB_OK; +} + +static int bytebuf_flush(struct bytebuf_t *b, int fd) { +    if (b->len <= 0) { +        return TB_OK; +    } +    ssize_t write_rv = write(fd, b->buf, b->len); +    if (write_rv < 0 || (size_t)write_rv != b->len) { +        // Note, errno will be 0 on partial write +        global.last_errno = errno; +        return TB_ERR; +    } +    b->len = 0; +    return TB_OK; +} + +static int bytebuf_reserve(struct bytebuf_t *b, size_t sz) { +    if (b->cap >= sz) { +        return TB_OK; +    } +    size_t newcap = b->cap > 0 ? b->cap : 1; +    while (newcap < sz) { +        newcap *= 2; +    } +    char *newbuf; +    if (b->buf) { +        newbuf = (char *)tb_realloc(b->buf, newcap); +    } else { +        newbuf = (char *)tb_malloc(newcap); +    } +    if (!newbuf) { +        return TB_ERR_MEM; +    } +    b->buf = newbuf; +    b->cap = newcap; +    return TB_OK; +} + +static int bytebuf_free(struct bytebuf_t *b) { +    if (b->buf) { +        tb_free(b->buf); +    } +    memset(b, 0, sizeof(*b)); +    return TB_OK; +} + +#endif // TB_IMPL | 
