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.
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 font was capable of showing these characters.
- The Terminal was capable of showing these characters.
- Python 3 was capable of displaying these characters in a
curses
window.
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 calllocale.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.
- Plain C was not capable of displaying these characters.
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.
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.