summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFelipe Sateler <fsateler@gmail.com>2018-11-14 20:57:27 -0300
committerFelipe Sateler <fsateler@gmail.com>2018-11-14 20:57:27 -0300
commitd9e91e56490adf4da82c7bcd771166715ae3e44f (patch)
tree64f587681160c2e718136437173309412440efb1
parent89379f02d98eeb70c8088c7dd15c102d871d82cc (diff)
parent571f3a015d2e0a0c74a75e4e666c565bdbe8baa4 (diff)
Update upstream source from tag 'upstream/1.0.0'
Update to upstream version '1.0.0' with Debian dir 8f82445e393bca0a549f3f8f0f7cb91fc3f1ec21
-rw-r--r--README.md57
-rw-r--r--data/meson.build3
-rw-r--r--data/wl-clipboard.1106
-rw-r--r--data/wl-copy.11
-rw-r--r--data/wl-paste.11
-rw-r--r--meson.build3
-rw-r--r--src/boilerplate.c210
-rw-r--r--src/boilerplate.h33
-rw-r--r--src/meson.build36
-rw-r--r--src/protocol/gtk-primary-selection.xml225
-rw-r--r--src/wl-copy.c286
-rw-r--r--src/wl-paste.c346
12 files changed, 1179 insertions, 128 deletions
diff --git a/README.md b/README.md
index 4673c1d..afa429e 100644
--- a/README.md
+++ b/README.md
@@ -21,31 +21,72 @@ $ wl-paste > clipboard.txt
# grep each pasted word in file source.c
$ for word in $(wl-paste); do grep $word source.c; done
+
+# copy the previous command
+$ wl-copy "!!"
+
+# replace the current selection with the list of types it's offered in
+$ wl-paste --list-types | wl-copy
```
+Although `wl-copy` and `wl-paste` are particularly optimized for plain text and
+other textual content formats, they fully support content of arbitrary MIME
+types. `wl-copy` automatically infers the type of the copied content by running
+`xdg-mime(1)` on it. `wl-paste` tries its best to pick a type to paste based on
+the list of offered MIME types and the extension of the file it's pasting into.
+If you're not satisfied with the type they pick or don't want to rely on this
+implicit type inference, you can explicitly specify the type to use with the
+`--type` option.
+
+# Options
+
+For `wl-copy`:
+
+* `-o`, `--paste-once` Only serve one paste request and then exit. Unless a clipboard manager specifically designed to prevent this is in use, this has the effect of clearing the clipboard after the first paste, which is useful for copying sensitive data such as passwords. Note that this may break pasting into some clients, in particular pasting into XWayland windows is known to break when this option is used.
+* `-f`, `--foreground` By default, `wl-copy` forks and serves data requests in the background; this option overrides that behavior, causing `wl-copy` to run in the foreground.
+* `-c`, `--clear` Instead of copying anything, clear the clipboard so that nothing is copied.
+
+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.
+
+For both:
+
+* `-p`, `--primary` Use the "primary" clipboard instead of the regular clipboard. This uses the private GTK+ primary selection protocol. See [the GNOME Wiki page on primary selection under Wayland](https://wiki.gnome.org/Initiatives/Wayland/PrimarySelection) for more details.
+* `-t mime/type`, `--type mime/type` Override the inferred MIME type for the content. For `wl-copy` this option controls which type `wl-copy` will offer the content as. For `wl-paste` it controls which of the offered types `wl-paste` will request the content in. In addition to specific MIME types such as _image/png_, `wl-paste` also accepts generic type names such as _text_ and _image_ which make it automatically pick some offered MIME type that matches the given generic name.
+
# Building
wl-clipboard is a simple Meson project, so building it is just:
```bash
+# clone
$ git clone https://github.com/bugaevc/wl-clipboard.git
$ cd wl-clipboard
+
+# build
$ meson build
$ cd build
$ ninja
+
+# install
+$ sudo ninja install
```
wl-clipboard only supports Linux (though patches to add BSD support are
-welcome!). You'll need `wayland-client` library & headers installed (try package
-named `wayland-devel` or `libwayland-dev`). wl-clipboard tries to use `xdg-mime`
-to infer content mime type, but will fall back to plain text if `xdg-mime` is
-unavailable.
+welcome!). The only manadatory dependency is the `wayland-client` library (try
+package named `wayland-devel` or `libwayland-dev`).
+
+Optional dependencies for building:
+* `wayland-scanner` for primary selection support using the bundled [gtk-primary-selection protocol](src/protocol/gtk-primary-selection.xml)
+* `wayland-protocols` (version 1.12 or later) for xdg-shell support (otherwise it won't run under compositors lacking `wl_shell` support, see [the issue #2](https://github.com/bugaevc/wl-clipboard/issues/2))
-If `wayland-scanner` and `wayland-protocols` (version 1.12 or later) are present
-at build time, wl-clipboard will be built with additional xdg-shell support;
-this may be helpful if your Wayland compositor does not support `wl_shell`.
+Optional dependencies for running:
+* `xdg-mime` for content type inference in `wl-copy` (try package named `xdg-utils`)
+* `/etc/mime.types` file for type inference in `wl-paste` (try package named `mime-support` or `mailcap`)
# License
-wl-clipboard is free sofrware, available under the GNU General Public License
+wl-clipboard is free software, available under the GNU General Public License
version 3 or later.
diff --git a/data/meson.build b/data/meson.build
new file mode 100644
index 0000000..add3440
--- /dev/null
+++ b/data/meson.build
@@ -0,0 +1,3 @@
+install_man('wl-copy.1')
+install_man('wl-paste.1')
+install_man('wl-clipboard.1') \ No newline at end of file
diff --git a/data/wl-clipboard.1 b/data/wl-clipboard.1
new file mode 100644
index 0000000..be5225b
--- /dev/null
+++ b/data/wl-clipboard.1
@@ -0,0 +1,106 @@
+.TH WL-CLIPBOARD 1 2018-10-03 wl-clipboard
+.SH NAME
+wl-clipboard \- Wayland copy and paste command line utiltites
+.SH SYNOPSIS
+.B wl-copy
+[\fB--primary\fR]
+[\fB--paste-once\fR]
+[\fB--foreground\fR]
+[\fB--clear\fR]
+[\fB--type \fImime/type\fR]
+[\fItext\fR...]
+.PP
+.B wl-paste
+[\fB--primary\fR]
+[\fB--no-newline\fR]
+[\fB--list-types\fR]
+[\fB--type \fImime/type\fR]
+.SH DESCRIPTION
+\fBwl-copy\fR copies the given \fItext\fR to the Wayland clipboard.
+If no \fItext\fR is given, \fBwl-copy\fR copies data from its standard input.
+.PP
+\fBwl-paste\fR pastes data from the Wayland clipboard to its standard output.
+.PP
+Although \fBwl-copy\fR and \fBwl-paste\fR are particularly optimized for plain
+text and other textual content formats, they fully support content of arbitrary
+MIME types. \fBwl-copy\fR automatically infers the type of the copied content by
+running \fBxdg-mime\fR(1) on it. \fBwl-paste\fR tries its best to pick a type to
+paste based on the list of offered MIME types and the extension of the file it's
+pasting into. If you're not satisfied with the type they pick or don't want to
+rely on this implicit type inference, you can explicitly specify the type to use
+with the \fB--type\fR option.
+.SH OPTIONS
+.TP
+\fB-p\fR, \fB--primary
+Use the "primary" clipboard instead of the regular clipboard. This uses the
+private GTK+ primary selection protocol. See
+.UR https://wiki.gnome.org/Initiatives/Wayland/PrimarySelection
+the GNOME Wiki page on primary selection under Wayland
+.UE
+for more details.
+.TP
+\fB-o\fR, \fB--paste-once
+Only serve one paste request and then exit. Unless a clipboard manager
+specifically designed to prevent this is in use, this has the effect of clearing
+the clipboard after the first paste, which is useful for copying sensitive data
+such as passwords. Note that this may break pasting into some clients, in
+particular pasting into XWayland windows is known to break when this option is
+used.
+.TP
+\fB-f\fR, \fB--foreground
+By default, \fBwl-copy\fR forks and serves data requests in the background; this
+option overrides that behavior, causing \fBwl-copy\fR to run in the foreground.
+.TP
+\fB-c\fR, \fB--clear
+Instead of copying anything, clear the clipboard so that nothing is copied.
+.TP
+\fB-n\fR, \fB--no-newline
+Do not append a newline character after the pasted clipboard content. This
+option is automatically enabled for non-text content types.
+.TP
+\fB-t\fI mime/type\fR, \fB--type\fI mime/type
+Override the automatically selected MIME type. For \fBwl-copy\fR this option
+controls which type \fBwl-copy\fR will offer the content as. For \fBwl-paste\fR
+it controls which of the offered types \fBwl-paste\fR will request the content
+in. In addition to specific MIME types such as \fIimage/png\fR, \fBwl-paste\fR
+also accepts generic type names such as \fItext\fR and \fIimage\fR which make it
+automatically pick some offered MIME type that matches the given generic name.
+.TP
+\fB-l\fR, \fB--list-types
+Instead of pasting the selection, output the list of MIME types it is offered
+in.
+.SH ENVIRONMENT
+.TP
+WAYLAND_DISPLAY
+Specifies what Wayland server \fBwl-copy\fR and \fBwl-paste\fR should connect
+to. This is the same environment variable that you pass to other Wayland
+clients, such as graphical aplications, that connect to this Wayland server. It
+is normally set up automatically by the graphical session and the Wayland
+compositor. See
+.BR wl_display_connect (3)
+for more details.
+.TP
+WAYLAND_DEBUG
+When set to \fB1\fR, causes the \fBwayland-client\fR(7) library to log every
+interaction \fBwl-copy\fR and \fBwl-paste\fR make with the Wayland compositor to
+stderr.
+.SH EXAMPLES
+$
+.BI wl-copy " Hello world!"
+.PP
+$
+.IB "ls ~/Downloads" " | wl-copy"
+.PP
+$
+.BI "wl-copy < " ~/Pictures/photo.png
+.PP
+$
+.B wl-copy \(dq!!\(dq
+.PP
+$
+.BI "wl-paste -n > " clipboard.txt
+.PP
+$
+.B wl-paste --list-types | wl-copy
+.SH SEE ALSO
+.BR xclip (1) \ No newline at end of file
diff --git a/data/wl-copy.1 b/data/wl-copy.1
new file mode 100644
index 0000000..d441642
--- /dev/null
+++ b/data/wl-copy.1
@@ -0,0 +1 @@
+.so man1/wl-clipboard.1 \ No newline at end of file
diff --git a/data/wl-paste.1 b/data/wl-paste.1
new file mode 100644
index 0000000..d441642
--- /dev/null
+++ b/data/wl-paste.1
@@ -0,0 +1 @@
+.so man1/wl-clipboard.1 \ No newline at end of file
diff --git a/meson.build b/meson.build
index 3eba534..29c669a 100644
--- a/meson.build
+++ b/meson.build
@@ -1,6 +1,7 @@
project('wl-clipboard', 'c',
- version: '0.1.0',
+ version: '1.0.0',
meson_version: '>= 0.44.0',
)
subdir('src')
+subdir('data')
diff --git a/src/boilerplate.c b/src/boilerplate.c
index aa02199..2fa1baf 100644
--- a/src/boilerplate.c
+++ b/src/boilerplate.c
@@ -38,7 +38,7 @@ void registry_global_handler
registry,
name,
&wl_seat_interface,
- 5
+ 1
);
} else if (strcmp(interface, "wl_compositor") == 0) {
compositor = wl_registry_bind(
@@ -72,6 +72,16 @@ void registry_global_handler
);
}
#endif
+#ifdef HAVE_GTK_PRIMARY_SELECTION
+ else if (strcmp(interface, "gtk_primary_selection_device_manager") == 0) {
+ primary_selection_device_manager = wl_registry_bind(
+ registry,
+ name,
+ &gtk_primary_selection_device_manager_interface,
+ 1
+ );
+ }
+#endif
}
void registry_global_remove_handler
@@ -81,13 +91,91 @@ void registry_global_remove_handler
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
+) {
+ 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 *seat,
+ uint32_t capabilities
+) {
+ if (capabilities & WL_SEAT_CAPABILITY_KEYBOARD) {
+ struct wl_keyboard *keyboard = wl_seat_get_keyboard(seat);
+ wl_keyboard_add_listener (keyboard, &keayboard_listener, NULL);
+ } else {
+ if (action_on_no_keyboard != NULL) {
+ action_on_no_keyboard();
+ }
+ }
+}
+
+const struct wl_seat_listener seat_listener = {
+ .capabilities = seat_capabilities_handler
+};
void shell_surface_ping
(
@@ -170,7 +258,7 @@ const struct xdg_wm_base_listener xdg_wm_base_listener = {
#endif
void init_wayland_globals() {
- display = wl_display_connect(NULL);
+ display = wl_display_connect(NULL);
if (display == NULL) {
bail("Failed to connect to a Wayland server");
}
@@ -195,7 +283,21 @@ void init_wayland_globals() {
bail("Missing a required global object");
}
- data_device = wl_data_device_manager_get_data_device(data_device_manager, seat);
+ wl_seat_add_listener (seat, &seat_listener, NULL);
+
+ data_device = wl_data_device_manager_get_data_device(
+ data_device_manager,
+ seat
+ );
+#ifdef HAVE_GTK_PRIMARY_SELECTION
+ if (primary_selection_device_manager != NULL) {
+ primary_selection_device =
+ gtk_primary_selection_device_manager_get_device(
+ primary_selection_device_manager,
+ seat
+ );
+ }
+#endif
}
void popup_tiny_invisible_surface() {
@@ -293,24 +395,25 @@ int get_serial() {
return global_serial;
}
+int mime_type_is_text(const char *mime_type) {
+ return str_has_prefix(mime_type, "text/")
+ || strcmp(mime_type, "TEXT") == 0
+ || strcmp(mime_type, "STRING") == 0
+ || strcmp(mime_type, "UTF8_STRING") == 0;
+}
+
+int str_has_prefix(const char *string, const char *prefix) {
+ size_t prefix_length = strlen(prefix);
+ return strncmp(string, prefix, prefix_length) == 0;
+}
+
char *path_for_fd(int fd) {
char fdpath[64];
snprintf(fdpath, sizeof(fdpath), "/proc/self/fd/%d", fd);
return realpath(fdpath, NULL);
}
-char *infer_mime_type_of_file(int fd) {
- char *file_path = path_for_fd(fd);
-
- // filter pipes out
- if (file_path == NULL) {
- return NULL;
- }
- if (file_path[0] != '/') {
- free(file_path);
- return NULL;
- }
-
+char *infer_mime_type_from_contents(const char *file_path) {
int pipefd[2];
pipe(pipefd);
if (fork() == 0) {
@@ -325,7 +428,6 @@ char *infer_mime_type_of_file(int fd) {
}
close(pipefd[1]);
- free(file_path);
int wstatus;
wait(&wstatus);
if (WIFEXITED(wstatus) && WEXITSTATUS(wstatus) == 0) {
@@ -335,7 +437,7 @@ char *infer_mime_type_of_file(int fd) {
close(pipefd[0]);
res[len] = 0;
- if (strncmp(res, "inode/", strlen("inode/")) == 0) {
+ if (str_has_prefix(res, "inode/")) {
free(res);
return NULL;
}
@@ -346,13 +448,62 @@ char *infer_mime_type_of_file(int fd) {
return NULL;
}
-char *dump_into_a_temp_file(int fd) {
+const char *get_file_extension(const char *file_path) {
+ const char *name = strrchr(file_path, '/');
+ if (name == NULL) {
+ name = file_path;
+ }
+ const char *ext = strrchr(name, '.');
+ if (ext == NULL) {
+ return NULL;
+ }
+ return ext + 1;
+}
+
+char *infer_mime_type_from_name(const char *file_path) {
+ const char *actual_ext = get_file_extension(file_path);
+ if (actual_ext == NULL) {
+ return NULL;
+ }
+
+ FILE *f = fopen("/etc/mime.types", "r");
+ if (f == NULL) {
+ return NULL;
+ }
+
+ for (char line[200]; fgets(line, sizeof(line), f) != NULL;) {
+ // skip comments and blank lines
+ if (line[0] == '#' || line[0] == '\n') {
+ continue;
+ }
+
+ // 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?
+ continue;
+ }
+ char *lineptr = line + consumed;
+ for (char ext[200]; sscanf(lineptr, "%s%n", ext, &consumed) == 1;) {
+ if (strcmp(ext, actual_ext) == 0) {
+ fclose(f);
+ return strdup(mime_type);
+ }
+ lineptr += consumed;
+ }
+ }
+ fclose(f);
+ return NULL;
+}
+
+char *dump_stdin_into_a_temp_file() {
char dirpath[] = "/tmp/wl-copy-buffer-XXXXXX";
if (mkdtemp(dirpath) != dirpath) {
perror("mkdtemp");
- bail("");
+ exit(1);
}
- char *original_path = path_for_fd(fd);
+ char *original_path = path_for_fd(STDIN_FILENO);
char *res_path = malloc(PATH_MAX + 1);
memcpy(res_path, dirpath, sizeof(dirpath));
@@ -366,14 +517,15 @@ char *dump_into_a_temp_file(int fd) {
}
if (fork() == 0) {
- char *src;
- if (original_path != NULL) {
- src = original_path;
- } else {
- src = "/dev/stdin";
+ FILE *res = fopen(res_path, "w");
+ if (res == NULL) {
+ perror("fopen");
+ exit(1);
}
- execlp("cp", "cp", src, res_path, NULL);
- perror("exec cp");
+ dup2(fileno(res), STDOUT_FILENO);
+ fclose(res);
+ execlp("cat", "cat", NULL);
+ perror("exec cat");
exit(1);
}
diff --git a/src/boilerplate.h b/src/boilerplate.h
index ca09d6b..621da1e 100644
--- a/src/boilerplate.h
+++ b/src/boilerplate.h
@@ -20,12 +20,14 @@
#include <wayland-client.h>
#include <stdio.h>
-#include <string.h> // strcmp
+#include <unistd.h>
+#include <getopt.h>
+#include <string.h>
+#include <ctype.h> // isupper
#include <fcntl.h> // open
#include <sys/stat.h> // open
#include <sys/types.h> // open
#include <stdlib.h> // exit
-#include <unistd.h> // execl, STDOUT_FILENO
#include <libgen.h> // basename
#include <sys/wait.h>
#include <sys/syscall.h> // syscall, SYS_memfd_create
@@ -35,8 +37,15 @@
# include "xdg-shell-client.h"
#endif
+#ifdef HAVE_GTK_PRIMARY_SELECTION
+# include "gtk-primary-selection.h"
+#endif
+
#define bail(message) do { fprintf(stderr, message "\n"); exit(1); } while (0)
+#define text_plain "text/plain"
+#define text_plain_utf8 "text/plain;charset=utf-8"
+
struct wl_display *display;
struct wl_data_device_manager *data_device_manager;
struct wl_seat *seat;
@@ -54,16 +63,30 @@ struct xdg_toplevel *xdg_toplevel;
struct wl_data_device *data_device;
+#ifdef HAVE_GTK_PRIMARY_SELECTION
+struct gtk_primary_selection_device_manager *primary_selection_device_manager;
+struct gtk_primary_selection_device *primary_selection_device;
+#endif
+
void init_wayland_globals(void);
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);
+
int get_serial(void);
-// free() return values when done
+int mime_type_is_text(const char *mime_type);
+int str_has_prefix(const char *string, const char *prefix);
+
+// functions below this line return owned strings,
+// free() their return values when done with them
+
char *path_for_fd(int fd);
-char *infer_mime_type_of_file(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
-char *dump_into_a_temp_file(int fd);
+char *dump_stdin_into_a_temp_file(void);
diff --git a/src/meson.build b/src/meson.build
index 06619b3..c0b0c67 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -4,9 +4,11 @@ wayland_scanner = find_program('wayland-scanner', required: false)
wayland_protocols = dependency('wayland-protocols', version: '>= 1.12', required: false)
have_xdg_shell = wayland_scanner.found() and wayland_protocols.found()
+have_gtk_primary_selection = wayland_scanner.found()
conf_data = configuration_data()
conf_data.set('HAVE_XDG_SHELL', have_xdg_shell)
+conf_data.set('HAVE_GTK_PRIMARY_SELECTION', have_gtk_primary_selection)
configure_file(output: 'config.h', configuration: conf_data)
if not have_xdg_shell
@@ -31,7 +33,39 @@ else
xdg_shell = static_library('xdg-shell', xdg_shell_c, xdg_shell_client_h)
endif
-boilerplate = static_library('wl-clipboard-boilerplate', 'boilerplate.c', dependencies: wayland, link_with: xdg_shell)
+if not have_gtk_primary_selection
+ warning('Building without primary selection support')
+ gtk_primary_selection = static_library('gtk-primary-selection')
+else
+ gtk_primary_selection_xml = join_paths('protocol', 'gtk-primary-selection.xml')
+
+ gtk_primary_selection_client_h = custom_target(
+ 'gtk-primary-selection client header',
+ input: gtk_primary_selection_xml,
+ output: 'gtk-primary-selection.h',
+ command: [wayland_scanner, 'client-header', '@INPUT@', '@OUTPUT@']
+ )
+
+ gtk_primary_selection_c = custom_target(
+ 'gtk-primary-selection code',
+ input: gtk_primary_selection_xml,
+ output: 'gtk-primary-selection.c',
+ command: [wayland_scanner, 'private-code', '@INPUT@', '@OUTPUT@']
+ )
+
+ gtk_primary_selection = static_library(
+ 'gtk-primary-selection',
+ gtk_primary_selection_c,
+ gtk_primary_selection_client_h
+ )
+endif
+
+boilerplate = static_library(
+ 'wl-clipboard-boilerplate',
+ 'boilerplate.c',
+ dependencies: wayland,
+ link_with: [xdg_shell, gtk_primary_selection]
+)
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) \ No newline at end of file
diff --git a/src/protocol/gtk-primary-selection.xml b/src/protocol/gtk-primary-selection.xml
new file mode 100644
index 0000000..02cab94
--- /dev/null
+++ b/src/protocol/gtk-primary-selection.xml
@@ -0,0 +1,225 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<protocol name="gtk_primary_selection">
+ <copyright>
+ Copyright © 2015, 2016 Red Hat
+
+ Permission is hereby granted, free of charge, to any person obtaining a
+ copy of this software and associated documentation files (the "Software"),
+ to deal in the Software without restriction, including without limitation
+ the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ and/or sell copies of the Software, and to permit persons to whom the
+ Software is furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice (including the next
+ paragraph) shall be included in all copies or substantial portions of the
+ Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ DEALINGS IN THE SOFTWARE.
+ </copyright>
+
+ <description summary="Primary selection protocol">
+ This protocol provides the ability to have a primary selection device to
+ match that of the X server. This primary selection is a shortcut to the
+ common clipboard selection, where text just needs to be selected in order
+ to allow copying it elsewhere. The de facto way to perform this action
+ is the middle mouse button, although it is not limited to this one.
+
+ Clients wishing to honor primary selection should create a primary
+ selection source and set it as the selection through
+ wp_primary_selection_device.set_selection whenever the text selection
+ changes. In order to minimize calls in pointer-driven text selection,
+ it should happen only once after the operation finished. Similarly,
+ a NULL source should be set when text is unselected.
+
+ wp_primary_selection_offer objects are first announced through the
+ wp_primary_selection_device.data_offer event. Immediately after this event,
+ the primary data offer will emit wp_primary_selection_offer.offer events
+ to let know of the mime types being offered.
+
+ When the primary selection changes, the client with the keyboard focus
+ will receive wp_primary_selection_device.selection events. Only the client
+ with the keyboard focus will receive such events with a non-NULL
+ wp_primary_selection_offer. Across keyboard focus changes, previously
+ focused clients will receive wp_primary_selection_device.events with a
+ NULL wp_primary_selection_offer.
+
+ In order to request the primary selection data, the client must pass
+ a recent serial pertaining to the press event that is triggering the
+ operation, if the compositor deems the serial valid and recent, the
+ wp_primary_selection_source.send event will happen in the other end
+ to let the transfer begin. The client owning the primary selection
+ should write the requested data, and close the file descriptor
+ immediately.
+
+ If the primary selection owner client disappeared during the transfer,
+ the client reading the data will receive a
+ wp_primary_selection_device.selection event with a NULL
+ wp_primary_selection_offer, the client should take this as a hint
+ to finish the reads related to the no longer existing offer.
+
+ The primary selection owner should be checking for errors during
+ writes, merely cancelling the ongoing transfer if any happened.
+ </description>
+
+ <interface name="gtk_primary_selection_device_manager" version="1">
+ <description summary="X primary selection emulation">
+ The primary selection device manager is a singleton global object that
+ provides access to the primary selection. It allows to create
+ wp_primary_selection_source objects, as well as retrieving the per-seat
+ wp_primary_selection_device objects.
+ </description>
+
+ <request name="create_source">
+ <description summary="create a new primary selection source">
+ Create a new primary selection source.
+ </description>
+ <arg name="id" type="new_id" interface="gtk_primary_selection_source"/>
+ </request>
+
+ <request name="get_device">
+ <description summary="create a new primary selection device">
+ Create a new data device for a given seat.
+ </description>
+ <arg name="id" type="new_id" interface="gtk_primary_selection_device"/>
+ <arg name="seat" type="object" interface="wl_seat"/>
+ </request>
+
+ <request name="destroy" type="destructor">
+ <description summary="destroy the primary selection device manager">
+ Destroy the primary selection device manager.
+ </description>
+ </request>
+ </interface>
+
+ <interface name="gtk_primary_selection_device" version="1">
+ <request name="set_selection">
+ <description summary="set the primary selection">
+ Replaces the current selection. The previous owner of the primary selection
+ will receive a wp_primary_selection_source.cancelled event.
+
+ To unset the selection, set the source to NULL.
+ </description>
+ <arg name="source" type="object" interface="gtk_primary_selection_source" allow-null="true"/>
+ <arg name="serial" type="uint" summary="serial of the event that triggered this request"/>
+ </request>
+
+ <event name="data_offer">
+ <description summary="introduce a new wp_primary_selection_offer">
+ Introduces a new wp_primary_selection_offer object that may be used
+ to receive the current primary selection. Immediately following this
+ event, the new wp_primary_selection_offer object will send
+ wp_primary_selection_offer.offer events to describe the offered mime
+ types.
+ </description>
+ <arg name="offer" type="new_id" interface="gtk_primary_selection_offer"/>
+ </event>
+
+ <event name="selection">
+ <description summary="advertise a new primary selection">
+ The wp_primary_selection_device.selection event is sent to notify the
+ client of a new primary selection. This event is sent after the
+ wp_primary_selection.data_offer event introducing this object, and after
+ the offer has announced its mimetypes through
+ wp_primary_selection_offer.offer.
+
+ The data_offer is valid until a new offer or NULL is received
+ or until the client loses keyboard focus. The client must destroy the
+ previous selection data_offer, if any, upon receiving this event.
+ </description>
+ <arg name="id" type="object" interface="gtk_primary_selection_offer" allow-null="true"/>
+ </event>
+
+ <request name="destroy" type="destructor">
+ <description summary="destroy the primary selection device">
+ Destroy the primary selection device.
+ </description>
+ </request>
+ </interface>
+
+ <interface name="gtk_primary_selection_offer" version="1">
+ <description summary="offer to transfer primary selection contents">
+ A wp_primary_selection_offer represents an offer to transfer the contents
+ of the primary selection clipboard to the client. Similar to
+ wl_data_offer, the offer also describes the mime types that the source
+ will transferthat the
+ data can be converted to and provides the mechanisms for transferring the
+ data directly to the client.
+ </description>
+
+ <request name="receive">
+ <description summary="request that the data is transferred">
+ To transfer the contents of the primary selection clipboard, the client
+ issues this request and indicates the mime type that it wants to
+ receive. The transfer happens through the passed file descriptor
+ (typically created with the pipe system call). The source client writes
+ the data in the mime type representation requested and then closes the
+ file descriptor.
+
+ The receiving client reads from the read end of the pipe until EOF and
+ closes its end, at which point the transfer is complete.
+ </description>
+ <arg name="mime_type" type="string"/>
+ <arg name="fd" type="fd"/>
+ </request>
+
+ <request name="destroy" type="destructor">
+ <description summary="destroy the primary selection offer">
+ Destroy the primary selection offer.
+ </description>
+ </request>
+
+ <event name="offer">
+ <description summary="advertise offered mime type">
+ Sent immediately after creating announcing the wp_primary_selection_offer
+ through wp_primary_selection_device.data_offer. One event is sent per
+ offered mime type.
+ </description>
+ <arg name="mime_type" type="string"/>
+ </event>
+ </interface>
+
+ <interface name="gtk_primary_selection_source" version="1">
+ <description summary="offer to replace the contents of the primary selection">
+ The source side of a wp_primary_selection_offer, it provides a way to
+ describe the offered data and respond to requests to transfer the
+ requested contents of the primary selection clipboard.
+ </description>
+
+ <request name="offer">
+ <description summary="add an offered mime type">
+ This request adds a mime type to the set of mime types advertised to
+ targets. Can be called several times to offer multiple types.
+ </description>
+ <arg name="mime_type" type="string"/>
+ </request>
+
+ <request name="destroy" type="destructor">
+ <description summary="destroy the primary selection source">
+ Destroy the primary selection source.
+ </description>
+ </request>
+
+ <event name="send">
+ <description summary="send the primary selection contents">
+ Request for the current primary selection contents from the client.
+ Send the specified mime type over the passed file descriptor, then
+ close it.
+ </description>
+ <arg name="mime_type" type="string"/>
+ <arg name="fd" type="fd"/>
+ </event>
+
+ <event name="cancelled">
+ <description summary="request for primary selection contents was canceled">
+ This primary selection source is no longer valid. The client should
+ clean up and destroy this primary selection source.
+ </description>
+ </event>
+ </interface>
+</protocol>
diff --git a/src/wl-copy.c b/src/wl-copy.c
index 78b9d82..24c6918 100644
--- a/src/wl-copy.c
+++ b/src/wl-copy.c
@@ -18,29 +18,21 @@
#include "boilerplate.h"
-const char **data_to_copy = NULL;
+char * const *data_to_copy = NULL;
char *temp_file_to_copy = NULL;
+int paste_once = 0;
-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
-) {
+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");
- const char **dataptr = data_to_copy;
+ if (f == NULL) {
+ perror("fdopen");
+ exit(1);
+ }
+ char * const *dataptr = data_to_copy;
for (int is_first = 1; *dataptr != NULL; dataptr++, is_first = 0) {
if (!is_first) {
fwrite(" ", 1, 1, f);
@@ -54,20 +46,19 @@ void data_source_send_handler
if (fork() == 0) {
dup2(fd, STDOUT_FILENO);
execlp("cat", "cat", temp_file_to_copy, NULL);
- // failed to execl
perror("exec cat");
exit(1);
}
close(fd);
wait(NULL);
}
+
+ if (paste_once) {
+ exit(0);
+ }
}
-void data_source_cancelled_handler
-(
- void *data,
- struct wl_data_source *data_source
-) {
+void do_cancel() {
// we're done!
if (temp_file_to_copy != NULL) {
execlp("rm", "rm", "-r", dirname(temp_file_to_copy), NULL);
@@ -78,59 +69,250 @@ void data_source_cancelled_handler
}
}
+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
};
+#ifdef HAVE_GTK_PRIMARY_SELECTION
+
+void primary_selection_source_send_handler
+(
+ void *data,
+ struct gtk_primary_selection_source *primary_selection_source,
+ const char *mime_type,
+ int fd
+) {
+ do_send(mime_type, fd);
+}
+
+void primary_selection_source_cancelled_handler
+(
+ void *data,
+ struct gtk_primary_selection_source *primary_selection_source
+) {
+ do_cancel();
+}
+
+const struct gtk_primary_selection_source_listener
+primary_selection_source_listener = {
+ .send = primary_selection_source_send_handler,
+ .cancelled = primary_selection_source_cancelled_handler
+};
+
+struct gtk_primary_selection_source *primary_selection_source;
+
+void do_set_primary_selection(uint32_t serial) {
+
+ gtk_primary_selection_device_set_selection(
+ primary_selection_device,
+ primary_selection_source,
+ serial
+ );
+
+ wl_display_roundtrip(display);
+ destroy_popup_surface();
+}
+
+void complain_about_missing_keyboard() {
+ bail("Setting primary selection is not supported without a keyboard");
+}
+
+#endif
+
+const char *plain_text_formats[] = {
+ text_plain,
+ text_plain_utf8,
+ "TEXT",
+ "STRING",
+ "UTF8_STRING",
+ NULL
+};
+
void offer_plain_text(struct wl_data_source *data_source) {
- wl_data_source_offer(data_source, "text/plain");
- wl_data_source_offer(data_source, "text/plain;charset=utf-8");
- wl_data_source_offer(data_source, "TEXT");
- wl_data_source_offer(data_source, "STRING");
- wl_data_source_offer(data_source, "UTF8_STRING");
+ for (const char **format = plain_text_formats; *format != NULL; format++) {
+ wl_data_source_offer(data_source, *format);
+ }
}
-int main(int argc, const char *argv[]) {
+#ifdef HAVE_GTK_PRIMARY_SELECTION
+void offer_plain_text_primary(
+ struct gtk_primary_selection_source *primary_selection_source
+) {
+ for (const char **format = plain_text_formats; *format != NULL; format++) {
+ gtk_primary_selection_source_offer(primary_selection_source, *format);
+ }
+}
+#endif
+
+int main(int argc, char * const argv[]) {
+ int stay_in_foreground = 0;
+ int clear = 0;
char *mime_type = NULL;
- if (argc > 1) {
- // copy our command-line args
- data_to_copy = argv + 1;
- } else {
- // copy stdin
- mime_type = infer_mime_type_of_file(STDIN_FILENO);
- temp_file_to_copy = dump_into_a_temp_file(STDIN_FILENO);
+ int primary = 0;
+
+ static struct option long_options[] = {
+ {"primary", no_argument, 0, 'p'},
+ {"paste-once", no_argument, 0, 'o'},
+ {"foreground", no_argument, 0, 'f'},
+ {"clear", no_argument, 0, 'c'},
+ {"type", required_argument, 0, 't'}
+ };
+ while (1) {
+ int option_index;
+ int c = getopt_long(argc, argv, "pofct:", long_options, &option_index);
+ if (c == -1) {
+ break;
+ }
+ if (c == 0) {
+ c = long_options[option_index].val;
+ }
+ switch (c) {
+ case 'p':
+ primary = 1;
+ break;
+ case 'o':
+ paste_once = 1;
+ break;
+ case 'f':
+ stay_in_foreground = 1;
+ break;
+ case 'c':
+ clear = 1;
+ break;
+ case 't':
+ mime_type = strdup(optarg);
+ break;
+ default:
+ // getopt has already printed an error message
+ exit(1);
+ }
}
init_wayland_globals();
- if (fork() != 0) {
- // exit in the parent, but leave the
- // child running in the background
- exit(0);
+ if (primary) {
+#ifdef HAVE_GTK_PRIMARY_SELECTION
+ if (primary_selection_device_manager == NULL) {
+ bail("Primary selection is not supported on this compositor");
+ }
+#else
+ bail("wl-clipboard was built without primary selection support");
+#endif
}
- struct wl_data_source *data_source = wl_data_device_manager_create_data_source(data_device_manager);
- wl_data_source_add_listener(data_source, &data_source_listener, NULL);
+ if (!clear) {
+ if (optind < argc) {
+ // copy our command-line args
+ data_to_copy = &argv[optind];
+ } else {
+ // copy stdin
+ temp_file_to_copy = dump_stdin_into_a_temp_file();
+ if (mime_type == NULL) {
+ mime_type = infer_mime_type_from_contents(temp_file_to_copy);
+ }
+ }
+ }
- if (mime_type != NULL) {
- if (strcmp(mime_type, "text/plain") == 0) {
- offer_plain_text(data_source);
+ if (!stay_in_foreground && !clear) {
+ if (fork() != 0) {
+ // exit in the parent, but leave the
+ // child running in the background
+ exit(0);
+ }
+ }
+
+ if (!primary) {
+ struct wl_data_source *data_source =
+ wl_data_device_manager_create_data_source(data_device_manager);
+ wl_data_source_add_listener(data_source, &data_source_listener, NULL);
+
+ if (mime_type != NULL) {
+ if (strcmp(mime_type, text_plain) == 0) {
+ offer_plain_text(data_source);
+ } else {
+ wl_data_source_offer(data_source, mime_type);
+ if (mime_type_is_text(mime_type)) {
+ // offer plain text as well
+ offer_plain_text(data_source);
+ }
+ }
+ free(mime_type);
} else {
- wl_data_source_offer(data_source, mime_type);
+ offer_plain_text(data_source);
}
- free(mime_type);
+
+ wl_data_device_set_selection(data_device, data_source, get_serial());
} else {
- offer_plain_text(data_source);
- }
+#ifdef HAVE_GTK_PRIMARY_SELECTION
+ primary_selection_source =
+ gtk_primary_selection_device_manager_create_source(
+ primary_selection_device_manager
+ );
+ gtk_primary_selection_source_add_listener(
+ primary_selection_source,
+ &primary_selection_source_listener,
+ NULL
+ );
- wl_data_device_set_selection(data_device, data_source, get_serial());
+ if (mime_type != NULL) {
+ if (strcmp(mime_type, text_plain) == 0) {
+ offer_plain_text_primary(primary_selection_source);
+ } else {
+ gtk_primary_selection_source_offer(
+ primary_selection_source,
+ mime_type
+ );
+ if (mime_type_is_text(mime_type)) {
+ // offer plain text as well
+ offer_plain_text_primary(primary_selection_source);
+ }
+ }
+ free(mime_type);
+ } else {
+ offer_plain_text_primary(primary_selection_source);
+ }
- while (1) {
- wl_display_dispatch(display);
+ action_on_popup_surface_getting_focus = do_set_primary_selection;
+ action_on_no_keyboard = complain_about_missing_keyboard;
+ popup_tiny_invisible_surface();
+#endif
+ }
+
+ if (clear) {
+ wl_display_roundtrip(display);
+ exit(0);
}
- bail("Unreachable");
+ while (wl_display_dispatch(display) >= 0);
+
+ perror("wl_display_dispatch");
+ return 1;
}
diff --git a/src/wl-paste.c b/src/wl-paste.c
index dee0b35..08403a9 100644
--- a/src/wl-paste.c
+++ b/src/wl-paste.c
@@ -18,14 +18,195 @@
#include "boilerplate.h"
-char *mime_type;
+struct {
+ char *explicit_type;
+ char *inferred_type;
+ int no_newline;
+ int list_types;
+} options;
+
+struct {
+ 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;
+
+void do_paste(int pipefd[2]) {
+ destroy_popup_surface();
+
+ wl_display_roundtrip(display);
+
+ if (fork() == 0) {
+ dup2(pipefd[0], STDIN_FILENO);
+ close(pipefd[0]);
+ close(pipefd[1]);
+ execlp("cat", "cat", NULL);
+ perror("exec cat");
+ exit(1);
+ }
+ close(pipefd[0]);
+ close(pipefd[1]);
+ wait(NULL);
+ if (!options.no_newline) {
+ write(STDOUT_FILENO, "\n", 1);
+ }
+ exit(0);
+}
+
+void do_process_offer(const char *offered_type) {
+ if (options.list_types) {
+ printf("%s\n", offered_type);
+ } else {
+ if (
+ options.explicit_type != NULL &&
+ strcmp(offered_type, options.explicit_type) == 0
+ ) {
+ available_types.explicit_available = 1;
+ }
+ if (
+ options.inferred_type != NULL &&
+ strcmp(offered_type, options.inferred_type) == 0
+ ) {
+ available_types.inferred_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)
+ ) {
+ available_types.any_text = strdup(offered_type);
+ }
+ if (available_types.any == NULL) {
+ available_types.any = strdup(offered_type);
+ }
+ if (
+ options.explicit_type != NULL &&
+ available_types.having_explicit_as_prefix == NULL &&
+ str_has_prefix(offered_type, options.explicit_type)
+ ) {
+ available_types.having_explicit_as_prefix = strdup(offered_type);
+ }
+ }
+}
+
+#define try_explicit \
+if (available_types.explicit_available) \
+ return options.explicit_type
+
+#define try_inferred \
+if (available_types.inferred_available) \
+ return options.inferred_type
+
+#define try_text_plain_utf8 \
+if (available_types.plain_text_utf8_available) \
+ return text_plain_utf8
+
+#define try_text_plain \
+if (available_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
+
+#define try_any_text \
+if (available_types.any_text != NULL) \
+ return available_types.any_text
+
+#define try_any \
+if (available_types.any != NULL) \
+ return available_types.any
+
+const char *mime_type_to_request_inner() {
+ if (options.explicit_type != NULL) {
+ if (strcmp(options.explicit_type, "text") == 0) {
+ try_text_plain_utf8;
+ try_text_plain;
+ try_any_text;
+ } else if (strchr(options.explicit_type, '/') != NULL) {
+ try_explicit;
+ } else if (isupper(options.explicit_type[0])) {
+ try_explicit;
+ } else {
+ try_explicit;
+ try_prefixed;
+ }
+ } else {
+ // no mime type requested explicitly, try to guess
+ if (options.inferred_type == NULL) {
+ try_text_plain_utf8;
+ try_text_plain;
+ try_any_text;
+ try_any;
+ } else if (mime_type_is_text(options.inferred_type)) {
+ try_inferred;
+ try_text_plain_utf8;
+ try_text_plain;
+ try_any_text;
+ } else {
+ try_inferred;
+ }
+ }
+ bail("No suitable type of content copied");
+}
+
+#undef try_explicit
+#undef try_inferred
+#undef try_text_plain_utf8
+#undef try_text_plain
+#undef try_prefixed
+#undef try_any_text
+#undef try_any
+
+const char *mime_type_to_request() {
+ const char *res = mime_type_to_request_inner();
+ // never append a newline character to binary content
+ if (!mime_type_is_text(res)) {
+ options.no_newline = 1;
+ }
+ return res;
+}
+
+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);
+}
+
+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
(
@@ -37,33 +218,17 @@ void data_device_selection
bail("No selection");
}
- int pipefd[2];
- pipe(pipefd);
-
- if (mime_type != NULL) {
- wl_data_offer_receive(data_offer, mime_type, pipefd[1]);
- free(mime_type);
- } else {
- wl_data_offer_receive(data_offer, "text/plain", pipefd[1]);
+ if (options.list_types) {
+ exit(0);
}
- destroy_popup_surface();
+ int pipefd[2];
+ pipe(pipefd);
- wl_display_roundtrip(display);
+ wl_data_offer_receive(data_offer, mime_type_to_request(), pipefd[1]);
+ free_types();
- if (fork() == 0) {
- dup2(pipefd[0], STDIN_FILENO);
- close(pipefd[0]);
- close(pipefd[1]);
- execl("/bin/cat", "cat", NULL);
- // failed to execl
- perror("exec /bin/cat");
- exit(1);
- }
- close(pipefd[0]);
- close(pipefd[1]);
- wait(NULL);
- exit(0);
+ do_paste(pipefd);
}
const struct wl_data_device_listener data_device_listener = {
@@ -71,23 +236,140 @@ const struct wl_data_device_listener data_device_listener = {
.selection = data_device_selection
};
-int main(int argc, const char *argv[]) {
+#ifdef HAVE_GTK_PRIMARY_SELECTION
+
+void primary_selection_offer_offer
+(
+ void *data,
+ struct gtk_primary_selection_offer *primary_selection_offer,
+ const char *offered_mime_type
+) {
+ do_process_offer(offered_mime_type);
+}
+
+const struct gtk_primary_selection_offer_listener
+primary_selection_offer_listener = {
+ .offer = primary_selection_offer_offer
+};
+
+void primary_selection_device_data_offer
+(
+ void *data,
+ struct gtk_primary_selection_device *primary_selection_device,
+ struct gtk_primary_selection_offer *primary_selection_offer
+) {
+ gtk_primary_selection_offer_add_listener(
+ primary_selection_offer,
+ &primary_selection_offer_listener,
+ NULL
+ );
+}
+
+void primary_selection_device_selection
+(
+ void *data,
+ struct gtk_primary_selection_device *primary_selection_device,
+ struct gtk_primary_selection_offer *primary_selection_offer
+) {
+ if (primary_selection_offer == NULL) {
+ bail("No selection");
+ }
+
+ if (options.list_types) {
+ exit(0);
+ }
+
+ int pipefd[2];
+ pipe(pipefd);
+
+ gtk_primary_selection_offer_receive(
+ primary_selection_offer,
+ mime_type_to_request(),
+ pipefd[1]
+ );
+ free_types();
+
+ do_paste(pipefd);
+}
+
+const struct gtk_primary_selection_device_listener
+primary_selection_device_listener = {
+ .data_offer = primary_selection_device_data_offer,
+ .selection = primary_selection_device_selection
+};
+
+#endif
+
+int main(int argc, char * const argv[]) {
- mime_type = infer_mime_type_of_file(STDOUT_FILENO);
+ int primary = 0;
+
+ static struct option long_options[] = {
+ {"primary", no_argument, 0, 'p'},
+ {"no-newline", no_argument, 0, 'n'},
+ {"list-types", no_argument, 0, 'l'},
+ {"type", required_argument, 0, 't'}
+ };
+ while (1) {
+ int option_index;
+ int c = getopt_long(argc, argv, "pnlt:", long_options, &option_index);
+ if (c == -1) {
+ break;
+ }
+ if (c == 0) {
+ c = long_options[option_index].val;
+ }
+ switch (c) {
+ case 'p':
+ primary = 1;
+ break;
+ case 'n':
+ options.no_newline = 1;
+ break;
+ case 'l':
+ options.list_types = 1;
+ break;
+ case 't':
+ options.explicit_type = strdup(optarg);
+ break;
+ default:
+ // getopt has already printed an error message
+ exit(1);
+ }
+ }
+
+ 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();
- wl_data_device_add_listener(data_device, &data_device_listener, NULL);
+ if (!primary) {
+ wl_data_device_add_listener(data_device, &data_device_listener, NULL);
+ } else {
+#ifdef HAVE_GTK_PRIMARY_SELECTION
+ if (primary_selection_device_manager == NULL) {
+ bail("Primary selection is not supported on this compositor");
+ }
+ gtk_primary_selection_device_add_listener(
+ primary_selection_device,
+ &primary_selection_device_listener,
+ NULL
+ );
+#else
+ bail("wl-clipboard was built without primary selection support");
+#endif
+ }
// HACK:
// pop up a tiny invisible surface to get the keyboard focus,
// otherwise we won't be notified of the selection
popup_tiny_invisible_surface();
- while (1) {
- wl_display_dispatch(display);
- }
+ while (wl_display_dispatch(display) >= 0);
- // never reached
+ perror("wl_display_dispatch");
return 1;
}