packages feed

linenoise (empty) → 0.3.0

raw patch · 11 files changed

+1719/−0 lines, 11 filesdep +basedep +bytestringdep +exceptionssetup-changed

Dependencies added: base, bytestring, exceptions, linenoise, mtl, text, unliftio-core

Files

+ LICENSE view
@@ -0,0 +1,29 @@+Copyright (c) 2010-2013, Salvatore Sanfilippo <antirez at gmail dot com>+Copyright (c) 2010-2013, Pieter Noordhuis <pcnoordhuis at gmail dot com>+Copyright (c) 2014-2017, Stephen Diehl <stephen.m.diehl at gmail.com>+Copyright (c) 2019, Eric Conlon <ejconlon at gmail.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.
+ README.md view
@@ -0,0 +1,44 @@+# linenoise++[![CircleCI](https://circleci.com/gh/ejconlon/haskell-linenoise/tree/master.svg?style=svg)](https://circleci.com/gh/ejconlon/haskell-linenoise/tree/master)++A lightweight readline-replacement library for Haskell based on the `linenoise` library. (Not production ready!)+See the [demo app](https://github.com/ejconlon/haskell-linenoise/blob/master/app/Main.hs) for usage, or see the [climb](https://github.com/ejconlon/climb) library for higher-level building blocks for GHCi-like REPLs with colon-commands.++## Differences from alternatives++* `haskeline`+  * This uses FFI to a minimal C library vs pure Haskell+  * This uses standard MTL/Unlift typeclasses vs custom ones.+  * This does not require the use of a specific monad transformer.+* `readline`+  * This offers a `MonadIO/MonadUnliftIO` interface vs raw `IO`.+  * This vendors and statically links its underlying library to simplify the build process.++## License and attribution++This library includes the source code and license for `linenoise` in `cbits`.+It is a fork of an older [library](https://github.com/sdiehl/haskell-linenoise) with all licensing and+attribution preserved.++## Development workflow++This project uses `stack` to build.++The `Makefile` has a bunch of relevant phony targets for a development workflow including++* `build` - Build the library and demo+* `download` - Update vendored `linenoise`+* `demo` - Run the demo app+* `deps` - Install dev tools+* `lint` - Lint with `hlint`+* `format` - Format with `stylish-haskell`+* `docs` - Generate docs++In addition, there are targets starting with `ci` that are invoked in various CI phases.++## TODO++* Support unicode (may involve vendoring a `linenoise` [fork](https://github.com/yhirose/linenoise/tree/utf8-support))+* Verify that the FFI modifications for `ByteString` are memory-safe+* Upload to Hackage
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ app/Main.hs view
@@ -0,0 +1,33 @@+{-# LANGUAGE OverloadedStrings #-}++module Main (main) where++import Control.Monad (void)+import Control.Monad.IO.Class (liftIO)+import Control.Monad.State.Strict (get, modify)+import Data.Text (Text)+import qualified Data.Text as Text+import qualified Data.Text.IO as TIO+import Linenoise++type History = [Text]++type Repl a = ReplT () History IO a++runRepl :: Repl a -> History -> IO (a, History)+runRepl n = runReplT n ()++completer :: Text -> Repl [Text]+completer line = filter (Text.isPrefixOf line) <$> get++action :: Text -> Repl ReplDirective+action x = do+  modify (x:)+  liftIO (TIO.putStrLn x)+  pure ReplContinue++repl :: Repl ()+repl = replM ReplContinue ">>> " action (byWord completer)++main :: IO ()+main = void (runRepl repl [])
+ cbits/linenoise.c view
@@ -0,0 +1,1201 @@+/* linenoise.c -- 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-2016, 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.+ *+ * 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 n chars+ *+ * CUB (CUrsor Backward)+ *    Sequence: ESC [ n D+ *    Effect: moves cursor backward n chars+ *+ * The following is used to get the terminal width if getting+ * the width with the TIOCGWINSZ ioctl fails+ *+ * DSR (Device Status Report)+ *    Sequence: ESC [ 6 n+ *    Effect: reports the current cusor position as ESC [ n ; m R+ *            where n is the row and m is the column+ *+ * 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.+ *+ * When linenoiseClearScreen() is called, two additional escape sequences+ * are used in order to clear the screen and position the cursor at home+ * position.+ *+ * CUP (Cursor position)+ *    Sequence: ESC [ H+ *    Effect: moves the cursor to upper left corner+ *+ * ED (Erase display)+ *    Sequence: ESC [ 2 J+ *    Effect: clear the whole screen+ *+ */++#include <termios.h>+#include <unistd.h>+#include <stdlib.h>+#include <stdio.h>+#include <errno.h>+#include <string.h>+#include <stdlib.h>+#include <ctype.h>+#include <sys/stat.h>+#include <sys/types.h>+#include <sys/ioctl.h>+#include <unistd.h>+#include "linenoise.h"++#define LINENOISE_DEFAULT_HISTORY_MAX_LEN 100+#define LINENOISE_MAX_LINE 4096+static char *unsupported_term[] = {"dumb","cons25","emacs",NULL};+static linenoiseCompletionCallback *completionCallback = NULL;+static linenoiseHintsCallback *hintsCallback = NULL;+static linenoiseFreeHintsCallback *freeHintsCallback = NULL;++static struct termios orig_termios; /* In order to restore at exit.*/+static int rawmode = 0; /* For atexit() function to check if restore is needed*/+static int mlmode = 0;  /* Multi line mode. Default is single line. */+static int atexit_registered = 0; /* Register atexit just 1 time. */+static int history_max_len = LINENOISE_DEFAULT_HISTORY_MAX_LEN;+static int history_len = 0;+static char **history = NULL;++/* The linenoiseState structure represents the state during line editing.+ * We pass this state to functions implementing specific editing+ * functionalities. */+struct linenoiseState {+    int ifd;            /* Terminal stdin file descriptor. */+    int ofd;            /* Terminal stdout file descriptor. */+    char *buf;          /* Edited line buffer. */+    size_t buflen;      /* Edited line buffer size. */+    const char *prompt; /* Prompt to display. */+    size_t plen;        /* Prompt length. */+    size_t pos;         /* Current cursor position. */+    size_t oldpos;      /* Previous refresh cursor position. */+    size_t len;         /* Current edited line length. */+    size_t cols;        /* Number of columns in terminal. */+    size_t maxrows;     /* Maximum num of rows used so far (multiline mode) */+    int history_index;  /* The history index we are currently editing. */+};++enum KEY_ACTION{+	KEY_NULL = 0,	    /* NULL */+	CTRL_A = 1,         /* Ctrl+a */+	CTRL_B = 2,         /* Ctrl-b */+	CTRL_C = 3,         /* Ctrl-c */+	CTRL_D = 4,         /* Ctrl-d */+	CTRL_E = 5,         /* Ctrl-e */+	CTRL_F = 6,         /* Ctrl-f */+	CTRL_H = 8,         /* Ctrl-h */+	TAB = 9,            /* Tab */+	CTRL_K = 11,        /* Ctrl+k */+	CTRL_L = 12,        /* Ctrl+l */+	ENTER = 13,         /* Enter */+	CTRL_N = 14,        /* Ctrl-n */+	CTRL_P = 16,        /* Ctrl-p */+	CTRL_T = 20,        /* Ctrl-t */+	CTRL_U = 21,        /* Ctrl+u */+	CTRL_W = 23,        /* Ctrl+w */+	ESC = 27,           /* Escape */+	BACKSPACE =  127    /* Backspace */+};++static void linenoiseAtExit(void);+int linenoiseHistoryAdd(const char *line);+static void refreshLine(struct linenoiseState *l);++/* Debugging macro. */+#if 0+FILE *lndebug_fp = NULL;+#define lndebug(...) \+    do { \+        if (lndebug_fp == NULL) { \+            lndebug_fp = fopen("/tmp/lndebug.txt","a"); \+            fprintf(lndebug_fp, \+            "[%d %d %d] p: %d, rows: %d, rpos: %d, max: %d, oldmax: %d\n", \+            (int)l->len,(int)l->pos,(int)l->oldpos,plen,rows,rpos, \+            (int)l->maxrows,old_rows); \+        } \+        fprintf(lndebug_fp, ", " __VA_ARGS__); \+        fflush(lndebug_fp); \+    } while (0)+#else+#define lndebug(fmt, ...)+#endif++/* ======================= Low level terminal handling ====================== */++/* Set if to use or not the multi line mode. */+void linenoiseSetMultiLine(int ml) {+    mlmode = ml;+}++/* Return true if the terminal name is in the list of terminals we know are+ * not able to understand basic escape sequences. */+static int isUnsupportedTerm(void) {+    char *term = getenv("TERM");+    int j;++    if (term == NULL) return 0;+    for (j = 0; unsupported_term[j]; j++)+        if (!strcasecmp(term,unsupported_term[j])) return 1;+    return 0;+}++/* Raw mode: 1960 magic shit. */+static int enableRawMode(int fd) {+    struct termios raw;++    if (!isatty(STDIN_FILENO)) goto fatal;+    if (!atexit_registered) {+        atexit(linenoiseAtExit);+        atexit_registered = 1;+    }+    if (tcgetattr(fd,&orig_termios) == -1) goto fatal;++    raw = orig_termios;  /* modify the original mode */+    /* 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; raw.c_cc[VTIME] = 0; /* 1 byte, no timer */++    /* put terminal in raw mode after flushing */+    if (tcsetattr(fd,TCSAFLUSH,&raw) < 0) goto fatal;+    rawmode = 1;+    return 0;++fatal:+    errno = ENOTTY;+    return -1;+}++static void disableRawMode(int fd) {+    /* Don't even check the return value as it's too late. */+    if (rawmode && tcsetattr(fd,TCSAFLUSH,&orig_termios) != -1)+        rawmode = 0;+}++/* Use the ESC [6n escape sequence to query the horizontal cursor position+ * and return it. On error -1 is returned, on success the position of the+ * cursor. */+static int getCursorPosition(int ifd, int ofd) {+    char buf[32];+    int cols, rows;+    unsigned int i = 0;++    /* Report cursor location */+    if (write(ofd, "\x1b[6n", 4) != 4) return -1;++    /* Read the response: ESC [ rows ; cols R */+    while (i < sizeof(buf)-1) {+        if (read(ifd,buf+i,1) != 1) break;+        if (buf[i] == 'R') break;+        i++;+    }+    buf[i] = '\0';++    /* Parse it. */+    if (buf[0] != ESC || buf[1] != '[') return -1;+    if (sscanf(buf+2,"%d;%d",&rows,&cols) != 2) return -1;+    return cols;+}++/* Try to get the number of columns in the current terminal, or assume 80+ * if it fails. */+static int getColumns(int ifd, int ofd) {+    struct winsize ws;++    if (ioctl(1, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) {+        /* ioctl() failed. Try to query the terminal itself. */+        int start, cols;++        /* Get the initial position so we can restore it later. */+        start = getCursorPosition(ifd,ofd);+        if (start == -1) goto failed;++        /* Go to right margin and get position. */+        if (write(ofd,"\x1b[999C",6) != 6) goto failed;+        cols = getCursorPosition(ifd,ofd);+        if (cols == -1) goto failed;++        /* Restore position. */+        if (cols > start) {+            char seq[32];+            snprintf(seq,32,"\x1b[%dD",cols-start);+            if (write(ofd,seq,strlen(seq)) == -1) {+                /* Can't recover... */+            }+        }+        return cols;+    } else {+        return ws.ws_col;+    }++failed:+    return 80;+}++/* Clear the screen. Used to handle ctrl+l */+void linenoiseClearScreen(void) {+    if (write(STDOUT_FILENO,"\x1b[H\x1b[2J",7) <= 0) {+        /* nothing to do, just to avoid warning. */+    }+}++/* Beep, used for completion when there is nothing to complete or when all+ * the choices were already shown. */+static void linenoiseBeep(void) {+    fprintf(stderr, "\x7");+    fflush(stderr);+}++/* ============================== Completion ================================ */++/* Free a list of completion option populated by linenoiseAddCompletion(). */+static void freeCompletions(linenoiseCompletions *lc) {+    size_t i;+    for (i = 0; i < lc->len; i++)+        free(lc->cvec[i]);+    if (lc->cvec != NULL)+        free(lc->cvec);+}++/* 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. */+static int completeLine(struct linenoiseState *ls) {+    linenoiseCompletions lc = { 0, NULL };+    int nread, nwritten;+    char c = 0;++    completionCallback(ls->buf,&lc);+    if (lc.len == 0) {+        linenoiseBeep();+    } else {+        size_t stop = 0, i = 0;++        while(!stop) {+            /* Show completion or original buffer */+            if (i < lc.len) {+                struct linenoiseState saved = *ls;++                ls->len = ls->pos = strlen(lc.cvec[i]);+                ls->buf = lc.cvec[i];+                refreshLine(ls);+                ls->len = saved.len;+                ls->pos = saved.pos;+                ls->buf = saved.buf;+            } else {+                refreshLine(ls);+            }++            nread = read(ls->ifd,&c,1);+            if (nread <= 0) {+                freeCompletions(&lc);+                return -1;+            }++            switch(c) {+                case 9: /* tab */+                    i = (i+1) % (lc.len+1);+                    if (i == lc.len) linenoiseBeep();+                    break;+                case 27: /* escape */+                    /* Re-show original buffer */+                    if (i < lc.len) refreshLine(ls);+                    stop = 1;+                    break;+                default:+                    /* Update buffer and return */+                    if (i < lc.len) {+                        nwritten = snprintf(ls->buf,ls->buflen,"%s",lc.cvec[i]);+                        ls->len = ls->pos = nwritten;+                    }+                    stop = 1;+                    break;+            }+        }+    }++    freeCompletions(&lc);+    return c; /* Return last read character */+}++/* Register a callback function to be called for tab-completion. */+void linenoiseSetCompletionCallback(linenoiseCompletionCallback *fn) {+    completionCallback = fn;+}++/* Register a hits function to be called to show hits to the user at the+ * right of the prompt. */+void linenoiseSetHintsCallback(linenoiseHintsCallback *fn) {+    hintsCallback = fn;+}++/* Register a function to free the hints returned by the hints callback+ * registered with linenoiseSetHintsCallback(). */+void linenoiseSetFreeHintsCallback(linenoiseFreeHintsCallback *fn) {+    freeHintsCallback = fn;+}++/* This function is used by the callback function registered by the user+ * in order to add completion options given the input string when the+ * user typed <tab>. See the example.c source code for a very easy to+ * understand example. */+void linenoiseAddCompletion(linenoiseCompletions *lc, const char *str) {+    size_t len = strlen(str);+    char *copy, **cvec;++    copy = malloc(len+1);+    if (copy == NULL) return;+    memcpy(copy,str,len+1);+    cvec = realloc(lc->cvec,sizeof(char*)*(lc->len+1));+    if (cvec == NULL) {+        free(copy);+        return;+    }+    lc->cvec = cvec;+    lc->cvec[lc->len++] = copy;+}++/* =========================== Line editing ================================= */++/* We define a very simple "append buffer" structure, that is an heap+ * allocated string where we can append to. This is useful in order to+ * write all the escape sequences in a buffer and flush them to the standard+ * output in a single call, to avoid flickering effects. */+struct abuf {+    char *b;+    int len;+};++static void abInit(struct abuf *ab) {+    ab->b = NULL;+    ab->len = 0;+}++static void abAppend(struct abuf *ab, const char *s, int len) {+    char *new = realloc(ab->b,ab->len+len);++    if (new == NULL) return;+    memcpy(new+ab->len,s,len);+    ab->b = new;+    ab->len += len;+}++static void abFree(struct abuf *ab) {+    free(ab->b);+}++/* Helper of refreshSingleLine() and refreshMultiLine() to show hints+ * to the right of the prompt. */+void refreshShowHints(struct abuf *ab, struct linenoiseState *l, int plen) {+    char seq[64];+    if (hintsCallback && plen+l->len < l->cols) {+        int color = -1, bold = 0;+        char *hint = hintsCallback(l->buf,&color,&bold);+        if (hint) {+            int hintlen = strlen(hint);+            int hintmaxlen = l->cols-(plen+l->len);+            if (hintlen > hintmaxlen) hintlen = hintmaxlen;+            if (bold == 1 && color == -1) color = 37;+            if (color != -1 || bold != 0)+                snprintf(seq,64,"\033[%d;%d;49m",bold,color);+            else+                seq[0] = '\0';+            abAppend(ab,seq,strlen(seq));+            abAppend(ab,hint,hintlen);+            if (color != -1 || bold != 0)+                abAppend(ab,"\033[0m",4);+            /* Call the function to free the hint returned. */+            if (freeHintsCallback) freeHintsCallback(hint);+        }+    }+}++/* 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. */+static void refreshSingleLine(struct linenoiseState *l) {+    char seq[64];+    size_t plen = strlen(l->prompt);+    int fd = l->ofd;+    char *buf = l->buf;+    size_t len = l->len;+    size_t pos = l->pos;+    struct abuf ab;++    while((plen+pos) >= l->cols) {+        buf++;+        len--;+        pos--;+    }+    while (plen+len > l->cols) {+        len--;+    }++    abInit(&ab);+    /* Cursor to left edge */+    snprintf(seq,64,"\r");+    abAppend(&ab,seq,strlen(seq));+    /* Write the prompt and the current buffer content */+    abAppend(&ab,l->prompt,strlen(l->prompt));+    abAppend(&ab,buf,len);+    /* Show hits if any. */+    refreshShowHints(&ab,l,plen);+    /* Erase to right */+    snprintf(seq,64,"\x1b[0K");+    abAppend(&ab,seq,strlen(seq));+    /* Move cursor to original position. */+    snprintf(seq,64,"\r\x1b[%dC", (int)(pos+plen));+    abAppend(&ab,seq,strlen(seq));+    if (write(fd,ab.b,ab.len) == -1) {} /* Can't recover from write error. */+    abFree(&ab);+}++/* 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. */+static void refreshMultiLine(struct linenoiseState *l) {+    char seq[64];+    int plen = strlen(l->prompt);+    int rows = (plen+l->len+l->cols-1)/l->cols; /* rows used by current buf. */+    int rpos = (plen+l->oldpos+l->cols)/l->cols; /* cursor relative row. */+    int rpos2; /* rpos after refresh. */+    int col; /* colum position, zero-based. */+    int old_rows = l->maxrows;+    int fd = l->ofd, j;+    struct abuf ab;++    /* Update maxrows if needed. */+    if (rows > (int)l->maxrows) l->maxrows = rows;++    /* First step: clear all the lines used before. To do so start by+     * going to the last row. */+    abInit(&ab);+    if (old_rows-rpos > 0) {+        lndebug("go down %d", old_rows-rpos);+        snprintf(seq,64,"\x1b[%dB", old_rows-rpos);+        abAppend(&ab,seq,strlen(seq));+    }++    /* Now for every row clear it, go up. */+    for (j = 0; j < old_rows-1; j++) {+        lndebug("clear+up");+        snprintf(seq,64,"\r\x1b[0K\x1b[1A");+        abAppend(&ab,seq,strlen(seq));+    }++    /* Clean the top line. */+    lndebug("clear");+    snprintf(seq,64,"\r\x1b[0K");+    abAppend(&ab,seq,strlen(seq));++    /* Write the prompt and the current buffer content */+    abAppend(&ab,l->prompt,strlen(l->prompt));+    abAppend(&ab,l->buf,l->len);++    /* Show hits if any. */+    refreshShowHints(&ab,l,plen);++    /* 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 (l->pos &&+        l->pos == l->len &&+        (l->pos+plen) % l->cols == 0)+    {+        lndebug("<newline>");+        abAppend(&ab,"\n",1);+        snprintf(seq,64,"\r");+        abAppend(&ab,seq,strlen(seq));+        rows++;+        if (rows > (int)l->maxrows) l->maxrows = rows;+    }++    /* Move cursor to right position. */+    rpos2 = (plen+l->pos+l->cols)/l->cols; /* current cursor relative row. */+    lndebug("rpos2 %d", rpos2);++    /* Go up till we reach the expected positon. */+    if (rows-rpos2 > 0) {+        lndebug("go-up %d", rows-rpos2);+        snprintf(seq,64,"\x1b[%dA", rows-rpos2);+        abAppend(&ab,seq,strlen(seq));+    }++    /* Set column. */+    col = (plen+(int)l->pos) % (int)l->cols;+    lndebug("set col %d", 1+col);+    if (col)+        snprintf(seq,64,"\r\x1b[%dC", col);+    else+        snprintf(seq,64,"\r");+    abAppend(&ab,seq,strlen(seq));++    lndebug("\n");+    l->oldpos = l->pos;++    if (write(fd,ab.b,ab.len) == -1) {} /* Can't recover from write error. */+    abFree(&ab);+}++/* Calls the two low level functions refreshSingleLine() or+ * refreshMultiLine() according to the selected mode. */+static void refreshLine(struct linenoiseState *l) {+    if (mlmode)+        refreshMultiLine(l);+    else+        refreshSingleLine(l);+}++/* Insert the character 'c' at cursor current position.+ *+ * On error writing to the terminal -1 is returned, otherwise 0. */+int linenoiseEditInsert(struct linenoiseState *l, char c) {+    if (l->len < l->buflen) {+        if (l->len == l->pos) {+            l->buf[l->pos] = c;+            l->pos++;+            l->len++;+            l->buf[l->len] = '\0';+            if ((!mlmode && l->plen+l->len < l->cols && !hintsCallback)) {+                /* Avoid a full update of the line in the+                 * trivial case. */+                if (write(l->ofd,&c,1) == -1) return -1;+            } else {+                refreshLine(l);+            }+        } else {+            memmove(l->buf+l->pos+1,l->buf+l->pos,l->len-l->pos);+            l->buf[l->pos] = c;+            l->len++;+            l->pos++;+            l->buf[l->len] = '\0';+            refreshLine(l);+        }+    }+    return 0;+}++/* Move cursor on the left. */+void linenoiseEditMoveLeft(struct linenoiseState *l) {+    if (l->pos > 0) {+        l->pos--;+        refreshLine(l);+    }+}++/* Move cursor on the right. */+void linenoiseEditMoveRight(struct linenoiseState *l) {+    if (l->pos != l->len) {+        l->pos++;+        refreshLine(l);+    }+}++/* Move cursor to the start of the line. */+void linenoiseEditMoveHome(struct linenoiseState *l) {+    if (l->pos != 0) {+        l->pos = 0;+        refreshLine(l);+    }+}++/* Move cursor to the end of the line. */+void linenoiseEditMoveEnd(struct linenoiseState *l) {+    if (l->pos != l->len) {+        l->pos = l->len;+        refreshLine(l);+    }+}++/* Substitute the currently edited line with the next or previous history+ * entry as specified by 'dir'. */+#define LINENOISE_HISTORY_NEXT 0+#define LINENOISE_HISTORY_PREV 1+void linenoiseEditHistoryNext(struct linenoiseState *l, int dir) {+    if (history_len > 1) {+        /* Update the current history entry before to+         * overwrite it with the next one. */+        free(history[history_len - 1 - l->history_index]);+        history[history_len - 1 - l->history_index] = strdup(l->buf);+        /* Show the new entry */+        l->history_index += (dir == LINENOISE_HISTORY_PREV) ? 1 : -1;+        if (l->history_index < 0) {+            l->history_index = 0;+            return;+        } else if (l->history_index >= history_len) {+            l->history_index = history_len-1;+            return;+        }+        strncpy(l->buf,history[history_len - 1 - l->history_index],l->buflen);+        l->buf[l->buflen-1] = '\0';+        l->len = l->pos = strlen(l->buf);+        refreshLine(l);+    }+}++/* 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 linenoiseEditDelete(struct linenoiseState *l) {+    if (l->len > 0 && l->pos < l->len) {+        memmove(l->buf+l->pos,l->buf+l->pos+1,l->len-l->pos-1);+        l->len--;+        l->buf[l->len] = '\0';+        refreshLine(l);+    }+}++/* Backspace implementation. */+void linenoiseEditBackspace(struct linenoiseState *l) {+    if (l->pos > 0 && l->len > 0) {+        memmove(l->buf+l->pos-1,l->buf+l->pos,l->len-l->pos);+        l->pos--;+        l->len--;+        l->buf[l->len] = '\0';+        refreshLine(l);+    }+}++/* Delete the previosu word, maintaining the cursor at the start of the+ * current word. */+void linenoiseEditDeletePrevWord(struct linenoiseState *l) {+    size_t old_pos = l->pos;+    size_t diff;++    while (l->pos > 0 && l->buf[l->pos-1] == ' ')+        l->pos--;+    while (l->pos > 0 && l->buf[l->pos-1] != ' ')+        l->pos--;+    diff = old_pos - l->pos;+    memmove(l->buf+l->pos,l->buf+old_pos,l->len-old_pos+1);+    l->len -= diff;+    refreshLine(l);+}++/* 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. */+static int linenoiseEdit(int stdin_fd, int stdout_fd, char *buf, size_t buflen, const char *prompt)+{+    struct linenoiseState l;++    /* Populate the linenoise state that we pass to functions implementing+     * specific editing functionalities. */+    l.ifd = stdin_fd;+    l.ofd = stdout_fd;+    l.buf = buf;+    l.buflen = buflen;+    l.prompt = prompt;+    l.plen = strlen(prompt);+    l.oldpos = l.pos = 0;+    l.len = 0;+    l.cols = getColumns(stdin_fd, stdout_fd);+    l.maxrows = 0;+    l.history_index = 0;++    /* Buffer starts empty. */+    l.buf[0] = '\0';+    l.buflen--; /* Make sure there is always space for the nulterm */++    /* The latest history entry is always our current buffer, that+     * initially is just an empty string. */+    linenoiseHistoryAdd("");++    if (write(l.ofd,prompt,l.plen) == -1) return -1;+    while(1) {+        char c;+        int nread;+        char seq[3];++        nread = read(l.ifd,&c,1);+        if (nread <= 0) return l.len;++        /* 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 = completeLine(&l);+            /* Return on errors */+            if (c < 0) return l.len;+            /* Read next character when 0 */+            if (c == 0) continue;+        }++        switch(c) {+        case ENTER:    /* enter */+            history_len--;+            free(history[history_len]);+            if (mlmode) linenoiseEditMoveEnd(&l);+            if (hintsCallback) {+                /* Force a refresh without hints to leave the previous+                 * line as the user typed it after a newline. */+                linenoiseHintsCallback *hc = hintsCallback;+                hintsCallback = NULL;+                refreshLine(&l);+                hintsCallback = hc;+            }+            return (int)l.len;+        case CTRL_C:     /* ctrl-c */+            errno = EAGAIN;+            return -1;+        case BACKSPACE:   /* backspace */+        case 8:     /* ctrl-h */+            linenoiseEditBackspace(&l);+            break;+        case CTRL_D:     /* ctrl-d, remove char at right of cursor, or if the+                            line is empty, act as end-of-file. */+            if (l.len > 0) {+                linenoiseEditDelete(&l);+            } else {+                history_len--;+                free(history[history_len]);+                return -1;+            }+            break;+        case CTRL_T:    /* ctrl-t, swaps current character with previous. */+            if (l.pos > 0 && l.pos < l.len) {+                int aux = buf[l.pos-1];+                buf[l.pos-1] = buf[l.pos];+                buf[l.pos] = aux;+                if (l.pos != l.len-1) l.pos++;+                refreshLine(&l);+            }+            break;+        case CTRL_B:     /* ctrl-b */+            linenoiseEditMoveLeft(&l);+            break;+        case CTRL_F:     /* ctrl-f */+            linenoiseEditMoveRight(&l);+            break;+        case CTRL_P:    /* ctrl-p */+            linenoiseEditHistoryNext(&l, LINENOISE_HISTORY_PREV);+            break;+        case CTRL_N:    /* ctrl-n */+            linenoiseEditHistoryNext(&l, LINENOISE_HISTORY_NEXT);+            break;+        case ESC:    /* escape sequence */+            /* Read the next two bytes representing the escape sequence.+             * Use two calls to handle slow terminals returning the two+             * chars at different times. */+            if (read(l.ifd,seq,1) == -1) break;+            if (read(l.ifd,seq+1,1) == -1) break;++            /* ESC [ sequences. */+            if (seq[0] == '[') {+                if (seq[1] >= '0' && seq[1] <= '9') {+                    /* Extended escape, read additional byte. */+                    if (read(l.ifd,seq+2,1) == -1) break;+                    if (seq[2] == '~') {+                        switch(seq[1]) {+                        case '3': /* Delete key. */+                            linenoiseEditDelete(&l);+                            break;+                        }+                    }+                } else {+                    switch(seq[1]) {+                    case 'A': /* Up */+                        linenoiseEditHistoryNext(&l, LINENOISE_HISTORY_PREV);+                        break;+                    case 'B': /* Down */+                        linenoiseEditHistoryNext(&l, LINENOISE_HISTORY_NEXT);+                        break;+                    case 'C': /* Right */+                        linenoiseEditMoveRight(&l);+                        break;+                    case 'D': /* Left */+                        linenoiseEditMoveLeft(&l);+                        break;+                    case 'H': /* Home */+                        linenoiseEditMoveHome(&l);+                        break;+                    case 'F': /* End*/+                        linenoiseEditMoveEnd(&l);+                        break;+                    }+                }+            }++            /* ESC O sequences. */+            else if (seq[0] == 'O') {+                switch(seq[1]) {+                case 'H': /* Home */+                    linenoiseEditMoveHome(&l);+                    break;+                case 'F': /* End*/+                    linenoiseEditMoveEnd(&l);+                    break;+                }+            }+            break;+        default:+            if (linenoiseEditInsert(&l,c)) return -1;+            break;+        case CTRL_U: /* Ctrl+u, delete the whole line. */+            buf[0] = '\0';+            l.pos = l.len = 0;+            refreshLine(&l);+            break;+        case CTRL_K: /* Ctrl+k, delete from current to end of line. */+            buf[l.pos] = '\0';+            l.len = l.pos;+            refreshLine(&l);+            break;+        case CTRL_A: /* Ctrl+a, go to the start of the line */+            linenoiseEditMoveHome(&l);+            break;+        case CTRL_E: /* ctrl+e, go to the end of the line */+            linenoiseEditMoveEnd(&l);+            break;+        case CTRL_L: /* ctrl+l, clear screen */+            linenoiseClearScreen();+            refreshLine(&l);+            break;+        case CTRL_W: /* ctrl+w, delete previous word */+            linenoiseEditDeletePrevWord(&l);+            break;+        }+    }+    return l.len;+}++/* This special mode is used by linenoise in order to print scan codes+ * on screen for debugging / development purposes. It is implemented+ * by the linenoise_example program using the --keycodes option. */+void linenoisePrintKeyCodes(void) {+    char quit[4];++    printf("Linenoise key codes debugging mode.\n"+            "Press keys to see scan codes. Type 'quit' at any time to exit.\n");+    if (enableRawMode(STDIN_FILENO) == -1) return;+    memset(quit,' ',4);+    while(1) {+        char c;+        int nread;++        nread = read(STDIN_FILENO,&c,1);+        if (nread <= 0) continue;+        memmove(quit,quit+1,sizeof(quit)-1); /* shift string to left. */+        quit[sizeof(quit)-1] = c; /* Insert current char on the right. */+        if (memcmp(quit,"quit",sizeof(quit)) == 0) break;++        printf("'%c' %02x (%d) (type quit to exit)\n",+            isprint(c) ? c : '?', (int)c, (int)c);+        printf("\r"); /* Go left edge manually, we are in raw mode. */+        fflush(stdout);+    }+    disableRawMode(STDIN_FILENO);+}++/* This function calls the line editing function linenoiseEdit() using+ * the STDIN file descriptor set in raw mode. */+static int linenoiseRaw(char *buf, size_t buflen, const char *prompt) {+    int count;++    if (buflen == 0) {+        errno = EINVAL;+        return -1;+    }++    if (enableRawMode(STDIN_FILENO) == -1) return -1;+    count = linenoiseEdit(STDIN_FILENO, STDOUT_FILENO, buf, buflen, prompt);+    disableRawMode(STDIN_FILENO);+    printf("\n");+    return count;+}++/* This function is called when linenoise() is called with the standard+ * input file descriptor not attached to a TTY. So for example when the+ * program using linenoise is called in pipe or with a file redirected+ * to its standard input. In this case, we want to be able to return the+ * line regardless of its length (by default we are limited to 4k). */+static char *linenoiseNoTTY(void) {+    char *line = NULL;+    size_t len = 0, maxlen = 0;++    while(1) {+        if (len == maxlen) {+            if (maxlen == 0) maxlen = 16;+            maxlen *= 2;+            char *oldval = line;+            line = realloc(line,maxlen);+            if (line == NULL) {+                if (oldval) free(oldval);+                return NULL;+            }+        }+        int c = fgetc(stdin);+        if (c == EOF || c == '\n') {+            if (c == EOF && len == 0) {+                free(line);+                return NULL;+            } else {+                line[len] = '\0';+                return line;+            }+        } else {+            line[len] = c;+            len++;+        }+    }+}++/* 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. */+char *linenoise(const char *prompt) {+    char buf[LINENOISE_MAX_LINE];+    int count;++    if (!isatty(STDIN_FILENO)) {+        /* Not a tty: read from file / pipe. In this mode we don't want any+         * limit to the line size, so we call a function to handle that. */+        return linenoiseNoTTY();+    } else if (isUnsupportedTerm()) {+        size_t len;++        printf("%s",prompt);+        fflush(stdout);+        if (fgets(buf,LINENOISE_MAX_LINE,stdin) == NULL) return NULL;+        len = strlen(buf);+        while(len && (buf[len-1] == '\n' || buf[len-1] == '\r')) {+            len--;+            buf[len] = '\0';+        }+        return strdup(buf);+    } else {+        count = linenoiseRaw(buf,LINENOISE_MAX_LINE,prompt);+        if (count == -1) return NULL;+        return strdup(buf);+    }+}++/* This is just a wrapper the user may want to call in order to make sure+ * the linenoise returned buffer is freed with the same allocator it was+ * created with. Useful when the main program is using an alternative+ * allocator. */+void linenoiseFree(void *ptr) {+    free(ptr);+}++/* ================================ History ================================= */++/* Free the history, but does not reset it. Only used when we have to+ * exit() to avoid memory leaks are reported by valgrind & co. */+static void freeHistory(void) {+    if (history) {+        int j;++        for (j = 0; j < history_len; j++)+            free(history[j]);+        free(history);+    }+}++/* At exit we'll try to fix the terminal to the initial conditions. */+static void linenoiseAtExit(void) {+    disableRawMode(STDIN_FILENO);+    freeHistory();+}++/* This is the API call to add a new entry in the linenoise history.+ * It uses a fixed array of char pointers that are shifted (memmoved)+ * when the history max length is reached in order to remove the older+ * entry and make room for the new one, so it is not exactly suitable for huge+ * histories, but will work well for a few hundred of entries.+ *+ * Using a circular buffer is smarter, but a bit more complex to handle. */+int linenoiseHistoryAdd(const char *line) {+    char *linecopy;++    if (history_max_len == 0) return 0;++    /* Initialization on first call. */+    if (history == NULL) {+        history = malloc(sizeof(char*)*history_max_len);+        if (history == NULL) return 0;+        memset(history,0,(sizeof(char*)*history_max_len));+    }++    /* Don't add duplicated lines. */+    if (history_len && !strcmp(history[history_len-1], line)) return 0;++    /* Add an heap allocated copy of the line in the history.+     * If we reached the max length, remove the older line. */+    linecopy = strdup(line);+    if (!linecopy) return 0;+    if (history_len == history_max_len) {+        free(history[0]);+        memmove(history,history+1,sizeof(char*)*(history_max_len-1));+        history_len--;+    }+    history[history_len] = linecopy;+    history_len++;+    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. */+int linenoiseHistorySetMaxLen(int len) {+    char **new;++    if (len < 1) return 0;+    if (history) {+        int tocopy = history_len;++        new = malloc(sizeof(char*)*len);+        if (new == NULL) return 0;++        /* If we can't copy everything, free the elements we'll not use. */+        if (len < tocopy) {+            int j;++            for (j = 0; j < tocopy-len; j++) free(history[j]);+            tocopy = len;+        }+        memset(new,0,sizeof(char*)*len);+        memcpy(new,history+(history_len-tocopy), sizeof(char*)*tocopy);+        free(history);+        history = new;+    }+    history_max_len = len;+    if (history_len > history_max_len)+        history_len = history_max_len;+    return 1;+}++/* Save the history in the specified file. On success 0 is returned+ * otherwise -1 is returned. */+int linenoiseHistorySave(const char *filename) {+    mode_t old_umask = umask(S_IXUSR|S_IRWXG|S_IRWXO);+    FILE *fp;+    int j;++    fp = fopen(filename,"w");+    umask(old_umask);+    if (fp == NULL) return -1;+    chmod(filename,S_IRUSR|S_IWUSR);+    for (j = 0; j < history_len; j++)+        fprintf(fp,"%s\n",history[j]);+    fclose(fp);+    return 0;+}++/* 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. */+int linenoiseHistoryLoad(const char *filename) {+    FILE *fp = fopen(filename,"r");+    char buf[LINENOISE_MAX_LINE];++    if (fp == NULL) return -1;++    while (fgets(buf,LINENOISE_MAX_LINE,fp) != NULL) {+        char *p;++        p = strchr(buf,'\r');+        if (!p) p = strchr(buf,'\n');+        if (p) *p = '\0';+        linenoiseHistoryAdd(buf);+    }+    fclose(fp);+    return 0;+}
+ linenoise.cabal view
@@ -0,0 +1,69 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.31.2.+--+-- see: https://github.com/sol/hpack+--+-- hash: 9c5adcaeb5854166e0c083c000d0b95aefa62722036500d6cc7104cdd13c9ea3++name:           linenoise+version:        0.3.0+synopsis:       A lightweight readline-replacement library for Haskell+description:    Please see the README on GitHub at <https://github.com/ejconlon/haskell-linenoise#readme>+category:       User Interfaces+homepage:       https://github.com/ejconlon/haskell-linenoise#readme+bug-reports:    https://github.com/ejconlon/haskell-linenoise/issues+author:         Stephen Diehl, Eric Conlon+maintainer:     ejconlon@gmail.com+copyright:      2014 Stephen Diehl, 2019 Eric Conlon+license:        BSD3+license-file:   LICENSE+build-type:     Simple+extra-source-files:+    README.md++source-repository head+  type: git+  location: https://github.com/ejconlon/haskell-linenoise++library+  exposed-modules:+      Linenoise+      Linenoise.Unlift+      Linenoise.Repl+      Linenoise.Completion+  other-modules:+      Linenoise.FFI+      Paths_linenoise+  hs-source-dirs:+      src+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wpartial-fields -Wredundant-constraints -fno-warn-unused-top-binds+  include-dirs:+      cbits+  c-sources:+      cbits/linenoise.c+  build-depends:+      base >=4.12 && <5+    , bytestring >=0.10+    , exceptions >=0.10+    , mtl >=2.2+    , text >=1.2+    , unliftio-core >=0.1+  default-language: Haskell2010++executable linenoise-demo+  main-is: Main.hs+  other-modules:+      Paths_linenoise+  hs-source-dirs:+      app+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wpartial-fields -Wredundant-constraints -fno-warn-unused-top-binds -threaded -rtsopts -with-rtsopts=-N+  build-depends:+      base >=4.12 && <5+    , bytestring >=0.10+    , exceptions >=0.10+    , linenoise+    , mtl >=2.2+    , text >=1.2+    , unliftio-core >=0.1+  default-language: Haskell2010
+ src/Linenoise.hs view
@@ -0,0 +1,23 @@+-- | Re-exports of all public entities.+module Linenoise+  ( InputResult (..)+  , ReplDirective (..)+  , ReplT (..)+  , addHistory+  , byWord+  , clearScreen+  , getInputLine+  , historyLoad+  , historySave+  , printKeycodes+  , replM+  , runReplT+  , setCompletion+  , setMultiline+  , stifleHistory+  ) where++import Linenoise.Completion (byWord)+import Linenoise.Repl (ReplDirective (..), ReplT (..), replM, runReplT)+import Linenoise.Unlift (InputResult (..), addHistory, clearScreen, getInputLine, historyLoad, historySave,+                         printKeycodes, setCompletion, setMultiline, stifleHistory)
+ src/Linenoise/Completion.hs view
@@ -0,0 +1,32 @@+{-# LANGUAGE OverloadedStrings #-}++module Linenoise.Completion+  ( byWord+  ) where++import Data.Text (Text)+import qualified Data.Text as Text++-- | Complete by word.+byWord :: Monad m => (Text -> m [Text]) -> (Text -> m [Text])+byWord f line = do+  let split = Text.words line+  case split of+    [] -> f line+    [_] -> f line+    sp -> do+      let (x,xs) = (last sp, init sp)+      res <- f x+      case res of+        [] -> pure [line]+        [y] ->+          pure [Text.unwords xs <> " " <> x <> trimComplete x y <> " "]+        ys ->+          pure (map (complete x xs) ys)++complete :: Text -> [Text] -> Text -> Text+complete x xs y =+  Text.unwords xs <> " " <> x <> trimComplete x y++trimComplete :: Text -> Text -> Text+trimComplete = Text.drop . Text.length
+ src/Linenoise/FFI.hs view
@@ -0,0 +1,135 @@+{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE DeriveFoldable #-}+{-# LANGUAGE DeriveTraversable #-}+{-# LANGUAGE ForeignFunctionInterface #-}++-- | Just an FFI layer over the C library.+module Linenoise.FFI+  ( InputResult (..)+  , addHistory+  , clearScreen+  , getInputLine+  , historyLoad+  , historySave+  , printKeycodes+  , setCompletion+  , setMultiline+  , stifleHistory+  ) where++import Data.ByteString (ByteString)+import qualified Data.ByteString.Unsafe as BSU+import Data.Foldable (forM_)+import Foreign+import Foreign.C.Error (eAGAIN, getErrno, resetErrno)+import Foreign.C.String+import Foreign.C.Types (CChar, CInt (..), CSize)++foreign import ccall "linenoise.h linenoise"+  linenoise :: CString -> IO CString+foreign import ccall "linenoise.h linenoiseHistoryAdd"+  linenoiseHistoryAdd :: Ptr CChar -> IO CInt+foreign import ccall "linenoise.h linenoiseHistorySetMaxLen"+  linenoiseHistorySetMaxLen :: CInt -> IO CInt+foreign import ccall "linenoise.h linenoiseHistorySave"+  linenoiseHistorySave :: CString -> IO ()+foreign import ccall "linenoise.h linenoiseHistoryLoad"+  linenoiseHistoryLoad :: CString -> IO ()+foreign import ccall "linenoise.h linenoiseClearScreen"+  linenoiseClearScreen :: IO ()+foreign import ccall "linenoise.h linenoiseSetMultiLine"+  linenoiseSetMultiLine :: CInt -> IO ()+foreign import ccall "linenoise.h linenoisePrintKeyCodes"+  linenoisePrintKeyCodes :: IO ()+foreign import ccall "linenoise.h linenoiseSetCompletionCallback"+  linenoiseSetCompletionCallback :: FunPtr CompleteFunc -> IO ()+foreign import ccall "linenoise.h linenoiseAddCompletion"+  linenoiseAddCompletion :: Completion -> CString -> IO ()++foreign import ccall "wrapper"+    makeFunPtr :: CompleteFunc -> IO (FunPtr CompleteFunc)++data CompletionType = CompletionType CSize (Ptr (Ptr CChar))+  deriving (Show, Eq)++type Completion = Ptr CompletionType++instance Storable CompletionType where+  sizeOf _ = 8+  alignment = sizeOf+  peek ptr = do+    a <- peekByteOff ptr 0+    b <- peekByteOff ptr 4+    pure (CompletionType a b)+  poke = error "no poke"++-- Completion C callback+type CompleteFunc = (CString -> Completion -> IO ())++-- Make a completion function pointer.+makeCompletion :: (ByteString -> IO [ByteString]) -> (CString -> Completion -> IO ())+makeCompletion f buf lc = do+  line <- BSU.unsafePackCString buf+  comps <- f line+  forM_ comps (`BSU.unsafeUseAsCString` linenoiseAddCompletion lc)++-- | Result of getInputLine.+data InputResult a+  = InterruptResult        -- ^ ctrl+c+  | EofResult              -- ^ ctrl+d+  | LineResult !a          -- Possibly empty line.+  deriving (Eq, Show, Functor, Foldable, Traversable)++-- | Run the prompt, yielding a string.+getInputLine :: ByteString -> IO (InputResult ByteString)+getInputLine prompt = do+  res <- BSU.unsafeUseAsCString prompt $ \str -> do+    ptr <- linenoise str+    maybePeek BSU.unsafePackCString ptr+  errno <- getErrno+  if errno == eAGAIN+    then resetErrno >> pure InterruptResult+    else pure (maybe EofResult LineResult res)++-- | Add to current history.+addHistory :: ByteString -> IO ()+addHistory =+  flip BSU.unsafeUseAsCString $ \str -> do+    _ <- linenoiseHistoryAdd str+    pure ()++-- | Limit the maximum history length.+stifleHistory :: Int -> IO ()+stifleHistory len = do+  _ <- linenoiseHistorySetMaxLen $ fromIntegral len+  pure ()++-- | Save history to a file.+historySave :: FilePath -> IO ()+historySave fname = do+  str <- newCString fname+  linenoiseHistorySave str++-- | Load history from a file.+historyLoad :: FilePath -> IO ()+historyLoad fname = do+  str <- newCString fname+  linenoiseHistoryLoad str++-- | Clear the screen.+clearScreen :: IO ()+clearScreen = linenoiseClearScreen++-- | Enable/Disable multiline input.+setMultiline :: Bool -> IO ()+setMultiline = linenoiseSetMultiLine . fromBool++-- | Print keycodes.+printKeycodes :: IO ()+printKeycodes = linenoisePrintKeyCodes++-- | Set the current completion function.+setCompletion :: (ByteString -> IO [ByteString]) -> IO ()+setCompletion f = do+  cb <- makeFunPtr (makeCompletion f)+  linenoiseSetCompletionCallback cb
+ src/Linenoise/Repl.hs view
@@ -0,0 +1,93 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses #-}++-- | Helper functions and transformer to write your own REPLs.+module Linenoise.Repl+  ( ReplDirective (..)+  , ReplT (..)+  , replM+  , runReplT+  ) where++import Control.Applicative (Alternative)+import Control.Monad (MonadPlus)+import Control.Monad.Catch (MonadCatch, MonadThrow)+import Control.Monad.Fail (MonadFail)+import Control.Monad.Fix (MonadFix)+import Control.Monad.IO.Class (MonadIO (..))+import Control.Monad.IO.Unlift (MonadUnliftIO (..), UnliftIO (..))+import Control.Monad.Reader (MonadReader, ReaderT (..), ask)+import Control.Monad.State.Strict (MonadState (..))+import Control.Monad.Trans (MonadTrans (..))+import Control.Monad.Zip (MonadZip)+import Data.Text (Text)+import Data.IORef (IORef, newIORef, readIORef, writeIORef)+import Linenoise.Unlift (InputResult (..))+import qualified Linenoise.Unlift as Unlift++-- | Basic monad transformer with mutable state that can be used with all "Linenoise.Unlift" functions.+--   You do not have to use this, but it's here to cover most of what you would need without having+--   to roll your own newtype.+newtype ReplT r s m a = ReplT { unReplT :: ReaderT r (ReaderT (IORef s) m) a }+  deriving (Functor, Applicative, Monad, MonadIO,+            Alternative, MonadPlus, MonadFix, MonadZip,+            MonadFail, MonadThrow, MonadCatch,+            MonadReader r)++askRef :: Applicative m => ReplT r s m (IORef s)+askRef = ReplT (ReaderT (const (ReaderT pure)))++refReplT :: ReplT r s m a -> r -> IORef s -> m a+refReplT n r = runReaderT (runReaderT (unReplT n) r)++instance MonadTrans (ReplT r s) where+  lift = ReplT . lift . lift++instance MonadUnliftIO m => MonadUnliftIO (ReplT r s m) where+  askUnliftIO = do+    UnliftIO run <- lift askUnliftIO+    r <- ask+    ref <- askRef+    pure (UnliftIO (\n -> run (refReplT n r ref)))++instance MonadIO m => MonadState s (ReplT r s m) where+  get = ReplT (ReaderT (const (ReaderT (liftIO . readIORef))))+  put s = ReplT (ReaderT (const (ReaderT (\ref -> liftIO (writeIORef ref s)))))++-- | Run a ReplT.+runReplT :: MonadIO m => ReplT r s m a -> r -> s -> m (a, s)+runReplT n r s = do+  ref <- liftIO (newIORef s)+  res <- refReplT n r ref+  final <- liftIO (readIORef ref)+  pure (res, final)++-- | Directive to control voluntary REPL termination.+data ReplDirective+  = ReplQuit+  | ReplContinue+  deriving (Eq, Show)++-- | Run a simple REPL.+replM+  :: MonadUnliftIO m+  => ReplDirective                   -- ^ Directive on interrupt+  -> Text                            -- ^ Prompt+  -> (Text -> m ReplDirective)       -- ^ Action+  -> (Text -> m [Text])              -- ^ Completion+  -> m ()+replM onInterrupt prompt action comp = loop where+  loop = do+    Unlift.setCompletion comp+    res <- Unlift.getInputLine prompt+    directive <- case res of+      InterruptResult -> pure onInterrupt+      EofResult -> pure ReplQuit+      LineResult line -> do+        directive <- action line+        Unlift.addHistory line+        pure directive+    case directive of+      ReplContinue -> loop+      ReplQuit -> pure ()
+ src/Linenoise/Unlift.hs view
@@ -0,0 +1,58 @@+-- | Unlifted versions of all FFI methods.+module Linenoise.Unlift+  ( InputResult (..)+  , addHistory+  , clearScreen+  , getInputLine+  , historyLoad+  , historySave+  , printKeycodes+  , setCompletion+  , setMultiline+  , stifleHistory+  ) where++import Control.Monad.IO.Class (MonadIO, liftIO)+import Control.Monad.IO.Unlift (MonadUnliftIO, withRunInIO)+import Data.Text (Text)+import Data.Text.Encoding (decodeUtf8, encodeUtf8)+import Linenoise.FFI (InputResult (..))+import qualified Linenoise.FFI as FFI++-- | Add to current history.+addHistory :: MonadIO m => Text -> m ()+addHistory = liftIO . FFI.addHistory . encodeUtf8++-- | Clear the screen.+clearScreen :: MonadIO m => m ()+clearScreen = liftIO FFI.clearScreen++-- | Run the prompt, yielding a string.+getInputLine :: MonadIO m => Text -> m (InputResult Text)+getInputLine = liftIO . fmap (fmap decodeUtf8) . FFI.getInputLine . encodeUtf8++-- | Load history from a file.+historyLoad :: MonadIO m => FilePath -> m ()+historyLoad = liftIO . FFI.historyLoad++-- | Save history to a file.+historySave :: MonadIO m => FilePath -> m ()+historySave = liftIO . FFI.historySave++-- | Print keycodes.+printKeycodes :: MonadIO m => m ()+printKeycodes = liftIO FFI.printKeycodes++-- | Set the current completion function.+setCompletion :: MonadUnliftIO m => (Text -> m [Text]) -> m ()+setCompletion f =+  let g = fmap (fmap encodeUtf8) . f . decodeUtf8+  in withRunInIO (\runInIO -> FFI.setCompletion (runInIO . g))++-- | Enable/Disable multiline input.+setMultiline :: MonadIO m => Bool -> m ()+setMultiline = liftIO . FFI.setMultiline++-- | Limit the maximum history length.+stifleHistory :: MonadIO m => Int -> m ()+stifleHistory = liftIO . FFI.stifleHistory