packages feed

linenoise 0.4.2 → 0.5.0

raw patch · 3 files changed

+495/−81 lines, 3 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Linenoise: ReplT :: ReaderT r (ReaderT (IORef s) m) a -> ReplT r s m a
+ Linenoise: ReplT :: ReaderT r (ReaderT (IORef s) m) a -> ReplT r s (m :: Type -> Type) a
- Linenoise: [unReplT] :: ReplT r s m a -> ReaderT r (ReaderT (IORef s) m) a
+ Linenoise: [unReplT] :: ReplT r s (m :: Type -> Type) a -> ReaderT r (ReaderT (IORef s) m) a
- Linenoise: newtype ReplT r s m a
+ Linenoise: newtype ReplT r s (m :: Type -> Type) a
- Linenoise.Repl: ReplT :: ReaderT r (ReaderT (IORef s) m) a -> ReplT r s m a
+ Linenoise.Repl: ReplT :: ReaderT r (ReaderT (IORef s) m) a -> ReplT r s (m :: Type -> Type) a
- Linenoise.Repl: [unReplT] :: ReplT r s m a -> ReaderT r (ReaderT (IORef s) m) a
+ Linenoise.Repl: [unReplT] :: ReplT r s (m :: Type -> Type) a -> ReaderT r (ReaderT (IORef s) m) a
- Linenoise.Repl: newtype ReplT r s m a
+ Linenoise.Repl: newtype ReplT r s (m :: Type -> Type) a

Files

cbits/linenoise.c view
@@ -115,6 +115,7 @@ #include <sys/types.h> #include <sys/ioctl.h> #include <unistd.h>+#include <stdint.h> #include "linenoise.h"  #define LINENOISE_DEFAULT_HISTORY_MAX_LEN 100@@ -136,6 +137,315 @@ static int history_len = 0; static char **history = NULL; +/* =========================== UTF-8 support ================================ */++/* Return the number of bytes that compose the UTF-8 character starting at+ * 'c'. This function assumes a valid UTF-8 encoding and handles the four+ * standard byte patterns:+ *   0xxxxxxx -> 1 byte (ASCII)+ *   110xxxxx -> 2 bytes+ *   1110xxxx -> 3 bytes+ *   11110xxx -> 4 bytes */+static int utf8ByteLen(char c) {+    unsigned char uc = (unsigned char)c;+    if ((uc & 0x80) == 0)    return 1;   /* 0xxxxxxx: ASCII */+    if ((uc & 0xE0) == 0xC0) return 2;   /* 110xxxxx: 2-byte seq */+    if ((uc & 0xF0) == 0xE0) return 3;   /* 1110xxxx: 3-byte seq */+    if ((uc & 0xF8) == 0xF0) return 4;   /* 11110xxx: 4-byte seq */+    return 1; /* Fallback for invalid encoding, treat as single byte. */+}++/* Decode a UTF-8 sequence starting at 's' into a Unicode codepoint.+ * Returns the codepoint value. Assumes valid UTF-8 encoding. */+static uint32_t utf8DecodeChar(const char *s, size_t *len) {+    unsigned char *p = (unsigned char *)s;+    uint32_t cp;++    if ((*p & 0x80) == 0) {+        *len = 1;+        return *p;+    } else if ((*p & 0xE0) == 0xC0) {+        *len = 2;+        cp = (*p & 0x1F) << 6;+        cp |= (p[1] & 0x3F);+        return cp;+    } else if ((*p & 0xF0) == 0xE0) {+        *len = 3;+        cp = (*p & 0x0F) << 12;+        cp |= (p[1] & 0x3F) << 6;+        cp |= (p[2] & 0x3F);+        return cp;+    } else if ((*p & 0xF8) == 0xF0) {+        *len = 4;+        cp = (*p & 0x07) << 18;+        cp |= (p[1] & 0x3F) << 12;+        cp |= (p[2] & 0x3F) << 6;+        cp |= (p[3] & 0x3F);+        return cp;+    }+    *len = 1;+    return *p; /* Fallback for invalid sequences. */+}++/* Check if codepoint is a variation selector (emoji style modifiers). */+static int isVariationSelector(uint32_t cp) {+    return cp == 0xFE0E || cp == 0xFE0F;  /* Text/emoji style */+}++/* Check if codepoint is a skin tone modifier. */+static int isSkinToneModifier(uint32_t cp) {+    return cp >= 0x1F3FB && cp <= 0x1F3FF;+}++/* Check if codepoint is Zero Width Joiner. */+static int isZWJ(uint32_t cp) {+    return cp == 0x200D;+}++/* Check if codepoint is a Regional Indicator (for flag emoji). */+static int isRegionalIndicator(uint32_t cp) {+    return cp >= 0x1F1E6 && cp <= 0x1F1FF;+}++/* Check if codepoint is a combining mark or other zero-width character. */+static int isCombiningMark(uint32_t cp) {+    return (cp >= 0x0300 && cp <= 0x036F) ||   /* Combining Diacriticals */+           (cp >= 0x1AB0 && cp <= 0x1AFF) ||   /* Combining Diacriticals Extended */+           (cp >= 0x1DC0 && cp <= 0x1DFF) ||   /* Combining Diacriticals Supplement */+           (cp >= 0x20D0 && cp <= 0x20FF) ||   /* Combining Diacriticals for Symbols */+           (cp >= 0xFE20 && cp <= 0xFE2F);     /* Combining Half Marks */+}++/* Check if codepoint extends the previous character (doesn't start a new grapheme). */+static int isGraphemeExtend(uint32_t cp) {+    return isVariationSelector(cp) || isSkinToneModifier(cp) ||+           isZWJ(cp) || isCombiningMark(cp);+}++/* Decode the UTF-8 codepoint ending at position 'pos' (exclusive) and+ * return its value. Also sets *cplen to the byte length of the codepoint. */+static uint32_t utf8DecodePrev(const char *buf, size_t pos, size_t *cplen) {+    if (pos == 0) {+        *cplen = 0;+        return 0;+    }+    /* Scan backwards to find the start byte. */+    size_t i = pos;+    do {+        i--;+    } while (i > 0 && (pos - i) < 4 && ((unsigned char)buf[i] & 0xC0) == 0x80);+    *cplen = pos - i;+    size_t dummy;+    return utf8DecodeChar(buf + i, &dummy);+}++/* Given a buffer and a position, return the byte length of the grapheme+ * cluster before that position. A grapheme cluster includes:+ * - The base character+ * - Any following variation selectors, skin tone modifiers+ * - ZWJ sequences (emoji joined by Zero Width Joiner)+ * - Regional indicator pairs (flag emoji) */+static size_t utf8PrevCharLen(const char *buf, size_t pos) {+    if (pos == 0) return 0;++    size_t total = 0;+    size_t curpos = pos;++    /* First, get the last codepoint. */+    size_t cplen;+    uint32_t cp = utf8DecodePrev(buf, curpos, &cplen);+    if (cplen == 0) return 0;+    total += cplen;+    curpos -= cplen;++    /* If we're at an extending character, we need to find what it extends.+     * Keep going back through the grapheme cluster. */+    while (curpos > 0) {+        size_t prevlen;+        uint32_t prevcp = utf8DecodePrev(buf, curpos, &prevlen);+        if (prevlen == 0) break;++        if (isZWJ(prevcp)) {+            /* ZWJ joins two emoji. Include the ZWJ and continue to get+             * the preceding character. */+            total += prevlen;+            curpos -= prevlen;+            /* Now get the character before ZWJ. */+            prevcp = utf8DecodePrev(buf, curpos, &prevlen);+            if (prevlen == 0) break;+            total += prevlen;+            curpos -= prevlen;+            cp = prevcp;+            continue;  /* Check if there's more extending before this. */+        } else if (isGraphemeExtend(cp)) {+            /* Current cp is an extending character; include previous. */+            total += prevlen;+            curpos -= prevlen;+            cp = prevcp;+            continue;+        } else if (isRegionalIndicator(cp) && isRegionalIndicator(prevcp)) {+            /* Two regional indicators form a flag. But we need to be careful:+             * flags are always pairs, so only join if we're at an even boundary.+             * For simplicity, just join one pair. */+            total += prevlen;+            curpos -= prevlen;+            break;+        } else {+            /* No more extending; we've found the start of the cluster. */+            break;+        }+    }++    return total;+}++/* Given a buffer, position and total length, return the byte length of the+ * grapheme cluster at the current position. */+static size_t utf8NextCharLen(const char *buf, size_t pos, size_t len) {+    if (pos >= len) return 0;++    size_t total = 0;+    size_t curpos = pos;++    /* Get the first codepoint. */+    size_t cplen;+    uint32_t cp = utf8DecodeChar(buf + curpos, &cplen);+    total += cplen;+    curpos += cplen;++    int isRI = isRegionalIndicator(cp);++    /* Consume any extending characters that follow. */+    while (curpos < len) {+        size_t nextlen;+        uint32_t nextcp = utf8DecodeChar(buf + curpos, &nextlen);++        if (isZWJ(nextcp) && curpos + nextlen < len) {+            /* ZWJ: include it and the following character. */+            total += nextlen;+            curpos += nextlen;+            /* Get the character after ZWJ. */+            nextcp = utf8DecodeChar(buf + curpos, &nextlen);+            total += nextlen;+            curpos += nextlen;+            continue;  /* Check for more extending after the joined char. */+        } else if (isGraphemeExtend(nextcp)) {+            /* Variation selector, skin tone, combining mark, etc. */+            total += nextlen;+            curpos += nextlen;+            continue;+        } else if (isRI && isRegionalIndicator(nextcp)) {+            /* Second regional indicator for a flag pair. */+            total += nextlen;+            curpos += nextlen;+            isRI = 0;  /* Only pair once. */+            continue;+        } else {+            break;+        }+    }++    return total;+}++/* Return the display width of a Unicode codepoint. This is a heuristic+ * that works for most common cases:+ * - Control chars and zero-width: 0 columns+ * - Grapheme-extending chars (VS, skin tone, ZWJ): 0 columns+ * - ASCII printable: 1 column+ * - Wide chars (CJK, emoji, fullwidth): 2 columns+ * - Everything else: 1 column+ *+ * This is not a full wcwidth() implementation, but a minimal heuristic+ * that handles emoji and CJK characters reasonably well. */+static int utf8CharWidth(uint32_t cp) {+    /* Control characters and combining marks: zero width. */+    if (cp < 32 || (cp >= 0x7F && cp < 0xA0)) return 0;+    if (isCombiningMark(cp)) return 0;++    /* Grapheme-extending characters: zero width.+     * These modify the preceding character rather than taking space. */+    if (isVariationSelector(cp)) return 0;+    if (isSkinToneModifier(cp)) return 0;+    if (isZWJ(cp)) return 0;++    /* Wide character ranges - these display as 2 columns:+     * - CJK Unified Ideographs and Extensions+     * - Fullwidth forms+     * - Various emoji ranges */+    if (cp >= 0x1100 &&+        (cp <= 0x115F ||                      /* Hangul Jamo */+         cp == 0x2329 || cp == 0x232A ||      /* Angle brackets */+         (cp >= 0x231A && cp <= 0x231B) ||    /* Watch, Hourglass */+         (cp >= 0x23E9 && cp <= 0x23F3) ||    /* Various symbols */+         (cp >= 0x23F8 && cp <= 0x23FA) ||    /* Various symbols */+         (cp >= 0x25AA && cp <= 0x25AB) ||    /* Small squares */+         (cp >= 0x25B6 && cp <= 0x25C0) ||    /* Play/reverse buttons */+         (cp >= 0x25FB && cp <= 0x25FE) ||    /* Squares */+         (cp >= 0x2600 && cp <= 0x26FF) ||    /* Misc Symbols (sun, cloud, etc) */+         (cp >= 0x2700 && cp <= 0x27BF) ||    /* Dingbats (❤, ✂, etc) */+         (cp >= 0x2934 && cp <= 0x2935) ||    /* Arrows */+         (cp >= 0x2B05 && cp <= 0x2B07) ||    /* Arrows */+         (cp >= 0x2B1B && cp <= 0x2B1C) ||    /* Squares */+         cp == 0x2B50 || cp == 0x2B55 ||      /* Star, circle */+         (cp >= 0x2E80 && cp <= 0xA4CF &&+          cp != 0x303F) ||                    /* CJK ... Yi */+         (cp >= 0xAC00 && cp <= 0xD7A3) ||    /* Hangul Syllables */+         (cp >= 0xF900 && cp <= 0xFAFF) ||    /* CJK Compatibility Ideographs */+         (cp >= 0xFE10 && cp <= 0xFE1F) ||    /* Vertical forms */+         (cp >= 0xFE30 && cp <= 0xFE6F) ||    /* CJK Compatibility Forms */+         (cp >= 0xFF00 && cp <= 0xFF60) ||    /* Fullwidth Forms */+         (cp >= 0xFFE0 && cp <= 0xFFE6) ||    /* Fullwidth Signs */+         (cp >= 0x1F1E6 && cp <= 0x1F1FF) ||  /* Regional Indicators (flags) */+         (cp >= 0x1F300 && cp <= 0x1F64F) ||  /* Misc Symbols and Emoticons */+         (cp >= 0x1F680 && cp <= 0x1F6FF) ||  /* Transport and Map Symbols */+         (cp >= 0x1F900 && cp <= 0x1F9FF) ||  /* Supplemental Symbols */+         (cp >= 0x1FA00 && cp <= 0x1FAFF) ||  /* Chess, Extended-A */+         (cp >= 0x20000 && cp <= 0x2FFFF)))   /* CJK Extension B and beyond */+        return 2;++    return 1; /* Default: single width */+}++/* 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. */+static size_t utf8StrWidth(const char *s, size_t len) {+    size_t width = 0;+    size_t i = 0;+    int after_zwj = 0;  /* Track if previous char was ZWJ */++    while (i < len) {+        size_t clen;+        uint32_t cp = utf8DecodeChar(s + i, &clen);++        if (after_zwj) {+            /* Character after ZWJ: don't add width, it's joined.+             * But do check for extending chars after it. */+            after_zwj = 0;+        } else {+            width += utf8CharWidth(cp);+        }++        /* Check if this is a ZWJ - next char will be joined. */+        if (isZWJ(cp)) {+            after_zwj = 1;+        }++        i += clen;+    }+    return width;+}++/* Return the display width of a single UTF-8 character at position 's'. */+static int utf8SingleCharWidth(const char *s, size_t len) {+    if (len == 0) return 0;+    size_t clen;+    uint32_t cp = utf8DecodeChar(s, &clen);+    return utf8CharWidth(cp);+}+ enum KEY_ACTION{ 	KEY_NULL = 0,	    /* NULL */ 	CTRL_A = 1,         /* Ctrl+a */@@ -220,6 +530,13 @@ static int enableRawMode(int fd) {     struct termios raw; +    /* Test mode: when LINENOISE_ASSUME_TTY is set, skip terminal setup.+     * This allows testing via pipes without a real terminal. */+    if (getenv("LINENOISE_ASSUME_TTY")) {+        rawmode = 1;+        return 0;+    }+     if (!isatty(STDIN_FILENO)) goto fatal;     if (!atexit_registered) {         atexit(linenoiseAtExit);@@ -253,6 +570,11 @@ }  static void disableRawMode(int fd) {+    /* Test mode: nothing to restore. */+    if (getenv("LINENOISE_ASSUME_TTY")) {+        rawmode = 0;+        return;+    }     /* Don't even check the return value as it's too late. */     if (rawmode && tcsetattr(fd,TCSAFLUSH,&orig_termios) != -1)         rawmode = 0;@@ -288,6 +610,10 @@ static int getColumns(int ifd, int ofd) {     struct winsize ws; +    /* Test mode: use LINENOISE_COLS env var for fixed width. */+    char *cols_env = getenv("LINENOISE_COLS");+    if (cols_env) return atoi(cols_env);+     if (ioctl(1, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) {         /* ioctl() failed. Try to query the terminal itself. */         int start, cols;@@ -505,16 +831,29 @@ }  /* Helper of refreshSingleLine() and refreshMultiLine() to show hints- * to the right of the prompt. */-void refreshShowHints(struct abuf *ab, struct linenoiseState *l, int plen) {+ * 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];-    if (hintsCallback && plen+l->len < l->cols) {+    size_t bufwidth = utf8StrWidth(l->buf, l->len);+    if (hintsCallback && pwidth + bufwidth < 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;+            size_t hintlen = strlen(hint);+            size_t hintwidth = utf8StrWidth(hint, hintlen);+            size_t hintmaxwidth = l->cols - (pwidth + bufwidth);+            /* Truncate hint to fit, respecting UTF-8 boundaries. */+            if (hintwidth > hintmaxwidth) {+                size_t i = 0, w = 0;+                while (i < hintlen) {+                    size_t clen = utf8NextCharLen(hint, i, hintlen);+                    int cwidth = utf8SingleCharWidth(hint + i, clen);+                    if (w + cwidth > hintmaxwidth) break;+                    w += cwidth;+                    i += clen;+                }+                hintlen = i;+            }             if (bold == 1 && color == -1) color = 37;             if (color != -1 || bold != 0)                 snprintf(seq,64,"\033[%d;%d;49m",bold,color);@@ -536,23 +875,44 @@  * cursor position, and number of columns of the terminal.  *  * Flags is REFRESH_* macros. The function can just remove the old- * prompt, just write it, or both. */+ * prompt, just write it, or both.+ *+ * This function is UTF-8 aware and uses display widths (not byte counts)+ * for cursor positioning and horizontal scrolling. */ static void refreshSingleLine(struct linenoiseState *l, int flags) {     char seq[64];-    size_t plen = strlen(l->prompt);+    size_t pwidth = utf8StrWidth(l->prompt, l->plen); /* Prompt display width */     int fd = l->ofd;     char *buf = l->buf;-    size_t len = l->len;-    size_t pos = l->pos;+    size_t len = l->len;    /* Byte length of buffer to display */+    size_t pos = l->pos;    /* Byte position of cursor */+    size_t poscol;          /* Display column of cursor */+    size_t lencol;          /* Display width of buffer */     struct abuf ab; -    while((plen+pos) >= l->cols) {-        buf++;-        len--;-        pos--;+    /* Calculate the display width up to cursor and total display width. */+    poscol = utf8StrWidth(buf, pos);+    lencol = utf8StrWidth(buf, len);++    /* Scroll the buffer horizontally if cursor is past the right edge.+     * We need to trim full UTF-8 characters from the left until the+     * cursor position fits within the terminal width. */+    while (pwidth + poscol >= l->cols) {+        size_t clen = utf8NextCharLen(buf, 0, len);+        int cwidth = utf8SingleCharWidth(buf, clen);+        buf += clen;+        len -= clen;+        pos -= clen;+        poscol -= cwidth;+        lencol -= cwidth;     }-    while (plen+len > l->cols) {-        len--;++    /* Trim from the right if the line still doesn't fit. */+    while (pwidth + lencol > l->cols) {+        size_t clen = utf8PrevCharLen(buf, len);+        int cwidth = utf8SingleCharWidth(buf + len - clen, clen);+        len -= clen;+        lencol -= cwidth;     }      abInit(&ab);@@ -562,14 +922,19 @@      if (flags & REFRESH_WRITE) {         /* Write the prompt and the current buffer content */-        abAppend(&ab,l->prompt,strlen(l->prompt));+        abAppend(&ab,l->prompt,l->plen);         if (maskmode == 1) {-            while (len--) abAppend(&ab,"*",1);+            /* In mask mode, we output one '*' per UTF-8 character, not byte */+            size_t i = 0;+            while (i < len) {+                abAppend(&ab,"*",1);+                i += utf8NextCharLen(buf, i, len);+            }         } else {             abAppend(&ab,buf,len);         }-        /* Show hits if any. */-        refreshShowHints(&ab,l,plen);+        /* Show hints if any. */+        refreshShowHints(&ab,l,pwidth);     }      /* Erase to right */@@ -577,8 +942,8 @@     abAppend(&ab,seq,strlen(seq));      if (flags & REFRESH_WRITE) {-        /* Move cursor to original position. */-        snprintf(seq,sizeof(seq),"\r\x1b[%dC", (int)(pos+plen));+        /* Move cursor to original position (using display column, not byte). */+        snprintf(seq,sizeof(seq),"\r\x1b[%dC", (int)(poscol+pwidth));         abAppend(&ab,seq,strlen(seq));     } @@ -592,14 +957,18 @@  * cursor position, and number of columns of the terminal.  *  * Flags is REFRESH_* macros. The function can just remove the old- * prompt, just write it, or both. */+ * prompt, just write it, or both.+ *+ * This function is UTF-8 aware and uses display widths for positioning. */ static void refreshMultiLine(struct linenoiseState *l, int flags) {     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. */+    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. */+    int rpos = l->oldrpos;   /* cursor relative row from previous refresh. */     int rpos2; /* rpos after refresh. */-    int col; /* colum position, zero-based. */+    int col; /* column position, zero-based. */     int old_rows = l->oldrows;     int fd = l->ofd, j;     struct abuf ab;@@ -634,22 +1003,26 @@      if (flags & REFRESH_WRITE) {         /* Write the prompt and the current buffer content */-        abAppend(&ab,l->prompt,strlen(l->prompt));+        abAppend(&ab,l->prompt,l->plen);         if (maskmode == 1) {-            unsigned int i;-            for (i = 0; i < l->len; i++) abAppend(&ab,"*",1);+            /* In mask mode, output one '*' per UTF-8 character, not byte */+            size_t i = 0;+            while (i < l->len) {+                abAppend(&ab,"*",1);+                i += utf8NextCharLen(l->buf, i, l->len);+            }         } else {             abAppend(&ab,l->buf,l->len);         } -        /* Show hits if any. */-        refreshShowHints(&ab,l,plen);+        /* Show hints if any. */+        refreshShowHints(&ab,l,pwidth);          /* 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)+            (poswidth+pwidth) % l->cols == 0)         {             lndebug("<newline>");             abAppend(&ab,"\n",1);@@ -660,10 +1033,10 @@         }          /* Move cursor to right position. */-        rpos2 = (plen+l->pos+l->cols)/l->cols; /* Current cursor relative row */+        rpos2 = (pwidth+poswidth+l->cols)/l->cols; /* Current cursor relative row */         lndebug("rpos2 %d", rpos2); -        /* Go up till we reach the expected positon. */+        /* Go up till we reach the expected position. */         if (rows-rpos2 > 0) {             lndebug("go-up %d", rows-rpos2);             snprintf(seq,64,"\x1b[%dA", rows-rpos2);@@ -671,7 +1044,7 @@         }          /* Set column. */-        col = (plen+(int)l->pos) % (int)l->cols;+        col = (pwidth+poswidth) % l->cols;         lndebug("set col %d", 1+col);         if (col)             snprintf(seq,64,"\r\x1b[%dC", col);@@ -682,6 +1055,7 @@      lndebug("\n");     l->oldpos = l->pos;+    if (flags & REFRESH_WRITE) l->oldrpos = rpos2;      if (write(fd,ab.b,ab.len) == -1) {} /* Can't recover from write error. */     abFree(&ab);@@ -718,29 +1092,37 @@     } } -/* Insert the character 'c' at cursor current position.+/* 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, char c) {-    if (l->len < l->buflen) {+int linenoiseEditInsert(struct linenoiseState *l, const char *c, size_t clen) {+    if (l->len + clen <= l->buflen) {         if (l->len == l->pos) {-            l->buf[l->pos] = c;-            l->pos++;-            l->len++;+            /* 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 && l->plen+l->len < l->cols && !hintsCallback)) {-                /* Avoid a full update of the line in the-                 * trivial case. */-                char d = (maskmode==1) ? '*' : c;-                if (write(l->ofd,&d,1) == -1) return -1;+            if ((!mlmode &&+                 utf8StrWidth(l->prompt,l->plen)+utf8StrWidth(l->buf,l->len) < l->cols &&+                 !hintsCallback)) {+                /* Avoid a full update of the line in the trivial case:+                 * single-width char, no hints, fits in one line. */+                if (maskmode == 1) {+                    if (write(l->ofd,"*",1) == -1) return -1;+                } else {+                    if (write(l->ofd,c,clen) == -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++;+            /* 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);         }@@ -748,18 +1130,18 @@     return 0; } -/* Move cursor on the left. */+/* Move cursor on the left. Moves by one UTF-8 character, not byte. */ void linenoiseEditMoveLeft(struct linenoiseState *l) {     if (l->pos > 0) {-        l->pos--;+        l->pos -= utf8PrevCharLen(l->buf, l->pos);         refreshLine(l);     } } -/* Move cursor on the right. */+/* 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++;+        l->pos += utf8NextCharLen(l->buf, l->pos, l->len);         refreshLine(l);     } }@@ -807,39 +1189,44 @@ }  /* Delete the character at the right of the cursor without altering the cursor- * position. Basically this is what happens with the "Delete" keyboard key. */+ * position. Basically this is what happens with the "Delete" keyboard key.+ * Now handles multi-byte UTF-8 characters. */ 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--;+        size_t clen = utf8NextCharLen(l->buf, l->pos, l->len);+        memmove(l->buf+l->pos, l->buf+l->pos+clen, l->len-l->pos-clen);+        l->len -= clen;         l->buf[l->len] = '\0';         refreshLine(l);     } } -/* Backspace implementation. */+/* Backspace implementation. Deletes the UTF-8 character before the cursor. */ 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--;+        size_t clen = utf8PrevCharLen(l->buf, l->pos);+        memmove(l->buf+l->pos-clen, l->buf+l->pos, l->len-l->pos);+        l->pos -= clen;+        l->len -= clen;         l->buf[l->len] = '\0';         refreshLine(l);     } } -/* Delete the previosu word, maintaining the cursor at the start of the- * current word. */+/* Delete the previous word, maintaining the cursor at the start of the+ * current word. Handles UTF-8 by moving character-by-character. */ void linenoiseEditDeletePrevWord(struct linenoiseState *l) {     size_t old_pos = l->pos;     size_t diff; +    /* Skip spaces before the word (move backwards by UTF-8 chars). */     while (l->pos > 0 && l->buf[l->pos-1] == ' ')-        l->pos--;+        l->pos -= utf8PrevCharLen(l->buf, l->pos);+    /* Skip non-space characters (move backwards by UTF-8 chars). */     while (l->pos > 0 && l->buf[l->pos-1] != ' ')-        l->pos--;+        l->pos -= utf8PrevCharLen(l->buf, l->pos);     diff = old_pos - l->pos;-    memmove(l->buf+l->pos,l->buf+old_pos,l->len-old_pos+1);+    memmove(l->buf+l->pos, l->buf+old_pos, l->len-old_pos+1);     l->len -= diff;     refreshLine(l); }@@ -886,6 +1273,7 @@      l->cols = getColumns(stdin_fd, stdout_fd);     l->oldrows = 0;+    l->oldrpos = 1;  /* Cursor starts on row 1. */     l->history_index = 0;      /* Buffer starts empty. */@@ -895,7 +1283,7 @@     /* If stdin is not a tty, stop here with the initialization. We      * will actually just read a line from standard input in blocking      * mode later, in linenoiseEditFeed(). */-    if (!isatty(l->ifd)) return 0;+    if (!isatty(l->ifd) && !getenv("LINENOISE_ASSUME_TTY")) return 0;      /* The latest history entry is always our current buffer, that      * initially is just an empty string. */@@ -928,14 +1316,18 @@ char *linenoiseEditFeed(struct linenoiseState *l) {     /* Not a TTY, pass control to line reading without character      * count limits. */-    if (!isatty(l->ifd)) return linenoiseNoTTY();+    if (!isatty(l->ifd) && !getenv("LINENOISE_ASSUME_TTY")) return linenoiseNoTTY();      char c;     int nread;     char seq[3];      nread = read(l->ifd,&c,1);-    if (nread <= 0) return NULL;+    if (nread < 0) {+        return (errno == EAGAIN || errno == EWOULDBLOCK) ? linenoiseEditMore : NULL;+    } else if (nread == 0) {+        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@@ -981,11 +1373,17 @@         }         break;     case CTRL_T:    /* ctrl-t, swaps current character with previous. */+        /* Handle UTF-8: swap the two UTF-8 characters around cursor. */         if (l->pos > 0 && l->pos < l->len) {-            int aux = l->buf[l->pos-1];-            l->buf[l->pos-1] = l->buf[l->pos];-            l->buf[l->pos] = aux;-            if (l->pos != l->len-1) l->pos++;+            char tmp[32];+            size_t prevlen = utf8PrevCharLen(l->buf, l->pos);+            size_t currlen = utf8NextCharLen(l->buf, l->pos, l->len);+            size_t prevstart = l->pos - prevlen;+            /* 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);+            memcpy(l->buf + prevstart, tmp, currlen);+            if (l->pos + currlen <= l->len) l->pos += currlen;             refreshLine(l);         }         break;@@ -1057,7 +1455,22 @@         }         break;     default:-        if (linenoiseEditInsert(l,c)) return NULL;+        /* Handle UTF-8 multi-byte sequences. When we receive the first byte+         * of a multi-byte UTF-8 character, read the remaining bytes to+         * complete the sequence before inserting. */+        {+            char utf8[4];+            int utf8len = utf8ByteLen(c);+            utf8[0] = c;+            if (utf8len > 1) {+                /* Read remaining bytes of the UTF-8 sequence. */+                int i;+                for (i = 1; i < utf8len; i++) {+                    if (read(l->ifd, utf8+i, 1) != 1) break;+                }+            }+            if (linenoiseEditInsert(l, utf8, utf8len)) return NULL;+        }         break;     case CTRL_U: /* Ctrl+u, delete the whole line. */         l->buf[0] = '\0';@@ -1091,7 +1504,7 @@  * returns something different than NULL. At this point the user input  * is in the buffer, and we can restore the terminal in normal mode. */ void linenoiseEditStop(struct linenoiseState *l) {-    if (!isatty(l->ifd)) return;+    if (!isatty(l->ifd) && !getenv("LINENOISE_ASSUME_TTY")) return;     disableRawMode(l->ifd);     printf("\n"); }@@ -1189,7 +1602,7 @@ char *linenoise(const char *prompt) {     char buf[LINENOISE_MAX_LINE]; -    if (!isatty(STDIN_FILENO)) {+    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();@@ -1318,7 +1731,7 @@     fp = fopen(filename,"w");     umask(old_umask);     if (fp == NULL) return -1;-    chmod(filename,S_IRUSR|S_IWUSR);+    fchmod(fileno(fp),S_IRUSR|S_IWUSR);     for (j = 0; j < history_len; j++)         fprintf(fp,"%s\n",history[j]);     fclose(fp);
cbits/linenoise.h view
@@ -65,6 +65,7 @@     size_t len;         /* Current edited line length. */     size_t cols;        /* Number of columns in terminal. */     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. */ }; 
linenoise.cabal view
@@ -1,13 +1,13 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.37.0.+-- This file has been generated from package.yaml by hpack version 0.39.1. -- -- see: https://github.com/sol/hpack ----- hash: fe699d3f8d98d113f63c1ac0e1cd5597a7e3533238e63980f5e5c6a1b8a2dfff+-- hash: 42a9541c6a153888178292a118515374ece2c2851a61148bcec2105c19708638  name:           linenoise-version:        0.4.2+version:        0.5.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