mirror of
https://codeberg.org/dnkl/foot.git
synced 2026-02-05 04:06:08 -05:00
2236 lines
62 KiB
C
2236 lines
62 KiB
C
#include "selection.h"
|
||
|
||
#include <ctype.h>
|
||
#include <string.h>
|
||
#include <unistd.h>
|
||
#include <fcntl.h>
|
||
#include <errno.h>
|
||
#include <wctype.h>
|
||
|
||
#include <sys/epoll.h>
|
||
#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",
|
||
};
|
||
|
||
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
|
||
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:
|
||
xassert(false);
|
||
return;
|
||
}
|
||
|
||
xassert(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;
|
||
|
||
while (c == CELL_MULT_COL_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_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 =
|
||
!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;
|
||
|
||
/* 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;
|
||
while (c == CELL_MULT_COL_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_LO + term->composed_count))
|
||
{
|
||
c = term->composed[c - CELL_COMB_CHARS_LO].base;
|
||
}
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
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;
|
||
|
||
while (c == CELL_MULT_COL_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_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 =
|
||
!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;
|
||
|
||
/* 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;
|
||
while (c == CELL_MULT_COL_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_LO + term->composed_count))
|
||
{
|
||
c = term->composed[c - CELL_COMB_CHARS_LO].base;
|
||
}
|
||
|
||
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};
|
||
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_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:
|
||
xassert(false);
|
||
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;
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
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_MULT_COL_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_MULT_COL_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_MULT_COL_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_MULT_COL_SPACER);
|
||
xassert(term->grid->rows[pivot_end->row & (term->grid->num_rows - 1)]->
|
||
cells[pivot_end->col].wc != CELL_MULT_COL_SPACER);
|
||
}
|
||
|
||
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};
|
||
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};
|
||
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:
|
||
xassert(false);
|
||
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_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,
|
||
&(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;
|
||
|
||
find_word_boundary_left(term, &pivot_start, term->selection.spaces_only);
|
||
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:
|
||
xassert(false);
|
||
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:
|
||
xassert(false);
|
||
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);
|
||
selection_to_primary(seat, term, serial);
|
||
}
|
||
|
||
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, "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);
|
||
|
||
/* 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;
|
||
|
||
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 ((end = memchr(start, '\n', ctx->buf.idx - (start - ctx->buf.data))) != NULL) {
|
||
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 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);
|
||
|
||
xassert(i + 1 <= left);
|
||
p += i + 1;
|
||
left -= i + 1;
|
||
goto again;
|
||
}
|
||
}
|
||
|
||
ctx->decoder(ctx, p, left);
|
||
left = 0;
|
||
}
|
||
|
||
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,
|
||
.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, "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;
|
||
}
|
||
|
||
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:
|
||
/* 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;
|
||
|
||
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,
|
||
};
|