]> cgit.babelmonkeys.de Git - jubjub.git/commitdiff
Add line editing support to the CLI UI
authorFlorian Zeitz <florob@babelmonkeys.de>
Wed, 17 Apr 2013 18:51:31 +0000 (20:51 +0200)
committerFlorian Zeitz <florob@babelmonkeys.de>
Wed, 17 Apr 2013 18:51:31 +0000 (20:51 +0200)
src/gui/cli/JubCLIChatUI.h
src/gui/cli/JubCLIChatUI.m
src/gui/cli/JubCLIColor.h
src/gui/cli/JubCLIUI.h
src/gui/cli/JubCLIUI.m
src/gui/cli/Makefile
src/gui/cli/linenoise.h [new file with mode: 0644]
src/gui/cli/linenoise.m [new file with mode: 0644]

index c0f88743f1f005a8cb9b3af5560ad67cdc3a67bc..7ebe0b1dfa46d228c3a072f113361aad083c7aa0 100644 (file)
@@ -5,6 +5,7 @@
 @interface JubCLIChatUI: OFObject <JubChatUI>
 {
        jub_send_block_t _sendBlock;
+       OFString *_title;
 }
 
 - (void)send: (OFString*)text;
index 5803ba75589c0ca95cc383cbf15164ea495b944f..19e3629c87f21c4d1b42005724cf5019023495a9 100644 (file)
@@ -1,6 +1,8 @@
 #import "JubCLIChatUI.h"
 #import "JubCLIColor.h"
 
+#import "linenoise.h"
+
 @implementation JubCLIChatUI
 - initWithTitle: (OFString*)title
      closeBlock: (jub_close_block_t)closeBlock
@@ -10,6 +12,7 @@
 
        @try {
                _sendBlock = [sendBlock copy];
+               _title = [title copy];
        } @catch (id e) {
                [self release];
                @throw e;
 - (void)dealloc
 {
        [_sendBlock release];
+       [_title release];
        [super dealloc];
 }
 
 - (void)addMessage: (OFString*)text
            sender: (OFString*)sender
 {
-       [of_stdout writeFormat: BOLD("%@:") @" %@\n", sender, text];
+       [of_stdout writeString: @"\r" COL_IN(@"-> ")];
+       [of_stdout writeFormat: BOLD(@"%@:") @" %@\n", sender, text];
+       [[Linenoise sharedLinenoise] refreshLine];
 }
 
 - (void)send: (OFString*)text
 {
+       [of_stdout writeString: @"\033[1A" COL_OUT(@"<- ")];
+       [of_stdout writeFormat: BOLD(@"%@:") @" %@\n", _title, text];
+
        _sendBlock(text);
 }
 @end
index 147196903da273df02b42af72787fb90983b3c43..acca2d9f7e7d61b538e6f30e247327514d5f8c3d 100644 (file)
@@ -1,4 +1,4 @@
-#define BOLD(text) @"\033[1m" text "\033[0m"
+#define BOLD(text) @"\033[1m" text @"\033[0m"
 
 #define COL_CHAT(text) @"\033[32;1m" text @"\033[0m"
 #define COL_ONLINE(text) @"\033[32m" text @"\033[0m"
@@ -6,3 +6,6 @@
 #define COL_XA(text) @"\033[34;1m" text @"\033[0m"
 #define COL_DND(text) @"\033[35;1m" text @"\033[0m"
 #define COL_OFFLINE(text) @"\033[31m" text @"\033[0m"
+
+#define COL_IN(text) @"\033[32;1m" text @"\033[0m"
+#define COL_OUT(text) @"\033[34;1m" text @"\033[0m"
index b610d0a18bce9c4a1a35c09e00c3f5534aff7efd..222fc27471b62468d7baa2be8433fb5b4a086fe5 100644 (file)
@@ -2,6 +2,7 @@
 #import <ObjXMPP/ObjXMPP.h>
 
 #import "JubUI.h"
+#import "linenoise.h"
 
 @class JubCLIChatUI;
 @class JubChatClient;
index 6f0b03a68c5d51440a6c13d36b8f5850f7845a27..3cf59c2b05b96b5e5fb6a57ce528c4b77c7c6672 100644 (file)
@@ -4,6 +4,8 @@
 #import "JubCLICommand.h"
 #import "JubCLIUI.h"
 
+#include <string.h>
+
 BEGINCLICOMMAND(JubCLIReplyCommand, @":r", nil,
     @"Sets the sender of the last incomming message as the default recipient")
 {
@@ -177,11 +179,68 @@ ENDCLICOMMAND
        [super dealloc];
 }
 
+static JubCLIUI *completionData;
+static void completionCallback(OFString *buf, OFList *lc)
+{
+       if ([buf length] < 3)
+               return;
+
+       if (![buf hasPrefix: @":s "] && ![buf hasPrefix: @":m "] &&
+           ![buf hasPrefix: @":t "])
+               return;
+
+       if ([buf hasPrefix: @":t"]) {
+               OFString *options[] = {
+                       @":t available",
+                       @":t away",
+                       @":t dnd",
+                       @":t xa",
+                       @":t chat",
+                       @":t unavailable"
+               };
+
+               for (int i = 0; i < 6; i++) {
+                       if (![options[i] hasPrefix: buf])
+                               continue;
+                       [lc appendObject: options[i]];
+               }
+
+               return;
+       }
+
+       OFString *command = [buf substringWithRange: of_range(0, 3)];
+       OFString *query = [buf substringWithRange: of_range(3, [buf length]-3)];
+       OFDictionary *contacts = completionData.client.contactManager.contacts;
+       for (OFString *key in contacts) {
+               if (![key  hasPrefix: query])
+                       continue;
+               [lc appendObject: [command stringByAppendingString: key]];
+       }
+}
+
 - (void)startUIThread
 {
-       [of_stdin asyncReadLineWithTarget: self
-                                selector: @selector(Jub_userInputWithStream:
-                                                    line:exception:)];
+       completionData = self;
+
+       [[OFThread threadWithBlock: ^(void) {
+               OFString *line;
+               Linenoise *reader = [Linenoise sharedLinenoise];
+               reader.multiline = true;
+               reader.completionCallback = completionCallback;
+
+               while ((line = [reader readInputWithPrompt: @"> "]) != nil) @autoreleasepool {
+                       [self Jub_userInputWithStream: nil
+                                                line: line
+                                           exception: nil];
+                       if ([line length] != 0)
+                               [reader addHistoryItem: line];
+               }
+               [self Jub_userInputWithStream: nil
+                                        line: nil
+                                   exception: nil];
+
+               return nil;
+       }] start];
 }
 
 -      (void)client: (JubChatClient*)client
@@ -204,16 +263,18 @@ ENDCLICOMMAND
                           line: (OFString*)line
                      exception: (OFException*)exception
 {
-       if (line == nil)
+       if (line == nil || exception != nil)
                [OFApplication terminate];
 
        if ([line length] == 0)
                return YES;
 
        if ([line characterAtIndex: 0] != ':') {
-               if (_sink == nil)
+               if (_sink == nil) {
                        [of_stdout writeLine: @"No default sink selected, "
                            @"type `:h` for help"];
+                       return YES;
+               }
 
                [_sink send: line];
 
@@ -288,6 +349,7 @@ ENDCLICOMMAND
 -   (void)contact: (XMPPContact*)contact
   didSendPresence: (XMPPPresence*)presence
 {
+       [of_stdout writeFormat: @"\r"];
        [of_stdout writeFormat: BOLD("%@") @" is now in state ", presence.from];
 
        if ([presence.type isEqual: @"unavailable"])
@@ -307,5 +369,7 @@ ENDCLICOMMAND
                [of_stdout writeFormat: @": %@", presence.status];
 
        [of_stdout writeString: @"\n"];
+
+       [[Linenoise sharedLinenoise] refreshLine];
 }
 @end
index 807a20ad238be8ebbb1edd74f77b5ec3dbb7295e..2fd5a45b193a1c46ad06a99e4e7c351b70822e18 100644 (file)
@@ -1,6 +1,7 @@
 STATIC_LIB_NOINST = cli.a
 SRCS = JubCLIUI.m      \
-       JubCLIChatUI.m
+       JubCLIChatUI.m  \
+       linenoise.m
 
 include ../../../buildsys.mk
 
diff --git a/src/gui/cli/linenoise.h b/src/gui/cli/linenoise.h
new file mode 100644 (file)
index 0000000..188eece
--- /dev/null
@@ -0,0 +1,105 @@
+/* linenoise.h -- guerrilla line editing library against the idea that a
+ * line editing lib needs to be 20,000 lines of C code.
+ *
+ * See linenoise.m for more information.
+ *
+ * ------------------------------------------------------------------------
+ *
+ * Copyright (c) 2010, Salvatore Sanfilippo <antirez at gmail dot com>
+ * Copyright (c) 2010, Pieter Noordhuis <pcnoordhuis at gmail dot com>
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ *  *  Redistributions of source code must retain the above copyright
+ *     notice, this list of conditions and the following disclaimer.
+ *
+ *  *  Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import <ObjFW/ObjFW.h>
+
+#include <termios.h>
+#include <unistd.h>
+
+typedef void(linenoiseCompletionCallback)(OFString *, OFList*);
+
+enum linenoiseDirection {
+       LINENOISE_HISTORY_NEXT,
+       LINENOISE_HISTORY_PREV
+};
+
+@interface Linenoise : OFObject <OFApplicationDelegate>
+{
+       bool _multiline;
+
+       bool _rawmode; // For atexit() function to check if restore is needed
+       struct termios _orig_termios; // In order to restore at exit
+
+       linenoiseCompletionCallback *_completionCallback;
+
+       size_t _maximalHistoryLength;
+       ssize_t _historyIndex;  // The history index we are currently editing
+       OFMutableArray *_history;
+
+       OFFile *_term;          // Terminal file descriptor
+
+       OFMutableString *_buf;  // Edited line buffer
+       OFString *_prompt;      // Prompt to display
+
+       size_t _pos;            // Current cursor position
+       size_t _oldpos;         // Previous refresh cursor position
+
+       size_t _cols;           // Number of columns in terminal
+       size_t _maxrows;        // Maximum num of rows used so far (multiline)
+}
+@property (assign) bool multiline;
+@property (assign) size_t maximalHistoryLength;
+@property (assign) linenoiseCompletionCallback *completionCallback;
+
++ (Linenoise*)sharedLinenoise;
+
+- (OFString*)readInputWithPrompt: (OFString*)prompt;
+- (void)refreshLine;
+- (void)clearScreen;
+- (void)setCompletionCallback: (linenoiseCompletionCallback*)fn;
+- (int)addHistoryItem: (OFString*)line;
+- (void)saveHistoryToFile: (OFString*)filename;
+- (void)loadHistoryFromFile: (OFString*)filename;
+
+- (int)LN_editInsertCharacter: (int)c;
+- (void)LN_editMoveLeft;
+- (void)LN_editMoveRight;
+- (void)LN_editHistoryNextInDirection: (enum linenoiseDirection)dir;
+- (void)LN_editDelete;
+- (void)LN_editDeletePreviousWord;
+- (OFString*)LN_editWithFD: (int)fd
+                   prompt: (OFString*)prompt;
+- (OFString*)LN_editRawWithPrompt: (OFString*)prompt;
+
+- (int)LN_completeLine;
+- (void)LN_refreshSingleLine;
+- (void)LN_refreshMultiLine;
+- (bool)LN_isUnsupportedTerm;
+- (int)LN_enableRawModeForFD: (int)fd;
+- (void)LN_disableRawModeForFD: (int)fd;
+- (int)LN_getColumns;
+- (void)LN_beep;
+@end
diff --git a/src/gui/cli/linenoise.m b/src/gui/cli/linenoise.m
new file mode 100644 (file)
index 0000000..4bfd7df
--- /dev/null
@@ -0,0 +1,869 @@
+/* linenoise.m -- guerrilla line editing library against the idea that a
+ * line editing lib needs to be 20,000 lines of C code.
+ *
+ * You can find the latest source code at:
+ *
+ *      http://github.com/antirez/linenoise
+ *
+ * Does a number of crazy assumptions that happen to be true in 99.9999% of
+ * the 2010 UNIX computers around.
+ *
+ * ------------------------------------------------------------------------
+ *
+ * Copyright (c) 2010-2013, Salvatore Sanfilippo <antirez at gmail dot com>
+ * Copyright (c) 2010-2013, Pieter Noordhuis <pcnoordhuis at gmail dot com>
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *
+ *     * Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in the
+ *       documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * ------------------------------------------------------------------------
+ *
+ * References:
+ * - http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
+ * - http://www.3waylabs.com/nw/WWW/products/wizcon/vt220.html
+ *
+ * Todo list:
+ * - Filter bogus Ctrl+<char> combinations.
+ * - Win32 support
+ *
+ * Bloat:
+ * - History search like Ctrl+r in readline?
+ *
+ * List of escape sequences used by this program, we do everything just
+ * with three sequences. In order to be so cheap we may have some
+ * flickering effect with some slow terminal, but the lesser sequences
+ * the more compatible.
+ *
+ * CHA (Cursor Horizontal Absolute)
+ *     Sequence: ESC [ n G
+ *     Effect: moves cursor to column n
+ *
+ * EL (Erase Line)
+ *     Sequence: ESC [ n K
+ *     Effect: if n is 0 or missing, clear from cursor to end of line
+ *     Effect: if n is 1, clear from beginning of line to cursor
+ *     Effect: if n is 2, clear entire line
+ *
+ * CUF (CUrsor Forward)
+ *     Sequence: ESC [ n C
+ *     Effect: moves cursor forward of n chars
+ *
+ * When multi line mode is enabled, we also use an additional escape
+ * sequence. However multi line editing is disabled by default.
+ *
+ * CUU (Cursor Up)
+ *     Sequence: ESC [ n A
+ *     Effect: moves cursor up of n chars.
+ *
+ * CUD (Cursor Down)
+ *     Sequence: ESC [ n B
+ *     Effect: moves cursor down of n chars.
+ *
+ * The following are used to clear the screen: ESC [ H ESC [ 2 J
+ * This is actually composed of two sequences:
+ *
+ * cursorhome
+ *     Sequence: ESC [ H
+ *     Effect: moves the cursor to upper left corner
+ *
+ * ED2 (Clear entire screen)
+ *     Sequence: ESC [ 2 J
+ *     Effect: clear the whole screen
+ *
+ */
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <errno.h>
+#include <string.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/ioctl.h>
+
+#import "linenoise.h"
+
+#define LINENOISE_DEFAULT_HISTORY_MAX_LEN 100
+#define LINENOISE_MAX_LINE 4096
+
+static Linenoise *instance = nil;
+
+// At exit we'll try to fix the terminal to the initial conditions.
+static void linenoiseAtExit(void)
+{
+       [instance LN_disableRawModeForFD: STDIN_FILENO];
+}
+
+
+@implementation Linenoise
+@synthesize multiline = _multiline;
+@synthesize completionCallback = _completionCallback;
+
++ (Linenoise*)sharedLinenoise
+{
+       return instance;
+}
+
++ (void)initialize
+{
+       static bool initialized = false;
+       if (!initialized) {
+               initialized = true;
+               instance = [[self alloc] init];
+               atexit(linenoiseAtExit);
+       }
+}
+
+- init
+{
+       self = [super init];
+
+       _maximalHistoryLength = LINENOISE_DEFAULT_HISTORY_MAX_LEN;
+       _history =
+           [[OFMutableArray alloc] initWithCapacity: _maximalHistoryLength];
+
+       return self;
+}
+
+- (void)dealloc
+{
+       [_history release];
+
+       [super dealloc];
+}
+
+/* =========================== Line editing ================================= */
+
+
+/* Insert the character 'c' at cursor current position.
+ *
+ * On error writing to the terminal -1 is returned, otherwise 0. */
+- (int)LN_editInsertCharacter: (int)c
+{
+       static char tmp[7];
+       static int fill = 0;
+       tmp[fill++] = c;
+       tmp[fill] = '\0';
+       OFString *ins;
+
+       @try {
+               ins = @(tmp);
+       }
+       @catch (id e) {
+               return 0;
+       }
+
+       fill = 0;
+       [_buf insertString: ins
+                  atIndex: _pos++];
+       [self refreshLine];
+
+       return 0;
+}
+
+/* Move cursor on the left. */
+- (void)LN_editMoveLeft
+{
+       if (_pos > 0) {
+               _pos--;
+               [self refreshLine];
+       }
+}
+
+/* Move cursor on the right. */
+- (void)LN_editMoveRight
+{
+       if (_pos != [_buf length]) {
+               _pos++;
+               [self refreshLine];
+       }
+}
+
+/* Substitute the currently edited line with the next or previous history
+ * entry as specified by 'dir'. */
+- (void)LN_editHistoryNextInDirection: (enum linenoiseDirection)dir
+{
+       size_t count = [_history count];
+       if (count > 1) {
+               /* Update the current history entry before to
+                * overwrite it with the next one. */
+               _history[count - 1 - _historyIndex] = _buf;
+               // Show the new entry
+               _historyIndex += (dir == LINENOISE_HISTORY_PREV) ? 1 : -1;
+               if (_historyIndex < 0) {
+                       _historyIndex = 0;
+                       return;
+               } else if (_historyIndex >= count) {
+                       _historyIndex = count - 1;
+                       return;
+               }
+               [_buf release];
+               _buf = [_history[count - 1 - _historyIndex] mutableCopy];
+               _pos = [_buf length];
+               [self refreshLine];
+       }
+}
+
+/* Delete the character at the right of the cursor without altering the cursor
+ * position. Basically this is what happens with the "Delete" keyboard key. */
+- (void)LN_editDelete
+{
+       size_t len = [_buf length];
+       if (len > 0 && _pos < len) {
+               [_buf deleteCharactersInRange: of_range(_pos, 1)];
+               [self refreshLine];
+       }
+}
+
+/* Backspace implementation. */
+- (void)LN_editBackspace
+{
+       size_t len = [_buf length];
+       if (_pos > 0 && len > 0) {
+               _pos--;
+               [_buf deleteCharactersInRange: of_range(_pos, 1)];
+               [self refreshLine];
+       }
+}
+
+/* Delete the previosu word, maintaining the cursor at the start of the
+ * current word. */
+- (void)LN_editDeletePreviousWord
+{
+       size_t old_pos = _pos;
+
+       while (_pos > 0 && [_buf characterAtIndex: _pos - 1] == ' ')
+               _pos--;
+       while (_pos > 0 && [_buf characterAtIndex: _pos - 1] != ' ')
+               _pos--;
+
+       [_buf deleteCharactersInRange: of_range(_pos, old_pos - _pos)];
+       [self refreshLine];
+}
+
+/* This function is the core of the line editing capability of linenoise.
+ * It expects 'fd' to be already in "raw mode" so that every key pressed
+ * will be returned ASAP to read().
+ *
+ * The resulting string is put into 'buf' when the user type enter, or
+ * when ctrl+d is typed.
+ *
+ * The function returns the length of the current buffer. */
+- (OFString*)LN_editWithFD: (int)fd
+                   prompt: (OFString*)prompt
+{
+       /* Populate the linenoise state that we pass to functions implementing
+        * specific editing functionalities. */
+       _term = [OFFile fileWithFileDescriptor: fd];
+       _buf = [@"" mutableCopy];
+       _prompt = prompt;
+       _oldpos = _pos = 0;
+       _cols = [self LN_getColumns];
+       _maxrows = 0;
+       _historyIndex = 0;
+
+       /* The latest history entry is always our current buffer, that
+        * initially is just an empty string. */
+       [self addHistoryItem: @""];
+
+       [_term writeString: prompt];
+
+       while (1) {
+               char c;
+               size_t nread;
+               char seq[2], seq2[2];
+
+               nread = [_term readIntoBuffer: &c
+                                     length: 1];
+               if (nread == 0) {
+                       return [_buf autorelease];
+               }
+
+               /* Only autocomplete when the callback is set.
+                * It returns < 0 when there was an error reading from fd.
+                * Otherwise it will return the character that should be
+                * handled next. */
+               if (c == 9 && _completionCallback != NULL) {
+                       c = [self LN_completeLine];
+                       // Return on errors
+                       if (c < 0) {
+                               return [_buf autorelease];
+                       }
+                       // Read next character when 0
+                       if (c == 0)
+                               continue;
+               }
+
+               switch (c) {
+               case 13: // enter
+                       [_history removeLastObject];
+                       return [_buf autorelease];
+               case 3: // ctrl-c
+                       errno = EAGAIN;
+                       return nil;
+               case 8:   // ctrl-h
+               case 127: // backspace
+                       [self LN_editBackspace];
+                       break;
+               case 4: // ctrl-d
+                       /* remove char at right of cursor, or if the
+                        * line is empty, act as end-of-file. */
+                       if ([_buf length] > 0) {
+                               [self LN_editDelete];
+                       } else {
+                               [_history removeLastObject];
+                               return nil;
+                       }
+                       break;
+               case 20: { // ctrl-t, swaps current character with previous.
+                       size_t pos = _pos;
+                       if (pos <= 0 || pos >= [_buf length])
+                               break;
+
+                       OFMutableString *reverse =
+                           [[_buf substringWithRange: of_range(pos - 1, 2)]
+                           mutableCopy];
+                       [reverse reverse];
+
+                       [_buf replaceCharactersInRange: of_range(pos - 1, 2)
+                                           withString: reverse];
+
+                       if (pos != [_buf length])
+                               _pos++;
+                       [self refreshLine];
+                       break;
+                }
+               case 2: // ctrl-b
+                       [self LN_editMoveLeft];
+                       break;
+               case 6: // ctrl-f
+                       [self LN_editMoveRight];
+                       break;
+               case 16: // ctrl-p
+                       [self LN_editHistoryNextInDirection:
+                           LINENOISE_HISTORY_PREV];
+                       break;
+               case 14: // ctrl-n
+                       [self LN_editHistoryNextInDirection:
+                           LINENOISE_HISTORY_NEXT];
+                       break;
+               case 27: // escape sequence
+                       /* Read the next two bytes representing the
+                        * escape sequence. */
+                       if ([_term readIntoBuffer: seq
+                                         length: 2] != 2)
+                               break;
+
+                       if (seq[0] == 91 && seq[1] == 68) {
+                               /* Left arrow */
+                               [self LN_editMoveLeft];
+                       } else if (seq[0] == 91 && seq[1] == 67) {
+                               /* Right arrow */
+                               [self LN_editMoveRight];
+                       } else if (seq[0] == 91 &&
+                           (seq[1] == 65 || seq[1] == 66)) {
+                               /* Up and Down arrows */
+                               [self LN_editHistoryNextInDirection:
+                                   (seq[1] == 65) ? LINENOISE_HISTORY_PREV
+                                                  : LINENOISE_HISTORY_NEXT];
+                       } else if (seq[0] == 91 && seq[1] > 48 && seq[1] < 55) {
+                               // extended escape, read additional two bytes.
+                               if ([_term readIntoBuffer: seq2
+                                                  length: 2] < 1)
+                                       break;
+                               if (seq[1] == 51 && seq2[0] == 126) {
+                                       /* Delete key. */
+                                       [self LN_editDelete];
+                               }
+                       }
+                       break;
+               default:
+                       if ([self LN_editInsertCharacter: c])
+                               return nil;
+                       break;
+               case 21: // Ctrl+u, delete the whole line.
+                       _buf = [@"" mutableCopy];
+                       _pos = 0;
+                       [self refreshLine];
+                       break;
+               case 11: { // Ctrl+k, delete from current to end of line.
+                       size_t pos = _pos;
+                       size_t diff = [_buf length] - pos;
+                       [_buf deleteCharactersInRange:
+                           of_range(pos, diff)];
+                       [self refreshLine];
+                       break;
+               }
+               case 1: // Ctrl+a, go to the start of the line
+                       _pos = 0;
+                       [self refreshLine];
+                       break;
+               case 5: // ctrl+e, go to the end of the line
+                       _pos = [_buf length];
+                       [self refreshLine];
+                       break;
+               case 12: // ctrl+l, clear screen
+                       [self clearScreen];
+                       [self refreshLine];
+                       break;
+               case 23: // ctrl+w, delete previous word
+                       [self LN_editDeletePreviousWord];
+                       break;
+               }
+       }
+       return [_buf autorelease];
+}
+
+/* This function calls the line editing function linenoiseEdit() using
+ * the STDIN file descriptor set in raw mode. */
+- (OFString*)LN_editRawWithPrompt: (OFString*)prompt
+{
+       OFString *ret;
+       int fd = [of_stdin fileDescriptorForReading];
+
+       if (!isatty(fd))
+               return [of_stdin readLine];
+
+       if ([self LN_enableRawModeForFD: fd] == -1)
+               return nil;
+       ret = [self LN_editWithFD: fd
+                          prompt: prompt];
+       [self LN_disableRawModeForFD: fd];
+       [of_stdout writeString: @"\n"];
+
+       return ret;
+}
+
+/* The high level function that is the main API of the linenoise library.
+ * This function checks if the terminal has basic capabilities, just checking
+ * for a blacklist of stupid terminals, and later either calls the line
+ * editing function or uses dummy fgets() so that you will be able to type
+ * something even in the most desperate of the conditions. */
+- (OFString*)readInputWithPrompt: (OFString*)prompt
+{
+       if ([self LN_isUnsupportedTerm]) {
+               OFString *ret;
+
+               _prompt = [prompt retain];
+
+               [of_stdout writeString: prompt];
+               [of_stdout flushWriteBuffer];
+
+               ret = [of_stdin readLine];
+
+               [_prompt release];
+               _prompt = nil;
+
+               return ret;
+       } else
+               return [self LN_editRawWithPrompt: prompt];
+}
+
+
+/* ============================== Completion ================================ */
+
+/* This is an helper function for linenoiseEdit() and is called when the
+ * user types the <tab> key in order to complete the string currently in the
+ * input.
+ *
+ * The state of the editing is encapsulated into the pointed linenoiseState
+ * structure as described in the structure definition. */
+- (int)LN_completeLine
+{
+       OFList *lc = [OFList new];
+       int nread;
+       char c = 0;
+
+       _completionCallback(_buf, lc);
+       if ([lc count] == 0) {
+               [self LN_beep];
+       } else {
+               bool stop = false;
+               size_t i = 0;
+               of_list_object_t *completion = [lc firstListObject];
+               size_t count = [lc count];
+
+               while (!stop) {
+                       // Show completion or original buffer
+                       if (i < count) {
+                               size_t saved_pos = _pos;
+                               OFMutableString *saved_buf = _buf;
+
+                               _buf = [completion->object mutableCopy];
+                               _pos = [_buf length];
+                               [self refreshLine];
+                               _pos = saved_pos;
+                               _buf = saved_buf;
+                       } else {
+                               [self refreshLine];
+                       }
+
+                       nread = [_term readIntoBuffer: &c
+                                              length: 1];
+                       if (nread == 0) {
+                               [lc release];
+                               return -1;
+                       }
+
+                       switch (c) {
+                       case 9: // tab
+                               i = (i + 1) % (count + 1);
+                               if (i == count) {
+                                       [self LN_beep];
+                                       break;
+                               }
+
+                               completion = completion->next;
+                               if (completion == NULL)
+                                       completion = [lc firstListObject];
+                               break;
+                       case 27: // escape
+                               // Re-show original buffer
+                               if (i < count)
+                                       [self refreshLine];
+                               stop = true;
+                               break;
+                       default:
+                               // Update buffer and return
+                               if (i < count) {
+                                       [_buf release];
+                                       _buf =
+                                           [completion->object mutableCopy];
+                                       _pos =
+                                           [_buf length];
+                               }
+                               stop = true;
+                               break;
+                       }
+               }
+       }
+
+       [lc release];
+       return c; // Return last read character
+}
+
+/* ================================ History ================================= */
+
+// Using a circular buffer is smarter, but a bit more complex to handle.
+- (int)addHistoryItem: (OFString*)line
+{
+       if (_maximalHistoryLength == 0)
+               return 0;
+
+       size_t len = [_history count];
+
+       if (len == _maximalHistoryLength) {
+               for (size_t i = 0; i < len - 1; i++)
+                       _history[i] = _history[i+1];
+               [_history removeLastObject];
+       }
+
+       [_history addObject: line];
+       return 1;
+}
+
+/* Set the maximum length for the history. This function can be called even
+ * if there is already some history, the function will make sure to retain
+ * just the latest 'len' elements if the new history length value is smaller
+ * than the amount of items already inside the history. */
+- (void)setMaximalHistoryLength: (size_t)len
+{
+       if (len < 1)
+               @throw [OFInvalidArgumentException
+                   exceptionWithClass: [self class]
+                             selector: _cmd];
+
+       OFMutableArray *old = _history, *new;
+       int tocopy = len < [old count] ? len : [old count];
+
+       new = [[OFMutableArray alloc] initWithCapacity: len];
+
+       for (int i = 0; i < tocopy; i++)
+               [new addObject: old[i]];
+
+       _history = new;
+       [old release];
+
+       _maximalHistoryLength = len;
+}
+
+- (size_t)maximalHistoryLength
+{
+       return _maximalHistoryLength;
+}
+
+/* Save the history in the specified file. On success 0 is returned
+ * otherwise -1 is returned. */
+- (void)saveHistoryToFile: (OFString*)filename
+{
+       OFFile *file = [OFFile fileWithPath: filename
+                                      mode: @"w"];
+       for (int j = 0; j < [_history count]; j++)
+               [file writeLine: _history[j]];
+       [file close];
+}
+
+/* Load the history from the specified file. If the file does not exist
+ * zero is returned and no operation is performed.
+ *
+ * If the file exists and the operation succeeded 0 is returned, otherwise
+ * on error -1 is returned. */
+- (void)loadHistoryFromFile: (OFString*)filename
+{
+       OFFile *file = [OFFile fileWithPath: filename
+                                      mode: @"r"];
+       OFString *line;
+       while ((line = [file readLine]) != nil)
+               [self addHistoryItem: line];
+       [file close];
+}
+
+
+/* =========================== Line editing ================================= */
+
+// Clear the screen. Used to handle ctrl+l
+- (void)clearScreen
+{
+       [of_stdout writeString: @"\x1b[H\x1b[2J"];
+}
+
+/* Calls the two low level functions LN_refreshSingleLine or
+ * LN_refreshMultiLine according to the selected mode. */
+- (void)refreshLine
+{
+       if (_buf == nil)
+               return;
+
+       if (_multiline)
+               [self LN_refreshMultiLine];
+       else
+               [self LN_refreshSingleLine];
+}
+
+/* Single line low level line refresh.
+ *
+ * Rewrite the currently edited line accordingly to the buffer content,
+ * cursor position, and number of columns of the terminal. */
+- (void)LN_refreshSingleLine;
+{
+       size_t plen = [_prompt length];
+       size_t pos = _pos;
+       OFString *buf = _buf;
+
+       if ((plen + pos) >= _cols) {
+               size_t offset = (plen + pos) - _cols;
+               size_t buflen = [buf length] - offset;
+               buf = [buf substringWithRange: of_range(offset, buflen)];
+               pos -= offset;
+       }
+
+       // Cursor to left edge
+       [_term writeString: @"\x1b[0G"];
+       // Write the prompt and the current buffer content
+       [_term writeString: _prompt];
+       [_term writeString: buf];
+       // Erase to right
+       [_term writeString: @"\x1b[0K"];
+       // Move cursor to original position
+       [_term writeFormat: @"\x1b[0G\x1b[%zdC", pos + plen];
+}
+
+/* Multi line low level line refresh.
+ *
+ * Rewrite the currently edited line accordingly to the buffer content,
+ * cursor position, and number of columns of the terminal. */
+- (void)LN_refreshMultiLine
+{
+       size_t plen = [_prompt length];
+
+       // rows used by current buf.
+       size_t rows = (plen + [_buf length] + _cols - 1) / _cols;
+       // cursor relative row.
+       size_t rpos = (plen + _oldpos + _cols) / _cols;
+       // rpos after refresh.
+       size_t rpos2;
+       ssize_t old_rows = _maxrows, j;
+
+       // Update maxrows if needed.
+       if (rows > _maxrows)
+               _maxrows = rows;
+
+#ifdef LN_DEBUG
+       OFFile *file = [OFFile fileWithPath: @"/tmp/debug.txt"
+                                      mode: @"a"];
+       [file writeFormat: @"[%zd %zd %zd] p: %zd, rows: %zd, rpos: %zd, "
+           @"max: %zd, oldmax: %zd", [_buf length], _pos, _oldpos, plen, rows,
+           rpos, _maxrows, old_rows];
+#endif
+
+       /* First step: clear all the lines used before. To do so start by
+        * going to the last row. */
+       if (old_rows > rpos) {
+#ifdef LN_DEBUG
+               [file writeFormat: @", go down %zd", old_rows - rpos];
+#endif
+               [_term writeFormat: @"\x1b[%zdB", old_rows - rpos];
+       }
+
+       // Now for every row clear it, go up.
+       for (j = 0; j < old_rows - 1; j++) {
+#ifdef LN_DEBUG
+               [file writeString: @", clear+up"];
+#endif
+               [_term writeString: @"\x1b[0G\x1b[0K\x1b[1A"];
+       }
+
+       // Clean the top line.
+#ifdef LN_DEBUG
+       [file writeString: @", clear"];
+#endif
+       [_term writeString: @"\x1b[0G\x1b[0K"];
+
+       // Write the prompt and the current buffer content
+       [_term writeString: _prompt];
+       [_term writeString: _buf];
+
+       /* If we are at the very end of the screen with our prompt, we need to
+        * emit a newline and move the prompt to the first column. */
+       if (_pos && _pos == [_buf length] && (_pos + plen) % _cols == 0) {
+#ifdef LN_DEBUG
+               [file writeString: @", <newline>"];
+#endif
+               [_term writeString: @"\n\x1b[0G"];
+               rows++;
+               if (rows > _maxrows)
+                       _maxrows = rows;
+       }
+
+       // Move cursor to right position.
+       rpos2 = (plen + _pos + _cols) / _cols;
+       // current cursor relative row.
+#ifdef LN_DEBUG
+       [file writeFormat: @", rpos2 %zd", rpos2];
+#endif
+       // Go up till we reach the expected positon.
+       if (rows - rpos2 > 0) {
+#ifdef LN_DEBUG
+               [file writeFormat: @", go-up %zd", rows - rpos2];
+#endif
+               [_term writeFormat: @"\x1b[%zdA", rows - rpos2];
+       }
+       /* Set column. */
+#ifdef LN_DEBUG
+       [file writeFormat: @", set col %zd", 1 + ((plen + _pos) % _cols)];
+#endif
+       [_term writeFormat: @"\x1b[%zdG", 1 + ((plen + _pos) % _cols)];
+
+       _oldpos = _pos;
+
+#ifdef LN_DEBUG
+       [file writeString: @"\n"];
+       [file close];
+#endif
+}
+
+/* ======================= Low level terminal handling ====================== */
+
+/* Return true if the terminal name is in the list of terminals we know are
+ * not able to understand basic escape sequences. */
+
+- (bool)LN_isUnsupportedTerm
+{
+       static char *unsupported_term[] = { "dumb", "cons25", NULL };
+       char *term = getenv("TERM");
+       int j;
+
+       if (term == NULL)
+               return false;
+       for (j = 0; unsupported_term[j]; j++)
+               if (!strcasecmp(term, unsupported_term[j]))
+                       return true;
+       return false;
+}
+
+/* Raw mode: 1960 magic shit. */
+- (int)LN_enableRawModeForFD: (int)fd
+{
+       struct termios raw;
+
+       if (!isatty(STDIN_FILENO))
+               goto fatal;
+       if (tcgetattr(fd, &_orig_termios) == -1)
+               goto fatal;
+
+       // modify the original mode
+       raw = _orig_termios;
+
+       /* input modes: no break, no CR to NL, no parity check, no strip char,
+        * no start/stop output control. */
+       raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
+       // output modes - disable post processing
+       raw.c_oflag &= ~(OPOST);
+       // control modes - set 8 bit chars
+       raw.c_cflag |= (CS8);
+       /* local modes - choing off, canonical off, no extended functions,
+        * no signal chars (^Z, ^C) */
+       raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
+       /* control chars - set return condition: min number of bytes and timer.
+        * We want read to return every single byte, without timeout. */
+       raw.c_cc[VMIN] = 1;
+       // 1 byte, no timer
+       raw.c_cc[VTIME] = 0;
+
+       // put terminal in raw mode after flushing
+       if (tcsetattr(fd, TCSAFLUSH, &raw) < 0)
+               goto fatal;
+       _rawmode = true;
+       return 0;
+
+fatal:
+       errno = ENOTTY;
+       return -1;
+}
+
+- (void)LN_disableRawModeForFD: (int)fd
+{
+       if (_rawmode && tcsetattr(fd, TCSAFLUSH, &_orig_termios) != -1)
+               _rawmode = false;
+}
+
+/* Try to get the number of columns in the current terminal, or assume 80
+ * if it fails. */
+- (int)LN_getColumns
+{
+       struct winsize ws;
+
+       if (ioctl(1, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0)
+               return 80;
+       return ws.ws_col;
+}
+
+/* Beep, used for completion when there is nothing to complete or when all
+ * the choices were already shown. */
+- (void)LN_beep
+{
+       fprintf(stderr, "\x7");
+       fflush(stderr);
+}
+@end