TUI: mouse drag selection with OSC 52 copy; Markdown emphasis follows CommonMark; v0.5.0
- 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:
+194
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user