Files
Hermit/src/textsel.c
T
XYZ 929f7125b8 Windows: encode and decode text as UTF-8 instead of trusting the locale
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()
2026-10-06 17:56:55 +08:00

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;
}
}
}