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!
Awesome - I really enjoyed the YAPC::EU talk on this module. A good pitch for Perl 6 too!