A fast, lightweight and minimalistic Wayland terminal emulator
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

2313 lines
65 KiB

#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>
#define LOG_MODULE "selection"
#define LOG_ENABLE_DBG 0
#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"
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",
[DATA_OFFER_MIME_TEXT_TEXT] = "TEXT",
[DATA_OFFER_MIME_TEXT_STRING] = "STRING",
[DATA_OFFER_MIME_TEXT_UTF8_STRING] = "UTF8_STRING",
};
bool
selection_on_rows(const struct terminal *term, int row_start, int row_end)
{
LOG_DBG("on rows: %d-%d, range: %d-%d (offset=%d)",
term->selection.start.row, term->selection.end.row,
row_start, row_end, term->grid->offset);
if (term->selection.end.row < 0)
return false;
xassert(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;
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;
}
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];
xassert(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];
xassert(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_CHAR_WISE:
case SELECTION_WORD_WISE:
case SELECTION_LINE_WISE:
foreach_selected_normal(term, start, end, cb, data);
return;
case SELECTION_BLOCK:
foreach_selected_block(term, start, end, cb, data);
return;
case SELECTION_NONE:
break;
}
BUG("Invalid selection kind");
}
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, true);
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;
}
void
selection_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;
while (c >= CELL_SPACER) {
xassert(pos->col > 0);
if (pos->col == 0)
return;
pos->col--;
c = r->cells[pos->col].wc;
}
if (c >= CELL_COMB_CHARS_LO && c <= CELL_COMB_CHARS_HI)
c = composed_lookup(term->composed, c - CELL_COMB_CHARS_LO)->chars[0];
bool initial_is_space = c == 0 || iswspace(c);
bool initial_is_delim =
!initial_is_space && !isword(c, spaces_only, term->conf->word_delimiters);
bool initial_is_word =
c != 0 && isword(c, spaces_only, term->conf->word_delimiters);
while (true) {
int next_col = pos->col - 1;
int next_row = pos->row;
const struct row *row = grid_row_in_view(term->grid, next_row);
/* Linewrap */
if (next_col < 0) {
next_col = term->cols - 1;
if (--next_row < 0)
break;
row = grid_row_in_view(term->grid, next_row);
if (row->linebreak) {
/* Hard linebreak, treat as space. I.e. break selection */
break;
}
}
c = row->cells[next_col].wc;
while (c >= CELL_SPACER) {
xassert(next_col > 0);
if (--next_col < 0)
return;
c = row->cells[next_col].wc;
}
if (c >= CELL_COMB_CHARS_LO && c <= CELL_COMB_CHARS_HI)
c = composed_lookup(term->composed, c - CELL_COMB_CHARS_LO)->chars[0];
bool is_space = c == 0 || iswspace(c);
bool is_delim =
!is_space && !isword(c, spaces_only, term->conf->word_delimiters);
bool is_word =
c != 0 && isword(c, spaces_only, term->conf->word_delimiters);
if (initial_is_space && !is_space)
break;
if (initial_is_delim && !is_delim)
break;
if (initial_is_word && !is_word)
break;
pos->col = next_col;
pos->row = next_row;
}
}
void
selection_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;
while (c >= CELL_SPACER) {
xassert(pos->col > 0);
if (pos->col == 0)
return;
pos->col--;
c = r->cells[pos->col].wc;
}
if (c >= CELL_COMB_CHARS_LO && c <= CELL_COMB_CHARS_HI)
c = composed_lookup(term->composed, c - CELL_COMB_CHARS_LO)->chars[0];
bool initial_is_space = c == 0 || iswspace(c);
bool initial_is_delim =
!initial_is_space && !isword(c, spaces_only, term->conf->word_delimiters);
bool initial_is_word =
c != 0 && isword(c, spaces_only, term->conf->word_delimiters);
while (true) {
int next_col = pos->col + 1;
int next_row = pos->row;
const struct row *row = grid_row_in_view(term->grid, next_row);
/* Linewrap */
if (next_col >= term->cols) {
if (row->linebreak) {
/* Hard linebreak, treat as space. I.e. break selection */
break;
}
next_col = 0;
if (++next_row >= term->rows)
break;
row = grid_row_in_view(term->grid, next_row);
}
c = row->cells[next_col].wc;
while (c >= CELL_SPACER) {
if (++next_col >= term->cols) {
next_col = 0;
if (++next_row >= term->rows)
return;
}
c = row->cells[next_col].wc;
}
if (c >= CELL_COMB_CHARS_LO && c <= CELL_COMB_CHARS_HI)
c = composed_lookup(term->composed, c - CELL_COMB_CHARS_LO)->chars[0];
bool is_space = c == 0 || iswspace(c);
bool is_delim =
!is_space && !isword(c, spaces_only, term->conf->word_delimiters);
bool is_word =
c != 0 && isword(c, spaces_only, term->conf->word_delimiters);
if (initial_is_space && !is_space)
break;
if (initial_is_delim && !is_delim)
break;
if (initial_is_word && !is_word)
break;
pos->col = next_col;
pos->row = next_row;
}
}
void
selection_start(struct terminal *term, int col, int row,
enum selection_kind kind,
bool spaces_only)
{
selection_cancel(term);
LOG_DBG("%s selection started at %d,%d",
kind == SELECTION_CHAR_WISE ? "character-wise" :
kind == SELECTION_WORD_WISE ? "word-wise" :
kind == SELECTION_LINE_WISE ? "line-wise" :
kind == SELECTION_BLOCK ? "block" : "<unknown>",
row, col);
term->selection.kind = kind;
term->selection.ongoing = true;
term->selection.spaces_only = spaces_only;
switch (kind) {
case SELECTION_CHAR_WISE:
case SELECTION_BLOCK:
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_WORD_WISE: {
struct coord start = {col, row}, end = {col, row};
selection_find_word_boundary_left(term, &start, spaces_only);
selection_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_LINE_WISE:
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;
case SELECTION_NONE:
BUG("Invalid selection kind");
break;
}
}
/* 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;
}
row->dirty = true;
cell->attrs.selected = 0;
cell->attrs.clean = 0;
return true;
}
static bool
premark_selected(struct terminal *term, struct row *row, struct cell *cell,
int col, void *data)
{
struct mark_context *ctx = data;
xassert(ctx != NULL);
if (ctx->last_row != row) {
ctx->last_row = row;
ctx->empty_count = 0;
}
if (cell->wc == 0 && term->selection.kind != SELECTION_BLOCK) {
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;
ctx->empty_count = 0;
return true;
}
static bool
mark_selected(struct terminal *term, struct row *row, struct cell *cell,
int col, void *data)
{
struct mark_context *ctx = data;
xassert(ctx != NULL);
if (ctx->last_row != row) {
ctx->last_row = row;
ctx->empty_count = 0;
}
if (cell->wc == 0 && term->selection.kind != SELECTION_BLOCK) {
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;
}
}
ctx->empty_count = 0;
return true;
}
static void
selection_modify(struct terminal *term, struct coord start, struct coord end)
{
xassert(term->selection.start.row != -1);
xassert(start.row != -1 && start.col != -1);
xassert(end.row != -1 && end.col != -1);
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);
}
static void
set_pivot_point_for_block_and_char_wise(struct terminal *term,
struct coord start,
enum selection_direction new_direction)
{
struct coord *pivot_start = &term->selection.pivot.start;
struct coord *pivot_end = &term->selection.pivot.end;
*pivot_start = start;
/* First, make sure ‘start’ isn’t in the middle of a
* multi-column character */
while (true) {
const struct row *row = term->grid->rows[pivot_start->row & (term->grid->num_rows - 1)];
const struct cell *cell = &row->cells[pivot_start->col];
if (cell->wc < CELL_SPACER)
break;
/* Multi-column chars don’t cross rows */
xassert(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 & (term->grid->num_rows - 1)];
const wchar_t wc = row->cells[pivot_end->col].wc;
keep_going = wc >= CELL_SPACER;
if (pivot_end->col == 0) {
if (pivot_end->row - term->grid->view <= 0)
break;
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 & (term->grid->num_rows - 1)];
const wchar_t wc = pivot_start->col < term->cols - 1
? row->cells[pivot_start->col + 1].wc : 0;
keep_going = wc >= CELL_SPACER;
if (pivot_start->col >= term->cols - 1) {
if (pivot_start->row - term->grid->view >= term->rows - 1)
break;
pivot_start->col = 0;
pivot_start->row++;
} else
pivot_start->col++;
}
}
xassert(term->grid->rows[pivot_start->row & (term->grid->num_rows - 1)]->
cells[pivot_start->col].wc <= CELL_SPACER);
xassert(term->grid->rows[pivot_end->row & (term->grid->num_rows - 1)]->
cells[pivot_end->col].wc <= CELL_SPACER + 1);
}
void
selection_update(struct terminal *term, int col, int row)
{
if (term->selection.start.row < 0)
return;
if (!term->selection.ongoing)
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);
xassert(term->grid->view + row != -1);
struct coord new_start = term->selection.start;
struct coord new_end = {col, term->grid->view + row};
/* 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 (term->selection.kind == SELECTION_BLOCK) {
if (new_end.col > pivot_start->col)
new_direction = SELECTION_RIGHT;
else
new_direction = SELECTION_LEFT;
if (term->selection.direction == SELECTION_UNDIR)
set_pivot_point_for_block_and_char_wise(term, *pivot_start, new_direction);
if (new_direction == SELECTION_LEFT)
new_start = *pivot_end;
else
new_start = *pivot_start;
term->selection.direction = new_direction;
} else {
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)
{
set_pivot_point_for_block_and_char_wise(
term, *pivot_start, new_direction);
}
if (new_direction == SELECTION_LEFT) {
xassert(pivot_end->row >= 0);
new_start = *pivot_end;
} else
new_start = *pivot_start;
term->selection.direction = new_direction;
}
}
}
switch (term->selection.kind) {
case SELECTION_CHAR_WISE:
case SELECTION_BLOCK:
break;
case SELECTION_WORD_WISE:
switch (term->selection.direction) {
case SELECTION_LEFT: {
struct coord end = {col, row};
selection_find_word_boundary_left(
term, &end, term->selection.spaces_only);
new_end = (struct coord){end.col, term->grid->view + end.row};
break;
}
case SELECTION_RIGHT: {
struct coord end = {col, row};
selection_find_word_boundary_right(
term, &end, term->selection.spaces_only);
new_end = (struct coord){end.col, term->grid->view + end.row};
break;
}
case SELECTION_UNDIR:
break;
}
break;
case SELECTION_LINE_WISE:
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;
case SELECTION_NONE:
BUG("Invalid selection kind");
break;
}
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];
/* 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_SPACER)
new_start.col--;
while (new_end.col < term->cols - 1 &&
row_end->cells[new_end.col + 1].wc >= CELL_SPACER)
new_end.col++;
} else {
while (new_end.col >= 1 &&
row_end->cells[new_end.col].wc >= CELL_SPACER)
new_end.col--;
while (new_start.col < term->cols - 1 &&
row_start->cells[new_start.col + 1].wc >= CELL_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,
&(struct mark_context){0});
}
static void
selection_extend_normal(struct terminal *term, int col, int row,
enum selection_kind new_kind)
{
const struct coord *start = &term->selection.start;
const struct coord *end = &term->selection.end;
if (start->row > end->row ||
(start->row == end->row && start->col > end->col))
{
const struct coord *tmp = start;
start = end;
end = tmp;
}
xassert(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;
}
}
switch (term->selection.kind) {
case SELECTION_CHAR_WISE:
xassert(new_kind == SELECTION_CHAR_WISE);
set_pivot_point_for_block_and_char_wise(term, new_start, direction);
break;
case SELECTION_WORD_WISE: {
xassert(new_kind == SELECTION_CHAR_WISE ||
new_kind == SELECTION_WORD_WISE);
struct coord pivot_start = {new_start.col, new_start.row - term->grid->view};
struct coord pivot_end = pivot_start;
selection_find_word_boundary_left(
term, &pivot_start, term->selection.spaces_only);
selection_find_word_boundary_right(
term, &pivot_end, term->selection.spaces_only);
term->selection.pivot.start =
(struct coord){pivot_start.col, term->grid->view + pivot_start.row};
term->selection.pivot.end =
(struct coord){pivot_end.col, term->grid->view + pivot_end.row};
break;
}
case SELECTION_LINE_WISE:
xassert(new_kind == SELECTION_CHAR_WISE ||
new_kind == SELECTION_LINE_WISE);
term->selection.pivot.start = (struct coord){0, new_start.row};
term->selection.pivot.end = (struct coord){term->cols - 1, new_start.row};
break;
case SELECTION_BLOCK:
case SELECTION_NONE:
BUG("Invalid selection kind in this context");
break;
}
term->selection.kind = new_kind;
term->selection.direction = direction;
selection_modify(term, new_start, new_end);
}
static void
selection_extend_block(struct terminal *term, int col, int row)
{
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;
enum selection_direction direction = SELECTION_UNDIR;
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};
}
}
direction = col > new_start.col ? SELECTION_RIGHT : SELECTION_LEFT;
set_pivot_point_for_block_and_char_wise(term, new_start, direction);
term->selection.direction = direction;
selection_modify(term, new_start, new_end);
}
void
selection_extend(struct seat *seat, struct terminal *term,
int col, int row, enum selection_kind new_kind)
{
if (term->selection.start.row < 0 || term->selection.end.row < 0) {
/* No existing selection */
return;
}
if (term->selection.kind == SELECTION_BLOCK && new_kind != SELECTION_BLOCK)
return;
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:
BUG("Invalid selection kind");
return;
case SELECTION_CHAR_WISE:
case SELECTION_WORD_WISE:
case SELECTION_LINE_WISE:
selection_extend_normal(term, col, row, new_kind);
break;
case SELECTION_BLOCK:
selection_extend_block(term, col, row);
break;
}
}
//static const struct zwp_primary_selection_source_v1_listener primary_selection_source_listener;
void
selection_finalize(struct seat *seat, struct terminal *term, uint32_t serial)
{
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;
xassert(term->selection.start.row != -1);
xassert(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;
}
xassert(term->selection.start.row <= term->selection.end.row);
switch (term->conf->selection_target) {
case SELECTION_TARGET_NONE:
break;
case SELECTION_TARGET_PRIMARY:
selection_to_primary(seat, term, serial);
break;
case SELECTION_TARGET_CLIPBOARD:
selection_to_clipboard(seat, term, serial);
break;
case SELECTION_TARGET_BOTH:
selection_to_primary(seat, term, serial);
selection_to_clipboard(seat, term, serial);
break;
}
}
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,
&unmark_selected, &(struct mark_context){0});
render_refresh(term);
}
term->selection.kind = SELECTION_NONE;
term->selection.start = (struct coord){-1, -1};
term->selection.end = (struct coord){-1, -1};
term->selection.pivot.start = (struct coord){-1, -1};
term->selection.pivot.end = (struct coord){-1, -1};
term->selection.direction = SELECTION_UNDIR;
term->selection.ongoing = false;
}
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 */
xassert(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;
xassert(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)
{
xassert(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) {
xassert(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;
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_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;
xassert(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;
xassert(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 don’t 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,
};
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;
xassert(primary->text != NULL);
send_clipboard_or_primary(seat, fd, primary->text, "primary");
}
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;
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 */
xassert(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);
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, 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, mime_type_map[DATA_OFFER_MIME_TEXT_TEXT]);;
wl_data_source_offer(clipboard->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_STRING]);
wl_data_source_offer(clipboard->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_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);
/* Needed when sending the selection to other client */
xassert(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;
bool bracketed;
void (*decoder)(struct clipboard_receive *ctx, char *data, size_t size);
void (*finish)(struct clipboard_receive *ctx);
/* 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;
xassert(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_finish_plain(struct clipboard_receive *ctx)
{
}
static bool
decode_one_uri(struct clipboard_receive *ctx, char *uri, size_t len)
{
LOG_DBG("URI: \"%.*s\"", (int)len, uri);
if (len == 0)
return false;
char *scheme, *host, *path;
if (!uri_parse(uri, len, &scheme, NULL, NULL, &host, NULL, &path, NULL, NULL)) {
LOG_ERR("drag-and-drop: invalid URI: %.*s", (int)len, uri);
return false;
}
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(uri, len, ctx->user);
free(scheme);
free(host);
free(path);
return true;
}
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 (true) {
for (end = start; end < &ctx->buf.data[ctx->buf.idx]; end++) {
if (*end == '\r' || *end == '\n')
break;
}
if (end >= &ctx->buf.data[ctx->buf.idx])
break;
decode_one_uri(ctx, start, end - start);
start = end + 1;
}
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 void
fdm_receive_finish_uri(struct clipboard_receive *ctx)
{
LOG_DBG("finish: %.*s", (int)ctx->buf.idx, ctx->buf.data);
decode_one_uri(ctx, ctx->buf.data, ctx->buf.idx);
}
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 replace:
* - \r\n -> \r
* - \n -> \r
* - C0 -> <nothing> (strip non-formatting C0 characters)
* - \e -> <nothing> (i.e. strip ESC)
*/
char *p = text;
size_t left = count;
#define skip_one() \
do { \
ctx->decoder(ctx, p, i); \
xassert(i + 1 <= left); \
p += i + 1; \
left -= i + 1; \
} while (0)
again:
for (size_t i = 0; i < left; i++) {
switch (p[i]) {
default:
break;
case '\n':
if (!ctx->bracketed)
p[i] = '\r';
break;
case '\r':
/* Convert \r\n -> \r */
if (!ctx->bracketed && i + 1 < left && p[i + 1] == '\n') {
i++;
skip_one();
goto again;
}
break;
/* C0 non-formatting control characters (\b \t \n \r excluded) */
case '\x01': case '\x02': case '\x03': case '\x04': case '\x05':
case '\x06': case '\x07': case '\x0e': case '\x0f': case '\x10':
case '\x11': case '\x12': case '\x13': case '\x14': case '\x15':
case '\x16': case '\x17': case '\x18': case '\x19': case '\x1a':
case '\x1b': case '\x1c': case '\x1d': case '\x1e': case '\x1f':
skip_one();
goto again;
/* Additional control characters stripped by default (but
* configurable) in XTerm: BS, HT, DEL */
case '\b': case '\t': case '\v': case '\f': case '\x7f':
if (!ctx->bracketed) {
skip_one();
goto again;
}
break;
}
}
ctx->decoder(ctx, p, left);
left = 0;
}
#undef skip_one
done:
ctx->finish(ctx);
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) {
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,
.bracketed = term->bracketed_paste,
.decoder = (mime_type == DATA_OFFER_MIME_URI_LIST
? &fdm_receive_decoder_uri
: &fdm_receive_decoder_plain),
.finish = (mime_type == DATA_OFFER_MIME_URI_LIST
? &fdm_receive_finish_uri
: &fdm_receive_finish_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;
}
/* 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;
}
LOG_DBG("receive from clipboard: mime-type=%s",
mime_type_map[clipboard->mime_type]);
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, 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);
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;
xassert(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 */
xassert(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, mime_type_map[DATA_OFFER_MIME_TEXT_TEXT]);
zwp_primary_selection_source_v1_offer(primary->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_STRING]);
zwp_primary_selection_source_v1_offer(primary->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_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;
}
LOG_DBG("receive from primary: mime-type=%s",
mime_type_map[primary->mime_type]);
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:
case DATA_OFFER_MIME_TEXT_TEXT:
case DATA_OFFER_MIME_TEXT_STRING:
/* 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:
case DATA_OFFER_MIME_TEXT_UTF8_STRING:
/* text/plain;charset=utf-8 is preferred over text/plain */
switch (*type) {
case DATA_OFFER_MIME_UNSET:
case DATA_OFFER_MIME_TEXT_PLAIN:
case DATA_OFFER_MIME_TEXT_TEXT:
case DATA_OFFER_MIME_TEXT_STRING:
*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;
xassert(offer == seat->clipboard.data_offer);
/* Remember _which_ terminal the current DnD offer is targeting */
xassert(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;
}
}
/* Either terminal is already busy sending paste data, or mouse
* pointer isn’t 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;
xassert(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)
{
/* Selection offer from other client */
struct seat *seat = data;
if (offer == NULL)
data_offer_reset(&seat->clipboard);
else
xassert(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
xassert(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,
};