From 0855afdb1ea7734501914679e50be3d324744d83 Mon Sep 17 00:00:00 2001 From: Phil Jones Date: Sat, 19 Nov 2022 10:30:18 +0000 Subject: Add support for pasting from clipboard. --- src/main.c | 259 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 251 insertions(+), 8 deletions(-) (limited to 'src/main.c') diff --git a/src/main.c b/src/main.c index 142991b..3783937 100644 --- a/src/main.c +++ b/src/main.c @@ -26,6 +26,7 @@ #include "shm.h" #include "string_vec.h" #include "string_vec.h" +#include "unicode.h" #include "xmalloc.h" #undef MAX @@ -33,6 +34,9 @@ #define MAX(a, b) ((a) > (b) ? (a) : (b)) #define MIN(a, b) ((a) < (b) ? (a) : (b)) +static const char *mime_type_text_plain = "text/plain"; +static const char *mime_type_text_plain_utf8 = "text/plain;charset=utf-8"; + static uint32_t gettime_ms() { struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); @@ -371,6 +375,123 @@ static const struct wl_seat_listener wl_seat_listener = { .name = wl_seat_name, }; +static void wl_data_offer_offer( + void *data, + struct wl_data_offer *wl_data_offer, + const char *mime_type) +{ + struct clipboard *clipboard = data; + + /* Only accept plain text, and prefer utf-8. */ + if (!strcmp(mime_type, mime_type_text_plain)) { + if (clipboard->mime_type != NULL) { + clipboard->mime_type = mime_type_text_plain; + } + } else if (!strcmp(mime_type, mime_type_text_plain_utf8)) { + clipboard->mime_type = mime_type_text_plain_utf8; + } +} + +static void wl_data_offer_source_actions( + void *data, + struct wl_data_offer *wl_data_offer, + uint32_t source_actions) +{ + /* Deliberately left blank */ +} + +static void wl_data_offer_action( + void *data, + struct wl_data_offer *wl_data_offer, + uint32_t action) +{ + /* Deliberately left blank */ +} + +static const struct wl_data_offer_listener wl_data_offer_listener = { + .offer = wl_data_offer_offer, + .source_actions = wl_data_offer_source_actions, + .action = wl_data_offer_action +}; + +static void wl_data_device_data_offer( + void *data, + struct wl_data_device *wl_data_device, + struct wl_data_offer *wl_data_offer) +{ + struct clipboard *clipboard = data; + clipboard_reset(clipboard); + clipboard->wl_data_offer = wl_data_offer; + wl_data_offer_add_listener( + wl_data_offer, + &wl_data_offer_listener, + clipboard); +} + +static void wl_data_device_enter( + void *data, + struct wl_data_device *wl_data_device, + uint32_t serial, + struct wl_surface *wl_surface, + int32_t x, + int32_t y, + struct wl_data_offer *wl_data_offer) +{ + /* Drag-and-drop is just ignored for now. */ + wl_data_offer_accept( + wl_data_offer, + serial, + NULL); + wl_data_offer_set_actions( + wl_data_offer, + WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE, + WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE); +} + +static void wl_data_device_leave( + void *data, + struct wl_data_device *wl_data_device) +{ + /* Deliberately left blank */ +} + +static void wl_data_device_motion( + void *data, + struct wl_data_device *wl_data_device, + uint32_t time, + int32_t x, + int32_t y) +{ + /* Deliberately left blank */ +} + +static void wl_data_device_drop( + void *data, + struct wl_data_device *wl_data_device) +{ + /* Deliberately left blank */ +} + +static void wl_data_device_selection( + void *data, + struct wl_data_device *wl_data_device, + struct wl_data_offer *wl_data_offer) +{ + struct clipboard *clipboard = data; + if (wl_data_offer == NULL) { + clipboard_reset(clipboard); + } +} + +static const struct wl_data_device_listener wl_data_device_listener = { + .data_offer = wl_data_device_data_offer, + .enter = wl_data_device_enter, + .leave = wl_data_device_leave, + .motion = wl_data_device_motion, + .drop = wl_data_device_drop, + .selection = wl_data_device_selection +}; + static void output_geometry( void *data, struct wl_output *wl_output, @@ -518,6 +639,13 @@ static void registry_global( &wl_shm_interface, 1); log_debug("Bound to shm %u.\n", name); + } else if (!strcmp(interface, wl_data_device_manager_interface.name)) { + tofi->wl_data_device_manager = wl_registry_bind( + wl_registry, + name, + &wl_data_device_manager_interface, + 3); + log_debug("Bound to data device manager %u.\n", name); } else if (!strcmp(interface, zwlr_layer_shell_v1_interface.name)) { if (version < 4) { log_warning("Using an outdated compositor, " @@ -857,6 +985,70 @@ static bool do_submit(struct tofi *tofi) return true; } +static void read_clipboard(struct tofi *tofi) +{ + struct entry *entry = &tofi->window.entry; + /* Buffer for 4 UTF-8 bytes plus a null terminator. */ + char buffer[5]; + memset(buffer, 0, N_ELEM(buffer)); + errno = 0; + bool eof = false; + while (entry->input_utf32_length < N_ELEM(entry->input_utf32)) { + for (size_t i = 0; i < 4; i++) { + /* + * Read input 1 byte at a time. This is slow, but easy, + * and speed of pasting shouldn't really matter. + */ + int res = read(tofi->clipboard.fd, &buffer[i], 1); + if (res == 0) { + eof = true; + break; + } else if (res == -1) { + if (errno == EAGAIN) { + /* + * There was no more data to be read, + * but EOF hasn't been reached yet. + * + * This could mean more than a pipe's + * capacity (64k) of data was sent, in + * which case we'd potentially skip + * a character, but we should hit the + * input length limit long before that. + */ + input_refresh_results(tofi); + tofi->window.surface.redraw = true; + return; + } + log_error("Failed to read clipboard: %s\n", strerror(errno)); + clipboard_finish_paste(&tofi->clipboard); + return; + } + uint32_t unichar = utf8_to_utf32_validate(buffer); + if (unichar == (uint32_t)-2) { + /* The current character isn't complete yet. */ + continue; + } else if (unichar == (uint32_t)-1) { + log_error("Invalid UTF-8 character in clipboard: %s\n", buffer); + break; + } else { + entry->input_utf32[entry->input_utf32_length] = unichar; + entry->input_utf32_length++; + break; + } + } + memset(buffer, 0, N_ELEM(buffer)); + if (eof) { + break; + } + } + entry->input_utf32[MIN(entry->input_utf32_length, N_ELEM(entry->input_utf32) - 1)] = U'\0'; + + clipboard_finish_paste(&tofi->clipboard); + + input_refresh_results(tofi); + tofi->window.surface.redraw = true; +} + int main(int argc, char *argv[]) { /* Call log_debug to initialise the timers we use for perf checking. */ @@ -1200,6 +1392,19 @@ int main(int argc, char *argv[]) tofi.window.margin_left / tofi.window.scale); wl_surface_commit(tofi.window.surface.wl_surface); + /* + * Create a data device and setup a listener for data offers. This is + * required for clipboard support. + */ + tofi.wl_data_device = wl_data_device_manager_get_data_device( + tofi.wl_data_device_manager, + tofi.wl_seat); + wl_data_device_add_listener( + tofi.wl_data_device, + &wl_data_device_listener, + &tofi.clipboard); + + /* * Now that we've done all our Wayland-related setup, we do another * roundtrip. This should cause the layer surface window to be @@ -1286,8 +1491,8 @@ int main(int argc, char *argv[]) * order of the various functions called here. */ while (!tofi.closed) { - struct pollfd pollfd; - pollfd.fd = wl_display_get_fd(tofi.wl_display); + struct pollfd pollfds[2] = {{0}, {0}}; + pollfds[0].fd = wl_display_get_fd(tofi.wl_display); /* Make sure we're ready to receive events on the main queue. */ while (wl_display_prepare_read(tofi.wl_display) != 0) { @@ -1296,8 +1501,8 @@ int main(int argc, char *argv[]) /* Make sure all our requests have been sent to the server. */ while (wl_display_flush(tofi.wl_display) != 0) { - pollfd.events = POLLOUT; - poll(&pollfd, 1, -1); + pollfds[0].events = POLLOUT; + poll(&pollfds[0], 1, -1); } /* @@ -1314,8 +1519,20 @@ int main(int argc, char *argv[]) } } - pollfd.events = POLLIN | POLLPRI; - int res = poll(&pollfd, 1, timeout); + pollfds[0].events = POLLIN | POLLPRI; + int res; + if (tofi.clipboard.fd == 0) { + res = poll(&pollfds[0], 1, timeout); + } else { + /* + * We're trying to paste from the clipboard, which is + * done by reading from a pipe, so poll that file + * descriptor as well. + */ + pollfds[1].fd = tofi.clipboard.fd; + pollfds[1].events = POLLIN | POLLPRI; + res = poll(pollfds, 2, timeout); + } if (res == 0) { /* * No events to process and no error - we presumably @@ -1333,8 +1550,29 @@ int main(int argc, char *argv[]) /* There was an error polling the display. */ wl_display_cancel_read(tofi.wl_display); } else { - /* Events to read, so put them on the queue. */ - wl_display_read_events(tofi.wl_display); + if (pollfds[0].revents & (POLLIN | POLLPRI)) { + /* Events to read, so put them on the queue. */ + wl_display_read_events(tofi.wl_display); + } else { + /* + * No events to read - we were woken up to + * handle clipboard data. + */ + wl_display_cancel_read(tofi.wl_display); + } + if (pollfds[1].revents & (POLLIN | POLLPRI)) { + /* Read clipboard data. */ + if (tofi.clipboard.fd > 0) { + read_clipboard(&tofi); + } + } + if (pollfds[1].revents & POLLHUP) { + /* + * The other end of the clipboard pipe has + * closed, cleanup. + */ + clipboard_finish_paste(&tofi.clipboard); + } } /* Handle any events we read. */ @@ -1373,6 +1611,11 @@ int main(int argc, char *argv[]) wl_pointer_release(tofi.wl_pointer); } wl_compositor_destroy(tofi.wl_compositor); + if (tofi.clipboard.wl_data_offer != NULL) { + wl_data_offer_destroy(tofi.clipboard.wl_data_offer); + } + wl_data_device_release(tofi.wl_data_device); + wl_data_device_manager_destroy(tofi.wl_data_device_manager); wl_seat_release(tofi.wl_seat); { struct output_list_element *el; -- cgit v1.2.3