Fugu

Fugu::REPL(3p) Perl Library Manual Fugu::REPL(3p)

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

    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';

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(%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() 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() 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() 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($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($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($bytes) filters the bytes with display_filter() and writes them to the output handle. The method returns the object.

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() 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() 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($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() puts the terminal settings back and returns the object. The method is idempotent, and the destructor calls it.

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.

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.

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.

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?');

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.

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

Dick Olsson <hi@senzilla.io>

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.

2026-10-16 OpenBSD