Introducing Terminal::Print

After a long incubation period of intermittent hacking, I am pleased to present Perl hackers worldwide with the option to quickly and easily print to their console screen.

Ok.. why?

From the module's README:

Terminal::Print intends to provide the essential underpinnings of command-line printing, to be the fuel for the fire, so to speak, for libraries which might aim towards 'command- line user interfaces' (CUI), asynchronous monitoring, rogue-like adventures, screensavers, video art, etc.

Let's first point out that this module is a serious attempt at solving a relatively frivolous problem. There are plenty of existing text display engines (hello, ncurses!), game engines, and other Perl modules that already solve many command line display (CLD) issues.

However: ncurses is not re-entrant. Furthermore, its UTF-8 support is (understandably) bolted on. Looking around at other C library options, it came to dawn on me: why was I involving "native" options, anyway?

Yes, Perl 6 has a great FFI with NativeCall, but it also has amazing Unicode support and a heap of async primitives that could be really useful in the context of CLD. None of which required native stuffs directly.

So.. let's skip the whole native library concern and see what we can get done with pure Perl (and a little help from tput).

Ok... how?

Under the hood, the Terminal::Print class imports a module called Terminal::Print::Commands. This is where the magic happens: a BEGIN block which shells out to tput and gets the correct escape codes for the current terminal.

(tput is a small utility that comes with ncurses that queries the terminfo databse to get escape sequences for your various needs across a galaxy of different TTYs. In practice I suspect it returns the ANSI escape sequences for most of us now, but -- better safe than sorry.)

Small note about precompilation

For the sake of speed, I have pushed the only two "truly" hard-dynamic shell-outs to yet another file, Terminal::Print::Dimensions. This file gets pragmad with no precompilation -- crucially keeping the width and height dynamic at run time.

The rest of the escape code responses from tput will have been pre-compiled, so if you run into issues while switching between TTYs, this could be your problem. Please file a bug report.

OO::Monitors is that good magic

When instantiated, Terminal::Print creates a Terminal::Print::Grid object. This object is totally awesome! Written in the exact semantics of a class, it is instead defined with the monitor keyword (provided by OO::Monitors) as opposed to class.

With that single word change, Terminal::Print::Grid becomes thread-safe -- only one thread can be "in" a given method at a given time. This allowed me to drastically reduce the complexity of Terminal::Print::Grid. This module had a tendency to tarpit before I tried to add async, so it's just unbelievably awesome to be able to ship with a lean, mean, and eminently readable implementation. Fourth rewrite being the charm, apparently!

Ok.. show and tell!

Because I deeply agree with Dione Warwick, I'll present here my very first test case (now shipping as examples/show-love.p6):

my @colors = <red magenta yellow white>;

my $b = Terminal::Print.new;
$b.initialize-screen;

for $b.indices.pick(*) -> [$x,$y] {
    next unless $x %% 3;
    $b.print-cell: $x, $y, %( char => '♥', color => @colors.roll );
}

sleep 2;
$b.shutdown-screen;

First I create a list of colors I'd like to randomly choose from. Then I create a Terminal::Print object and, crucially, I call .initialize-screen. This saves the current state, blanks the terminal, and hides the cursor.

It then proceeds to iterate through the randomized indices of $b, skipping any coordinates where $x is not divisible by three. (Using .grep directly on the indices list might be a bit faster, but I enjoy this syntax -- it allows me to match against the decomposed $x instead of $_[0] and, to be blunt, I'm not running Perl 6 screen animations for speed thrills).

.print-cell takes three arguments: an 'x', a 'y', and a "cell". The cell can be a single character or a hash with char and color defined. The colors conform to what is provided by Terminal::ANSIColor, so you can change background color and bold/underline as well.

By the time this for loop is finished, one would hopefully feel the intended effect of a 'hug via terminal'. One will have, at least, witnessed a screen full of colorful hearts.

Then we call .shutdown-screen, et voila! Everything is back to normal.

Now, just as a tease (a proper introduction to the golfing syntax will need to wait for a different post), I present you with a golfed version of the above:

perl6 -MTerminal::Print -e 'd({ for in.grep({$_[0]%%3}).pick(*) ->[$x,$y] {cl($x,$y,"♥",fgc.roll)}; slp 2; .keep});'

Ok.. where's the async?

It's wherever you put the async, my friend!

Observe this example, adapted from the test suite (we don't wait 15 seconds in the test suite unless you specify a number of seconds via an env var):

draw( -> Promise $p {
    my $secondly = Supply.interval(1);
    $secondly.tap: { T.print-string(T.columns/2, T.rows/2, DateTime.now(formatter => { sprintf "%02d:%02d:%02d",.hour,.minute,.second })) };
    my $ender = Supply.interval(15);
    $ender.tap: { $++ && $p.keep };
});

A Supply is created which will "tick" at intervals of 1 second. We pass a block to .tap, which places it in a queue of blocks to be fired at every "tick". In effect, this prints a clock on the screen which updates every second.

Another timer of 15 seconds is set. We use an anonymous state variable to create a logical expression which won't evaluate to true the first time our 15s Supply fires. In other words, $++ becomes true on the second iteration (when the timer actually hits 15, the first "tick" is at 0), which then keeps the Promise that has been passed into the subroutine.

That's the nifty thing about calling draw -- it will call .initialize-screen and .shutdown-screen for us. All we have to do is take a Promise as our only argument and .keep it once we are finished with our drawing. At which point your entire script can end up being a one-liner, with varying degrees of obfuscation vs verbosity.

This example also shows some additional golfy niceness, as we have a Terminal::Print object available in our scope via T. Until I hear a good reason not to, we will be shipping a T object for you in any scope you choose to use Terminal::Print in. If you run into any issues with the current setup, or have strong opinions against having golf-isms imported by default, raise a ticket and let's have a chat!

Ok.. where?

Ah, thanks for asking! We are currently hosted on github @ Terminal::Print

Thanks for reading!

1 Comment

Awesome - I really enjoyed the YAPC::EU talk on this module. A good pitch for Perl 6 too!

Leave a comment

About ab5tract

user-pic For the love of Perl.