Roguelike authors might be interested in using Unicode in curses, particularly for drawing Codepage 437 symbols in modern OSes, or just to get “beautiful” characters like Brogue.

This post is more-or-less an updated version of this howto page from 2014:

This page explains the new API well with some function cheatsheet too:

Journeyman on Discord has written a small header to make CP437 symbols easier in C:

Prerequisites

This assumes you have the following:

  • A terminal which supports Unicode - mate-terminal and guake do for me
  • A font with Unicode ligatures - PxPlus IBM VGA 8x16 does for me
  • Know how to type Unicode symbols on your system. On Linux I do:
    • MATE desktop: Ctrl+Shift and hold them, u and release it, type hex code, release all keys
    • vim Insert Mode: Ctrl+v and release it, u and release it, type hex code
  • The ncurses and ncursesw library package, development package, documentation package. On Ubuntu 22.04 this is at least: libncurses6 libncursesw6 libncurses-dev ncurses-doc

man ncurses

There are two common configurations of the library:

ncursesw
  the so-called "wide"  library, which handles multibyte characters (see the
  section on ALTERNATE CONFIGURATIONS). The "wide" library includes all of the
  calls from the "normal" library.  It adds about one third more calls using
  data types which store multibyte characters:

  cchar_t
    corresponds to chtype. However it is a structure, because more data is
    stored than can fit into an integer. The characters are large enough to
    require a full integer value - and there may be more than one character per
    cell. The video attributes and color are stored in separate fields of the
    structure.

    Each cell (row and column) in a WINDOW is stored as a cchar_t.

    The setcchar(3X) and getcchar(3X) functions store and retrieve the data
    from a cchar_t structure.

  wchar_t
    stores a “wide” character. Like chtype, this may be an integer.

  wint_t
    stores a wchar_t or WEOF - not the same, though both may have the same size.

  The "wide" library provides new functions which are analogous to functions in
  the "normal" library. There is a naming convention which relates many of the
  normal/wide variants: a "_w" is inserted into the name.  For example, waddch
  becomes wadd_wch.
ALTERNATE CONFIGURATIONS

  --enable-widec
    The configure script renames the library and (if the --disable-overwrite
    option is used) puts the header files in a different subdirectory. All of
    the library names have a "w" appended to them, i.e., instead of

      -lncurses

    you link with

      -lncursesw

    You must also enable the wide-character features in the header file when
    compiling for the wide-character library to use the extended
    (wide-character) functions. The symbol which enables these features has
    changed since XSI Curses, Issue 4:

    * Originally, the wide-character feature required the symbol
      _XOPEN_SOURCE_EXTENDED but that was only valid for XPG4 (1996).

    * Later, that was deemed conflicting with _XOPEN_SOURCE defined to 500.

    * As of mid-2018, none of the features in this implementation require a
      _XOPEN_SOURCE feature greater than 600. However, X/Open Curses, Issue 7
      (2009) recommends defining it to 700.

    * Alternatively, you can enable the feature by defining NCURSES_WIDECHAR
      with the caveat that some other header file than curses.h may require a
      specific value for _XOPEN_SOURCE (or a system-specific symbol).

    The curses.h file which is installed for the wide-character library is
    designed to be compatible with the normal library's header. Only the size
    of the WINDOW structure differs, and very few applications require more
    than a pointer to WINDOWs.

    If the headers are installed allowing overwrite, the wide-character
    library's headers should be installed last, to allow applications to be
    built using either library from the same set of headers.

Headers

Before any library includes in all your files, even if not using ncurses in those files, define:

#define _XOPEN_SOURCE_EXTENDED

If you are including <stdarg.h> to get variadics, you must include that before including ncurses.

Aside: you should be including system libraries (like stdarg) before external libraries (like ncursesw) anyway, see https://google.github.io/styleguide/cppguide.html#Names_and_Order_of_Includes:

Include headers in the following order: Related header, C system headers, C++ standard library headers, other libraries’ headers, your project’s headers.

Finally, to include ncurses with wide support, do not use the regular #include <curses.h>, instead you need to:

#include <ncursesw/curses.h>

Compilation

When linking, ensure your linker is picking up the wide version of the library with:

-lncursesw

I presume if you are using other curses helpers like panels, they still go first, eg:

-lpanel -lncursesw

but I haven’t tested this yet.

Locale - System

Check your locale in the terminal with:

$ locale
LANG=en_AU.UTF-8
LANGUAGE=en_AU:en
LC_CTYPE="en_AU.UTF-8"
...

On Ubuntu, you can change what locales to generate interactively with:

sudo dpkg-reconfigure locales

Or uncomment locales of interest in here:

$ grep -E "^[^#]" /etc/locale.gen
en_AU.UTF-8 UTF-8
en_US.UTF-8 UTF-8

Then either of these commands probably work:

dpkg-reconfigure --frontend noninteractive locales
sudo locale-gen

I suggest to always generate en_US.UTF-8 even if it’s not your primary locale, because most systems probably have the US locale (which have not intentionally excluded English altogether).

Locale - Program Code

The default locale C does not support wide characters by default. Set your program’s locale before starting curses with:

#include <locale.h>
#include <ncursesw/curses.h>

int main(void)
{
    setlocale(LC_ALL, "en_US.UTF-8");
    initscr();

If your curses program prints escape codes like ^@ instead of Unicode, you haven’t got locales working right.

Using Wide Characters

To get the wide character type wchar_t:

#include <stddef.h>

A wchar_t literal is a “long” character or string qualified by L, so:

wchar_t my_wide_character = L'a';
wchar_t my_wide_string[] = L"Hello";

wchar_t my_hammer_and_sickle_unicode = L'☭'
wchar_t my_hammer_and_sickle_codepoint = L'\x262d' // ☭ 

Long string literals are null-terminated with the long null character: L'\0'

ncursesw also supports its own “complex character type” cchar_t which includes a character and attributes like bold, colorpair, etc. You can pack its contents and attributes with setcchar(), which takes arguments of:

  • Pointer to cchar_t to pack
  • Pointer to wide character string, terminated with wide null L\0
  • ncurses wide attributes starting WA_*
  • short color pair
  • options not used here (it’s for when you need more than SHRT_MAX colorpairs)
cchar_t my_cchar = { 0 };
setcchar(&my_cchar, L"☭", WA_NORMAL, colorpair(C_RED, C_BLK), NULL);
mvadd_wch(1, 1, &my_cchar);
refresh();
wint_t keypress = { 0 };
int ret = get_wch(&keypress);

Functions - ncurses

Change to using all wide character and new API functions, don’t use any of the old functions anymore.

Set attributes with wattr_set(). Use new API version of the attribute macros, like changing A_BOLD to WA_BOLD.

Get input characters with get_wch(). Note the return value is a status - OK for wide character, KEY_CODE_YES for function key, ERR for error. The actual keypressed is placed into the *wch parameter you provide to the function. This is different to the old getch() which returns the key pressed.

I don’t see the use of packing entire strings full of complex characters (cchar_t), so for strings, use wchar_t and long strings and place with mvaddwstr() or mvwaddwstr().

For single characters, you can pack a cchar_t with setcchar() and place it with mvadd_wch() or mvwadd_wch().

However you can also skip using cchar_t altogether and write single long characters (eg: L"@") with the long string functions.

A complete list of wide curses functions is in the source like:

Functions - Wide Strings

To use wide string functions you must #include <wchar.h>. Some references:

A few replacement functions are:

Purpose Old New
string length strlen() wcslen()
string n length strnlen() wcsnlen()
string width N/A wcswidth()
string compare strcmp() wcscmp()
string n compare strncmp() wcsncmp()
string copy strcpy() wcscpy()
string n copy strncpy() wcpncpy()
memory set memset() wmemset()
string to long int strtol() wcstol()
string to double strtod() wcstod()
  • Convert old char strings to wchar_t with mbstowcs() “multibyte string to wide-character string” from <stdlib.h>, or swprintf with %hs reference
  • Note file I/O functions like fgetws(), fputws(), fwprintf(), swprintf(), etc

A Complete Example

#define _XOPEN_SOURCE_EXTENDED

/* gcc -o ncwtest -std=c99 -Wall -Wextra -Wpedantic main.c -lncursesw */

#define SCREEN_X  80
#define SCREEN_Y  24

#include <inttypes.h>
#include <locale.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>

#include <ncursesw/curses.h>

/**
 * Get the value of our custom curses color pair.
 *
 * @param fg  Foreground color using one of the COLOR_ macros
 * @param bg  Background color using one of the COLOR_ macros
 * @return    Curses color pair to use with COLOR_PAIR()
 */
static short colorpair(short fg, short bg)
{
        // the original 16 colorpairs should not be modified
        return 16 + fg + (bg * 8);
}

void curses_on(void)
{
        initscr();

        if (has_colors() && can_change_color()) {
                start_color();
                for (short bg = 0; bg < 8; bg++) {
                        for (short fg = 0; fg < 8; fg++) {
                                short pair = colorpair(fg, bg);
                                init_pair(pair, fg, bg);
                        }
                }
        }

        cbreak();
        noecho();
        keypad(stdscr, TRUE);
        curs_set(0);
}

void curses_off(void)
{
        erase();
        refresh();
        endwin();
}

int main(void)
{
        setlocale(LC_ALL, "en_US.UTF-8");
        curses_on();

        //wchar_t my_sym[] = { L'\x262d', L'\0' }; // ☭
        cchar_t my_cchar = { 0 };
        attr_t my_attrs = WA_NORMAL;
        setcchar(&my_cchar, L"☭", my_attrs, colorpair(COLOR_RED, COLOR_BLACK), NULL);

        mvadd_wch(1, 1, &my_cchar);

        for (int y = 1; y < (SCREEN_Y - 1); y++) {
                for (int x = 1; x < (SCREEN_X - 2); x++) {
                        mvadd_wch(y, x, &my_cchar);
                }
        }

        refresh();

        wint_t keypress = { 0 };
        int ret = get_wch(&keypress);

        curses_off();
        return 0;
}

History

  • v1.0 - 2022-08-06 - Initial commit
  • v1.1 - 2022-09-26 - Added wide char stuff, made some header bits clearer
  • v1.2 - 2022-09-27 - Clarified text more, simplified standalone example
  • v1.3 - 2022-10-04 - Wide character function table, added some more common functions