From: Florian Zeitz Date: Wed, 17 Apr 2013 18:51:31 +0000 (+0200) Subject: Add line editing support to the CLI UI X-Git-Url: https://git.babelmonkeys.de/?a=commitdiff_plain;h=74bc901b59ed844b3308224b7acdb78a9446d27a;p=jubjub.git Add line editing support to the CLI UI --- diff --git a/src/gui/cli/JubCLIChatUI.h b/src/gui/cli/JubCLIChatUI.h index c0f8874..7ebe0b1 100644 --- a/src/gui/cli/JubCLIChatUI.h +++ b/src/gui/cli/JubCLIChatUI.h @@ -5,6 +5,7 @@ @interface JubCLIChatUI: OFObject { jub_send_block_t _sendBlock; + OFString *_title; } - (void)send: (OFString*)text; diff --git a/src/gui/cli/JubCLIChatUI.m b/src/gui/cli/JubCLIChatUI.m index 5803ba7..19e3629 100644 --- a/src/gui/cli/JubCLIChatUI.m +++ b/src/gui/cli/JubCLIChatUI.m @@ -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; @@ -21,17 +24,23 @@ - (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 diff --git a/src/gui/cli/JubCLIColor.h b/src/gui/cli/JubCLIColor.h index 1471969..acca2d9 100644 --- a/src/gui/cli/JubCLIColor.h +++ b/src/gui/cli/JubCLIColor.h @@ -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" diff --git a/src/gui/cli/JubCLIUI.h b/src/gui/cli/JubCLIUI.h index b610d0a..222fc27 100644 --- a/src/gui/cli/JubCLIUI.h +++ b/src/gui/cli/JubCLIUI.h @@ -2,6 +2,7 @@ #import #import "JubUI.h" +#import "linenoise.h" @class JubCLIChatUI; @class JubChatClient; diff --git a/src/gui/cli/JubCLIUI.m b/src/gui/cli/JubCLIUI.m index 6f0b03a..3cf59c2 100644 --- a/src/gui/cli/JubCLIUI.m +++ b/src/gui/cli/JubCLIUI.m @@ -4,6 +4,8 @@ #import "JubCLICommand.h" #import "JubCLIUI.h" +#include + 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 diff --git a/src/gui/cli/Makefile b/src/gui/cli/Makefile index 807a20a..2fd5a45 100644 --- a/src/gui/cli/Makefile +++ b/src/gui/cli/Makefile @@ -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 index 0000000..188eece --- /dev/null +++ b/src/gui/cli/linenoise.h @@ -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 + * Copyright (c) 2010, Pieter Noordhuis + * + * 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 + +#include +#include + +typedef void(linenoiseCompletionCallback)(OFString *, OFList*); + +enum linenoiseDirection { + LINENOISE_HISTORY_NEXT, + LINENOISE_HISTORY_PREV +}; + +@interface Linenoise : OFObject +{ + 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 index 0000000..4bfd7df --- /dev/null +++ b/src/gui/cli/linenoise.m @@ -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 + * Copyright (c) 2010-2013, Pieter Noordhuis + * + * 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+ 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 +#include +#include +#include +#include +#include +#include + +#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 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: @", "]; +#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