foot/selection.c

2075 lines
57 KiB
C
Raw Normal View History

2019-07-11 09:51:51 +02:00
#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>
#include <sys/timerfd.h>
2019-07-11 09:51:51 +02:00
#define LOG_MODULE "selection"
2019-07-11 10:01:46 +02:00
#define LOG_ENABLE_DBG 0
2019-07-11 09:51:51 +02:00
#include "log.h"
#include "async.h"
#include "commands.h"
#include "config.h"
#include "extract.h"
#include "grid.h"
#include "misc.h"
#include "render.h"
#include "uri.h"
#include "util.h"
#include "vt.h"
#include "xmalloc.h"
2019-07-11 09:51:51 +02:00
static const char *const mime_type_map[] = {
[DATA_OFFER_MIME_UNSET] = NULL,
[DATA_OFFER_MIME_TEXT_PLAIN] = "text/plain",
[DATA_OFFER_MIME_TEXT_UTF8] = "text/plain;charset=utf-8",
[DATA_OFFER_MIME_URI_LIST] = "text/uri-list",
};
bool
selection_enabled(const struct terminal *term, struct seat *seat)
{
return
seat->mouse.col >= 0 && seat->mouse.row >= 0 &&
(term->mouse_tracking == MOUSE_NONE ||
term_mouse_grabbed(term, seat) ||
term->is_searching);
}
bool
term: scrolling: hopefully fix all selection/scrolling related crashes When scrolling, there are a couple of cases where an existing selection must be canceled because we cannot meaningfully represent it after scrolling. These are when the selection is (partly) inside: * The top scrolling region * The bottom scrolling region * The new lines scrolled in. I.e. re-used lines For the scrolling regions, the real problem is when the selection crosses the scrolling region boundary; a selection that is completely inside a scrolling regions _might_ be possible to keep, but we would need to translate the selection coordinates to the new scrolling region lines. For simplicity, we cancel the selection if it touches the scrolling region. Period. The last item, newly scrolled in lines is when the selection covers very old lines and we're now wrapping around the scrollback history. Then there's a fourth problem case: when the user has started a selection, but hasn't yet moved the cursor. In this case, we have no end point. What's more problematic is that when the user (after scrolling) moves the cursor, we try to create a huge selection that covers mostly empty (NULL) rows, causing us to crash. This can happen e.g. when reverse scrolling in such a way that we wrap around the scrollback history. The actual viewport in this case is something like `-n - m`. But the selection we'll end up trying to create will be `m - (rows - n)`. This range may very well contain NULL rows. To deal with this, we simply cancel the selection.
2020-05-17 15:34:49 +02:00
selection_on_rows(const struct terminal *term, int row_start, int row_end)
{
term: scrolling: hopefully fix all selection/scrolling related crashes When scrolling, there are a couple of cases where an existing selection must be canceled because we cannot meaningfully represent it after scrolling. These are when the selection is (partly) inside: * The top scrolling region * The bottom scrolling region * The new lines scrolled in. I.e. re-used lines For the scrolling regions, the real problem is when the selection crosses the scrolling region boundary; a selection that is completely inside a scrolling regions _might_ be possible to keep, but we would need to translate the selection coordinates to the new scrolling region lines. For simplicity, we cancel the selection if it touches the scrolling region. Period. The last item, newly scrolled in lines is when the selection covers very old lines and we're now wrapping around the scrollback history. Then there's a fourth problem case: when the user has started a selection, but hasn't yet moved the cursor. In this case, we have no end point. What's more problematic is that when the user (after scrolling) moves the cursor, we try to create a huge selection that covers mostly empty (NULL) rows, causing us to crash. This can happen e.g. when reverse scrolling in such a way that we wrap around the scrollback history. The actual viewport in this case is something like `-n - m`. But the selection we'll end up trying to create will be `m - (rows - n)`. This range may very well contain NULL rows. To deal with this, we simply cancel the selection.
2020-05-17 15:34:49 +02:00
LOG_DBG("on rows: %d-%d, range: %d-%d (offset=%d)",
term->selection.start.row, term->selection.end.row,
term: scrolling: hopefully fix all selection/scrolling related crashes When scrolling, there are a couple of cases where an existing selection must be canceled because we cannot meaningfully represent it after scrolling. These are when the selection is (partly) inside: * The top scrolling region * The bottom scrolling region * The new lines scrolled in. I.e. re-used lines For the scrolling regions, the real problem is when the selection crosses the scrolling region boundary; a selection that is completely inside a scrolling regions _might_ be possible to keep, but we would need to translate the selection coordinates to the new scrolling region lines. For simplicity, we cancel the selection if it touches the scrolling region. Period. The last item, newly scrolled in lines is when the selection covers very old lines and we're now wrapping around the scrollback history. Then there's a fourth problem case: when the user has started a selection, but hasn't yet moved the cursor. In this case, we have no end point. What's more problematic is that when the user (after scrolling) moves the cursor, we try to create a huge selection that covers mostly empty (NULL) rows, causing us to crash. This can happen e.g. when reverse scrolling in such a way that we wrap around the scrollback history. The actual viewport in this case is something like `-n - m`. But the selection we'll end up trying to create will be `m - (rows - n)`. This range may very well contain NULL rows. To deal with this, we simply cancel the selection.
2020-05-17 15:34:49 +02:00
row_start, row_end, term->grid->offset);
if (term->selection.end.row < 0)
return false;
term: scrolling: hopefully fix all selection/scrolling related crashes When scrolling, there are a couple of cases where an existing selection must be canceled because we cannot meaningfully represent it after scrolling. These are when the selection is (partly) inside: * The top scrolling region * The bottom scrolling region * The new lines scrolled in. I.e. re-used lines For the scrolling regions, the real problem is when the selection crosses the scrolling region boundary; a selection that is completely inside a scrolling regions _might_ be possible to keep, but we would need to translate the selection coordinates to the new scrolling region lines. For simplicity, we cancel the selection if it touches the scrolling region. Period. The last item, newly scrolled in lines is when the selection covers very old lines and we're now wrapping around the scrollback history. Then there's a fourth problem case: when the user has started a selection, but hasn't yet moved the cursor. In this case, we have no end point. What's more problematic is that when the user (after scrolling) moves the cursor, we try to create a huge selection that covers mostly empty (NULL) rows, causing us to crash. This can happen e.g. when reverse scrolling in such a way that we wrap around the scrollback history. The actual viewport in this case is something like `-n - m`. But the selection we'll end up trying to create will be `m - (rows - n)`. This range may very well contain NULL rows. To deal with this, we simply cancel the selection.
2020-05-17 15:34:49 +02:00
assert(term->selection.start.row != -1);
row_start += term->grid->offset;
row_end += term->grid->offset;
const struct coord *start = &term->selection.start;
const struct coord *end = &term->selection.end;
term: scrolling: hopefully fix all selection/scrolling related crashes When scrolling, there are a couple of cases where an existing selection must be canceled because we cannot meaningfully represent it after scrolling. These are when the selection is (partly) inside: * The top scrolling region * The bottom scrolling region * The new lines scrolled in. I.e. re-used lines For the scrolling regions, the real problem is when the selection crosses the scrolling region boundary; a selection that is completely inside a scrolling regions _might_ be possible to keep, but we would need to translate the selection coordinates to the new scrolling region lines. For simplicity, we cancel the selection if it touches the scrolling region. Period. The last item, newly scrolled in lines is when the selection covers very old lines and we're now wrapping around the scrollback history. Then there's a fourth problem case: when the user has started a selection, but hasn't yet moved the cursor. In this case, we have no end point. What's more problematic is that when the user (after scrolling) moves the cursor, we try to create a huge selection that covers mostly empty (NULL) rows, causing us to crash. This can happen e.g. when reverse scrolling in such a way that we wrap around the scrollback history. The actual viewport in this case is something like `-n - m`. But the selection we'll end up trying to create will be `m - (rows - n)`. This range may very well contain NULL rows. To deal with this, we simply cancel the selection.
2020-05-17 15:34:49 +02:00
if ((row_start <= start->row && row_end >= start->row) ||
(row_start <= end->row && row_end >= end->row))
{
/* The range crosses one of the selection boundaries */
return true;
}
/* For the last check we must ensure start <= end */
if (start->row > end->row) {
const struct coord *tmp = start;
start = end;
end = tmp;
}
term: scrolling: hopefully fix all selection/scrolling related crashes When scrolling, there are a couple of cases where an existing selection must be canceled because we cannot meaningfully represent it after scrolling. These are when the selection is (partly) inside: * The top scrolling region * The bottom scrolling region * The new lines scrolled in. I.e. re-used lines For the scrolling regions, the real problem is when the selection crosses the scrolling region boundary; a selection that is completely inside a scrolling regions _might_ be possible to keep, but we would need to translate the selection coordinates to the new scrolling region lines. For simplicity, we cancel the selection if it touches the scrolling region. Period. The last item, newly scrolled in lines is when the selection covers very old lines and we're now wrapping around the scrollback history. Then there's a fourth problem case: when the user has started a selection, but hasn't yet moved the cursor. In this case, we have no end point. What's more problematic is that when the user (after scrolling) moves the cursor, we try to create a huge selection that covers mostly empty (NULL) rows, causing us to crash. This can happen e.g. when reverse scrolling in such a way that we wrap around the scrollback history. The actual viewport in this case is something like `-n - m`. But the selection we'll end up trying to create will be `m - (rows - n)`. This range may very well contain NULL rows. To deal with this, we simply cancel the selection.
2020-05-17 15:34:49 +02:00
if (row_start >= start->row && row_end <= end->row) {
LOG_INFO("ON ROWS");
return true;
}
return false;
}
void
selection_view_up(struct terminal *term, int new_view)
{
if (likely(term->selection.start.row < 0))
return;
if (likely(new_view < term->grid->view))
return;
term->selection.start.row += term->grid->num_rows;
if (term->selection.end.row >= 0)
term->selection.end.row += term->grid->num_rows;
}
void
selection_view_down(struct terminal *term, int new_view)
{
if (likely(term->selection.start.row < 0))
return;
if (likely(new_view > term->grid->view))
return;
term->selection.start.row &= term->grid->num_rows - 1;
if (term->selection.end.row >= 0)
term->selection.end.row &= term->grid->num_rows - 1;
}
static void
foreach_selected_normal(
struct terminal *term, struct coord _start, struct coord _end,
bool (*cb)(struct terminal *term, struct row *row, struct cell *cell, int col, 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++)
{
if (!cb(term, row, &row->cells[c], c, data))
return;
}
start_col = 0;
}
}
static void
foreach_selected_block(
struct terminal *term, struct coord _start, struct coord _end,
bool (*cb)(struct terminal *term, struct row *row, struct cell *cell, int col, 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++) {
if (!cb(term, row, &row->cells[c], c, data))
return;
}
}
}
static void
foreach_selected(
struct terminal *term, struct coord start, struct coord end,
bool (*cb)(struct terminal *term, struct row *row, struct cell *cell, int col, void *data),
void *data)
{
switch (term->selection.kind) {
case SELECTION_NORMAL:
foreach_selected_normal(term, start, end, cb, data);
return;
case SELECTION_BLOCK:
foreach_selected_block(term, start, end, cb, data);
return;
case SELECTION_NONE:
assert(false);
return;
}
assert(false);
}
static bool
extract_one_const_wrapper(struct terminal *term,
struct row *row, struct cell *cell,
int col, void *data)
{
return extract_one(term, row, cell, col, data);
}
char *
selection_to_text(const struct terminal *term)
{
if (term->selection.end.row == -1)
return NULL;
struct extraction_context *ctx = extract_begin(term->selection.kind);
if (ctx == NULL)
return NULL;
foreach_selected(
(struct terminal *)term, term->selection.start, term->selection.end,
&extract_one_const_wrapper, ctx);
char *text;
return extract_finish(ctx, &text, NULL) ? text : NULL;
}
static void
find_word_boundary_left(struct terminal *term, struct coord *pos,
bool spaces_only)
{
const struct row *r = grid_row_in_view(term->grid, pos->row);
wchar_t c = r->cells[pos->col].wc;
if (c >= CELL_COMB_CHARS_LO &&
c < (CELL_COMB_CHARS_LO + term->composed_count))
{
c = term->composed[c - CELL_COMB_CHARS_LO].base;
}
bool initial_is_space = c == 0 || iswspace(c);
bool initial_is_delim = c != 0 && c != CELL_MULT_COL_SPACER &&
!isword(c, spaces_only, term->conf->word_delimiters);
while (true) {
int next_col = pos->col - 1;
int next_row = pos->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 >= CELL_COMB_CHARS_LO &&
c < (CELL_COMB_CHARS_LO + term->composed_count))
{
c = term->composed[c - CELL_COMB_CHARS_LO].base;
}
bool is_space = c == 0 || iswspace(c);
if (initial_is_space && !is_space)
break;
if (!initial_is_space) {
bool is_word = c != 0 && c != CELL_MULT_COL_SPACER &&
isword(c, spaces_only, term->conf->word_delimiters);
if ((initial_is_delim && (is_word || is_space)) ||
(!initial_is_delim && !is_word))
{
break;
}
}
pos->col = next_col;
pos->row = next_row;
}
}
static void
find_word_boundary_right(struct terminal *term, struct coord *pos,
bool spaces_only)
{
const struct row *r = grid_row_in_view(term->grid, pos->row);
wchar_t c = r->cells[pos->col].wc;
if (c >= CELL_COMB_CHARS_LO &&
c < (CELL_COMB_CHARS_LO + term->composed_count))
{
c = term->composed[c - CELL_COMB_CHARS_LO].base;
}
bool initial_is_space = c == 0 || iswspace(c);
bool initial_is_delim = c != 0 && c != CELL_MULT_COL_SPACER &&
!isword(c, spaces_only, term->conf->word_delimiters);
while (true) {
int next_col = pos->col + 1;
int next_row = pos->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 >= CELL_COMB_CHARS_LO &&
c < (CELL_COMB_CHARS_LO + term->composed_count))
{
c = term->composed[c - CELL_COMB_CHARS_LO].base;
}
bool is_space = c == 0 || iswspace(c);
if (initial_is_space && !is_space)
break;
if (!initial_is_space) {
bool is_word = c != 0 && c != CELL_MULT_COL_SPACER &&
isword(c, spaces_only, term->conf->word_delimiters);
if ((initial_is_delim && (is_word || is_space)) ||
(!initial_is_delim && !is_word))
{
break;
}
}
pos->col = next_col;
pos->row = next_row;
}
}
2019-07-11 09:51:51 +02:00
void
selection_start(struct terminal *term, int col, int row,
enum selection_kind kind,
enum selection_semantic semantic,
bool spaces_only)
2019-07-11 09:51:51 +02:00
{
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.semantic = semantic;
term->selection.ongoing = true;
term->selection.spaces_only = spaces_only;
switch (semantic) {
case SELECTION_SEMANTIC_NONE:
term->selection.start = (struct coord){col, term->grid->view + row};
term->selection.end = (struct coord){-1, -1};
term->selection.pivot.start = term->selection.start;
term->selection.pivot.end = term->selection.end;
break;
case SELECTION_SEMANTIC_WORD: {
struct coord start = {col, row}, end = {col, row};
find_word_boundary_left(term, &start, spaces_only);
find_word_boundary_right(term, &end, spaces_only);
term->selection.start = (struct coord){
start.col, term->grid->view + start.row};
term->selection.pivot.start = term->selection.start;
term->selection.pivot.end = (struct coord){end.col, term->grid->view + end.row};
selection_update(term, end.col, end.row);
break;
}
case SELECTION_SEMANTIC_ROW:
term->selection.start = (struct coord){0, term->grid->view + row};
term->selection.pivot.start = term->selection.start;
term->selection.pivot.end = (struct coord){term->cols - 1, term->grid->view + row};
selection_update(term, term->cols - 1, row);
break;
}
2019-07-11 09:51:51 +02:00
}
/* Context used while (un)marking selected cells, to be able to
* exclude empty cells */
struct mark_context {
const struct row *last_row;
int empty_count;
};
static bool
unmark_selected(struct terminal *term, struct row *row, struct cell *cell,
int col, void *data)
{
if (cell->attrs.selected == 0 || (cell->attrs.selected & 2)) {
/* Ignore if already deselected, or if premarked for updated selection */
return true;
}
2020-07-15 09:21:39 +02:00
row->dirty = true;
cell->attrs.selected = 0;
cell->attrs.clean = 0;
return true;
}
2020-07-15 09:21:39 +02:00
static bool
premark_selected(struct terminal *term, struct row *row, struct cell *cell,
int col, void *data)
{
struct mark_context *ctx = data;
assert(ctx != NULL);
if (ctx->last_row != row) {
ctx->last_row = row;
ctx->empty_count = 0;
}
if (cell->wc == 0 && term->selection.kind == SELECTION_NORMAL) {
ctx->empty_count++;
return true;
}
/* Tell unmark to leave this be */
for (int i = 0; i < ctx->empty_count + 1; i++)
row->cells[col - i].attrs.selected |= 2;
return true;
}
static bool
mark_selected(struct terminal *term, struct row *row, struct cell *cell,
int col, void *data)
{
struct mark_context *ctx = data;
assert(ctx != NULL);
if (ctx->last_row != row) {
ctx->last_row = row;
ctx->empty_count = 0;
}
if (cell->wc == 0 && term->selection.kind == SELECTION_NORMAL) {
ctx->empty_count++;
return true;
}
for (int i = 0; i < ctx->empty_count + 1; i++) {
struct cell *c = &row->cells[col - i];
if (c->attrs.selected & 1)
c->attrs.selected = 1; /* Clear the pre-mark bit */
else {
row->dirty = true;
c->attrs.selected = 1;
c->attrs.clean = 0;
}
}
return true;
}
static void
selection_modify(struct terminal *term, struct coord start, struct coord end)
{
assert(term->selection.start.row != -1);
assert(start.row != -1 && start.col != -1);
assert(end.row != -1 && end.col != -1);
2020-08-23 07:42:20 +02:00
struct mark_context ctx = {0};
/* Premark all cells that *will* be selected */
foreach_selected(term, start, end, &premark_selected, &ctx);
memset(&ctx, 0, sizeof(ctx));
if (term->selection.end.row >= 0) {
/* Unmark previous selection, ignoring cells that are part of
* the new selection */
foreach_selected(term, term->selection.start, term->selection.end,
&unmark_selected, &ctx);
memset(&ctx, 0, sizeof(ctx));
}
term->selection.start = start;
term->selection.end = end;
/* Mark new selection */
foreach_selected(term, start, end, &mark_selected, &ctx);
render_refresh(term);
}
2019-07-11 09:51:51 +02:00
void
selection_update(struct terminal *term, int col, int row)
{
if (term->selection.start.row < 0)
return;
if (!term->selection.ongoing)
return;
2019-07-11 09:51:51 +02:00
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->grid->view + row != -1);
struct coord new_start = term->selection.start;
struct coord new_end = {col, term->grid->view + row};
size_t start_row_idx = new_start.row & (term->grid->num_rows - 1);
size_t end_row_idx = new_end.row & (term->grid->num_rows - 1);
const struct row *row_start = term->grid->rows[start_row_idx];
const struct row *row_end = term->grid->rows[end_row_idx];
/* Adjust start point if the selection has changed 'direction' */
if (!(new_end.row == new_start.row && new_end.col == new_start.col)) {
enum selection_direction new_direction = term->selection.direction;
struct coord *pivot_start = &term->selection.pivot.start;
struct coord *pivot_end = &term->selection.pivot.end;
if (new_end.row < pivot_start->row ||
(new_end.row == pivot_start->row && new_end.col < pivot_start->col))
{
/* New end point is before the start point */
new_direction = SELECTION_LEFT;
} else {
/* The new end point is after the start point */
new_direction = SELECTION_RIGHT;
}
if (term->selection.direction != new_direction) {
if (term->selection.direction == SELECTION_UNDIR &&
pivot_end->row < 0)
{
/* First, make sure start isnt in the middle of a
* multi-column character */
while (true) {
const struct row *row = term->grid->rows[pivot_start->row];
const struct cell *cell = &row->cells[pivot_start->col];
if (cell->wc != CELL_MULT_COL_SPACER)
break;
/* Multi-column chars dont cross rows */
assert(pivot_start->col > 0);
if (pivot_start->col == 0)
break;
pivot_start->col--;
}
/*
* Setup pivot end to be one character *before* start
* Which one we move, the end or start point, depends
* on the initial selection direction.
*/
*pivot_end = *pivot_start;
if (new_direction == SELECTION_RIGHT) {
bool keep_going = true;
while (keep_going) {
const struct row *row = term->grid->rows[pivot_end->row];
const wchar_t wc = row->cells[pivot_end->col].wc;
keep_going = wc == CELL_MULT_COL_SPACER;
if (pivot_end->col == 0) {
if (pivot_end->row > 0) {
pivot_end->col = term->cols - 1;
pivot_end->row--;
}
} else
pivot_end->col--;
}
} else {
bool keep_going = true;
while (keep_going) {
const struct row *row = term->grid->rows[pivot_start->row];
const wchar_t wc = pivot_start->col < term->cols - 1
? row->cells[pivot_start->col + 1].wc : 0;
keep_going = wc == CELL_MULT_COL_SPACER;
if (pivot_start->col >= term->cols - 1) {
if (pivot_start->row < term->rows - 1) {
pivot_start->col = 0;
pivot_start->row++;
}
} else
pivot_start->col++;
}
}
assert(term->grid->rows[pivot_start->row]->cells[pivot_start->col].wc != CELL_MULT_COL_SPACER);
assert(term->grid->rows[pivot_end->row]->cells[pivot_end->col].wc != CELL_MULT_COL_SPACER);
}
if (new_direction == SELECTION_LEFT) {
assert(pivot_end->row >= 0);
new_start = *pivot_end;
} else
new_start = *pivot_start;
term->selection.direction = new_direction;
}
}
switch (term->selection.semantic) {
case SELECTION_SEMANTIC_NONE:
break;
case SELECTION_SEMANTIC_WORD:
switch (term->selection.direction) {
case SELECTION_LEFT:
find_word_boundary_left(term, &new_end, term->selection.spaces_only);
break;
case SELECTION_RIGHT:
find_word_boundary_right(term, &new_end, term->selection.spaces_only);
break;
case SELECTION_UNDIR:
break;
}
break;
case SELECTION_SEMANTIC_ROW:
switch (term->selection.direction) {
case SELECTION_LEFT:
new_end.col = 0;
break;
case SELECTION_RIGHT:
new_end.col = term->cols - 1;
break;
case SELECTION_UNDIR:
break;
}
break;
}
/* If an end point is in the middle of a multi-column character,
* expand the selection to cover the entire character */
if (new_start.row < new_end.row ||
(new_start.row == new_end.row && new_start.col <= new_end.col))
{
while (new_start.col >= 1 &&
row_start->cells[new_start.col].wc == CELL_MULT_COL_SPACER)
new_start.col--;
while (new_end.col < term->cols - 1 &&
row_end->cells[new_end.col + 1].wc == CELL_MULT_COL_SPACER)
new_end.col++;
} else {
while (new_end.col >= 1 &&
row_end->cells[new_end.col].wc == CELL_MULT_COL_SPACER)
new_end.col--;
while (new_start.col < term->cols - 1 &&
row_start->cells[new_start.col + 1].wc == CELL_MULT_COL_SPACER)
new_start.col++;
}
selection_modify(term, new_start, new_end);
}
void
selection_dirty_cells(struct terminal *term)
{
if (term->selection.start.row < 0 || term->selection.end.row < 0)
return;
foreach_selected(
term, term->selection.start, term->selection.end, &mark_selected,
2020-08-23 07:42:20 +02:00
&(struct mark_context){0});
}
static void
selection_extend_normal(struct terminal *term, int col, int row, uint32_t serial)
{
const struct coord *start = &term->selection.start;
const struct coord *end = &term->selection.end;
2019-07-11 09:51:51 +02:00
if (start->row > end->row ||
(start->row == end->row && start->col > end->col))
{
const struct coord *tmp = start;
start = end;
end = tmp;
}
assert(start->row < end->row || start->col < end->col);
struct coord new_start, new_end;
enum selection_direction direction;
if (row < start->row || (row == start->row && col < start->col)) {
/* Extend selection to start *before* current start */
new_start = *end;
new_end = (struct coord){col, row};
direction = SELECTION_LEFT;
}
else if (row > end->row || (row == end->row && col > end->col)) {
/* Extend selection to end *after* current end */
new_start = *start;
new_end = (struct coord){col, row};
direction = SELECTION_RIGHT;
}
else {
/* Shrink selection from start or end, depending on which one is closest */
const int linear = row * term->cols + col;
if (abs(linear - (start->row * term->cols + start->col)) <
abs(linear - (end->row * term->cols + end->col)))
{
/* Move start point */
new_start = *end;
new_end = (struct coord){col, row};
direction = SELECTION_LEFT;
}
else {
/* Move end point */
new_start = *start;
new_end = (struct coord){col, row};
direction = SELECTION_RIGHT;
}
}
term->selection.direction = direction;
selection_modify(term, new_start, new_end);
}
static void
selection_extend_block(struct terminal *term, int col, int row, uint32_t serial)
{
const struct coord *start = &term->selection.start;
const struct coord *end = &term->selection.end;
struct coord top_left = {
.row = min(start->row, end->row),
.col = min(start->col, end->col),
};
struct coord top_right = {
.row = min(start->row, end->row),
.col = max(start->col, end->col),
};
struct coord bottom_left = {
.row = max(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),
};
struct coord new_start;
struct coord new_end;
if (row <= top_left.row ||
abs(row - top_left.row) < abs(row - bottom_left.row))
{
/* Move one of the top corners */
if (abs(col - top_left.col) < abs(col - top_right.col)) {
new_start = bottom_right;
new_end = (struct coord){col, row};
}
else {
new_start = bottom_left;
new_end = (struct coord){col, row};
}
}
else {
/* Move one of the bottom corners */
if (abs(col - bottom_left.col) < abs(col - bottom_right.col)) {
new_start = top_right;
new_end = (struct coord){col, row};
}
else {
new_start = top_left;
new_end = (struct coord){col, row};
}
}
selection_modify(term, new_start, new_end);
}
void
selection_extend(struct seat *seat, struct terminal *term,
int col, int row, uint32_t serial)
{
if (term->selection.start.row < 0 || term->selection.end.row < 0) {
/* No existing selection */
return;
}
2020-08-11 10:16:52 +02:00
term->selection.ongoing = true;
row += term->grid->view;
if ((row == term->selection.start.row && col == term->selection.start.col) ||
(row == term->selection.end.row && col == term->selection.end.col))
{
/* Extension point *is* one of the current end points */
return;
}
switch (term->selection.kind) {
case SELECTION_NONE:
assert(false);
return;
case SELECTION_NORMAL:
selection_extend_normal(term, col, row, serial);
break;
case SELECTION_BLOCK:
selection_extend_block(term, col, row, serial);
break;
}
selection_to_primary(seat, term, serial);
2019-07-11 09:51:51 +02:00
}
//static const struct zwp_primary_selection_source_v1_listener primary_selection_source_listener;
2019-07-11 17:34:52 +02:00
2019-07-11 09:51:51 +02:00
void
selection_finalize(struct seat *seat, struct terminal *term, uint32_t serial)
2019-07-11 09:51:51 +02:00
{
if (!term->selection.ongoing)
return;
LOG_DBG("selection finalize");
selection_stop_scroll_timer(term);
term->selection.ongoing = false;
if (term->selection.start.row < 0 || term->selection.end.row < 0)
return;
2019-07-11 09:51:51 +02:00
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(seat, term, serial);
2019-07-11 09:51:51 +02:00
}
void
selection_cancel(struct terminal *term)
{
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);
selection_stop_scroll_timer(term);
if (term->selection.start.row >= 0 && term->selection.end.row >= 0) {
foreach_selected(
term, term->selection.start, term->selection.end,
2020-08-23 07:42:20 +02:00
&unmark_selected, &(struct mark_context){0});
render_refresh(term);
}
2019-07-11 09:51:51 +02:00
term->selection.kind = SELECTION_NONE;
2019-07-11 09:51:51 +02:00
term->selection.start = (struct coord){-1, -1};
term->selection.end = (struct coord){-1, -1};
term->selection.direction = SELECTION_UNDIR;
term->selection.ongoing = false;
2019-07-11 09:51:51 +02:00
}
bool
selection_clipboard_has_data(const struct seat *seat)
{
return seat->clipboard.data_offer != NULL;
}
bool
selection_primary_has_data(const struct seat *seat)
{
return seat->primary.data_offer != NULL;
}
void
selection_clipboard_unset(struct seat *seat)
{
struct wl_clipboard *clipboard = &seat->clipboard;
if (clipboard->data_source == NULL)
return;
/* Kill previous data source */
assert(clipboard->serial != 0);
wl_data_device_set_selection(seat->data_device, NULL, clipboard->serial);
wl_data_source_destroy(clipboard->data_source);
clipboard->data_source = NULL;
clipboard->serial = 0;
free(clipboard->text);
clipboard->text = NULL;
}
void
selection_primary_unset(struct seat *seat)
{
struct wl_primary *primary = &seat->primary;
if (primary->data_source == NULL)
return;
assert(primary->serial != 0);
zwp_primary_selection_device_v1_set_selection(
seat->primary_selection_device, NULL, primary->serial);
zwp_primary_selection_source_v1_destroy(primary->data_source);
primary->data_source = NULL;
primary->serial = 0;
free(primary->text);
primary->text = NULL;
}
static bool
fdm_scroll_timer(struct fdm *fdm, int fd, int events, void *data)
{
if (events & EPOLLHUP)
return false;
struct terminal *term = data;
uint64_t expiration_count;
ssize_t ret = read(
term->selection.auto_scroll.fd,
&expiration_count, sizeof(expiration_count));
if (ret < 0) {
if (errno == EAGAIN)
return true;
LOG_ERRNO("failed to read selection scroll timer");
return false;
}
switch (term->selection.auto_scroll.direction) {
case SELECTION_SCROLL_NOT:
return true;
case SELECTION_SCROLL_UP:
cmd_scrollback_up(term, expiration_count);
selection_update(term, term->selection.auto_scroll.col, 0);
break;
case SELECTION_SCROLL_DOWN:
cmd_scrollback_down(term, expiration_count);
selection_update(term, term->selection.auto_scroll.col, term->rows - 1);
break;
}
return true;
}
void
selection_start_scroll_timer(struct terminal *term, int interval_ns,
enum selection_scroll_direction direction, int col)
{
assert(direction != SELECTION_SCROLL_NOT);
if (!term->selection.ongoing)
return;
if (term->selection.auto_scroll.fd < 0) {
int fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK);
if (fd < 0) {
LOG_ERRNO("failed to create selection scroll timer");
goto err;
}
if (!fdm_add(term->fdm, fd, EPOLLIN, &fdm_scroll_timer, term)) {
close(fd);
return;
}
term->selection.auto_scroll.fd = fd;
}
struct itimerspec timer;
if (timerfd_gettime(term->selection.auto_scroll.fd, &timer) < 0) {
LOG_ERRNO("failed to get current selection scroll timer value");
goto err;
}
if (timer.it_value.tv_sec == 0 && timer.it_value.tv_nsec == 0)
timer.it_value.tv_nsec = 1;
timer.it_interval.tv_sec = interval_ns / 1000000000;
timer.it_interval.tv_nsec = interval_ns % 1000000000;
if (timerfd_settime(term->selection.auto_scroll.fd, 0, &timer, NULL) < 0) {
LOG_ERRNO("failed to set new selection scroll timer value");
goto err;
}
term->selection.auto_scroll.direction = direction;
term->selection.auto_scroll.col = col;
return;
err:
selection_stop_scroll_timer(term);
return;
}
void
selection_stop_scroll_timer(struct terminal *term)
{
if (term->selection.auto_scroll.fd < 0) {
assert(term->selection.auto_scroll.direction == SELECTION_SCROLL_NOT);
return;
}
fdm_del(term->fdm, term->selection.auto_scroll.fd);
term->selection.auto_scroll.fd = -1;
term->selection.auto_scroll.direction = SELECTION_SCROLL_NOT;
}
static void
target(void *data, struct wl_data_source *wl_data_source, const char *mime_type)
{
LOG_DBG("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;
2019-11-05 08:40:39 +01:00
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;
}
2019-11-05 08:40:39 +01:00
done:
fdm_del(fdm, fd);
free(ctx->data);
free(ctx);
return true;
}
static void
send_clipboard_or_primary(struct seat *seat, int fd, const char *selection,
const char *source_name)
{
/* 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 len = strlen(selection);
size_t async_idx = 0;
switch (async_write(fd, selection, len, &async_idx)) {
case ASYNC_WRITE_REMAIN: {
struct clipboard_send *ctx = xmalloc(sizeof(*ctx));
*ctx = (struct clipboard_send) {
.data = xstrdup(&selection[async_idx]),
.len = len - async_idx,
.idx = 0,
};
if (fdm_add(seat->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 write %zu bytes of %s selection data to FD=%d",
len, source_name, fd);
break;
}
close(fd);
}
static void
send(void *data, struct wl_data_source *wl_data_source, const char *mime_type,
int32_t fd)
{
struct seat *seat = data;
const struct wl_clipboard *clipboard = &seat->clipboard;
assert(clipboard->text != NULL);
send_clipboard_or_primary(seat, fd, clipboard->text, "clipboard");
}
static void
cancelled(void *data, struct wl_data_source *wl_data_source)
{
struct seat *seat = data;
struct wl_clipboard *clipboard = &seat->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;
}
/* We dont support dragging *from* */
static void
dnd_drop_performed(void *data, struct wl_data_source *wl_data_source)
{
//LOG_DBG("DnD drop performed");
}
static void
dnd_finished(void *data, struct wl_data_source *wl_data_source)
{
//LOG_DBG("DnD finished");
}
static void
action(void *data, struct wl_data_source *wl_data_source, uint32_t dnd_action)
{
//LOG_DBG("DnD action: %u", 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,
};
2019-07-11 17:34:52 +02:00
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 seat *seat = data;
const struct wl_primary *primary = &seat->primary;
2019-07-11 17:34:52 +02:00
assert(primary->text != NULL);
send_clipboard_or_primary(seat, fd, primary->text, "primary");
2019-07-11 17:34:52 +02:00
}
static void
primary_cancelled(void *data,
struct zwp_primary_selection_source_v1 *zwp_primary_selection_source_v1)
{
struct seat *seat = data;
struct wl_primary *primary = &seat->primary;
2019-07-11 17:34:52 +02:00
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 seat *seat, struct terminal *term, char *text, uint32_t serial)
{
struct wl_clipboard *clipboard = &seat->clipboard;
if (clipboard->data_source != NULL) {
/* Kill previous data source */
assert(clipboard->serial != 0);
wl_data_device_set_selection(seat->data_device, NULL, clipboard->serial);
wl_data_source_destroy(clipboard->data_source);
free(clipboard->text);
clipboard->data_source = NULL;
clipboard->serial = 0;
clipboard->text = NULL;
}
clipboard->data_source
= wl_data_device_manager_create_data_source(term->wl->data_device_manager);
2019-07-11 16:37:45 +02:00
if (clipboard->data_source == NULL) {
LOG_ERR("failed to create clipboard data source");
return false;
2019-07-11 16:37:45 +02:00
}
clipboard->text = text;
2019-07-11 16:37:45 +02:00
/* Configure source */
wl_data_source_offer(clipboard->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_UTF8]);
wl_data_source_offer(clipboard->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_PLAIN]);
wl_data_source_offer(clipboard->data_source, "STRING");
wl_data_source_offer(clipboard->data_source, "TEXT");
wl_data_source_offer(clipboard->data_source, "UTF8_STRING");
wl_data_source_add_listener(clipboard->data_source, &data_source_listener, seat);
wl_data_device_set_selection(seat->data_device, clipboard->data_source, serial);
2019-07-11 16:37:45 +02:00
/* Needed when sending the selection to other client */
assert(serial != 0);
clipboard->serial = serial;
return true;
}
void
selection_to_clipboard(struct seat *seat, struct terminal *term, uint32_t serial)
{
if (term->selection.start.row < 0 || term->selection.end.row < 0)
return;
/* Get selection as a string */
char *text = selection_to_text(term);
if (!text_to_clipboard(seat, term, text, serial))
free(text);
}
struct clipboard_receive {
int read_fd;
int timeout_fd;
struct itimerspec timeout;
void (*decoder)(struct clipboard_receive *ctx, char *data, size_t size);
/* URI state */
bool add_space;
struct {
char *data;
size_t sz;
size_t idx;
} buf;
/* Callback data */
void (*cb)(char *data, size_t size, void *user);
void (*done)(void *user);
void *user;
};
static void
clipboard_receive_done(struct fdm *fdm, struct clipboard_receive *ctx)
{
fdm_del(fdm, ctx->timeout_fd);
fdm_del(fdm, ctx->read_fd);
ctx->done(ctx->user);
free(ctx->buf.data);
free(ctx);
}
static bool
fdm_receive_timeout(struct fdm *fdm, int fd, int events, void *data)
{
struct clipboard_receive *ctx = data;
if (events & EPOLLHUP)
return false;
assert(events & EPOLLIN);
uint64_t expire_count;
ssize_t ret = read(fd, &expire_count, sizeof(expire_count));
if (ret < 0) {
if (errno == EAGAIN)
return true;
LOG_ERRNO("failed to read clipboard timeout timer");
return false;
}
LOG_WARN("no data received from clipboard in %llu seconds, aborting",
(unsigned long long)ctx->timeout.it_value.tv_sec);
clipboard_receive_done(fdm, ctx);
return true;
}
static void
fdm_receive_decoder_plain(struct clipboard_receive *ctx, char *data, size_t size)
{
ctx->cb(data, size, ctx->user);
}
static void
fdm_receive_decoder_uri(struct clipboard_receive *ctx, char *data, size_t size)
{
while (ctx->buf.idx + size > ctx->buf.sz) {
size_t new_sz = ctx->buf.sz == 0 ? size : 2 * ctx->buf.sz;
ctx->buf.data = xrealloc(ctx->buf.data, new_sz);
ctx->buf.sz = new_sz;
}
memcpy(&ctx->buf.data[ctx->buf.idx], data, size);
ctx->buf.idx += size;
char *start = ctx->buf.data;
char *end = NULL;
while ((end = memchr(start, '\n', ctx->buf.idx - (start - ctx->buf.data))) != NULL) {
const size_t len = end - start;
LOG_DBG("URI: \"%.*s\"", (int)len, start);
char *scheme, *host, *path;
if (!uri_parse(start, len, &scheme, NULL, NULL, &host, NULL, &path, NULL, NULL)) {
LOG_ERR("drag-and-drop: invalid URI: %.*s", (int)len, start);
start = end + 1;
continue;
}
if (ctx->add_space)
ctx->cb(" ", 1, ctx->user);
ctx->add_space = true;
if (strcmp(scheme, "file") == 0 && hostname_is_localhost(host)) {
ctx->cb("'", 1, ctx->user);
ctx->cb(path, strlen(path), ctx->user);
ctx->cb("'", 1, ctx->user);
} else
ctx->cb(start, len, ctx->user);
start = end + 1;
free(scheme);
free(host);
free(path);
}
const size_t ofs = start - ctx->buf.data;
const size_t left = ctx->buf.idx - ofs;
memmove(&ctx->buf.data[0], &ctx->buf.data[ofs], left);
ctx->buf.idx = left;
}
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;
/* Reset timeout timer */
if (timerfd_settime(ctx->timeout_fd, 0, &ctx->timeout, NULL) < 0) {
LOG_ERRNO("failed to re-arm clipboard timeout timer");
return false;
}
/* 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 */
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->decoder(ctx, p, i);
assert(i + 1 <= left);
p += i + 1;
left -= i + 1;
goto again;
}
}
ctx->decoder(ctx, p, left);
left = 0;
}
done:
clipboard_receive_done(fdm, ctx);
return true;
}
static void
begin_receive_clipboard(struct terminal *term, int read_fd,
enum data_offer_mime_type mime_type,
void (*cb)(char *data, size_t size, void *user),
void (*done)(void *user), void *user)
{
int timeout_fd = -1;
struct clipboard_receive *ctx = NULL;
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");
goto err;
}
timeout_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
if (timeout_fd < 0) {
LOG_ERRNO("failed to create clipboard timeout timer FD");
goto err;
}
const struct itimerspec timeout = {.it_value = {.tv_sec = 2}};
if (timerfd_settime(timeout_fd, 0, &timeout, NULL) < 0) {
2020-08-27 19:55:27 +02:00
LOG_ERRNO("failed to arm clipboard timeout timer");
goto err;
}
ctx = xmalloc(sizeof(*ctx));
*ctx = (struct clipboard_receive) {
.read_fd = read_fd,
.timeout_fd = timeout_fd,
.timeout = timeout,
.decoder = (mime_type == DATA_OFFER_MIME_URI_LIST
? &fdm_receive_decoder_uri
: &fdm_receive_decoder_plain),
.cb = cb,
.done = done,
.user = user,
};
if (!fdm_add(term->fdm, read_fd, EPOLLIN, &fdm_receive, ctx) ||
!fdm_add(term->fdm, timeout_fd, EPOLLIN, &fdm_receive_timeout, ctx))
{
goto err;
}
return;
err:
free(ctx);
fdm_del(term->fdm, timeout_fd);
fdm_del(term->fdm, read_fd);
done(user);
}
void
text_from_clipboard(struct seat *seat, struct terminal *term,
void (*cb)(char *data, size_t size, void *user),
void (*done)(void *user), void *user)
{
struct wl_clipboard *clipboard = &seat->clipboard;
if (clipboard->data_offer == NULL ||
clipboard->mime_type == DATA_OFFER_MIME_UNSET)
{
done(user);
return;
}
2019-07-11 16:37:45 +02:00
/* 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");
done(user);
return;
}
int read_fd = fds[0];
int write_fd = fds[1];
2019-07-11 16:37:45 +02:00
/* Give write-end of pipe to other client */
wl_data_offer_receive(
clipboard->data_offer, mime_type_map[clipboard->mime_type], write_fd);
2019-07-11 16:37:45 +02:00
/* 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, clipboard->mime_type, cb, done, user);
}
static void
receive_offer(char *data, size_t size, void *user)
{
struct terminal *term = user;
assert(term->is_sending_paste_data);
term_paste_data_to_slave(term, data, size);
}
static void
receive_offer_done(void *user)
{
struct terminal *term = user;
if (term->bracketed_paste)
term_paste_data_to_slave(term, "\033[201~", 6);
term->is_sending_paste_data = false;
/* Make sure we send any queued up non-paste data */
if (tll_length(term->ptmx_buffers) > 0)
fdm_event_add(term->fdm, term->ptmx, EPOLLOUT);
}
void
selection_from_clipboard(struct seat *seat, struct terminal *term, uint32_t serial)
{
if (term->is_sending_paste_data) {
/* We're already pasting... */
return;
}
struct wl_clipboard *clipboard = &seat->clipboard;
if (clipboard->data_offer == NULL)
return;
term->is_sending_paste_data = true;
if (term->bracketed_paste)
term_paste_data_to_slave(term, "\033[200~", 6);
text_from_clipboard(seat, term, &receive_offer, &receive_offer_done, term);
}
bool
text_to_primary(struct seat *seat, struct terminal *term, char *text, uint32_t serial)
{
if (term->wl->primary_selection_device_manager == NULL)
return false;
struct wl_primary *primary = &seat->primary;
/* TODO: somehow share code with the clipboard equivalent */
if (seat->primary.data_source != NULL) {
/* Kill previous data source */
assert(primary->serial != 0);
zwp_primary_selection_device_v1_set_selection(
seat->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->text = NULL;
}
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, mime_type_map[DATA_OFFER_MIME_TEXT_UTF8]);
zwp_primary_selection_source_v1_offer(primary->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_PLAIN]);
zwp_primary_selection_source_v1_offer(primary->data_source, "STRING");
zwp_primary_selection_source_v1_offer(primary->data_source, "TEXT");
zwp_primary_selection_source_v1_offer(primary->data_source, "UTF8_STRING");
zwp_primary_selection_source_v1_add_listener(primary->data_source, &primary_selection_source_listener, seat);
zwp_primary_selection_device_v1_set_selection(seat->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 seat *seat, struct terminal *term, uint32_t serial)
{
if (term->wl->primary_selection_device_manager == NULL)
return;
/* Get selection as a string */
char *text = selection_to_text(term);
if (!text_to_primary(seat, term, text, serial))
free(text);
}
void
text_from_primary(
struct seat *seat, struct terminal *term,
void (*cb)(char *data, size_t size, void *user),
void (*done)(void *user), void *user)
{
if (term->wl->primary_selection_device_manager == NULL) {
done(user);
return;
}
struct wl_primary *primary = &seat->primary;
if (primary->data_offer == NULL ||
primary->mime_type == DATA_OFFER_MIME_UNSET)
{
done(user);
return;
}
/* 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");
done(user);
return;
}
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, mime_type_map[primary->mime_type], 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, primary->mime_type, cb, done, user);
}
void
selection_from_primary(struct seat *seat, struct terminal *term)
{
if (term->wl->primary_selection_device_manager == NULL)
return;
if (term->is_sending_paste_data) {
/* We're already pasting... */
return;
}
struct wl_primary *primary = &seat->primary;
if (primary->data_offer == NULL)
return;
term->is_sending_paste_data = true;
if (term->bracketed_paste)
term_paste_data_to_slave(term, "\033[200~", 6);
text_from_primary(seat, term, &receive_offer, &receive_offer_done, term);
}
static void
select_mime_type_for_offer(const char *_mime_type,
enum data_offer_mime_type *type)
{
enum data_offer_mime_type mime_type = DATA_OFFER_MIME_UNSET;
/* Translate offered mime type to our mime type enum */
for (size_t i = 0; i < ALEN(mime_type_map); i++) {
if (mime_type_map[i] == NULL)
continue;
if (strcmp(_mime_type, mime_type_map[i]) == 0) {
mime_type = i;
break;
}
}
LOG_DBG("mime-type: %s -> %s (offered type was %s)",
mime_type_map[*type], mime_type_map[mime_type], _mime_type);
/* Mime-type transition; if the new mime-type is "better" than
* previously offered types, use the new type */
switch (mime_type) {
case DATA_OFFER_MIME_TEXT_PLAIN:
/* text/plain is our least preferred type. Only use if current
* type is unset */
switch (*type) {
case DATA_OFFER_MIME_UNSET:
*type = mime_type;
break;
default:
break;
}
break;
case DATA_OFFER_MIME_TEXT_UTF8:
/* text/plain;charset=utf-8 is preferred over text/plain */
switch (*type) {
case DATA_OFFER_MIME_UNSET:
case DATA_OFFER_MIME_TEXT_PLAIN:
*type = mime_type;
break;
default:
break;
}
break;
case DATA_OFFER_MIME_URI_LIST:
/* text/uri-list is always used when offered */
*type = mime_type;
break;
case DATA_OFFER_MIME_UNSET:
break;
}
}
static void
data_offer_reset(struct wl_clipboard *clipboard)
{
if (clipboard->data_offer != NULL) {
wl_data_offer_destroy(clipboard->data_offer);
clipboard->data_offer = NULL;
}
clipboard->window = NULL;
clipboard->mime_type = DATA_OFFER_MIME_UNSET;
}
static void
offer(void *data, struct wl_data_offer *wl_data_offer, const char *mime_type)
{
struct seat *seat = data;
select_mime_type_for_offer(mime_type, &seat->clipboard.mime_type);
}
static void
source_actions(void *data, struct wl_data_offer *wl_data_offer,
uint32_t source_actions)
{
#if defined(_DEBUG) && LOG_ENABLE_DBG
char actions_as_string[1024];
size_t idx = 0;
actions_as_string[0] = '\0';
actions_as_string[sizeof(actions_as_string) - 1] = '\0';
for (size_t i = 0; i < 31; i++) {
if (((source_actions >> i) & 1) == 0)
continue;
enum wl_data_device_manager_dnd_action action = 1 << i;
const char *s = NULL;
switch (action) {
case WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE: s = NULL; break;
case WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY: s = "copy"; break;
case WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE: s = "move"; break;
case WL_DATA_DEVICE_MANAGER_DND_ACTION_ASK: s = "ask"; break;
}
if (s == NULL)
continue;
strncat(actions_as_string, s, sizeof(actions_as_string) - idx - 1);
idx += strlen(s);
strncat(actions_as_string, ", ", sizeof(actions_as_string) - idx - 1);
idx += 2;
}
/* Strip trailing ", " */
if (strlen(actions_as_string) > 2)
actions_as_string[strlen(actions_as_string) - 2] = '\0';
LOG_DBG("DnD actions: %s (0x%08x)", actions_as_string, source_actions);
#endif
}
static void
offer_action(void *data, struct wl_data_offer *wl_data_offer, uint32_t dnd_action)
{
#if defined(_DEBUG) && LOG_ENABLE_DBG
const char *s = NULL;
switch (dnd_action) {
case WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE: s = "<none>"; break;
case WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY: s = "copy"; break;
case WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE: s = "move"; break;
case WL_DATA_DEVICE_MANAGER_DND_ACTION_ASK: s = "ask"; break;
}
LOG_DBG("DnD offer action: %s (0x%08x)", s, dnd_action);
#endif
}
static const struct wl_data_offer_listener data_offer_listener = {
.offer = &offer,
.source_actions = &source_actions,
.action = &offer_action,
};
static void
data_offer(void *data, struct wl_data_device *wl_data_device,
struct wl_data_offer *offer)
{
struct seat *seat = data;
data_offer_reset(&seat->clipboard);
seat->clipboard.data_offer = offer;
wl_data_offer_add_listener(offer, &data_offer_listener, seat);
}
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 *offer)
{
struct seat *seat = data;
struct wayland *wayl = seat->wayl;
assert(offer == seat->clipboard.data_offer);
2020-10-28 19:34:49 +01:00
/* Remember _which_ terminal the current DnD offer is targeting */
assert(seat->clipboard.window == NULL);
tll_foreach(wayl->terms, it) {
if (term_surface_kind(it->item, surface) == TERM_SURF_GRID &&
!it->item->is_sending_paste_data)
{
wl_data_offer_accept(
offer, serial, mime_type_map[seat->clipboard.mime_type]);
wl_data_offer_set_actions(
offer,
WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY,
WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY);
seat->clipboard.window = it->item->window;
return;
}
}
2020-10-28 19:34:49 +01:00
/* Either terminal is already busy sending paste data, or mouse
* pointer isnt over the grid */
seat->clipboard.window = NULL;
wl_data_offer_set_actions(offer, 0, 0);
}
static void
leave(void *data, struct wl_data_device *wl_data_device)
{
struct seat *seat = data;
seat->clipboard.window = NULL;
}
static void
motion(void *data, struct wl_data_device *wl_data_device, uint32_t time,
wl_fixed_t x, wl_fixed_t y)
{
}
struct dnd_context {
struct terminal *term;
struct wl_data_offer *data_offer;
};
static void
receive_dnd(char *data, size_t size, void *user)
{
struct dnd_context *ctx = user;
receive_offer(data, size, ctx->term);
}
static void
receive_dnd_done(void *user)
{
struct dnd_context *ctx = user;
wl_data_offer_finish(ctx->data_offer);
wl_data_offer_destroy(ctx->data_offer);
receive_offer_done(ctx->term);
free(ctx);
}
static void
drop(void *data, struct wl_data_device *wl_data_device)
{
struct seat *seat = data;
assert(seat->clipboard.window != NULL);
struct terminal *term = seat->clipboard.window->term;
struct wl_clipboard *clipboard = &seat->clipboard;
struct dnd_context *ctx = xmalloc(sizeof(*ctx));
*ctx = (struct dnd_context){
.term = term,
.data_offer = clipboard->data_offer,
};
/* 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");
free(ctx);
return;
}
int read_fd = fds[0];
int write_fd = fds[1];
LOG_DBG("DnD drop: mime-type=%s", mime_type_map[clipboard->mime_type]);
/* Give write-end of pipe to other client */
wl_data_offer_receive(
clipboard->data_offer, mime_type_map[clipboard->mime_type], write_fd);
/* Don't keep our copy of the write-end open (or we'll never get EOF) */
close(write_fd);
term->is_sending_paste_data = true;
if (term->bracketed_paste)
term_paste_data_to_slave(term, "\033[200~", 6);
begin_receive_clipboard(
term, read_fd, clipboard->mime_type,
&receive_dnd, &receive_dnd_done, ctx);
/* data offer is now “owned” by the receive context */
clipboard->data_offer = NULL;
clipboard->mime_type = DATA_OFFER_MIME_UNSET;
}
static void
selection(void *data, struct wl_data_device *wl_data_device,
struct wl_data_offer *offer)
{
2019-07-11 16:37:45 +02:00
/* Selection offer from other client */
struct seat *seat = data;
if (offer == NULL)
data_offer_reset(&seat->clipboard);
else
assert(offer == seat->clipboard.data_offer);
}
const struct wl_data_device_listener data_device_listener = {
.data_offer = &data_offer,
.enter = &enter,
.leave = &leave,
.motion = &motion,
.drop = &drop,
.selection = &selection,
};
static void
primary_offer(void *data,
struct zwp_primary_selection_offer_v1 *zwp_primary_selection_offer,
const char *mime_type)
{
LOG_DBG("primary offer: %s", mime_type);
struct seat *seat = data;
select_mime_type_for_offer(mime_type, &seat->primary.mime_type);
}
static const struct zwp_primary_selection_offer_v1_listener primary_selection_offer_listener = {
.offer = &primary_offer,
};
static void
primary_offer_reset(struct wl_primary *primary)
{
if (primary->data_offer != NULL) {
zwp_primary_selection_offer_v1_destroy(primary->data_offer);
primary->data_offer = NULL;
}
primary->mime_type = DATA_OFFER_MIME_UNSET;
}
static void
primary_data_offer(void *data,
struct zwp_primary_selection_device_v1 *zwp_primary_selection_device,
struct zwp_primary_selection_offer_v1 *offer)
{
struct seat *seat = data;
primary_offer_reset(&seat->primary);
seat->primary.data_offer = offer;
zwp_primary_selection_offer_v1_add_listener(
offer, &primary_selection_offer_listener, seat);
}
static void
primary_selection(void *data,
struct zwp_primary_selection_device_v1 *zwp_primary_selection_device,
struct zwp_primary_selection_offer_v1 *offer)
{
/* Selection offer from other client, for primary */
struct seat *seat = data;
if (offer == NULL)
primary_offer_reset(&seat->primary);
else
assert(seat->primary.data_offer == offer);
}
const struct zwp_primary_selection_device_v1_listener primary_selection_device_listener = {
.data_offer = &primary_data_offer,
.selection = &primary_selection,
};