diff --git a/README.md b/README.md index 925cd02..66231f9 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ For `wl-paste`: * `-n`, `--no-newline` Do not append a newline character after the pasted clipboard content. This option is automatically enabled for non-text content types. * `-l`, `--list-types` Instead of pasting the selection, output the list of MIME types it is offered in. +* `-w command...`, `--watch command...` Instead of pasting once and exiting, continuously watch the clipboard for changes, and run the specified command each time a new selection appears. The spawned process can read the clipboard contents from its standard input. This mode requires a compositor that supports the [wlroots data-control protocol](https://github.com/swaywm/wlr-protocols/blob/master/unstable/wlr-data-control-unstable-v1.xml). For both: diff --git a/completions/bash/wl-paste b/completions/bash/wl-paste index 85eb7bf..91003fe 100644 --- a/completions/bash/wl-paste +++ b/completions/bash/wl-paste @@ -49,10 +49,19 @@ _wl_clipboard_complete_paste_types() { _wl_paste_completion() { compopt +o default - local cur prev opts types seats + local cur prev opts types seats offset + + for (( offset=1; offset < COMP_CWORD; offset++ )); do + cur="${COMP_WORDS[offset]}" + if [ \( "x${cur:0:1}" = "x-" -a "x${cur:1:2}" != "x-" -a "${cur: -1}" = "w" \) -o "$cur" = "--watch" ]; then + _command_offset $(($offset+1)) + return + fi + done + cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" - opts="-n --no-newline -l --list-types -p --primary -t --type -s --seat -v --version -h --help" + opts="-n --no-newline -l --list-types -w --watch -p --primary -t --type -s --seat -v --version -h --help" if [ "$prev" = ">" ]; then compopt -o default COMPREPLY=() diff --git a/data/wl-clipboard.1 b/data/wl-clipboard.1 index c3fc327..035697d 100644 --- a/data/wl-clipboard.1 +++ b/data/wl-clipboard.1 @@ -1,4 +1,4 @@ -.TH WL-CLIPBOARD 1 2019-04-15 wl-clipboard +.TH WL-CLIPBOARD 1 2019-09-15 wl-clipboard .SH NAME wl-clipboard \- Wayland copy and paste command line utilities .SH SYNOPSIS @@ -16,6 +16,7 @@ wl-clipboard \- Wayland copy and paste command line utilities [\fB--primary\fR] [\fB--no-newline\fR] [\fB--list-types\fR] +[\fB--watch \fIcommand...\fR] [\fB--type \fImime/type\fR] [\fB--seat \fIseat-name\fR] .SH DESCRIPTION @@ -85,6 +86,12 @@ system, there is little reason to use this option. Instead of pasting the selection, output the list of MIME types it is offered in. .TP +\fB-w\fI command...\fR, \fB--watch \fIcommand... +Instead of pasting once and exiting, continuously watch the clipboard for +changes, and run the specified \fIcommand\fR each time a new selection appears. +The spawned process can read the clipboard contents from its standard input. +This mode requires a compositor that supports the wlroots data-control protocol. +.TP \fB-v\fR, \fB--version Display the version of wl-clipboard and some short info about its license. .TP diff --git a/src/boilerplate.c b/src/boilerplate.c deleted file mode 100644 index 0742bf8..0000000 --- a/src/boilerplate.c +++ /dev/null @@ -1,603 +0,0 @@ -/* wl-clipboard - * - * Copyright © 2019 Sergey Bugaev - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "boilerplate.h" - -void registry_global_handler -( - void *data, - struct wl_registry *registry, - uint32_t name, - const char *interface, - uint32_t version -) { - if (strcmp(interface, "wl_data_device_manager") == 0) { - data_device_manager = wl_registry_bind( - registry, - name, - &wl_data_device_manager_interface, - 1 - ); - } else if (strcmp(interface, "wl_seat") == 0) { - struct wl_seat *new_seat = wl_registry_bind( - registry, - name, - &wl_seat_interface, - 2 - ); - process_new_seat(new_seat); - } else if (strcmp(interface, "wl_compositor") == 0) { - compositor = wl_registry_bind( - registry, - name, - &wl_compositor_interface, - 3 - ); - } else if (strcmp(interface, "wl_shm") == 0) { - shm = wl_registry_bind( - registry, - name, - &wl_shm_interface, - 1 - ); - } else if (strcmp(interface, "wl_shell") == 0) { - shell = wl_registry_bind( - registry, - name, - &wl_shell_interface, - 1 - ); - } -#ifdef HAVE_XDG_SHELL - else if (strcmp(interface, "xdg_wm_base") == 0) { - xdg_wm_base = wl_registry_bind( - registry, - name, - &xdg_wm_base_interface, - 1 - ); - } -#endif -#ifdef HAVE_WLR_LAYER_SHELL - else if (strcmp(interface, "zwlr_layer_shell_v1") == 0) { - layer_shell = wl_registry_bind( - registry, - name, - &zwlr_layer_shell_v1_interface, - 1 - ); - } -#endif -#ifdef HAVE_GTK_PRIMARY_SELECTION - else if (strcmp(interface, "gtk_primary_selection_device_manager") == 0) { - gtk_primary_selection_device_manager = wl_registry_bind( - registry, - name, - >k_primary_selection_device_manager_interface, - 1 - ); - } -#endif -#ifdef HAVE_WP_PRIMARY_SELECTION - else if (strcmp(interface, "zwp_primary_selection_device_manager_v1") == 0) { - primary_selection_device_manager = wl_registry_bind( - registry, - name, - &zwp_primary_selection_device_manager_v1_interface, - 1 - ); - } -#endif -#ifdef HAVE_WLR_DATA_CONTROL - else if (strcmp(interface, "zwlr_data_control_manager_v1") == 0) { - data_control_manager = wl_registry_bind( - registry, - name, - &zwlr_data_control_manager_v1_interface, - 1 - ); - } -#endif -} - -void registry_global_remove_handler -( - void *data, - struct wl_registry *registry, - uint32_t name -) {} - -const struct wl_registry_listener registry_listener = { - .global = registry_global_handler, - .global_remove = registry_global_remove_handler -}; - -void keyboard_keymap_handler -( - void *data, - struct wl_keyboard *keyboard, - uint32_t format, - int fd, - uint32_t size -) { - close(fd); -} - -void keyboard_enter_handler -( - void *data, - struct wl_keyboard *keyboard, - uint32_t serial, - struct wl_surface *surface, - struct wl_array *keys -) { - struct wl_seat *this_seat = (struct wl_seat *) data; - // when we get to here, global seat is already initialized - if (this_seat != seat) { - return; - } - if (action_on_popup_surface_getting_focus != NULL) { - action_on_popup_surface_getting_focus(serial); - } -} - -void keyboard_leave_handler -( - void *data, - struct wl_keyboard *keyboard, - uint32_t serial, - struct wl_surface *surface -) {} - -void keyboard_key_handler -( - void *data, - struct wl_keyboard *keyboard, - uint32_t serial, - uint32_t time, - uint32_t key, - uint32_t state -) {} - -void keyboard_modifiers_handler -( - void *data, - struct wl_keyboard *keyboard, - uint32_t serial, - uint32_t mods_depressed, - uint32_t mods_latched, - uint32_t mods_locked, - uint32_t group -) {} - -const struct wl_keyboard_listener keayboard_listener = { - .keymap = keyboard_keymap_handler, - .enter = keyboard_enter_handler, - .leave = keyboard_leave_handler, - .key = keyboard_key_handler, - .modifiers = keyboard_modifiers_handler, -}; - -void seat_capabilities_handler -( - void *data, - struct wl_seat *this_seat, - uint32_t capabilities -) { - // stash the capabilities of this seat for later - void *user_data = (void *) (unsigned long) capabilities; - wl_seat_set_user_data(this_seat, user_data); - - if (capabilities & WL_SEAT_CAPABILITY_KEYBOARD) { - struct wl_keyboard *keyboard = wl_seat_get_keyboard(this_seat); - wl_keyboard_add_listener(keyboard, &keayboard_listener, this_seat); - } -} - -void seat_name_handler -( - void *data, - struct wl_seat *this_seat, - const char *name -) { - if (requested_seat_name == NULL) { - return; - } - if (strcmp(name, requested_seat_name) == 0) { - seat = this_seat; - } -} - -const struct wl_seat_listener seat_listener = { - .capabilities = seat_capabilities_handler, - .name = seat_name_handler -}; - -#define UNSET_CAPABILITIES ((void *) (uint32_t) 35) - -void process_new_seat(struct wl_seat *new_seat) { - if (seat != NULL) { - wl_seat_destroy(new_seat); - return; - } - if (requested_seat_name == NULL) { - seat = new_seat; - } - wl_seat_add_listener(new_seat, &seat_listener, UNSET_CAPABILITIES); -} - -int ensure_seat_has_keyboard() { - void *user_data = wl_seat_get_user_data(seat); - while (user_data == UNSET_CAPABILITIES) { - wl_display_roundtrip(display); - user_data = wl_seat_get_user_data(seat); - } - - uint32_t capabilities = (uint32_t) (unsigned long) user_data; - if (capabilities & WL_SEAT_CAPABILITY_KEYBOARD) { - return 1; - } - - if (action_on_no_keyboard != NULL) { - action_on_no_keyboard(); - } - return 0; -} - -#undef UNSET_CAPABILITIES - -void shell_surface_ping -( - void *data, - struct wl_shell_surface *shell_surface, - uint32_t serial -) { - wl_shell_surface_pong(shell_surface, serial); -} - -void shell_surface_configure -( - void *data, - struct wl_shell_surface *shell_surface, - uint32_t edges, - int32_t width, - int32_t height -) {} - -void shell_surface_popup_done -( - void *data, - struct wl_shell_surface *shell_surface -) {} - -const struct wl_shell_surface_listener shell_surface_listener = { - .ping = shell_surface_ping, - .configure = shell_surface_configure, - .popup_done = shell_surface_popup_done -}; - -#ifdef HAVE_XDG_SHELL - -void xdg_toplevel_configure_handler -( - void *data, - struct xdg_toplevel *xdg_toplevel, - int32_t width, - int32_t height, - struct wl_array *states -) {} - -void xdg_toplevel_close_handler -( - void *data, - struct xdg_toplevel *xdg_toplevel -) {} - -const struct xdg_toplevel_listener xdg_toplevel_listener = { - .configure = xdg_toplevel_configure_handler, - .close = xdg_toplevel_close_handler -}; - -void xdg_surface_configure_handler -( - void *data, - struct xdg_surface *xdg_surface, - uint32_t serial -) { - xdg_surface_ack_configure(xdg_surface, serial); -} - -const struct xdg_surface_listener xdg_surface_listener = { - .configure = xdg_surface_configure_handler -}; - -void xdg_wm_base_ping_handler -( - void *data, - struct xdg_wm_base *xdg_wm_base, - uint32_t serial -) { - xdg_wm_base_pong(xdg_wm_base, serial); -} - -const struct xdg_wm_base_listener xdg_wm_base_listener = { - .ping = xdg_wm_base_ping_handler -}; - -#endif - -#ifdef HAVE_WLR_LAYER_SHELL - -void layer_surface_configure_handler -( - void *data, - struct zwlr_layer_surface_v1 *layer_surface, - uint32_t serial, - uint32_t width, - uint32_t height -) { - zwlr_layer_surface_v1_ack_configure(layer_surface, serial); -} - -void layer_surface_closed_handler -( - void *data, - struct zwlr_layer_surface_v1 *layer_surface -) {} - -const struct zwlr_layer_surface_v1_listener layer_surface_listener = { - .configure = layer_surface_configure_handler, - .closed = layer_surface_closed_handler -}; - -#endif - -void init_wayland_globals() { - display = wl_display_connect(NULL); - if (display == NULL) { - bail("Failed to connect to a Wayland server"); - } - - struct wl_registry *registry = wl_display_get_registry(display); - wl_registry_add_listener(registry, ®istry_listener, NULL); - - // wait for the "initial" set of globals to appear - wl_display_roundtrip(display); - - if ( - data_device_manager == NULL || - compositor == NULL || - shm == NULL || - (shell == NULL -#ifdef HAVE_XDG_SHELL - && xdg_wm_base == NULL -#endif -#ifdef HAVE_WLR_LAYER_SHELL - && layer_shell == NULL -#endif - ) - ) { - bail("Missing a required global object"); - } - - if (seat == NULL && requested_seat_name != NULL) { - wl_display_roundtrip(display); - } - if (seat == NULL) { - if (requested_seat_name == NULL) { - bail("No seat available"); - } - bail("Cannot find the requested seat"); - } - - data_device = wl_data_device_manager_get_data_device( - data_device_manager, - seat - ); -#ifdef HAVE_GTK_PRIMARY_SELECTION - if (gtk_primary_selection_device_manager != NULL) { - gtk_primary_selection_device = - gtk_primary_selection_device_manager_get_device( - gtk_primary_selection_device_manager, - seat - ); - } -#endif -#ifdef HAVE_WP_PRIMARY_SELECTION - if (primary_selection_device_manager != NULL) { - primary_selection_device = - zwp_primary_selection_device_manager_v1_get_device( - primary_selection_device_manager, - seat - ); - } -#endif -#ifdef HAVE_WLR_DATA_CONTROL - if (data_control_manager != NULL) { - data_control_device = - zwlr_data_control_manager_v1_get_data_device( - data_control_manager, - seat - ); - use_wlr_data_control = 1; - } -#endif -} - -void ensure_has_primary_selection() { -#ifdef HAVE_GTK_PRIMARY_SELECTION - if (gtk_primary_selection_device_manager != NULL) { - return; - } -#endif -#ifdef HAVE_WP_PRIMARY_SELECTION - if (primary_selection_device_manager != NULL) { - return; - } -#endif - -#if defined(HAVE_GTK_PRIMARY_SELECTION) || defined(HAVE_WP_PRIMARY_SELECTION) - bail("Primary selection is not supported on this compositor"); -#else - bail("wl-clipboard was built without primary selection support"); -#endif -} - -void popup_tiny_invisible_surface() { - // HACK: - // pop up a tiny invisible surface to get the keyboard focus, - // otherwise we won't be notified of the selection - - if (!ensure_seat_has_keyboard()) { - return; - } - - // make sure that we get the keyboard - // object before creating the surface, - // so that we get the enter event - wl_display_dispatch(display); - - surface = wl_compositor_create_surface(compositor); - -#ifdef HAVE_WLR_LAYER_SHELL - if (layer_shell != NULL) { - // use wlr-layer-shell - layer_surface = zwlr_layer_shell_v1_get_layer_surface( - layer_shell, - surface, - NULL, // output - ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY, - "wl-clipboard" // namespace - ); - zwlr_layer_surface_v1_add_listener( - layer_surface, - &layer_surface_listener, - NULL - ); - zwlr_layer_surface_v1_set_keyboard_interactivity(layer_surface, 1); - // signal that the surface is ready to be configured - wl_surface_commit(surface); - // wait for the surface to be configured - wl_display_roundtrip(display); - } else -#endif - if (shell != NULL) { - // use wl_shell - shell_surface = wl_shell_get_shell_surface(shell, surface); - wl_shell_surface_set_toplevel(shell_surface); - wl_shell_surface_set_title(shell_surface, "wl-clipboard"); - } else { -#ifdef HAVE_XDG_SHELL - // use xdg-shell - xdg_wm_base_add_listener(xdg_wm_base, &xdg_wm_base_listener, NULL); - xdg_surface = xdg_wm_base_get_xdg_surface(xdg_wm_base, surface); - xdg_surface_add_listener(xdg_surface, &xdg_surface_listener, NULL); - xdg_toplevel = xdg_surface_get_toplevel(xdg_surface); - xdg_toplevel_add_listener(xdg_toplevel, &xdg_toplevel_listener, NULL); - xdg_toplevel_set_title(xdg_toplevel, "wl-clipboard"); - // signal that the surface is ready to be configured - wl_surface_commit(surface); - // wait for the surface to be configured - wl_display_roundtrip(display); -#else - bail("Unreachable: HAVE_XDG_SHELL undefined and no wl_shell"); -#endif - } - - if (surface == NULL) { - // it's possible that we've been given focus without us - // ever commiting a buffer, in which case the handlers - // may have already destroyed the surface; there's no - // way or need for us to commit a buffer in that case - return; - } - - int width = 1; - int height = 1; - int stride = width * 4; - int size = stride * height; // bytes - - // open an anonymous file and write some zero bytes to it - int fd = create_anonymous_file(); - ftruncate(fd, size); - - // turn it into a shared memory pool - struct wl_shm_pool *pool = wl_shm_create_pool(shm, fd, size); - - // allocate the buffer in that pool - struct wl_buffer *buffer = wl_shm_pool_create_buffer(pool, - 0, width, height, stride, WL_SHM_FORMAT_ARGB8888); - // zeros in ARGB8888 mean fully transparent - - wl_surface_attach(surface, buffer, 0, 0); - wl_surface_damage(surface, 0, 0, width, height); - wl_surface_commit(surface); -} - -void destroy_popup_surface() { -#ifdef HAVE_WLR_LAYER_SHELL - if (layer_surface != NULL) { - zwlr_layer_surface_v1_destroy(layer_surface); - layer_surface = NULL; - } -#endif - if (shell_surface != NULL) { - wl_shell_surface_destroy(shell_surface); - shell_surface = NULL; - } -#ifdef HAVE_XDG_SHELL - if (xdg_toplevel != NULL) { - xdg_toplevel_destroy(xdg_toplevel); - xdg_toplevel = NULL; - } - if (xdg_surface != NULL) { - xdg_surface_destroy(xdg_surface); - xdg_surface = NULL; - } -#endif - if (surface != NULL) { - wl_surface_destroy(surface); - surface = NULL; - } -} - -static uint32_t global_serial; - -void callback_done -( - void *data, - struct wl_callback *callback, - uint32_t serial -) { - global_serial = serial; -} - -const struct wl_callback_listener callback_listener = { - .done = callback_done -}; - -uint32_t get_serial() { - struct wl_callback *callback = wl_display_sync(display); - wl_callback_add_listener(callback, &callback_listener, NULL); - - while (global_serial == 0) { - wl_display_dispatch(display); - } - - return global_serial; -} diff --git a/src/boilerplate.h b/src/boilerplate.h deleted file mode 100644 index f6f6712..0000000 --- a/src/boilerplate.h +++ /dev/null @@ -1,112 +0,0 @@ -/* wl-clipboard - * - * Copyright © 2019 Sergey Bugaev - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "config.h" - -#include "util/string.h" -#include "util/files.h" -#include "util/misc.h" - -#include -#include -#include -#include -#include -#include -#include // isupper -#include // open -#include // open -#include // open -#include // exit -#include // basename -#include -#include // PATH_MAX - - -#ifdef HAVE_XDG_SHELL -# include "xdg-shell.h" -#endif - -#ifdef HAVE_WP_PRIMARY_SELECTION -# include "wp-primary-selection.h" -#endif - -#ifdef HAVE_WLR_LAYER_SHELL -# include "wlr-layer-shell.h" -#endif - -#ifdef HAVE_GTK_PRIMARY_SELECTION -# include "gtk-primary-selection.h" -#endif - -#ifdef HAVE_WLR_DATA_CONTROL -# include "wlr-data-control.h" -#endif - -struct wl_display *display; -struct wl_data_device_manager *data_device_manager; -struct wl_seat *seat; -struct wl_compositor *compositor; -struct wl_shm *shm; -struct wl_shell *shell; -struct wl_surface *surface; -struct wl_shell_surface *shell_surface; - -#ifdef HAVE_XDG_SHELL -struct xdg_wm_base *xdg_wm_base; -struct xdg_surface *xdg_surface; -struct xdg_toplevel *xdg_toplevel; -#endif - -#ifdef HAVE_WLR_LAYER_SHELL -struct zwlr_layer_shell_v1 *layer_shell; -struct zwlr_layer_surface_v1 *layer_surface; -#endif - -struct wl_data_device *data_device; - -#ifdef HAVE_GTK_PRIMARY_SELECTION -struct gtk_primary_selection_device_manager *gtk_primary_selection_device_manager; -struct gtk_primary_selection_device *gtk_primary_selection_device; -#endif - -#ifdef HAVE_WP_PRIMARY_SELECTION -struct zwp_primary_selection_device_manager_v1 *primary_selection_device_manager; -struct zwp_primary_selection_device_v1 *primary_selection_device; -#endif - -#ifdef HAVE_WLR_DATA_CONTROL -struct zwlr_data_control_manager_v1 *data_control_manager; -struct zwlr_data_control_device_v1 *data_control_device; -#endif - -void process_new_seat(struct wl_seat *new_seat); -const char *requested_seat_name; - -void init_wayland_globals(void); -int use_wlr_data_control; - -void popup_tiny_invisible_surface(void); -void destroy_popup_surface(void); - -void (*action_on_popup_surface_getting_focus)(uint32_t serial); -void (*action_on_no_keyboard)(void); - -void ensure_has_primary_selection(void); - -uint32_t get_serial(void); diff --git a/src/includes/selection-protocols.h b/src/includes/selection-protocols.h new file mode 100644 index 0000000..32e74a7 --- /dev/null +++ b/src/includes/selection-protocols.h @@ -0,0 +1,38 @@ +/* wl-clipboard + * + * Copyright © 2019 Sergey Bugaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef INCLUDES_SELECTION_PROTOCOLS_H +#define INCLUDES_SELECTION_PROTOCOLS_H + +#include "config.h" + +#include + +#ifdef HAVE_WP_PRIMARY_SELECTION +# include "wp-primary-selection.h" +#endif + +#ifdef HAVE_GTK_PRIMARY_SELECTION +# include "gtk-primary-selection.h" +#endif + +#ifdef HAVE_WLR_DATA_CONTROL +# include "wlr-data-control.h" +#endif + +#endif /* INCLUDES_SELECTION_PROTOCOLS_H */ diff --git a/src/includes/shell-protocols.h b/src/includes/shell-protocols.h new file mode 100644 index 0000000..feb39f5 --- /dev/null +++ b/src/includes/shell-protocols.h @@ -0,0 +1,30 @@ +/* wl-clipboard + * + * Copyright © 2019 Sergey Bugaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef INCLUDES_SHELL_PROTOCOLS_H +#define INCLUDES_SHELL_PROTOCOLS_H + +#include "config.h" + +#include + +#ifdef HAVE_XDG_SHELL +# include "xdg-shell.h" +#endif + +#endif /* INCLUDES_SHELL_PROTOCOLS_H */ diff --git a/src/meson.build b/src/meson.build index 3fbc3e7..9837e9a 100644 --- a/src/meson.build +++ b/src/meson.build @@ -7,7 +7,6 @@ if wayland_scanner.found() have_xdg_shell = wayland_protocols.found() have_wp_primary_selection = wayland_protocols.found() and wayland_protocols.version().version_compare('>= 1.17') # these are bundled - have_wlr_layer_shell = have_xdg_shell # depends on xdg_popup have_gtk_primary_selection = true have_wlr_data_control = true @@ -18,7 +17,6 @@ if wayland_scanner.found() endif else have_xdg_shell = false - have_wlr_layer_shell = false have_gtk_primary_selection = false have_wlr_data_control = false endif @@ -32,7 +30,6 @@ conf_data = configuration_data() conf_data.set('PROJECT_VERSION', '"@0@"'.format(meson.project_version())) conf_data.set('HAVE_XDG_SHELL', have_xdg_shell) conf_data.set('HAVE_WP_PRIMARY_SELECTION', have_wp_primary_selection) -conf_data.set('HAVE_WLR_LAYER_SHELL', have_wlr_layer_shell) conf_data.set('HAVE_GTK_PRIMARY_SELECTION', have_gtk_primary_selection) conf_data.set('HAVE_WLR_DATA_CONTROL', have_wlr_data_control) @@ -59,11 +56,6 @@ if have_wp_primary_selection protocols += [['wp-primary-selection', wp_primary_selection_xml]] endif -if have_wlr_layer_shell - wlr_layer_shell_xml = join_paths('protocol', 'wlr-layer-shell-unstable-v1.xml') - protocols += [['wlr-layer-shell', wlr_layer_shell_xml]] -endif - if not have_gtk_primary_selection warning('Building without primary selection support') else @@ -93,21 +85,45 @@ foreach protocol : protocols protocol_deps += lib endforeach -boilerplate = static_library( - 'wl-clipboard-boilerplate', +lib = static_library( + 'wl-clipboard', [ - 'boilerplate.c', - 'util/string.h', 'util/string.c', 'util/files.h', 'util/files.c', 'util/misc.h', 'util/misc.c', + + 'includes/shell-protocols.h', + 'includes/selection-protocols.h', + + 'types/source.h', + 'types/source.c', + 'types/offer.h', + 'types/offer.c', + 'types/device.h', + 'types/device.c', + 'types/device-manager.h', + 'types/device-manager.c', + 'types/keyboard.h', + 'types/keyboard.c', + 'types/seat.h', + 'types/seat.c', + 'types/shell.h', + 'types/shell.c', + 'types/shell-surface.h', + 'types/shell-surface.c', + 'types/popup-surface.h', + 'types/popup-surface.c', + 'types/registry.h', + 'types/registry.c', + 'types/copy-action.h', + 'types/copy-action.c', ], dependencies: wayland, link_with: protocol_deps ) -executable('wl-copy', 'wl-copy.c', dependencies: wayland, link_with: boilerplate, install: true) -executable('wl-paste', 'wl-paste.c', dependencies: wayland, link_with: boilerplate, install: true) +executable('wl-copy', 'wl-copy.c', dependencies: wayland, link_with: lib, install: true) +executable('wl-paste', 'wl-paste.c', dependencies: wayland, link_with: lib, install: true) diff --git a/src/protocol/wlr-data-control-unstable-v1.xml b/src/protocol/wlr-data-control-unstable-v1.xml index a588755..75e8671 100644 --- a/src/protocol/wlr-data-control-unstable-v1.xml +++ b/src/protocol/wlr-data-control-unstable-v1.xml @@ -2,6 +2,7 @@ Copyright © 2018 Simon Ser + Copyright © 2019 Ivan Molodetskikh Permission to use, copy, modify, distribute, and sell this software and its documentation for any purpose is hereby granted @@ -40,7 +41,7 @@ interface version number is reset. - + This interface is a manager that allows creating per-seat data device controls. @@ -70,7 +71,7 @@ - + This interface allows a client to manage a seat's selection. @@ -79,8 +80,14 @@ - All objects created by the device will still remain valid, until their - appropriate destroy request has been called. + This request asks the compositor to set the selection to the data from + the source on behalf of the client. + + The given source may not be used in any further set_selection or + set_primary_selection requests. Attempting to use a previously used + source is a protocol error. + + To unset the selection, set the source to NULL. @@ -95,20 +102,19 @@ The data_offer event introduces a new wlr_data_control_offer object, - which will subsequently be used in the wlr_data_control_device.selection - event. Immediately following the wlr_data_control_device.data_offer - event, the new data_offer object will send out - wlr_data_control_offer.offer events to describe the MIME types it - offers. - - This event replaces the previous data offer, which should be destroyed - by the client. + which will subsequently be used in either the + wlr_data_control_device.selection event (for the regular clipboard + selections) or the wlr_data_control_device.primary_selection event (for + the primary clipboard selections). Immediately following the + wlr_data_control_device.data_offer event, the new data_offer object + will send out wlr_data_control_offer.offer events to describe the MIME + types it offers. - + The selection event is sent out to notify the client of a new wlr_data_control_offer for the selection for this device. The wlr_data_control_device.data_offer and the wlr_data_control_offer.offer @@ -118,6 +124,9 @@ wlr_data_control_offer or NULL is received. The client must destroy the previous selection wlr_data_control_offer, if any, upon receiving this event. + + The first selection event is sent upon binding the + wlr_data_control_device object. @@ -129,6 +138,51 @@ the client. + + + + + + The primary_selection event is sent out to notify the client of a new + wlr_data_control_offer for the primary selection for this device. The + wlr_data_control_device.data_offer and the wlr_data_control_offer.offer + events are sent out immediately before this event to introduce the data + offer object. The primary_selection event is sent to a client when a + new primary selection is set. The wlr_data_control_offer is valid until + a new wlr_data_control_offer or NULL is received. The client must + destroy the previous primary selection wlr_data_control_offer, if any, + upon receiving this event. + + If the compositor supports primary selection, the first + primary_selection event is sent upon binding the + wlr_data_control_device object. + + + + + + + This request asks the compositor to set the primary selection to the + data from the source on behalf of the client. + + The given source may not be used in any further set_selection or + set_primary_selection requests. Attempting to use a previously used + source is a protocol error. + + To unset the primary selection, set the source to NULL. + + The compositor will ignore this request if it does not support primary + selection. + + + + + + + diff --git a/src/protocol/wlr-layer-shell-unstable-v1.xml b/src/protocol/wlr-layer-shell-unstable-v1.xml deleted file mode 100644 index 216e0d9..0000000 --- a/src/protocol/wlr-layer-shell-unstable-v1.xml +++ /dev/null @@ -1,285 +0,0 @@ - - - - Copyright © 2017 Drew DeVault - - Permission to use, copy, modify, distribute, and sell this - software and its documentation for any purpose is hereby granted - without fee, provided that the above copyright notice appear in - all copies and that both that copyright notice and this permission - notice appear in supporting documentation, and that the name of - the copyright holders not be used in advertising or publicity - pertaining to distribution of the software without specific, - written prior permission. The copyright holders make no - representations about the suitability of this software for any - purpose. It is provided "as is" without express or implied - warranty. - - THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS - SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND - FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY - SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN - AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, - ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF - THIS SOFTWARE. - - - - - Clients can use this interface to assign the surface_layer role to - wl_surfaces. Such surfaces are assigned to a "layer" of the output and - rendered with a defined z-depth respective to each other. They may also be - anchored to the edges and corners of a screen and specify input handling - semantics. This interface should be suitable for the implementation of - many desktop shell components, and a broad number of other applications - that interact with the desktop. - - - - - Create a layer surface for an existing surface. This assigns the role of - layer_surface, or raises a protocol error if another role is already - assigned. - - Creating a layer surface from a wl_surface which has a buffer attached - or committed is a client error, and any attempts by a client to attach - or manipulate a buffer prior to the first layer_surface.configure call - must also be treated as errors. - - You may pass NULL for output to allow the compositor to decide which - output to use. Generally this will be the one that the user most - recently interacted with. - - Clients can specify a namespace that defines the purpose of the layer - surface. - - - - - - - - - - - - - - - - - These values indicate which layers a surface can be rendered in. They - are ordered by z depth, bottom-most first. Traditional shell surfaces - will typically be rendered between the bottom and top layers. - Fullscreen shell surfaces are typically rendered at the top layer. - Multiple surfaces can share a single layer, and ordering within a - single layer is undefined. - - - - - - - - - - - - An interface that may be implemented by a wl_surface, for surfaces that - are designed to be rendered as a layer of a stacked desktop-like - environment. - - Layer surface state (size, anchor, exclusive zone, margin, interactivity) - is double-buffered, and will be applied at the time wl_surface.commit of - the corresponding wl_surface is called. - - - - - Sets the size of the surface in surface-local coordinates. The - compositor will display the surface centered with respect to its - anchors. - - If you pass 0 for either value, the compositor will assign it and - inform you of the assignment in the configure event. You must set your - anchor to opposite edges in the dimensions you omit; not doing so is a - protocol error. Both values are 0 by default. - - Size is double-buffered, see wl_surface.commit. - - - - - - - - Requests that the compositor anchor the surface to the specified edges - and corners. If two orthoginal edges are specified (e.g. 'top' and - 'left'), then the anchor point will be the intersection of the edges - (e.g. the top left corner of the output); otherwise the anchor point - will be centered on that edge, or in the center if none is specified. - - Anchor is double-buffered, see wl_surface.commit. - - - - - - - Requests that the compositor avoids occluding an area of the surface - with other surfaces. The compositor's use of this information is - implementation-dependent - do not assume that this region will not - actually be occluded. - - A positive value is only meaningful if the surface is anchored to an - edge, rather than a corner. The zone is the number of surface-local - coordinates from the edge that are considered exclusive. - - Surfaces that do not wish to have an exclusive zone may instead specify - how they should interact with surfaces that do. If set to zero, the - surface indicates that it would like to be moved to avoid occluding - surfaces with a positive excluzive zone. If set to -1, the surface - indicates that it would not like to be moved to accommodate for other - surfaces, and the compositor should extend it all the way to the edges - it is anchored to. - - For example, a panel might set its exclusive zone to 10, so that - maximized shell surfaces are not shown on top of it. A notification - might set its exclusive zone to 0, so that it is moved to avoid - occluding the panel, but shell surfaces are shown underneath it. A - wallpaper or lock screen might set their exclusive zone to -1, so that - they stretch below or over the panel. - - The default value is 0. - - Exclusive zone is double-buffered, see wl_surface.commit. - - - - - - - Requests that the surface be placed some distance away from the anchor - point on the output, in surface-local coordinates. Setting this value - for edges you are not anchored to has no effect. - - The exclusive zone includes the margin. - - Margin is double-buffered, see wl_surface.commit. - - - - - - - - - - Set to 1 to request that the seat send keyboard events to this layer - surface. For layers below the shell surface layer, the seat will use - normal focus semantics. For layers above the shell surface layers, the - seat will always give exclusive keyboard focus to the top-most layer - which has keyboard interactivity set to true. - - Layer surfaces receive pointer, touch, and tablet events normally. If - you do not want to receive them, set the input region on your surface - to an empty region. - - Events is double-buffered, see wl_surface.commit. - - - - - - - This assigns an xdg_popup's parent to this layer_surface. This popup - should have been created via xdg_surface::get_popup with the parent set - to NULL, and this request must be invoked before committing the popup's - initial state. - - See the documentation of xdg_popup for more details about what an - xdg_popup is and how it is used. - - - - - - - When a configure event is received, if a client commits the - surface in response to the configure event, then the client - must make an ack_configure request sometime before the commit - request, passing along the serial of the configure event. - - If the client receives multiple configure events before it - can respond to one, it only has to ack the last configure event. - - A client is not required to commit immediately after sending - an ack_configure request - it may even ack_configure several times - before its next surface commit. - - A client may send multiple ack_configure requests before committing, but - only the last request sent before a commit indicates which configure - event the client really is responding to. - - - - - - - This request destroys the layer surface. - - - - - - The configure event asks the client to resize its surface. - - Clients should arrange their surface for the new states, and then send - an ack_configure request with the serial sent in this configure event at - some point before committing the new surface. - - The client is free to dismiss all but the last configure event it - received. - - The width and height arguments specify the size of the window in - surface-local coordinates. - - The size is a hint, in the sense that the client is free to ignore it if - it doesn't resize, pick a smaller size (to satisfy aspect ratio or - resize in steps of NxM pixels). If the client picks a smaller size and - is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the - surface will be centered on this axis. - - If the width or height arguments are zero, it means the client should - decide its own window dimension. - - - - - - - - - The closed event is sent by the compositor when the surface will no - longer be shown. The output may have been destroyed or the user may - have asked for it to be removed. Further changes to the surface will be - ignored. The client should destroy the resource after receiving this - event, and create a new surface if they so choose. - - - - - - - - - - - - - - - - - diff --git a/src/types/copy-action.c b/src/types/copy-action.c new file mode 100644 index 0000000..7ddce62 --- /dev/null +++ b/src/types/copy-action.c @@ -0,0 +1,204 @@ +/* wl-clipboard + * + * Copyright © 2019 Sergey Bugaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "types/copy-action.h" +#include "types/device.h" +#include "types/source.h" +#include "types/popup-surface.h" + +#include "util/misc.h" + +#include +#include +#include +#include +#include +#include +#include + +static struct popup_surface *current_popup_surface = NULL; +static struct wl_array pending_selections; + +static void on_focus( + struct popup_surface *popup_surface, + uint32_t serial +) { + /* Set the pending selections */ + struct wl_display *wl_display = NULL; + struct copy_action **ptr; + wl_array_for_each(ptr, &pending_selections) { + struct copy_action *self = *ptr; + wl_display = self->device->wl_display; + device_set_selection(self->device, self->source, serial, self->primary); + } + + /* Make sure they reach the display + * before we do anything else, such + * as destroying the surface or exiting. + */ + wl_display_roundtrip(wl_display); + + /* Destroy the surface */ + popup_surface_destroy(current_popup_surface); + current_popup_surface = NULL; + + /* Invoke the callbacks */ + wl_array_for_each(ptr, &pending_selections) { + struct copy_action *self = *ptr; + if (self->did_set_selection_callback != NULL) { + self->did_set_selection_callback(self); + } + } + + /* Finally, destroy the array */ + wl_array_release(&pending_selections); +} + +static void do_send(struct source *source, const char *mime_type, int fd) { + struct copy_action *self = source->data; + + /* Unset O_NONBLOCK */ + fcntl(fd, F_SETFL, 0); + + if (self->copy_source.file_path != NULL) { + /* Copy the file to the given file descriptor + * by spawning an appropriate cat process. + */ + pid_t pid = fork(); + if (pid < 0) { + perror("fork"); + close(fd); + return; + } + if (pid == 0) { + dup2(fd, STDOUT_FILENO); + close(fd); + execlp("cat", "cat", self->copy_source.file_path, NULL); + perror("exec cat"); + exit(1); + } + close(fd); + /* Wait for the cat process to exit. This effectively + * means waiting for the other side to read the whole + * file. In theory, a malicious client could perform a + * denial-of-serivice attack against us. Perhaps we + * should switch to an asynchronous child waiting scheme + * instead. + */ + wait(NULL); + } else { + /* We'll perform the copy ourselves */ + FILE *f = fdopen(fd, "w"); + if (f == NULL) { + perror("fdopen"); + close(fd); + return; + } + + if (self->copy_source.data.ptr != NULL) { + /* Just copy the given chunk of data */ + fwrite( + self->copy_source.data.ptr, + 1, + self->copy_source.data.len, + f + ); + } else if (self->copy_source.argv != NULL) { + /* Copy an argv-style string array, + * inserting spaces between items. + */ + int is_first = 1; + for (argv_t word = self->copy_source.argv; *word != NULL; word++) { + if (!is_first) { + fwrite(" ", 1, 1, f); + } + is_first = 0; + fwrite(*word, 1, strlen(*word), f); + } + } else { + bail("Unreachable: nothing to copy"); + } + + fclose(f); + } + + + if (self->pasted_callback != NULL) { + self->pasted_callback(self); + } +} + +static void forward_cancel(struct source *source) { + struct copy_action *self = source->data; + if (self->cancelled_callback != NULL) { + self->cancelled_callback(self); + } +} + +void copy_action_init(struct copy_action *self) { + if (self->source != NULL) { + self->source->send_callback = do_send; + self->source->cancelled_callback = forward_cancel; + self->source->data = self; + } + + /* See if we can just set the selection directly */ + if (!self->device->needs_popup_surface) { + /* The simple case. + * In this case, setting the selection + * doesn't actually require a serial, + * so passing zero will do. + */ + device_set_selection(self->device, self->source, 0, self->primary); + /* Make sure it reaches the display + * before we do anything else. + */ + wl_display_roundtrip(self->device->wl_display); + /* And invoke the callback */ + if (self->did_set_selection_callback != NULL) { + self->did_set_selection_callback(self); + } + return; + } + + /* The complicated case. + * Add our action to the list of the pending + * actions to be done when the popup surface + * gets foces, and initialize both the surface + * and the array if needed. + */ + if (current_popup_surface == NULL) { + wl_array_init(&pending_selections); + } + struct copy_action **ptr = (struct copy_action **) wl_array_add( + &pending_selections, + sizeof(struct copy_action *) + ); + *ptr = self; + + /* Only initialize the surface after + * we've initialized the array, because + * it might get keyboard focus immediately. + */ + if (current_popup_surface == NULL) { + current_popup_surface = self->popup_surface; + current_popup_surface->on_focus = on_focus; + current_popup_surface->data = self; + popup_surface_init(current_popup_surface); + } +} diff --git a/src/types/copy-action.h b/src/types/copy-action.h new file mode 100644 index 0000000..5d46f20 --- /dev/null +++ b/src/types/copy-action.h @@ -0,0 +1,58 @@ +/* wl-clipboard + * + * Copyright © 2019 Sergey Bugaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TYPES_COPY_ACTION_H +#define TYPES_COPY_ACTION_H + +#include "util/string.h" + +#include + +struct device; +struct source; +struct popup_surface; + +struct copy_source { + /* Exactly one of these fields must be non-null if the source + * is non-null, otherwise all these fields must be null. + */ + const char *file_path; + argv_t argv; + struct { + const char *ptr; + size_t len; + } data; +}; + +struct copy_action { + /* These fields are initialized by the creator */ + struct device *device; + struct source *source; + struct popup_surface *popup_surface; + int primary; + + void (*did_set_selection_callback)(struct copy_action *self); + void (*pasted_callback)(struct copy_action *self); + void (*cancelled_callback)(struct copy_action *self); + + struct copy_source copy_source; +}; + +void copy_action_init(struct copy_action *self); + +#endif /* TYPES_COPY_ACTION_H */ diff --git a/src/types/device-manager.c b/src/types/device-manager.c new file mode 100644 index 0000000..bd6e04e --- /dev/null +++ b/src/types/device-manager.c @@ -0,0 +1,140 @@ +/* wl-clipboard + * + * Copyright © 2019 Sergey Bugaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "types/device-manager.h" +#include "types/device.h" +#include "types/source.h" +#include "types/seat.h" +#include "includes/selection-protocols.h" + +#include + + +struct source *device_manager_create_source(struct device_manager *self) { + return self->do_create_source(self); +} + +struct device *device_manager_get_device( + struct device_manager *self, + struct seat *seat +) { + return self->do_get_device(self, seat); +} + +/* Macros to reduce implementation boilerplate */ + +#define CREATE_SOURCE(type, source_type, method_name) \ +static struct source *device_manager_ ## type ## _do_create_source( \ + struct device_manager *self \ +) { \ + struct type *proxy = (struct type *) self->proxy; \ + struct source *source = calloc(1, sizeof(struct source)); \ + source->proxy = (struct wl_proxy *) type ## _ ## method_name(proxy); \ + source_init_ ## source_type(source); \ + return source; \ +} + +#define GET_DEVICE(type, device_type, method_name) \ +static struct device *device_manager_ ## type ## _do_get_device( \ + struct device_manager *self, \ + struct seat *seat_wrapper \ +) { \ + struct type *proxy = (struct type *) self->proxy; \ + struct wl_seat *seat = (struct wl_seat *) seat_wrapper->proxy; \ + struct device *device = calloc(1, sizeof(struct device)); \ + device->proxy = (struct wl_proxy *) type ## _ ## method_name(proxy, seat); \ + device->wl_display = self->wl_display; \ + device_init_ ## device_type(device); \ + return device; \ +} + +#define INIT(type) \ +void device_manager_init_ ## type(struct device_manager *self) { \ + self->do_create_source = device_manager_ ## type ## _do_create_source; \ + self->do_get_device = device_manager_ ## type ## _do_get_device; \ +} + + +/* Core Wayland implementation */ + +CREATE_SOURCE(wl_data_device_manager, wl_data_source, create_data_source) +GET_DEVICE(wl_data_device_manager, wl_data_device, get_data_device) +INIT(wl_data_device_manager) + + +/* gtk-primary-selection implementation */ + +#ifdef HAVE_GTK_PRIMARY_SELECTION + +CREATE_SOURCE( + gtk_primary_selection_device_manager, + gtk_primary_selection_source, + create_source +) + +GET_DEVICE( + gtk_primary_selection_device_manager, + gtk_primary_selection_device, + get_device +) + +INIT(gtk_primary_selection_device_manager) + +#endif /* HAVE_GTK_PRIMARY_SELECTION */ + + +/* wp-primary-selection implementation */ + +#ifdef HAVE_WP_PRIMARY_SELECTION + +CREATE_SOURCE( + zwp_primary_selection_device_manager_v1, + zwp_primary_selection_source_v1, + create_source +) + +GET_DEVICE( + zwp_primary_selection_device_manager_v1, + zwp_primary_selection_device_v1, + get_device +) + +INIT(zwp_primary_selection_device_manager_v1) + +#endif /* HAVE_WP_PRIMARY_SELECTION */ + + +/* wlr-data-control implementation */ + +#ifdef HAVE_WLR_DATA_CONTROL + +CREATE_SOURCE( + zwlr_data_control_manager_v1, + zwlr_data_control_source_v1, + create_data_source +) + +GET_DEVICE( + zwlr_data_control_manager_v1, + zwlr_data_control_device_v1, + get_data_device +) + +INIT(zwlr_data_control_manager_v1) + +#endif /* HAVE_WLR_DATA_CONTROL */ diff --git a/src/types/device-manager.h b/src/types/device-manager.h new file mode 100644 index 0000000..bb7e88d --- /dev/null +++ b/src/types/device-manager.h @@ -0,0 +1,72 @@ +/* wl-clipboard + * + * Copyright © 2019 Sergey Bugaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TYPES_DEVICE_MANAGER_H +#define TYPES_DEVICE_MANAGER_H + +#include "includes/selection-protocols.h" + +#include + +struct seat; +struct device; +struct source; + +struct device_manager { + /* These fields are initialized by the creator */ + struct wl_proxy *proxy; + struct wl_display *wl_display; + + /* These fields are initialized by the implementation */ + struct source *(*do_create_source)(struct device_manager *self); + struct device *(*do_get_device)( + struct device_manager *self, + struct seat *seat + ); +}; + +struct source *device_manager_create_source(struct device_manager *self); + +struct device *device_manager_get_device( + struct device_manager *self, + struct seat *seat +); + +/* Initializers */ + +void device_manager_init_wl_data_device_manager(struct device_manager *self); + +#ifdef HAVE_GTK_PRIMARY_SELECTION +void device_manager_init_gtk_primary_selection_device_manager( + struct device_manager *self +); +#endif + +#ifdef HAVE_WP_PRIMARY_SELECTION +void device_manager_init_zwp_primary_selection_device_manager_v1( + struct device_manager *self +); +#endif + +#ifdef HAVE_WLR_DATA_CONTROL +void device_manager_init_zwlr_data_control_manager_v1( + struct device_manager *self +); +#endif + +#endif /* TYPES_DEVICE_MANAGER_H */ diff --git a/src/types/device.c b/src/types/device.c new file mode 100644 index 0000000..16525a6 --- /dev/null +++ b/src/types/device.c @@ -0,0 +1,271 @@ +/* wl-clipboard + * + * Copyright © 2019 Sergey Bugaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "config.h" +#include "types/device.h" +#include "types/offer.h" +#include "types/source.h" +#include "includes/selection-protocols.h" + +#include +#include + +int device_supports_selection(struct device *self, int primary) { + return self->supports_selection(self, primary); +} + +void device_set_selection( + struct device *self, + struct source *source, + uint32_t serial, + int primary +) { + self->do_set_selection(self, source, serial, primary); +} + +/* Macros to reduce implementation boilerplate */ + +#define SUPPORTS_SELECTION(type, expr) \ +static int device_supports_selection_on_ ## type(struct device *self, int primary) { \ + return expr; \ +} + +#define SET_SELECTION_IMPL(type, source_type, impl) \ +static void device_set_selection_on_ ## type( \ + struct device *self, \ + struct source *source_wrapper, \ + uint32_t serial, \ + int primary \ +) { \ + struct type *device = (struct type *) self->proxy; \ + struct source_type *source = NULL; \ + if (source_wrapper != NULL) { \ + source = (struct source_type *) source_wrapper->proxy; \ + } \ + impl \ +} + +#define DATA_OFFER_HANDLER(type, offer_type) \ +static void type ## _data_offer_handler( \ + void *data, \ + struct type *device, \ + struct offer_type *offer_proxy \ +) { \ + struct device *self = data; \ + struct offer *offer = calloc(1, sizeof(struct offer)); \ + offer->proxy = (struct wl_proxy *) offer_proxy; \ + offer_init_ ## offer_type(offer); \ + if (self->new_offer_callback) { \ + self->new_offer_callback(offer); \ + } \ +} + +#define SELECTION_HANDLER(type, offer_type, selection_event, primary) \ +static void type ## _ ## selection_event ## _handler( \ + void *data, \ + struct type *device, \ + struct offer_type *offer_proxy \ +) { \ + struct device *self = data; \ + struct offer *offer = NULL; \ + if (offer_proxy != NULL) { \ + offer = wl_proxy_get_user_data((struct wl_proxy *) offer_proxy); \ + } \ + if (self->selection_callback) { \ + self->selection_callback(offer, primary); \ + } \ +} + +#define INIT(type, needs_surface) \ +void device_init_ ## type(struct device *self) { \ + struct type *device = (struct type *) self->proxy; \ + type ## _add_listener(device, &type ## _listener, self); \ + self->supports_selection = device_supports_selection_on_ ## type; \ + self->needs_popup_surface = needs_surface; \ + self->do_set_selection = device_set_selection_on_ ## type; \ +} + + +/* Core Wayland implementation */ + +SUPPORTS_SELECTION(wl_data_device, !primary) + +SET_SELECTION_IMPL(wl_data_device, wl_data_source, { + wl_data_device_set_selection(device, source, serial); +}) + +DATA_OFFER_HANDLER(wl_data_device, wl_data_offer) + +SELECTION_HANDLER(wl_data_device, wl_data_offer, selection, 0) + +static const struct wl_data_device_listener wl_data_device_listener = { + .data_offer = wl_data_device_data_offer_handler, + .selection = wl_data_device_selection_handler +}; + +INIT(wl_data_device, 1) + + +/* gtk-primary-selection implementation */ + +#ifdef HAVE_GTK_PRIMARY_SELECTION + +SUPPORTS_SELECTION(gtk_primary_selection_device, primary) + +SET_SELECTION_IMPL(gtk_primary_selection_device, gtk_primary_selection_source, { + gtk_primary_selection_device_set_selection(device, source, serial); +}) + +DATA_OFFER_HANDLER(gtk_primary_selection_device, gtk_primary_selection_offer) + +SELECTION_HANDLER( + gtk_primary_selection_device, + gtk_primary_selection_offer, + selection, + 1 +) + +static const struct gtk_primary_selection_device_listener +gtk_primary_selection_device_listener = { + .data_offer = gtk_primary_selection_device_data_offer_handler, + .selection = gtk_primary_selection_device_selection_handler +}; + +INIT(gtk_primary_selection_device, 1) + +#endif /* HAVE_GTK_PRIMARY_SELECTION */ + + +/* wp-primary-selection implementation */ + +#ifdef HAVE_WP_PRIMARY_SELECTION + +SUPPORTS_SELECTION(zwp_primary_selection_device_v1, primary) + +SET_SELECTION_IMPL( + zwp_primary_selection_device_v1, + zwp_primary_selection_source_v1, + { + zwp_primary_selection_device_v1_set_selection(device, source, serial); + } +) + +DATA_OFFER_HANDLER( + zwp_primary_selection_device_v1, + zwp_primary_selection_offer_v1 +) + +SELECTION_HANDLER( + zwp_primary_selection_device_v1, + zwp_primary_selection_offer_v1, + selection, + 1 +) + +static const struct zwp_primary_selection_device_v1_listener +zwp_primary_selection_device_v1_listener = { + .data_offer = zwp_primary_selection_device_v1_data_offer_handler, + .selection = zwp_primary_selection_device_v1_selection_handler +}; + +INIT(zwp_primary_selection_device_v1, 1) + +#endif /* HAVE_WP_PRIMARY_SELECTION */ + + +/* wlr-data-control implementation */ + +#ifdef HAVE_WLR_DATA_CONTROL + +enum TriState { + Unknown, + Yes, + No +}; + +/* Whether wlr-data-control supports primary selection */ +static enum TriState device_wlr_supports_primary_selection = Unknown; +static int device_get_wlr_supports_selection(struct device *self, int primary) { + if (!primary) { + return 1; + } + + if (device_wlr_supports_primary_selection == Yes) { + return 1; + } else if (device_wlr_supports_primary_selection == No) { + return 0; + } + + wl_display_roundtrip(self->wl_display); + + if (device_wlr_supports_primary_selection == Yes) { + return 1; + } else { + device_wlr_supports_primary_selection = No; + return 0; + } +} + +SUPPORTS_SELECTION( + zwlr_data_control_device_v1, + device_get_wlr_supports_selection(self, primary) +) + +SET_SELECTION_IMPL(zwlr_data_control_device_v1, zwlr_data_control_source_v1, { + if (!primary) { + zwlr_data_control_device_v1_set_selection(device, source); + } else { + zwlr_data_control_device_v1_set_primary_selection(device, source); + } +}) + +DATA_OFFER_HANDLER(zwlr_data_control_device_v1, zwlr_data_control_offer_v1) + +SELECTION_HANDLER( + zwlr_data_control_device_v1, + zwlr_data_control_offer_v1, + selection, + 0 +) + +static void zwlr_data_control_device_v1_primary_selection_handler( + void *data, + struct zwlr_data_control_device_v1 *device, + struct zwlr_data_control_offer_v1 *offer_proxy +) { + device_wlr_supports_primary_selection = Yes; + struct device *self = data; + struct offer *offer = NULL; + if (offer_proxy != NULL) { + offer = wl_proxy_get_user_data((struct wl_proxy *) offer_proxy); + } + if (self->selection_callback != NULL) { + self->selection_callback(offer, 1); + } +} + +static const struct zwlr_data_control_device_v1_listener +zwlr_data_control_device_v1_listener = { + .data_offer = zwlr_data_control_device_v1_data_offer_handler, + .selection = zwlr_data_control_device_v1_selection_handler, + .primary_selection = zwlr_data_control_device_v1_primary_selection_handler +}; + +INIT(zwlr_data_control_device_v1, 0) + +#endif /* HAVE_WLR_DATA_CONTROL */ diff --git a/src/types/device.h b/src/types/device.h new file mode 100644 index 0000000..5f5e3fe --- /dev/null +++ b/src/types/device.h @@ -0,0 +1,74 @@ +/* wl-clipboard + * + * Copyright © 2019 Sergey Bugaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TYPES_DEVICE_H +#define TYPES_DEVICE_H + +#include "includes/selection-protocols.h" + +#include + +struct offer; +struct source; + +struct device { + /* These fields are initialized by the creator */ + void (*new_offer_callback)(struct offer *offer); + void (*selection_callback)(struct offer *offer, int primary); + void *data; + + struct wl_proxy *proxy; + struct wl_display *wl_display; + + /* These fields are initialized by the implementation */ + int (*supports_selection)(struct device *self, int primary); + int needs_popup_surface; + void (*do_set_selection)( + struct device *self, + struct source *source, + uint32_t serial, + int primary + ); +}; + +int device_supports_selection(struct device *self, int primary); + +void device_set_selection( + struct device *self, + struct source *source, + uint32_t serial, + int primary +); + +/* Initializers */ + +void device_init_wl_data_device(struct device *self); + +#ifdef HAVE_GTK_PRIMARY_SELECTION +void device_init_gtk_primary_selection_device(struct device *self); +#endif + +#ifdef HAVE_WP_PRIMARY_SELECTION +void device_init_zwp_primary_selection_device_v1(struct device *self); +#endif + +#ifdef HAVE_WLR_DATA_CONTROL +void device_init_zwlr_data_control_device_v1(struct device *self); +#endif + +#endif /* TYPES_DEVICE_H */ diff --git a/src/types/keyboard.c b/src/types/keyboard.c new file mode 100644 index 0000000..99ca793 --- /dev/null +++ b/src/types/keyboard.c @@ -0,0 +1,82 @@ +/* wl-clipboard + * + * Copyright © 2019 Sergey Bugaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "types/keyboard.h" + +#include + +static void wl_keyboard_keymap_handler( + void *data, + struct wl_keyboard *keyboard, + uint32_t format, + int fd, + uint32_t size +) { + close(fd); +} + +static void wl_keyboard_enter_handler( + void *data, + struct wl_keyboard *keyboard, + uint32_t serial, + struct wl_surface *surface, + struct wl_array *keys +) { + struct keyboard *self = (struct keyboard *) data; + if (self->on_focus != NULL) { + self->on_focus(self, serial); + } +} + +static void wl_keyboard_leave_handler( + void *data, + struct wl_keyboard *keyboard, + uint32_t serial, + struct wl_surface *surface +) {} + +static void wl_keyboard_key_handler( + void *data, + struct wl_keyboard *keyboard, + uint32_t serial, + uint32_t time, + uint32_t key, + uint32_t state +) {} + +static void wl_keyboard_modifiers_handler( + void *data, + struct wl_keyboard *keyboard, + uint32_t serial, + uint32_t mods_depressed, + uint32_t mods_latched, + uint32_t mods_locked, + uint32_t group +) {} + +static const struct wl_keyboard_listener wl_keyboard_listener = { + .keymap = wl_keyboard_keymap_handler, + .enter = wl_keyboard_enter_handler, + .leave = wl_keyboard_leave_handler, + .key = wl_keyboard_key_handler, + .modifiers = wl_keyboard_modifiers_handler, +}; + +void keyboard_init(struct keyboard *self) { + wl_keyboard_add_listener(self->proxy, &wl_keyboard_listener, self); +} diff --git a/src/types/keyboard.h b/src/types/keyboard.h new file mode 100644 index 0000000..e5f02d0 --- /dev/null +++ b/src/types/keyboard.h @@ -0,0 +1,34 @@ +/* wl-clipboard + * + * Copyright © 2019 Sergey Bugaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TYPES_KEYBOARD_H +#define TYPES_KEYBOARD_H + +#include +#include + +struct keyboard { + /* These fields are initialized by the creator */ + struct wl_keyboard *proxy; + void (*on_focus)(struct keyboard *self, uint32_t serial); + void *data; +}; + +void keyboard_init(struct keyboard *self); + +#endif /* TYPES_KEYBOARD_H */ diff --git a/src/types/offer.c b/src/types/offer.c new file mode 100644 index 0000000..c21a562 --- /dev/null +++ b/src/types/offer.c @@ -0,0 +1,107 @@ +/* wl-clipboard + * + * Copyright © 2019 Sergey Bugaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "types/offer.h" +#include "includes/selection-protocols.h" + +#include +#include +#include + +void offer_receive(struct offer *self, const char *mime_type, int fd) { + self->do_receive(self->proxy, mime_type, fd); +} + +void offer_destroy(struct offer *self) { + self->do_destroy(self->proxy); + wl_array_release(&self->offered_mime_types); + free(self); +} + + +/* Macros to reduce implementation boilerplate */ + +#define OFFER_HANDLER(type) \ +static void type ## _offer_handler( \ + void *data, \ + struct type *proxy, \ + const char *mime_type \ +) { \ + struct offer *self = data; \ + char *ptr = wl_array_add( \ + &self->offered_mime_types, \ + strlen(mime_type) + 1 \ + ); \ + strcpy(ptr, mime_type); \ +} + +#define LISTENER(type) \ +static const struct type ## _listener type ## _listener = { \ + .offer = type ## _offer_handler \ +}; + +#define INIT(type) \ +void offer_init_ ## type(struct offer *self) { \ + self->do_receive = \ + (void (*)(struct wl_proxy *, const char *, int)) type ## _receive; \ + self->do_destroy = (void (*)(struct wl_proxy *)) type ## _destroy; \ + wl_array_init(&self->offered_mime_types); \ + struct type *proxy = (struct type *) self->proxy; \ + type ## _add_listener(proxy, &type ## _listener, self); \ +} + + +/* Core Wayland implementation */ + +OFFER_HANDLER(wl_data_offer) +LISTENER(wl_data_offer) +INIT(wl_data_offer) + + +/* gtk-primary-selection implementation */ + +#ifdef HAVE_GTK_PRIMARY_SELECTION + +OFFER_HANDLER(gtk_primary_selection_offer) +LISTENER(gtk_primary_selection_offer) +INIT(gtk_primary_selection_offer) + +#endif /* HAVE_GTK_PRIMARY_SELECTION */ + + +/* wp-primary-selection implementation */ + +#ifdef HAVE_WP_PRIMARY_SELECTION + +OFFER_HANDLER(zwp_primary_selection_offer_v1) +LISTENER(zwp_primary_selection_offer_v1) +INIT(zwp_primary_selection_offer_v1) + +#endif /* HAVE_WP_PRIMARY_SELECTION */ + + +/* wlr-data-control implementation */ + +#ifdef HAVE_WLR_DATA_CONTROL + +OFFER_HANDLER(zwlr_data_control_offer_v1) +LISTENER(zwlr_data_control_offer_v1) +INIT(zwlr_data_control_offer_v1) + +#endif /* HAVE_WLR_DATA_CONTROL */ + diff --git a/src/types/offer.h b/src/types/offer.h new file mode 100644 index 0000000..800466b --- /dev/null +++ b/src/types/offer.h @@ -0,0 +1,65 @@ +/* wl-clipboard + * + * Copyright © 2019 Sergey Bugaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TYPES_OFFER_H +#define TYPES_OFFER_H + +#include "includes/selection-protocols.h" + +#include +#include + +struct offer { + /* This field is initialized by the creator */ + struct wl_proxy *proxy; + + /* These fields are initialized by the implementation */ + void (*do_receive)(struct wl_proxy *proxy, const char *mime_type, int fd); + void (*do_destroy)(struct wl_proxy *proxy); + struct wl_array offered_mime_types; +}; + +#define offer_for_each_mime_type(offer, mime_type) \ +for ( \ + const char *mime_type = offer->offered_mime_types.data; \ + mime_type != offer->offered_mime_types.data + \ + offer->offered_mime_types.size; \ + mime_type += strlen(mime_type) + 1 \ +) + + +void offer_receive(struct offer *self, const char *mime_type, int fd); +void offer_destroy(struct offer *self); + +/* Initializers */ + +void offer_init_wl_data_offer(struct offer *self); + +#ifdef HAVE_GTK_PRIMARY_SELECTION +void offer_init_gtk_primary_selection_offer(struct offer *self); +#endif + +#ifdef HAVE_WP_PRIMARY_SELECTION +void offer_init_zwp_primary_selection_offer_v1(struct offer *self); +#endif + +#ifdef HAVE_WLR_DATA_CONTROL +void offer_init_zwlr_data_control_offer_v1(struct offer *self); +#endif + +#endif /* TYPES_OFFER_H */ diff --git a/src/types/popup-surface.c b/src/types/popup-surface.c new file mode 100644 index 0000000..b629a77 --- /dev/null +++ b/src/types/popup-surface.c @@ -0,0 +1,136 @@ +/* wl-clipboard + * + * Copyright © 2019 Sergey Bugaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "types/popup-surface.h" +#include "types/registry.h" +#include "types/seat.h" +#include "types/keyboard.h" +#include "types/shell.h" +#include "types/shell-surface.h" +#include "util/files.h" +#include "util/misc.h" + +#include +#include +#include + +static void forward_on_focus( + struct keyboard *keyboard, + uint32_t serial +) { + struct popup_surface *self = (struct popup_surface *) keyboard->data; + if (self->on_focus != NULL) { + self->on_focus(self, serial); + } +} + +void popup_surface_init(struct popup_surface *self) { + self->shell = registry_find_shell(self->registry); + if (self->shell == NULL) { + bail("Missing a shell"); + } + + self->keyboard = seat_get_keyboard(self->seat); + if (self->keyboard == NULL) { + bail("This seat has no keyboard"); + } + self->keyboard->on_focus = forward_on_focus; + self->keyboard->data = self; + + /* Make sure that we get the keyboard + * object before we create the surface, + * so that we get the enter event. + */ + wl_display_dispatch(self->registry->wl_display); + + + struct wl_compositor *wl_compositor = self->registry->wl_compositor; + if (wl_compositor == NULL) { + bail("Missing the compositor"); + } + self->wl_surface = wl_compositor_create_surface(wl_compositor); + self->shell_surface = shell_create_shell_surface( + self->shell, + self->wl_surface + ); + + /* Signal that the surface is ready to be configured */ + wl_surface_commit(self->wl_surface); + wl_display_roundtrip(self->registry->wl_display); + + if (self->wl_surface == NULL) { + /* It's possible that we were given focus + * (without ever commiting a buffer) during + * the above roundtrip, in which case we have + * already fired the callback and have likely + * already destroyed the surface. No need to + * do anything further in that case. + */ + free(self); + return; + } + + /* Remember that after this point, we should + * free() self when it gets destroyed. + */ + self->should_free_self = 1; + + int width = 1; + int height = 1; + int stride = width * 4; + int size = stride * height; + + /* Open an anonymous file and write some zero bytes to it */ + int fd = create_anonymous_file(); + ftruncate(fd, size); + + /* Create a shared memory pool */ + struct wl_shm *wl_shm = self->registry->wl_shm; + if (wl_shm == NULL) { + bail("Missing the shm"); + } + struct wl_shm_pool *wl_shm_pool = wl_shm_create_pool(wl_shm, fd, size); + + /* Allocate the buffer in that pool */ + struct wl_buffer *wl_buffer = wl_shm_pool_create_buffer( + wl_shm_pool, + 0, + width, + height, + stride, + WL_SHM_FORMAT_ARGB8888 + ); + /* We're using ARGB, so zero bytes mean + * a fully transparent pixel, which happens + * to be exactly what we want. + */ + + wl_surface_attach(self->wl_surface, wl_buffer, 0, 0); + wl_surface_damage(self->wl_surface, 0, 0, width, height); + wl_surface_commit(self->wl_surface); +} + +void popup_surface_destroy(struct popup_surface *self) { + shell_surface_destroy(self->shell_surface); + wl_surface_destroy(self->wl_surface); + free(self->shell); + + if (self->should_free_self) { + free(self); + } +} diff --git a/src/types/popup-surface.h b/src/types/popup-surface.h new file mode 100644 index 0000000..8847519 --- /dev/null +++ b/src/types/popup-surface.h @@ -0,0 +1,48 @@ +/* wl-clipboard + * + * Copyright © 2019 Sergey Bugaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TYPES_POPUP_SURFACE_H +#define TYPES_POPUP_SURFACE_H + +#include + +struct registry; +struct shell; +struct shell_surface; +struct seat; +struct keyboard; + +struct popup_surface { + /* These fields are initialized by the creator */ + struct registry *registry; + struct seat *seat; + void (*on_focus)(struct popup_surface *self, uint32_t serial); + void *data; + + /* These fields are initialized by the implementation */ + struct shell *shell; + struct shell_surface *shell_surface; + struct wl_surface *wl_surface; + struct keyboard *keyboard; + int should_free_self; +}; + +void popup_surface_init(struct popup_surface *self); +void popup_surface_destroy(struct popup_surface *self); + +#endif /* TYPES_POPUP_SURFACE_H */ diff --git a/src/types/registry.c b/src/types/registry.c new file mode 100644 index 0000000..67c7fde --- /dev/null +++ b/src/types/registry.c @@ -0,0 +1,206 @@ +/* wl-clipboard + * + * Copyright © 2019 Sergey Bugaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "types/registry.h" +#include "types/seat.h" +#include "types/shell.h" +#include "types/device-manager.h" +#include "includes/shell-protocols.h" +#include "includes/selection-protocols.h" +#include "util/misc.h" + +#include +#include + +#define BIND(interface_name, known_version) \ +if (strcmp(interface, #interface_name) == 0) { \ + self->interface_name = wl_registry_bind( \ + wl_registry, \ + name, \ + &interface_name ## _interface, \ + known_version \ + ); \ +} + +static void wl_registry_global_handler( + void *data, + struct wl_registry *wl_registry, + uint32_t name, + const char *interface, + uint32_t version +) { + struct registry *self = (struct registry *) data; + + BIND(wl_compositor, 2) + BIND(wl_shm, 1) + + /* Shells */ + + BIND(wl_shell, 1) + +#ifdef HAVE_XDG_SHELL + BIND(xdg_wm_base, 1) +#endif + + /* Device managers */ + + BIND(wl_data_device_manager, 1) + +#ifdef HAVE_GTK_PRIMARY_SELECTION + BIND(gtk_primary_selection_device_manager, 1) +#endif + +#ifdef HAVE_WP_PRIMARY_SELECTION + BIND(zwp_primary_selection_device_manager_v1, 1) +#endif + +#ifdef HAVE_WLR_DATA_CONTROL + BIND(zwlr_data_control_manager_v1, version > 2 ? 2 : version) +#endif + + if (strcmp(interface, "wl_seat") == 0) { + struct seat *seat = calloc(1, sizeof(struct seat)); + seat->proxy = wl_registry_bind( + wl_registry, + name, + &wl_seat_interface, + 2 + ); + seat_init(seat); + struct seat **ptr = wl_array_add(&self->seats, sizeof(struct seat *)); + *ptr = seat; + } +} + +static void wl_registry_global_remove_handler( + void *data, + struct wl_registry *wl_registry, + uint32_t name +) {} + +static const struct wl_registry_listener wl_registry_listener = { + .global = wl_registry_global_handler, + .global_remove = wl_registry_global_remove_handler +}; + +void registry_init(struct registry *self) { + self->proxy = wl_display_get_registry(self->wl_display); + wl_registry_add_listener(self->proxy, &wl_registry_listener, self); +} + +struct shell *registry_find_shell(struct registry *self) { + struct shell *shell = calloc(1, sizeof(struct shell)); + + if (self->wl_shell != NULL) { + shell->proxy = (struct wl_proxy *) self->wl_shell; + shell_init_wl_shell(shell); + return shell; + } + +#ifdef HAVE_XDG_SHELL + if (self->xdg_wm_base != NULL) { + shell->proxy = (struct wl_proxy *) self->xdg_wm_base; + shell_init_xdg_shell(shell); + return shell; + } +#endif + + free(shell); + return NULL; +} + +#define TRY(type) \ +if (self->type != NULL) { \ + device_manager->proxy = (struct wl_proxy *) self->type; \ + device_manager_init_ ## type(device_manager); \ + return device_manager; \ +} + +struct device_manager *registry_find_device_manager( + struct registry *self, + int primary +) { + struct device_manager *device_manager + = calloc(1, sizeof(struct device_manager)); + device_manager->wl_display = self->wl_display; + + /* For regular selection, we just look at the two supported + * protocols. We prefer wlr-data-control, as it doesn't require + * us to use the popup surface hack. + */ + + if (!primary) { +#ifdef HAVE_WLR_DATA_CONTROL + TRY(zwlr_data_control_manager_v1) +#endif + TRY(wl_data_device_manager) + + free(device_manager); + return NULL; + } + + /* For primary selection, it's a bit more complicated. We also + * prefer wlr-data-control, but we don't know in advance whether + * the compositor supports primary selection, as unlike with + * other protocols here, the mere presence of wlr-data-control + * does not imply primary selection support. However, we assume + * that if a compositor supports primary selection at all, then + * if it supports wlr-data-control v2 it also supports primary + * selection over wlr-data-control; which is only reasonable. + */ + +#ifdef HAVE_WLR_DATA_CONTROL + if (self->zwlr_data_control_manager_v1 != NULL) { + struct wl_proxy *proxy + = (struct wl_proxy *) self->zwlr_data_control_manager_v1; + if (wl_proxy_get_version(proxy) >= 2) { + device_manager->proxy = proxy; + device_manager_init_zwlr_data_control_manager_v1(device_manager); + return device_manager; + } + } +#endif + +#ifdef HAVE_WP_PRIMARY_SELECTION + TRY(zwp_primary_selection_device_manager_v1) +#endif + +#ifdef HAVE_GTK_PRIMARY_SELECTION + TRY(gtk_primary_selection_device_manager) +#endif + + free(device_manager); + return NULL; +} + +struct seat *registry_find_seat( + struct registry *self, + const char *name +) { + /* Ensure we get all the seat info */ + wl_display_roundtrip(self->wl_display); + + struct seat **ptr; + wl_array_for_each(ptr, &self->seats) { + struct seat *seat = *ptr; + if (name == NULL || strcmp(seat->name, name) == 0) { + return seat; + } + } + return NULL; +} diff --git a/src/types/registry.h b/src/types/registry.h new file mode 100644 index 0000000..e52e427 --- /dev/null +++ b/src/types/registry.h @@ -0,0 +1,81 @@ +/* wl-clipboard + * + * Copyright © 2019 Sergey Bugaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TYPES_REGISTRY_H +#define TYPES_REGISTRY_H + +#include "includes/shell-protocols.h" +#include "includes/selection-protocols.h" + +#include + +struct shell; +struct device_manager; +struct seat; + +struct registry { + /* This field is initialized by the creator */ + struct wl_display *wl_display; + + /* These fields are initialized by the implementation */ + + struct wl_registry *proxy; + struct wl_array seats; + + struct wl_compositor *wl_compositor; + struct wl_shm *wl_shm; + + /* Shells */ + + struct wl_shell *wl_shell; +#ifdef HAVE_XDG_SHELL + struct xdg_wm_base *xdg_wm_base; +#endif + + /* Device managers */ + + struct wl_data_device_manager *wl_data_device_manager; +#ifdef HAVE_GTK_PRIMARY_SELECTION + struct gtk_primary_selection_device_manager + *gtk_primary_selection_device_manager; +#endif +#ifdef HAVE_WP_PRIMARY_SELECTION + struct zwp_primary_selection_device_manager_v1 + *zwp_primary_selection_device_manager_v1; +#endif +#ifdef HAVE_WLR_DATA_CONTROL + struct zwlr_data_control_manager_v1 + *zwlr_data_control_manager_v1; +#endif +}; + +void registry_init(struct registry *self); + +struct shell *registry_find_shell(struct registry *self); + +struct device_manager *registry_find_device_manager( + struct registry *self, + int primary +); + +struct seat *registry_find_seat( + struct registry *self, + const char *name +); + +#endif /* TYPES_REGISTRY_H */ diff --git a/src/types/seat.c b/src/types/seat.c new file mode 100644 index 0000000..6e48450 --- /dev/null +++ b/src/types/seat.c @@ -0,0 +1,61 @@ +/* wl-clipboard + * + * Copyright © 2019 Sergey Bugaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "types/seat.h" +#include "types/keyboard.h" + +#include +#include +#include + +static void wl_seat_capabilities_handler( + void *data, + struct wl_seat *seat, + uint32_t capabilities +) { + struct seat *self = (struct seat *) data; + self->capabilities = capabilities; +} + +static void wl_seat_name_handler( + void *data, + struct wl_seat *seat, + const char *name +) { + struct seat *self = (struct seat *) data; + self->name = strdup(name); +} + +static const struct wl_seat_listener wl_seat_listener = { + .capabilities = wl_seat_capabilities_handler, + .name = wl_seat_name_handler +}; + +void seat_init(struct seat *self) { + wl_seat_add_listener(self->proxy, &wl_seat_listener, self); +} + +struct keyboard *seat_get_keyboard(struct seat *self) { + if ((self->capabilities & WL_SEAT_CAPABILITY_KEYBOARD) == 0) { + return NULL; + } + struct keyboard *keyboard = calloc(1, sizeof(struct keyboard)); + keyboard->proxy = wl_seat_get_keyboard(self->proxy); + keyboard_init(keyboard); + return keyboard; +} diff --git a/src/types/seat.h b/src/types/seat.h new file mode 100644 index 0000000..cd92073 --- /dev/null +++ b/src/types/seat.h @@ -0,0 +1,39 @@ +/* wl-clipboard + * + * Copyright © 2019 Sergey Bugaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TYPES_SEAT_H +#define TYPES_SEAT_H + +#include + +struct keyboard; + +struct seat { + /* This field is initialized by the creator */ + struct wl_seat *proxy; + + /* These fields are initialized by the implementation */ + char *name; + uint32_t capabilities; +}; + +void seat_init(struct seat *self); +struct keyboard *seat_get_keyboard(struct seat *self); + + +#endif /* TYPES_SEAT_H */ diff --git a/src/types/shell-surface.c b/src/types/shell-surface.c new file mode 100644 index 0000000..ba7e61b --- /dev/null +++ b/src/types/shell-surface.c @@ -0,0 +1,125 @@ +/* wl-clipboard + * + * Copyright © 2019 Sergey Bugaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "types/shell-surface.h" +#include "includes/shell-protocols.h" + +#include +#include + +void shell_surface_destroy(struct shell_surface *self) { + self->do_destroy(self); + free(self); +} + + +/* Core Wayland implementation */ + +static void wl_shell_surface_ping( + void *data, + struct wl_shell_surface *wl_shell_surface, + uint32_t serial +) { + wl_shell_surface_pong(wl_shell_surface, serial); +} + +static void wl_shell_surface_configure( + void *data, + struct wl_shell_surface *wl_shell_surface, + uint32_t edges, + int32_t width, + int32_t height +) {} + +static void wl_shell_surface_popup_done( + void *data, + struct wl_shell_surface *wl_shell_surface +) {} + +static const struct wl_shell_surface_listener wl_shell_surface_listener = { + .ping = wl_shell_surface_ping, + .configure = wl_shell_surface_configure, + .popup_done = wl_shell_surface_popup_done +}; + +static void destroy_wl_shell_surface(struct shell_surface *self) { + struct wl_shell_surface *proxy = (struct wl_shell_surface *) self->proxy; + wl_shell_surface_destroy(proxy); +} + +void shell_surface_init_wl_shell_surface(struct shell_surface *self) { + struct wl_shell_surface *proxy = (struct wl_shell_surface *) self->proxy; + wl_shell_surface_add_listener(proxy, &wl_shell_surface_listener, self); + wl_shell_surface_set_toplevel(proxy); + wl_shell_surface_set_title(proxy, "wl-clipboard"); + self->do_destroy = destroy_wl_shell_surface; +} + + +/* xdg-shell implementation */ + +#ifdef HAVE_XDG_SHELL + +static void xdg_toplevel_configure_handler( + void *data, + struct xdg_toplevel *xdg_toplevel, + int32_t width, + int32_t height, + struct wl_array *states +) {} + +static void xdg_toplevel_close_handler( + void *data, + struct xdg_toplevel *xdg_toplevel +) {} + +static const struct xdg_toplevel_listener xdg_toplevel_listener = { + .configure = xdg_toplevel_configure_handler, + .close = xdg_toplevel_close_handler +}; + +static void xdg_surface_configure_handler( + void *data, + struct xdg_surface *xdg_surface, + uint32_t serial +) { + xdg_surface_ack_configure(xdg_surface, serial); +} + +static const struct xdg_surface_listener xdg_surface_listener = { + .configure = xdg_surface_configure_handler +}; + +static void destroy_xdg_surface(struct shell_surface *self) { + struct xdg_toplevel *toplevel = (struct xdg_toplevel *) self->proxy2; + struct xdg_surface *proxy = (struct xdg_surface *) self->proxy; + xdg_toplevel_destroy(toplevel); + xdg_surface_destroy(proxy); +} + +void shell_surface_init_xdg_surface(struct shell_surface *self) { + struct xdg_surface *proxy = (struct xdg_surface *) self->proxy; + xdg_surface_add_listener(proxy, &xdg_surface_listener, self); + struct xdg_toplevel *toplevel = xdg_surface_get_toplevel(proxy); + self->proxy2 = (struct wl_proxy *) toplevel; + xdg_toplevel_add_listener(toplevel, &xdg_toplevel_listener, self); + xdg_toplevel_set_title(toplevel, "wl-clipboard"); + self->do_destroy = destroy_xdg_surface; +} + +#endif /* HAVE_XDG_SHELL */ diff --git a/src/types/shell-surface.h b/src/types/shell-surface.h new file mode 100644 index 0000000..d888553 --- /dev/null +++ b/src/types/shell-surface.h @@ -0,0 +1,44 @@ +/* wl-clipboard + * + * Copyright © 2019 Sergey Bugaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TYPES_SHELL_SURFACE_H +#define TYPES_SHELL_SURFACE_H + +#include "includes/shell-protocols.h" + +struct shell_surface { + /* This field is initialized by the creator */ + struct wl_proxy *proxy; + + /* These fields are initialized by the implementation */ + struct wl_proxy *proxy2; + void (*do_destroy)(struct shell_surface *self); +}; + +void shell_surface_destroy(struct shell_surface *self); + + +/* Initializers */ + +void shell_surface_init_wl_shell_surface(struct shell_surface *self); + +#ifdef HAVE_XDG_SHELL +void shell_surface_init_xdg_surface(struct shell_surface *self); +#endif + +#endif /* TYPES_SHELL_SURFACE_H */ diff --git a/src/types/shell.c b/src/types/shell.c new file mode 100644 index 0000000..36d192a --- /dev/null +++ b/src/types/shell.c @@ -0,0 +1,93 @@ +/* wl-clipboard + * + * Copyright © 2019 Sergey Bugaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "types/shell-surface.h" +#include "types/shell.h" +#include "includes/shell-protocols.h" + +#include + +struct shell_surface *shell_create_shell_surface( + struct shell *self, + struct wl_surface *surface +) { + return self->do_create_shell_surface(self, surface); +} + + +/* Core Wayland implementation */ + +static struct shell_surface *wl_shell_create_shell_surface( + struct shell *self, + struct wl_surface *surface +) { + struct wl_shell *shell = (struct wl_shell *) self->proxy; + struct wl_shell_surface *wl_shell_surface = + wl_shell_get_shell_surface(shell, surface); + + struct shell_surface *shell_surface + = calloc(1, sizeof(struct shell_surface)); + shell_surface->proxy = (struct wl_proxy *) wl_shell_surface; + shell_surface_init_wl_shell_surface(shell_surface); + return shell_surface; +} + +void shell_init_wl_shell(struct shell *self) { + self->do_create_shell_surface = wl_shell_create_shell_surface; +} + + +/* xdg-shell implementation */ + +#ifdef HAVE_XDG_SHELL + +static struct shell_surface *xdg_shell_create_shell_surfacce( + struct shell *self, + struct wl_surface *surface +) { + struct xdg_wm_base *wm_base = (struct xdg_wm_base *) self->proxy; + + struct shell_surface *shell_surface + = calloc(1, sizeof(struct shell_surface)); + + struct xdg_surface *xdg_surface = + xdg_wm_base_get_xdg_surface(wm_base, surface); + shell_surface->proxy = (struct wl_proxy *) xdg_surface; + shell_surface_init_xdg_surface(shell_surface); + return shell_surface; +} + +static void xdg_wm_base_ping_handler( + void *data, + struct xdg_wm_base *wm_base, + uint32_t serial +) { + xdg_wm_base_pong(wm_base, serial); +} + +static const struct xdg_wm_base_listener xdg_wm_base_listener = { + .ping = xdg_wm_base_ping_handler +}; + +void shell_init_xdg_shell(struct shell *self) { + struct xdg_wm_base *wm_base = (struct xdg_wm_base *) self->proxy; + xdg_wm_base_add_listener(wm_base, &xdg_wm_base_listener, self); + self->do_create_shell_surface = xdg_shell_create_shell_surfacce; +} + +#endif /* HAVE_XDG_SHELL */ diff --git a/src/types/shell.h b/src/types/shell.h new file mode 100644 index 0000000..9b866eb --- /dev/null +++ b/src/types/shell.h @@ -0,0 +1,53 @@ +/* wl-clipboard + * + * Copyright © 2019 Sergey Bugaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TYPES_SHELL_H +#define TYPES_SHELL_H + +#include "includes/shell-protocols.h" + +#include + +struct shell_surface; + +struct shell { + /* This field is initialized by the creator */ + struct wl_proxy *proxy; + + /* This field is initialized by the implementation */ + struct shell_surface *(*do_create_shell_surface)( + struct shell *self, + struct wl_surface *surface + ); +}; + +struct shell_surface *shell_create_shell_surface( + struct shell *self, + struct wl_surface *surface +); + + +/* Initializers */ + +void shell_init_wl_shell(struct shell *self); + +#ifdef HAVE_XDG_SHELL +void shell_init_xdg_shell(struct shell *self); +#endif + +#endif /* TYPES_SHELL_H */ diff --git a/src/types/source.c b/src/types/source.c new file mode 100644 index 0000000..94d57ce --- /dev/null +++ b/src/types/source.c @@ -0,0 +1,136 @@ +/* wl-clipboard + * + * Copyright © 2019 Sergey Bugaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "config.h" +#include "includes/selection-protocols.h" +#include "types/source.h" +#include "util/string.h" + +#include +#include + +void source_offer(struct source *self, char *mime_type) { + self->do_offer(self->proxy, mime_type); +} + +void source_destroy(struct source *self) { + self->do_destroy(self->proxy); + free(self); +} + + +/* Macros to reduce implementation boilerplate */ + +#define SEND_HANDLER(type) \ +static void type ## _send_handler( \ + void *data, \ + struct type *proxy, \ + const char *mime_type, \ + int fd \ +) { \ + struct source *self = data; \ + if (self->send_callback != NULL) { \ + self->send_callback(self, mime_type, fd); \ + } else { \ + close(fd); \ + } \ +} + +#define CANCELLED_HANDLER(type) \ +static void type ## _cancelled_handler( \ + void *data, \ + struct type *proxy \ +) { \ + struct source *self = data; \ + if (self->cancelled_callback != NULL) { \ + self->cancelled_callback(self); \ + } \ +} + +#define LISTENER(type) \ +static const struct type ## _listener type ## _listener = { \ + .send = type ## _send_handler, \ + .cancelled = type ## _cancelled_handler \ +}; + +#define INIT(type) \ +void source_init_ ## type(struct source *self) { \ + self->do_offer = \ + (void (*)(struct wl_proxy *, const char *)) type ## _offer; \ + self->do_destroy = (void (*)(struct wl_proxy *)) type ## _destroy; \ + struct type *proxy = (struct type *) self->proxy; \ + type ## _add_listener(proxy, &type ## _listener, self); \ +} + + +/* Core Wayland implementation */ + +static void wl_data_source_target_handler +( + void *data, + struct wl_data_source *wl_data_source, + const char *mime_type +) {} + +SEND_HANDLER(wl_data_source) +CANCELLED_HANDLER(wl_data_source) + +static const struct wl_data_source_listener wl_data_source_listener = { + .target = wl_data_source_target_handler, + .send = wl_data_source_send_handler, + .cancelled = wl_data_source_cancelled_handler +}; + +INIT(wl_data_source) + + +/* gtk-primary-selection implementation */ + +#ifdef HAVE_GTK_PRIMARY_SELECTION + +SEND_HANDLER(gtk_primary_selection_source) +CANCELLED_HANDLER(gtk_primary_selection_source) +LISTENER(gtk_primary_selection_source) +INIT(gtk_primary_selection_source) + +#endif /* HAVE_GTK_PRIMARY_SELECTION */ + + +/* wp-primary-selection implementation */ + +#ifdef HAVE_WP_PRIMARY_SELECTION + +SEND_HANDLER(zwp_primary_selection_source_v1) +CANCELLED_HANDLER(zwp_primary_selection_source_v1) +LISTENER(zwp_primary_selection_source_v1) +INIT(zwp_primary_selection_source_v1) + +#endif /* HAVE_WP_PRIMARY_SELECTION */ + + +/* wlr-data-control implementation */ + +#ifdef HAVE_WLR_DATA_CONTROL + +SEND_HANDLER(zwlr_data_control_source_v1) +CANCELLED_HANDLER(zwlr_data_control_source_v1) +LISTENER(zwlr_data_control_source_v1) +INIT(zwlr_data_control_source_v1) + +#endif /* HAVE_WLR_DATA_CONTROL */ + diff --git a/src/types/source.h b/src/types/source.h new file mode 100644 index 0000000..43c0675 --- /dev/null +++ b/src/types/source.h @@ -0,0 +1,57 @@ +/* wl-clipboard + * + * Copyright © 2019 Sergey Bugaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TYPES_SOURCE_H +#define TYPES_SOURCE_H + +#include "includes/selection-protocols.h" + + +struct source { + /* These fields are initialized by the creator */ + void (*send_callback)(struct source *self, const char *mime_type, int fd); + void (*cancelled_callback)(struct source *self); + void *data; + + struct wl_proxy *proxy; + + /* These fields are initialized by the implementation */ + void (*do_offer)(struct wl_proxy *proxy, const char *mime_type); + void (*do_destroy)(struct wl_proxy *proxy); +}; + +void source_offer(struct source *self, char *mime_type); +void source_destroy(struct source *self); + +/* Initializers */ + +void source_init_wl_data_source(struct source *self); + +#ifdef HAVE_GTK_PRIMARY_SELECTION +void source_init_gtk_primary_selection_source(struct source *self); +#endif + +#ifdef HAVE_WP_PRIMARY_SELECTION +void source_init_zwp_primary_selection_source_v1(struct source *self); +#endif + +#ifdef HAVE_WLR_DATA_CONTROL +void source_init_zwlr_data_control_source_v1(struct source *self); +#endif + +#endif /* TYPES_SOURCE_H */ diff --git a/src/util/files.c b/src/util/files.c index 9384001..591eb4e 100644 --- a/src/util/files.c +++ b/src/util/files.c @@ -69,14 +69,15 @@ void trim_trailing_newline(const char *file_path) { int seek_res = lseek(fd, -1, SEEK_END); if (seek_res < 0 && errno == EINVAL) { - // empty file + /* It was an empty file */ goto out; } else if (seek_res < 0) { perror("lseek"); goto out; } - // otherwise, seek_res is the new file size - + /* If the seek was successful, seek_res is the + * new file size after trimming the newline. + */ char last_char; int read_res = read(fd, &last_char, 1); if (read_res != 1) { @@ -99,9 +100,18 @@ char *path_for_fd(int fd) { } char *infer_mime_type_from_contents(const char *file_path) { + /* Spawn xdg-mime query filetype */ int pipefd[2]; pipe(pipefd); - if (fork() == 0) { + + pid_t pid = fork(); + if (pid < 0) { + perror("fork"); + close(pipefd[0]); + close(pipefd[1]); + return NULL; + } + if (pid == 0) { dup2(pipefd[1], STDOUT_FILENO); close(pipefd[0]); close(pipefd[1]); @@ -115,22 +125,27 @@ char *infer_mime_type_from_contents(const char *file_path) { close(pipefd[1]); int wstatus; wait(&wstatus); - if (WIFEXITED(wstatus) && WEXITSTATUS(wstatus) == 0) { - char *res = malloc(256); - size_t len = read(pipefd[0], res, 256); - len--; // trim the newline + + /* See if that worked */ + if (!WIFEXITED(wstatus) || WEXITSTATUS(wstatus) != 0) { close(pipefd[0]); - res[len] = 0; - - if (str_has_prefix(res, "inode/")) { - free(res); - return NULL; - } - - return res; + return NULL; } + + /* Read the result */ + char *res = malloc(256); + size_t len = read(pipefd[0], res, 256); + /* Trim the newline */ + len--; + res[len] = 0; close(pipefd[0]); - return NULL; + + if (str_has_prefix(res, "inode/")) { + free(res); + return NULL; + } + + return res; } char *infer_mime_type_from_name(const char *file_path) { @@ -148,20 +163,20 @@ char *infer_mime_type_from_name(const char *file_path) { } for (char line[200]; fgets(line, sizeof(line), f) != NULL;) { - // skip comments and blank lines + /* Skip comments and black lines */ if (line[0] == '#' || line[0] == '\n') { continue; } - // each line consists of a mime type and a list of extensions + /* Each line consists of a mime type and a list of extensions */ char mime_type[200]; int consumed; - if (sscanf(line, "%s%n", mime_type, &consumed) != 1) { - // malformed line? + if (sscanf(line, "%199s%n", mime_type, &consumed) != 1) { + /* A malformed line, perhaps? */ continue; } char *lineptr = line + consumed; - for (char ext[200]; sscanf(lineptr, "%s%n", ext, &consumed) == 1;) { + for (char ext[200]; sscanf(lineptr, "%199s%n", ext, &consumed) == 1;) { if (strcmp(ext, actual_ext) == 0) { fclose(f); return strdup(mime_type); @@ -173,21 +188,36 @@ char *infer_mime_type_from_name(const char *file_path) { return NULL; } +/* Returns the name of a new file */ char *dump_stdin_into_a_temp_file() { + /* Create a temp directory to host out file */ char dirpath[] = "/tmp/wl-copy-buffer-XXXXXX"; if (mkdtemp(dirpath) != dirpath) { perror("mkdtemp"); exit(1); } + + /* Pick a name for the file we'll be + * creating inside that directory. We + * try to preserve the origial name for + * the mime type inference to work. + */ char *original_path = path_for_fd(STDIN_FILENO); char *name = original_path != NULL ? basename(original_path) : "stdin"; + /* Construct the path */ char *res_path = malloc(strlen(dirpath) + 1 + strlen(name) + 1); memcpy(res_path, dirpath, sizeof(dirpath)); - strcat(res_path, "/"); - strcat(res_path, name); + res_path[sizeof(dirpath) - 1] = '/'; + strcpy(res_path + sizeof(dirpath), name); - if (fork() == 0) { + /* Spawn cat to perform the copy */ + pid_t pid = fork(); + if (pid < 0) { + perror("fork"); + exit(1); + } + if (pid == 0) { int fd = creat(res_path, S_IRUSR | S_IWUSR); if (fd < 0) { perror("creat"); @@ -205,8 +235,8 @@ char *dump_stdin_into_a_temp_file() { if (original_path != NULL) { free(original_path); } - if (WIFEXITED(wstatus) && WEXITSTATUS(wstatus) == 0) { - return res_path; + if (!WIFEXITED(wstatus) || WEXITSTATUS(wstatus) != 0) { + bail("Failed to copy the file"); } - bail("Failed to copy the file"); + return res_path; } diff --git a/src/util/files.h b/src/util/files.h index ec8fa3e..2a2d4b6 100644 --- a/src/util/files.h +++ b/src/util/files.h @@ -23,14 +23,15 @@ int create_anonymous_file(void); void trim_trailing_newline(const char *file_path); -// these functions return owned strings, so make sure -// to free() their return values when done with them +/* These functions return owned strings, so make sure + * to free() their return values when done with them. + */ char *path_for_fd(int fd); char *infer_mime_type_from_contents(const char *file_path); char *infer_mime_type_from_name(const char *file_path); -// returns the name of a new file +/* Returns the name of a new file */ char *dump_stdin_into_a_temp_file(void); #endif /* UTIL_FILES_H */ diff --git a/src/util/misc.c b/src/util/misc.c index 934792e..f252a62 100644 --- a/src/util/misc.c +++ b/src/util/misc.c @@ -31,3 +31,16 @@ void print_version_info() { "There is NO WARRANTY, to the extent permitted by law.\n" ); } + +void complain_about_selection_support(int primary) { + if (!primary) { + /* We always expect to find at least wl_data_device_manager */ + bail("Missing a required global object"); + } + +#if !defined(HAVE_WP_PRIMARY_SELECTION) && !defined(HAVE_GTK_PRIMARY_SELECTION) + bail("wl-clipboard was built without primary selection support"); +#endif + + bail("The compositor does not seem to support primary selection"); +} diff --git a/src/util/misc.h b/src/util/misc.h index 54890b9..be5298f 100644 --- a/src/util/misc.h +++ b/src/util/misc.h @@ -26,4 +26,6 @@ void print_version_info(void); +void complain_about_selection_support(int primary); + #endif /* UTIL_MISC_H */ diff --git a/src/util/string.c b/src/util/string.c index 757b87a..f39db31 100644 --- a/src/util/string.c +++ b/src/util/string.c @@ -22,13 +22,35 @@ #include int mime_type_is_text(const char *mime_type) { - return str_has_prefix(mime_type, "text/") + /* A heuristic to detect plain text mime types */ + + /* Types that explicitly declare they're textual */ + int basic + = str_has_prefix(mime_type, "text/") || strcmp(mime_type, "TEXT") == 0 || strcmp(mime_type, "STRING") == 0 - || strcmp(mime_type, "UTF8_STRING") == 0 + || strcmp(mime_type, "UTF8_STRING") == 0; + + /* Common script and markup types */ + int common + = strstr(mime_type, "json") != NULL || str_has_suffix(mime_type, "script") || str_has_suffix(mime_type, "xml") - || strstr(mime_type, "json") != NULL; + || str_has_suffix(mime_type, "yaml"); + + /* Special-case PGP and SSH keys. + * A public SSH key is typically stored + * in a file that has a name similar to + * id_rsa.pub, which xdg-mime misidentifies + * as being a Publisher file. Note that it + * handles private keys, which do not have + * a .pub extension, correctly. + */ + int special + = strstr(mime_type, "application/vnd.ms-publisher") != NULL + || str_has_suffix(mime_type, "pgp-keys"); + + return basic || common || special; } int str_has_prefix(const char *string, const char *prefix) { diff --git a/src/util/string.h b/src/util/string.h index 4602652..239cf58 100644 --- a/src/util/string.h +++ b/src/util/string.h @@ -22,6 +22,8 @@ #define text_plain "text/plain" #define text_plain_utf8 "text/plain;charset=utf-8" +typedef char * const *argv_t; + int mime_type_is_text(const char *mime_type); int str_has_prefix(const char *string, const char *prefix); diff --git a/src/wl-copy.c b/src/wl-copy.c index c4a3e62..89948bc 100644 --- a/src/wl-copy.c +++ b/src/wl-copy.c @@ -16,16 +16,75 @@ * along with this program. If not, see . */ -#include "boilerplate.h" +#include "types/copy-action.h" +#include "types/source.h" +#include "types/device.h" +#include "types/device-manager.h" +#include "types/registry.h" +#include "types/popup-surface.h" -char * const *data_to_copy = NULL; -char *temp_file_to_copy = NULL; -int paste_once = 0; +#include "util/files.h" +#include "util/string.h" +#include "util/misc.h" -void do_cancel() { - // we're done! - if (temp_file_to_copy != NULL) { - execlp("rm", "rm", "-r", dirname(temp_file_to_copy), NULL); +#include +#include +#include +#include +#include +#include + +static struct { + int stay_in_foreground; + int clear; + char *mime_type; + int trim_newline; + int paste_once; + int primary; + int regular; + struct wl_array seat_names; +} options; + +static struct { + int unset; + int live; +} copy_actions_cnt; + +static void did_set_selection_callback(struct copy_action *copy_action) { + copy_actions_cnt.unset--; + + if (options.clear) { + copy_actions_cnt.live--; + if (copy_actions_cnt.live == 0) { + exit(0); + } + } + + if (!options.stay_in_foreground && copy_actions_cnt.unset == 0) { + /* Move to background. + * We fork our process and leave the + * child running in the background, + * while exiting in the parent. + */ + pid_t pid = fork(); + if (pid < 0) { + perror("fork"); + /* Proceed without forking */ + } + if (pid > 0) { + exit(0); + } + } +} + +static void cleanup_and_exit(struct copy_action *copy_action) { + /* We're done copying! + * All that's left to do now is to + * clean up after ourselves and exit.*/ + char *temp_file = (char *) copy_action->copy_source.file_path; + if (temp_file != NULL) { + /* Clean up our temporary file */ + execlp("rm", "rm", "-r", dirname(temp_file), NULL); perror("exec rm"); exit(1); } else { @@ -33,319 +92,123 @@ void do_cancel() { } } -void do_send(const char *mime_type, int fd) { - // unset O_NONBLOCK - fcntl(fd, F_SETFL, 0); - if (data_to_copy != NULL) { - // copy the specified data, separated by spaces - FILE *f = fdopen(fd, "w"); - if (f == NULL) { - perror("fdopen"); - exit(1); +static void cancelled_callback(struct copy_action *copy_action) { + copy_actions_cnt.live--; + if (copy_actions_cnt.live == 0) { + cleanup_and_exit(copy_action); + } +} + +static void pasted_callback(struct copy_action *copy_action) { + if (options.paste_once) { + cleanup_and_exit(copy_action); + } +} + +static void set_up_selection_for_seat( + struct wl_display *wl_display, + struct registry *registry, + struct seat *seat, + struct copy_source copy_source, + int primary +) { + /* Create the device */ + struct device_manager *device_manager + = registry_find_device_manager(registry, primary); + if (device_manager == NULL) { + complain_about_selection_support(primary); + } + + struct device *device = device_manager_get_device(device_manager, seat); + + if (!device_supports_selection(device, primary)) { + complain_about_selection_support(primary); + } + + copy_actions_cnt.live++; + copy_actions_cnt.unset++; + + /* Create and initialize the copy action */ + struct copy_action *copy_action = calloc(1, sizeof(struct copy_action)); + copy_action->device = device; + copy_action->primary = primary; + copy_action->copy_source = copy_source; + + /* Create the source */ + if (!options.clear) { + copy_action->source = device_manager_create_source(device_manager); + if (options.mime_type != NULL) { + source_offer(copy_action->source, options.mime_type); } - char * const *dataptr = data_to_copy; - for (int is_first = 1; *dataptr != NULL; dataptr++, is_first = 0) { - if (!is_first) { - fwrite(" ", 1, 1, f); - } - fwrite(*dataptr, 1, strlen(*dataptr), f); + if (options.mime_type == NULL || mime_type_is_text(options.mime_type)) { + /* Offer a few generic plain text formats */ + source_offer(copy_action->source, text_plain); + source_offer(copy_action->source, text_plain_utf8); + source_offer(copy_action->source, "TEXT"); + source_offer(copy_action->source, "STRING"); + source_offer(copy_action->source, "UTF8_STRING"); } - fclose(f); - } else { - // copy from the temp file; for that, we delegate to a - // (hopefully) highly optimized implementation of copying - if (fork() == 0) { - dup2(fd, STDOUT_FILENO); - execlp("cat", "cat", temp_file_to_copy, NULL); - perror("exec cat"); - exit(1); + } + + if (device->needs_popup_surface) { + copy_action->popup_surface = calloc(1, sizeof(struct popup_surface)); + copy_action->popup_surface->registry = registry; + copy_action->popup_surface->seat = seat; + } + + copy_action->did_set_selection_callback = did_set_selection_callback; + copy_action->pasted_callback = pasted_callback; + copy_action->cancelled_callback = cancelled_callback; + copy_action_init(copy_action); +} + +static void set_up_selection( + struct wl_display *wl_display, + struct registry *registry, + struct copy_source copy_source, + int primary +) { + struct seat *seat = NULL; + + /* Go over the requested seat names */ + for ( + const char *seat_name = options.seat_names.data; + seat_name != options.seat_names.data + options.seat_names.size; + seat_name += strlen(seat_name) + 1 + ) { + seat = registry_find_seat(registry, seat_name); + if (seat == NULL) { + bail("No such seat"); } - close(fd); - wait(NULL); + set_up_selection_for_seat( + wl_display, + registry, + seat, + copy_source, + primary + ); } - if (paste_once) { - do_cancel(); + /* See if any seat was requested at all */ + if (seat == NULL) { + /* No seat was explicitly requested. + * Try to find any seat. + */ + seat = registry_find_seat(registry, NULL); + if (seat == NULL) { + bail("Missing a seat"); + } + set_up_selection_for_seat( + wl_display, + registry, + seat, + copy_source, + primary + ); } } -void data_source_target_handler -( - void *data, - struct wl_data_source *data_source, - const char *mime_type -) {} - -void data_source_send_handler -( - void *data, - struct wl_data_source *data_source, - const char *mime_type, - int fd -) { - do_send(mime_type, fd); -} - -void data_source_cancelled_handler -( - void *data, - struct wl_data_source *data_source -) { - do_cancel(); -} - -const struct wl_data_source_listener data_source_listener = { - .target = data_source_target_handler, - .send = data_source_send_handler, - .cancelled = data_source_cancelled_handler -}; - -struct wl_data_source *data_source; - -void set_data_selection(uint32_t serial) { - wl_data_device_set_selection(data_device, data_source, serial); - wl_display_roundtrip(display); - destroy_popup_surface(); -} - -void try_setting_data_selection_directly() { - set_data_selection(get_serial()); -} - - -void complain_about_missing_keyboard() { - bail("Setting primary selection is not supported without a keyboard"); -} - -#ifdef HAVE_GTK_PRIMARY_SELECTION - -void gtk_primary_selection_source_send_handler -( - void *data, - struct gtk_primary_selection_source *gtk_primary_selection_source, - const char *mime_type, - int fd -) { - do_send(mime_type, fd); -} - -void gtk_primary_selection_source_cancelled_handler -( - void *data, - struct gtk_primary_selection_source *gtk_primary_selection_source -) { - do_cancel(); -} - -const struct gtk_primary_selection_source_listener -gtk_primary_selection_source_listener = { - .send = gtk_primary_selection_source_send_handler, - .cancelled = gtk_primary_selection_source_cancelled_handler -}; - -struct gtk_primary_selection_source *gtk_primary_selection_source; - -void set_gtk_primary_selection(uint32_t serial) { - - gtk_primary_selection_device_set_selection( - gtk_primary_selection_device, - gtk_primary_selection_source, - serial - ); - - wl_display_roundtrip(display); - destroy_popup_surface(); -} - -#endif - -#ifdef HAVE_WP_PRIMARY_SELECTION - -void primary_selection_source_send_handler -( - void *data, - struct zwp_primary_selection_source_v1 *primary_selection_source, - const char *mime_type, - int fd -) { - do_send(mime_type, fd); -} - -void primary_selection_source_cancelled_handler -( - void *data, - struct zwp_primary_selection_source_v1 *primary_selection_source -) { - do_cancel(); -} - -const struct zwp_primary_selection_source_v1_listener -primary_selection_source_listener = { - .send = primary_selection_source_send_handler, - .cancelled = primary_selection_source_cancelled_handler -}; - -struct zwp_primary_selection_source_v1 *primary_selection_source; - -void set_primary_selection(uint32_t serial) { - - zwp_primary_selection_device_v1_set_selection( - primary_selection_device, - primary_selection_source, - serial - ); - - wl_display_roundtrip(display); - destroy_popup_surface(); -} - -#endif - -#ifdef HAVE_WLR_DATA_CONTROL - -void data_control_source_send_handler -( - void *data, - struct zwlr_data_control_source_v1 *data_control_source, - const char *mime_type, - int fd -) { - do_send(mime_type, fd); -} - -void data_control_source_cancelled_handler -( - void *data, - struct zwlr_data_control_source_v1 *data_source -) { - do_cancel(); -} - -const struct zwlr_data_control_source_v1_listener -data_control_source_listener = { - .send = data_control_source_send_handler, - .cancelled = data_control_source_cancelled_handler -}; -#endif - -void do_offer -( - char *mime_type, - void *source, - void (*offer_f)(void *source, const char *type) -) { - if (mime_type == NULL || mime_type_is_text(mime_type)) { - // offer a few generic plain text formats - offer_f(source, text_plain); - offer_f(source, text_plain_utf8); - offer_f(source, "TEXT"); - offer_f(source, "STRING"); - offer_f(source, "UTF8_STRING"); - } - if (mime_type != NULL) { - offer_f(source, mime_type); - } - free(mime_type); -} - -void init_selection(char *mime_type) { - if (use_wlr_data_control) { -#ifdef HAVE_WLR_DATA_CONTROL - struct zwlr_data_control_source_v1 *data_control_source = - zwlr_data_control_manager_v1_create_data_source( - data_control_manager - ); - zwlr_data_control_source_v1_add_listener( - data_control_source, - &data_control_source_listener, - NULL - ); - - do_offer( - mime_type, - data_control_source, - (void (*)(void *, const char *)) zwlr_data_control_source_v1_offer - ); - - zwlr_data_control_device_v1_set_selection( - data_control_device, - data_control_source - ); -#endif - } else { - data_source = wl_data_device_manager_create_data_source( - data_device_manager - ); - wl_data_source_add_listener(data_source, &data_source_listener, NULL); - - do_offer( - mime_type, - data_source, - (void (*)(void *, const char *)) wl_data_source_offer - ); - - action_on_popup_surface_getting_focus = set_data_selection; - action_on_no_keyboard = try_setting_data_selection_directly; - popup_tiny_invisible_surface(); - } -} - -void init_primary_selection(char *mime_type) { - ensure_has_primary_selection(); - -#ifdef HAVE_WP_PRIMARY_SELECTION - if (primary_selection_device_manager != NULL) { - primary_selection_source = - zwp_primary_selection_device_manager_v1_create_source( - primary_selection_device_manager - ); - zwp_primary_selection_source_v1_add_listener( - primary_selection_source, - &primary_selection_source_listener, - NULL - ); - - do_offer( - mime_type, - primary_selection_source, - (void (*)(void *, const char *)) - zwp_primary_selection_source_v1_offer - ); - - action_on_popup_surface_getting_focus = set_primary_selection; - action_on_no_keyboard = complain_about_missing_keyboard; - popup_tiny_invisible_surface(); - return; - } -#endif - -#ifdef HAVE_GTK_PRIMARY_SELECTION - if (gtk_primary_selection_device_manager != NULL) { - gtk_primary_selection_source = - gtk_primary_selection_device_manager_create_source( - gtk_primary_selection_device_manager - ); - gtk_primary_selection_source_add_listener( - gtk_primary_selection_source, - >k_primary_selection_source_listener, - NULL - ); - - do_offer( - mime_type, - gtk_primary_selection_source, - (void (*)(void *, const char *)) gtk_primary_selection_source_offer - ); - - action_on_popup_surface_getting_focus = set_gtk_primary_selection; - action_on_no_keyboard = complain_about_missing_keyboard; - popup_tiny_invisible_surface(); - return; - } -#endif -} - -void print_usage(FILE *f, const char *argv0) { +static void print_usage(FILE *f, const char *argv0) { fprintf( f, "Usage:\n" @@ -356,6 +219,7 @@ void print_usage(FILE *f, const char *argv0) { "\t-o, --paste-once\tOnly serve one paste request and then exit.\n" "\t-f, --foreground\tStay in the foreground instead of forking.\n" "\t-c, --clear\t\tInstead of copying anything, clear the clipboard.\n" + "\t-r, --regular\t\tUse the regular clipboard.\n" "\t-p, --primary\t\tUse the \"primary\" clipboard.\n" "\t-n, --trim-newline\tDo not copy the trailing newline character.\n" "\t-t, --type mime/type\t" @@ -372,17 +236,12 @@ void print_usage(FILE *f, const char *argv0) { ); } -int main(int argc, char * const argv[]) { - +static void parse_options(int argc, argv_t argv) { if (argc < 1) { bail("Empty argv"); } - int stay_in_foreground = 0; - int clear = 0; - char *mime_type = NULL; - int primary = 0; - int trim_newline = 0; + wl_array_init(&options.seat_names); static struct option long_options[] = { {"version", no_argument, 0, 'v'}, @@ -392,13 +251,14 @@ int main(int argc, char * const argv[]) { {"paste-once", no_argument, 0, 'o'}, {"foreground", no_argument, 0, 'f'}, {"clear", no_argument, 0, 'c'}, + {"regular", no_argument, 0, 'r'}, {"type", required_argument, 0, 't'}, {"seat", required_argument, 0, 's'}, {0, 0, 0, 0} }; while (1) { int option_index; - const char *opts = "vhpnofct:s:"; + const char *opts = "vhpnofcrt:s:"; int c = getopt_long(argc, argv, opts, long_options, &option_index); if (c == -1) { break; @@ -414,75 +274,84 @@ int main(int argc, char * const argv[]) { print_usage(stdout, argv[0]); exit(0); case 'p': - primary = 1; + options.primary = 1; break; case 'n': - trim_newline = 1; + options.trim_newline = 1; break; case 'o': - paste_once = 1; + options.paste_once = 1; break; case 'f': - stay_in_foreground = 1; + options.stay_in_foreground = 1; break; case 'c': - clear = 1; + options.clear = 1; + break; + case 'r': + options.regular = 1; break; case 't': - mime_type = strdup(optarg); + options.mime_type = strdup(optarg); break; - case 's': - requested_seat_name = strdup(optarg); + case 's':; + char *ptr = wl_array_add(&options.seat_names, strlen(optarg) + 1); + strcpy(ptr, optarg); break; default: - // getopt has already printed an error message + /* getopt has already printed an error message */ print_usage(stderr, argv[0]); exit(1); } } +} - init_wayland_globals(); +int main(int argc, argv_t argv) { + parse_options(argc, argv); - if (primary) { - ensure_has_primary_selection(); + struct wl_display *wl_display = wl_display_connect(NULL); + if (wl_display == NULL) { + bail("Failed to connect to a Wayland server"); } - if (!clear) { + struct registry *registry = calloc(1, sizeof(struct registry)); + registry->wl_display = wl_display; + registry_init(registry); + + /* Wait for the initial set of globals to appear */ + wl_display_roundtrip(wl_display); + + struct copy_source copy_source = { 0 }; + + if (!options.clear) { if (optind < argc) { - // copy our command-line args - data_to_copy = &argv[optind]; + /* Copy our command-line arguments */ + copy_source.argv = &argv[optind]; } else { - // copy stdin - temp_file_to_copy = dump_stdin_into_a_temp_file(); - if (trim_newline) { - trim_trailing_newline(temp_file_to_copy); + /* Copy data from our stdin */ + char *temp_file = dump_stdin_into_a_temp_file(); + if (options.trim_newline) { + trim_trailing_newline(temp_file); } - if (mime_type == NULL) { - mime_type = infer_mime_type_from_contents(temp_file_to_copy); + if (options.mime_type == NULL) { + options.mime_type = infer_mime_type_from_contents(temp_file); } + copy_source.file_path = temp_file; } } - if (!stay_in_foreground && !clear) { - if (fork() != 0) { - // exit in the parent, but leave the - // child running in the background - exit(0); - } + /* By default, or if the regular clipboard + * is explicitly requested, use the regular + * clipboard. + */ + if (!options.primary || options.regular) { + set_up_selection(wl_display, registry, copy_source, 0); + } + if (options.primary) { + set_up_selection(wl_display, registry, copy_source, 1); } - if (!primary) { - init_selection(mime_type); - } else { - init_primary_selection(mime_type); - } - - if (clear) { - wl_display_roundtrip(display); - exit(0); - } - - while (wl_display_dispatch(display) >= 0); + while (wl_display_dispatch(wl_display) >= 0); perror("wl_display_dispatch"); return 1; diff --git a/src/wl-paste.c b/src/wl-paste.c index d3d4022..36a1142 100644 --- a/src/wl-paste.c +++ b/src/wl-paste.c @@ -16,97 +16,119 @@ * along with this program. If not, see . */ -#include "boilerplate.h" +#include "types/offer.h" +#include "types/device.h" +#include "types/device-manager.h" +#include "types/registry.h" +#include "types/popup-surface.h" -struct { +#include "util/files.h" +#include "util/string.h" +#include "util/misc.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +static struct { char *explicit_type; char *inferred_type; int no_newline; int list_types; + int primary; + int watch; + argv_t watch_command; + const char *seat_name; } options; -struct { +struct types { int explicit_available; int inferred_available; int plain_text_utf8_available; int plain_text_available; - char *having_explicit_as_prefix; - char *any_text; - char *any; -} available_types; + const char *having_explicit_as_prefix; + const char *any_text; + const char *any; +}; -void do_process_offer(const char *offered_type) { - if (options.list_types) { - printf("%s\n", offered_type); - } else { +static struct wl_display *wl_display = NULL; +static struct popup_surface *popup_surface = NULL; + +static struct types classify_offer_types(struct offer *offer) { + struct types types = { 0 }; + offer_for_each_mime_type(offer, mime_type) { if ( options.explicit_type != NULL && - strcmp(offered_type, options.explicit_type) == 0 + strcmp(mime_type, options.explicit_type) == 0 ) { - available_types.explicit_available = 1; + types.explicit_available = 1; } if ( options.inferred_type != NULL && - strcmp(offered_type, options.inferred_type) == 0 + strcmp(mime_type, options.inferred_type) == 0 ) { - available_types.inferred_available = 1; + types.inferred_available = 1; + } + if (strcmp(mime_type, text_plain_utf8) == 0) { + types.plain_text_utf8_available = 1; + } + if (strcmp(mime_type, text_plain) == 0) { + types.plain_text_available = 1; } if ( - strcmp(offered_type, text_plain_utf8) == 0) { - available_types.plain_text_utf8_available = 1; - } - if ( - strcmp(offered_type, text_plain) == 0) { - available_types.plain_text_available = 1; - } - if ( - available_types.any_text == NULL && - mime_type_is_text(offered_type) + types.any_text == NULL && + mime_type_is_text(mime_type) ) { - available_types.any_text = strdup(offered_type); + types.any_text = mime_type; } - if (available_types.any == NULL) { - available_types.any = strdup(offered_type); + if (types.any == NULL) { + types.any = mime_type; } if ( options.explicit_type != NULL && - available_types.having_explicit_as_prefix == NULL && - str_has_prefix(offered_type, options.explicit_type) + types.having_explicit_as_prefix == NULL && + str_has_prefix(mime_type, options.explicit_type) ) { - available_types.having_explicit_as_prefix = strdup(offered_type); + types.having_explicit_as_prefix = mime_type; } } + return types; } #define try_explicit \ -if (available_types.explicit_available) \ +if (types.explicit_available) \ return options.explicit_type #define try_inferred \ -if (available_types.inferred_available) \ +if (types.inferred_available) \ return options.inferred_type #define try_text_plain_utf8 \ -if (available_types.plain_text_utf8_available) \ +if (types.plain_text_utf8_available) \ return text_plain_utf8 #define try_text_plain \ -if (available_types.plain_text_available) \ +if (types.plain_text_available) \ return text_plain #define try_prefixed \ -if (available_types.having_explicit_as_prefix != NULL) \ - return available_types.having_explicit_as_prefix +if (types.having_explicit_as_prefix != NULL) \ + return types.having_explicit_as_prefix #define try_any_text \ -if (available_types.any_text != NULL) \ - return available_types.any_text +if (types.any_text != NULL) \ + return types.any_text #define try_any \ -if (available_types.any != NULL) \ - return available_types.any +if (types.any != NULL) \ + return types.any -const char *mime_type_to_request() { +static const char *mime_type_to_request(struct types types) { if (options.explicit_type != NULL) { if (strcmp(options.explicit_type, "text") == 0) { try_text_plain_utf8; @@ -121,7 +143,9 @@ const char *mime_type_to_request() { try_prefixed; } } else { - // no mime type requested explicitly, try to guess + /* No mime type requested explicitly, + * so try to guess. + */ if (options.inferred_type == NULL) { try_text_plain_utf8; try_text_plain; @@ -136,7 +160,7 @@ const char *mime_type_to_request() { try_inferred; } } - bail("No suitable type of content copied"); + return NULL; } #undef try_explicit @@ -147,29 +171,41 @@ const char *mime_type_to_request() { #undef try_any_text #undef try_any -void free_types() { - free(available_types.having_explicit_as_prefix); - free(available_types.any_text); - free(available_types.any); - free(options.explicit_type); - free(options.inferred_type); -} +static void selection_callback(struct offer *offer, int primary) { + /* Ignore events we're not interested in */ + if (primary != options.primary) { + if (offer != NULL) { + offer_destroy(offer); + }; + return; + } -void do_paste -( - void *offer, - void (*receive_f)(void *offer, const char *mime_type, int fd) -) { if (offer == NULL) { + if (options.watch) { + return; + } bail("No selection"); } if (options.list_types) { + offer_for_each_mime_type(offer, mime_type) { + printf("%s\n", mime_type); + } exit(0); } - const char *mime_type = mime_type_to_request(); - // never append a newline character to binary content + struct types types = classify_offer_types(offer); + const char *mime_type = mime_type_to_request(types); + + if (mime_type == NULL) { + if (options.watch) { + offer_destroy(offer); + return; + } + bail("No suitable type of content copied"); + } + + /* Never append a newline character to binary content */ if (!mime_type_is_text(mime_type)) { options.no_newline = 1; } @@ -177,216 +213,65 @@ void do_paste int pipefd[2]; pipe(pipefd); - receive_f(offer, mime_type, pipefd[1]); + offer_receive(offer, mime_type, pipefd[1]); - free_types(); - destroy_popup_surface(); + if (popup_surface != NULL) { + popup_surface_destroy(popup_surface); + popup_surface = NULL; + } + wl_display_roundtrip(wl_display); - wl_display_roundtrip(display); - - if (fork() == 0) { + /* Spawn a cat to perform the copy. + * If watch mode is active, we spawn + * a custom command instead. + */ + pid_t pid = fork(); + if (pid < 0) { + perror("fork"); + if (options.watch) { + /* Try to cope without exiting completely */ + close(pipefd[0]); + close(pipefd[1]); + offer_destroy(offer); + return; + } + exit(1); + } + if (pid == 0) { dup2(pipefd[0], STDIN_FILENO); close(pipefd[0]); close(pipefd[1]); - execlp("cat", "cat", NULL); - perror("exec cat"); + if (options.watch) { + execvp(options.watch_command[0], options.watch_command); + fprintf( + stderr, + "Failed to spawn %s: %s", + options.watch_command[0], + strerror(errno) + ); + } else { + execlp("cat", "cat", NULL); + perror("exec cat"); + } exit(1); } close(pipefd[0]); close(pipefd[1]); wait(NULL); - if (!options.no_newline) { + if (!options.no_newline && !options.watch) { write(STDOUT_FILENO, "\n", 1); } - exit(0); + + offer_destroy(offer); + + if (!options.watch) { + free(options.explicit_type); + free(options.inferred_type); + exit(0); + } } -void data_offer_offer -( - void *data, - struct wl_data_offer *data_offer, - const char *offered_mime_type -) { - do_process_offer(offered_mime_type); -} - -const struct wl_data_offer_listener data_offer_listener = { - .offer = data_offer_offer -}; - -void data_device_data_offer -( - void *data, - struct wl_data_device *data_device, - struct wl_data_offer *data_offer -) { - wl_data_offer_add_listener(data_offer, &data_offer_listener, NULL); -} - -void data_device_selection -( - void *data, - struct wl_data_device *data_device, - struct wl_data_offer *data_offer -) { - do_paste( - data_offer, - (void (*)(void *, const char *, int)) wl_data_offer_receive - ); -} - -const struct wl_data_device_listener data_device_listener = { - .data_offer = data_device_data_offer, - .selection = data_device_selection -}; - -#ifdef HAVE_GTK_PRIMARY_SELECTION - -void gtk_primary_selection_offer_offer -( - void *data, - struct gtk_primary_selection_offer *gtk_primary_selection_offer, - const char *offered_mime_type -) { - do_process_offer(offered_mime_type); -} - -const struct gtk_primary_selection_offer_listener -gtk_primary_selection_offer_listener = { - .offer = gtk_primary_selection_offer_offer -}; - -void gtk_primary_selection_device_data_offer -( - void *data, - struct gtk_primary_selection_device *gtk_primary_selection_device, - struct gtk_primary_selection_offer *gtk_primary_selection_offer -) { - gtk_primary_selection_offer_add_listener( - gtk_primary_selection_offer, - >k_primary_selection_offer_listener, - NULL - ); -} - -void gtk_primary_selection_device_selection -( - void *data, - struct gtk_primary_selection_device *gtk_primary_selection_device, - struct gtk_primary_selection_offer *gtk_primary_selection_offer -) { - do_paste( - gtk_primary_selection_offer, - (void (*)(void *, const char *, int)) - gtk_primary_selection_offer_receive - ); -} - -const struct gtk_primary_selection_device_listener -gtk_primary_selection_device_listener = { - .data_offer = gtk_primary_selection_device_data_offer, - .selection = gtk_primary_selection_device_selection -}; - -#endif - -#ifdef HAVE_WP_PRIMARY_SELECTION - -void primary_selection_offer_offer -( - void *data, - struct zwp_primary_selection_offer_v1 *primary_selection_offer, - const char *offered_mime_type -) { - do_process_offer(offered_mime_type); -} - -const struct zwp_primary_selection_offer_v1_listener -primary_selection_offer_listener = { - .offer = primary_selection_offer_offer -}; - -void primary_selection_device_data_offer -( - void *data, - struct zwp_primary_selection_device_v1 *primary_selection_device, - struct zwp_primary_selection_offer_v1 *primary_selection_offer -) { - zwp_primary_selection_offer_v1_add_listener( - primary_selection_offer, - &primary_selection_offer_listener, - NULL - ); -} - -void primary_selection_device_selection -( - void *data, - struct zwp_primary_selection_device_v1 *primary_selection_device, - struct zwp_primary_selection_offer_v1 *primary_selection_offer -) { - do_paste( - primary_selection_offer, - (void (*)(void *, const char *, int)) - zwp_primary_selection_offer_v1_receive - ); -} - -const struct zwp_primary_selection_device_v1_listener -primary_selection_device_listener = { - .data_offer = primary_selection_device_data_offer, - .selection = primary_selection_device_selection -}; - -#endif - -#ifdef HAVE_WLR_DATA_CONTROL -void data_control_offer_offer -( - void *data, - struct zwlr_data_control_offer_v1 *data_offer, - const char *offered_mime_type -) { - do_process_offer(offered_mime_type); -} - -const struct zwlr_data_control_offer_v1_listener data_control_offer_listener = { - .offer = data_control_offer_offer -}; - -void data_control_device_data_offer -( - void *data, - struct zwlr_data_control_device_v1 *data_control_device, - struct zwlr_data_control_offer_v1 *data_control_offer -) { - zwlr_data_control_offer_v1_add_listener( - data_control_offer, - &data_control_offer_listener, - NULL - ); -} - -void data_control_device_selection -( - void *data, - struct zwlr_data_control_device_v1 *data_control_device, - struct zwlr_data_control_offer_v1 *data_control_offer -) { - do_paste( - data_control_offer, - (void (*)(void *, const char *, int)) zwlr_data_control_offer_v1_receive - ); -} - -const struct zwlr_data_control_device_v1_listener -data_control_device_listener = { - .data_offer = data_control_device_data_offer, - .selection = data_control_device_selection -}; -#endif - -void print_usage(FILE *f, const char *argv0) { +static void print_usage(FILE *f, const char *argv0) { fprintf( f, "Usage:\n" @@ -396,6 +281,8 @@ void print_usage(FILE *f, const char *argv0) { "\t-n, --no-newline\tDo not append a newline character.\n" "\t-l, --list-types\tInstead of pasting, list the offered types.\n" "\t-p, --primary\t\tUse the \"primary\" clipboard.\n" + "\t-w, --watch command\t" + "Run a command each time the selection changes.\n" "\t-t, --type mime/type\t" "Override the inferred MIME type for the content.\n" "\t-s, --seat seat-name\t" @@ -409,70 +296,25 @@ void print_usage(FILE *f, const char *argv0) { ); } -void init_selection() { - if (use_wlr_data_control) { -#ifdef HAVE_WLR_DATA_CONTROL - zwlr_data_control_device_v1_add_listener( - data_control_device, - &data_control_device_listener, - NULL - ); -#endif - } else { - wl_data_device_add_listener(data_device, &data_device_listener, NULL); - popup_tiny_invisible_surface(); - } -} - -void init_primary_selection() { - ensure_has_primary_selection(); - -#ifdef HAVE_WP_PRIMARY_SELECTION - if (primary_selection_device != NULL) { - zwp_primary_selection_device_v1_add_listener( - primary_selection_device, - &primary_selection_device_listener, - NULL - ); - popup_tiny_invisible_surface(); - return; - } -#endif - -#ifdef HAVE_GTK_PRIMARY_SELECTION - if (gtk_primary_selection_device != NULL) { - gtk_primary_selection_device_add_listener( - gtk_primary_selection_device, - >k_primary_selection_device_listener, - NULL - ); - popup_tiny_invisible_surface(); - return; - } -#endif -} - -int main(int argc, char * const argv[]) { - +static void parse_options(int argc, argv_t argv) { if (argc < 1) { bail("Empty argv"); } - int primary = 0; - static struct option long_options[] = { {"version", no_argument, 0, 'v'}, {"help", no_argument, 0, 'h'}, {"primary", no_argument, 0, 'p'}, {"no-newline", no_argument, 0, 'n'}, {"list-types", no_argument, 0, 'l'}, + {"watch", required_argument, 0, 'w'}, {"type", required_argument, 0, 't'}, {"seat", required_argument, 0, 's'}, {0, 0, 0, 0} }; while (1) { int option_index; - const char *opts = "vhpnlt:s:"; + const char *opts = "vhpnlw:t:s:"; int c = getopt_long(argc, argv, opts, long_options, &option_index); if (c == -1) { break; @@ -488,7 +330,7 @@ int main(int argc, char * const argv[]) { print_usage(stdout, argv[0]); exit(0); case 'p': - primary = 1; + options.primary = 1; break; case 'n': options.no_newline = 1; @@ -496,34 +338,101 @@ int main(int argc, char * const argv[]) { case 'l': options.list_types = 1; break; + case 'w': + options.watch = 1; + options.watch_command = (argv_t) &argv[optind - 1]; + /* We're going to forward the rest of our + * arguments to the command we spawn, so stop + * trying to process further options. + */ + return; case 't': options.explicit_type = strdup(optarg); break; case 's': - requested_seat_name = strdup(optarg); + options.seat_name = strdup(optarg); break; default: - // getopt has already printed an error message + /* getopt has already printed an error message */ print_usage(stderr, argv[0]); exit(1); } } + if (optind != argc) { + fprintf(stderr, "Unexpected argument: %s\n", argv[optind]); + print_usage(stderr, argv[0]); + exit(1); + } +} + +int main(int argc, argv_t argv) { + parse_options(argc, argv); + char *path = path_for_fd(STDOUT_FILENO); if (path != NULL && options.explicit_type == NULL) { options.inferred_type = infer_mime_type_from_name(path); } free(path); - init_wayland_globals(); - - if (!primary) { - init_selection(); - } else { - init_primary_selection(); + wl_display = wl_display_connect(NULL); + if (wl_display == NULL) { + bail("Failed to connect to a Wayland server"); } - while (wl_display_dispatch(display) >= 0); + struct registry *registry = calloc(1, sizeof(struct registry)); + registry->wl_display = wl_display; + registry_init(registry); + + /* Wait for the initial set of globals to appear */ + wl_display_roundtrip(wl_display); + + struct seat *seat = registry_find_seat(registry, options.seat_name); + if (seat == NULL) { + if (options.seat_name != NULL) { + bail("No such seat"); + } else { + bail("Missing a seat"); + } + } + + /* Create the device */ + struct device_manager *device_manager + = registry_find_device_manager(registry, options.primary); + if (device_manager == NULL) { + complain_about_selection_support(options.primary); + } + + struct device *device = device_manager_get_device(device_manager, seat); + /* Set up the callback before checking whether the device + * actually supports the kind of selection we need, because + * checking for the support might roundtrip. + */ + device->selection_callback = selection_callback; + + if (!device_supports_selection(device, options.primary)) { + complain_about_selection_support(options.primary); + } + + if (device->needs_popup_surface) { + if (options.watch) { + bail( + "Watch mode requires a compositor that supports " + "wlroots data-control protocol" + ); + } + /* If we cannot get the selection directly, pop up + * a surface. When it gets focus, we'll immediately + * get the selection events, se we don't need to do + * anything special on the surface getting focus. + */ + popup_surface = calloc(1, sizeof(struct popup_surface)); + popup_surface->registry = registry; + popup_surface->seat = seat; + popup_surface_init(popup_surface); + } + + while (wl_display_dispatch(wl_display) >= 0); perror("wl_display_dispatch"); return 1;