diff --git a/csi.c b/csi.c index 176cc3ac..6a2d19ff 100644 --- a/csi.c +++ b/csi.c @@ -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; diff --git a/input.c b/input.c index 6a829a70..249f7605 100644 --- a/input.c +++ b/input.c @@ -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: diff --git a/osc.c b/osc.c index 95af5bff..ff126285 100644 --- a/osc.c +++ b/osc.c @@ -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); diff --git a/osc.h b/osc.h index 0820a8fc..91a002ec 100644 --- a/osc.h +++ b/osc.h @@ -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); diff --git a/render.c b/render.c index cf5f969a..c04ce9f8 100644 --- a/render.c +++ b/render.c @@ -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) diff --git a/search.c b/search.c index ef0a15bb..23a0413f 100644 --- a/search.c +++ b/search.c @@ -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; diff --git a/selection.c b/selection.c index bb0d3f1b..e8dc4361 100644 --- a/selection.c +++ b/selection.c @@ -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, }; - diff --git a/selection.h b/selection.h index b6ad099a..f201d35f 100644 --- a/selection.h +++ b/selection.h @@ -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, diff --git a/terminal.c b/terminal.c index 8eafbcbe..f8947aa6 100644 --- a/terminal.c +++ b/terminal.c @@ -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; +} diff --git a/terminal.h b/terminal.h index 446d5f23..d2d8bbaf 100644 --- a/terminal.h +++ b/terminal.h @@ -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) diff --git a/url-mode.c b/url-mode.c index 45fdaf7a..fb2f10d6 100644 --- a/url-mode.c +++ b/url-mode.c @@ -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; } diff --git a/wayland.c b/wayland.c index f5737c1e..7d20e3de 100644 --- a/wayland.c +++ b/wayland.c @@ -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 diff --git a/wayland.h b/wayland.h index 9cbd1023..6944bcbc 100644 --- a/wayland.h +++ b/wayland.h @@ -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 */