PHP - Using ncurses

From LXF Wiki

Revision as of 10:35, 7 Jul 2006; view current revision
←Older revision | Newer revision→
Table of contents

PHP: Curses on the console

We find out there's a reason it's called "curses"...


What's the One True Way: the command-line ot the GUI? Let me tell you, O my brothers, about a middle ground: curses. No, not the raw Anglo-Saxon so commonly heard in pubs at kicking-out time, but the text-based graphical libraries that lets programmers draw simple dialogs and interfaces for use in the console.

This might sound like just the thing to snatch the conch away from Nick and Rebecca as they stand bickering, but there are two problems with it. First, curses are very limited: you get a mittful of colours, lines, and letters, and not much more. This is problematic: witness the beauty of the X-based Firefox compared to the cloudy confusion that reigns over the curses-based Lynx. One you understand with little effort, the other requires manual pages to learn.

The other problem with curses is that it is a surprisingly tricky thing to master. Little effort seems to have been expended to make curses easy to learn or use. It has a great many functions, many of which have unpronounceable names (our favourite is ncurses_mvwaddstr()), and you need to be very precise in order to get anything other than a blank screen. Curses, more often than not, will leave you cursing.


On your marks...

The curses implementation on Linux and most other free systems is called ncurses, and you almost certainly already have it installed. However, you may not have the development files installed, so go to your package manager and ensure you have libncurses-devel installed (note: this may have some sort of version number, eg libncurses5-devel). To use ncurses in PHP, you will need to recompile using --with-ncurses in your configure line.

With that done, onto our first code. We need quite a few functions to get a basic interface: ncurses_init(), ncurses_newwin(), ncurses_wborder(), ncurses_wrefresh(), ncurses_wgetch(), and ncurses_end() - these are all used in later examples, so its important you master them.


First up, ncurses_init() initialises the ncurses library and allows you to draw to the screen. It takes no parameters, and you can ignore its return value. Next, ncurses_newwin(), which creates a new window of the dimensions you pass to it, and returns a handle to that window. The dimensions are the number of rows you want, the number of columns, plus the start row and column offset from the top-left corner. If you specify 0 for each of these, ncurses will create a window for you that takes up the entire screen.

Moving on, ncurses_wborder() is used to draw a border around a window. The first parameter of this function should be the return value from ncurses_newwin(), and each of the eight other parameters should be 0. The eight parameters decide whether you want to draw the left border, the right border, the top border and the bottom border, then whether you want to draw the top-left corner, the top-right corner, the bottom-left corner and the bottom-right corner. Specifying 0 means, cunningly, "draw this border/corner", whereas 1 means "skip this border/corner".

The remaining three functions are a great deal easier. On line four there's ncurses_wrefresh(), which takes our window as its only parameter and forces ncurses to draw all our changes to the screen. Then there's ncurses_wgetch(), which also takes a window as its only parameter, but causes ncurses to pause what it is doing in order to wait for a key press - this has the effect of keeping our screen in place until you hit any key. Finally, there's ncurses_end(), which clears the screen and frees up all our resources; after calling this, you cannot draw to the ncurses screen again.

Here's the code:

<?php
   ncurses_init();
   $screen = ncurses_newwin(0, 0, 0, 0);
   ncurses_wborder($screen, 0,0, 0,0, 0,0, 0,0);
   ncurses_wrefresh($screen);
   ncurses_wgetch($screen);
   ncurses_end();
?>

So, six lines of PHP code - what does that give us? Well, try not to get disheartened, but if you run the code all you'll see is a large, empty box! To run the script, you'll need to open a terminal window either in X (or quit out of X entirely if you want), and run it using "php yourscript.php". If you get an error about ncurses_init() not being a recognised function, you may not have run "make install", or you may have accidentally installed PHP into a different location than an existing PHP install.


Foiled again!

Okay, so perhaps you're less than thrilled with our empty box, so without further ado we're going to modify the box script to be more interesting: we're going to add some text! If you're reading this on the train, please try hard to constrain your excitement.

To add text to our box, we need two new functions: ncurses_getmaxyx(), for reading the dimensions of our console; and ncurses_mvwaddstr() for moving the insertion point to a specific location and writing a string. The "insertion point" is really just the system caret (the flashing cursor where you type text - just to make sure we're on the same wavelength!), and this remains present in ncurses.

So, to display text, we move the caret to where we want the text to start, then we print our string. The ncurses_getmaxyx() function takes a window as its first parameter, then two variables where you'd like it to start the size of the window in rows and columns. The ncurses_mvwaddstr() function takes a window as its first parameter, a row position and column position for its second and third parameters (where it should move the caret to), then the string to print as its fourth parameter. So, insert these three lines immediately after the ncurses_wborder() function in your script:

ncurses_getmaxyx($screen, $row, $col);
$string = "He thrusts his fists against the posts";
ncurses_mvwaddstr($screen, $row / 2, ($col / 2) - ($strlen($string) / 2), $string);

In that code, both the row and column positions passed into ncurses_mvwaddstr() are calculated so that the string is centred on the screen.

If you run your modified script now, you'll see the text centred as planned, however you will also see the system caret at the end. This detracts a little from the look, but we can use the ncurses_curs_set() function to hide this. Pass it 0 to hide the caret, or 1 to display it. So, add this line immediately after the call to ncurses_init():

ncurses_curs_set(0);

So we now have a box with text in, and no system caret - marvellous. See figure 1 for how this looks.

php69-fig1.png-thumb.png (http://www.linuxformat.co.uk/images/wiki/php69-fig1.png)
Figure 1: A box with text in - I think we deserve a medal for all this work!


A dash of colour

Black and white is dull, no matter what Guinness may tell you, so it's time we started sprucing up our output. There are two ways to do this: we can use text attributes (bold, underline, inverted), and colours. Both these techniques make your output look smarter and more interesting, so you should make good use of them.

First, text attributes. These are enabled and disabled like a state machine - you turn bolding on for a window, and every piece of text written from then on is bold, until you disable it again. To do this properly, we're going to add a window title to the top of the screen that will be printed in reverse text (ie the background becomes white and the text is black). We'll then print the main text in bold.

The only new things to learn here are the ncurses_wattron() and ncurses_wattroff() functions for enabling and disabling text attributes for a window. Add these three lines immediately after the line ncurses_wborder():

ncurses_wattron($screen, NCURSES_A_REVERSE);
ncurses_mvwaddstr($screen, 1, 2, "LXF69 PHP");
ncurses_wattroff($screen, NCURSES_A_REVERSE);

That adds a title at row 1 column 2, printed in reverse text. You should also amend the main call to ncurses_mvaddstr() to this:

ncurses_wattron($screen, NCURSES_A_BOLD);
ncurses_mvwaddstr($screen, $row / 2, ($col / 2) - (strlen($string) / 2), $string);
ncurses_wattroff($screen, NCURSES_A_BOLD);
php69-fig2.png-thumb.png (http://www.linuxformat.co.uk/images/wiki/php69-fig2.png)
Figure 2.

That bolds the text. If you're interested in trying out other attributes, use NCURSES_A_UNDERLINE, NCURSES_A_BLINK, or NCURSES_A_INVIS (for invisible text).

Colours are a bit more difficult. First, you must initialise ncurses colour processing by calling the ncurses_start_color() function. You then need to assign colour combinations (pairs of foreground and background colours) to numbers. Finally, you need to call ncurses_wcolor_set() to select a colour pair for text - this takes a window and a colour number as its parameters.

This is quite a long code block, but it's worth it:

<?php
   ncurses_init();
   ncurses_curs_set(0);
   /// COMMENT ONE
   ncurses_start_color();
   ncurses_init_pair(1, NCURSES_COLOR_RED, NCURSES_COLOR_BLACK);
   ncurses_init_pair(2, NCURSES_COLOR_GREEN, NCURSES_COLOR_BLACK);
   ncurses_init_pair(3, NCURSES_COLOR_YELLOW, NCURSES_COLOR_BLACK);
   ncurses_init_pair(4, NCURSES_COLOR_BLUE, NCURSES_COLOR_BLACK);
   ncurses_init_pair(5, NCURSES_COLOR_CYAN, NCURSES_COLOR_BLACK);
   ncurses_init_pair(6, NCURSES_COLOR_MAGENTA, NCURSES_COLOR_BLACK);
   $screen = ncurses_newwin( 0, 0, 0, 0);
   ncurses_wborder($screen, 0,0, 0,0, 0,0, 0,0);
   ncurses_wattron($screen, NCURSES_A_REVERSE);
   ncurses_mvwaddstr($screen, 1, 2, "LXF69 PHP");
   ncurses_wattroff($screen, NCURSES_A_REVERSE);
   ncurses_getmaxyx($screen, $row, $col);
   $string = "He thrusts his fists against the posts";
   ncurses_wattron($screen, NCURSES_A_BOLD);
   /// COMMENT TWO
   $startrow = ($row / 2) - 3;
   $startcol = ($col / 2) - 3 - (strlen($string) / 2);
   for ($i = 1; $i <= 6; ++$i) {
      /// COMMENT THREE
      ncurses_wcolor_set($screen, $i);
      ncurses_mvwaddstr($screen, $startrow + $i, $startcol + $i, $string);
   }
   ncurses_wattroff($screen, NCURSES_A_BOLD);
   ncurses_wrefresh($screen);
   ncurses_wgetch($screen);
   ncurses_end();
?>

I've marked three comment blocks in that text that require further explanation. First, after calling ncurses_start_color(), ncurses_init_pair() is called six times to assign various colour combinations to the numbers 1 to 6 (colour 0 is the default white on black). These are used later.

At comment two, we precalculate the centre row and column for our string, as these are used several times in our loop and it saves some processing power. However, note that both have an extra -3 in there because the loop is being used to print out the string six times, and this forces the text to start three rows up and three columns to the left.

Comment three shows where the colour is set to the value of the loop counter, $i. The line after that shows how the $startrow and $startcol values are incremented by the loop counter. So, the end result is that the first line is printed at centre - 3, the second printed at centre - 3 + 1, etc; this should give us a nice cascade effect.

Run the script as before, and you should see something similar to figure 2 - colours, text attributes, and a cascade all in one.


Split personalities

To this point we have only been using one window for our output, and admittedly that works well for most basic projects. However, sometimes you want to guarantee that one part of your output doesn't clash with another part - perhaps you want to have text at the bottom, and want to guarantee that text at the top won't overwrite it no matter how long it may be.

Using multiple windows is really just about making two calls to ncurses_newwin(), then passing the appropriate parameter into later functions. To demonstrate this would require more code than I'm willing to print (after all, I don't want our printer running out of cyan!), so you'll find it on your coverdisc as 6curses.php.

I have scattered comments throughout that script to point out the changes, however it's largely just down to splitting the output between two windows. The key part of the action is inside the loop:

if ($i % 2) {
   ncurses_wcolor_set($winleft, $i);
   ncurses_mvwaddstr($winleft, $startrow + $i, 0, $string);
} else {
   ncurses_wcolor_set($winright, $i);
   ncurses_mvwaddstr($winright, $startrow + $i, 0, $string);
}

That simply runs the $i loop counter through modulus, returning either 1 or 0 - a 1 prints to the left-hand screen ($winleft), and a 0 prints to the right ($winright).


php69-fig3.png-thumb.png (http://www.linuxformat.co.uk/images/wiki/php69-fig3.png)
Figure 3.


Making a menu

For our penultimate trick, we're going to produce a working menu system that can be controlled with the cursor keys and Enter. This takes even more work, and again there isn't room to print the code here so you should refer to the file 7curses.php.

Lots of the file remains unchanged from previous copies, but there are some new spots. First, there's an $items array that contains four menu items for the screen. You can hard code this script if you wanted to, but it would end up being longer and less easy to customise, so stick with this array.

This time the drawing part of the script takes place in an infinite loop (the "while(true)" part) so that it keeps on rendering the menu and responding to key presses until any key other than Up, Down, Left, Right, and Enter is pressed. To handle this input, we snag the return value of ncurses_wgetch(), which is what causes the whole display to block waiting for the user to do something.

Once we have the return value from ncurses_wgetch(), it gets passed into the switch statement and we act according to what was pressed. The "case 13" is for the Return key; ncurses doesn't have a constant for that. As you can see in the code, pressing Enter changes the screen text to be the colour we assigned near the beginning. Up and Down change the selected menu item, and by checking whether the $itemselected value is -1 or greater than the number of menu items, the selection will wrap if the user tries to move beyond the options.

Figure 3 shows the output of this script - the text is cyan because I chose the Liberty item, but the other colours work too.

Although curses are a pain to learn, they are the key to advanced command-line user interfaces. Just remember: no cursing, no power; some cursing, some power; much cursing, and your wife kicks you out.


Wacky Races

Okay, time for the grand finale: so far all our scripts have produced dull bits of text and boxes. However, ncurses can do much, much more, as you'll soon find out!

Again, the script is far too long for printing, so pull up 8curses.php in front of you while you read. This script emulates the letter waterfall screen popularised by the film The Matrix. Of course, ncurses only has a limited set of colours, meaning that we have only white, normal green and bold green to work with (this looks slightly brighter on-screen).

What we need to do is create a list of letters that we want to be shown, then create an array of letter chains that should fall from top to bottom. The last letter should change every movement of the chain, and the previous letters should move up to the end of the array then get deleted. This sounds harder than it is!

We have the same infinite while loop as before, but to make the script easier to maintain I have split up the chain-moving code and the chain-creation code into individual functions (cunningly named movechains() and createchain()). These get called each iteration of the loop, and a delay of 1/5th of a second (200,000 microseconds) is programmed in so that the whole thing doesn't fly by too quickly.

Each time the loop goes around, both movechains() and createchains() are called. The movechains() function works by looping through each chain in the $chains array, adding a random letter to the front and taking one off the end. Then, back in the main loop, the createchains() function is called to create a new chain each "tick" - this is a random number of random letters, positioned in a random column, then added to the $chains array.

Once those two functions have been called, we're into the foreach loop beneath them. This contains a nested loop: we select each chain in the $chains array, then select each letter in the $letters array of each chain, and draw it to the screen. If it's the first letter in the chain, we draw it in bold white; if it's the second character we use bold green, and if it's the third or higher character we use normal green. We also draw a blank character after each chain, so that they clear up the space above them as they move.

There is no call to ncurses_wgetch() this time. Instead, we use ncurses_wrefresh(), then just sleep for 200,000 microseconds before looping around again. As a result of this, you need to use Ctrl-C to exit the program once it runs.

Hopefully your script now looks something like figure 4. It is perhaps in need of some more shades of green in order to look a bit more realistic, but I think it's more than enough to impress your friends with your mastery of ncurses!

php69-fig4.png-thumb.png (http://www.linuxformat.co.uk/images/wiki/php69-fig4.png)
There's a hidden message in there somewhere - please seek help if you can read it.


Top Tip

The ncurses section of the PHP library is sparsely documented at best - if you're looking for more information, use "man ncurses" at the command line to read the C documentation and start from there. There are several off-shoot man pages from that one, such as "man curs_color" to learn about colours, and there's certainly much more documentation there than there is in the official PHP manual.