NAME

Fugu::REPL - a line editor for an operator prompt

SYNOPSIS

use Fugu::REPL;

my $repl = Fugu::REPL->new(
    prompt   => 'fugu> ',
    commands => { help => 'show this help', quit => 'end the session' },
    complete => sub ($word, $line) { return @candidates },
    watch    => [$reply_pipe],
);

while (defined(my $line = $repl->read_line)) {
    $repl->show($output);
}
exit 1 if $repl->event eq 'watch';

DESCRIPTION

Fugu::REPL reads one operator command line from a terminal. It edits the line with a fixed emacs-style key subset, completes a word, keeps a session history in memory, and filters untrusted bytes before it shows them.

The module stands alone. It uses core Perl only, it loads no other Fugu:: module, and it never logs: every failure is a return value. It operates inside the stdio tty promises of pledge(2): it opens no file, creates no process, and reaches no network. The escape sequences are the fixed ANSI set, because a terminfo read needs the rpath promise.

The editor holds the terminal in raw mode only while it reads. restore() puts the terminal back on every exit path, and the destructor calls it.

This document is the interface contract of the module. The FuguPass and FuguTTX specifications cite it, so a change here coordinates with both.

new

new(%args) builds an editor. The method opens nothing and changes no terminal setting.

in

The input handle. The default is STDIN.

out

The output handle. The default is STDOUT.

prompt

The prompt string. The default is >.

commands

The command table, as a hash reference of name to summary.

prefix

The command prefix, as a string. The default is the empty string. commands and prefix describe one command language: a tool with bare command words has an empty prefix, and a tool that marks a client command with a solidus has the prefix /.

complete

A code reference for word completion.

watch

Extra read handles, as an array reference. A handle that becomes readable ends the prompt read.

history_size

The count of lines that the history keeps. The default is 500.

new dies when complete is not a code reference, and when watch holds a handle with no descriptor. Both are programming errors.

read_line

read_line() reads one line and returns it, without the terminator.

The method returns undef at an end of file, when a watched handle becomes readable, and when the operator interrupts the line. event() reports which outcome occurred, so a caller never guesses.

The method holds raw mode only for the length of the call. The line joins the history when it is not empty and not the previous line.

event

event() returns the outcome of the most recent read_line() or confirm() call: line, eof, watch, or interrupt. Before the first call it returns undef.

An interrupt at the prompt clears the line. It does not end the session; the caller decides what an interrupt means outside the prompt.

ready_handle

ready_handle() returns the watched handle that ended the read, after an event of watch. It returns undef otherwise.

A closed handle is readable at an end of file. Thus a caller learns that its peer went away, and the caller decides what that means.

confirm

confirm($question) asks a yes-or-no question. The method returns 1 for y and yes, in each letter case, and 0 for every other answer. The default is no: an empty answer, an end of file, and an interrupt all answer no. A gate that a stray keystroke can open is not a gate. The answer stays out of the history.

display_filter

display_filter($bytes) returns the bytes that are safe to show. This is a plain function, not a method, because a caller filters bytes that no editor read.

The filter keeps printable ASCII, the line feed, and the horizontal tab. It removes DEL (0x7F) and the C1 range (0x80 to 0x9F), as a raw byte and as a UTF-8 sequence alike. Every other valid UTF-8 sequence survives whole, and every other byte becomes one question mark.

An escape sequence in tool output can rewrite what the operator sees. It can hide the change that the operator is about to approve, so every untrusted byte passes this filter before display.

show

show($bytes) filters the bytes with display_filter() and writes them to the output handle. The method returns the object.

help_text

help_text() returns the help for the command table: one line for each command, with the prefix, the name, and the summary, in sorted order. The module generates the help, so the table and the help cannot disagree.

history

history() returns the session history, oldest first. add_history($line) appends one line and drops the oldest line above history_size.

The history lives in memory only, for the session. The module never writes a history file, because a history file leaks the words that an operator typed.

is_interactive

is_interactive() returns 1 when the input handle is a terminal, and 0 otherwise.

With a 0 answer the module reads plain lines: no editing, no history recall, and no escape output. Scripted tests drive a session in this mode.

prompt, commands, watch

prompt($string) sets or reads the prompt. commands($hashref) sets or reads the command table. watch($arrayref) sets or reads the watched handles.

A caller changes the completion set when the session state changes, for example when an index opens after an unlock.

restore

restore() puts the terminal settings back and returns the object. The method is idempotent, and the destructor calls it.

THE KEY SUBSET

The editor implements one fixed set of keys.

Enter               accept the line
Tab                 complete the word before the cursor,
                    and cycle through the candidates
Shift-Tab           cycle back through the candidates
Ctrl-A, Home        go to the start of the line
Ctrl-E, End         go to the end of the line
Ctrl-B, Left        go back one character
Ctrl-F, Right       go forward one character
Ctrl-P, Up          recall the previous history line
Ctrl-N, Down        recall the next history line
Backspace, Ctrl-H   delete the character before the cursor
Ctrl-D              delete the character under the cursor,
                    or end the input on an empty line
Ctrl-K              delete to the end of the line
Ctrl-U              delete the whole line
Ctrl-W              delete the word before the cursor
Ctrl-L              draw the line again
Ctrl-C              clear the line and report an interrupt

An unknown escape sequence does nothing. The editor never inserts the bytes of a sequence into the line.

COMPLETION

The complete callback receives the word before the cursor and the whole line. It returns the candidate list.

The module completes a command name from the command table when the cursor is in the first word of the command language. It calls the callback otherwise. One candidate replaces the word. Several candidates extend the word to the common prefix. When no extension remains, Tab shows the list once, selects the first candidate, and starts a cycle. Each further Tab selects the next candidate, and Shift-Tab selects the previous one. The cycle wraps, and every other key ends it.

The callback runs in the caller's process, at the prompt. A callback that blocks blocks the prompt.

RETURN VALUES

read_line() returns the line, or undef at an end of file, on a watch event, and on an interrupt. confirm() returns 1 or 0. event() returns line, eof, watch, interrupt, or undef before the first read. ready_handle() returns a handle after a watch event and undef otherwise.

EXAMPLES

This example runs a session that a peer on a reply pipe can end:

my $repl = Fugu::REPL->new(
    commands => { help => 'show this help', quit => 'end the session' },
    watch    => [$reply],
);

while (1) {
    my $line = $repl->read_line;
    if (!defined $line) {
        next if $repl->event eq 'interrupt';
        last;
    }
    last if $line eq 'quit';
    $repl->show( handle_command($line) );
}

This example gates a dangerous step:

apply_change() if $repl->confirm('Apply the change?');

ERRORS

new() and the watch() accessor die on a programming error: a complete argument that is not a code reference, or a watch handle with no descriptor. Every other failure is a return value, and event() names it.

SEE ALSO

pledge(2), termios(4), Fugu::CLI, Fugu::Signal

AUTHORS

Dick Olsson <hi@senzilla.io>

CAVEATS

The editor counts a UTF-8 sequence as one character for the cursor. A double-width character therefore draws one column short.

A line that grows past the terminal width wraps, and the redraw then trails the wrapped part. An operator command line is short in practice.

A candidate list can be longer than the terminal. The module shows the whole list and draws the prompt again; it has no page mode.

A signal that interrupts a blocked read reports the event interrupt. The caller checks its own signal flags and decides whether to read again.