setlocale(LC_ALL, "") maps to the ANSI code page on Windows -- 936 (GBK)
on a Chinese system -- so wcrtomb() produces GBK where the protocol wants
UTF-8, and mbrtowc() reads UTF-8 as GBK. Typed Chinese therefore reached
the model as mojibake, and once the buffer held correct UTF-8 the input
line rendered as mojibake instead. Windows' C runtime has no ".UTF-8"
locale to switch to, so the conversion has to be explicit.
os_compat.[ch]
* add os_utf8_encode() and os_utf8_decode(), locale-independent, with
range and overlong-form checks
src/tui.c
* input_put_wc(), the command-palette filter and the ask_user buffer use
os_utf8_encode() instead of wcrtomb()
src/textsel.c
* cst_utf8_decode() uses os_utf8_decode() instead of mbrtowc()
201 lines
4.8 KiB
C
201 lines
4.8 KiB
C
#ifndef _GNU_SOURCE
|
|
#define _GNU_SOURCE /* wcwidth */
|
|
#endif
|
|
|
|
#include "cst_textsel.h"
|
|
#include "os_compat.h"
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <wctype.h>
|
|
|
|
int cst_utf8_decode(const char *s, size_t avail, wchar_t *wc, size_t *eaten) {
|
|
unsigned long cp = 0;
|
|
int n;
|
|
if (avail == 0)
|
|
return -1;
|
|
/* os_utf8_decode(), not mbrtowc(): the latter follows setlocale(), which on
|
|
* Windows means the ANSI code page, so UTF-8 text would be decoded as GBK
|
|
* and rendered as mojibake. */
|
|
n = os_utf8_decode(s, avail, &cp);
|
|
if (n <= 0) {
|
|
/* Not a valid sequence: fall back to one raw byte, as before. */
|
|
*wc = (wchar_t)(unsigned char)s[0];
|
|
*eaten = 1;
|
|
return 0;
|
|
}
|
|
*wc = (wchar_t)cp;
|
|
*eaten = (size_t)n;
|
|
return 0;
|
|
}
|
|
|
|
int cst_wcwidth(wchar_t wc) {
|
|
#ifdef _WIN32
|
|
unsigned long c = (unsigned long)wc;
|
|
if (c == 0)
|
|
return 0;
|
|
if (c < 32 || (c >= 0x7f && c < 0xa0))
|
|
return -1;
|
|
if (c >= 0x1100 &&
|
|
(c <= 0x115f || c == 0x2329 || c == 0x232a ||
|
|
(c >= 0x2e80 && c <= 0xa4cf && c != 0x303f) ||
|
|
(c >= 0xac00 && c <= 0xd7a3) || (c >= 0xf900 && c <= 0xfaff) ||
|
|
(c >= 0xfe10 && c <= 0xfe19) || (c >= 0xfe30 && c <= 0xfe6f) ||
|
|
(c >= 0xff00 && c <= 0xff60) || (c >= 0xffe0 && c <= 0xffe6) ||
|
|
(c >= 0x20000 && c <= 0x2fffd) || (c >= 0x30000 && c <= 0x3fffd)))
|
|
return 2;
|
|
return 1;
|
|
#else
|
|
return wcwidth(wc);
|
|
#endif
|
|
}
|
|
|
|
int cst_utf8_width(wchar_t wc) {
|
|
int w = cst_wcwidth(wc);
|
|
if (w < 0)
|
|
return 1;
|
|
return w;
|
|
}
|
|
|
|
size_t cst_ansi_skip(const char *s, size_t n) {
|
|
if (n < 2 || (unsigned char)s[0] != 0x1b)
|
|
return 0;
|
|
if (s[1] == '[') {
|
|
size_t i = 2;
|
|
while (i < n) {
|
|
unsigned char c = (unsigned char)s[i++];
|
|
if (c >= 0x40 && c <= 0x7e)
|
|
return i;
|
|
}
|
|
return 0;
|
|
}
|
|
return 2;
|
|
}
|
|
|
|
void cst_selbuf_free(cst_selbuf_t *b) {
|
|
free(b->s);
|
|
b->s = NULL;
|
|
b->n = b->cap = 0;
|
|
}
|
|
|
|
static int selbuf_put(cst_selbuf_t *b, const char *p, size_t n) {
|
|
if (b->n + n + 1 > b->cap) {
|
|
size_t nc = b->cap ? b->cap * 2 : 256;
|
|
while (nc < b->n + n + 1)
|
|
nc *= 2;
|
|
char *ns = realloc(b->s, nc);
|
|
if (!ns)
|
|
return -1;
|
|
b->s = ns;
|
|
b->cap = nc;
|
|
}
|
|
memcpy(b->s + b->n, p, n);
|
|
b->n += n;
|
|
b->s[b->n] = 0;
|
|
return 0;
|
|
}
|
|
|
|
int cst_selbuf_putc(cst_selbuf_t *b, char c) { return selbuf_put(b, &c, 1); }
|
|
|
|
/* One drawn character: its bytes in the line and the columns it covers. */
|
|
typedef struct {
|
|
size_t off, len;
|
|
int col, width;
|
|
wchar_t wc;
|
|
} cell_t;
|
|
|
|
/* Advances to the next drawn character of line, skipping escapes. */
|
|
static int next_cell(const char *line, size_t n, size_t *i, int *col,
|
|
cell_t *c) {
|
|
while (*i < n) {
|
|
size_t skip = cst_ansi_skip(line + *i, n - *i);
|
|
if (skip) {
|
|
*i += skip;
|
|
continue;
|
|
}
|
|
size_t eaten;
|
|
cst_utf8_decode(line + *i, n - *i, &c->wc, &eaten);
|
|
c->off = *i;
|
|
c->len = eaten;
|
|
c->col = *col;
|
|
c->width = cst_utf8_width(c->wc);
|
|
*i += eaten;
|
|
*col += c->width;
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int cst_textsel_append(cst_selbuf_t *b, const char *line, int indent, int c1,
|
|
int c2, int trim) {
|
|
size_t n = line ? strlen(line) : 0, i = 0;
|
|
size_t start = b->n;
|
|
int col = indent;
|
|
cell_t c;
|
|
while (next_cell(line, n, &i, &col, &c)) {
|
|
if (c2 >= 0 && c.col > c2)
|
|
break;
|
|
if (c.width == 0 || c.col + c.width - 1 < c1)
|
|
continue;
|
|
if (selbuf_put(b, line + c.off, c.len) < 0)
|
|
return -1;
|
|
}
|
|
if (trim)
|
|
while (b->n > start && b->s[b->n - 1] == ' ')
|
|
b->s[--b->n] = 0;
|
|
return 0;
|
|
}
|
|
|
|
static int is_word(wchar_t wc) {
|
|
return iswalnum((wint_t)wc) || (wc < 128 && wc && strchr("_-./:@~%+=?&#$", (int)wc));
|
|
}
|
|
|
|
void cst_textsel_snap(const char *line, int indent, int *c1, int *c2) {
|
|
size_t n = line ? strlen(line) : 0, i = 0;
|
|
int x = indent;
|
|
cell_t c;
|
|
while (next_cell(line, n, &i, &x, &c)) {
|
|
if (c.width < 2)
|
|
continue;
|
|
int last = c.col + c.width - 1;
|
|
if (*c1 > c.col && *c1 <= last)
|
|
*c1 = c.col;
|
|
if (*c2 >= c.col && *c2 < last)
|
|
*c2 = last;
|
|
}
|
|
}
|
|
|
|
void cst_textsel_word(const char *line, int indent, int col, int *c1, int *c2) {
|
|
*c1 = *c2 = col;
|
|
size_t n = line ? strlen(line) : 0, i = 0;
|
|
int x = indent;
|
|
cell_t c;
|
|
int run_start = -1, in_run = 0;
|
|
while (next_cell(line, n, &i, &x, &c)) {
|
|
if (c.width == 0)
|
|
continue;
|
|
int word = is_word(c.wc);
|
|
if (word && !in_run)
|
|
run_start = c.col;
|
|
in_run = word;
|
|
if (col >= c.col && col < c.col + c.width) {
|
|
if (!word) {
|
|
*c1 = c.col;
|
|
*c2 = c.col + c.width - 1;
|
|
return;
|
|
}
|
|
*c1 = run_start;
|
|
*c2 = c.col + c.width - 1;
|
|
cell_t d;
|
|
while (next_cell(line, n, &i, &x, &d)) {
|
|
if (d.width == 0)
|
|
continue;
|
|
if (!is_word(d.wc))
|
|
break;
|
|
*c2 = d.col + d.width - 1;
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|