There are several eggs available already that will give your application line editing capabilities. Each on of these offers its own share of advantages: readline being mature (and some might say bloated) and linenoise being bsd licensed and small.
Both of these are made available through chicken's FFI but this approach has a drawback when it comes to threading:
Blocking I/O will block all threads, except for some socket operations (see the section about the tcp unit). An exception is the read-eval-print loop on UNIX platforms: waiting for input will not block other threads, provided the current input port reads input from a console.
(taken from notes to srfi-18)
This means that code like this will not work as expected:
(use linenoise miscmacros srfi-18 tcp) (define-values (i o) (tcp-connect "ifmud.port4000.com" 4000)) (define reader (make-thread (lambda () (let loop () (thread-wait-for-i/o! (port->fileno i)) (while (and (char-ready? i) (not (eof-object? (peek-char i)))) (printf "~a" (read-char i)) (flush-output)) (loop))))) (thread-start! reader) (let loop () (let ((c (linenoise ""))) (fprintf o "~a~%" c) (flush-output o) (if (equal? c "quit") (begin (close-input-port i) (close-output-port o) (exit 0)) (loop))))
Here you can see that I wanted a small telnet client for playing on IFmud. But instead of getting the expected behaviour, the loop blocked the reader procedure. By repeatedly hitting the return key one could see IFmud's banner scrolling by.
So another approach was needed. I liked the simplicity of the linenoise library and I kept hearing the need for a line editing lib for csi, especially from newcomers (or people that do not want to run CSI inside EMACS).
After thinking about it for a while feature creep set in. I settled for these features:
- written in scheme (although some FFI calls are still needed for setting the terminal)
- user expendable by providing own key handlers
- Minimal set of dependencies as I don't want to bloat up other people's code with my egg
Enter parley. Think of it as "Parley - When you need to negotiate input". It is a small chicken egg with a dependency for the stty egg, which may also vanish soon.
Parley can be used as a drop-in replacement for linenoise. The above example will work like a charm if you replace linenoise with parley.
Currently the following key bindings are implemented:
- Cursor movement: Arrow keys (left right), CTRL-A, CTRL-E
- History: Arrow keys (up down), CTRL-P, CTRL-N for previous and next in history
- Editing / Deletion: CTRL-U for the current line, CTRL-K for everything after the cursor, CTRL-T swap current char with previous
A handler for a character is a procedure that follows the simple rule #1. Accept and return this (rather long) list of the following arguments:
- prompt
- The shown prompt for the line or ""
- in
- the input port
- out
- the output port
- line
- The current line as string
- pos
- The current (0-based) cursor position with respect to line
- return
- A continuation that should be called if the input is done. This usually breaks out of the prompt loop.
New key bindings are added with the add-key-binding! procedure, which will override the current handler for the given key if present. If you like to add an escape sequence set the esc-sequence: keyword parameter to #t. This will match on ESC Sequences like '\x1b[<char>'. So if you are not scared of adding yet another dependency you can add a handler for deleting the last typed word before the cursor with CTRL-W like this:
(use parley srfi-14) (define (delete-last-word line pos) (let* ((del-pos (or (string-index-right line (char-set-union char-set:whitespace char-set:punctuation) pos) 0)) (left-part (if (> del-pos 0) (string-take line del-pos) "")) (npos (- pos (- pos del-pos)))) (values (string-append left-part (string-drop line pos)) npos))) (define (cw prompt in out line pos exit) (receive (l p) (delete-last-word line pos) (list prompt in out l p exit))) (add-key-binding! #\x17 cw)
Parley of course can also be used within CSI. Just add this to .csirc:
(use parley) (let ((old (current-input-port))) (current-input-port (make-parley-port old)))
Things that are still on my list are:
- How to properly support unicode?
- Drawing of caret notation currently needs more than one char which distorts the line calculation stuff.
- Separate state from module instance so you can nest several prompts and do not mix history for example
So I hope this small introduction did wet your appetite for this shiny new egg. Please mail me war stories of your usage and the key handlers you have made. If there are enough it might be worth bundling them together.