|
PluginNCurses
AboutThe ncurses bindings wrap up a great deal (well over 100 functions) of the ncurses API for use in JavaScript. Instead of providing an API directly compatible with the low-level C API (as earlier versions of this plugin did), it provides an object-oriented interface. This is much easier to manage, in terms of window ownership, subwindows, etc., than the low-level interface. That said, the OO interface is only a simple wrapper around the curses API (essentially adding only child ownership semantics), and will require some curses know-how in order to use effectively. As always, curses "takes some getting used to." The advantages of writing curses apps in JS instead of C include:
Unfortunately, no amount of wrapping will make the overall difficulties of developing with curses completely disappear. TODOs:
ncurses features which i have no plans to implement include:
Source Codehttp://code.google.com/p/v8-juice/source/browse/extra-plugins/src/ncurses-oo HistoryCurses has been around since the 80's and is the de facto library for implementing text-base user interfaces. It presumably gets its name from the fact that one spends half his time cursing while using it. ncurses is the de facto open source implementation of curses (curses is actually a standard), and is available on almost every platform out there. This plugin is derived from an old ncurses C++ wrapper which i forked out of the ncurses 5.4 source tree in March of 2005. At one point it was also the basis for JavaScript/ncurses binding for SpiderMonkey. The first version of this plugin modelled the C-style API. After it was implemented, i realized that the particular method of JS/C++ wrapping which it used was not only type-unsafe on the native side, but also easily subject to segfaults from script-side code. Aside from that, it also got seriously broken in some refactoring of v8-juice. Rather than fix/refactor it, i decided an OO wrapper would be more useful, and revived the ancient ncurses C++ wrapper for use as a basis. For new and old curses programmers alike, Dan Gookin's Programmer's Guide to nCurses is an excellent resource on how to work with the library. Printed in 2007, it is the only curses book i'm aware of which was penned in the 21st century. Curiously, even though Thomas Dickey still releases patches to ncurses on a weekly basis, not much has fundamentally changed in curses since... well, since a very long time. So if you programmed curses back in 1992 and still remember how it works, there won't be much of a learning curve for you here. API OverviewThis plugin installs the following items:
Naming ConventionsCurses was developed back in a time when it was cool to use the shortest, most ambiguous or cryptic names possible. For example, refresh() by itself could mean anything, and scrl() and scroll() are both scrolling related, but without looking in the man pages i can't tell you which one does what. In order to make this API easier to use... for the vast majority of the cases we've kept the cryptic names! Why? Because it makes it simpler to go look up their meanings in the curses man pages. If we renamed them, curses gurus would be confused by the new names and curses newbies wouldn't be able to find the man pages. The functions which don't use names taken directly from the curses API normally derive their names from the underlying C++ wrapper (which was originally developed by someone else). An example which comes to mind is NCWindow.scroll(), which behaves like wscrl(), and not like scroll(). Error ReportingMost error codes used by curses are integers with the value ERR for failure, and non-ERR on success (honestly, that's what the curses standard specifies). In practice, non-ERR is simply OK, which is 0. Many errors are reported via exceptions, either because they come from the JS side of the bindings (as opposed to being an ncurses error value) or because they inherit that behaviour from the underlying C++ wrapper (which throws for a number of error cases, most notably for failed window construction). The ncurses ObjectThis plugin installs an object (of an unspecified class) called ncurses which holds all of the plugin's classes and the following non-class functions:
Aside from those functions, nearly all standard ncurses macros are defined as symbolic constants of the ncurses object. For example, ncurses.A_BOLD and ncurses.A_UNDERLINE. ripoffline()ACHTUNG: EXPERIMENTAL and it might go away. Known to cause segfaults post-main() in some case, or cause an app to not exit curses mode at shutdown (hosing the console state). It has also been observed to interfere with NCPad scrolling operations (for reasons i cannot comprehend). ripoffline() allows one to "rip off" lines from the screen. These lines are outside of the stdscr area (and in fact cause stdscr to be smaller) and have several important restrictions described below and in the man pages for ripoffline() (though the native function has a slightly different signature). ripoffline(), if used at all, must be called before any window objects are created (more specifically, before curses mode is ever started). It can only rip one line at a time, and can only be called up to 5 times (these are limitations of curses). Each time it is called, it rips one line from the top or bottom of the screen, depending on the boolean parameter passed to it. Each line becomes a new window object, which is accessed via the roundabout approach shown below. load_plugin('v8-juice-ncurses-oo');
var nc = ncurses;
nc.ripoffline(true); // rip from top
nc.ripoffline(false); // rip from bottom
/** We MUST (it is a curses requirement) initialize
curses before we can access the ripped-off lines. That is done by creating
our first window object: */
var root = new nc.NCPanel();
// Now we can access the ripped-off lines like:
function setupRips()
{
var cl = ['white','red','white','black','cyan'];
for( var i = 0; i < 5; ++i ) // 5 is a built-in limit of curses
{
var rw = nc.getRippedLine(i);
if( ! rw ) break;
rw.bkgd( nc.color_pair( cl[i], cl[i+1] ) );
rw.addstr("ripped line #"+i+" ="+rw);
rw.refresh();
}
}
setupRips();When using ripoffline(), if you find that your ncurses apps are not returning to normal screen mode when the last window is destroyed, try calling ncwindow.endwin() directly after destroying the last window. That will forcibly shut down curses mode and destroy those dangling ripped-off lines. Calling endwin() while other windows are still open can lead to a memory leak, and is not recommended. NCWindow ClassNCWindow is the base class of most other classes in this package. It is a wrapper around an ncurses WINDOW handle, and provides most functions necessary for working with curses windows. Member Function ListMost of the functions listed below are not documented here. Most of them map 1-to-1 with ncurses functions of the same (or very similar) names, and are described in great detail in the ncurses man pages. That also explains why their names are so cryptic and do not follow common JavaScript naming conventions. The exceptions to the documentation rule are functions which are unique to this API or, for reasons of argument passing conventions, work differently than in the C API. Some functions which are overloaded in the native C++ API instead have several variants with slightly different names (all conforming to common curses naming conventions). e.g. getch() vs mvgetch(). At some point i may go back and implement proper overloads for JS, but for the moment it's more trouble than it's worth. Here we go... Constructors
Geometry, Cursor, and Movement
Colors, Status, and Attributes
Drawing/Output
User Input
Misc.
TODOsTODOs:
NCPanel ClassNCPanel is analogous to the curses PANEL type. It is basically a top-level window widget which can be stacked with other panels and moved around (both of which are problematic with non-panel windows). Member Function ListIn addition to the NCWindow API, NCPanel has:
Additionally, some methods are overridden at the native level to accommodate for the slightly differing update/refresh conventions between windows and panels. NCPad and NCFramedPad ClassesNCPad is a NCWindow subclass wrapping the very unusual ncurses PAD type. Pads are windows which may be larger than the screen (normal curses windows cannot be out of the screen bounds). They have some very intricate refreshing rules and are difficult to use properly (at least in my experience). They are, however, very useful in creating scrollable areas. The NCFramedPad class is identical to the NCPad class in every way, including the constructor arguments, with the exception that it will draw a border around its scrollable area and will draw scroll indicators along the right and bottom of the scrollable area. Whether to use NCPad or NCFramedPad for a particular case is largely a matter of UI design tastes. Known bugs
Member Function ListNCPad deserves much more documentation than is given here. Maybe someday. Until then... Aside from the NCWindow API (not all of which is strictly legal for a pad - see the curses man pages!), NCPad also provides:
The NCPad class also has the following shared symbolic contants, all of which are numeric and all of which are accepted as arguments to the requestOp() and mapKeyToReq() member functions. The native API (documented in ncwindow.hpp) goes into much more detail about these.
TODOs:
Tips and TricksGeneral tips include:
Send debugging output to another consoleAny output to the console from non-curses methods (e.g. print()) may hose the screen state. If you want to send debugging output to a separate console, open that console and type tty. That will give a device name, like /dev/pts/4. From your terminal where you're running ncurses code, you can then do: ~> v8-juice-shell myScript.js 2>/dev/pts/4 # adjust that value, of course! and the output which is intended for std::cerr will then go to that console. Do not redirect stdout to another console, or curses will try to write to that console (but won't work properly)! Alternately, you can use NCWindow.captureCerr(), but that will only work if all client debugging code uses the std::cerr object for debug output (as opposed to using fprintf(stderr,...) or similar). When using both captureCerr() and captureCout() on the same window it is sometimes useful to pass color attributes to one or both of the streams so that their output looks different: mywin.captureCout(); // cout will use mywin's current colors
mywin.captureCerr( ncurses.color_pair('red','black') | ncurses.A_BOLD );
...
mywin.captureReset(); // restores cout/cerr to their previous statesNote that JS has no standard (or even pseudostandard routines) which output to stderr, and it is generally expected that all intercepted std::cerr output will be coming from debuggering routines. Newer versions of the v8-juice shell application (under src/client/shell/) have the command-line option --print-cerr, which forces the JS print() function to use std::cerr, instead of std::cout, for output. Combined with redirection to another console, this is often useful when debugging curses applications. Inheriting Window Types from JSTo summarize a very long story... If you want to inherit any of the window classes from JS classes, follow the instructions on the ClassBinder page. Here is a shortened summary of what a subclass looks like: /** Custon NCPanel subclass: */
function MyPanel()
{
var argv = Array.prototype.slice.apply(arguments,[0]);
// ^^^ convert 'arguments' pseudoarray to a real array
// for use with:
this.prototype = this.__proto__ = new ncurses.NCPanel(argv);
// (this instanceof ncurses.NCPanel) is now true!
... add members, etc. ...
// To call an inherited implementation of a member:
this.toString = function() {
return '[MyPanel:'+this.__proto__.toString()+']';
// ^^^ this.prototype.toString() won't work for this purpose!
// so use __proto__ inside of member functions instead.
};
return this;
}
var p = new MyPanel(10,10,4,4);
p.bkgd(ncurses.color_pair('white','red'));
...
p.close();ExamplesIt's hard to make short/concise examples for curses. i'll try to get some written up at some point. Until then... NCPanel/NCPad ExampleMost of my curses UIs use panels as the primary UI widgets, and then i add the app-specific widgets to those. i never touch stdscr, and instead do everything on panels. This saves a lot of hassle when it comes to moving windows and refreshing overlapping windows. For example, using a screen-sized panel as the "root window" overcomes screen corruption problems which we see when we have panels on top of stdscr and use stdscr for output. Summary: don't output anything to stdscr unless it's the only window your app needs. Here's an example: load_plugin('v8-juice-ncurses-oo');
var nc = ncurses;
nc.curs_set(0);
var root = new nc.NCPanel(); // full-screen panel.
root.attrset( nc.color_pair('white','red') );
root.mvaddstr(4,0,"I am the root panel.");
root.captureCout();
root.captureCerr( nc.color_pair('red','white') | ncurses.A_BLINK | ncurses.A_BOLD );
print("std::cout is redirected here.");
function tryPad()
{
try
{
var pnl = new nc.NCPanel(10,30,3,3);
print('new child =',pnl.name());
pnl.scrollok(true);
pnl.bkgd( nc.intVal(' ') | nc.color_pair('white','blue') );
var pad = new nc.NCPad(pnl,100,100);
print("Wrapper pad added.");
pad.bkgd( nc.intVal(' ') | nc.color_pair('blue','white') | ncurses.A_BIM );
pad.mapKeyToReq( ncurses.KEY_CTRL('G'), ncurses.NCPad.PadReqExit );
pad.scrollok(true);
cn = pad.name();
for( var i = 0; i < 5; ++i )
{
pad.addstr("#"+i+": This is window "+cn+"\n");
}
pad.addstr("Going into input loop.\nTry scrolling.\nTap Ctrl-G to stop.\n");
pnl.refresh();
pad.inputLoop();
pad.addstr("Input loop finished.");
pad.refresh();
print("Now tap a key to destroy these objects.");
ncurses.beep();
pnl.getch();
pad.close();
pnl.close();
}
catch(e)
{
for( var i = 0; i < 5; ++i ) ncurses.beep();
print("exception:",e);
}
}
tryPad();
root.refresh();
print("All done. Tap a key to quit.");
ncurses.beep();
var rc = root.getch();
root.close();
print("Done! :-D");
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||