Fixing Ruby ncurses Unicode Character Display on Linux Terminals

A little side-project of mine is a role-playing game written in Ruby that runs in the terminal and uses Unicode/ASCII characters instead of bitmap pixel graphics. In my personal tradition of these kinds of side projects, this is called TermQuickRPG. It's a work-in progress, so there's not a lot to do in the sample game at the moment.

How I found out why special characters wouldn't draw under Linux

After I finished a little scenario with some custom scripting on the maps, I wanted to share the game with friends. Some have Linux machines running, and since I use the curses gem, I thought I was good to go. But no such luck: on macOS, it behaves totally different. On Linux, the Unicode Box Drawing characters cannot be printed. I get garbage output instead.

macOS and Linux Terminal output compared

So the unicode characters don't get printed. Bummer. I am using Linux Mint 19 (a Ubuntu derivate) in a VM to test this, by the way. Here are my initial test results:

The last one made me curious. The Python 3 docs say:

Note: Since version 5.4, the ncurses library decides how to interpret non-ASCII data using the nl_langinfo function. That means that you have to call locale.setlocale() in the application and encode Unicode strings using one of the system’s available encodings. This example uses the system’s default encoding:

import locale locale.setlocale(locale.LC_ALL, '') code = locale.getpreferredencoding()

Then use code as the encoding for str.encode() calls.

And sure enough! When I call setlocale(locale.LC_ALL, ''), the Python sample did display the box drawing characters; without, it didn't. There was no such setting in Ruby, though, and it seems no tinkering with the LC_ALL environment variable and file encodings did help.

That's when I tried a quick sample in plain C.

Wait, what? Python is, C isn't? (Not even with setlocale called from C.)

So I dug into the Python code. The Python implementation of addstr, the curses function that will eventually print a string on screen, reveals that for some environments, mvwaddwstr is used. That's part of ncursesw.

Once I installed ncursesw sudo apt install libncursesw5-dev and compiled the C code with the -lncursesw option and called mvwprintw (note the trailing "w", which makes this part of ncursesw, not ncurses!) – sure enough, it did output the characters just fine.

Curses's internal representation of the string contents I was giving it did work with the ncursesw library, not with the curses or ncurses library.

There's a ncursesw ruby gem, too, and it does work just as fine once you change the code to use that gem's API.

Well, the ruby/curses gem says it in the README, too, once I looked a second time:

Requires ncurses or ncursesw (with wide character support).

Wide character support is what I was looking for all the time. I just didn't pay attention to this stuff after I settled for the ruby/curses gem because its API was so nice. Sheesh!

Adjusting the game to ncursesw

The ruby/curses gem supports ncursesw, actually. It just loads the older stuff first, if possible. It comes 3rd since 2016. Switching the order of the #if defined compile-time macros to load ncursesw first, instead, instantly made the nice ruby/curses gem's API do the job just as well as the ncursesw gem I mentioned above. No need to adjust even a single line of code!

Naturally, I created a pull request to incorporate the changes after local testing.

It even works on Linux! Sort of. My linux terminal displays the box drawing characters as wide unicode characters (which was my problem in the first place), so each box drawing character takes up the same screen space as 2 regular characters.

All the space characters are only half as wide as the boxes, the house walls, and the smiling faces.

Up next, I'll have to figure out if I can enforce double character width on macOS (why doesn't macOS have to use wide-character support at all?) and adjust everything to these new width constraints. Or the other way around, get Linux terminals to display more narrow characters instead.

Or maybe I'll switch to either a graphics-based renderer or PDCurses, which can use SDL to draw characters, it seems. We'll see about that.

Browse the blog archive