Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Console C API

The functions a client of the Console module calls: the helpers of C_EDITLINE (keyboard, TAB completion, inline hints, history) and the ncurses helpers the ycli windows draw with.

Source code:

Every editline_* function takes the C_EDITLINE gobj as its first argument and reads its private data with no check of the gclass: pass it the gobj gobj_create_service(..., C_EDITLINE, ...) returned, nothing else.

The canonical client is ycli (utils/c/ycli/src/c_cli.c): it creates the editline, registers both callbacks, then opens the keyboard:

priv->gobj_editline = gobj_create_service(
    "editline",
    C_EDITLINE,
    kw_editline,    // prompt, history_file, use_ncurses, colors, cx, cy, subscriber
    gobj
);
editline_set_completion_callback(priv->gobj_editline, cli_completion_cb, gobj);
editline_set_hints_callback(priv->gobj_editline, cli_hints_cb, cli_free_hint_cb, gobj);
priv->tty_fd = tty_keyboard_init();     // then a yev read event feeds EV_KEYCHAR

tty_keyboard_init()

tty_keyboard_init() gives the caller a file descriptor of the terminal to read the keyboard from: a dup() of STDIN_FILENO, marked close-on-exec and put in raw mode (no echo, no line buffering, Ctrl+C and Ctrl+Z arrive as keys instead of signals). The first call registers an atexit() handler that puts the terminal back as it was. The editline does not read the descriptor: the caller reads it (ycli, ycommand and mqtt_tui, with a yev_create_read_event()) and sends each key to the editline as EV_KEYCHAR.

int tty_keyboard_init(void);

Parameters — none.

Returns

The new file descriptor, or -1 when the terminal cannot be put in raw mode, already logged: “NOT a TTY” when stdin is not a terminal (a pipe, a cron job, no login session), “tcgetattr() FAILED” / “tcsetattr() FAILED” with the errno. ycli then says “Cannot open a tty device. Are you in a valid login session?” and stops.

Example

int tty_fd = tty_keyboard_init();
if(tty_fd < 0) {
    return -1;  // Error already logged
}
yev_event_h yev_reading = yev_create_read_event(
    yuno_event_loop(),
    yev_callback,
    gobj,
    tty_fd,
    gbuffer_create(1024, 1024)
);

editline_set_completion_callback()

editline_set_completion_callback() registers the function the editline calls when TAB is pressed. The callback receives the current line and fills a list of candidates with editline_add_completion(): none beeps, one replaces the line at once, several are shown to pick from (with their descriptions). A second call replaces the first; cb = NULL disables completion.

typedef void (*editline_completion_cb_t)(
    hgobj gobj,                     // the C_EDITLINE gobj
    const char *buf,                // the line as typed so far
    editline_completions_t *lc,     // fill it with editline_add_completion()
    void *user_data
);

void editline_set_completion_callback(
    hgobj gobj,
    editline_completion_cb_t cb,
    void *user_data
);

Parameters

KeyTypeDescription
gobjhgobjThe C_EDITLINE gobj.
cbeditline_completion_cb_tThe completion callback, or NULL to disable completion.
user_datavoid *Handed back to cb on every call (ycli passes its own gobj).

Returns — nothing.

Example — complete the first word against a table of commands:

PRIVATE void my_completion_cb(
    hgobj editline_gobj,
    const char *buf,
    editline_completions_t *lc,
    void *user_data
) {
    static const char *commands[] = {"help", "history", "list-yunos", 0};
    size_t len = strlen(buf);
    for(int i = 0; commands[i]; i++) {
        if(strncmp(commands[i], buf, len) == 0) {
            editline_add_completion(lc, commands[i], NULL);
        }
    }
}

editline_set_completion_callback(gobj_editline, my_completion_cb, gobj);

editline_add_completion()

editline_add_completion() appends one candidate to the list a completion callback is filling. Both strings are copied (with gbmem_*), and the editline frees the list after the TAB is handled: the caller keeps ownership of what it passed.

void editline_add_completion(
    editline_completions_t *lc,
    const char *str,
    const char *desc
);

Parameters

KeyTypeDescription
lceditline_completions_t *The list the callback received.
strconst char *The WHOLE line that replaces the current one if this candidate is picked, not only the word being completed.
descconst char *A description shown next to the candidate when there are several; NULL or "" for none.

Returns — nothing. When memory cannot be allocated the candidate is not added and nothing is logged (the allocator logs it).

Example — complete the last word and keep what was typed before it, as ycli does for the parameters of a command:

const char *last = strrchr(buf, ' ');
last = last ? last + 1 : buf;
char candidate[1024];
snprintf(candidate, sizeof(candidate), "%.*s%s",
    (int)(last - buf), buf,     // the head of the line, untouched
    "service="                  // the word that completes the last token
);
editline_add_completion(lc, candidate, "name of the service to command");

editline_set_hints_callback()

editline_set_hints_callback() registers the function that draws an inline hint: a text shown to the right of the cursor, in gray by default, on every refresh of the line (ycli shows there the parameters a command still takes). The callback returns NULL for no hint, or a string the editline draws and then hands to free_cb. A second call replaces the first; cb = NULL disables hints.

typedef char *(*editline_hints_cb_t)(
    hgobj gobj,             // the C_EDITLINE gobj
    const char *buf,        // the line as typed so far
    int *out_color,         // ANSI SGR foreground color, default 90 (gray)
    int *out_bold,          // 1 bold, 0 normal
    void *user_data
);
typedef void (*editline_free_hint_cb_t)(
    char *hint,
    void *user_data
);

void editline_set_hints_callback(
    hgobj gobj,
    editline_hints_cb_t cb,
    editline_free_hint_cb_t free_cb,
    void *user_data
);

Parameters

KeyTypeDescription
gobjhgobjThe C_EDITLINE gobj.
cbeditline_hints_cb_tThe hints callback, or NULL to disable hints.
free_cbeditline_free_hint_cb_tCalled with each hint after it is drawn, so the callback may use any allocator. With NULL nothing is freed: pass NULL only when cb returns static strings.
user_datavoid *Handed back to cb and free_cb.

Returns — nothing.

Example

PRIVATE char *my_hints_cb(
    hgobj editline_gobj,
    const char *buf,
    int *out_color,
    int *out_bold,
    void *user_data
) {
    if(strcmp(buf, "kill-yuno") == 0) {
        *out_color = 90;
        *out_bold = 0;
        return gbmem_strdup(" id=<yuno id>");
    }
    return NULL;
}

PRIVATE void my_free_hint_cb(char *hint, void *user_data)
{
    gbmem_free(hint);
}

editline_set_hints_callback(gobj_editline, my_hints_cb, my_free_hint_cb, gobj);

editline_history_count()

editline_history_count() returns the number of entries of the in-memory history. The empty slot the editline keeps for the line being edited is not counted, so the numbers match what a history listing shows and what a !N expansion refers to.

int editline_history_count(hgobj gobj);

Parameters

KeyTypeDescription
gobjhgobjThe C_EDITLINE gobj.

Returns

The number of history entries, 0 when there is none.

Example — see editline_history_get().


editline_history_get()

editline_history_get() returns one entry of the in-memory history, numbered from 1 (the oldest) to editline_history_count() (the newest). History is de-duplicated on insert: a line typed again moves to the newest place.

const char *editline_history_get(hgobj gobj, int idx);

Parameters

KeyTypeDescription
gobjhgobjThe C_EDITLINE gobj.
idxintThe 1-based index of the entry.

Returns

The entry, owned by the editline (do not free it, and do not keep it past the next line entered: the history may be reordered or trimmed). NULL when idx is out of range.

Example — ycli’s !history [filter]:

int n = editline_history_count(gobj_editline);
for(int i = 1; i <= n; i++) {
    const char *h = editline_history_get(gobj_editline, i);
    if(!h || !*h) {
        continue;
    }
    if(!empty_string(filter) && !strstr(h, filter)) {
        continue;
    }
    printf("%4d  %s\n", i, h);
}

open_ncurses()

open_ncurses() starts ncurses on the terminal: it honours the user’s locale first (setlocale(LC_ALL, ""), so wide ncurses decodes UTF-8 and counts multi-cell glyphs), then initscr(), cbreak(), noecho(), the keypad on, and halfdelay(1) -- a read waits a tenth of a second at most. When the terminal has colors it starts them with the terminal’s own default foreground and background (-1), which get_paint_color() needs.

WINDOW *open_ncurses(hgobj gobj);

Parameters

KeyTypeDescription
gobjhgobjNot used.

Returns

The stdscr window. NULL when ncurses is ALREADY open: the call does nothing, and the caller keeps the window it got the first time.

Notes

Pair it with close_ncurses() on every exit path, or the terminal is left in raw mode. ycli registers the close with atexit() before opening.

Example

PRIVATE void my_close_ncurses(void)
{
    close_ncurses();
}

atexit(my_close_ncurses);
WINDOW *wn = open_ncurses(gobj);

close_ncurses()

close_ncurses() ends ncurses (endwin()) and gives the terminal back as it was. It does nothing when ncurses is not open, so it is safe to call twice (from an atexit() handler and from a normal stop).

void close_ncurses(void);

Parameters — none.

Returns — nothing.

Example — see open_ncurses().


get_paint_color()

get_paint_color() turns a pair of color names into the ncurses attribute that paints with them, creating the color pair the first time the combination is asked for and reusing it afterwards.

int get_paint_color(const char *fg_color, const char *bg_color);

Parameters

KeyTypeDescription
fg_colorconst char *Foreground: black, red, green, yellow, blue, magenta, cyan or white (case does not matter). NULL, "", default, def or an unknown name is the terminal’s default color.
bg_colorconst char *Background, same names.

Returns

The attribute (COLOR_PAIR(n)) to give wattron() or wbkgdset(). 0, no color attribute, when the terminal has no colors, ncurses is not open, a color is beyond what the terminal supports, or the color pairs are exhausted: a 0 paints with the default colors, and is not an error.

Example — the default background of a ycli window, then its text color:

int def_attr = get_paint_color(
    gobj_read_str_attr(gobj, "fg_color"),
    gobj_read_str_attr(gobj, "bg_color")
);
if(def_attr) {
    wbkgdset(wn, ' ' | def_attr);
}

int attr = get_paint_color("white", "blue");
if(attr) {
    wattron(wn, attr);
}