TUI: mouse drag selection with OSC 52 copy; Markdown emphasis follows CommonMark; v0.5.0
ci / linux (clang) (push) Failing after 21m53s
ci / linux (gcc) (push) Failing after 48s
ci / musl (push) Canceled after 0s
ci / macos (push) Canceled after 0s
ci / sanitizers (push) Canceled after 11m39s

- Drag, double-click (word) and triple-click (line) selection in the log,
  auto-scroll at the edges, soft-wrapped lines joined on copy.
- sashidown ANSI renderer: delimiter runs with flanking rules, so
  intraword underscores (test_apply_patch.c) stay literal; unmatched
  * _ and backticks are shown as-is; * and _ do not close each other.
- tests: test_textsel, test_markdown.
This commit is contained in:
WHC
2026-10-06 14:24:42 +08:00
parent b28bca2cfc
commit 1c26cc6e30
11 changed files with 875 additions and 111 deletions
+194
View File
@@ -0,0 +1,194 @@
#ifndef _GNU_SOURCE
#define _GNU_SOURCE /* wcwidth */
#endif
#include "cst_textsel.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) {
if (avail == 0)
return -1;
mbstate_t st;
memset(&st, 0, sizeof(st));
size_t n = mbrtowc(wc, s, avail, &st);
if (n == (size_t)-1 || n == (size_t)-2 || n == 0) {
*wc = (unsigned char)s[0];
*eaten = 1;
return 0;
}
*eaten = 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;
}
}
}