mirror of
https://codeberg.org/dnkl/foot.git
synced 2026-02-05 04:06:08 -05:00
Previously when updating a selection, we would unmark *all* cells in the old selection, and then mark all cells in the new selection. This caused *all* cells to be dirtied and thus re-rendered. Avoid this, by adding a temporary state to the cells' selected state. Before unmarking the old selection, pre-mark the new selection using a temporary state. When unmarking the old selection, ignore cells in this temporary state. When marking the new selection, ignore cells in this temporary state (except clearing the temporary state).
1131 lines
29 KiB
C
1131 lines
29 KiB
C
#include "selection.h"
|
|
|
|
#include <ctype.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <fcntl.h>
|
|
#include <errno.h>
|
|
#include <wctype.h>
|
|
|
|
#include <sys/epoll.h>
|
|
|
|
#define LOG_MODULE "selection"
|
|
#define LOG_ENABLE_DBG 0
|
|
#include "log.h"
|
|
|
|
#include "async.h"
|
|
#include "grid.h"
|
|
#include "misc.h"
|
|
#include "render.h"
|
|
#include "vt.h"
|
|
|
|
#define min(x, y) ((x) < (y) ? (x) : (y))
|
|
#define max(x, y) ((x) > (y) ? (x) : (y))
|
|
|
|
bool
|
|
selection_enabled(const struct terminal *term)
|
|
{
|
|
return
|
|
term->mouse_tracking == MOUSE_NONE ||
|
|
term_mouse_grabbed(term) ||
|
|
term->is_searching;
|
|
}
|
|
|
|
bool
|
|
selection_on_row_in_view(const struct terminal *term, int row_no)
|
|
{
|
|
if (term->selection.start.row == -1 || term->selection.end.row == -1)
|
|
return false;
|
|
|
|
const struct coord *start = &term->selection.start;
|
|
const struct coord *end = &term->selection.end;
|
|
assert(start->row <= end->row);
|
|
|
|
row_no += term->grid->view;
|
|
return row_no >= start->row && row_no <= end->row;
|
|
}
|
|
|
|
static void
|
|
foreach_selected_normal(
|
|
struct terminal *term, struct coord _start, struct coord _end,
|
|
void (*cb)(struct terminal *term, struct row *row, struct cell *cell, void *data),
|
|
void *data)
|
|
{
|
|
const struct coord *start = &_start;
|
|
const struct coord *end = &_end;
|
|
|
|
int start_row, end_row;
|
|
int start_col, end_col;
|
|
|
|
if (start->row < end->row) {
|
|
start_row = start->row;
|
|
end_row = end->row;
|
|
start_col = start->col;
|
|
end_col = end->col;
|
|
} else if (start->row > end->row) {
|
|
start_row = end->row;
|
|
end_row = start->row;
|
|
start_col = end->col;
|
|
end_col = start->col;
|
|
} else {
|
|
start_row = end_row = start->row;
|
|
start_col = min(start->col, end->col);
|
|
end_col = max(start->col, end->col);
|
|
}
|
|
|
|
for (int r = start_row; r <= end_row; r++) {
|
|
size_t real_r = r & (term->grid->num_rows - 1);
|
|
struct row *row = term->grid->rows[real_r];
|
|
assert(row != NULL);
|
|
|
|
for (int c = start_col;
|
|
c <= (r == end_row ? end_col : term->cols - 1);
|
|
c++)
|
|
{
|
|
cb(term, row, &row->cells[c], data);
|
|
}
|
|
|
|
start_col = 0;
|
|
}
|
|
}
|
|
|
|
static void
|
|
foreach_selected_block(
|
|
struct terminal *term, struct coord _start, struct coord _end,
|
|
void (*cb)(struct terminal *term, struct row *row, struct cell *cell, void *data),
|
|
void *data)
|
|
{
|
|
const struct coord *start = &_start;
|
|
const struct coord *end = &_end;
|
|
|
|
struct coord top_left = {
|
|
.row = min(start->row, end->row),
|
|
.col = min(start->col, end->col),
|
|
};
|
|
|
|
struct coord bottom_right = {
|
|
.row = max(start->row, end->row),
|
|
.col = max(start->col, end->col),
|
|
};
|
|
|
|
for (int r = top_left.row; r <= bottom_right.row; r++) {
|
|
size_t real_r = r & (term->grid->num_rows - 1);
|
|
struct row *row = term->grid->rows[real_r];
|
|
assert(row != NULL);
|
|
|
|
for (int c = top_left.col; c <= bottom_right.col; c++)
|
|
cb(term, row, &row->cells[c], data);
|
|
}
|
|
}
|
|
|
|
static void
|
|
foreach_selected(
|
|
struct terminal *term, struct coord start, struct coord end,
|
|
void (*cb)(struct terminal *term, struct row *row, struct cell *cell, void *data),
|
|
void *data)
|
|
{
|
|
switch (term->selection.kind) {
|
|
case SELECTION_NORMAL:
|
|
return foreach_selected_normal(term, start, end, cb, data);
|
|
|
|
case SELECTION_BLOCK:
|
|
return foreach_selected_block(term, start, end, cb, data);
|
|
|
|
case SELECTION_NONE:
|
|
assert(false);
|
|
return;
|
|
}
|
|
|
|
assert(false);
|
|
}
|
|
|
|
static size_t
|
|
min_bufsize_for_extraction(const struct terminal *term)
|
|
{
|
|
const struct coord *start = &term->selection.start;
|
|
const struct coord *end = &term->selection.end;
|
|
|
|
switch (term->selection.kind) {
|
|
case SELECTION_NONE:
|
|
return 0;
|
|
|
|
case SELECTION_NORMAL:
|
|
if (term->selection.end.row == -1)
|
|
return 0;
|
|
|
|
assert(term->selection.start.row != -1);
|
|
|
|
if (start->row > end->row) {
|
|
const struct coord *tmp = start;
|
|
start = end;
|
|
end = tmp;
|
|
}
|
|
|
|
if (start->row == end->row)
|
|
return end->col - start->col + 1;
|
|
else {
|
|
size_t cells = 0;
|
|
|
|
/* Add one extra column on each row, for \n */
|
|
|
|
cells += term->cols - start->col + 1;
|
|
cells += (term->cols + 1) * (end->row - start->row - 1);
|
|
cells += end->col + 1 + 1;
|
|
return cells;
|
|
}
|
|
|
|
case SELECTION_BLOCK: {
|
|
struct coord top_left = {
|
|
.row = min(start->row, end->row),
|
|
.col = min(start->col, end->col),
|
|
};
|
|
|
|
struct coord bottom_right = {
|
|
.row = max(start->row, end->row),
|
|
.col = max(start->col, end->col),
|
|
};
|
|
|
|
/* Add one extra column on each row, for \n */
|
|
int cols = bottom_right.col - top_left.col + 1 + 1;
|
|
int rows = bottom_right.row - top_left.row + 1;
|
|
return rows * cols;
|
|
}
|
|
}
|
|
|
|
assert(false);
|
|
return 0;
|
|
}
|
|
|
|
struct extract {
|
|
wchar_t *buf;
|
|
size_t size;
|
|
size_t idx;
|
|
size_t empty_count;
|
|
struct row *last_row;
|
|
struct cell *last_cell;
|
|
};
|
|
|
|
static void
|
|
extract_one(struct terminal *term, struct row *row, struct cell *cell,
|
|
void *data)
|
|
{
|
|
struct extract *ctx = data;
|
|
|
|
if (ctx->last_row != NULL && row != ctx->last_row &&
|
|
((term->selection.kind == SELECTION_NORMAL && ctx->last_cell->wc == 0) ||
|
|
term->selection.kind == SELECTION_BLOCK))
|
|
{
|
|
/* Last cell was the last column in the selection */
|
|
ctx->buf[ctx->idx++] = L'\n';
|
|
ctx->empty_count = 0;
|
|
}
|
|
|
|
if (cell->wc == 0) {
|
|
ctx->empty_count++;
|
|
ctx->last_row = row;
|
|
ctx->last_cell = cell;
|
|
return;
|
|
}
|
|
|
|
/* Replace empty cells with spaces when followed by non-empty cell */
|
|
assert(ctx->idx + ctx->empty_count <= ctx->size);
|
|
for (size_t i = 0; i < ctx->empty_count; i++)
|
|
ctx->buf[ctx->idx++] = L' ';
|
|
ctx->empty_count = 0;
|
|
|
|
assert(ctx->idx + 1 <= ctx->size);
|
|
ctx->buf[ctx->idx++] = cell->wc;
|
|
|
|
ctx->last_row = row;
|
|
ctx->last_cell = cell;
|
|
}
|
|
|
|
static char *
|
|
extract_selection(const struct terminal *term)
|
|
{
|
|
const size_t max_cells = min_bufsize_for_extraction(term);
|
|
const size_t buf_size = max_cells + 1;
|
|
|
|
struct extract ctx = {
|
|
.buf = malloc(buf_size * sizeof(wchar_t)),
|
|
.size = buf_size,
|
|
};
|
|
|
|
foreach_selected(
|
|
(struct terminal *)term, term->selection.start, term->selection.end,
|
|
&extract_one, &ctx);
|
|
|
|
if (ctx.idx == 0) {
|
|
/* Selection of empty cells only */
|
|
ctx.buf[ctx.idx] = L'\0';
|
|
} else {
|
|
assert(ctx.idx > 0);
|
|
assert(ctx.idx < ctx.size);
|
|
if (ctx.buf[ctx.idx - 1] == L'\n')
|
|
ctx.buf[ctx.idx - 1] = L'\0';
|
|
else
|
|
ctx.buf[ctx.idx] = L'\0';
|
|
}
|
|
|
|
size_t len = wcstombs(NULL, ctx.buf, 0);
|
|
if (len == (size_t)-1) {
|
|
LOG_ERRNO("failed to convert selection to UTF-8");
|
|
free(ctx.buf);
|
|
return NULL;
|
|
}
|
|
|
|
char *ret = malloc(len + 1);
|
|
wcstombs(ret, ctx.buf, len + 1);
|
|
free(ctx.buf);
|
|
return ret;
|
|
}
|
|
|
|
void
|
|
selection_start(struct terminal *term, int col, int row,
|
|
enum selection_kind kind)
|
|
{
|
|
if (!selection_enabled(term))
|
|
return;
|
|
|
|
selection_cancel(term);
|
|
|
|
LOG_DBG("%s selection started at %d,%d",
|
|
kind == SELECTION_NORMAL ? "normal" :
|
|
kind == SELECTION_BLOCK ? "block" : "<unknown>",
|
|
row, col);
|
|
term->selection.kind = kind;
|
|
term->selection.start = (struct coord){col, term->grid->view + row};
|
|
term->selection.end = (struct coord){-1, -1};
|
|
}
|
|
|
|
static void
|
|
unmark_selected(struct terminal *term, struct row *row, struct cell *cell,
|
|
void *data)
|
|
{
|
|
if (cell->attrs.selected == 0 || (cell->attrs.selected & 2)) {
|
|
/* Ignore if already deselected, or if premarked for updated selection */
|
|
return;
|
|
}
|
|
|
|
row->dirty = 1;
|
|
cell->attrs.selected = 0;
|
|
cell->attrs.clean = 0;
|
|
}
|
|
|
|
static void
|
|
premark_selected(struct terminal *term, struct row *row, struct cell *cell,
|
|
void *data)
|
|
{
|
|
/* Tell unmark to leave this be */
|
|
cell->attrs.selected |= 2;
|
|
}
|
|
|
|
static void
|
|
mark_selected(struct terminal *term, struct row *row, struct cell *cell,
|
|
void *data)
|
|
{
|
|
if (cell->attrs.selected & 1) {
|
|
cell->attrs.selected = 1; /* Clear the pre-mark bit */
|
|
return;
|
|
}
|
|
|
|
row->dirty = 1;
|
|
cell->attrs.selected = 1;
|
|
cell->attrs.clean = 0;
|
|
}
|
|
|
|
void
|
|
selection_update(struct terminal *term, int col, int row)
|
|
{
|
|
if (!selection_enabled(term))
|
|
return;
|
|
|
|
LOG_DBG("selection updated: start = %d,%d, end = %d,%d -> %d, %d",
|
|
term->selection.start.row, term->selection.start.col,
|
|
term->selection.end.row, term->selection.end.col,
|
|
row, col);
|
|
|
|
assert(term->selection.start.row != -1);
|
|
assert(term->grid->view + row != -1);
|
|
|
|
struct coord new_end = {col, term->grid->view + row};
|
|
|
|
/* Premark all cells that *will* be selected */
|
|
foreach_selected(
|
|
term, term->selection.start, new_end, &premark_selected, NULL);
|
|
|
|
if (term->selection.end.row != -1) {
|
|
/* Unmark previous selection, ignoring cells that are part of
|
|
* the new selection */
|
|
foreach_selected(term, term->selection.start, term->selection.end,
|
|
&unmark_selected, NULL);
|
|
}
|
|
|
|
term->selection.end = new_end;
|
|
assert(term->selection.start.row != -1 && term->selection.end.row != -1);
|
|
|
|
/* Mark new selection */
|
|
foreach_selected(
|
|
term, term->selection.start, term->selection.end, &mark_selected, NULL);
|
|
|
|
render_refresh(term);
|
|
}
|
|
|
|
static const struct zwp_primary_selection_source_v1_listener primary_selection_source_listener;
|
|
|
|
void
|
|
selection_finalize(struct terminal *term, uint32_t serial)
|
|
{
|
|
if (!selection_enabled(term))
|
|
return;
|
|
|
|
if (term->selection.start.row == -1 || term->selection.end.row == -1)
|
|
return;
|
|
|
|
assert(term->selection.start.row != -1);
|
|
assert(term->selection.end.row != -1);
|
|
|
|
if (term->selection.start.row > term->selection.end.row ||
|
|
(term->selection.start.row == term->selection.end.row &&
|
|
term->selection.start.col > term->selection.end.col))
|
|
{
|
|
struct coord tmp = term->selection.start;
|
|
term->selection.start = term->selection.end;
|
|
term->selection.end = tmp;
|
|
}
|
|
|
|
assert(term->selection.start.row <= term->selection.end.row);
|
|
selection_to_primary(term, serial);
|
|
}
|
|
|
|
void
|
|
selection_cancel(struct terminal *term)
|
|
{
|
|
if (!selection_enabled(term))
|
|
return;
|
|
|
|
LOG_DBG("selection cancelled: start = %d,%d end = %d,%d",
|
|
term->selection.start.row, term->selection.start.col,
|
|
term->selection.end.row, term->selection.end.col);
|
|
|
|
if (term->selection.start.row != -1 && term->selection.end.row != -1) {
|
|
foreach_selected(
|
|
term, term->selection.start, term->selection.end,
|
|
&unmark_selected, NULL);
|
|
render_refresh(term);
|
|
}
|
|
|
|
term->selection.kind = SELECTION_NONE;
|
|
term->selection.start = (struct coord){-1, -1};
|
|
term->selection.end = (struct coord){-1, -1};
|
|
}
|
|
|
|
void
|
|
selection_mark_word(struct terminal *term, int col, int row, bool spaces_only,
|
|
uint32_t serial)
|
|
{
|
|
if (!selection_enabled(term))
|
|
return;
|
|
|
|
selection_cancel(term);
|
|
|
|
struct coord start = {col, row};
|
|
struct coord end = {col, row};
|
|
|
|
const struct row *r = grid_row_in_view(term->grid, start.row);
|
|
wchar_t c = r->cells[start.col].wc;
|
|
|
|
if (!(c == 0 || !isword(c, spaces_only))) {
|
|
while (true) {
|
|
int next_col = start.col - 1;
|
|
int next_row = start.row;
|
|
|
|
/* Linewrap */
|
|
if (next_col < 0) {
|
|
next_col = term->cols - 1;
|
|
if (--next_row < 0)
|
|
break;
|
|
}
|
|
|
|
const struct row *row = grid_row_in_view(term->grid, next_row);
|
|
|
|
c = row->cells[next_col].wc;
|
|
if (c == 0 || !isword(c, spaces_only))
|
|
break;
|
|
|
|
start.col = next_col;
|
|
start.row = next_row;
|
|
}
|
|
}
|
|
|
|
r = grid_row_in_view(term->grid, end.row);
|
|
c = r->cells[end.col].wc;
|
|
|
|
if (!(c == 0 || !isword(c, spaces_only))) {
|
|
while (true) {
|
|
int next_col = end.col + 1;
|
|
int next_row = end.row;
|
|
|
|
/* Linewrap */
|
|
if (next_col >= term->cols) {
|
|
next_col = 0;
|
|
if (++next_row >= term->rows)
|
|
break;
|
|
}
|
|
|
|
const struct row *row = grid_row_in_view(term->grid, next_row);
|
|
|
|
c = row->cells[next_col].wc;
|
|
if (c == '\0' || !isword(c, spaces_only))
|
|
break;
|
|
|
|
end.col = next_col;
|
|
end.row = next_row;
|
|
}
|
|
}
|
|
|
|
selection_start(term, start.col, start.row, SELECTION_NORMAL);
|
|
selection_update(term, end.col, end.row);
|
|
selection_finalize(term, serial);
|
|
}
|
|
|
|
void
|
|
selection_mark_row(struct terminal *term, int row, uint32_t serial)
|
|
{
|
|
selection_start(term, 0, row, SELECTION_NORMAL);
|
|
selection_update(term, term->cols - 1, row);
|
|
selection_finalize(term, serial);
|
|
}
|
|
|
|
static void
|
|
target(void *data, struct wl_data_source *wl_data_source, const char *mime_type)
|
|
{
|
|
LOG_WARN("TARGET: mime-type=%s", mime_type);
|
|
}
|
|
|
|
struct clipboard_send {
|
|
char *data;
|
|
size_t len;
|
|
size_t idx;
|
|
};
|
|
|
|
static bool
|
|
fdm_send(struct fdm *fdm, int fd, int events, void *data)
|
|
{
|
|
struct clipboard_send *ctx = data;
|
|
|
|
if (events & EPOLLHUP)
|
|
goto done;
|
|
|
|
switch (async_write(fd, ctx->data, ctx->len, &ctx->idx)) {
|
|
case ASYNC_WRITE_REMAIN:
|
|
return true;
|
|
|
|
case ASYNC_WRITE_DONE:
|
|
break;
|
|
|
|
case ASYNC_WRITE_ERR:
|
|
LOG_ERRNO(
|
|
"failed to asynchronously write %zu of selection data to FD=%d",
|
|
ctx->len - ctx->idx, fd);
|
|
break;
|
|
}
|
|
|
|
done:
|
|
fdm_del(fdm, fd);
|
|
free(ctx->data);
|
|
free(ctx);
|
|
return true;
|
|
}
|
|
|
|
static void
|
|
send(void *data, struct wl_data_source *wl_data_source, const char *mime_type,
|
|
int32_t fd)
|
|
{
|
|
struct wayland *wayl = data;
|
|
const struct wl_clipboard *clipboard = &wayl->clipboard;
|
|
|
|
assert(clipboard != NULL);
|
|
assert(clipboard->text != NULL);
|
|
|
|
const char *selection = clipboard->text;
|
|
const size_t len = strlen(selection);
|
|
|
|
/* Make it NONBLOCK:ing right away - we don't want to block if the
|
|
* initial attempt to send the data synchronously fails */
|
|
int flags;
|
|
if ((flags = fcntl(fd, F_GETFL)) < 0 ||
|
|
fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0)
|
|
{
|
|
LOG_ERRNO("failed to set O_NONBLOCK");
|
|
return;
|
|
}
|
|
|
|
size_t async_idx = 0;
|
|
switch (async_write(fd, selection, len, &async_idx)) {
|
|
case ASYNC_WRITE_REMAIN: {
|
|
struct clipboard_send *ctx = malloc(sizeof(*ctx));
|
|
*ctx = (struct clipboard_send) {
|
|
.data = strdup(selection),
|
|
.len = len,
|
|
.idx = async_idx,
|
|
};
|
|
|
|
if (fdm_add(wayl->fdm, fd, EPOLLOUT, &fdm_send, ctx))
|
|
return;
|
|
|
|
free(ctx->data);
|
|
free(ctx);
|
|
break;
|
|
}
|
|
|
|
case ASYNC_WRITE_DONE:
|
|
break;
|
|
|
|
case ASYNC_WRITE_ERR:
|
|
LOG_ERRNO(
|
|
"failed to write %zu bytes of clipboard selection data to FD=%d",
|
|
len, fd);
|
|
break;
|
|
}
|
|
|
|
close(fd);
|
|
}
|
|
|
|
static void
|
|
cancelled(void *data, struct wl_data_source *wl_data_source)
|
|
{
|
|
struct wayland *wayl = data;
|
|
struct wl_clipboard *clipboard = &wayl->clipboard;
|
|
assert(clipboard->data_source == wl_data_source);
|
|
|
|
wl_data_source_destroy(clipboard->data_source);
|
|
clipboard->data_source = NULL;
|
|
clipboard->serial = 0;
|
|
|
|
free(clipboard->text);
|
|
clipboard->text = NULL;
|
|
}
|
|
|
|
static void
|
|
dnd_drop_performed(void *data, struct wl_data_source *wl_data_source)
|
|
{
|
|
}
|
|
|
|
static void
|
|
dnd_finished(void *data, struct wl_data_source *wl_data_source)
|
|
{
|
|
}
|
|
|
|
static void
|
|
action(void *data, struct wl_data_source *wl_data_source, uint32_t dnd_action)
|
|
{
|
|
}
|
|
|
|
static const struct wl_data_source_listener data_source_listener = {
|
|
.target = &target,
|
|
.send = &send,
|
|
.cancelled = &cancelled,
|
|
.dnd_drop_performed = &dnd_drop_performed,
|
|
.dnd_finished = &dnd_finished,
|
|
.action = &action,
|
|
};
|
|
|
|
static void
|
|
primary_send(void *data,
|
|
struct zwp_primary_selection_source_v1 *zwp_primary_selection_source_v1,
|
|
const char *mime_type, int32_t fd)
|
|
{
|
|
struct wayland *wayl = data;
|
|
const struct wl_primary *primary = &wayl->primary;
|
|
|
|
assert(primary != NULL);
|
|
assert(primary->text != NULL);
|
|
|
|
const char *selection = primary->text;
|
|
const size_t len = strlen(selection);
|
|
|
|
int flags;
|
|
if ((flags = fcntl(fd, F_GETFL)) < 0 ||
|
|
fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0)
|
|
{
|
|
LOG_ERRNO("failed to set O_NONBLOCK");
|
|
return;
|
|
}
|
|
|
|
size_t async_idx = 0;
|
|
switch (async_write(fd, selection, len, &async_idx)) {
|
|
case ASYNC_WRITE_REMAIN: {
|
|
struct clipboard_send *ctx = malloc(sizeof(*ctx));
|
|
*ctx = (struct clipboard_send) {
|
|
.data = strdup(selection),
|
|
.len = len,
|
|
.idx = async_idx,
|
|
};
|
|
|
|
if (fdm_add(wayl->fdm, fd, EPOLLOUT, &fdm_send, ctx))
|
|
return;
|
|
|
|
free(ctx->data);
|
|
free(ctx);
|
|
break;
|
|
}
|
|
|
|
case ASYNC_WRITE_DONE:
|
|
break;
|
|
|
|
case ASYNC_WRITE_ERR:
|
|
LOG_ERRNO(
|
|
"failed to write %zu bytes of primary selection data to FD=%d",
|
|
len, fd);
|
|
break;
|
|
}
|
|
|
|
close(fd);
|
|
}
|
|
|
|
static void
|
|
primary_cancelled(void *data,
|
|
struct zwp_primary_selection_source_v1 *zwp_primary_selection_source_v1)
|
|
{
|
|
struct wayland *wayl = data;
|
|
struct wl_primary *primary = &wayl->primary;
|
|
|
|
zwp_primary_selection_source_v1_destroy(primary->data_source);
|
|
primary->data_source = NULL;
|
|
primary->serial = 0;
|
|
|
|
free(primary->text);
|
|
primary->text = NULL;
|
|
}
|
|
|
|
static const struct zwp_primary_selection_source_v1_listener primary_selection_source_listener = {
|
|
.send = &primary_send,
|
|
.cancelled = &primary_cancelled,
|
|
};
|
|
|
|
bool
|
|
text_to_clipboard(struct terminal *term, char *text, uint32_t serial)
|
|
{
|
|
struct wl_clipboard *clipboard = &term->wl->clipboard;
|
|
|
|
if (clipboard->data_source != NULL) {
|
|
/* Kill previous data source */
|
|
assert(clipboard->serial != 0);
|
|
wl_data_device_set_selection(term->wl->data_device, NULL, clipboard->serial);
|
|
wl_data_source_destroy(clipboard->data_source);
|
|
free(clipboard->text);
|
|
|
|
clipboard->data_source = NULL;
|
|
clipboard->serial = 0;
|
|
}
|
|
|
|
clipboard->data_source
|
|
= wl_data_device_manager_create_data_source(term->wl->data_device_manager);
|
|
|
|
if (clipboard->data_source == NULL) {
|
|
LOG_ERR("failed to create clipboard data source");
|
|
return false;
|
|
}
|
|
|
|
clipboard->text = text;
|
|
|
|
/* Configure source */
|
|
wl_data_source_offer(clipboard->data_source, "text/plain;charset=utf-8");
|
|
wl_data_source_add_listener(clipboard->data_source, &data_source_listener, term->wl);
|
|
wl_data_device_set_selection(term->wl->data_device, clipboard->data_source, serial);
|
|
|
|
/* Needed when sending the selection to other client */
|
|
assert(serial != 0);
|
|
clipboard->serial = serial;
|
|
return true;
|
|
}
|
|
|
|
void
|
|
selection_to_clipboard(struct terminal *term, uint32_t serial)
|
|
{
|
|
if (term->selection.start.row == -1 || term->selection.end.row == -1)
|
|
return;
|
|
|
|
/* Get selection as a string */
|
|
char *text = extract_selection(term);
|
|
if (!text_to_clipboard(term, text, serial))
|
|
free(text);
|
|
}
|
|
|
|
struct clipboard_receive {
|
|
/* Callback data */
|
|
void (*cb)(const char *data, size_t size, void *user);
|
|
void (*done)(void *user);
|
|
void *user;
|
|
};
|
|
|
|
static bool
|
|
fdm_receive(struct fdm *fdm, int fd, int events, void *data)
|
|
{
|
|
struct clipboard_receive *ctx = data;
|
|
|
|
if ((events & EPOLLHUP) && !(events & EPOLLIN))
|
|
goto done;
|
|
|
|
/* Read until EOF */
|
|
while (true) {
|
|
char text[256];
|
|
ssize_t count = read(fd, text, sizeof(text));
|
|
|
|
if (count == -1) {
|
|
if (errno == EAGAIN || errno == EWOULDBLOCK)
|
|
return true;
|
|
|
|
LOG_ERRNO("failed to read clipboard data");
|
|
break;
|
|
}
|
|
|
|
if (count == 0)
|
|
break;
|
|
|
|
/* Call cb while at same time replacing \r\n with \n */
|
|
const char *p = text;
|
|
size_t left = count;
|
|
again:
|
|
for (size_t i = 0; i < left - 1; i++) {
|
|
if (p[i] == '\r' && p[i + 1] == '\n') {
|
|
ctx->cb(p, i, ctx->user);
|
|
|
|
assert(i + 1 <= left);
|
|
p += i + 1;
|
|
left -= i + 1;
|
|
goto again;
|
|
}
|
|
}
|
|
|
|
ctx->cb(p, left, ctx->user);
|
|
left = 0;
|
|
}
|
|
|
|
done:
|
|
fdm_del(fdm, fd);
|
|
ctx->done(ctx->user);
|
|
free(ctx);
|
|
return true;
|
|
}
|
|
|
|
static void
|
|
begin_receive_clipboard(struct terminal *term, int read_fd,
|
|
void (*cb)(const char *data, size_t size, void *user),
|
|
void (*done)(void *user), void *user)
|
|
{
|
|
int flags;
|
|
if ((flags = fcntl(read_fd, F_GETFL)) < 0 ||
|
|
fcntl(read_fd, F_SETFL, flags | O_NONBLOCK) < 0)
|
|
{
|
|
LOG_ERRNO("failed to set O_NONBLOCK");
|
|
close(read_fd);
|
|
return done(user);
|
|
}
|
|
|
|
struct clipboard_receive *ctx = malloc(sizeof(*ctx));
|
|
*ctx = (struct clipboard_receive) {
|
|
.cb = cb,
|
|
.done = done,
|
|
.user = user,
|
|
};
|
|
|
|
if (!fdm_add(term->fdm, read_fd, EPOLLIN, &fdm_receive, ctx)) {
|
|
close(read_fd);
|
|
free(ctx);
|
|
done(user);
|
|
}
|
|
}
|
|
|
|
void
|
|
text_from_clipboard(struct terminal *term, uint32_t serial,
|
|
void (*cb)(const char *data, size_t size, void *user),
|
|
void (*done)(void *user), void *user)
|
|
{
|
|
struct wl_clipboard *clipboard = &term->wl->clipboard;
|
|
if (clipboard->data_offer == NULL)
|
|
return done(user);
|
|
|
|
/* Prepare a pipe the other client can write its selection to us */
|
|
int fds[2];
|
|
if (pipe2(fds, O_CLOEXEC) == -1) {
|
|
LOG_ERRNO("failed to create pipe");
|
|
return done(user);
|
|
}
|
|
|
|
int read_fd = fds[0];
|
|
int write_fd = fds[1];
|
|
|
|
/* Give write-end of pipe to other client */
|
|
wl_data_offer_receive(
|
|
clipboard->data_offer, "text/plain;charset=utf-8", write_fd);
|
|
|
|
/* Don't keep our copy of the write-end open (or we'll never get EOF) */
|
|
close(write_fd);
|
|
|
|
begin_receive_clipboard(term, read_fd, cb, done, user);
|
|
}
|
|
|
|
static void
|
|
from_clipboard_cb(const char *data, size_t size, void *user)
|
|
{
|
|
struct terminal *term = user;
|
|
term_to_slave(term, data, size);
|
|
}
|
|
|
|
static void
|
|
from_clipboard_done(void *user)
|
|
{
|
|
struct terminal *term = user;
|
|
|
|
if (term->bracketed_paste)
|
|
term_to_slave(term, "\033[201~", 6);
|
|
}
|
|
|
|
void
|
|
selection_from_clipboard(struct terminal *term, uint32_t serial)
|
|
{
|
|
struct wl_clipboard *clipboard = &term->wl->clipboard;
|
|
if (clipboard->data_offer == NULL)
|
|
return;
|
|
|
|
if (term->bracketed_paste)
|
|
term_to_slave(term, "\033[200~", 6);
|
|
|
|
text_from_clipboard(
|
|
term, serial, &from_clipboard_cb, &from_clipboard_done, term);
|
|
}
|
|
|
|
bool
|
|
text_to_primary(struct terminal *term, char *text, uint32_t serial)
|
|
{
|
|
if (term->wl->primary_selection_device_manager == NULL)
|
|
return false;
|
|
|
|
struct wl_primary *primary = &term->wl->primary;
|
|
|
|
/* TODO: somehow share code with the clipboard equivalent */
|
|
if (term->wl->primary.data_source != NULL) {
|
|
/* Kill previous data source */
|
|
|
|
assert(primary->serial != 0);
|
|
zwp_primary_selection_device_v1_set_selection(
|
|
term->wl->primary_selection_device, NULL, primary->serial);
|
|
zwp_primary_selection_source_v1_destroy(primary->data_source);
|
|
free(primary->text);
|
|
|
|
primary->data_source = NULL;
|
|
primary->serial = 0;
|
|
}
|
|
|
|
primary->data_source
|
|
= zwp_primary_selection_device_manager_v1_create_source(
|
|
term->wl->primary_selection_device_manager);
|
|
|
|
if (primary->data_source == NULL) {
|
|
LOG_ERR("failed to create clipboard data source");
|
|
return false;
|
|
}
|
|
|
|
/* Get selection as a string */
|
|
primary->text = text;
|
|
|
|
/* Configure source */
|
|
zwp_primary_selection_source_v1_offer(primary->data_source, "text/plain;charset=utf-8");
|
|
zwp_primary_selection_source_v1_add_listener(primary->data_source, &primary_selection_source_listener, term->wl);
|
|
zwp_primary_selection_device_v1_set_selection(term->wl->primary_selection_device, primary->data_source, serial);
|
|
|
|
/* Needed when sending the selection to other client */
|
|
primary->serial = serial;
|
|
return true;
|
|
}
|
|
|
|
void
|
|
selection_to_primary(struct terminal *term, uint32_t serial)
|
|
{
|
|
if (term->wl->primary_selection_device_manager == NULL)
|
|
return;
|
|
|
|
/* Get selection as a string */
|
|
char *text = extract_selection(term);
|
|
if (!text_to_primary(term, text, serial))
|
|
free(text);
|
|
}
|
|
|
|
void
|
|
text_from_primary(
|
|
struct terminal *term,
|
|
void (*cb)(const char *data, size_t size, void *user),
|
|
void (*done)(void *user), void *user)
|
|
{
|
|
if (term->wl->primary_selection_device_manager == NULL)
|
|
return done(user);
|
|
|
|
struct wl_primary *primary = &term->wl->primary;
|
|
if (primary->data_offer == NULL)
|
|
return done(user);
|
|
|
|
/* Prepare a pipe the other client can write its selection to us */
|
|
int fds[2];
|
|
if (pipe2(fds, O_CLOEXEC) == -1) {
|
|
LOG_ERRNO("failed to create pipe");
|
|
return done(user);
|
|
}
|
|
|
|
int read_fd = fds[0];
|
|
int write_fd = fds[1];
|
|
|
|
/* Give write-end of pipe to other client */
|
|
zwp_primary_selection_offer_v1_receive(
|
|
primary->data_offer, "text/plain;charset=utf-8", write_fd);
|
|
|
|
/* Don't keep our copy of the write-end open (or we'll never get EOF) */
|
|
close(write_fd);
|
|
|
|
begin_receive_clipboard(term, read_fd, cb, done, user);
|
|
}
|
|
|
|
void
|
|
selection_from_primary(struct terminal *term)
|
|
{
|
|
if (term->wl->primary_selection_device_manager == NULL)
|
|
return;
|
|
|
|
struct wl_clipboard *clipboard = &term->wl->clipboard;
|
|
if (clipboard->data_offer == NULL)
|
|
return;
|
|
|
|
if (term->bracketed_paste)
|
|
term_to_slave(term, "\033[200~", 6);
|
|
|
|
text_from_primary(term, &from_clipboard_cb, &from_clipboard_done, term);
|
|
}
|
|
|
|
#if 0
|
|
static void
|
|
offer(void *data, struct wl_data_offer *wl_data_offer, const char *mime_type)
|
|
{
|
|
}
|
|
|
|
static void
|
|
source_actions(void *data, struct wl_data_offer *wl_data_offer,
|
|
uint32_t source_actions)
|
|
{
|
|
}
|
|
|
|
static void
|
|
offer_action(void *data, struct wl_data_offer *wl_data_offer, uint32_t dnd_action)
|
|
{
|
|
}
|
|
|
|
static const struct wl_data_offer_listener data_offer_listener = {
|
|
.offer = &offer,
|
|
.source_actions = &source_actions,
|
|
.action = &offer_action,
|
|
};
|
|
#endif
|
|
|
|
static void
|
|
data_offer(void *data, struct wl_data_device *wl_data_device,
|
|
struct wl_data_offer *id)
|
|
{
|
|
}
|
|
|
|
static void
|
|
enter(void *data, struct wl_data_device *wl_data_device, uint32_t serial,
|
|
struct wl_surface *surface, wl_fixed_t x, wl_fixed_t y,
|
|
struct wl_data_offer *id)
|
|
{
|
|
}
|
|
|
|
static void
|
|
leave(void *data, struct wl_data_device *wl_data_device)
|
|
{
|
|
}
|
|
|
|
static void
|
|
motion(void *data, struct wl_data_device *wl_data_device, uint32_t time,
|
|
wl_fixed_t x, wl_fixed_t y)
|
|
{
|
|
}
|
|
|
|
static void
|
|
drop(void *data, struct wl_data_device *wl_data_device)
|
|
{
|
|
}
|
|
|
|
static void
|
|
selection(void *data, struct wl_data_device *wl_data_device,
|
|
struct wl_data_offer *id)
|
|
{
|
|
/* Selection offer from other client */
|
|
|
|
struct wayland *wayl = data;
|
|
struct wl_clipboard *clipboard = &wayl->clipboard;
|
|
|
|
if (clipboard->data_offer != NULL)
|
|
wl_data_offer_destroy(clipboard->data_offer);
|
|
|
|
clipboard->data_offer = id;
|
|
#if 0
|
|
if (id != NULL)
|
|
wl_data_offer_add_listener(id, &data_offer_listener, term);
|
|
#endif
|
|
}
|
|
|
|
const struct wl_data_device_listener data_device_listener = {
|
|
.data_offer = &data_offer,
|
|
.enter = &enter,
|
|
.leave = &leave,
|
|
.motion = &motion,
|
|
.drop = &drop,
|
|
.selection = &selection,
|
|
};
|
|
|
|
#if 0
|
|
static void
|
|
primary_offer(void *data,
|
|
struct zwp_primary_selection_offer_v1 *zwp_primary_selection_offer,
|
|
const char *mime_type)
|
|
{
|
|
}
|
|
|
|
static const struct zwp_primary_selection_offer_v1_listener primary_selection_offer_listener = {
|
|
.offer = &primary_offer,
|
|
};
|
|
#endif
|
|
|
|
static void
|
|
primary_data_offer(void *data,
|
|
struct zwp_primary_selection_device_v1 *zwp_primary_selection_device,
|
|
struct zwp_primary_selection_offer_v1 *offer)
|
|
{
|
|
}
|
|
|
|
static void
|
|
primary_selection(void *data,
|
|
struct zwp_primary_selection_device_v1 *zwp_primary_selection_device,
|
|
struct zwp_primary_selection_offer_v1 *id)
|
|
{
|
|
/* Selection offer from other client, for primary */
|
|
|
|
struct wayland *wayl = data;
|
|
struct wl_primary *primary = &wayl->primary;
|
|
|
|
if (primary->data_offer != NULL)
|
|
zwp_primary_selection_offer_v1_destroy(primary->data_offer);
|
|
|
|
primary->data_offer = id;
|
|
#if 0
|
|
if (id != NULL) {
|
|
zwp_primary_selection_offer_v1_add_listener(
|
|
id, &primary_selection_offer_listener, term);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
const struct zwp_primary_selection_device_v1_listener primary_selection_device_listener = {
|
|
.data_offer = &primary_data_offer,
|
|
.selection = &primary_selection,
|
|
};
|