osc: implement OSC-5522, kitty's extended version of OSC-52

OSC-5522[^1] gives the terminal application access to all offered
mime-types when reading the clipboard, and allows setting multiple
mime-types when writing to the clipboard (with different contents, if
it so wishes).

In addition to the base protocol, we also implement the _event
extension_[^2], where a paste action (e.g. ctrl+shift+v) results in an
unsolicited mime-type listing being sent to the terminal
application (as if it had issued a mime-type query itself), instead of
the clipboard content being pasted directly. The application follows
up with an explicit read request (or chooses to ignore the event).

The protocol supports "passwords", as a way of bypassing terminal
popups asking the user for approval, after the first popup has been
approved by the user. Foot doesn't implement this kind of user
approval, but all read and write requests are denied with EPERM if the
user has disabled OSC copy/pasting with the security.osc52
configuration option. In addition, if disabled, event reporting cannot
be enabled at all (i.e. 'CSI ? 5522 h' is ignored).

[^1]: https://sw.kovidgoyal.net/kitty/clipboard/
[^2]: https://rockorager.dev/misc/bracketed-paste-mime/
This commit is contained in:
Daniel Eklöf 2026-05-15 18:35:51 +02:00
parent 4bc8a39d6c
commit c366e322eb
No known key found for this signature in database
GPG key ID: 5BBD4992C116573F
13 changed files with 839 additions and 126 deletions

18
csi.c
View file

@ -576,6 +576,21 @@ decset_decrst(struct terminal *term, unsigned param, bool enable)
term_disable_size_notifications(term);
break;
case 5522:
if (enable) {
if (term_osc_paste_allowed(term))
term->kitty_clipboard.emit_events = true;
else {
static bool have_warned = false;
if (!have_warned) {
have_warned = true;
LOG_WARN("attempt to enable private mode 5522 ignored: disabled in configuration");
}
}
} else
term->kitty_clipboard.emit_events = false;
break;
case 8452:
term->sixel.cursor_right_of_graphics = enable;
break;
@ -665,6 +680,7 @@ decrqm(const struct terminal *term, unsigned param)
: decrpm(term->grapheme_shaping);
case 2031: return decrpm(term->report_theme_changes);
case 2048: return decrpm(term->size_notifications);
case 5522: return decrpm(term->kitty_clipboard.emit_events);
case 8452: return decrpm(term->sixel.cursor_right_of_graphics);
case 737769: return decrpm(term_ime_is_enabled(term));
}
@ -711,6 +727,7 @@ xtsave(struct terminal *term, unsigned param)
case 2027: term->xtsave.grapheme_shaping = term->grapheme_shaping; break;
case 2031: term->xtsave.report_theme_changes = term->report_theme_changes; break;
case 2048: term->xtsave.size_notifications = term->size_notifications; break;
case 5522: term->xtsave.kitty_clipboard = term->kitty_clipboard.emit_events; break;
case 8452: term->xtsave.sixel_cursor_right_of_graphics = term->sixel.cursor_right_of_graphics; break;
case 737769: term->xtsave.ime = term_ime_is_enabled(term); break;
}
@ -756,6 +773,7 @@ xtrestore(struct terminal *term, unsigned param)
case 2027: enable = term->xtsave.grapheme_shaping; break;
case 2031: enable = term->xtsave.report_theme_changes; break;
case 2048: enable = term->xtsave.size_notifications; break;
case 5522: enable = term->xtsave.kitty_clipboard; break;
case 8452: enable = term->xtsave.sixel_cursor_right_of_graphics; break;
case 737769: enable = term->xtsave.ime; break;

21
input.c
View file

@ -29,18 +29,15 @@
#include "keymap.h"
#include "kitty-keymap.h"
#include "macros.h"
#include "quirks.h"
#include "osc.h"
#include "render.h"
#include "search.h"
#include "selection.h"
#include "spawn.h"
#include "terminal.h"
#include "tokenize.h"
#include "unicode-mode.h"
#include "url-mode.h"
#include "util.h"
#include "vt.h"
#include "xkbcommon-vmod.h"
#include "xmalloc.h"
#include "xsnprintf.h"
@ -182,13 +179,21 @@ execute_binding(struct seat *seat, struct terminal *term,
return true;
case BIND_ACTION_CLIPBOARD_PASTE:
selection_from_clipboard(seat, term, serial);
term_reset_view(term);
if (likely(!term->kitty_clipboard.emit_events)) {
selection_from_clipboard(seat, term, serial);
term_reset_view(term);
} else {
kitty_clipboard_query(term, false);
}
return true;
case BIND_ACTION_PRIMARY_PASTE:
selection_from_primary(seat, term);
term_reset_view(term);
if (likely(!term->kitty_clipboard.emit_events)) {
selection_from_primary(seat, term);
term_reset_view(term);
} else {
kitty_clipboard_query(term, true);
}
return true;
case BIND_ACTION_SEARCH_START:

537
osc.c
View file

@ -51,24 +51,14 @@ osc_to_clipboard(struct terminal *term, const char *target,
}
/* Find a seat in which the terminal has focus */
struct seat *seat = NULL;
tll_foreach(term->wl->seats, it) {
if (it->item.kbd_focus == term) {
seat = &it->item;
break;
}
}
struct seat *seat = term_first_focused_seat(term);
if (seat == NULL) {
LOG_WARN("OSC52: client tried to write to clipboard data while window was unfocused");
LOG_WARN("OSC-52: client tried to write to clipboard data while window was unfocused");
return;
}
const bool copy_allowed = term->conf->security.osc52 == OSC52_ENABLED
|| term->conf->security.osc52 == OSC52_COPY_ENABLED;
if (!copy_allowed) {
LOG_DBG("ignoring copy request: disabled in configuration");
if (!term_osc_copy_allowed(term)) {
LOG_DBG("OSC-52: ignoring copy request: disabled in configuration");
return;
}
@ -93,13 +83,13 @@ osc_to_clipboard(struct terminal *term, const char *target,
if (to_clipboard) {
char *copy = xstrdup(decoded);
if (!text_to_clipboard(seat, term, copy, seat->kbd.serial))
if (!text_to_clipboard(seat, term, copy, NULL, seat->kbd.serial))
free(copy);
}
if (to_primary) {
char *copy = xstrdup(decoded);
if (!text_to_primary(seat, term, copy, seat->kbd.serial))
if (!text_to_primary(seat, term, copy, NULL, seat->kbd.serial))
free(copy);
}
@ -111,6 +101,14 @@ struct clip_context {
struct terminal *term;
uint8_t buf[3];
int idx;
struct {
bool is_kitty;
bool from_primary;
size_t chunk_bytes_written; /* Bytes written so far, in current chunk */
char *encoded_mime_type; /* Currently "active" mime-type, base64 encoded */
tll(char *) mime_types; /* Remaining mime-types */
} kitty;
};
static void
@ -137,6 +135,7 @@ from_clipboard_cb(char *text, size_t size, void *user)
term_paste_data_to_slave(term, chunk, 4);
free(chunk);
ctx->kitty.chunk_bytes_written += 3;
ctx->idx = 0;
}
}
@ -146,18 +145,33 @@ from_clipboard_cb(char *text, size_t size, void *user)
xassert(ctx->idx == 0);
int remaining = left % 3;
const int remaining = left % 3;
for (int i = remaining; i > 0; i--)
ctx->buf[ctx->idx++] = text[size - i];
xassert(ctx->idx == remaining);
char *chunk = base64_encode((const uint8_t *)t, left / 3 * 3);
const size_t count = left / 3 * 3;
char *chunk = base64_encode(t, count);
xassert(chunk != NULL);
xassert(strlen(chunk) % 4 == 0);
if (unlikely(ctx->kitty.is_kitty && ctx->kitty.chunk_bytes_written + count > 4096)) {
/* New chunk */
term_paste_data_to_slave(term, "\033\\", 2);
term_paste_data_to_slave(term, "\033]5522;type=read:status=DATA:mime=", 34);
term_paste_data_to_slave(term, ctx->kitty.encoded_mime_type,
strlen(ctx->kitty.encoded_mime_type));
term_paste_data_to_slave(term, ";", 1);
ctx->kitty.chunk_bytes_written = 0;
}
term_paste_data_to_slave(term, chunk, strlen(chunk));
ctx->kitty.chunk_bytes_written += count;
free(chunk);
}
static void kitty_clipboard_read_next_mime_type(struct clip_context *ctx);
static void
from_clipboard_done(void *user)
{
@ -168,6 +182,7 @@ from_clipboard_done(void *user)
char res[4];
base64_encode_final(ctx->buf, ctx->idx, res);
term_paste_data_to_slave(term, res, 4);
ctx->idx = 0;
}
if (term->vt.osc.bel)
@ -175,12 +190,27 @@ from_clipboard_done(void *user)
else
term_paste_data_to_slave(term, "\033\\", 2);
if (ctx->kitty.is_kitty) {
free(ctx->kitty.encoded_mime_type);
ctx->kitty.encoded_mime_type = NULL;
/* Continue with the next mime-type, if there are any left */
if (tll_length(ctx->kitty.mime_types) > 0) {
kitty_clipboard_read_next_mime_type(ctx);
return;
}
/* If not, we're done! */
term_paste_data_to_slave(term, "\033]5522;type=read:status=DONE\033\\", 30);
}
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);
free(ctx);
}
@ -188,23 +218,14 @@ static void
osc_from_clipboard(struct terminal *term, const char *source)
{
/* Find a seat in which the terminal has focus */
struct seat *seat = NULL;
tll_foreach(term->wl->seats, it) {
if (it->item.kbd_focus == term) {
seat = &it->item;
break;
}
}
struct seat *seat = term_first_focused_seat(term);
if (seat == NULL) {
LOG_WARN("OSC52: client tried to read clipboard data while window was unfocused");
LOG_WARN("OSC-52: client tried to read clipboard data while window was unfocused");
return;
}
const bool paste_allowed = term->conf->security.osc52 == OSC52_ENABLED
|| term->conf->security.osc52 == OSC52_PASTE_ENABLED;
if (!paste_allowed) {
LOG_DBG("ignoring paste request: disabled in configuration");
if (!term_osc_paste_allowed(term)) {
LOG_DBG("OSC-52: ignoring paste request: disabled in configuration");
return;
}
@ -261,12 +282,12 @@ osc_from_clipboard(struct terminal *term, const char *source)
if (from_clipboard) {
text_from_clipboard(
seat, term, true, &from_clipboard_cb, &from_clipboard_done, ctx);
seat, term, true, &from_clipboard_cb, &from_clipboard_done, ctx, NULL);
}
if (from_primary) {
text_from_primary(
seat, term, true, &from_clipboard_cb, &from_clipboard_done, ctx);
seat, term, true, &from_clipboard_cb, &from_clipboard_done, ctx, NULL);
}
}
@ -296,6 +317,453 @@ osc_selection(struct terminal *term, char *string)
osc_to_clipboard(term, string, p);
}
void
kitty_clipboard_query(struct terminal *term, bool primary)
{
/*
* Enumerate the available mime-types
*/
LOG_DBG("OSC-5522: query mime-types: primary=%d", primary);
/* Find a seat in which the terminal has focus */
struct seat *seat = term_first_focused_seat(term);
if (seat == NULL) {
LOG_WARN("OSC-5522: client tried to read clipboard data "
"while window was unfocused");
term_to_slave(term, "\033]5522;type=read:status=ENOSYS\033\\", 32);
return;
}
const mime_list_t *mime_list = !primary
? &seat->clipboard.all_mime_types
: &seat->primary.all_mime_types;
term_to_slave(term, "\033]5522;type=read:", 17);
if (primary)
term_to_slave(term, "loc=primary:", 12);
term_to_slave(term, "status=OK\033\\", 11);
term_to_slave(term, "\033]5522;type=read:status=DATA:mime=Lg==;", 39); /* base64(".") == "Lg==" */
char *mime_types = NULL;
size_t len = 0;
size_t pos = 0;
/* Calculate total length of the mime list */
tll_foreach(*mime_list, it)
len += strlen(it->item) + 1;
mime_types = xmalloc(len + 1);
mime_types[0] = '\0';
tll_foreach(*mime_list, it) {
strcpy(mime_types + pos, it->item);
pos += strlen(it->item);
mime_types[pos++] = ' ';
}
if (len > 0) {
/* Shave off the last ' ' */
len--;
mime_types[len] = '\0';
} else {
mime_types[0] = '\0';
}
char *encoded_mime_list = base64_encode_oneshot(mime_types, len);
term_to_slave(term, encoded_mime_list, strlen(encoded_mime_list));
free(encoded_mime_list);
free(mime_types);
term_to_slave(term, "\033\\", 2);
term_to_slave(term, "\033]5522;type=read:status=DONE\033\\", 30);
}
static void
kitty_clipboard_read_next_mime_type(struct clip_context *ctx)
{
struct terminal *term = ctx->term;
struct seat *seat = ctx->seat;
xassert(term->is_sending_paste_data);
xassert(ctx->kitty.encoded_mime_type == NULL);
xassert(tll_length(ctx->kitty.mime_types) > 0);
/* Pop next mime-type to read */
char *mime_type = tll_pop_front(ctx->kitty.mime_types);
const size_t mime_len = strlen(mime_type);
ctx->kitty.encoded_mime_type = base64_encode_oneshot(mime_type, mime_len);
term_paste_data_to_slave(term, "\033]5522;type=read:status=OK\033\\", 28);
term_paste_data_to_slave(term, "\033]5522;type=read:status=DATA:mime=", 34);
term_paste_data_to_slave(term, ctx->kitty.encoded_mime_type,
strlen(ctx->kitty.encoded_mime_type));
term_paste_data_to_slave(term, ";", 1);
if (!ctx->kitty.from_primary) {
text_from_clipboard(
seat, term, true, &from_clipboard_cb, &from_clipboard_done,
ctx, mime_type);
} else {
text_from_primary(
seat, term, true, &from_clipboard_cb, &from_clipboard_done,
ctx, mime_type);
}
free(mime_type);
}
static void
kitty_clipboard_read(struct terminal *term, bool primary, char *mime_types)
{
LOG_DBG("OSC-5522: read: primary=%d, mime-type=%s", primary, mime_types);
if (!term_osc_paste_allowed(term)) {
LOG_DBG("OSC-5522: ignoring paste request: disabled in configuration");
term_to_slave(term, "\033]5522;type=read:status=EPERM\033\\", 31);
return;
}
if (term->is_sending_paste_data) {
term_to_slave(term, "\033]5522;type=read:status=EBUSY\033\\", 31);
return;
}
/* Find a seat in which the terminal has focus */
struct seat *seat = term_first_focused_seat(term);
if (seat == NULL) {
LOG_WARN("OSC-5522: client tried to read clipboard data "
"while window was unfocused");
term_to_slave(term, "\033]5522;type=read:status=ENOSYS\033\\", 32);
return;
}
struct clip_context *ctx = xmalloc(sizeof(*ctx));
*ctx = (struct clip_context) {
.seat = seat,
.term = term,
.kitty = {
.is_kitty = true,
.from_primary = primary,
.mime_types = tll_init(),
},
};
/* Split upt the space-separated list of mime-types we're about to read */
for (char *save = NULL, *mime_type = strtok_r(mime_types, " ", &save);
mime_type != NULL;
mime_type = strtok_r(NULL, " ", &save))
{
tll_push_back(ctx->kitty.mime_types, xstrdup(mime_type));
}
/* Start reading the first mime-type */
term->is_sending_paste_data = true;
kitty_clipboard_read_next_mime_type(ctx);
}
void
kitty_clipboard_reset(struct terminal *term)
{
free(term->kitty_clipboard.active_mime_type);
free(term->kitty_clipboard.data);
tll_foreach(term->kitty_clipboard.committed_mime_data, it) {
struct kitty_mime_data *mime_data = &it->item;
free(mime_data->mime_type);
free(mime_data->data);
tll_remove(term->kitty_clipboard.committed_mime_data, it);
}
tll_foreach(term->kitty_clipboard.mime_aliases, it) {
struct kitty_mime_alias *alias = &it->item;
free(alias->target);
free(alias->alias);
tll_remove(term->kitty_clipboard.mime_aliases, it);
}
term->kitty_clipboard.for_primary = false;
term->kitty_clipboard.has_error = false;
term->kitty_clipboard.error = NULL;
term->kitty_clipboard.active_mime_type = NULL;
term->kitty_clipboard.data = NULL;
term->kitty_clipboard.data_len = 0;
}
static void
kitty_clipboard_wdata_commit(struct terminal *term)
{
if (term->kitty_clipboard.active_mime_type == NULL)
return;
LOG_DBG("committing %zu bytes of %s data",
term->kitty_clipboard.data_len,
term->kitty_clipboard.active_mime_type);
tll_push_back(term->kitty_clipboard.committed_mime_data,
((struct kitty_mime_data){
.mime_type = term->kitty_clipboard.active_mime_type,
.data = term->kitty_clipboard.data,
.data_len = term->kitty_clipboard.data_len}));
term->kitty_clipboard.active_mime_type = NULL;
term->kitty_clipboard.data = NULL;
term->kitty_clipboard.data_len = 0;
}
static void
kitty_clipboard_write_finish(struct terminal *term)
{
if (term->kitty_clipboard.has_error) {
/* There were error(s) along the way - report the first error */
char reply[64];
int n = snprintf(reply, sizeof(reply),
"\033]5522;type=write:status=%s\033\\",
term->kitty_clipboard.error);
term_to_slave(term, reply, n);
return;
}
/*
* No errors, now it's time to actually write to the clipboard
*/
struct seat *seat = term_first_focused_seat(term);
if (seat == NULL) {
LOG_WARN("OSC-5522: client tried to write6 clipboard data "
"while window was unfocused");
term_to_slave(term, "\033]5522;type=write:status=ENOSYS\033\\", 33);
return;
}
const size_t data_count = tll_length(term->kitty_clipboard.committed_mime_data);
const size_t mime_count = data_count + tll_length(term->kitty_clipboard.mime_aliases);
/* Package clipboard contents for text_to_{clipboard,primary}() */
struct kitty_clipboard_offer clip;
clip.data = xmalloc(data_count * sizeof(seat->clipboard.kitty.data[0]));
clip.data_len = xmalloc(data_count * sizeof(seat->clipboard.kitty.data_len[0]));
clip.data_count = data_count;
clip.mime_data_map = xmalloc(mime_count * sizeof(seat->clipboard.kitty.mime_data_map[0]));
clip.mime_data_map_count = mime_count;
size_t i = 0;
tll_foreach(term->kitty_clipboard.committed_mime_data, it) {
clip.data[i] = it->item.data;
clip.data_len[i] = it->item.data_len;
clip.mime_data_map[i].mime_type = it->item.mime_type;
clip.mime_data_map[i].data_idx = i;
i++;
/* Remove without freeing, data is now owned by clip */
tll_remove(term->kitty_clipboard.committed_mime_data, it);
}
tll_foreach(term->kitty_clipboard.mime_aliases, it) {
const struct kitty_mime_alias *alias = &it->item;
/* Find target index */
ssize_t idx = -1;
for (size_t j = 0; j < data_count; j++) {
if (streq(clip.mime_data_map[j].mime_type, alias->target)) {
idx = j;
break;
}
}
xassert(idx >= 0);
xassert(idx < data_count);
clip.mime_data_map[i].mime_type = alias->alias;
clip.mime_data_map[i].data_idx = idx;
/* TODO: can we make target point to the original mime-type, so that we don't have to free it here? */
free(alias->target);
tll_remove(term->kitty_clipboard.mime_aliases, it);
}
if (!term->kitty_clipboard.for_primary)
text_to_clipboard(seat, term, NULL, &clip, seat->kbd.serial);
else
text_to_primary(seat, term, NULL, &clip, seat->kbd.serial);
term_to_slave(term, "\033]5522;type=write:status=DONE\033\\", 31);
}
static void
kitty_clipboard(struct terminal *term, char *string)
{
char *ctx = NULL;
char *params = strtok_r(string, ";", &ctx);
char *encoded_payload = strtok_r(NULL, ";", &ctx);
enum { UNSPECIFIED, READ, WRITE, WDATA, WALIAS } type = UNSPECIFIED;
enum { CLIPBOARD, PRIMARY } location = CLIPBOARD;
const char *encoded_mime_type = NULL;
/* Parse the parameter (key/value pairs) section */
ctx = NULL;
for (char *p = strtok_r(params, ":", &ctx);
p != NULL;
p = strtok_r(NULL, ":", &ctx))
{
char *value = strchr(p, '=');
if (value == NULL)
continue;
*value = '\0';
value++;
LOG_DBG("OSC-5522: param: %s=%s", p, value);
if (streq(p, "type")) {
if (streq(value, "read"))
type = READ;
else if (streq(value, "write"))
type = WRITE;
else if (streq(value, "wdata"))
type = WDATA;
else if (streq(value, "walias"))
type = WALIAS;
else
LOG_WARN("OSC-5522: invalid 'type': %s", value);
}
else if (streq(p, "loc")) {
if (streq(value, "primary"))
location = PRIMARY;
else
LOG_WARN("OSC-5522: invalid 'loc': %s", value);
}
else if (streq(p, "mime")) {
encoded_mime_type = value;
}
else if (streq(p, "pw"))
;
else if (streq(p, "name"))
;
else
LOG_WARN("OSC-5522: unrecognized key/value parameter: %s=%s",
p, value);
}
if (type == UNSPECIFIED) {
LOG_WARN("OSC-5522: 'type' not specified");
/* Can't reply with status=EINVAL since we don't have a valid type */
return;
}
char *mime_type = encoded_mime_type != NULL
? base64_decode(encoded_mime_type, NULL)
: NULL;
size_t payload_len = 0;
char *payload = encoded_payload != NULL
? base64_decode(encoded_payload, &payload_len)
: NULL;
switch (type) {
case READ:
if (unlikely(payload == NULL))
term_to_slave(term, "\033]5522;type=read:status=EINVAL\033\\", 32);
else {
if (streq(payload, "."))
kitty_clipboard_query(term, location == PRIMARY);
else
kitty_clipboard_read(term, location == PRIMARY, payload);
}
break;
case WRITE: {
/* Reset */
kitty_clipboard_reset(term);
if (!term_osc_copy_allowed(term)) {
LOG_DBG("OSC-5522: ignoring copy request: disabled in configuration");
term->kitty_clipboard.has_error = true;
term->kitty_clipboard.error = "EPERM";
} else if (term->is_sending_paste_data) {
term->kitty_clipboard.has_error = true;
term->kitty_clipboard.error = "EBUSY";
} else
term->kitty_clipboard.for_primary = location == PRIMARY;
break;
}
case WDATA:
if (encoded_mime_type == NULL && encoded_payload == NULL) {
/* This was the last packet */
kitty_clipboard_wdata_commit(term);
kitty_clipboard_write_finish(term);
kitty_clipboard_reset(term);
} else if (!term->kitty_clipboard.has_error) {
if (unlikely(mime_type == NULL || payload == NULL)) {
term->kitty_clipboard.has_error = true;
term->kitty_clipboard.error = "EINVAL";
} else {
if (term->kitty_clipboard.active_mime_type == NULL ||
!streq(term->kitty_clipboard.active_mime_type, mime_type))
{
/*
* This chunk has a different mime-type than the
* last one. We need to commit the last one, start
* a new buffer for the new mime-type.
*/
if (term->kitty_clipboard.active_mime_type != NULL)
kitty_clipboard_wdata_commit(term);
term->kitty_clipboard.active_mime_type = mime_type;
mime_type = NULL;
}
const size_t old_len = term->kitty_clipboard.data_len;
const size_t new_len = old_len + payload_len;
term->kitty_clipboard.data = xrealloc(term->kitty_clipboard.data, new_len);
memcpy(&term->kitty_clipboard.data[old_len], payload, payload_len);
term->kitty_clipboard.data_len = new_len;
}
}
break;
case WALIAS:
if (!term->kitty_clipboard.has_error) {
if (unlikely(mime_type == NULL || payload == NULL)) {
term->kitty_clipboard.has_error = true;
term->kitty_clipboard.error = "EINVAL";
} else {
/* Aliases is a space separated list of mime-types */
char *save = NULL;
for (const char *alias = strtok_r(payload, " ", &save);
alias != NULL;
alias = strtok_r(NULL, " ", &save))
{
tll_push_back(
term->kitty_clipboard.mime_aliases,
((struct kitty_mime_alias){
.target = xstrdup(mime_type),
.alias = xstrdup(alias)}));
}
}
}
break;
case UNSPECIFIED:
BUG("'type' is unspecified");
break;
}
free(mime_type);
free(payload);
}
static void
osc_flash(struct terminal *term)
{
@ -1501,6 +1969,9 @@ osc_dispatch(struct terminal *term)
case 52: /* Copy to/from clipboard/primary */
osc_selection(term, string);
break;
case 5522:
kitty_clipboard(term, string);
break;
case 66: /* text-size protocol (kitty) */
kitty_text_size(term, string);

3
osc.h
View file

@ -5,3 +5,6 @@
bool osc_ensure_size(struct terminal *term, size_t required_size);
void osc_dispatch(struct terminal *term);
void kitty_clipboard_reset(struct terminal *term);
void kitty_clipboard_query(struct terminal *term, bool primary);

View file

@ -3734,13 +3734,7 @@ render_search_box(struct terminal *term)
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
/* TODO: do we want to/need to handle multi-seat? */
struct seat *ime_seat = NULL;
tll_foreach(term->wl->seats, it) {
if (it->item.kbd_focus == term) {
ime_seat = &it->item;
break;
}
}
struct seat *ime_seat = term_first_focused_seat(term);
size_t text_len = term->search.len;
if (ime_seat != NULL && ime_seat->ime.preedit.text != NULL)

View file

@ -1375,13 +1375,13 @@ execute_binding(struct seat *seat, struct terminal *term,
case BIND_ACTION_SEARCH_CLIPBOARD_PASTE:
text_from_clipboard(
seat, term, false, &from_clipboard_cb, &from_clipboard_done, term);
seat, term, false, &from_clipboard_cb, &from_clipboard_done, term, NULL);
*update_search_result = *redraw = true;
return true;
case BIND_ACTION_SEARCH_PRIMARY_PASTE:
text_from_primary(
seat, term, false, &from_clipboard_cb, &from_clipboard_done, term);
seat, term, false, &from_clipboard_cb, &from_clipboard_done, term, NULL);
*update_search_result = *redraw = true;
return true;

View file

@ -1631,6 +1631,29 @@ selection_primary_has_data(const struct seat *seat)
return seat->primary.data_offer != NULL;
}
static void
clipboard_reset(struct wl_clipboard *clipboard)
{
for (size_t i = 0; i < clipboard->kitty.data_count; i++)
free(clipboard->kitty.data[i]);
for (size_t i = 0; i < clipboard->kitty.mime_data_map_count; i++)
free(clipboard->kitty.mime_data_map[i].mime_type);
free(clipboard->text);
free(clipboard->kitty.data);
free(clipboard->kitty.data_len);
free(clipboard->kitty.mime_data_map);
clipboard->data_source = NULL;
clipboard->serial = 0;
clipboard->text = NULL;
clipboard->kitty.data = NULL;
clipboard->kitty.mime_data_map = NULL;
clipboard->kitty.data_count = 0;
clipboard->kitty.mime_data_map_count = 0;
}
void
selection_clipboard_unset(struct seat *seat)
{
@ -1643,12 +1666,30 @@ selection_clipboard_unset(struct seat *seat)
xassert(clipboard->serial != 0);
wl_data_device_set_selection(seat->data_device, NULL, clipboard->serial);
wl_data_source_destroy(clipboard->data_source);
clipboard_reset(clipboard);
}
clipboard->data_source = NULL;
clipboard->serial = 0;
static void
primary_reset(struct wl_primary *primary)
{
free(clipboard->text);
clipboard->text = NULL;
for (size_t i = 0; i < primary->kitty.data_count; i++)
free(primary->kitty.data[i]);
for (size_t i = 0; i < primary->kitty.mime_data_map_count; i++)
free(primary->kitty.mime_data_map[i].mime_type);
free(primary->text);
free(primary->kitty.data);
free(primary->kitty.data_len);
free(primary->kitty.mime_data_map);
primary->data_source = NULL;
primary->serial = 0;
primary->text = NULL;
primary->kitty.data = NULL;
primary->kitty.mime_data_map = NULL;
primary->kitty.data_count = 0;
primary->kitty.mime_data_map_count = 0;
}
void
@ -1663,12 +1704,7 @@ selection_primary_unset(struct seat *seat)
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;
primary_reset(primary);
}
static bool
@ -1816,8 +1852,8 @@ done:
}
static void
send_clipboard_or_primary(struct seat *seat, int fd, const char *selection,
const char *source_name)
send_clipboard_or_primary(struct seat *seat, int fd, const uint8_t *selection,
size_t len, 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 */
@ -1829,17 +1865,18 @@ send_clipboard_or_primary(struct seat *seat, int fd, const char *selection,
return;
}
size_t len = selection != NULL ? strlen(selection) : 0;
size_t async_idx = 0;
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]),
//.data = xstrdup(&selection[async_idx]),
.data = xmalloc(len - async_idx),
.len = len - async_idx,
.idx = 0,
};
memcpy(ctx->data, &selection[async_idx], ctx->len);
if (fdm_add(seat->wayl->fdm, fd, EPOLLOUT, &fdm_send, ctx))
return;
@ -1868,7 +1905,21 @@ send(void *data, struct wl_data_source *wl_data_source, const char *mime_type,
struct seat *seat = data;
const struct wl_clipboard *clipboard = &seat->clipboard;
send_clipboard_or_primary(seat, fd, clipboard->text, "clipboard");
if (clipboard->text != NULL) {
send_clipboard_or_primary(
seat, fd, (const uint8_t *)clipboard->text, strlen(clipboard->text), "clipboard");
} else {
for (size_t i = 0; i < clipboard->kitty.mime_data_map_count; i++) {
const struct mime_data_map *map = &clipboard->kitty.mime_data_map[i];
if (streq(map->mime_type, mime_type)) {
send_clipboard_or_primary(
seat, fd, clipboard->kitty.data[map->data_idx],
clipboard->kitty.data_len[map->data_idx], "clipboard");
break;
}
}
}
}
static void
@ -1879,11 +1930,7 @@ cancelled(void *data, struct wl_data_source *wl_data_source)
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;
clipboard_reset(clipboard);
}
/* We don't support dragging *from* */
@ -1922,7 +1969,21 @@ primary_send(void *data,
struct seat *seat = data;
const struct wl_primary *primary = &seat->primary;
send_clipboard_or_primary(seat, fd, primary->text, "primary");
if (primary->text != NULL) {
send_clipboard_or_primary(
seat, fd, (const uint8_t *)primary->text, strlen(primary->text), "primary");
} else {
for (size_t i = 0; i < primary->kitty.mime_data_map_count; i++) {
const struct mime_data_map *map = &primary->kitty.mime_data_map[i];
if (streq(map->mime_type, mime_type)) {
send_clipboard_or_primary(
seat, fd, primary->kitty.data[map->data_idx],
primary->kitty.data_len[map->data_idx], "primary");
break;
}
}
}
}
static void
@ -1933,11 +1994,7 @@ primary_cancelled(void *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;
primary_reset(primary);
}
static const struct zwp_primary_selection_source_v1_listener primary_selection_source_listener = {
@ -1946,12 +2003,15 @@ static const struct zwp_primary_selection_source_v1_listener primary_selection_s
};
bool
text_to_clipboard(struct seat *seat, struct terminal *term, char *text, uint32_t serial)
text_to_clipboard(struct seat *seat, struct terminal *term, char *text,
struct kitty_clipboard_offer *kitty_data, uint32_t serial)
{
if (text == NULL || text[0] == '\0')
return false;
xassert(serial != 0);
xassert((text != NULL && kitty_data == NULL) ||
(text == NULL && kitty_data != NULL));
struct wl_clipboard *clipboard = &seat->clipboard;
@ -1960,11 +2020,7 @@ text_to_clipboard(struct seat *seat, struct terminal *term, char *text, uint32_t
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_reset(clipboard);
}
clipboard->data_source
@ -1978,11 +2034,23 @@ text_to_clipboard(struct seat *seat, struct terminal *term, char *text, uint32_t
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]);
if (text != NULL) {
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]);
} else {
xassert(clipboard->kitty.data_count == 0);
xassert(clipboard->kitty.mime_data_map_count == 0);
clipboard->kitty = *kitty_data;
for (size_t i = 0; i < clipboard->kitty.mime_data_map_count; i++) {
wl_data_source_offer(
clipboard->data_source, clipboard->kitty.mime_data_map[i].mime_type);
}
}
wl_data_source_add_listener(clipboard->data_source, &data_source_listener, seat);
wl_data_device_set_selection(seat->data_device, clipboard->data_source, serial);
@ -2000,7 +2068,7 @@ selection_to_clipboard(struct seat *seat, struct terminal *term, uint32_t serial
/* Get selection as a string */
char *text = selection_to_text(term);
if (!text_to_clipboard(seat, term, text, serial))
if (!text_to_clipboard(seat, term, text, NULL, serial))
free(text);
}
@ -2339,11 +2407,11 @@ void
text_from_clipboard(struct seat *seat, struct terminal *term,
bool no_strip,
void (*cb)(char *data, size_t size, void *user),
void (*done)(void *user), void *user)
void (*done)(void *user), void *user, const char *custom_mime_type)
{
struct wl_clipboard *clipboard = &seat->clipboard;
if (clipboard->data_offer == NULL ||
clipboard->mime_type == DATA_OFFER_MIME_UNSET)
(clipboard->mime_type == DATA_OFFER_MIME_UNSET && custom_mime_type == NULL))
{
done(user);
return;
@ -2357,21 +2425,25 @@ text_from_clipboard(struct seat *seat, struct terminal *term,
return;
}
LOG_DBG("receive from clipboard: mime-type=%s",
mime_type_map[clipboard->mime_type]);
const char *mime_type = custom_mime_type == NULL
? mime_type_map[clipboard->mime_type]
: custom_mime_type;
LOG_DBG("receive from clipboard: mime-type=%s", 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);
wl_data_offer_receive(clipboard->data_offer, 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, no_strip, read_fd, clipboard->mime_type, cb, done, user);
term, no_strip, read_fd,
custom_mime_type == NULL ? clipboard->mime_type : DATA_OFFER_MIME_TYPE_CUSTOM,
cb, done, user);
}
static void
@ -2415,11 +2487,12 @@ selection_from_clipboard(struct seat *seat, struct terminal *term, uint32_t seri
term_paste_data_to_slave(term, "\033[200~", 6);
text_from_clipboard(
seat, term, false, &receive_offer, &receive_offer_done, term);
seat, term, false, &receive_offer, &receive_offer_done, term, NULL);
}
bool
text_to_primary(struct seat *seat, struct terminal *term, char *text, uint32_t serial)
text_to_primary(struct seat *seat, struct terminal *term, char *text,
struct kitty_clipboard_offer *kitty_data, uint32_t serial)
{
if (text == NULL || text[0] == '\0')
return false;
@ -2428,6 +2501,8 @@ text_to_primary(struct seat *seat, struct terminal *term, char *text, uint32_t s
return false;
xassert(serial != 0);
xassert((text != NULL && kitty_data == NULL) ||
(text == NULL && kitty_data != NULL));
struct wl_primary *primary = &seat->primary;
@ -2439,11 +2514,7 @@ text_to_primary(struct seat *seat, struct terminal *term, char *text, uint32_t s
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_reset(primary);
}
primary->data_source
@ -2459,11 +2530,26 @@ text_to_primary(struct seat *seat, struct terminal *term, char *text, uint32_t s
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]);
if (text != NULL) {
xassert(primary->kitty.data_count == 0);
xassert(primary->kitty.mime_data_map_count == 0);
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]);
} else {
xassert(primary->kitty.data_count == 0);
xassert(primary->kitty.mime_data_map_count == 0);
primary->kitty = *kitty_data;
for (size_t i = 0; i < primary->kitty.mime_data_map_count; i++) {
zwp_primary_selection_source_v1_offer(
primary->data_source, primary->kitty.mime_data_map[i].mime_type);
}
}
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);
@ -2481,7 +2567,7 @@ selection_to_primary(struct seat *seat, struct terminal *term, uint32_t serial)
/* Get selection as a string */
char *text = selection_to_text(term);
if (!text_to_primary(seat, term, text, serial))
if (!text_to_primary(seat, term, text, NULL, serial))
free(text);
}
@ -2489,7 +2575,7 @@ void
text_from_primary(
struct seat *seat, struct terminal *term, bool no_strip,
void (*cb)(char *data, size_t size, void *user),
void (*done)(void *user), void *user)
void (*done)(void *user), void *user, const char *custom_mime_type)
{
if (term->wl->primary_selection_device_manager == NULL) {
done(user);
@ -2498,7 +2584,7 @@ text_from_primary(
struct wl_primary *primary = &seat->primary;
if (primary->data_offer == NULL ||
primary->mime_type == DATA_OFFER_MIME_UNSET)
(primary->mime_type == DATA_OFFER_MIME_UNSET && custom_mime_type == NULL))
{
done(user);
return;
@ -2512,21 +2598,25 @@ text_from_primary(
return;
}
LOG_DBG("receive from primary: mime-type=%s",
mime_type_map[primary->mime_type]);
const char *mime_type = custom_mime_type == NULL
? mime_type_map[primary->mime_type]
: custom_mime_type;
LOG_DBG("receive from primary: mime-type=%s", 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);
zwp_primary_selection_offer_v1_receive(primary->data_offer, 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, no_strip, read_fd, primary->mime_type, cb, done, user);
term, no_strip, read_fd,
custom_mime_type == NULL ? primary->mime_type : DATA_OFFER_MIME_TYPE_CUSTOM,
cb, done, user);
}
void
@ -2549,7 +2639,7 @@ selection_from_primary(struct seat *seat, struct terminal *term)
term_paste_data_to_slave(term, "\033[200~", 6);
text_from_primary(
seat, term, false, &receive_offer, &receive_offer_done, term);
seat, term, false, &receive_offer, &receive_offer_done, term, NULL);
}
static void
@ -2614,6 +2704,9 @@ select_mime_type_for_offer(const char *_mime_type,
case DATA_OFFER_MIME_UNSET:
break;
case DATA_OFFER_MIME_TYPE_CUSTOM:
BUG("the custom mime-type should never have been selected");
}
}
@ -2627,6 +2720,7 @@ data_offer_reset(struct wl_clipboard *clipboard)
clipboard->window = NULL;
clipboard->mime_type = DATA_OFFER_MIME_UNSET;
tll_free_and_free(clipboard->all_mime_types, free);
}
static void
@ -2634,6 +2728,7 @@ 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);
tll_push_back(seat->clipboard.all_mime_types, xstrdup(mime_type));
}
static void
@ -2844,6 +2939,7 @@ drop(void *data, struct wl_data_device *wl_data_device)
/* data offer is now "owned" by the receive context */
clipboard->data_offer = NULL;
clipboard->mime_type = DATA_OFFER_MIME_UNSET;
tll_free_and_free(clipboard->all_mime_types, free);
}
static void
@ -2875,6 +2971,7 @@ primary_offer(void *data,
LOG_DBG("primary offer: %s", mime_type);
struct seat *seat = data;
select_mime_type_for_offer(mime_type, &seat->primary.mime_type);
tll_push_back(seat->primary.all_mime_types, xstrdup(mime_type));
}
static const struct zwp_primary_selection_offer_v1_listener primary_selection_offer_listener = {
@ -2890,6 +2987,7 @@ primary_offer_reset(struct wl_primary *primary)
}
primary->mime_type = DATA_OFFER_MIME_UNSET;
tll_free_and_free(primary->all_mime_types, free);
}
static void
@ -2922,4 +3020,3 @@ const struct zwp_primary_selection_device_v1_listener primary_selection_device_l
.data_offer = &primary_data_offer,
.selection = &primary_selection,
};

View file

@ -44,9 +44,11 @@ void selection_from_primary(struct seat *seat, struct terminal *term);
/* Copy text *to* primary/clipboard */
bool text_to_clipboard(
struct seat *seat, struct terminal *term, char *text, uint32_t serial);
struct seat *seat, struct terminal *term, char *text,
struct kitty_clipboard_offer *kitty_data, uint32_t serial);
bool text_to_primary(
struct seat *seat, struct terminal *term, char *text, uint32_t serial);
struct seat *seat, struct terminal *term, char *text,
struct kitty_clipboard_offer *kitty_data, uint32_t serial);
/*
* Copy text *from* primary/clipboard
@ -65,12 +67,12 @@ bool text_to_primary(
void text_from_clipboard(
struct seat *seat, struct terminal *term, bool no_strip,
void (*cb)(char *data, size_t size, void *user),
void (*done)(void *user), void *user);
void (*done)(void *user), void *user, const char *custom_mime_type);
void text_from_primary(
struct seat *seat, struct terminal *term, bool no_strip,
void (*cb)(char *data, size_t size, void *user),
void (*dont)(void *user), void *user);
void (*dont)(void *user), void *user, const char *custom_mime_type);
void selection_start_scroll_timer(
struct terminal *term, int interval_ns,

View file

@ -33,7 +33,7 @@
#include "ime.h"
#include "input.h"
#include "notify.h"
#include "quirks.h"
#include "osc.h"
#include "reaper.h"
#include "render.h"
#include "selection.h"
@ -1939,6 +1939,8 @@ term_destroy(struct terminal *term)
for (size_t i = 0; i < ALEN(term->notification_icons); i++)
notify_icon_free(&term->notification_icons[i]);
kitty_clipboard_reset(term);
sixel_fini(term);
term_ime_reset(term);
@ -2167,6 +2169,8 @@ term_reset(struct terminal *term, bool hard)
for (size_t i = 0; i < ALEN(term->notification_icons); i++)
notify_icon_free(&term->notification_icons[i]);
kitty_clipboard_reset(term);
term->grapheme_shaping = term->conf->tweak.grapheme_shaping;
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
@ -4830,3 +4834,28 @@ term_theme_get(const struct terminal *term)
? &term->conf->colors_dark
: &term->conf->colors_light;
}
bool
term_osc_paste_allowed(const struct terminal *term)
{
return term->conf->security.osc52 == OSC52_ENABLED
|| term->conf->security.osc52 == OSC52_PASTE_ENABLED;
}
bool
term_osc_copy_allowed(const struct terminal *term)
{
return term->conf->security.osc52 == OSC52_ENABLED
|| term->conf->security.osc52 == OSC52_COPY_ENABLED;
}
struct seat *
term_first_focused_seat(struct terminal *term)
{
tll_foreach(term->wl->seats, it) {
if (it->item.kbd_focus == term)
return &it->item;
}
return NULL;
}

View file

@ -398,6 +398,19 @@ struct colors {
enum which_color_theme active_theme;
};
struct kitty_mime_data {
char *mime_type;
uint8_t *data;
size_t data_len;
};
typedef tll(struct kitty_mime_data) kitty_mime_data_list_t;
struct kitty_mime_alias {
char *target;
char *alias;
};
typedef tll(struct kitty_mime_alias) kitty_mime_alias_list_t;
struct terminal {
struct fdm *fdm;
struct reaper *reaper;
@ -542,6 +555,7 @@ struct terminal {
bool report_theme_changes:1;
bool size_notifications:1;
bool kitty_clipboard:1;
bool sixel_display_mode:1;
bool sixel_private_palette:1;
@ -822,6 +836,30 @@ struct terminal {
/* State, to handle chunked notifications */
struct notification kitty_notification;
/* OSC-5522 */
struct {
bool emit_events; /* Enabled/disabled via private mode 5522 */
/*
* State for setting the clipboard (OSC-5522;type=write|wdata|walias)
*/
bool for_primary;
bool has_error; /* When set, all subsequent wdata|walias packets are ignored */
const char *error; /* EIO|EINVAL|ENOSYS|EPERM|EBUSY */
/* Current mime-type being collected (via multiple wdata packets) */
char *active_mime_type;
uint8_t *data;
size_t data_len;
/* mime-type aliases, collected via one or more walias packets */
kitty_mime_alias_list_t mime_aliases;
/* Finished mime-types (will be written to the clipboard when
* the final wdata packet is received) */
kitty_mime_data_list_t committed_mime_data;
} kitty_clipboard;
/* Currently active notifications, from foot's perspective (their
notification helper processes are still running) */
tll(struct notification) active_notifications;
@ -990,6 +1028,12 @@ void term_theme_switch_to_light(struct terminal *term);
void term_theme_toggle(struct terminal *term);
const struct color_theme *term_theme_get(const struct terminal *term);
bool term_osc_paste_allowed(const struct terminal *term);
bool term_osc_copy_allowed(const struct terminal *term);
/* Get (first) seat that is focusing this terminal instance */
struct seat *term_first_focused_seat(struct terminal *term);
static inline void term_reset_grapheme_state(struct terminal *term)
{
#if defined(FOOT_GRAPHEME_CLUSTERING)

View file

@ -168,7 +168,7 @@ activate_url(struct seat *seat, struct terminal *term, const struct url *url,
if (term->bracketed_paste)
term_to_slave(term, "\033[201~", 6);
}
if (text_to_clipboard(seat, term, url_string, seat->kbd.serial)) {
if (text_to_clipboard(seat, term, url_string, NULL, seat->kbd.serial)) {
/* Now owned by our clipboard “manager” */
url_string = NULL;
}

View file

@ -233,6 +233,24 @@ seat_destroy(struct seat *seat)
free(seat->primary.text);
free(seat->pointer.last_custom_xcursor);
free(seat->name);
for (size_t i = 0; i < seat->clipboard.kitty.data_count; i++)
free(seat->clipboard.kitty.data[i]);
free(seat->clipboard.kitty.data);
free(seat->clipboard.kitty.data_len);
for (size_t i = 0; i < seat->clipboard.kitty.mime_data_map_count; i++)
free(seat->clipboard.kitty.mime_data_map[i].mime_type);
free(seat->clipboard.kitty.mime_data_map);
tll_free_and_free(seat->clipboard.all_mime_types, free);
for (size_t i = 0; i < seat->primary.kitty.data_count; i++)
free(seat->primary.kitty.data[i]);
free(seat->primary.kitty.data);
free(seat->primary.kitty.data_len);
for (size_t i = 0; i < seat->primary.kitty.mime_data_map_count; i++)
free(seat->primary.kitty.mime_data_map[i].mime_type);
free(seat->primary.kitty.mime_data_map);
tll_free_and_free(seat->primary.all_mime_types, free);
}
static void

View file

@ -51,6 +51,8 @@ enum data_offer_mime_type {
DATA_OFFER_MIME_TEXT_TEXT,
DATA_OFFER_MIME_TEXT_STRING,
DATA_OFFER_MIME_TEXT_UTF8_STRING,
DATA_OFFER_MIME_TYPE_CUSTOM,
};
enum touch_state {
@ -76,22 +78,52 @@ struct wayl_sub_surface {
struct wl_subsurface *sub;
};
typedef tll(char *) mime_list_t;
struct mime_data_map {
char *mime_type;
size_t data_idx;
};
/* OSC-5522: kitty's extended OSC-52, with explicit mime-type support */
struct kitty_clipboard_offer {
/*
* - data[] is an array of contents
* - data_len[] is an array of the same size as data[], each element
* denoting the size of the corresponding data[] element
* - mime_data_map[] is an array mapping mime-types to an entry in data[]
*/
uint8_t **data;
size_t *data_len;
size_t data_count;
struct mime_data_map *mime_data_map;
size_t mime_data_map_count;
};
struct wl_window;
struct wl_clipboard {
struct wl_window *window; /* For DnD */
struct wl_data_source *data_source;
struct wl_data_offer *data_offer;
enum data_offer_mime_type mime_type;
enum data_offer_mime_type mime_type; /* Preferred mime-type */
mime_list_t all_mime_types; /* List of all offered mime-types (for OSC-5522) */
char *text;
uint32_t serial;
struct kitty_clipboard_offer kitty;
};
struct wl_primary {
struct zwp_primary_selection_source_v1 *data_source;
struct zwp_primary_selection_offer_v1 *data_offer;
enum data_offer_mime_type mime_type;
enum data_offer_mime_type mime_type; /* Preferred mime-type */
mime_list_t all_mime_types; /* List of all offered mime-types (for OSC-5522) */
char *text;
uint32_t serial;
struct kitty_clipboard_offer kitty;
};
/* Maps a mouse button to its "owning" surface */