diff options
-rw-r--r-- | meson.build | 1 | ||||
-rw-r--r-- | src/clipboard.c | 23 | ||||
-rw-r--r-- | src/clipboard.h | 15 | ||||
-rw-r--r-- | src/input.c | 44 | ||||
-rw-r--r-- | src/input.h | 1 | ||||
-rw-r--r-- | src/main.c | 259 | ||||
-rw-r--r-- | src/tofi.h | 5 | ||||
-rw-r--r-- | src/unicode.c | 5 | ||||
-rw-r--r-- | src/unicode.h | 1 |
9 files changed, 341 insertions, 13 deletions
diff --git a/meson.build b/meson.build index 0f4c74d..3071aed 100644 --- a/meson.build +++ b/meson.build @@ -92,6 +92,7 @@ add_project_arguments( ) common_sources = files( + 'src/clipboard.c', 'src/color.c', 'src/compgen.c', 'src/config.c', diff --git a/src/clipboard.c b/src/clipboard.c new file mode 100644 index 0000000..a046055 --- /dev/null +++ b/src/clipboard.c @@ -0,0 +1,23 @@ +#include <unistd.h> +#include "clipboard.h" + +void clipboard_finish_paste(struct clipboard *clipboard) +{ + if (clipboard->fd > 0) { + close(clipboard->fd); + clipboard->fd = 0; + } +} + +void clipboard_reset(struct clipboard *clipboard) +{ + if (clipboard->wl_data_offer != NULL) { + wl_data_offer_destroy(clipboard->wl_data_offer); + clipboard->wl_data_offer = NULL; + } + if (clipboard->fd > 0) { + close(clipboard->fd); + clipboard->fd = 0; + } + clipboard->mime_type = NULL; +} diff --git a/src/clipboard.h b/src/clipboard.h new file mode 100644 index 0000000..c5f3a4e --- /dev/null +++ b/src/clipboard.h @@ -0,0 +1,15 @@ +#ifndef CLIPBOARD_H +#define CLIPBOARD_H + +#include <wayland-client.h> + +struct clipboard { + struct wl_data_offer *wl_data_offer; + const char *mime_type; + int fd; +}; + +void clipboard_finish_paste(struct clipboard *clipboard); +void clipboard_reset(struct clipboard *clipboard); + +#endif /* CLIPBOARD_H */ diff --git a/src/input.c b/src/input.c index 0e1a5f9..4d82017 100644 --- a/src/input.c +++ b/src/input.c @@ -1,5 +1,8 @@ +#include <fcntl.h> #include <linux/input-event-codes.h> +#include <unistd.h> #include "input.h" +#include "log.h" #include "nelem.h" #include "tofi.h" #include "unicode.h" @@ -9,10 +12,10 @@ static void add_character(struct tofi *tofi, xkb_keycode_t keycode); static void delete_character(struct tofi *tofi); static void delete_word(struct tofi *tofi); static void clear_input(struct tofi *tofi); +static void paste(struct tofi *tofi); static void select_previous_result(struct tofi *tofi); static void select_next_result(struct tofi *tofi); static void reset_selection(struct tofi *tofi); -static void refresh_results(struct tofi *tofi); void input_handle_keypress(struct tofi *tofi, xkb_keycode_t keycode) { @@ -50,6 +53,14 @@ void input_handle_keypress(struct tofi *tofi, xkb_keycode_t keycode) ) { clear_input(tofi); + } else if (key == KEY_V + && xkb_state_mod_name_is_active( + tofi->xkb_state, + XKB_MOD_NAME_CTRL, + XKB_STATE_MODS_EFFECTIVE) + ) + { + paste(tofi); } else if (sym == XKB_KEY_Up || sym == XKB_KEY_Left || sym == XKB_KEY_ISO_Left_Tab || (key == KEY_K && xkb_state_mod_name_is_active( @@ -130,7 +141,7 @@ void add_character(struct tofi *tofi, xkb_keycode_t keycode) reset_selection(tofi); } -void refresh_results(struct tofi *tofi) +void input_refresh_results(struct tofi *tofi) { struct entry *entry = &tofi->window.entry; @@ -164,7 +175,7 @@ void delete_character(struct tofi *tofi) entry->input_utf32_length--; entry->input_utf32[entry->input_utf32_length] = U'\0'; - refresh_results(tofi); + input_refresh_results(tofi); } void delete_word(struct tofi *tofi) @@ -184,7 +195,7 @@ void delete_word(struct tofi *tofi) } entry->input_utf32[entry->input_utf32_length] = U'\0'; - refresh_results(tofi); + input_refresh_results(tofi); } void clear_input(struct tofi *tofi) @@ -194,7 +205,30 @@ void clear_input(struct tofi *tofi) entry->input_utf32_length = 0; entry->input_utf32[0] = U'\0'; - refresh_results(tofi); + input_refresh_results(tofi); +} + +void paste(struct tofi *tofi) +{ + if (tofi->clipboard.wl_data_offer == NULL || tofi->clipboard.mime_type == NULL) { + return; + } + + /* + * Create a pipe, and give the write end to the compositor to give to + * the clipboard manager. + */ + errno = 0; + int fildes[2]; + if (pipe2(fildes, O_CLOEXEC | O_NONBLOCK) == -1) { + log_error("Failed to open pipe for clipboard: %s\n", strerror(errno)); + return; + } + wl_data_offer_receive(tofi->clipboard.wl_data_offer, tofi->clipboard.mime_type, fildes[1]); + close(fildes[1]); + + /* Keep the read end for reading in the main loop. */ + tofi->clipboard.fd = fildes[0]; } void select_previous_result(struct tofi *tofi) diff --git a/src/input.h b/src/input.h index f78c365..cb6c456 100644 --- a/src/input.h +++ b/src/input.h @@ -5,5 +5,6 @@ #include "tofi.h" void input_handle_keypress(struct tofi *tofi, xkb_keycode_t keycode); +void input_refresh_results(struct tofi *tofi); #endif /* INPUT_H */ @@ -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. */ @@ -1201,6 +1393,19 @@ int main(int argc, char *argv[]) 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 * configured, after which we're ready to start drawing to the screen. @@ -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; @@ -3,7 +3,9 @@ #include <stdbool.h> #include <stdint.h> +#include <wayland-client.h> #include <xkbcommon/xkbcommon.h> +#include "clipboard.h" #include "color.h" #include "entry.h" #include "image.h" @@ -30,6 +32,8 @@ struct tofi { struct wl_compositor *wl_compositor; struct wl_seat *wl_seat; struct wl_shm *wl_shm; + struct wl_data_device_manager *wl_data_device_manager; + struct wl_data_device *wl_data_device; struct zwlr_layer_shell_v1 *zwlr_layer_shell; struct wl_list output_list; struct output_list_element *default_output; @@ -49,6 +53,7 @@ struct tofi { bool closed; int32_t output_width; int32_t output_height; + struct clipboard clipboard; struct { struct surface surface; struct zwlr_layer_surface_v1 *zwlr_layer_surface; diff --git a/src/unicode.c b/src/unicode.c index ad8a777..3833fb6 100644 --- a/src/unicode.c +++ b/src/unicode.c @@ -12,6 +12,11 @@ uint32_t utf8_to_utf32(const char *s) return g_utf8_get_char(s); } +uint32_t utf8_to_utf32_validate(const char *s) +{ + return g_utf8_get_char_validated(s, -1); +} + uint32_t utf32_isprint(uint32_t c) { return g_unichar_isprint(c); diff --git a/src/unicode.h b/src/unicode.h index 45631d9..e198231 100644 --- a/src/unicode.h +++ b/src/unicode.h @@ -6,6 +6,7 @@ uint8_t utf32_to_utf8(uint32_t c, char *buf); uint32_t utf8_to_utf32(const char *s); +uint32_t utf8_to_utf32_validate(const char *s); uint32_t utf32_isprint(uint32_t c); uint32_t utf32_isspace(uint32_t c); |