# Getting started

Termpaint is a low level terminal interface library. The output model is a grid of character cells with attributes specifying colors and style (like bold or underline).

Input from the terminal is passed into the library which then calls a user-provided event callback with e.g. character or key press events.

The core of termpaint does not do any operating system or environment specific I/O. In the general case the application provides an integration pointer and callbacks for all terminal I/O. This integration can connect to an existing event loop or just wrap blocking terminal I/O.

For applications that deal with any kind of asynchronous events, apart from the terminal, it’s recommended to use a custom integration or an external integration into an general event loop. But for simple applications that only deal with synchronous data sources termpaint ships with a very simplistic sample integration in a add-on called termpaintx. This integration helps to get started quickly for simple applications. Let’s start with a “Hello World” style application.

## Hello world

First you need to include the pkg-config dependency named `termpaint` into your application’s build. Next include the needed headers into your source:

includes

    #include "termpaint.h"
    #include "termpaintx.h"

The most code is for the main function:

main code

     1termpaint_integration *integration;
     2termpaint_terminal *terminal;
     3termpaint_surface *surface;
     4
     5bool quit = false;
     6
     7integration = termpaintx_full_integration_setup_terminal_fullscreen(
     8            "+kbdsig +kbdsigint",
     9            event_callback, &quit,
    10            &terminal);
    11surface = termpaint_terminal_get_surface(terminal);
    12termpaint_surface_clear(surface,
    13            TERMPAINT_DEFAULT_COLOR, TERMPAINT_DEFAULT_COLOR);
    14termpaint_surface_write_with_colors(surface,
    15            0, 0,
    16            "Hello World",
    17            TERMPAINT_DEFAULT_COLOR, TERMPAINT_DEFAULT_COLOR);
    18
    19termpaint_terminal_flush(terminal, false);
    20
    21while (!quit) {
    22    if (!termpaintx_full_integration_do_iteration(integration)) {
    23        // some kind of error
    24        break;
    25    }
    26}
    27
    28termpaint_terminal_free_with_restore(terminal);

This code starts with a few variables needed later. `integration` stores a pointer to the `integration object` needed after initialization to run keyboard input handling. `terminal` is used for interacting with the terminal as a whole and `surface` for interaction with the character cell grid. Finally `quit` is used for communication between the event handler (see below) and the loop waiting in the main function.

The first call uses `termpaintx_full_integration_setup_terminal_fullscreen()` to setup termpaint for full screen usage with the simple termpaintx integration. Its first arguement is a string with options for termpaint and options for termpaintx, followed by the event callback and it’s data pointer. The last parameter is an out-parameter and together with the return value they produce the pointers to the integration object and the terminal object.

The following line extracts the pointer to the primary surface of the terminal object. Surfaces in termpaint are objects that represent a character cell grid that have functions to allow writing text.

The next line clears the whole primary surface (which later is transferred to the terminal) with the given colors. It takes two colors because some terminals behave oddly if cleared space has the same foreground and background color set [1]. When two colors are specified in termpaint the order is always foreground color and then background color. In this example the default colors are used. These are special colors defined by the terminal. Additionally named, indexed and rgb colors are available.

`termpaint_surface_write_with_colors()` places text on the surface. It takes the position for the text to start, the text itself (in utf-8) and the colors. In termpaint coordinates are always 0-based and start in the upper-left corner.

The next line actually displays the content of the primary surface by calling `termpaint_terminal_flush()`.

The code that follows waits for the user to press `q` or the escape key. `termpaintx_full_integration_do_iteration()` waits for input from the terminal and lets termpaint process the data into events. For deciding if the input matches this example uses the event callback:

event callback

     1void event_callback(void *userdata, termpaint_event *event) {
     2    bool *quit = userdata;
     3    if (event->type == TERMPAINT_EV_CHAR) {
     4        if (event->c.length == 1 && event->c.string[0] == 'q') {
     5            *quit = true;
     6        }
     7    }
     8    if (event->type == TERMPAINT_EV_KEY) {
     9        if (event->key.atom == termpaint_input_escape()) {
    10            *quit = true;
    11        }
    12    }
    13}

This is a very simple callback only suitable for the example. Termpaint uses events of various types. But the most important events are key and character events. Character events (`TERMPAINT_EV_CHAR`) are emitted for printable characters like letters. Key events (`TERMPAINT_EV_KEY`) are for other keys like function keys, enter or space. Both additionally contain a bitflag that describes modifiers like alt and ctrl that have been held down while pressing the key. The strings are not nul-terminated. For key events the `atom` field can either be used as a string or directly compared to the return of functions like `termpaint_input_escape()`. See Keys for a complete list of keys.

Finally before termination the application `termpaint_terminal_free_with_restore()` should be called to restore the terminal to it’s normal state.

See `getting-started.c` for the whole source of this example.

## Input handling strategies

### Synchronous usage

Termpaint uses an event callback based design for handling input events. This fits well into designs which use a central event processing loop but out of the box it does not fit well for code that wants to synchronously wait for input in many places while processing, e.g. a usage styles where a menu or such is implemented by a function that only returns after the user has completed interacting with it.

For such synchronous usage it’s recommended to use an callback that just adds events to a application specific queue of events. And then implement a function that passes input from the terminal to termpaint until a new event appears.

In this way the application can also filter out events, it does not need, in these functions. As termpaint tries to avoid allocations in steady state operation, the event callback has to copy all needed data out of the original event to a safe place.

First we need some data structures and variables to save the events:

    1typedef struct event_ {
    2    int type;
    3    int modifier;
    4    char *string;
    5    struct event_* next;
    6} event;
    7
    8event* event_current; // unprocessed event
    9event* event_saved; // event to free on next call

Here the key or character is represented by a freshly allocated nul-terminated string.

The events are copied into this structure in the event handler:

     1void event_callback(void *userdata, termpaint_event *tp_event) {
     2    (void)userdata;
     3    // remember tp_event is only valid while this callback runs, so copy everything we need.
     4    event *copied_event = NULL;
     5    if (tp_event->type == TERMPAINT_EV_CHAR) {
     6        copied_event = malloc(sizeof(event));
     7        copied_event->type = tp_event->type;
     8        copied_event->modifier = tp_event->c.modifier;
     9        copied_event->string = strndup(tp_event->c.string, tp_event->c.length);
    10        copied_event->next = NULL;
    11    } else if (tp_event->type == TERMPAINT_EV_KEY) {
    12        copied_event = malloc(sizeof(event));
    13        copied_event->type = tp_event->type;
    14        copied_event->modifier = tp_event->key.modifier;
    15        copied_event->string = strndup(tp_event->key.atom, tp_event->key.length);
    16        copied_event->next = NULL;
    17    }
    18
    19    if (copied_event) {
    20        if (!event_current) {
    21            event_current = copied_event;
    22        } else {
    23            event* prev = event_current;
    24            while (prev->next) {
    25                prev = prev->next;
    26            }
    27            prev->next = copied_event;
    28        }
    29    }
    30}

In this case only key or character events are translated, all other events are filtered out. For the events the type, string and modifiers are copied and the new event is added to a linked list.

Next we need a function for the application to call to wait for the next event:

     1event* key_wait(termpaint_integration *integration) {
     2    while (!event_current) {
     3        if (!termpaintx_full_integration_do_iteration(integration)) {
     4            // some kind of error
     5            return NULL; // or some other error handling
     6        }
     7    }
     8
     9    if (event_saved) {
    10        free(event_saved->string);
    11        free(event_saved);
    12    }
    13    event *ret = event_current;
    14    event_current = ret->next;
    15    event_saved = ret;
    16    return ret;
    17}

The `key_wait` function either returns an already queued event or if no event is queued it calls `termpaintx_full_integration_do_iteration()` to wait for terminal data and process it. As soon as enough data is read and processed to give one or more suitable events it stops waiting for data and returns the first event. To easy usage this implementation internally handles freeing the last returned event in the next `key_wait` call.

Now the application can synchronously wait for events like this:

     1termpaint_terminal_flush(terminal, false);
     2
     3while (true) {
     4    event *event = key_wait(integration);
     5    if (event->type == TERMPAINT_EV_CHAR) {
     6        if (strcmp(event->string, "q") == 0) {
     7            break;
     8        }
     9    }
    10    if (event->type == TERMPAINT_EV_KEY) {
    11        if (strcmp(event->string, "Escape") == 0) {
    12            break;
    13        }
    14    }
    15}

Remember to flush the primary surface to make sure that the user can see the most recent content while the application waits for input. Or maybe even move the flush into the wait function, if so desired.

Given such an synchronous input function, parts of the user interface can be written as stand-alone functions like this:

     1bool quit_menu(termpaint_terminal *terminal, termpaint_integration *integration) {
     2    termpaint_surface *surface;
     3    surface = termpaint_terminal_get_surface(terminal);
     4
     5    int fg = TERMPAINT_DEFAULT_COLOR;
     6    int bg = TERMPAINT_DEFAULT_COLOR;
     7
     8    termpaint_surface_write_with_colors(surface, 20, 4, "Really quit? (y/N)",
     9                                        fg, bg);
    10
    11    termpaint_terminal_flush(terminal, false);
    12
    13    while (true) {
    14        event *event = key_wait(integration);
    15        if (event->type == TERMPAINT_EV_CHAR) {
    16            if (!strcmp(event->string, "y") || !strcmp(event->string, "Y")) {
    17                return true;
    18            }
    19            if (!strcmp(event->string, "n") || !strcmp(event->string, "N")) {
    20                return false;
    21            }
    22        }
    23        termpaint_surface_write_with_colors(surface, 20, 5,
    24                    "Please reply with either 'y' for yes or 'n' for no.",
    25                    fg, bg);
    26
    27        termpaint_terminal_flush(terminal, false);
    28    }
    29}

See `sync-event-handling.c` for the whole source of this example.

### Callback usage

Using callbacks directly needs some kind of event dispatching in the application. There are many ways this could be done, ranging in granularity from having one event processing function for the whole application over a function per “page” or having a fine grained “widget” structure with elaborate event routing rules.

A simple way to do event routing is to have global variables for an event handler and a void pointer for it’s data and switch those around while the user moves through the application. A very rough version of something like this could look like this:

Some global variables for drawing and event handling:

    1termpaint_terminal *terminal;
    2termpaint_surface *surface;
    3
    4void (*current_callback)(void *user_data, termpaint_event* event);
    5void *current_data;

Instead of passing the terminal and surface object around the whole application as parameters, having them as globals simplifies the code a lot.

The global `current_callback` and it’s `current_data` allows switching which function will get events to process while the user moves through the application.

    1void event_callback(void *userdata, termpaint_event *event) {
    2    (void)userdata;
    3    if (current_callback) {
    4        current_callback(current_data, event);
    5    }
    6}

The callback passed to the terminal object just passes the event on to the currently active event processing function.

The following is a simple confirm dialog. It can be started with a pointer to a bool where it stores it’s result. When started it replaces the currently active event handler with it’s own. When it is finished it saves the result, restores the previous handler and calls it without an event to indicate that the result is ready.

First the confirm dialog needs a place to store it’s data:

    1typedef struct quit_dialog_ {
    2    void (*saved_callback)(void *user_data, termpaint_event* event);
    3    void *saved_data;
    4    bool *result;
    5} quit_dialog;

Than the start function can save the old event handler and the result pointer and draw the initial user interface:

     1void quit_dialog_start(bool *result) {
     2    quit_dialog* dlg = calloc(1, sizeof(quit_dialog));
     3    dlg->saved_data = current_data;
     4    dlg->saved_callback = current_callback;
     5    dlg->result = result;
     6
     7    current_data = dlg;
     8    current_callback = quit_dialog_callback;
     9
    10    int fg = TERMPAINT_DEFAULT_COLOR;
    11    int bg = TERMPAINT_DEFAULT_COLOR;
    12
    13    termpaint_surface_write_with_colors(surface, 20, 4, "Really quit? (y/N)",
    14                                        fg, bg);
    15
    16    termpaint_terminal_flush(terminal, false);
    17}

Finally the event handling function reacts to user input and when finished saves the result, restores the event handler and calls it:

     1void quit_dialog_callback(void *userdata, termpaint_event *event) {
     2    quit_dialog* dlg = userdata;
     3
     4    if (!event) {
     5        return;
     6    }
     7
     8    if (event->type == TERMPAINT_EV_CHAR) {
     9        if (event->c.length == 1
    10                && (event->c.string[0] == 'y' || event->c.string[0] == 'Y')) {
    11            current_data = dlg->saved_data;
    12            current_callback = dlg->saved_callback;
    13            *dlg->result = true;
    14            free(dlg);
    15            current_callback(current_data, NULL);
    16            return;
    17        }
    18        if (event->c.length == 1
    19                && (event->c.string[0] == 'n' || event->c.string[0] == 'N')) {
    20            current_data = dlg->saved_data;
    21            current_callback = dlg->saved_callback;
    22            *dlg->result = false;
    23            free(dlg);
    24            current_callback(current_data, NULL);
    25            return;
    26        }
    27    }
    28    if (event->type == TERMPAINT_EV_CHAR || event->type == TERMPAINT_EV_KEY) {
    29        termpaint_surface_write_with_colors(surface, 20, 5,
    30                    "Please reply with either 'y' for yes or 'n' for no.",
    31                    TERMPAINT_DEFAULT_COLOR, TERMPAINT_DEFAULT_COLOR);
    32
    33        termpaint_terminal_flush(terminal, false);
    34    }
    35}

When using callbacks as primary means of composing the application logic, the main function just needs to wait for events in one central place.

Thus the central loop can simply be:

    1while (!quit) {
    2    if (!termpaintx_full_integration_do_iteration(integration)) {
    3        // some kind of error
    4        break;
    5    }
    6}
    7
    8termpaint_terminal_free_with_restore(terminal);

See `callback-event-handling.c` for the whole source of this example.

### Event loop usage

Existing event loops differ in their API in various details, but the general approach is fairly similar between common event loops.

Currently the only known library to integrate termpaint into a general purpose event loop is Tui Widgets which provides a widget framework using the event loop from QtCore.

The general steps are:

- Setup the operating system terminal I/O interface for unbuffered input and output processing

  On *nix like operation systems this generally means saving the current settings via `tcgetattr` and configuring the needed new settings. Perhaps using `termpaintx_fd_set_termios()`.

- Bridge input from the terminal file descriptor to termpaint

  After registering an input available notifier a typical implemantation would be:

      gsize amount;
      g_io_channel_read_chars(channel, buf, sizeof(buf) - 1, &amount, NULL);

      termpaint_terminal_add_input_data(terminal, buf, amount);

- Pass output by termpaint to the operation system terminal interface

  For best performance the output from termpaint should be buffered and send in reasonable blocks to the terminal.

  The integration will receive output date via it’s `write` callback and needs to flush it’s output buffer when the `flush` callback is called.

For best performance the integration should additionally use `termpaint_integration_set_request_callback()` to enable a mechanism for a delayed callback used in cases where terminal input can not be readily parsed without knowing if additional data is on it’s way from the terminal to the application.

Footnotes

[1]

TERMPAINT_DEFAULT_COLOR is special as it is replaced by the terminals with globally set colors. So using TERMPAINT_DEFAULT_COLOR as foreground and background doesn’t actually count as using the same color.