Tired of noisy interfaces? Bored of clicking and scrolling? Want to build a console interface but having a hard time with curses? Maybe it's time to ...
In this article, we are going to write a simple file browser. In a handful of lines of code you'll have the basics of something like the venerable midnight-commander. We'll also learn enough of the Raku programming language to impress your friends and scare your enemies. This entire example is in the mc.raku file in the eg directory of the Terminal-UI source code.
First, install Raku and Terminal::UI. For Raku, head over to
rakubrew.org and follow the instructions there.
This will also give you zef, Raku's package manager, so you can run:
zef install Terminal::UI
Okay! Now let's start with a "hello, world"ish program to see how things work. Our starting program will divide the screen in half, print a message, and wait for a keystroke. It looks like this:
#!/usr/bin/env raku
use Terminal::UI 'ui';
ui.setup: :2panes;
ui.panes[0].put: "welcome!";
ui.get-key;
ui.shutdown;
╔═════════════════════════════════════╗
║welcome! ║
║ ║
║ ║
║ ║
║ ║
║ ║
║ ║
║ ║
║ ║
╟─────────────────────────────────────╢
║ ║
║ ║
║ ║
║ ║
║ ║
║ ║
║ ║
║ ║
║ ║
╚═════════════════════════════════════╝
Let's break this down.
Using the module with 'ui' and calling setup gives us a
new ui object to work with, and a frame with two
panes which divide the screen in half vertically.
Note that ui.setup: :2panes is the same as ui.setup(panes => 2);
method calls can use a colon instead of parentheses, and numeric
named arguments can precede the name of the argument if you put a
colon first. This syntax sort of reminds me of Larry's "First Law of
Language Redesign: Everyone Wants the Colon".
Anyway, ui.panes are the two panes -- each one is a Terminal::UI::Pane
object, which has a method put which puts things on the screen.
More on this later. Also ui.get-key waits for a single key.
We need a way to get a directory listing! Let's dive right in and make this fancy! I'll explain afterwards.
use Terminal::ANSI::OO 't';
use Terminal::UI 'ui';
ui.setup: :2panes;
sub show-dir($dir) {
my \p = ui.panes[0];
p.clear;
p.put: [t.yellow => "$dir"];
p.put: ".. (up)", :meta( dir => $dir.parent );
for $dir.dir.sort({.d.Int,.fc}) {
next if .basename.starts-with('.');
p.put: [t.cyan => .basename ~ '/'], :meta(:dir($_)), :!scroll-ok when .d;
p.put: .basename, :meta(:file($_)), :!scroll-ok when .f;
}
p.select-first;
}
show-dir($*CWD);
ui.interact;
ui.shutdown;
First, using Terminal::ANSI::OO 't' brings a t object which can set the color.
When sending color strings, use an array [ ... ], and send pairs, where the key
is the color and the value is the string.
Also -- put can take a named parameter meta -- this associates metadata with the
current line. We're going to use this in a minute.
We send a function to sort to put directories first, then sort
alphabetically, case-insensitively.
The put method can also take a boolean -- scroll-ok -- we send a false value for
that because we don't want to scroll automatically when we show the directory contents.
The p.select-first line selects (and highlights) the first line of the pane.
Calling ui.interact enters an interaction event loop -- arrows move the selected line
up and down, and tab will switch between panes. So at this point, you can navigate up
and down, and scroll.
Oh, and $*CWD is the current working directory, and methods like parent and basename
and starts-with do what you think.
╔═════════════════════════════════════════════════╗ ║/home/bduggan/code/raku-terminal-ui ║ ║.. (up) ║ ║eg/ ║ ║lib/ ║ ║script/ ║ ║t/ ║ ║LICENSE ║ ║make ║ ║Makefile ║ ║META6.json ║ ║README.md ║ ╟─────────────────────────────────────────────────╢ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ╚═════════════════════════════════════════════════╝
╔═════════════════════════════════════════════════╗ ║/home/bduggan/code/raku-terminal-ui ║ ║.. (up) ║ ║eg/ ║ ║lib/ ║ ║script/ ║ ║t/ ║ ║LICENSE ║ ║make ║ ║Makefile ║ ║META6.json ║ ║README.md ║ ╟─────────────────────────────────────────────────╢ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ╚═════════════════════════════════════════════════╝
We're almost done with our example -- to allow selection to actually work, we
use the on method -- setting up the action to perform when select is performed
on the first pane. That looks like this:
ui.panes[0].on: select => -> :%meta {
show-dir($_) with %meta<dir>;
show-file($_) with %meta<file>;
}
Now we can for instance, select the eg/ directory. Oh first here's show-file :
sub show-file($file) {
ui.panes[1].clear;
start ui.panes[1].put: $file, :!scroll-ok;
}
We use start to run in a thread (safe, **cough** unlike curses **cough**). And
this works because put calls .lines on whatever is being put -- so in this
case it calls $file.lines and then puts each line on the screen one at a time.
Now we have a way to browse a file system and read text files.
As a final piece, let's add an example of how to set up a custom
key binding to show some information about a file. Typing an i
will show information about a file.
ui.bind: 'pane', i => 'info';
ui.panes[0].on: info => -> :%meta ( :$file, :$dir ) {
with $file {
ui.alert([
$file.basename,
"Last modified: { $file.modified.DateTime }"
])
}
}
We call bind with two arguments: pane, and a key value Pair.
The second argument -- the key-value Pair -- indicates that the
action info will be called when the i key is pressed.
The argument pane ties the parameters for the callback to the user
interface context. It indicates that the currently selected line (within
the currently active pane), will be examined, and the incoming %meta
will correspond to the metadata for that line.
An additional named argument -- $raw -- will also be sent, which will
have the raw text of the selected line.
Using on sets up this callback. In this case we're being a little
bit fancy and using argument destructuring to automatically extract the
file or dir metadata arguments in the signature of the callback. Then
we call ui.alert to display a little box with some information.
╔═════════════════════════════════════════════════╗ ║/home/bduggan/code/raku-terminal-ui/eg ║ ║.. (up) ║ ║alert.raku ║ ║color.raku ║ ║fixed-height.raku ║ ║full.raku ║ ║hello-world.raku ║ ║help.raku ║ ║input.raku ║ ║mc.raku ║ ║meta.raku ║ ╟─────────────────────────────────────────────────╢ ║#!/usr/bin/env raku ║ ║use Terminal::UI 'ui'; ║ ║use Terminal::ANSI::OO 't'; ║ ║ ║ ║ui.setup: :2panes; ║ ║ui.panes[0].put: "welcome!"; ║ ║ui.get-key; ║ ║ ║ ║my \p = ui.panes[0]; ║ ║p.clear; ║ ║p.put: [t.yellow => "$dir"]; ║ ╚═════════════════════════════════════════════════╝
Here are the final results -->
There's still some work to do to emulate all of the midnight-commander functionality, but hopefully this is enough to demonstrate some of the capabilities of Terminal UI.
Have fun and happy hacking!
╔═══════════════════════════════════════════════════════╗ ║/home/bduggan/code/raku-terminal-ui/eg ║ ║.. (up) ║ ║alert.raku ║ ║color.raku ║ ║fixed-height.raku ║ ║full.raku ║ ║hell╔════════════════════════════════════════════╗ ║ ║help║ ║ ║ ║inpu║ mc.raku ║ ║ ║mc.r║ Last modified: 2021-03-08T14:03:13.083051Z ║ ║ ╟────║ ║─────╢ ║#!/u║ ok ║ ║ ║ ╚════════════════════════════════════════════╝ ║ ║use Terminal::UI 'ui'; ║ ║use Terminal::ANSI::OO :t; ║ ║ ║ ║ui.setup: :2panes; ║ ║ui.panes[0].put: "welcome!"; ║ ║ui.get-key; ║ ║ ║ ╚═══════════════════════════════════════════════════════╝