circo

claudio's IRC oasis
git clone git://git.bitsmanent.org/circo
Log | Files | Refs | README | LICENSE

commit 35e5d520b1f20b07261ce537047c4aacf24049f8
parent e2cfa9e111c62bf47d162648811727aaa028efbd
Author: Claudio Alessi <smoppy@gmail.com>
Date:   Sat, 16 Apr 2022 14:28:22 +0200

Initial effort to support UTF-8.

Implementation is based on two macros which works on raw bytes (char *):
UTF8BYTES() which returns the size (total bytes) of a code point, and
UTF8CBYTE() which test for a continuation byte. This may not be the perfect
solution but seems to be working for all tested scenarios. Currently wchar_t is
only used to compute columns width but this may change in future.

In future I may cosider switching to libgrapheme or the like but, even if there
are various cleanups needed and some refactoring, since it works surprisingly
well, I will stuck with this simple design for a while.

Diffstat:
MREADME.md | 13++++++-------
Mcirco.c | 286+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Mconfig.def.h | 3++-
3 files changed, 207 insertions(+), 95 deletions(-)

diff --git a/README.md b/README.md @@ -13,22 +13,21 @@ circo is unique. Altough it's compactness, circo is a powerful tool with almost all the features one would expect from an IRC client for the console: -- Per-channel/query buffers -- Command line +- UTF-8 support +- Colors support +- Tab completion +- One screen per chan/user - Infinite scrolling - Resize handling -- Command history +- Commands history - Raw IRC commands -- Colors support -- Tab completion None of the CTCP specification has been (nor will be) implemented, which means no DCC at all. In other words: direct chat and files sending are not available. Status ====== -Some refactor and cleanups are needed but it's mostly working. UTF-8 is not -currently supported. +Some refactor and cleanups are needed but it's mostly working. The circo IRC client is actively developed and I'm having a lot of fun in writing it. If you'll also find yourself having fun with circo, please consider diff --git a/circo.c b/circo.c @@ -27,6 +27,7 @@ #include <termios.h> #include <time.h> #include <unistd.h> +#include <wchar.h> #include "printfc.h" @@ -38,6 +39,13 @@ char *argv0; #define ISCHANPFX(P) ((P) == '#' || (P) == '&') #define ISCHAN(B) ISCHANPFX((B)->name[0]) +/* UTF-8 utils */ +#define UTF8BYTES(X) ( ((X) & 0xF0) == 0xF0 ? 4 \ + : ((X) & 0xE0) == 0xE0 ? 3 \ + : ((X) & 0xC0) == 0xC0 ? 2 \ + : 1) +#define UTF8CBYTE(X) (((X) & 0xC0) == 0x80) + /* drawing flags */ #define REDRAW_BAR 1<<1 #define REDRAW_BUFFER 1<<2 @@ -61,7 +69,7 @@ char *argv0; #define CTRL_ALT(k) ((k) + (129 - 'a')) /* enums */ -enum { KeyUp = -50, KeyDown, KeyRight, KeyLeft, KeyHome, KeyEnd, KeyDel, KeyPgUp, KeyPgDw, KeyBackspace }; +enum { KeyFirst = -999, KeyUp, KeyDown, KeyRight, KeyLeft, KeyHome, KeyEnd, KeyDel, KeyPgUp, KeyPgDw, KeyBackspace, KeyLast }; enum { LineToOffset, OffsetToLine, TotalLines }; /* bufinfo() flags */ enum { @@ -134,6 +142,7 @@ void cmdln_chrdel(const Arg *arg); void cmdln_clear(const Arg *arg); void cmdln_complete(const Arg *arg); void cmdln_cursor(const Arg *arg); +void cmdln_submit(const Arg *arg); void cmdln_wdel(const Arg *arg); void detach(Buffer *b); int dial(char *host, char *port); @@ -148,6 +157,8 @@ void focusnext(const Arg *arg); void focusprev(const Arg *arg); void freebuf(Buffer *b); void freenames(Nick **names); +char *gcsfitcols(char *s, int maxw); +int gcswidth(char *s, int len); int getkey(void); void hangsup(void); void history(const Arg *arg); @@ -191,6 +202,7 @@ void strip_ctrlseqs(char *s); void trim(char *s); void usage(void); void usrin(void); +int utf8len(char *s, size_t sz); char *wordleft(char *str, int offset, int *size); /* variables */ @@ -454,30 +466,44 @@ cmd_topic(char *cmd, char *s) { void cmdln_chldel(const Arg *arg) { + int nb; + if(!sel->cmdoff) return; + for(nb = 1; UTF8CBYTE(sel->cmdbuf[sel->cmdoff - nb]); ++nb); if(sel->cmdoff < sel->cmdlen) - memmove(&sel->cmdbuf[sel->cmdoff - 1], &sel->cmdbuf[sel->cmdoff], + memmove(&sel->cmdbuf[sel->cmdoff - nb], &sel->cmdbuf[sel->cmdoff], sel->cmdlen - sel->cmdoff); - sel->cmdbuf[--sel->cmdlen] = '\0'; - --sel->cmdoff; + sel->cmdlen -= nb; + sel->cmdoff -= nb; + sel->cmdbuf[sel->cmdlen] = '\0'; sel->need_redraw |= REDRAW_CMDLN; } void cmdln_chrdel(const Arg *arg) { + int nb; + if(!sel->cmdlen) return; if(sel->cmdoff == sel->cmdlen) { - --sel->cmdoff; + for(nb = 1; UTF8CBYTE(sel->cmdbuf[sel->cmdoff - nb]); ++nb); + sel->cmdoff -= nb; sel->need_redraw |= REDRAW_CMDLN; return; } - memmove(&sel->cmdbuf[sel->cmdoff], &sel->cmdbuf[sel->cmdoff + 1], - sel->cmdlen - sel->cmdoff - 1); - sel->cmdbuf[--sel->cmdlen] = '\0'; - if(sel->cmdoff && sel->cmdoff == sel->cmdlen) - --sel->cmdoff; + nb = UTF8BYTES(sel->cmdbuf[sel->cmdoff]); + memmove(&sel->cmdbuf[sel->cmdoff], &sel->cmdbuf[sel->cmdoff + nb], + sel->cmdlen - sel->cmdoff); + + sel->cmdlen -= nb; + sel->cmdbuf[sel->cmdlen] = '\0'; + + if(sel->cmdoff && sel->cmdoff == sel->cmdlen) { + for(nb = 1; UTF8CBYTE(sel->cmdbuf[sel->cmdoff - nb]); ++nb); + sel->cmdoff -= nb; + } + sel->need_redraw |= REDRAW_CMDLN; } @@ -558,20 +584,61 @@ cmdln_complete(const Arg *arg) { void cmdln_cursor(const Arg *arg) { - if(!arg->i) { + int i = arg->i, nb; + + if(!i) { sel->cmdoff = 0; + sel->need_redraw |= REDRAW_CMDLN; + return; + } + if(i < 0) { + nb = 1; + while(++i <= 0) + while(UTF8CBYTE(sel->cmdbuf[sel->cmdoff - nb])) + ++nb; + nb = -nb; } else { - sel->cmdoff += arg->i; - if(sel->cmdoff < 0) { - sel->cmdoff = 0; - } - else if(sel->cmdoff > sel->cmdlen - 1) { - sel->cmdoff = sel->cmdlen - 1; - if(sel->cmdlen < sizeof sel->cmdbuf - 1) - ++sel->cmdoff; + nb = 0; + while(--i >= 0) + nb += UTF8BYTES(sel->cmdbuf[sel->cmdoff + nb]); + } + + sel->cmdoff += nb; + if(sel->cmdoff < 0) + sel->cmdoff = 0; + else if(sel->cmdoff > sel->cmdlen) + sel->cmdoff = sel->cmdlen; + sel->need_redraw |= REDRAW_CMDLN; +} + +void +cmdln_submit(const Arg *arg) { + logw(sel->cmdbuf); + logw("\n"); + if(sel->cmdbuf[0] == '\0') + return; + if(sel->cmdbuf[0] == '/') { + if(sel->cmdlen == 1) + return; + histpush(sel->cmdbuf, sel->cmdlen); + /* Note: network latency may delay drawings + * causing visual glitches. */ + parsecmd(); + } + else { + histpush(sel->cmdbuf, sel->cmdlen); + if(sel == status) { + bprintf(sel, "Cannot send text here.\n"); + return; } + else if(!srv) + bprintf(sel, "Not connected.\n"); + else + privmsg(sel->name, sel->cmdbuf); } + sel->cmdlen = sel->cmdoff = sel->histlnoff = 0; + sel->cmdbuf[sel->cmdlen] = '\0'; sel->need_redraw |= REDRAW_CMDLN; } @@ -665,7 +732,7 @@ drawbar(void) { void drawbuf(void) { - int x, y, c, i; + int x, y, c, i, nb, nx; if(!(cols && rows && sel->len)) return; @@ -676,6 +743,7 @@ drawbuf(void) { : bufinfo(sel->data, sel->len, 1 + (sel->nlines > x ? y : 0), LineToOffset); x = 1; y = 2; + for(; i < sel->len; ++i) { /* control sequences (for colors) */ @@ -689,8 +757,14 @@ drawbuf(void) { c = sel->data[i]; if(x <= cols) { if(c != '\n') { - x += mvprintf(x, y, "%c", c); - continue; + nb = UTF8BYTES(c); + nx = x + gcswidth(&sel->data[i], 1); + if(nx - 1 <= cols) { + mvprintf(x, y, "%.*s", nb, &sel->data[i]); + x = nx; + i += nb - 1; + continue; + } } mvprintf(x, y, "%s", CLEARRIGHT); } @@ -698,8 +772,12 @@ drawbuf(void) { x = 1; if(++y == rows) break; - if(c != '\n') - x += mvprintf(x, y, "%c", c); + if(c != '\n') { + nb = UTF8BYTES(c); + mvprintf(x, y, "%.*s", nb, &sel->data[i]); + x += gcswidth(&sel->data[i], 1); + i += nb - 1; + } if(x > cols && i < sel->len - 1 && sel->data[i + 1] == '\n') ++i; } @@ -712,25 +790,43 @@ drawbuf(void) { void drawcmdln(void) { - char buf[cols+1]; - char prompt[cols+1 + 31]; - int pslen, cmdsz, i, len; - - if(!(cols && rows)) - return; + char prompt[64], *buf, *p; + int s, w; /* size and width */ + int x = 1, colw = cols; + + sel->cmdpos = 1; + + /* prompt */ + s = snprintf(prompt, sizeof prompt, "[%s] ", sel->name); + w = gcswidth(prompt, colw - 1); + if(w > 0) { + s = gcsfitcols(prompt, colw - 1) - prompt; + mvprintf(x, rows, "%.*s", s, prompt); + x += w; + colw -= w; + sel->cmdpos += w; + } - pslen = snprintf(prompt, sizeof prompt, "[%s] ", sel->name); - cmdsz = pslen < cols ? cols - pslen : 0; - if(cmdsz) { - sel->cmdpos = pslen + (sel->cmdoff % cmdsz) + 1; - i = cmdsz * (sel->cmdoff / cmdsz); + /* buffer */ + for(p = buf = sel->cmdbuf; p < &sel->cmdbuf[sel->cmdoff]; p = gcsfitcols(p, colw)) { + if(p != sel->cmdbuf && p == buf) + break; + buf = p; } - else { - sel->cmdpos = cols; - i = 0; + w = gcswidth(buf, colw); + + /* leave room for the cursor */ + if(w >= colw && p == &sel->cmdbuf[sel->cmdoff]) { + buf = p; + w = gcswidth(buf, colw); } - len = snprintf(buf, sizeof buf, "%s%s", prompt, &sel->cmdbuf[i]); - mvprintf(1, rows, "%s%s", buf, len < cols ? CLEARRIGHT : ""); + + s = w ? gcsfitcols(buf, colw) - buf : 0; + mvprintf(x, rows, "%.*s%s", s, buf, w < colw ? CLEARRIGHT : ""); + + /* cursor position */ + for(p = buf; p < &sel->cmdbuf[sel->cmdoff]; p += UTF8BYTES(*p)) + sel->cmdpos += gcswidth(p, 1); } void * @@ -791,6 +887,30 @@ freenames(Nick **names) { } } +char * +gcsfitcols(char *s, int maxw) { + int w = 0; + + while(*s) { + w += gcswidth(s, 1); + if(w > maxw) + break; + s += UTF8BYTES(*s); + } + return s; +} + +int +gcswidth(char *s, int len) { + wchar_t *wcs = malloc(len * sizeof(wchar_t) + 1); + int n; + + if(!wcs) + return -1; + n = mbstowcs(wcs, s, len); + return n == -1 ? -1 : wcswidth(wcs, n); +} + Buffer * getbuf(char *name) { Buffer *b; @@ -827,6 +947,7 @@ getkey(void) { case '7': key = KeyHome; break; case '8': key = KeyEnd; break; } + readchar(); return key; } @@ -1405,61 +1526,52 @@ usage(void) { void usrin(void) { - Buffer *b; - int key = getkey(), i; + char graph[4]; + int key, nb, i; + key = getkey(); for(i = 0; i < LENGTH(keys); ++i) { if(keys[i].key == key) { keys[i].func(&keys[i].arg); - while(getkey() != -1); /* discard remaining input */ return; } } - do { - if(key == '\n') { - b = sel; - logw(sel->cmdbuf); - if(sel->cmdbuf[0] == '\0') - return; - if(sel->cmdbuf[0] == '/') { - if(sel->cmdlen == 1) - return; - histpush(sel->cmdbuf, sel->cmdlen); - /* Note: network latency may delay drawings - * causing visual glitches. */ - parsecmd(); - } - else { - histpush(sel->cmdbuf, sel->cmdlen); - if(sel == status) - bprintf(sel, "Cannot send text here.\n"); - else if(!srv) - bprintf(sel, "Not connected.\n"); - else - privmsg(sel->name, sel->cmdbuf); - } - if(b == sel) { - sel->cmdlen = sel->cmdoff = sel->histlnoff = 0; - b->cmdbuf[sel->cmdlen] = '\0'; - b->need_redraw |= REDRAW_CMDLN; - } - } - else if(isgraph(key) || key == ' ') { - if(sel->cmdlen == sizeof sel->cmdbuf - 1) { - sel->cmdbuf[sel->cmdoff] = key; - sel->need_redraw |= REDRAW_CMDLN; - return; - } - memmove(&sel->cmdbuf[sel->cmdoff+1], - &sel->cmdbuf[sel->cmdoff], - sel->cmdlen - sel->cmdoff); - sel->cmdbuf[sel->cmdoff] = key; - sel->cmdbuf[++sel->cmdlen] = '\0'; - if(sel->cmdlen < sizeof sel->cmdbuf - 1) - ++sel->cmdoff; - sel->need_redraw |= REDRAW_CMDLN; + + if(key >= KeyFirst && key <= KeyLast) + return; + if(iscntrl(key)) { + while((key = readchar()) != EOF); + return; + } + + nb = UTF8BYTES(key); + graph[0] = key; + for(i = 1; i < nb; ++i) { + key = readchar(); + if(key == EOF) { + /* TODO: preserve the state and return */ + while((key = readchar()) == EOF); } - } while((key = getkey()) != -1); + graph[i] = key; + } + + /* prevent overflow */ + if(sel->cmdlen + nb >= sizeof sel->cmdbuf) + return; + + /* move nb bytes to the right */ + memmove(&sel->cmdbuf[sel->cmdoff+nb], + &sel->cmdbuf[sel->cmdoff], + sel->cmdlen - sel->cmdoff); + + /* insert nb bytes at current offset */ + memcpy(&sel->cmdbuf[sel->cmdoff], graph, nb); + + sel->cmdlen += nb; + sel->cmdbuf[sel->cmdlen] = '\0'; + sel->cmdoff += nb; + + sel->need_redraw |= REDRAW_CMDLN; } char * diff --git a/config.def.h b/config.def.h @@ -7,7 +7,7 @@ char nick[32] = {0}; /* 0 means getenv("USER") */ char logfile[64] = "/tmp/circo.log"; /* Timestamp format; see strftime(3). */ -#define TIMESTAMP_FORMAT "%d/%m %T | " +#define TIMESTAMP_FORMAT "%T | " /* Used if no message is specified */ #define QUIT_MESSAGE "circo" @@ -45,6 +45,7 @@ static Key keys[] = { { KeyLeft, cmdln_cursor, {.i = -1}}, { CTRL('l'), cmdln_cursor, {.i = +1}}, { KeyRight, cmdln_cursor, {.i = +1}}, + { '\n', cmdln_submit, {0} }, { CTRL('w'), cmdln_wdel, {0} }, { CTRL('n'), focusnext, {0} }, { CTRL('p'), focusprev, {0} },