[Devlog #002] Introducing TMUI

Abner Coimbre  —  1 year ago [Edited 16 hours, 2 minutes later]
Handmade Reader,

Happy Halloween!

I am wrapping up the revision for Ave's terminal UI code, from which a single-header library, TMUI (Terminal Mode User Interface... because TMUX is taken), is being formed. Initially, whenever I needed to render to the terminal, I would make a number of ncurses calls and move on. I would compress them into functions, but they weren't straightforward to reuse for new UI components. For example, to enable the user to type into a text field, I would call FillEntry(...):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
int FillEntry(win_t entry, string_t *buf)
{
    int ch;

    assert(entry.curses_win);
    assert(buf);

    while ((ch = wgetch(entry.curses_win)) != '\n')
    {
        switch (ch)
        {
            case '\t':
            case KEY_RESIZE:
                return ch;

            case BS:
            case DEL:
            {
                if (!buf->sz)
                    continue;
                memset(buf + buf->sz - 1, '\0', sizeof(char));
            } break;

            default:
            {
                if (buf.sz == ENTRY_LEN)
                    continue;
                memset(buf + buf->sz, ch, sizeof(char));
            } break;
        }

        UpdateEntry(entry, buf);
    }

    return ch;
}

The entry parameter simply stores a ncurses WINDOW and some properties about the window (e.g. width, height, hot, cold, etc.). Whichever char is "typed into the entry" will be stored in buf, and then I call UpdateEntry(...) to reflect that on le screen:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void UpdateEntry(win_t entry, string_t *buf)
{
    assert(entry.curses_win);
    assert(buf);
    werase(entry.curses_win);

    wattron(entry.curses_win, entry.attr);

    if (entry.active)
        waddch(entry.curses_win, '[');

    if (entry.private)
    {
        char hidden_buf[ENTRY_LEN+1] = {0};
        memset(hidden_buf, '*', buf->sz * sizeof(char));
        waddstr(entry.curses_win, hidden_buf + EntryOffset(hidden_buf));
    }
    else
    {
        waddstr(entry.curses_win, buf->ptr + EntryOffset(buf->ptr));
    }

    if (entry.active)
        waddch(entry.curses_win, ']');

    wattroff(entry.curses_win, entry.attr);

    wrefresh(entry.curses_win);
}

So these two defunct functions would for example power the text fields present on the now-familiar login screen:



They're defunct because they weren't useful when considering entries that strongly deviated from the traditional entry like large text boxes or search fields. The edge cases would make the tangled mess I wanted to avoid through compression in the first place. I decided properly defining data types for each UI component and some related functions would be fine, even if some repetition were to occur. Then have the main code use this API without considering ncurses at all, and avoid the need to compress any of its routines. They follow a naming convention similar to ncurses, however, so it's not too unfamiliar. Below is the addch and delch tmui versions for the new txt_field_t types:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void tmui_txt_field_addch(txt_field_t *t, char ch)
{
    int bufsz;
    int titlesz;
    int vischrs; /* visible-characters-allowed count */

    assert(t);
    assert(t->win);
    assert(t->hot);

    bufsz = strlen(t->buf);
    titlesz = strlen(t->title);
    vischrs = TXT_FIELD_COLS - titlesz - 3;
    
    if (bufsz >= TXT_FIELD_MAX_LEN)
        return;

    memset(t->buf + bufsz++, ch, sizeof(char));

    ch = t->private ? '*' : ch;

    if (bufsz > vischrs && !t->private)
    {
        mvwaddstr(t->win, 0, titlesz+2, t->buf + ++t->offset);
        mvwaddch(t->win, 0, TXT_FIELD_COLS-2, ch);
    }
    else 
    {
        mvwaddch(t->win, 0, titlesz + bufsz + 1, ch);
        waddch(t->win, CLOSING_SYMBOLS[t->type]);
    }

    wrefresh(t->win);
}


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void tmui_txt_field_delch(txt_field_t *t)
{
    int bufsz;
    int titlesz;
    int vischrs; /* visible-characters-allowed count */

    assert(t);
    assert(t->win);
    assert(t->hot);

    bufsz = strlen(t->buf);

    if (!bufsz)
        return;

    titlesz = strlen(t->title);
    vischrs = TXT_FIELD_COLS - titlesz - 3;

    memset(t->buf+bufsz-1, 0, sizeof(char));

    if (bufsz-- > vischrs && !t->private)
    {
        mvwaddstr(t->win, 0, titlesz+2, t->buf + --t->offset);
    }
    else
    {
        mvwaddch(t->win, 0, titlesz + bufsz + 2, CLOSING_SYMBOLS[t->type]);
        mvwaddch(t->win, 0, titlesz + bufsz + 3, ' ');
    }

    wrefresh(t->win);
}

[Note: Funny how properly deleting a character is a little involved. We shouldn't dismiss the simple as trivial.]

These handle private text fields, a title, the offset when characters go over the width, and the style of the text field (curly braces, regular braces, parentheses, etc.). The text field itself now holds the contents typed in so far. There are other functions like txt_field_set_hot (whether to highlight it or not), txt_field_update (which replaced the generic FillEntry), etc. And there are similar functions for text boxes, buttons, labels, email items, etc... except when they're not similar. Then they have their own unique routines, and I call those instead :)

Of course, if these UI components ever proliferate, a less repetitive strategy would be wiser.



All of what you see are their own set of UI components now, with a proper barrier between my code and ncurses! One can introduce the notion of backends, and switch between an ncurses implementation of tmui with some future one. Of course, this isn't anything terribly new or exciting in the world of software development. However, now I may trivially add terminal theming using tmui, after which I complete Ave Terminal 1.0, and give my full attention to the graphical mode user interface (GMUI? No?).

Until November's monthly update. We'll see if I get to stream too (probably not due to Handmade Con!). Previous stream may be found here
#9172 Mārtiņš Možeiko  —  1 year ago
Nice!
Btw, for recording terminal I recommend using https://asciinema.org/
#9174 Abner Coimbre  —  1 year ago
This is awesome. I will try to use it for the next devlog!
#9179 Simon Anciaux  —  1 year ago
Do you intend to release Ave for windows ?
In the screenshot, you seem to spend a lot of screen space on borders, and as a result it only shows 5 e-mails. Is it close to the final "design" or will it be more compact ?
#9181 Abner Coimbre  —  1 year ago [Edited 3 minutes later]
Appreciate your questions.

mrmixer
Do you intend to release Ave for windows ?

Terminal Ave is for Linux, but it might work with Windows Bash. Graphical Ave will have Windows support.

mrmixer
You seem to spend a lot of screen space on borders, and as a result it only shows 5 e-mails.

Yeah there are two things I could do: (1) Move the e-mail items further up while compressing the space between each and (2) Allow more if the user grows the terminal window. I'll take these into consideration, so long as they don't hamper the simple visuals.
#9183 Mārtiņš Možeiko  —  1 year ago [Edited 0 minutes later]
Look how many e-mails you can fit on screen without any borders: http://i.imgur.com/3Pcgv6W.png (not mine e-mail, just a random pic from internet).
Log in to comment