linenoise 0.6.0 → 0.7.0
raw patch · 6 files changed
+1001/−120 lines, 6 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ Linenoise: DoneResult :: !InputResult a -> EditResult a
+ Linenoise: MoreResult :: EditResult a
+ Linenoise: data EditResult a
+ Linenoise: data EditSession
+ Linenoise: feedEditSession :: MonadIO m => EditSession -> m (EditResult Text)
+ Linenoise: freeEditSession :: MonadIO m => EditSession -> m ()
+ Linenoise: hideEditSession :: MonadIO m => EditSession -> m ()
+ Linenoise: showEditSession :: MonadIO m => EditSession -> m ()
+ Linenoise: startEditSession :: MonadIO m => Text -> m EditSession
+ Linenoise: stopEditSession :: MonadIO m => EditSession -> m ()
+ Linenoise: withEditSession :: MonadUnliftIO m => Text -> (EditSession -> m a) -> m a
+ Linenoise: withHiddenEditSession :: MonadUnliftIO m => EditSession -> m a -> m a
+ Linenoise.Unlift: DoneResult :: !InputResult a -> EditResult a
+ Linenoise.Unlift: MoreResult :: EditResult a
+ Linenoise.Unlift: data EditResult a
+ Linenoise.Unlift: data EditSession
+ Linenoise.Unlift: feedEditSession :: MonadIO m => EditSession -> m (EditResult Text)
+ Linenoise.Unlift: freeEditSession :: MonadIO m => EditSession -> m ()
+ Linenoise.Unlift: hideEditSession :: MonadIO m => EditSession -> m ()
+ Linenoise.Unlift: showEditSession :: MonadIO m => EditSession -> m ()
+ Linenoise.Unlift: startEditSession :: MonadIO m => Text -> m EditSession
+ Linenoise.Unlift: stopEditSession :: MonadIO m => EditSession -> m ()
+ Linenoise.Unlift: withEditSession :: MonadUnliftIO m => Text -> (EditSession -> m a) -> m a
+ Linenoise.Unlift: withHiddenEditSession :: MonadUnliftIO m => EditSession -> m a -> m a
Files
- cbits/linenoise.c +722/−104
- cbits/linenoise.h +6/−0
- linenoise.cabal +1/−1
- src/Linenoise.hs +22/−2
- src/Linenoise/FFI.hs +184/−11
- src/Linenoise/Unlift.hs +66/−2
cbits/linenoise.c view
@@ -119,18 +119,25 @@ #include "linenoise.h" #define LINENOISE_DEFAULT_HISTORY_MAX_LEN 100-#define LINENOISE_MAX_LINE 4096+#define LINENOISE_MAX_LINE (1024*1024) // That will get dynamically allocated+#define LINENOISE_INITIAL_BUFLEN 4096+#define PASTE_FOLD_THRESHOLD 200 // Min bytes to fold a single-line paste.+#define PASTE_FOLD_CONTEXT 8 // Context chars kept around generic folds.+#define PASTE_MAX_BYTES LINENOISE_MAX_LINE static char *unsupported_term[] = {"dumb","cons25","emacs",NULL}; static linenoiseCompletionCallback *completionCallback = NULL; static linenoiseHintsCallback *hintsCallback = NULL; static linenoiseFreeHintsCallback *freeHintsCallback = NULL;+static char *linenoiseReadLine(FILE *fp, int *err); static char *linenoiseNoTTY(void); static void refreshLineWithCompletion(struct linenoiseState *ls, linenoiseCompletions *lc, int flags); static void refreshLineWithFlags(struct linenoiseState *l, int flags);+static void linenoiseFoldClear(struct linenoiseState *l); static struct termios orig_termios; /* In order to restore at exit.*/ static int maskmode = 0; /* Show "***" instead of input. For passwords. */ static int rawmode = 0; /* For atexit() function to check if restore is needed*/+static int rawmode_output = STDOUT_FILENO; /* fd used for terminal escapes. */ 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;@@ -407,10 +414,28 @@ return 1; /* Default: single width */ } +/* If s[] points at an ANSI CSI escape sequence (e.g. a color change like+ * ESC [ 1 ; 32 m), return its length in bytes. Otherwise return 0.+ *+ * The caller must have already verified that s[0] == ESC (0x1b). The+ * sequence layout follows ECMA-48: ESC '[' , parameter bytes (0x30-0x3f),+ * intermediate bytes (0x20-0x2f), and a final byte (0x40-0x7e). */+static size_t ansiEscapeLen(const char *s, size_t len) {+ size_t i;+ if (len < 2 || s[1] != '[') return 0;+ i = 2;+ while (i < len && (unsigned char)s[i] >= 0x30 && (unsigned char)s[i] <= 0x3f) i++;+ while (i < len && (unsigned char)s[i] >= 0x20 && (unsigned char)s[i] <= 0x2f) i++;+ if (i >= len || (unsigned char)s[i] < 0x40 || (unsigned char)s[i] > 0x7e) return 0;+ return i + 1;+}+ /* Calculate the display width of a UTF-8 string of 'len' bytes. * This is used for cursor positioning in the terminal. * Handles grapheme clusters: characters joined by ZWJ contribute 0 width- * after the first character in the sequence. */+ * after the first character in the sequence.+ * ANSI CSI escape sequences (e.g. color codes in the prompt) are treated+ * as zero-width. */ static size_t utf8StrWidth(const char *s, size_t len) { size_t width = 0; size_t i = 0;@@ -420,6 +445,18 @@ size_t clen; uint32_t cp = utf8DecodeChar(s + i, &clen); + /* Skip ANSI CSI escape sequences entirely: they produce no+ * glyph, so they must not contribute to the display width.+ * Checked before the ZWJ state so a stray ZWJ immediately+ * followed by ESC cannot swallow the ESC byte. */+ if (cp == 0x1b) {+ size_t skip = ansiEscapeLen(s + i, len - i);+ if (skip > 0) {+ i += skip;+ continue;+ }+ }+ if (after_zwj) { /* Character after ZWJ: don't add width, it's joined. * But do check for extending chars after it. */@@ -562,6 +599,8 @@ /* put terminal in raw mode after flushing */ if (tcsetattr(fd,TCSAFLUSH,&raw) < 0) goto fatal; rawmode = 1;+ /* Ask the terminal to wrap paste input between ESC[200~ and ESC[201~. */+ if (write(rawmode_output, "\x1b[?2004h", 8) == -1) {} return 0; fatal:@@ -576,8 +615,11 @@ return; } /* Don't even check the return value as it's too late. */- if (rawmode && tcsetattr(fd,TCSAFLUSH,&orig_termios) != -1)+ if (rawmode && tcsetattr(fd,TCSAFLUSH,&orig_termios) != -1) {+ /* Leave bracketed paste mode when leaving raw mode. */+ if (write(rawmode_output, "\x1b[?2004l", 8) == -1) {} rawmode = 0;+ } } /* Use the ESC [6n escape sequence to query the horizontal cursor position@@ -688,10 +730,12 @@ struct linenoiseState saved = *ls; ls->len = ls->pos = strlen(lc->cvec[ls->completion_idx]); ls->buf = lc->cvec[ls->completion_idx];+ ls->fold_count = 0; refreshLineWithFlags(ls,flags); ls->len = saved.len; ls->pos = saved.pos; ls->buf = saved.buf;+ ls->fold_count = saved.fold_count; } else { refreshLineWithFlags(ls,flags); }@@ -723,6 +767,7 @@ if (lc.len == 0) { linenoiseBeep(); ls->in_completion = 0;+ c = 0; } else { switch(c) { case 9: /* tab */@@ -747,6 +792,7 @@ nwritten = snprintf(ls->buf,ls->buflen,"%s", lc.cvec[ls->completion_idx]); ls->len = ls->pos = nwritten;+ linenoiseFoldClear(ls); } ls->in_completion = 0; break;@@ -830,14 +876,310 @@ free(ab->b); } +/* A fold is a display-only replacement for a range in l->buf. The edited+ * buffer always keeps the real bytes; refresh code asks linenoiseRenderBuffer()+ * for a temporary printable version plus the cursor position inside it. */+struct linenoiseFold {+ size_t start;+ size_t end;+ char display[64];+ size_t displaylen;+};++struct linenoiseFolds {+ int count;+ struct linenoiseFold fold[LINENOISE_MAX_FOLDS];+};++/* Return the number of logical lines in the range. */+static size_t foldCountLines(const char *buf, size_t len) {+ size_t lines = 1, j;+ for (j = 0; j < len; j++) {+ if (buf[j] == '\n') lines++;+ }+ return lines;+}++/* Return true if the text should be folded: if it contains newlines or is at+ * least PASTE_FOLD_THRESHOLD bytes long. */+static int shouldFoldText(const char *buf, size_t len) {+ return memchr(buf, '\n', len) != NULL || len >= PASTE_FOLD_THRESHOLD;+}++/* Fill f->display with the text shown instead of the folded range. */+static void foldSetRenderedText(struct linenoiseFold *f, const char *buf) {+ size_t hidden = f->end - f->start;+ size_t lines = foldCountLines(buf + f->start, hidden);+ int n;++ if (lines > 1)+ n = snprintf(f->display,sizeof(f->display),"[... %zu pasted lines ...]",lines);+ else+ n = snprintf(f->display,sizeof(f->display),"[... %zu pasted chars ...]",hidden);+ if (n < 0) n = 0;+ f->displaylen = (size_t)n;+}++/* Populate f with one fold reconstructed from a history entry. History stores+ * the real text, but not the original paste boundaries, so we reconstruct+ * an approximation of text we want to hide on the fly: if it is long or+ * contains newlines. */+static int linenoiseBuildHistoryFold(struct linenoiseState *l, struct linenoiseFold *f) {+ f->start = f->end = f->displaylen = 0;+ if (l->len == 0 || maskmode) return 0;+ if (!shouldFoldText(l->buf,l->len)) return 0;++ f->start = 0;+ f->end = l->len;+ if (l->len > PASTE_FOLD_CONTEXT*2) {+ size_t pos = 0, chars = 0;+ int nl = 0;++ /* We leave (if possible) a few chars on+ * the start before the fold, to give context. */+ while (pos < l->len && chars < PASTE_FOLD_CONTEXT) {+ size_t step = utf8NextCharLen(l->buf,pos,l->len);+ if (step == 0 || pos + step > l->len) break;+ if (l->buf[pos] == '\n') nl = 1;+ pos += step;+ chars++;+ }+ f->start = nl ? 0 : pos;++ /* And also on the end side. */+ pos = l->len;+ chars = 0;+ nl = 0;+ while (pos > 0 && chars < PASTE_FOLD_CONTEXT) {+ size_t step = utf8PrevCharLen(l->buf,pos);+ if (step == 0 || step > pos) break;+ pos -= step;+ if (l->buf[pos] == '\n') nl = 1;+ chars++;+ }+ f->end = nl ? l->len : pos;+ if (f->start >= f->end) {+ f->start = 0;+ f->end = l->len;+ }+ }+ foldSetRenderedText(f,l->buf);+ return 1;+}++/* Populate fs with the folds to render for the current buffer. As a side+ * effect, the rendered text of each fold is updated. Return 1 if folding+ * should be used, or 0 if the buffer should be rendered as-is. */+static int linenoiseGetRenderFolds(struct linenoiseState *l, struct linenoiseFolds *fs) {+ int j;++ fs->count = 0;+ if (l->len == 0 || maskmode) return 0;++ for (j = 0; j < l->fold_count; j++) {+ struct linenoiseFold *f;+ size_t start = l->fold_start[j];+ size_t end = l->fold_end[j];++ if (start >= end || end > l->len) continue;+ f = fs->fold + fs->count++;+ f->start = start;+ f->end = end;+ foldSetRenderedText(f,l->buf);+ }+ return fs->count != 0;+}++/* Return the freshly allocated string content that is actually displayed in+ * the user prompt. It can be the actual edited line, or a special version+ * where pasted or multiline history ranges are replaced by their folded+ * "[...]" style versions. outpos is l->pos translated into this rendered+ * buffer. */+static int linenoiseRenderBuffer(struct linenoiseState *l, char **out, size_t *outlen, size_t *outpos) {+ struct linenoiseFolds fs;+ size_t len, pos, src, dst;+ char *r;+ int j, pos_set = 0;++ if (!linenoiseGetRenderFolds(l,&fs)) {+ /* Keep the refresh code simple: it always owns a temporary render+ * buffer, even when the render is identical to the real edit buffer. */+ r = malloc(l->len+1);+ if (r == NULL) return -1;+ memcpy(r,l->buf,l->len);+ r[l->len] = '\0';+ *out = r;+ *outlen = l->len;+ *outpos = l->pos;+ return 0;+ }++ /* Gaps are copied as-is, folded ranges are replaced by their markers.+ * The bytes inside each [start,end) range stay in l->buf but are not+ * emitted to the terminal. */+ len = l->len;+ for (j = 0; j < fs.count; j++) {+ struct linenoiseFold *f = fs.fold+j;+ len -= f->end - f->start;+ len += f->displaylen;+ }+ r = malloc(len+1);+ if (r == NULL) return -1;++ src = dst = 0;+ pos = 0;+ for (j = 0; j < fs.count; j++) {+ struct linenoiseFold *f = fs.fold+j;+ size_t gap = f->start - src;++ if (!pos_set && l->pos <= f->start) {+ pos = dst + (l->pos - src);+ pos_set = 1;+ }+ memcpy(r+dst,l->buf+src,gap);+ dst += gap;++ if (!pos_set && l->pos < f->end) {+ pos = dst + f->displaylen;+ pos_set = 1;+ }+ memcpy(r+dst,f->display,f->displaylen);+ dst += f->displaylen;+ if (!pos_set && l->pos == f->end) {+ pos = dst;+ pos_set = 1;+ }+ src = f->end;+ }+ if (!pos_set) pos = dst + (l->pos - src);+ memcpy(r+dst,l->buf+src,l->len-src);+ r[len] = '\0';++ *out = r;+ *outlen = len;+ *outpos = pos;+ return 0;+}++/* Return the number of bytes to move right from pos. If pos is at the start of+ * a folded range, the whole hidden range is skipped by one cursor movement. */+static size_t linenoiseEditNextLen(struct linenoiseState *l, size_t pos) {+ struct linenoiseFolds fs;+ int j;++ if (linenoiseGetRenderFolds(l,&fs)) {+ for (j = 0; j < fs.count; j++) {+ if (pos == fs.fold[j].start)+ return fs.fold[j].end - fs.fold[j].start;+ }+ }+ return utf8NextCharLen(l->buf,pos,l->len);+}++/* Return the number of bytes to move left from pos. If pos is at the end of a+ * folded range, the whole hidden range is skipped by one cursor movement. */+static size_t linenoiseEditPrevLen(struct linenoiseState *l, size_t pos) {+ struct linenoiseFolds fs;+ int j;++ if (linenoiseGetRenderFolds(l,&fs)) {+ for (j = 0; j < fs.count; j++) {+ if (pos == fs.fold[j].end)+ return fs.fold[j].end - fs.fold[j].start;+ }+ }+ return utf8PrevCharLen(l->buf,pos);+}++/* Add a fold range, keeping the array sorted by start offset. */+static void linenoiseFoldAdd(struct linenoiseState *l, size_t start, size_t end) {+ int j;++ if (start >= end || l->fold_count == LINENOISE_MAX_FOLDS) return;+ j = l->fold_count;+ while (j > 0 && start < l->fold_start[j-1]) {+ l->fold_start[j] = l->fold_start[j-1];+ l->fold_end[j] = l->fold_end[j-1];+ j--;+ }+ l->fold_start[j] = start;+ l->fold_end[j] = end;+ l->fold_count++;+}++/* Clear all remembered fold ranges. */+static void linenoiseFoldClear(struct linenoiseState *l) {+ l->fold_count = 0;+}++/* Remove one remembered fold range. */+static void linenoiseFoldRemove(struct linenoiseState *l, int j) {+ memmove(l->fold_start+j,l->fold_start+j+1,+ sizeof(size_t)*(l->fold_count-j-1));+ memmove(l->fold_end+j,l->fold_end+j+1,+ sizeof(size_t)*(l->fold_count-j-1));+ l->fold_count--;+}++/* Return true if [pos,pos+len) overlaps any folded range. */+static int linenoiseRangeOverlapsFold(struct linenoiseState *l, size_t pos, size_t len) {+ size_t end = pos + len;+ int j;++ for (j = 0; j < l->fold_count; j++) {+ if (end > l->fold_start[j] && pos < l->fold_end[j])+ return 1;+ }+ return 0;+}++/* Adjust fold ranges after an insertion. If insertion somehow lands inside a+ * fold, remove that fold because it no longer maps to an unchanged range. */+static void linenoiseAdjustFoldsAfterInsert(struct linenoiseState *l, size_t pos, size_t len) {+ int j = 0;++ while (j < l->fold_count) {+ if (pos <= l->fold_start[j]) {+ l->fold_start[j] += len;+ l->fold_end[j] += len;+ j++;+ } else if (pos < l->fold_end[j]) {+ linenoiseFoldRemove(l,j);+ } else {+ j++;+ }+ }+}++/* Adjust fold ranges after a deletion. If deletion overlaps a fold, remove+ * that fold because it no longer maps to an unchanged range. */+static void linenoiseAdjustFoldsAfterDelete(struct linenoiseState *l, size_t pos, size_t len) {+ size_t end = pos + len;+ int j = 0;++ while (j < l->fold_count) {+ if (end <= l->fold_start[j]) {+ l->fold_start[j] -= len;+ l->fold_end[j] -= len;+ j++;+ } else if (pos >= l->fold_end[j]) {+ j++;+ } else {+ linenoiseFoldRemove(l,j);+ }+ }+}+ /* Helper of refreshSingleLine() and refreshMultiLine() to show hints * to the right of the prompt. Now uses display widths for proper UTF-8. */-void refreshShowHints(struct abuf *ab, struct linenoiseState *l, int pwidth) {- char seq[64];- size_t bufwidth = utf8StrWidth(l->buf, l->len);- if (hintsCallback && pwidth + bufwidth < l->cols) {+void refreshShowHints(struct abuf *ab, struct linenoiseState *l, int pwidth, size_t bufwidth) {+ if (hintsCallback) {+ char seq[64]; int color = -1, bold = 0;- char *hint = hintsCallback(l->buf,&color,&bold);+ char *hint;++ if (pwidth + bufwidth >= l->cols) return;+ hint = hintsCallback(l->buf,&color,&bold); if (hint) { size_t hintlen = strlen(hint); size_t hintwidth = utf8StrWidth(hint, hintlen);@@ -883,16 +1225,22 @@ char seq[64]; size_t pwidth = utf8StrWidth(l->prompt, l->plen); /* Prompt display width */ int fd = l->ofd;- char *buf = l->buf;- size_t len = l->len; /* Byte length of buffer to display */- size_t pos = l->pos; /* Byte position of cursor */+ char *render = NULL;+ char *buf;+ size_t len; /* Byte length of buffer to display */+ size_t pos; /* Byte position of cursor in display buffer */ size_t poscol; /* Display column of cursor */ size_t lencol; /* Display width of buffer */+ size_t fullwidth; /* Display width before horizontal trimming. */ struct abuf ab; + if (linenoiseRenderBuffer(l,&render,&len,&pos) == -1) return;+ buf = render;+ /* Calculate the display width up to cursor and total display width. */ poscol = utf8StrWidth(buf, pos); lencol = utf8StrWidth(buf, len);+ fullwidth = lencol; /* Scroll the buffer horizontally if cursor is past the right edge. * We need to trim full UTF-8 characters from the left until the@@ -934,7 +1282,7 @@ abAppend(&ab,buf,len); } /* Show hints if any. */- refreshShowHints(&ab,l,pwidth);+ refreshShowHints(&ab,l,pwidth,fullwidth); } /* Erase to right */@@ -949,6 +1297,7 @@ if (write(fd,ab.b,ab.len) == -1) {} /* Can't recover from write error. */ abFree(&ab);+ free(render); } /* Multi line low level line refresh.@@ -963,9 +1312,11 @@ static void refreshMultiLine(struct linenoiseState *l, int flags) { char seq[64]; size_t pwidth = utf8StrWidth(l->prompt, l->plen); /* Prompt display width */- size_t bufwidth = utf8StrWidth(l->buf, l->len); /* Buffer display width */- size_t poswidth = utf8StrWidth(l->buf, l->pos); /* Cursor display width */- int rows = (pwidth+bufwidth+l->cols-1)/l->cols; /* rows used by current buf. */+ char *render = NULL;+ size_t render_len, render_pos;+ size_t bufwidth;+ size_t poswidth;+ int rows; /* rows used by current rendered buffer. */ int rpos = l->oldrpos; /* cursor relative row from previous refresh. */ int rpos2; /* rpos after refresh. */ int col; /* column position, zero-based. */@@ -973,6 +1324,10 @@ int fd = l->ofd, j; struct abuf ab; + if (linenoiseRenderBuffer(l,&render,&render_len,&render_pos) == -1) return;+ bufwidth = utf8StrWidth(render, render_len);+ poswidth = utf8StrWidth(render, render_pos);+ rows = (pwidth+bufwidth+l->cols-1)/l->cols; l->oldrows = rows; /* First step: clear all the lines used before. To do so start by@@ -1007,21 +1362,21 @@ if (maskmode == 1) { /* In mask mode, output one '*' per UTF-8 character, not byte */ size_t i = 0;- while (i < l->len) {+ while (i < render_len) { abAppend(&ab,"*",1);- i += utf8NextCharLen(l->buf, i, l->len);+ i += utf8NextCharLen(render, i, render_len); } } else {- abAppend(&ab,l->buf,l->len);+ abAppend(&ab,render,render_len); } /* Show hints if any. */- refreshShowHints(&ab,l,pwidth);+ refreshShowHints(&ab,l,pwidth,bufwidth); /* 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 &&+ render_pos == render_len && (poswidth+pwidth) % l->cols == 0) { lndebug("<newline>");@@ -1059,6 +1414,7 @@ if (write(fd,ab.b,ab.len) == -1) {} /* Can't recover from write error. */ abFree(&ab);+ free(render); } /* Calls the two low level functions refreshSingleLine() or@@ -1092,21 +1448,76 @@ } } +/* Grow the editing buffer if this state owns a growable buffer. Only the+ * blocking linenoise() API sets buflen_max: the multiplexing API still uses+ * the caller-provided fixed buffer. */+static int linenoiseEditGrow(struct linenoiseState *l, size_t needed) {+ size_t newlen;+ char *newbuf;++ if (needed <= l->buflen) return 0;++ /* buflen_max is zero when the caller provided a fixed buffer, as in the+ * multiplexing API: in that case there is nothing we can grow. */+ if (l->buflen_max == 0 || needed > l->buflen_max) return -1;++ /* Grow exponentially, but stop at the configured maximum before the+ * doubling would overflow or go past it. */+ newlen = l->buflen ? l->buflen : 16;+ while (newlen < needed) {+ if (newlen > l->buflen_max/2) {+ newlen = l->buflen_max;+ break;+ }+ newlen *= 2;+ }+ if (newlen < needed || newlen == SIZE_MAX) return -1;++ /* Allocate one extra byte for the nul terminator. */+ newbuf = realloc(l->buf,newlen+1);+ if (newbuf == NULL) return -1;+ l->buf = newbuf;+ l->buflen = newlen;+ return 0;+}++/* Insert bytes into l->buf without repainting the prompt. The paste path uses+ * this to first store the real pasted bytes, then mark their range as folded,+ * and only then refresh so raw pasted newlines are never printed directly. */+static int linenoiseEditInsertNoRefresh(struct linenoiseState *l, const char *c, size_t clen) {+ size_t insert_pos = l->pos;++ if (clen > SIZE_MAX-l->len || linenoiseEditGrow(l,l->len+clen) == -1)+ return -1;++ if (l->len == l->pos) {+ memcpy(l->buf+l->pos,c,clen);+ } else {+ memmove(l->buf+l->pos+clen,l->buf+l->pos,l->len-l->pos);+ memcpy(l->buf+l->pos,c,clen);+ }+ l->pos += clen;+ l->len += clen;+ l->buf[l->len] = '\0';+ linenoiseAdjustFoldsAfterInsert(l,insert_pos,clen);+ return 0;+}+ /* Insert the character(s) 'c' of length 'clen' at cursor current position. * This handles both single-byte ASCII and multi-byte UTF-8 sequences. * * On error writing to the terminal -1 is returned, otherwise 0. */ int linenoiseEditInsert(struct linenoiseState *l, const char *c, size_t clen) {- if (l->len + clen <= l->buflen) {- if (l->len == l->pos) {- /* Append at end of line. */- memcpy(l->buf+l->pos, c, clen);- l->pos += clen;- l->len += clen;- l->buf[l->len] = '\0';- if ((!mlmode &&- utf8StrWidth(l->prompt,l->plen)+utf8StrWidth(l->buf,l->len) < l->cols &&- !hintsCallback)) {+ if (l->len == l->pos) {+ int needs_refresh = memchr(c, '\n', clen) != NULL ||+ memchr(c, '\r', clen) != NULL;++ if (linenoiseEditInsertNoRefresh(l,c,clen) == -1) return 0;+ if (!needs_refresh && !mlmode && !hintsCallback &&+ (maskmode || l->fold_count == 0))+ {+ size_t bufwidth = utf8StrWidth(l->buf,l->len);+ if (utf8StrWidth(l->prompt,l->plen)+bufwidth < l->cols) { /* Avoid a full update of the line in the trivial case: * single-width char, no hints, fits in one line. */ if (maskmode == 1) {@@ -1114,18 +1525,13 @@ } else { if (write(l->ofd,c,clen) == -1) return -1; }- } else {- refreshLine(l);+ return 0; }- } else {- /* Insert in the middle of the line. */- memmove(l->buf+l->pos+clen, l->buf+l->pos, l->len-l->pos);- memcpy(l->buf+l->pos, c, clen);- l->len += clen;- l->pos += clen;- l->buf[l->len] = '\0';- refreshLine(l); }+ refreshLine(l);+ } else {+ if (linenoiseEditInsertNoRefresh(l,c,clen) == -1) return 0;+ refreshLine(l); } return 0; }@@ -1133,7 +1539,7 @@ /* Move cursor on the left. Moves by one UTF-8 character, not byte. */ void linenoiseEditMoveLeft(struct linenoiseState *l) { if (l->pos > 0) {- l->pos -= utf8PrevCharLen(l->buf, l->pos);+ l->pos -= linenoiseEditPrevLen(l, l->pos); refreshLine(l); } }@@ -1141,7 +1547,7 @@ /* Move cursor on the right. Moves by one UTF-8 character, not byte. */ void linenoiseEditMoveRight(struct linenoiseState *l) { if (l->pos != l->len) {- l->pos += utf8NextCharLen(l->buf, l->pos, l->len);+ l->pos += linenoiseEditNextLen(l, l->pos); refreshLine(l); } }@@ -1168,6 +1574,10 @@ #define LINENOISE_HISTORY_PREV 1 void linenoiseEditHistoryNext(struct linenoiseState *l, int dir) { if (history_len > 1) {+ const char *src;+ size_t len;+ struct linenoiseFold f;+ /* Update the current history entry before to * overwrite it with the next one. */ free(history[history_len - 1 - l->history_index]);@@ -1181,9 +1591,23 @@ 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);++ /* Copy the selected history entry into the edit buffer. With the+ * fixed-buffer API, truncate the entry if it does not fit. */+ src = history[history_len - 1 - l->history_index];+ len = strlen(src);+ if (linenoiseEditGrow(l,len) == -1 && len > l->buflen)+ len = l->buflen;+ memcpy(l->buf,src,len);+ l->buf[len] = '\0';+ l->len = l->pos = len;+ linenoiseFoldClear(l);++ /* History stores the real text, but not the original paste ranges.+ * If the recalled entry needs folding, create one display fold now+ * so text typed after recall remains outside the folded range. */+ if (linenoiseBuildHistoryFold(l,&f))+ linenoiseFoldAdd(l,f.start,f.end); refreshLine(l); } }@@ -1193,7 +1617,8 @@ * Now handles multi-byte UTF-8 characters. */ void linenoiseEditDelete(struct linenoiseState *l) { if (l->len > 0 && l->pos < l->len) {- size_t clen = utf8NextCharLen(l->buf, l->pos, l->len);+ size_t clen = linenoiseEditNextLen(l, l->pos);+ linenoiseAdjustFoldsAfterDelete(l,l->pos,clen); memmove(l->buf+l->pos, l->buf+l->pos+clen, l->len-l->pos-clen); l->len -= clen; l->buf[l->len] = '\0';@@ -1204,7 +1629,8 @@ /* Backspace implementation. Deletes the UTF-8 character before the cursor. */ void linenoiseEditBackspace(struct linenoiseState *l) { if (l->pos > 0 && l->len > 0) {- size_t clen = utf8PrevCharLen(l->buf, l->pos);+ size_t clen = linenoiseEditPrevLen(l, l->pos);+ linenoiseAdjustFoldsAfterDelete(l,l->pos-clen,clen); memmove(l->buf+l->pos-clen, l->buf+l->pos, l->len-l->pos); l->pos -= clen; l->len -= clen;@@ -1221,11 +1647,12 @@ /* Skip spaces before the word (move backwards by UTF-8 chars). */ while (l->pos > 0 && l->buf[l->pos-1] == ' ')- l->pos -= utf8PrevCharLen(l->buf, l->pos);+ l->pos -= linenoiseEditPrevLen(l, l->pos); /* Skip non-space characters (move backwards by UTF-8 chars). */ while (l->pos > 0 && l->buf[l->pos-1] != ' ')- l->pos -= utf8PrevCharLen(l->buf, l->pos);+ l->pos -= linenoiseEditPrevLen(l, l->pos); diff = old_pos - l->pos;+ linenoiseAdjustFoldsAfterDelete(l,l->pos,diff); memmove(l->buf+l->pos, l->buf+old_pos, l->len-old_pos+1); l->len -= diff; refreshLine(l);@@ -1263,12 +1690,15 @@ l->ofd = stdout_fd != -1 ? stdout_fd : STDOUT_FILENO; l->buf = buf; l->buflen = buflen;+ l->buflen_max = 0; l->prompt = prompt; l->plen = strlen(prompt); l->oldpos = l->pos = 0; l->len = 0;+ linenoiseFoldClear(l); /* Enter raw mode. */+ rawmode_output = l->ofd; if (enableRawMode(l->ifd) == -1) return -1; l->cols = getColumns(stdin_fd, stdout_fd);@@ -1293,6 +1723,135 @@ return 0; } +/* Make sure the temporary paste buffer can hold len+need bytes. Return -1 on+ * allocation failure or if the requested size is over PASTE_MAX_BYTES. */+static int pasteBufferReserve(char **buf, size_t *cap, size_t len, size_t need) {+ size_t want;+ char *nb;++ /* Nothing to do if the current paste buffer already has room for the+ * bytes collected so far plus the new bytes we want to append. */+ if (*cap >= len + need) return 0;++ /* Start small, then double like the line buffer. The cap avoids turning a+ * huge paste into an unbounded allocation attempt. */+ want = *cap ? *cap : 64;+ while (want < len + need) {+ size_t doubled = want*2;+ if (doubled <= want || doubled > PASTE_MAX_BYTES) {+ want = PASTE_MAX_BYTES;+ break;+ }+ want = doubled;+ }+ if (want < len + need) return -1;++ /* realloc(NULL, want) handles the first allocation too. */+ nb = realloc(*buf, want);+ if (nb == NULL) return -1;+ *buf = nb;+ *cap = want;+ return 0;+}++/* Append bytes to the temporary paste buffer, growing both it and l->buf as+ * needed. Return -1 if the paste is too large or allocation fails. */+static int pasteBufferAppend(struct linenoiseState *l, char **buf, size_t *cap,+ size_t *len, const char *s, size_t slen, size_t maxlen) {+ size_t needed;++ if (*len > maxlen || slen > maxlen-*len) return -1;+ if (*len > SIZE_MAX-slen) return -1;+ needed = *len+slen;+ if (l->len > SIZE_MAX-needed) return -1;+ if (linenoiseEditGrow(l,l->len+needed) == -1) return -1;+ if (pasteBufferReserve(buf,cap,*len,slen) == -1) return -1;+ memcpy(*buf+*len,s,slen);+ *len = needed;+ return 0;+}++/* Read a bracketed paste until ESC[201~ and insert the real bytes. If folding+ * is needed, remember the inserted range so only rendering is shortened. */+static void linenoiseEditPaste(struct linenoiseState *l) {+ static const char END[] = "\x1b[201~";+ const size_t ENDLEN = sizeof(END)-1;+ char *buf = NULL;+ size_t cap = 0, len = 0, match = 0;+ size_t maxlen = l->buflen_max ? l->buflen_max : l->buflen;+ int overflowed = 0;++ maxlen = maxlen > l->len ? maxlen - l->len : 0;+ if (maxlen > PASTE_MAX_BYTES) maxlen = PASTE_MAX_BYTES;+ /* Once all fold slots are used, consume later pastes without storing them. */+ if (l->fold_count == LINENOISE_MAX_FOLDS) maxlen = 0;++ while (1) {+ char c;+ if (read(l->ifd, &c, 1) != 1) break;++ /* Track a possible ESC[201~ terminator without copying it into the+ * paste. If it turns out to be ordinary input, flush the partial+ * match below. */+ if (c == END[match]) {+ match++;+ if (match == ENDLEN) break;+ continue;+ }++ if (match > 0) {+ if (!overflowed &&+ pasteBufferAppend(l,&buf,&cap,&len,END,match,maxlen) == -1)+ overflowed = 1;+ match = 0;+ if (c == END[0]) {+ match = 1;+ continue;+ }+ }++ if (!overflowed &&+ pasteBufferAppend(l,&buf,&cap,&len,&c,1,maxlen) == -1)+ overflowed = 1;+ }++ if (overflowed) {+ free(buf);+ linenoiseBeep();+ return;+ }+ if (buf == NULL) return;++ {+ /* Normalize pasted CR and CRLF to LF, so the edit buffer uses one+ * internal newline representation. */+ size_t r = 0, w = 0;+ while (r < len) {+ if (buf[r] == '\r') {+ buf[w++] = '\n';+ r += (r+1 < len && buf[r+1] == '\n') ? 2 : 1;+ } else {+ buf[w++] = buf[r++];+ }+ }+ len = w;+ }++ if (!maskmode && shouldFoldText(buf,len)) {+ size_t start = l->pos;+ if (linenoiseEditInsertNoRefresh(l,buf,len) == -1) {+ free(buf);+ linenoiseBeep();+ return;+ }+ linenoiseFoldAdd(l,start,start+len);+ refreshLine(l);+ } else {+ linenoiseEditInsert(l,buf,len);+ }+ free(buf);+}+ char *linenoiseEditMore = "If you see this, you are misusing the API: when linenoiseEditFeed() is called, if it returns linenoiseEditMore the user is yet editing the line. See the README file for more information."; /* This function is part of the multiplexed API of linenoise, see the top@@ -1329,15 +1888,14 @@ return NULL; } - /* 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 ((l->in_completion || c == 9) && completionCallback != NULL) {- c = completeLine(l,c);- /* Return on errors */- if (c < 0) return NULL;+ /* Only autocomplete when the callback is set. completeLine()+ * returns the character to be handled next, or zero when the+ * key was consumed to navigate completions. */+ if ((l->in_completion || c == 9 /* TAB */) && completionCallback != NULL) {+ int retval = completeLine(l,c); /* Read next character when 0 */- if (c == 0) return linenoiseEditMore;+ if (retval == 0) return linenoiseEditMore;+ c = retval; } switch(c) {@@ -1376,9 +1934,14 @@ /* Handle UTF-8: swap the two UTF-8 characters around cursor. */ if (l->pos > 0 && l->pos < l->len) { char tmp[32];- size_t prevlen = utf8PrevCharLen(l->buf, l->pos);- size_t currlen = utf8NextCharLen(l->buf, l->pos, l->len);+ size_t prevlen = linenoiseEditPrevLen(l, l->pos);+ size_t currlen = linenoiseEditNextLen(l, l->pos); size_t prevstart = l->pos - prevlen;+ if (prevlen > sizeof(tmp) || currlen > sizeof(tmp)) break;+ if (linenoiseRangeOverlapsFold(l,prevstart,prevlen+currlen)) {+ linenoiseBeep();+ break;+ } /* Copy current char to tmp, move previous char right, paste tmp. */ memcpy(tmp, l->buf + l->pos, currlen); memmove(l->buf + prevstart + currlen, l->buf + prevstart, prevlen);@@ -1409,15 +1972,28 @@ /* 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);+ char param[8];+ size_t plen = 1;+ char final = 0;++ param[0] = seq[1];+ while (plen < sizeof(param)) {+ char p;+ if (read(l->ifd,&p,1) != 1) break;+ if (p >= '0' && p <= '9') {+ param[plen++] = p;+ } else {+ final = p; break; } }+ if (final == '~') {+ if (plen == 1 && param[0] == '3') {+ linenoiseEditDelete(l);+ } else if (plen == 3 && memcmp(param,"200",3) == 0) {+ linenoiseEditPaste(l);+ }+ } } else { switch(seq[1]) { case 'A': /* Up */@@ -1475,9 +2051,11 @@ case CTRL_U: /* Ctrl+u, delete the whole line. */ l->buf[0] = '\0'; l->pos = l->len = 0;+ linenoiseFoldClear(l); refreshLine(l); break; case CTRL_K: /* Ctrl+k, delete from current to end of line. */+ linenoiseAdjustFoldsAfterDelete(l,l->pos,l->len-l->pos); l->buf[l->pos] = '\0'; l->len = l->pos; refreshLine(l);@@ -1512,21 +2090,29 @@ /* This just implements a blocking loop for the multiplexed API. * In many applications that are not event-drivern, we can just call * the blocking linenoise API, wait for the user to complete the editing- * and return the buffer. */-static char *linenoiseBlockingEdit(int stdin_fd, int stdout_fd, char *buf, size_t buflen, const char *prompt)+ * and return the buffer. This wrapper owns l.buf, so it can let the edit+ * state grow it dynamically for large pasted input. */+static char *linenoiseBlockingEdit(int stdin_fd, int stdout_fd, const char *prompt) { struct linenoiseState l;+ char *buf = malloc(LINENOISE_INITIAL_BUFLEN);+ char *res; - /* Editing without a buffer is invalid. */- if (buflen == 0) {- errno = EINVAL;+ if (buf == NULL) {+ errno = ENOMEM; return NULL; } - linenoiseEditStart(&l,stdin_fd,stdout_fd,buf,buflen,prompt);- char *res;+ if (linenoiseEditStart(&l,stdin_fd,stdout_fd,buf,+ LINENOISE_INITIAL_BUFLEN,prompt) == -1)+ {+ free(buf);+ return NULL;+ }+ l.buflen_max = LINENOISE_MAX_LINE; while((res = linenoiseEditFeed(&l)) == linenoiseEditMore); linenoiseEditStop(&l);+ free(l.buf); return res; } @@ -1538,6 +2124,7 @@ printf("Linenoise key codes debugging mode.\n" "Press keys to see scan codes. Type 'quit' at any time to exit.\n");+ rawmode_output = STDOUT_FILENO; if (enableRawMode(STDIN_FILENO) == -1) return; memset(quit,' ',4); while(1) {@@ -1558,27 +2145,33 @@ disableRawMode(STDIN_FILENO); } -/* 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) {+/* Read a newline-terminated record from fp with no fixed-size stack buffer.+ * Used for non-tty input, unsupported terminals, and history loading. */+static char *linenoiseReadLine(FILE *fp, int *err) { char *line = NULL;- size_t len = 0, maxlen = 0;+ size_t len = 0, cap = 0; + if (err) *err = 0;+ while(1) {- if (len == maxlen) {- if (maxlen == 0) maxlen = 16;- maxlen *= 2;+ if (len+1 >= cap) {+ size_t newcap = cap ? cap*2 : 16; char *oldval = line;- line = realloc(line,maxlen);+ if (newcap <= cap) {+ free(line);+ if (err) *err = 1;+ errno = ENOMEM;+ return NULL;+ }+ line = realloc(line,newcap); if (line == NULL) { if (oldval) free(oldval);+ if (err) *err = 1; return NULL; }+ cap = newcap; }- int c = fgetc(stdin);+ int c = fgetc(fp); if (c == EOF || c == '\n') { if (c == EOF && len == 0) { free(line);@@ -1594,33 +2187,43 @@ } } +/* 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. */+static char *linenoiseNoTTY(void) {+ return linenoiseReadLine(stdin,NULL);+}+ /* 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. */+ * editing function or uses a simple line reader 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];- if (!isatty(STDIN_FILENO) && !getenv("LINENOISE_ASSUME_TTY")) { /* 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()) {+ }++ if (isUnsupportedTerm()) {+ char *retval; 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')) {+ retval = linenoiseNoTTY();+ if (retval == NULL) return NULL;+ len = strlen(retval);+ while(len && retval[len-1] == '\r') { len--;- buf[len] = '\0';+ retval[len] = '\0'; }- return strdup(buf);- } else {- char *retval = linenoiseBlockingEdit(STDIN_FILENO,STDOUT_FILENO,buf,LINENOISE_MAX_LINE,prompt); return retval;+ } else {+ return linenoiseBlockingEdit(STDIN_FILENO,STDOUT_FILENO,prompt); } } @@ -1732,8 +2335,16 @@ umask(old_umask); if (fp == NULL) return -1; fchmod(fileno(fp),S_IRUSR|S_IWUSR);- for (j = 0; j < history_len; j++)- fprintf(fp,"%s\n",history[j]);+ for (j = 0; j < history_len; j++) {+ char *p = history[j];+ /* Keep the history file newline-separated: embedded newlines in an+ * entry are stored as CR and converted back by linenoiseHistoryLoad(). */+ while (*p) {+ fputc(*p == '\n' ? '\r' : *p, fp);+ p++;+ }+ fputc('\n', fp);+ } fclose(fp); return 0; }@@ -1745,17 +2356,24 @@ * on error -1 is returned. */ int linenoiseHistoryLoad(const char *filename) { FILE *fp = fopen(filename,"r");- char buf[LINENOISE_MAX_LINE];+ char *buf;+ int err = 0; if (fp == NULL) return -1; - while (fgets(buf,LINENOISE_MAX_LINE,fp) != NULL) {- char *p;+ while ((buf = linenoiseReadLine(fp,&err)) != NULL) {+ size_t j; - p = strchr(buf,'\r');- if (!p) p = strchr(buf,'\n');- if (p) *p = '\0';+ /* Rebuild embedded newlines that were saved as CR. */+ for (j = 0; buf[j]; j++) {+ if (buf[j] == '\r') buf[j] = '\n';+ } linenoiseHistoryAdd(buf);+ free(buf);+ }+ if (err || ferror(fp)) {+ fclose(fp);+ return -1; } fclose(fp); return 0;
cbits/linenoise.h view
@@ -47,6 +47,8 @@ extern char *linenoiseEditMore; +#define LINENOISE_MAX_FOLDS 16+ /* The linenoiseState structure represents the state during line editing. * We pass this state to functions implementing specific editing * functionalities. */@@ -58,6 +60,7 @@ int ofd; /* Terminal stdout file descriptor. */ char *buf; /* Edited line buffer. */ size_t buflen; /* Edited line buffer size. */+ size_t buflen_max; /* Max buffer size, or 0 if fixed. */ const char *prompt; /* Prompt to display. */ size_t plen; /* Prompt length. */ size_t pos; /* Current cursor position. */@@ -67,6 +70,9 @@ size_t oldrows; /* Rows used by last refrehsed line (multiline mode) */ int oldrpos; /* Cursor row from last refresh (for multiline clearing). */ int history_index; /* The history index we are currently editing. */+ int fold_count; /* Number of folded ranges. */+ size_t fold_start[LINENOISE_MAX_FOLDS]; /* Folded range start offsets. */+ size_t fold_end[LINENOISE_MAX_FOLDS]; /* Folded range end offsets. */ }; typedef struct linenoiseCompletions {
linenoise.cabal view
@@ -7,7 +7,7 @@ -- hash: 42a9541c6a153888178292a118515374ece2c2851a61148bcec2105c19708638 name: linenoise-version: 0.6.0+version: 0.7.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
src/Linenoise.hs view
@@ -1,12 +1,17 @@ -- | Re-exports of all public entities. module Linenoise- ( InputResult (..)+ ( EditResult (..)+ , EditSession+ , InputResult (..) , ReplDirective (..) , ReplT (..) , addHistory , byWord , clearScreen+ , feedEditSession+ , freeEditSession , getInputLine+ , hideEditSession , historyLoad , historySave , printKeycodes@@ -14,21 +19,36 @@ , runReplT , setCompletion , setMultiline+ , showEditSession+ , startEditSession+ , stopEditSession , stifleHistory+ , withEditSession+ , withHiddenEditSession ) where import Linenoise.Completion (byWord) import Linenoise.Repl (ReplDirective (..), ReplT (..), replM, runReplT) import Linenoise.Unlift- ( InputResult (..)+ ( EditResult (..)+ , EditSession+ , InputResult (..) , addHistory , clearScreen+ , feedEditSession+ , freeEditSession , getInputLine+ , hideEditSession , historyLoad , historySave , printKeycodes , setCompletion , setMultiline+ , showEditSession+ , startEditSession , stifleHistory+ , stopEditSession+ , withEditSession+ , withHiddenEditSession )
src/Linenoise/FFI.hs view
@@ -3,32 +3,81 @@ -- | Just an FFI layer over the C library. module Linenoise.FFI- ( InputResult (..)+ ( EditResult (..)+ , EditSession+ , InputResult (..) , addHistory , clearScreen+ , feedEditSession+ , freeEditSession , getInputLine+ , hideEditSession , historyLoad , historySave , printKeycodes , setCompletion , setMultiline+ , showEditSession+ , startEditSession+ , stopEditSession , stifleHistory+ , withEditSession+ , withHiddenEditSession ) where +import Control.Exception (bracket, bracket_) import Control.Monad (unless) import Data.ByteString (ByteString) import qualified Data.ByteString as BS import qualified Data.ByteString.Unsafe as BSU import Data.Foldable (for_)-import Foreign (FunPtr, Ptr, Storable (..), fromBool, maybePeek)-import Foreign.C.Error (eAGAIN, getErrno, resetErrno)+import Data.IORef (IORef, newIORef, readIORef, writeIORef)+import Foreign (FunPtr, Ptr, Storable (..), fromBool, nullPtr)+import Foreign.C.Error (eAGAIN, eNOENT, getErrno, resetErrno) import Foreign.C.String (CString, newCString)-import Foreign.C.Types (CChar, CInt (..), CSize)+import Foreign.C.Types (CChar, CInt (..), CSize (..))+import Foreign.Marshal.Alloc (free, mallocBytes)+import Foreign.Marshal.Utils (copyBytes) +-- | Opaque C @struct linenoiseState@ pointer target.+data LinenoiseState++-- | Active non-blocking linenoise edit session.+--+-- The session owns the C edit state, input buffer, and copied prompt. Prefer+-- 'withEditSession' so raw terminal mode is stopped and memory is released.+data EditSession = EditSession+ { editSessionState :: !(Ptr LinenoiseState)+ , editSessionBuffer :: !(Ptr CChar)+ , editSessionPrompt :: !CString+ , editSessionStopped :: !(IORef Bool)+ }+ foreign import ccall "linenoise.h linenoise" linenoise :: CString -> IO CString +foreign import ccall "linenoise.h linenoiseFree"+ linenoiseFree :: CString -> IO ()++foreign import ccall "linenoise.h linenoiseEditStart"+ linenoiseEditStart :: Ptr LinenoiseState -> CInt -> CInt -> Ptr CChar -> CSize -> CString -> IO CInt++foreign import ccall "linenoise.h linenoiseEditFeed"+ linenoiseEditFeed :: Ptr LinenoiseState -> IO CString++foreign import ccall "linenoise.h linenoiseEditStop"+ linenoiseEditStop :: Ptr LinenoiseState -> IO ()++foreign import ccall "linenoise.h linenoiseHide"+ linenoiseHide :: Ptr LinenoiseState -> IO ()++foreign import ccall "linenoise.h linenoiseShow"+ linenoiseShow :: Ptr LinenoiseState -> IO ()++foreign import ccall "linenoise.h &linenoiseEditMore"+ linenoiseEditMorePtr :: Ptr CString+ foreign import ccall "linenoise.h linenoiseHistoryAdd" linenoiseHistoryAdd :: Ptr CChar -> IO CInt @@ -92,17 +141,127 @@ | LineResult !a -- Possibly empty line. deriving (Eq, Show, Functor, Foldable, Traversable) +-- | Result of feeding one input event to a non-blocking edit session.+data EditResult a+ = -- | Editing is still in progress; feed more input.+ MoreResult+ | -- | Editing finished with interrupt, EOF, or a completed line.+ DoneResult !(InputResult a)+ 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)+ ptr <- BSU.unsafeUseAsCString prompt linenoise+ if ptr == nullPtr+ then do+ errno <- getErrno+ if errno == eAGAIN+ then resetErrno >> pure InterruptResult+ else pure EofResult+ else do+ line <- BS.packCString ptr+ linenoiseFree ptr+ pure (LineResult line) +-- | Start a non-blocking edit session with the given prompt.+--+-- The returned session must eventually be passed to 'stopEditSession' and+-- 'freeEditSession'. Prefer 'withEditSession' for scoped use.+startEditSession :: ByteString -> IO EditSession+startEditSession prompt = do+ state <- mallocBytes linenoiseStateBytes+ buffer <- mallocBytes editBufferBytes+ prompt' <- copyByteString prompt+ stopped <- newIORef False+ result <- linenoiseEditStart state (-1) (-1) buffer (fromIntegral editBufferBytes) prompt'+ if result == -1+ then do+ free state+ free buffer+ free prompt'+ fail "linenoiseEditStart failed"+ else+ pure+ EditSession+ { editSessionState = state+ , editSessionBuffer = buffer+ , editSessionPrompt = prompt'+ , editSessionStopped = stopped+ }++-- | Stop editing and restore normal terminal mode.+--+-- This operation is idempotent for a given 'EditSession'.+stopEditSession :: EditSession -> IO ()+stopEditSession EditSession {editSessionState = state, editSessionStopped = stopped} = do+ alreadyStopped <- readIORef stopped+ unless alreadyStopped $ do+ linenoiseEditStop state+ writeIORef stopped True++-- | Free memory owned by an edit session.+--+-- Call this only after 'stopEditSession'. Prefer 'withEditSession'.+freeEditSession :: EditSession -> IO ()+freeEditSession EditSession {editSessionState = state, editSessionBuffer = buffer, editSessionPrompt = prompt} = do+ free state+ free buffer+ free prompt++-- | Run an action with a managed non-blocking edit session.+--+-- The session is stopped and freed when the action exits, including on+-- exceptions.+withEditSession :: ByteString -> (EditSession -> IO a) -> IO a+withEditSession prompt = bracket (startEditSession prompt) release+ where+ release session = stopEditSession session >> freeEditSession session++-- | Feed one input event to a non-blocking edit session.+--+-- Returns 'MoreResult' until the user completes a line, interrupts, or sends+-- EOF.+feedEditSession :: EditSession -> IO (EditResult ByteString)+feedEditSession EditSession {editSessionState = state} = do+ ptr <- linenoiseEditFeed state+ more <- peek linenoiseEditMorePtr+ if ptr == more+ then pure MoreResult+ else+ if ptr == nullPtr+ then do+ errno <- getErrno+ if errno == eAGAIN+ then resetErrno >> pure (DoneResult InterruptResult)+ else+ if errno == eNOENT+ then resetErrno >> pure (DoneResult EofResult)+ else pure (DoneResult EofResult)+ else do+ line <- BS.packCString ptr+ linenoiseFree ptr+ pure (DoneResult (LineResult line))++-- | Hide the current prompt and edit buffer.+--+-- Use this before writing asynchronous output to the terminal.+hideEditSession :: EditSession -> IO ()+hideEditSession = linenoiseHide . editSessionState++-- | Redraw the current prompt and edit buffer.+--+-- Use this after writing asynchronous output to the terminal.+showEditSession :: EditSession -> IO ()+showEditSession = linenoiseShow . editSessionState++-- | Run an action while the current prompt and edit buffer are hidden.+--+-- This is the primitive for prompt-safe asynchronous terminal output. Callers+-- that can write concurrently should still serialize access with a lock.+withHiddenEditSession :: EditSession -> IO a -> IO a+withHiddenEditSession session = bracket_ (hideEditSession session) (showEditSession session)+ -- | Add to current history. addHistory :: ByteString -> IO () addHistory bs =@@ -145,3 +304,17 @@ setCompletion f = do cb <- makeFunPtr (makeCompletion f) linenoiseSetCompletionCallback cb++copyByteString :: ByteString -> IO CString+copyByteString bs =+ BSU.unsafeUseAsCStringLen bs $ \(ptr, len) -> do+ out <- mallocBytes (len + 1)+ copyBytes out ptr len+ pokeByteOff out len (0 :: CChar)+ pure out++linenoiseStateBytes :: Int+linenoiseStateBytes = 4096++editBufferBytes :: Int+editBufferBytes = 1024 * 1024 + 1
src/Linenoise/Unlift.hs view
@@ -1,15 +1,25 @@ -- | Unlifted versions of all FFI methods. module Linenoise.Unlift- ( InputResult (..)+ ( EditResult (..)+ , EditSession+ , InputResult (..) , addHistory , clearScreen+ , feedEditSession+ , freeEditSession , getInputLine+ , hideEditSession , historyLoad , historySave , printKeycodes , setCompletion , setMultiline+ , showEditSession+ , startEditSession+ , stopEditSession , stifleHistory+ , withEditSession+ , withHiddenEditSession ) where @@ -17,7 +27,7 @@ import Control.Monad.IO.Unlift (MonadUnliftIO, withRunInIO) import Data.Text (Text) import Data.Text.Encoding (decodeUtf8, encodeUtf8)-import Linenoise.FFI (InputResult (..))+import Linenoise.FFI (EditResult (..), EditSession, InputResult (..)) import qualified Linenoise.FFI as FFI -- | Add to current history.@@ -31,6 +41,60 @@ -- | Run the prompt, yielding a string. getInputLine :: (MonadIO m) => Text -> m (InputResult Text) getInputLine = liftIO . fmap (fmap decodeUtf8) . FFI.getInputLine . encodeUtf8++-- | Start a non-blocking edit session with the given prompt.+--+-- The returned session must eventually be passed to 'stopEditSession' and+-- 'freeEditSession'. Prefer 'withEditSession' for scoped use.+startEditSession :: (MonadIO m) => Text -> m EditSession+startEditSession = liftIO . FFI.startEditSession . encodeUtf8++-- | Stop editing and restore normal terminal mode.+--+-- This operation is idempotent for a given 'EditSession'.+stopEditSession :: (MonadIO m) => EditSession -> m ()+stopEditSession = liftIO . FFI.stopEditSession++-- | Free memory owned by an edit session.+--+-- Call this only after 'stopEditSession'. Prefer 'withEditSession'.+freeEditSession :: (MonadIO m) => EditSession -> m ()+freeEditSession = liftIO . FFI.freeEditSession++-- | Run an action with a managed non-blocking edit session.+--+-- The session is stopped and freed when the action exits, including on+-- exceptions.+withEditSession :: (MonadUnliftIO m) => Text -> (EditSession -> m a) -> m a+withEditSession prompt action =+ withRunInIO (\runInIO -> FFI.withEditSession (encodeUtf8 prompt) (runInIO . action))++-- | Feed one input event to a non-blocking edit session.+--+-- Returns 'MoreResult' until the user completes a line, interrupts, or sends+-- EOF.+feedEditSession :: (MonadIO m) => EditSession -> m (EditResult Text)+feedEditSession = liftIO . fmap (fmap decodeUtf8) . FFI.feedEditSession++-- | Hide the current prompt and edit buffer.+--+-- Use this before writing asynchronous output to the terminal.+hideEditSession :: (MonadIO m) => EditSession -> m ()+hideEditSession = liftIO . FFI.hideEditSession++-- | Redraw the current prompt and edit buffer.+--+-- Use this after writing asynchronous output to the terminal.+showEditSession :: (MonadIO m) => EditSession -> m ()+showEditSession = liftIO . FFI.showEditSession++-- | Run an action while the current prompt and edit buffer are hidden.+--+-- This is the primitive for prompt-safe asynchronous terminal output. Callers+-- that can write concurrently should still serialize access with a lock.+withHiddenEditSession :: (MonadUnliftIO m) => EditSession -> m a -> m a+withHiddenEditSession session action =+ withRunInIO (\runInIO -> FFI.withHiddenEditSession session (runInIO action)) -- | Load history from a file. historyLoad :: (MonadIO m) => FilePath -> m ()