Lever console had to be homebrew

If you've been reading about Lever, I first moved from gnu readline to editline, and then I finally ended up to writing my own editline routines.

There was no choice to do otherwise. Readline comes with an incompatible license. Editline took more effort to adjust for my language than writing it from scratch.

Modern terminals work with ANSI TTY escape codes. A line editor utility such as readline sets the terminal from baked mode to raw. The raw mode allows code to read keyboard input directly.

Line editors have to be aware of where the cursor is in the screen. ANSI TTYs are slightly backwards and only report cursor positions in the screen-coordinates. This is useless if you want to keep the text scrollable.

The state of the art in line editors for determining the cursor location is to calculate from the text stream where the cursor ends up to and reposition it with relative motion commands.

With some care you can have complete control of the console screen without crobbering the contents. Lever's line editor comes with two fairly novel features not seen elswhere.

Asynchronous writeout

Whenever something writes into the stdout while the readline operation takes place, the terminal output leaves fragments of the line editor into the output stream as a trash. Write into output with readline on is usually avoided for this reason.

At many computer programs the readline operation halts the program until you respond. Therefore it's only a problem at multithread/multiprocess programs that something else writes into the stdout and interferes with the terminal output. This almost always ends up with trash in the output stream, fragments of the line editor prompt in middle of the text.

This is a common case in async and interactive systems. Therefore I designed my line editor utility to take full control over the TTY outputs and get the logging routed through a function, rather than directly into stderr/stdout.

Doing this requires little bit of care to avoid faulty forms of logging from forming infinite exception loops. But otherwise it should work just well.

Apropos at tab completion

Another thing Lever's editline is doing different from others is to show short description during the tab completion. Just like the apropos command in Linux. When you start a Lever console and press tab, you get this:

LEVER 0.X.0
>> 
AssertionError          SourceLocationLines     backward
AttributeError          StringBuilder           bool
CallError               SystemExit              builtin
DocRef                  TTY                     chdir
Error                   TypeError               chr
Event                   UVError                 clamp
Exception               Uint8Array              class
FrozenError             Uint8Builder            coerce
IOError                 Uint8Slice              cos
Import                  UncatchedStopIteration  cross
InstructionError        Utf8Decoder             decode_utf8
KeyError                ValueError              dict
ModuleScope             abs                     dir
Pipe                    and                     doc
SourceLocation          axisangle               doc...

There are some glitches on this, but they'll be unwrinkled over time. Now if you write "ModuleScope" and then press tab again:

LEVER 0.X.0
>> ModuleScope
class ModuleScope(local:path, [parent:module_scope], [options:object])

The quick apropos is derived by inspecting the object.

Later it will be possible to also provide a slow apropos that comes directly from the offline documentation.

Short Do-it-yourself

Lever's console will be eventually accessible as a library, that's not done yet. Instead I show how you can make a console in Lever.

# This command sets the mode raw and allows to read
# the input character-by-character.
stdin.set_mode('raw')

# You need storage for the text you write.
readbuf = []

# And storage for the output.
writebuf = []

# An ordinary loop.
running = true
while running
    # Writing our contents to the screen.
    stdout.write(">> " ++ "".join(readbuf))

    # Here we loop through each hcharacter
    for ch in stdin.read()
        n = ord(ch)
        if ch == '\x03'              # responds to CTRL+C
            running = false          # otherwise you cannot exit.
        elif ch == '\r'              # response to return: clean the readbuf.
            text = "".join(readbuf)
            if text == "exit"        # another way to exit.
                running = false
            writebuf.append(text ++ "\n")
            readbuf = []
        elif ch == '\x7f'            # response to backspace.
            if readbuf.length > 0
                readbuf.pop()
        elif (0x20 <= n and n < 127) or n > 255
            readbuf.append(ch)       # writes to the readbuf.
        else
            # print out characters that were not understood.
            writebuf.append(repr(ch) ++ "\n")

    ## The refresh cycle
    # The libuv TTY is guaranteed to understand
    # ANSI escape codes.
    stdout.write('\033[0G') # moves to the left of the line.
    stdout.write('\033[0J') # clears off under and right
                            # of the cursor.
    # Write cycle into the output empties the writebuf.
    stdout.write("".join(writebuf))
    writebuf = []

The stdin and stdout are standard input and standard output streams. Otherwise we operate with lists and strings there. Those behave identically to their python counterparts.

Lever console started out as code like this. It eventually separated into two separate loops for the reads and writes and gained more features.

Similar posts