25 June 2012

Perlish Curses

So let's write silly little test programs. First thing to do is write a program that reads keystrokes and reports back what the keycode is.

Input

For a terminal program, anything above the standard alphanumeric keys can get exceedingly tricky, since there are many different keyboards and terminal programs available. Keystrokes like F1 and the left arrow key get returned as a sequence of escape characters: ^[OP and ^[[D in my case, but other terminal software may use different sequences. Happily, setting Curses::keypad(1); will tell ncurses to do its best at interpreting these special sequences for me, and calls to Curses::getch() will return a special value for those special keys. Less happily, as one Perl Monk discovered, the function will return characters and ctrl-keys as-is along with these special numeric values, so it can be a bit awkward to differentiate between them. Never mind, at least the rest of the library protects us from some of the C cruft inherited from curses.h - so addstr, waddstr, mvaddstr and mvwaddstr are all thankfully rolled into one.

Since we're attempting some semblance of Unicode support this time around, there's also some really !!Fun!! things to consider: What happens if I copy-paste some unicode characters (e.g. Chinese) into my terminal? And how the hell are we going to handle extended character input methods?

After playing with my keycode-reading program for a little, it seems like it won't be such a terrible problem after all. There's some special key sequences that feed a bunch of bytes into the program, and the byte sequence corresponds to the UTF-8 for the characters I pasted / entered. SCIM (or I guess it's ibus that I'm using nowadays?) seems to fire up just fine and lets me type Chinese into gnome-terminal. Curses doesn't interpret these byte sequences specially, so I'll have to do some parsing and reading of my own, but it's not the end of the world.

Output

As for "printing" characters to the screen - fun terminology that dates back to the early teletype machines where your output was literally printed onto a roll of paper -  it seems fairly easy to do in perl. There's significantly less arcane invocation required to configure things for utf-8. Here's the start of my program:-

#!/usr/bin/perl

use warnings;
use strict;
use utf8;
use Carp;
binmode(STDOUT, ":utf8");
binmode(STDERR, ":utf8");
    
use Text::CharWidth qw(mbwidth mbswidth mblen);
use Curses;

So far, so good. A little bit of standard Curses initialisation and we can print unicode text, no problem. It seems that I was worrying too much - if you can give Curses some multi-byte UTF-8 character sequence, it'll happily pass it on to your UTF-8 enabled terminal without looking too hard at it.

That's not to say things will be easy. Oh no.

Hurdles

Here's what I've got so far:-

Screenshot of curses test program

The top line is perl warning me about a potential bug. I forget what triggered it exactly, possibly the odd numeric/character duality of what getch() gives us. The nice thing is, however, that I have a way to show them without messing up the rest of the screen.

Ordinarily, messages from warn and die would just get crapped onto the screen wherever curses left the cursor position last, and possibly get overwritten by more output. By installing a few signal handlers and shoving the message into a variable, I can print it out and ensure that future screen refreshes will still show the last error:-

my $topline = "Perl-based keycode reading program! Press q to quit.";
BEGIN {
   $SIG{__WARN__} = sub { Curses::addstr(0, 0, $_[0]); $topline = $_[0]; };
   $SIG{__DIE__} = sub { Curses::endwin(); print STDERR "We crumble!\n"; };
}

Magic.

I also print out whatever useful information I can find; the termname will be handy later when we attempt to test our programs on terminals other than gnome-terminal (and start tearing our hair out because they all have subtle differences). I've also made it so that I can toggle curses' keypad mode with ctrl-K, letting me see either the raw or interpreted character sequences.

Then we've got some boxes. This is me experimenting with how wide characters affect the coordinate system curses uses; all coordinates are given as (row, column) but the interaction between single-width and full-width characters in screen coordinates is not specified.

As it turns out, the coordinates presume single-width character cells for the screen but still let you render a full-width character - it just spills into the adjacent cell. At least, this is true for my version of Curses.pm, my version of libncursesw.so, and my version of gnome-terminal. With all those levels of indirection, it becomes difficult to control the output with any degree of certainty.

Animation of some garbage text being left behind in curses
One flaw I've found so far is that when you attempt to draw a wide character inbetween two other wide characters, you get garbage hanging around on the screen. Here's a .gif of me moving a little 人 character around the rest of the screen. The garbage characters you see being left behind aren't really 'there' - forcing curses to redraw the entire screen fixes it - but it's still a nuisance. It might not really be curses, or the terminal's, or anyone's fault; asking to draw a character inbetween two others is a pretty odd request and you should expect odd results.

I started to think that I might not be able to use perl's curses after all, but in hindsight it's not so bad. You cannot always control what characters you're going to have to draw in what positions, especially with user input being a factor, but a bug like this can be mitigated:-
  • For a text editor, we're likely going to be drawing each line in one pass anyway, rather than flitting around the screen and repositioning the cursor a lot.
  • Likewise, if I were to make a silly little roguelike game - which I'm considering - I'd have fairly tight control over the cells used to display the game world, and could reasonably expect that I wouldn't accidentally hit this overdrawing problem.
  • In all other cases, such as needing to draw some sort of "dialog box" atop previous content, a quick call to Curses::clearok(1); Curses::refresh(); Curses::clearok(0); should fix things.

Note that I don't want to just set clearok(1) and be done with it. In this mode, curses makes no assumptions about existing screen content and redraws everything from scratch. Even with today's modern computers, if I set that and then hold the arrow keys down I'm going to notice some flicker as the entire screen updates. I don't want that. I certainly don't want it over a potentially slow remote shell!

There was one final problem I ran into but I'm hoping it won't be so important - I noticed that when attempting to use some (allegedly) full-width line drawing characters for the boxes, they were being treated as though they were single-width. So I tried using the Chinese characters you see above, and things worked fine - possibly somewhere along the line some part of the chain got the wrong idea about the widths.

Otherwise, I'm pretty happy with how this turned out and will continue experimenting with perl and curses.






No comments:

Post a Comment