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;
}
}
}
+269 -89
View File
@@ -10,6 +10,7 @@
#include "cst_plugins.h"
#include "cst_skills.h"
#include "cst_subagent.h"
#include "cst_textsel.h"
#include "cst_perms.h"
#include "cst_sandbox.h"
#include "cst_tools.h"
@@ -61,23 +62,7 @@ static int g_utf8 = 1; /* locale can show non-ASCII text */
#ifndef KEY_RESIZE
#define KEY_RESIZE 0632
#endif
/* MinGW often lacks wcwidth; simple East-Asian / ASCII heuristic. */
static int cst_wcwidth(wchar_t wc) {
if (wc == 0)
return 0;
if (wc < 32 || (wc >= 0x7f && wc < 0xa0))
return -1;
if (wc >= 0x1100 &&
(wc <= 0x115f || wc == 0x2329 || wc == 0x232a ||
(wc >= 0x2e80 && wc <= 0xa4cf && wc != 0x303f) ||
(wc >= 0xac00 && wc <= 0xd7a3) || (wc >= 0xf900 && wc <= 0xfaff) ||
(wc >= 0xfe10 && wc <= 0xfe19) || (wc >= 0xfe30 && wc <= 0xfe6f) ||
(wc >= 0xff00 && wc <= 0xff60) || (wc >= 0xffe0 && wc <= 0xffe6) ||
(wc >= 0x20000 && wc <= 0x2fffd) || (wc >= 0x30000 && wc <= 0x3fffd)))
return 2;
return 1;
}
#define wcwidth cst_wcwidth
#define wcwidth cst_wcwidth /* MinGW often lacks wcwidth */
#else
#include <ncurses.h>
#endif
@@ -139,6 +124,12 @@ static int cst_clock_gettime_mono(struct timespec *ts) {
#define KEY_KITTY_REPLY (KEY_OWN_BASE + 3)
#define KEY_KITTY_BASE (KEY_OWN_BASE + 16)
#define KITTY_CP_END 128
/* Transcript selection states. */
#define SEL_NONE 0
#define SEL_PRESS 1 /* button down, not moved off the pressed cell yet */
#define SEL_DRAG 2
#define SEL_SHOWN 3 /* released; highlighted until the next input */
#define MULTI_CLICK_MS 400
#ifdef NCURSES_VERSION
static int g_kitty_on;
@@ -301,6 +292,15 @@ typedef struct {
int row_blk_cap;
int cmd_x0, cmd_x1; /* "ctrl+p commands" in the status row */
int mouse_on;
int log_start; /* display line shown in logw row 0 */
/* Mouse selection in the transcript: display line + logw column. */
int sel_state; /* SEL_* */
int sel_mode; /* 1 characters, 2 words, 3 lines */
int sel_ay, sel_ax; /* anchor */
int sel_y, sel_x; /* current end */
int sel_press_y, sel_press_x; /* screen cell of the press */
int sel_clicks, sel_click_y, sel_click_x;
struct timespec sel_click_at;
const char *tip;
const char *sandbox_name; /* "landlock" / "seatbelt" / "rules" */
char where[300]; /* ~/path:branch */
@@ -404,26 +404,10 @@ static size_t utf8_prev_len(const char *s, size_t len) {
}
static int 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;
return cst_utf8_decode(s, avail, wc, eaten);
}
static int utf8_width(wchar_t wc) {
int w = wcwidth(wc);
if (w < 0)
return 1;
return w;
}
static int utf8_width(wchar_t wc) { return cst_utf8_width(wc); }
static int utf8_cols(const char *s, size_t n) {
int cols = 0;
@@ -454,20 +438,7 @@ static size_t utf8_fit_cols(const char *s, size_t n, int max_cols) {
return i;
}
static size_t 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;
}
static size_t ansi_skip(const char *s, size_t n) { return cst_ansi_skip(s, n); }
static char *xstrdup(const char *s) {
if (!s)
@@ -1272,6 +1243,8 @@ typedef struct {
int fill; /* pair painted across the whole row (0 = none) */
int bar; /* pair for a "┃" in column 0 (0 = none) */
int blk; /* hist block index, -1 for live / none */
int cont; /* soft-wrapped continuation of the previous row */
int cont_pad; /* leading columns of a continuation that only indent */
} dline_t;
static int g_dl_blk = -1; /* block being flattened, stamped into dline_t */
@@ -1311,6 +1284,8 @@ static int dlines_push(dlines_t *dl, const char *text, int pair, int bold,
L->fill = 0;
L->bar = 0;
L->blk = g_dl_blk;
L->cont = 0;
L->cont_pad = 0;
return 0;
}
@@ -1341,10 +1316,10 @@ static void push_wrapped(dlines_t *dl, const char *prefix, const char *body,
/* Walk body; for ANSI text, wrap on visible columns skipping SGR. */
size_t n = strlen(body);
size_t i = 0;
int first = 1;
int first = 1, soft = 0;
while (i < n && dl->n < MAX_RENDER_LINES) {
size_t start = i;
int col = 0;
int col = 0, pad = 0;
while (i < n) {
size_t sg = 0;
if ((unsigned char)body[i] == 0x1b) {
@@ -1387,7 +1362,7 @@ static void push_wrapped(dlines_t *dl, const char *prefix, const char *body,
first = 0;
} else {
/* continuation indent matching prefix visual width */
int pad = pref_cols;
pad = pref_cols;
if (pad < 0)
pad = 0;
if (pad > 40)
@@ -1400,10 +1375,11 @@ static void push_wrapped(dlines_t *dl, const char *prefix, const char *body,
line[pad] = 0;
}
}
dlines_push(dl, line, pair, bold, dim);
if (i < n && body[i - 1] != '\n' && col >= body_w) {
/* soft wrap — already advanced */
if (dlines_push(dl, line, pair, bold, dim) == 0 && soft) {
dl->lines[dl->n - 1].cont = 1;
dl->lines[dl->n - 1].cont_pad = pad;
}
soft = body[i - 1] != '\n';
}
}
@@ -2338,7 +2314,7 @@ static const char *const TIPS[] = {
"/undo reverts the file changes of the last turn",
"/provider switches provider; keys are saved per provider",
"Click a tool card or Thought to expand it",
"Shift+drag selects text while mouse support is on",
"Drag over the transcript to copy it; double-click copies a word",
};
static void draw_home(tui_t *t) {
@@ -2548,6 +2524,57 @@ static void draw_side(tui_t *t) {
wrefresh(s);
}
/* Ordered selection [y1:x1, y2:x2] over display lines, snapped to words or
* whole lines by the click count; x2 < 0 means the end of the line. */
static void sel_range(const tui_t *t, const dlines_t *dl, int *y1, int *x1,
int *y2, int *x2) {
int ay = t->sel_ay, ax = t->sel_ax, by = t->sel_y, bx = t->sel_x;
if (by < ay || (by == ay && bx < ax)) {
int ty = ay, tx = ax;
ay = by, ax = bx, by = ty, bx = tx;
}
if (t->sel_mode == 2) {
int c1, c2;
if (ay >= 0 && ay < dl->n) {
cst_textsel_word(dl->lines[ay].text, dl->lines[ay].indent, ax, &c1, &c2);
ax = c1;
}
if (by >= 0 && by < dl->n) {
cst_textsel_word(dl->lines[by].text, dl->lines[by].indent, bx, &c1, &c2);
bx = c2;
}
} else if (t->sel_mode == 3) {
while (ay > 0 && ay < dl->n && dl->lines[ay].cont)
ay--;
while (by + 1 < dl->n && dl->lines[by + 1].cont)
by++;
ax = 0;
bx = -1;
}
*y1 = ay, *x1 = ax, *y2 = by, *x2 = bx;
}
static void draw_selection(tui_t *t, const dlines_t *dl, int h, int w) {
if (t->sel_state != SEL_DRAG && t->sel_state != SEL_SHOWN)
return;
int y1, x1, y2, x2;
sel_range(t, dl, &y1, &x1, &y2, &x2);
for (int row = 0; row < h; row++) {
int li = t->log_start + row;
if (li < y1 || li > y2)
continue;
int a = li == y1 ? x1 : 0;
int b = li == y2 && x2 >= 0 ? x2 : w - 1;
if (li < dl->n)
cst_textsel_snap(dl->lines[li].text, dl->lines[li].indent, &a, &b);
if (b >= w)
b = w - 1;
if (a <= b)
mvwchgat(t->logw, row, a, b - a + 1, A_REVERSE,
HAS_COLOR() ? CP_PANEL : 0, NULL);
}
}
static void draw_log(tui_t *t) {
if (!t->logw)
return;
@@ -2581,6 +2608,7 @@ static void draw_log(tui_t *t) {
int start = total - h - t->log_scroll;
if (start < 0)
start = 0;
t->log_start = start;
if (t->row_blk_cap < h) {
int *nb = realloc(t->row_blk, (size_t)h * sizeof(int));
@@ -2604,6 +2632,7 @@ static void draw_log(tui_t *t) {
if (L->bar)
put_wc(t->logw, row, 0, L'┃', HAS_COLOR() ? (int)COLOR_PAIR(L->bar) : 0);
}
draw_selection(t, &dl, h, w);
if (t->log_scroll > 0 && h > 2) {
char more[128];
snprintf(more, sizeof(more),
@@ -2777,8 +2806,17 @@ static int input_visual_rows(const tui_t *t) {
}
static void apply_mouse(const tui_t *t) {
mousemask(t->mouse_on ? (mmask_t)ALL_MOUSE_EVENTS : 0, NULL);
mousemask(t->mouse_on ? (mmask_t)(ALL_MOUSE_EVENTS | REPORT_MOUSE_POSITION)
: 0,
NULL);
mouseinterval(0);
#ifdef NCURSES_VERSION
/* ncurses only asks for presses and releases (1000). 1002 also reports
* motion while a button is held, for drag selection; terminals treat it as
* replacing 1000, so it goes after mousemask and is turned off explicitly. */
printf(t->mouse_on ? "\033[?1002h" : "\033[?1002l");
fflush(stdout);
#endif
}
static void free_windows(tui_t *t) {
@@ -3919,7 +3957,7 @@ static void cmd_help(tui_t *t, char *arg, int *quit) {
push_system_line(t, " Enter send · Esc cancel turn · Ctrl+C twice or Ctrl+D "
"quit · Ctrl+P command palette");
push_system_line(t, " Mouse: click cards / Thought to expand, wheel scrolls, "
"Shift+drag selects · /mouse");
"drag copies (double-click word, triple line) · /mouse");
push_system_line(t, " Rebind in keybinds.json next to settings.json");
push_system_line(t, "");
}
@@ -4346,8 +4384,10 @@ static void cmd_mouse(tui_t *t, char *arg, int *quit) {
(void)quit;
t->mouse_on = !t->mouse_on;
apply_mouse(t);
push_system_line(t, t->mouse_on ? "(mouse on · Shift+drag selects text)"
: "(mouse off · the terminal selects text)");
push_system_line(t, t->mouse_on
? "(mouse on · drag selects and copies (OSC 52) · "
"Shift+drag uses the terminal's own selection)"
: "(mouse off · the terminal selects text)");
}
static void cmd_max_steps(tui_t *t, char *arg, int *quit) {
@@ -4533,25 +4573,16 @@ static void cmd_export(tui_t *t, char *arg, int *quit) {
push_system_line(t, "(exported to %s)", path);
}
static void cmd_copy(tui_t *t, char *arg, int *quit) {
(void)arg;
(void)quit;
const char *text = NULL;
for (int i = t->hist.n - 1; i >= 0 && !text; i--)
if (t->hist.items[i].kind == BLK_ASSISTANT && t->hist.items[i].body)
text = t->hist.items[i].body;
if (!text || !text[0]) {
push_system_line(t, "Nothing to copy yet");
return;
}
/* Puts text on the terminal's clipboard; returns the bytes sent, 0 on
* failure. */
static size_t osc52_copy(const char *text, size_t n) {
static const char b64[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
size_t n = strlen(text);
if (n > 75000)
n = 75000; /* many terminals cap OSC 52 near 100 KB */
char *enc = malloc((n + 2) / 3 * 4 + 1);
if (!enc)
return;
return 0;
size_t o = 0;
for (size_t i = 0; i < n; i += 3) {
unsigned v = (unsigned char)text[i] << 16;
@@ -4571,8 +4602,24 @@ static void cmd_copy(tui_t *t, char *arg, int *quit) {
printf("\033]52;c;%s\a", enc);
fflush(stdout);
free(enc);
push_system_line(t, "(copied %zu bytes of the last reply via OSC 52 — the "
"terminal must allow clipboard access)", n);
return n;
}
static void cmd_copy(tui_t *t, char *arg, int *quit) {
(void)arg;
(void)quit;
const char *text = NULL;
for (int i = t->hist.n - 1; i >= 0 && !text; i--)
if (t->hist.items[i].kind == BLK_ASSISTANT && t->hist.items[i].body)
text = t->hist.items[i].body;
if (!text || !text[0]) {
push_system_line(t, "Nothing to copy yet");
return;
}
size_t n = osc52_copy(text, strlen(text));
if (n)
push_system_line(t, "(copied %zu bytes of the last reply via OSC 52 — the "
"terminal must allow clipboard access)", n);
}
static void cmd_init(tui_t *t, char *arg, int *quit) {
@@ -4906,7 +4953,7 @@ static const slash_cmd_t SLASH[] = {
1, cmd_approval},
{"yolo", "", "", "toggle approving everything", 1, cmd_yolo},
{"sidebar", "", "", "show or hide the session sidebar", 1, cmd_sidebar},
{"mouse", "", "", "toggle mouse support (Shift+drag selects)", 1,
{"mouse", "", "", "toggle mouse support (drag copies)", 1,
cmd_mouse},
{"max-steps", "", "[N]", "tool steps allowed per turn", 1, cmd_max_steps},
{"status", "", "", "session, model, context and settings", 1, cmd_status},
@@ -5437,6 +5484,145 @@ static int handle_click(tui_t *t, int y, int x, int busy) {
return 0;
}
/* Transcript cell under screen (y, x) as display line + column. Outside the
* transcript returns 0, unless `clamp` pulls the point to its nearest edge. */
static int log_cell(const tui_t *t, int y, int x, int clamp, int *li,
int *col) {
if (t->home || !t->logw)
return 0;
int lh, lw;
getmaxyx(t->logw, lh, lw);
int ry = y - t->log_y, rx = x - t->log_x;
if (!clamp && (ry < 0 || ry >= lh || rx < 0 || rx >= lw))
return 0;
*li = t->log_start + clampi(ry, 0, lh - 1);
*col = clampi(rx, 0, lw - 1);
return 1;
}
static void sel_clear(tui_t *t) {
int shown = t->sel_state == SEL_DRAG || t->sel_state == SEL_SHOWN;
t->sel_state = SEL_NONE;
if (shown)
draw_log(t);
}
static void sel_copy(tui_t *t) {
int h, w;
getmaxyx(t->logw, h, w);
(void)h;
dlines_t dl = {0};
build_display(t, &dl, w);
int y1, x1, y2, x2;
sel_range(t, &dl, &y1, &x1, &y2, &x2);
cst_selbuf_t b = {0};
for (int k = y1 < 0 ? 0 : y1; k <= y2 && k < dl.n; k++) {
const dline_t *L = &dl.lines[k];
int c1 = k == y1 ? x1 : 0, c2 = k == y2 ? x2 : -1;
if (k > y1) {
if (!L->cont)
cst_selbuf_putc(&b, '\n');
else if (c1 < L->indent + L->cont_pad)
c1 = L->indent + L->cont_pad;
}
int joined = k < y2 && k + 1 < dl.n && dl.lines[k + 1].cont;
cst_textsel_append(&b, L->text, L->indent, c1, c2, !joined);
}
dlines_free(&dl);
if (b.n && osc52_copy(b.s, b.n)) {
size_t chars = 0;
for (size_t i = 0; i < b.n; i++)
chars += ((unsigned char)b.s[i] & 0xC0) != 0x80;
char msg[96];
snprintf(msg, sizeof(msg), "Copied %zu characters (OSC 52)", chars);
set_status(t, msg);
draw_status(t);
}
cst_selbuf_free(&b);
}
/* Drag while the button is held: extend the selection, scrolling the
* transcript when the pointer is above or below it. */
static void sel_motion(tui_t *t, int y, int x) {
int lh, lw;
getmaxyx(t->logw, lh, lw);
(void)lw;
int ry = y - t->log_y;
if (ry < 0 || ry >= lh) {
int before = t->log_scroll;
t->log_scroll += ry < 0 ? 1 : -1;
if (t->log_scroll < 0)
t->log_scroll = 0;
if (t->log_scroll != before)
draw_log(t); /* updates log_start */
}
int li, col;
if (!log_cell(t, y, x, 1, &li, &col))
return;
if (t->sel_state == SEL_PRESS && li == t->sel_ay && col == t->sel_ax)
return;
t->sel_state = SEL_DRAG;
t->sel_y = li;
t->sel_x = col;
draw_log(t);
}
/* Wheel, clicks and drag selection; -2 means quit. */
static int on_mouse(tui_t *t, const MEVENT *ev, int busy) {
mmask_t b = ev->bstate;
if (b & BUTTON4_PRESSED) {
t->log_scroll += SCROLL_SPEED;
draw_log(t);
return 0;
}
if (b & BUTTON5_PRESSED) {
t->log_scroll -= SCROLL_SPEED;
if (t->log_scroll < 0)
t->log_scroll = 0;
draw_log(t);
return 0;
}
if (b & REPORT_MOUSE_POSITION) {
if (t->sel_state == SEL_PRESS || t->sel_state == SEL_DRAG)
sel_motion(t, ev->y, ev->x);
return 0;
}
if (b & (BUTTON1_PRESSED | BUTTON1_CLICKED)) {
sel_clear(t);
int li, col;
if (!(b & BUTTON1_PRESSED) || !log_cell(t, ev->y, ev->x, 0, &li, &col))
return handle_click(t, ev->y, ev->x, busy);
if (t->sel_clicks && since_ms(&t->sel_click_at) < MULTI_CLICK_MS &&
li == t->sel_click_y && abs(col - t->sel_click_x) <= 1)
t->sel_clicks = t->sel_clicks % 3 + 1;
else
t->sel_clicks = 1;
clock_gettime(CLOCK_MONOTONIC, &t->sel_click_at);
t->sel_click_y = li;
t->sel_click_x = col;
t->sel_mode = t->sel_clicks;
t->sel_ay = t->sel_y = li;
t->sel_ax = t->sel_x = col;
t->sel_press_y = ev->y;
t->sel_press_x = ev->x;
t->sel_state = t->sel_clicks > 1 ? SEL_DRAG : SEL_PRESS;
if (t->sel_state == SEL_DRAG)
draw_log(t);
return 0;
}
if (b & BUTTON1_RELEASED) {
if (t->sel_state == SEL_PRESS) {
t->sel_state = SEL_NONE;
return handle_click(t, t->sel_press_y, t->sel_press_x, busy);
}
if (t->sel_state == SEL_DRAG) {
sel_copy(t);
t->sel_state = SEL_SHOWN;
}
}
return 0;
}
/* Tab completes after "/" at the start or an "@word" under the cursor. */
static int wants_completion(const tui_t *t) {
if (t->in_len && t->inbuf[0] == '/')
@@ -6192,6 +6378,9 @@ int cst_tui_run(cst_agent_t *agent) {
int rc = tui_wget_wch(t.input, &wch);
if (rc == ERR)
continue;
if (t.sel_state == SEL_SHOWN &&
!(rc == KEY_CODE_YES && (wch == KEY_MOUSE || wch == KEY_RESIZE)))
sel_clear(&t);
if (!t.in_paste &&
!(rc == KEY_CODE_YES &&
@@ -6263,6 +6452,7 @@ int cst_tui_run(cst_agent_t *agent) {
continue;
}
if (wch == KEY_RESIZE) {
t.sel_state = SEL_NONE; /* display lines rewrap */
layout(&t);
wtimeout(t.input, 80);
redraw_all(&t);
@@ -6271,18 +6461,8 @@ int cst_tui_run(cst_agent_t *agent) {
if (wch == KEY_MOUSE) {
MEVENT ev;
if (getmouse(&ev) == OK) {
if (ev.bstate & BUTTON4_PRESSED) {
t.log_scroll += SCROLL_SPEED;
draw_log(&t);
} else if (ev.bstate & BUTTON5_PRESSED) {
t.log_scroll -= SCROLL_SPEED;
if (t.log_scroll < 0)
t.log_scroll = 0;
draw_log(&t);
} else if (ev.bstate & (BUTTON1_PRESSED | BUTTON1_CLICKED)) {
if (handle_click(&t, ev.y, ev.x, busy) == -2)
goto out;
}
if (on_mouse(&t, &ev, busy) == -2)
goto out;
draw_input(&t); /* keep the terminal cursor in the box */
}
continue;
@@ -6495,7 +6675,7 @@ out:
tui_free(&t);
#ifdef NCURSES_VERSION
kitty_restore();
printf("\033[?2004l");
printf("\033[?1002l\033[?2004l");
fflush(stdout);
#endif
endwin();