diff --git a/BUILDING.md b/BUILDING.md new file mode 100644 index 0000000..0222fae --- /dev/null +++ b/BUILDING.md @@ -0,0 +1,63 @@ +# 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 setup build +$ cd build +$ ninja + +# Install +$ sudo meson install +``` + +wl-clipboard supports Linux and BSD systems, and is also known to work on +Mac OS X and GNU Hurd. The only mandatory dependency is the `wayland-client` +library (try package named `wayland-devel` or `libwayland-dev`). + +Optional (but highly recommended) dependencies for building: +* `wayland-scanner` +* `wayland-protocols` (version 1.12 or later) + +If these are found during configuration, wl-clipboard gets built with +additional protocols support, which enables features such as primary selection +support and `--watch` mode. If they are not found, Meson can optionally build +them on the spot as subprojects. + +Note that many compositors have dropped support for the `wl_shell` interface, +which means wl-clipboard will not work under them unless built with both +`wayland-scanner` and `wayland-protocols`. For this reason, you have to +explicitly opt into allowing building without these dependencies by specifying +`-D protocols=auto` (or `-D protocols=disabled`) when configuring with Meson. + +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`) +* `/usr/share/mime/globs2` file for for type inference in `wl-paste` (try + packaged named `shared-mime-info`) + +If you're packaging wl-clipboard for a distribution, please consider making +packages providing `xdg-mime`, `/etc/mime.types`, and `/usr/share/mime/globs2` +*weak* dependencies of the package providing wl-clipboard, meaning ones that +get installed along with wl-clipboard by default, but are not strictly required +by it. + +A package definition for wl-clipboard might therefore contain something along +the following lines: + +``` +Package: wl-clipboard +Build-Depends: + gcc, meson, ninja, + wayland-devel, wayland-scanner, + wayland-protocols >= 1.12 +Recommends: + xdg-utils, mime-support, shared-mime-info +``` diff --git a/README.md b/README.md index 915db62..b0ebd16 100644 --- a/README.md +++ b/README.md @@ -4,96 +4,35 @@ This project implements two command-line Wayland clipboard utilities, `wl-copy` and `wl-paste`, that let you easily copy data between the clipboard and Unix pipes, sockets, files and so on. -Usage is as simple as: - ```bash -# copy a simple text message +# Copy a simple text message: $ wl-copy Hello world! -# copy the list of files in Downloads +# Copy the list of files in ~/Downloads: $ ls ~/Downloads | wl-copy -# copy an image file +# Copy an image: $ wl-copy < ~/Pictures/photo.png -# paste to a file -$ 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 +# 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 +# Paste to a file: +$ wl-paste > clipboard.txt + +# Sort clipboard contents: +$ wl-paste | sort | wl-copy + +# Upload clipboard contents to a pastebin on each change: +$ wl-paste --watch nc paste.example.org 5555 ``` -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. +Please see the wl-clipboard(1) man page for more details. -# Options +# Installing -For `wl-copy`: - -* `-n`, `--trim-newline` Do not copy the trailing newline character if it is present in the input file. -* `-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 and when using the `--watch` mode. -* `-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: - -* `-p`, `--primary` Use the "primary" clipboard instead of the regular clipboard. -* `-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. -* `-s seat-name`, `--seat seat-name` Specify which seat `wl-copy` and `wl-paste` should work with. Wayland natively supports multi-seat configurations where each seat gets its own mouse pointer, keyboard focus, and among other things its own separate clipboard. The name of the default seat is likely _default_ or _seat0_, and additional seat names normally come form `udev(7)` property `ENV{WL_SEAT}`. You can view the list of the currently available seats as advertised by the compositor using the `weston-info(1)` tool. If you don't specify the seat name explicitly, `wl-copy` and `wl-paste` will pick a seat arbitrarily. If you are using a single-seat system, there is little reason to use this option. -* `-v`, `--version` Display the version of wl-clipboard and some short info about its license. -* `-h`, `--help` Display a short help message listing the available options. - -# 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 supports Linux and BSD systems, and is also known to work on -Mac OS X and GNU Hurd. The only mandatory dependency is the `wayland-client` -library (try package named `wayland-devel` or `libwayland-dev`). - -Optional dependencies for building: -* `wayland-scanner` -* `wayland-protocols` (version 1.12 or later) - -If these are found during configuration, wl-clipboard gets built with -additional protocols support, which enables features such as primary selection -support and `--watch` mode. - -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`) +wl-clipboard is likely available in your favorite Linux or BSD distro. For +building from source, see [BUILDING.md](BUILDING.md). # License @@ -102,5 +41,8 @@ version 3 or later. # Related projects -* [wl-clipboard-x11](https://github.com/brunelli/wl-clipboard-x11): A wrapper to use wl-clipboard as a drop-in replacement to X11 clipboard tools. -* [wl-clipboard-rs](https://github.com/YaLTeR/wl-clipboard-rs): A Rust crate (library) for working with the Wayland clipboard which includes a reimplementation of `wl-copy` and `wl-paste`. +* [wl-clipboard-x11](https://github.com/brunelli/wl-clipboard-x11): A wrapper to + use wl-clipboard as a drop-in replacement to X11 clipboard tools. +* [wl-clipboard-rs](https://github.com/YaLTeR/wl-clipboard-rs): A Rust crate + (library) for working with the Wayland clipboard which includes a + reimplementation of `wl-copy` and `wl-paste`. diff --git a/completions/fish/meson.build b/completions/fish/meson.build index b0bc411..cb1280c 100644 --- a/completions/fish/meson.build +++ b/completions/fish/meson.build @@ -2,7 +2,11 @@ fish_completion_dir = get_option('fishcompletiondir') fish = dependency('fish', required : false) if fish_completion_dir == '' and fish.found() - fish_completion_dir = fish.get_pkgconfig_variable('completionsdir') + if meson.version().version_compare('>= 0.58') + fish_completion_dir = fish.get_variable('completionsdir') + else + fish_completion_dir = fish.get_pkgconfig_variable('completionsdir') + endif elif fish_completion_dir == '' # Fish does not look in /usr/local/, which is the default prefix, # so we cannot use get_option("datadir"). Instead, they recommend diff --git a/completions/fish/wl-paste.fish b/completions/fish/wl-paste.fish index 2842019..04df3a4 100644 --- a/completions/fish/wl-paste.fish +++ b/completions/fish/wl-paste.fish @@ -22,7 +22,7 @@ function __wl_paste_types --description 'Print types with context' # Note fish does not handle passing unset variables # to commands well, thus setting clip to "--primary" and passing - # that to wl-paste wont work, so if statements are used instead + # that to wl-paste won't work, so if statements are used instead if test -n "$seat"; and test -n "$clip" wl-paste 2>/dev/null --seat "$seat" -p -l else if test -n "$seat" diff --git a/completions/zsh/_wl-copy b/completions/zsh/_wl-copy index 5fd9bd7..197a5b0 100644 --- a/completions/zsh/_wl-copy +++ b/completions/zsh/_wl-copy @@ -22,7 +22,7 @@ __all_seats() { fi if [[ -z $seats ]]; then - # seat0 is always a vaild seat and covers most cases, so its a good fallback. + # seat0 is always a valid seat and covers most cases, so it's a good fallback. compadd "$@" - seat0 else compadd "$@" -a seats diff --git a/data/wl-clipboard.1 b/data/wl-clipboard.1 index 93feed9..4243a91 100644 --- a/data/wl-clipboard.1 +++ b/data/wl-clipboard.1 @@ -1,24 +1,17 @@ -.TH WL-CLIPBOARD 1 2019-09-16 wl-clipboard +.TH WL-CLIPBOARD 1 2026-08-06 wl-clipboard .SH NAME wl-clipboard \- Wayland copy and paste command line utilities .SH SYNOPSIS .B wl-copy -[\fB--primary\fR] -[\fB--trim-newline\fR] -[\fB--paste-once\fR] -[\fB--foreground\fR] -[\fB--clear\fR] -[\fB--type \fImime/type\fR] -[\fB--seat \fIseat-name\fR] +[\fB\-\-primary\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] -[\fB--seat \fIseat-name\fR] -[\fB--watch \fIcommand\fR...\] +[\fB\-\-primary\fR] +[\fB\-\-type \fImime/type\fR] +.PP +Only the most useful options are listed here; see below for the full list. .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. @@ -32,36 +25,43 @@ 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. +with the \fB\-\-type\fR option. .SH OPTIONS +To parse options, wl-clipboard uses the +.BR getopt (3) +library routines, whose features depend on the C library in use. In particular, +it may be possible to specify \fB\-\-\fR as an argument on its own to prevent +any further arguments from getting parsed as options (which lets you copy text +containing words that start with the \fB-\fR sign), and to shorten long options +to their unambiguous prefixes. .TP -\fB-p\fR, \fB--primary +\fB\-p\fR, \fB\-\-primary Use the "primary" clipboard instead of the regular clipboard. .TP -\fB-o\fR, \fB--paste-once +\fB\-o\fR, \fB\-\-paste-once\fR (for \fBwl-copy\fR) 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. +such as passwords. Note that this may break pasting into some clients that +expect to be able to paste multiple times, in particular pasting into XWayland +windows is known to break when this option is used. .TP -\fB-f\fR, \fB--foreground +\fB\-f\fR, \fB\-\-foreground\fR (for \fBwl-copy\fR) 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 +\fB\-c\fR, \fB\-\-clear\fR (for \fBwl-copy\fR) Instead of copying anything, clear the clipboard so that nothing is copied. .TP -\fB-n\fR, \fB--trim-newline +\fB\-n\fR, \fB\-\-trim-newline\fR (for \fBwl-copy\fR) Do not copy the trailing newline character if it is present in the input file. .TP -\fB-n\fR, \fB--no-newline +\fB\-n\fR, \fB\-\-no-newline\fR (for \fBwl-paste\fR) Do not append a newline character after the pasted clipboard content. This option is automatically enabled for non-text content types and when using the -\fB--watch\fR mode. +\fB\-\-watch\fR mode. .TP -\fB-t\fI mime/type\fR, \fB--type\fI mime/type +\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 @@ -69,7 +69,7 @@ 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-s\fI seat-name\fR, \fB--seat\fI seat-name +\fB\-s\fI seat-name\fR, \fB\-\-seat\fI seat-name Specify which seat \fBwl-copy\fR and \fBwl-paste\fR should work with. Wayland natively supports multi-seat configurations where each seat gets its own mouse pointer, keyboard focus, and among other things its own separate clipboard. The @@ -83,24 +83,33 @@ tool. If you don't specify the seat name explicitly, \fBwl-copy\fR and \fBwl-paste\fR will pick a seat arbitrarily. If you are using a single-seat system, there is little reason to use this option. .TP -\fB-l\fR, \fB--list-types +\fB\-l\fR, \fB\-\-list-types\fR (for \fBwl-paste\fR) Instead of pasting the selection, output the list of MIME types it is offered in. .TP -\fB-w\fI command\fR..., \fB--watch \fIcommand\fR... +\fB\-w\fI command\fR..., \fB\-\-watch \fIcommand\fR... (for \fBwl-paste\fR) 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. +\fBwl-paste\fR also sets the \fBCLIPBOARD_STATE\fR and \fBCLIPBOARD_TYPE\fR +variables in the environment of the spawned processes (see below). +.IP This mode requires a compositor that supports the wlroots data-control protocol. .TP -\fB-v\fR, \fB--version +\fB\-\-sensitive\fR (for \fBwl-copy\fR) +Hint that the data being copied contains passwords, keys, or other sensitive +content. Some clipboard managers may react by not persisting the copied data in +the clipboard history. This corresponds to \fBCLIPBOARD_STATE=sensitive\fR (see +below). +.TP +\fB\-v\fR, \fB\-\-version Display the version of wl-clipboard and some short info about its license. .TP -\fB-h\fR, \fB--help +\fB\-h\fR, \fB\-\-help Display a short help message listing the available options. .SH ENVIRONMENT .TP -WAYLAND_DISPLAY +.B 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 applications, that connect to this Wayland server. It @@ -109,34 +118,144 @@ compositor. See .BR wl_display_connect (3) for more details. .TP -WAYLAND_DEBUG +.B 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. +.TP +.B CLIPBOARD_STATE +Set by \fBwl-paste\fR for the spawned command in \fB\-\-watch\fR mode. Currently +the following possible values are \fIdefined\fR: +.RS +.TP +CLIPBOARD_STATE=\fBdata +Indicates that the clipboard contains data that the spawned command can read +from its standard input. This is the most common case. +.TP +CLIPBOARD_STATE=\fBnil +Indicates that the clipboard is empty. In this case the spawned command's +standard input will be attached to \fI/dev/null\fR. Note that this is subtly +different from the clipboard containing zero-sized data (which can be achieved, +for instance, by running \fBwl-copy < /dev/null\fR). +.TP +CLIPBOARD_STATE=\fBclear +Indicates that the clipboard is empty because of an explicit clear request, such +as after running \fBwl-copy --clear\fR. As for \fBnil\fR, the command's standard +input will be attached to \fI/dev/null\fR. +.TP +CLIPBOARD_STATE=\fBsensitive +Indicates that the clipboard contains sensitive data such as a password or a +key. It is probably best to avoid visibly displaying or persistently saving +clipboard contents. +.RE +.IP +Any client programs implementing the \fBCLIPBOARD_STATE\fR protocol are +encouraged to implement proper support for all the values listed above, as well +as to fall back to some sensible behavior if \fBCLIPBOARD_STATE\fR is unset or +set to some unrecognized value (this is to leave the design space open for +future extensions). However, the currently existing Wayland clipboard protocols +don't let wl-clipboard identify the cases where \fBclear\fR and \fBsensitive\fR +values should be set. For this reason, currently, wl-clipboard never actually +sets \fBCLIPBOARD_STATE\fR to \fBclear\fR, and only sets it to \fBsensitive\fR +when it encounters \fBx-kde-passwordManagerHint\fR among the MIME types. +.IP +The \fBCLIPBOARD_STATE\fR protocol was intentionally designed to not be specific +to either wl-clipboard or Wayland; in fact, other clipboard tools are encouraged +to implement the same protocol. Currently, the SerenityOS +.BR paste (1) +utility is known to implement the same \fBCLIPBOARD_STATE\fR protocol. +.TP +.B CLIPBOARD_TYPE +Set by \fBwl-paste\fR for the spawned command in \fB\-\-watch\fR mode to the +MIME type of the data on its standard input, such as \fBimage/png\fR. Left +unset when there is no data to read, such as when the clipboard is empty. +.SH FILES +.TP +\fI/etc/mime.types\fR, \fI/usr/share/mime/globs2\fR +If present, read by \fBwl-paste\fR to infer the MIME type to paste in based on +the file name extension of its standard output. +.SH BUGS +Unless the Wayland compositor implements the wlroots data-control protocol, +wl-clipboard has to resort to using a hack to access the clipboard: it will +briefly pop up a tiny transparent surface (window). On some desktop +environments (in particular when using tiling window managers), this can cause +visual issues such as brief flashing. In some cases the Wayland compositor +doesn't give focus to the popup surface, which prevents wl-clipboard from +accessing the clipboard and manifests as a hang. +.PP +There is currently no way to copy data in multiple MIME types, such as multiple +image formats, at the same time. +.br +See +.nh +. +.hy +.PP +wl-clipboard is not always able to detect that a MIME type is textual, which may +break pasting into clients that expect textual formats, not +\fIapplication/something\fR. The workaround, same as for all format inference +issues, is to specify the desired MIME type explicitly, such as +\fBwl-copy \-\-type\fI text/plain\fR. +.PP +\fBwl-copy \-\-clear\fR and \fBwl-copy \-\-paste-once\fR don't always interact +well with clipboard managers that are overeager to preserve clipboard contents. +.PP +Applications written using the GTK 3 toolkit copy text with \(dq\er\en\(dq (also +known as CR LF) line endings, which takes most other software by surprise. +wl-clipboard does nothing to rectify this. The recommended workaround is piping +\fBwl-paste\fR output through +.BR dos2unix (1) +when pasting from a GTK 3 application. +.br +See +.nh +. +.hy +.PP +When trying to paste content copied with \fBwl-copy\fR, \fBwl-copy\fR does not +check whether the requested MIME type is among those it has offered, and always +provides the same data in response. .SH EXAMPLES +.TP +Copy a simple text message: $ .BI wl-copy " Hello world!" -.PP +.TP +Copy a message starting with dashes: +.BI "wl-copy -- " --long +.TP +Copy the list of files in \fI~/Downloads\fR: $ .IB "ls ~/Downloads" " | wl-copy" -.PP +.TP +Copy an image: $ .BI "wl-copy < " ~/Pictures/photo.png -.PP +.TP +Copy the previous command: $ .B wl-copy \(dq!!\(dq -.PP +.TP +Paste to a file, without appending a newline: $ -.BI "wl-paste -n > " clipboard.txt -.PP +.BI "wl-paste \-n > " clipboard.txt +.TP +Sort clipboard contents: $ -.B wl-paste --list-types | wl-copy +.B wl-paste | sort | wl-copy +.TP +Upload clipboard contents to a pastebin on each change: +$ +.BI "wl-paste --watch nc " "paste.example.org 5555 .SH AUTHOR Written by Sergey Bugaev. .SH REPORTING BUGS Report wl-clipboard bugs to +.br +Please make sure to mention which Wayland compositor you are using, and attach +\fBWAYLAND_DEBUG=1\fR debugging logs of wl-clipboard. .SH COPYRIGHT -Copyright \(co 2019 Sergey Bugaev. +Copyright \(co 2018-2026 Sergey Bugaev. License GPLv3+: GNU GPL version 3 or later . .br This is free software: you are free to change and redistribute it. diff --git a/meson.build b/meson.build index ca762a3..3109a50 100644 --- a/meson.build +++ b/meson.build @@ -1,7 +1,7 @@ project('wl-clipboard', 'c', - version: '2.0.0', - license: 'GPL3+', - meson_version: '>= 0.44.0', + version: '2.3.0', + license: 'GPL-3.0-or-later', + meson_version: '>= 0.47.0', default_options: 'c_std=gnu99' ) diff --git a/meson_options.txt b/meson_options.txt index 32b24ed..b6e621c 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,3 +1,6 @@ +option('protocols', + type: 'feature', + value: 'enabled') option('zshcompletiondir', type: 'string', value: '', diff --git a/src/includes/selection-protocols.h b/src/includes/selection-protocols.h index 32e74a7..f01070b 100644 --- a/src/includes/selection-protocols.h +++ b/src/includes/selection-protocols.h @@ -1,6 +1,6 @@ /* wl-clipboard * - * Copyright © 2019 Sergey Bugaev + * Copyright © 2018-2026 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 @@ -35,4 +35,8 @@ # include "wlr-data-control.h" #endif +#ifdef HAVE_EXT_DATA_CONTROL +# include "ext-data-control.h" +#endif + #endif /* INCLUDES_SELECTION_PROTOCOLS_H */ diff --git a/src/includes/shell-protocols.h b/src/includes/shell-protocols.h index feb39f5..5d700d1 100644 --- a/src/includes/shell-protocols.h +++ b/src/includes/shell-protocols.h @@ -1,6 +1,6 @@ /* wl-clipboard * - * Copyright © 2019 Sergey Bugaev + * Copyright © 2018-2026 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 @@ -27,4 +27,13 @@ # include "xdg-shell.h" #endif +#ifdef HAVE_GTK_SHELL +# include "gtk-shell.h" +#endif + +/* Not strictly speaking a shell */ +#ifdef HAVE_XDG_ACTIVATION +# include "xdg-activation.h" +#endif + #endif /* INCLUDES_SHELL_PROTOCOLS_H */ diff --git a/src/meson.build b/src/meson.build index 2ea6bb1..e3b9bcf 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,25 +1,7 @@ -wayland = dependency('wayland-client') - -wayland_scanner = find_program('wayland-scanner', required: false, native: true) -wayland_protocols = dependency('wayland-protocols', version: '>= 1.12', required: false) - -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_gtk_primary_selection = true - have_wlr_data_control = true - - if wayland.version().version_compare('>= 1.15') - scanner_code_command = 'private-code' - else - scanner_code_command = 'code' - endif -else - have_xdg_shell = false - have_gtk_primary_selection = false - have_wlr_data_control = false -endif +wayland = dependency( + 'wayland-client', + default_options: ['tests=false', 'documentation=false', 'dtd_validation=false'] +) cc = meson.get_compiler('c') have_memfd = cc.has_header_symbol('sys/syscall.h', 'SYS_memfd_create') @@ -27,125 +9,65 @@ have_shm_anon = cc.has_header_symbol('sys/mman.h', 'SHM_ANON') 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_GTK_PRIMARY_SELECTION', have_gtk_primary_selection) -conf_data.set('HAVE_WLR_DATA_CONTROL', have_wlr_data_control) - conf_data.set('HAVE_MEMFD', have_memfd) conf_data.set('HAVE_SHM_ANON', have_shm_anon) +subdir('protocol') + configure_file(output: 'config.h', configuration: conf_data) -if wayland_protocols.found() - protocols_path = wayland_protocols.get_pkgconfig_variable('pkgdatadir') -endif - -protocols = [] - -if not have_xdg_shell - warning('Building without xdg-shell support') -else - xdg_shell_xml = join_paths(protocols_path, 'stable', 'xdg-shell', 'xdg-shell.xml') - protocols += [['xdg-shell', xdg_shell_xml]] -endif - -if have_wp_primary_selection - wp_primary_selection_xml = join_paths(protocols_path, 'unstable', 'primary-selection', 'primary-selection-unstable-v1.xml') - protocols += [['wp-primary-selection', wp_primary_selection_xml]] -endif - -if not have_gtk_primary_selection - warning('Building without primary selection support') -else - gtk_primary_selection_xml = join_paths('protocol', 'gtk-primary-selection.xml') - protocols += [['gtk-primary-selection', gtk_primary_selection_xml]] -endif - -if have_wlr_data_control - wlr_data_control_xml = join_paths('protocol', 'wlr-data-control-unstable-v1.xml') - protocols += [['wlr-data-control', wlr_data_control_xml]] -endif - -protocol_deps = [] -protocol_headers = [] - -foreach protocol : protocols - name = protocol[0] - xml = protocol[1] - header = custom_target(name + ' client header', - input: xml, output: name + '.h', - command: [wayland_scanner, 'client-header', '@INPUT@', '@OUTPUT@'] - ) - impl = custom_target(name + ' code', - input: xml, output: name + '.c', - command: [wayland_scanner, scanner_code_command, '@INPUT@', '@OUTPUT@'] - ) - protocol_headers += header - lib = static_library(name, impl, header, dependencies: wayland) - protocol_deps += lib -endforeach - lib = static_library( 'wl-clipboard', - [ - protocol_headers, + protocol_headers, - 'includes/shell-protocols.h', - 'includes/selection-protocols.h', + 'includes/shell-protocols.h', + 'includes/selection-protocols.h', - 'util/string.h', - 'util/string.c', - 'util/files.h', - 'util/files.c', - 'util/misc.h', - 'util/misc.c', + 'util/string.h', + 'util/string.c', + 'util/files.h', + 'util/files.c', + 'util/misc.h', + 'util/misc.c', - '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', - ], + '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', protocol_headers], + 'wl-copy.c', + protocol_headers, dependencies: wayland, link_with: lib, install: true ) executable( 'wl-paste', - ['wl-paste.c', protocol_headers], - dependencies: wayland, - link_with: lib, - install: true -) -executable( - 'wl-clipboard-x11', - ['wl-clipboard-x11.c', protocol_headers], + 'wl-paste.c', + protocol_headers, dependencies: wayland, link_with: lib, install: true diff --git a/src/protocol/gtk-shell.xml b/src/protocol/gtk-shell.xml new file mode 100644 index 0000000..a8d51c6 --- /dev/null +++ b/src/protocol/gtk-shell.xml @@ -0,0 +1,107 @@ + + + + + gtk_shell is a protocol extension providing additional features for + clients implementing it. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/protocol/meson.build b/src/protocol/meson.build new file mode 100644 index 0000000..b63218b --- /dev/null +++ b/src/protocol/meson.build @@ -0,0 +1,111 @@ +protocols_opt = get_option('protocols') +wayland_scanner = find_program('wayland-scanner', required: protocols_opt, native: true) +wayland_protocols = dependency( + 'wayland-protocols', + version: '>= 1.12', + required: protocols_opt, + default_options: ['tests=false'] +) + +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') + have_xdg_activation = wayland_protocols.found() and wayland_protocols.version().version_compare('>= 1.21') + # these are bundled + have_gtk_primary_selection = true + have_wlr_data_control = true + have_ext_data_control = wayland_protocols.found() and wayland_protocols.version().version_compare('>= 1.39') + have_gtk_shell = true + + if wayland.version().version_compare('>= 1.15') + scanner_code_command = 'private-code' + else + scanner_code_command = 'code' + endif +else + have_xdg_shell = false + have_wp_primary_selection = false + have_xdg_activation = false + have_gtk_primary_selection = false + have_wlr_data_control = false + have_ext_data_control = false + have_gtk_shell = false +endif + +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_GTK_PRIMARY_SELECTION', have_gtk_primary_selection) +conf_data.set('HAVE_WLR_DATA_CONTROL', have_wlr_data_control) +conf_data.set('HAVE_EXT_DATA_CONTROL', have_ext_data_control) +conf_data.set('HAVE_XDG_ACTIVATION', have_xdg_activation) +conf_data.set('HAVE_GTK_SHELL', have_gtk_shell) + +if wayland_protocols.found() + if meson.version().version_compare('>= 0.58') + protocols_path = wayland_protocols.get_variable('pkgdatadir') + else + protocols_path = wayland_protocols.get_pkgconfig_variable('pkgdatadir') + endif +endif + +protocols = [] + +if not have_xdg_shell + warning('Building without xdg-shell support, wl-clipboard will be unable to work on some compositors') +else + xdg_shell_xml = join_paths(protocols_path, 'stable', 'xdg-shell', 'xdg-shell.xml') + protocols += [['xdg-shell', xdg_shell_xml]] +endif + +if have_wp_primary_selection + wp_primary_selection_xml = join_paths(protocols_path, 'unstable', 'primary-selection', 'primary-selection-unstable-v1.xml') + protocols += [['wp-primary-selection', wp_primary_selection_xml]] +endif + +if not have_gtk_primary_selection + warning('Building without primary selection support') +else + gtk_primary_selection_xml = 'gtk-primary-selection.xml' + protocols += [['gtk-primary-selection', gtk_primary_selection_xml]] +endif + +if have_wlr_data_control + wlr_data_control_xml = 'wlr-data-control-unstable-v1.xml' + protocols += [['wlr-data-control', wlr_data_control_xml]] +endif + +if have_ext_data_control + ext_data_control_xml = join_paths(protocols_path, 'staging', 'ext-data-control', 'ext-data-control-v1.xml') + protocols += [['ext-data-control', ext_data_control_xml]] +endif + +if have_xdg_activation + xdg_activation_xml = join_paths(protocols_path, 'staging', 'xdg-activation', 'xdg-activation-v1.xml') + protocols += [['xdg-activation', xdg_activation_xml]] +endif + +if have_gtk_shell + gtk_shell_xml = 'gtk-shell.xml' + protocols += [['gtk-shell', gtk_shell_xml]] +endif + + +protocol_deps = [] +protocol_headers = [] + +foreach protocol : protocols + name = protocol[0] + xml = protocol[1] + header = custom_target(name + ' client header', + input: xml, output: name + '.h', + command: [wayland_scanner, 'client-header', '@INPUT@', '@OUTPUT@'] + ) + impl = custom_target(name + ' code', + input: xml, output: name + '.c', + command: [wayland_scanner, scanner_code_command, '@INPUT@', '@OUTPUT@'] + ) + protocol_headers += header + lib = static_library(name, impl, header, dependencies: wayland) + protocol_deps += lib +endforeach diff --git a/src/types/copy-action.c b/src/types/copy-action.c index 5ef9ea1..79b0b0a 100644 --- a/src/types/copy-action.c +++ b/src/types/copy-action.c @@ -1,6 +1,6 @@ /* wl-clipboard * - * Copyright © 2019 Sergey Bugaev + * Copyright © 2018-2026 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 @@ -30,17 +30,24 @@ #include #include #include +#include static void do_set_selection(struct copy_action *self, uint32_t serial) { /* Set the selection and make sure it reaches * the display before we do anything else, * such as destroying the surface or exiting. + * Make sure to unset the callback to prevent + * reentrancy issues. */ + if (self->popup_surface != NULL) { + self->popup_surface->on_focus = NULL; + self->popup_surface->data = NULL; + } device_set_selection(self->device, self->source, serial, self->primary); wl_display_roundtrip(self->device->wl_display); /* Now, if we have used a popup surface, destroy it */ - if (self->device->needs_popup_surface) { + if (self->popup_surface != NULL) { popup_surface_destroy(self->popup_surface); self->popup_surface = NULL; } @@ -62,10 +69,24 @@ static void on_focus( static void do_send(struct source *source, const char *mime_type, int fd) { struct copy_action *self = source->data; - /* Unset O_NONBLOCK */ + /* Unset O_NONBLOCK */ fcntl(fd, F_SETFL, 0); - if (self->file_to_copy != NULL) { + if (!strcmp(mime_type, x_kde_password_manager_hint)) { + /* We always respond to x-kde-passwordManagerHint, + * even though we only offer it when --sensitive + * is set. + */ + const char *data = self->sensitive ? "secret" : "public"; + write(fd, data, strlen(data)); + close(fd); + /* This does not count as pasting, so don't invoke + * our pasted_callback, just return. + */ + return; + } + + if (self->fd_to_copy_from != -1) { /* Copy the file to the given file descriptor * by spawning an appropriate cat process. */ @@ -76,9 +97,13 @@ static void do_send(struct source *source, const char *mime_type, int fd) { return; } if (pid == 0) { + dup2(self->fd_to_copy_from, STDIN_FILENO); + close(self->fd_to_copy_from); dup2(fd, STDOUT_FILENO); close(fd); - execlp("cat", "cat", self->file_to_copy, NULL); + signal(SIGHUP, SIG_DFL); + signal(SIGPIPE, SIG_DFL); + execlp("cat", "cat", NULL); perror("exec cat"); exit(1); } @@ -90,7 +115,12 @@ static void do_send(struct source *source, const char *mime_type, int fd) { * should switch to an asynchronous child waiting scheme * instead. */ - wait(NULL); + waitpid(pid, NULL, 0); + /* Seek back to the beginning of the file */ + off_t rc = lseek(self->fd_to_copy_from, 0, SEEK_SET); + if (rc < 0) { + perror("lseek"); + } } else { /* We'll perform the copy ourselves */ FILE *f = fdopen(fd, "w"); diff --git a/src/types/copy-action.h b/src/types/copy-action.h index c0a1290..6c92e9a 100644 --- a/src/types/copy-action.h +++ b/src/types/copy-action.h @@ -1,6 +1,6 @@ /* wl-clipboard * - * Copyright © 2019 Sergey Bugaev + * Copyright © 2018-2026 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 @@ -33,6 +33,7 @@ struct copy_action { struct source *source; struct popup_surface *popup_surface; int primary; + int sensitive; void (*did_set_selection_callback)(struct copy_action *self); void (*pasted_callback)(struct copy_action *self); @@ -40,8 +41,9 @@ struct copy_action { /* Exactly one of these fields must be non-null if the source * is non-null, otherwise all these fields must be null. + * The null value for fd_to_copy_from is -1. */ - const char *file_to_copy; + int fd_to_copy_from; argv_t argv_to_copy; struct { const char *ptr; diff --git a/src/types/device-manager.c b/src/types/device-manager.c index bd6e04e..55e7aa7 100644 --- a/src/types/device-manager.c +++ b/src/types/device-manager.c @@ -1,6 +1,6 @@ /* wl-clipboard * - * Copyright © 2019 Sergey Bugaev + * Copyright © 2018-2026 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 @@ -138,3 +138,23 @@ GET_DEVICE( INIT(zwlr_data_control_manager_v1) #endif /* HAVE_WLR_DATA_CONTROL */ + +/* ext-data-control implementation */ + +#ifdef HAVE_EXT_DATA_CONTROL + +CREATE_SOURCE( + ext_data_control_manager_v1, + ext_data_control_source_v1, + create_data_source +) + +GET_DEVICE( + ext_data_control_manager_v1, + ext_data_control_device_v1, + get_data_device +) + +INIT(ext_data_control_manager_v1) + +#endif /* HAVE_EXT_DATA_CONTROL */ diff --git a/src/types/device-manager.h b/src/types/device-manager.h index bb7e88d..8629a57 100644 --- a/src/types/device-manager.h +++ b/src/types/device-manager.h @@ -1,6 +1,6 @@ /* wl-clipboard * - * Copyright © 2019 Sergey Bugaev + * Copyright © 2018-2026 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 @@ -69,4 +69,10 @@ void device_manager_init_zwlr_data_control_manager_v1( ); #endif +#ifdef HAVE_EXT_DATA_CONTROL +void device_manager_init_ext_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 index 16525a6..6282c1f 100644 --- a/src/types/device.c +++ b/src/types/device.c @@ -1,6 +1,6 @@ /* wl-clipboard * - * Copyright © 2019 Sergey Bugaev + * Copyright © 2018-2026 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 @@ -25,6 +25,12 @@ #include #include +enum TriState { + Unknown, + Yes, + No +}; + int device_supports_selection(struct device *self, int primary) { return self->supports_selection(self, primary); } @@ -41,7 +47,10 @@ void device_set_selection( /* Macros to reduce implementation boilerplate */ #define SUPPORTS_SELECTION(type, expr) \ -static int device_supports_selection_on_ ## type(struct device *self, int primary) { \ +static int device_supports_selection_on_ ## type( \ + struct device *self, \ + int primary \ +) { \ return expr; \ } @@ -192,12 +201,6 @@ INIT(zwp_primary_selection_device_v1, 1) #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) { @@ -269,3 +272,80 @@ zwlr_data_control_device_v1_listener = { INIT(zwlr_data_control_device_v1, 0) #endif /* HAVE_WLR_DATA_CONTROL */ + + +/* ext-data-control implementation */ + +#ifdef HAVE_EXT_DATA_CONTROL + +/* Whether ext-data-control supports primary selection */ +static enum TriState device_ext_supports_primary_selection = Unknown; +static int device_get_ext_supports_selection(struct device *self, int primary) { + if (!primary) { + return 1; + } + + if (device_ext_supports_primary_selection == Yes) { + return 1; + } else if (device_ext_supports_primary_selection == No) { + return 0; + } + + wl_display_roundtrip(self->wl_display); + + if (device_ext_supports_primary_selection == Yes) { + return 1; + } else { + device_ext_supports_primary_selection = No; + return 0; + } +} + +SUPPORTS_SELECTION( + ext_data_control_device_v1, + device_get_ext_supports_selection(self, primary) +) + +SET_SELECTION_IMPL(ext_data_control_device_v1, ext_data_control_source_v1, { + if (!primary) { + ext_data_control_device_v1_set_selection(device, source); + } else { + ext_data_control_device_v1_set_primary_selection(device, source); + } +}) + +DATA_OFFER_HANDLER(ext_data_control_device_v1, ext_data_control_offer_v1) + +SELECTION_HANDLER( + ext_data_control_device_v1, + ext_data_control_offer_v1, + selection, + 0 +) + +static void ext_data_control_device_v1_primary_selection_handler( + void *data, + struct ext_data_control_device_v1 *device, + struct ext_data_control_offer_v1 *offer_proxy +) { + device_ext_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 ext_data_control_device_v1_listener +ext_data_control_device_v1_listener = { + .data_offer = ext_data_control_device_v1_data_offer_handler, + .selection = ext_data_control_device_v1_selection_handler, + .primary_selection = ext_data_control_device_v1_primary_selection_handler +}; + +INIT(ext_data_control_device_v1, 0) + +#endif /* HAVE_EXT_DATA_CONTROL */ diff --git a/src/types/device.h b/src/types/device.h index 5f5e3fe..ad780e0 100644 --- a/src/types/device.h +++ b/src/types/device.h @@ -1,6 +1,6 @@ /* wl-clipboard * - * Copyright © 2019 Sergey Bugaev + * Copyright © 2018-2026 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 @@ -71,4 +71,8 @@ void device_init_zwp_primary_selection_device_v1(struct device *self); void device_init_zwlr_data_control_device_v1(struct device *self); #endif +#ifdef HAVE_EXT_DATA_CONTROL +void device_init_ext_data_control_device_v1(struct device *self); +#endif + #endif /* TYPES_DEVICE_H */ diff --git a/src/types/keyboard.c b/src/types/keyboard.c index 99ca793..12eb82f 100644 --- a/src/types/keyboard.c +++ b/src/types/keyboard.c @@ -1,6 +1,6 @@ /* wl-clipboard * - * Copyright © 2019 Sergey Bugaev + * Copyright © 2018-2026 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 diff --git a/src/types/keyboard.h b/src/types/keyboard.h index e5f02d0..4e95d85 100644 --- a/src/types/keyboard.h +++ b/src/types/keyboard.h @@ -1,6 +1,6 @@ /* wl-clipboard * - * Copyright © 2019 Sergey Bugaev + * Copyright © 2018-2026 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 diff --git a/src/types/offer.c b/src/types/offer.c index c21a562..cf3c7ff 100644 --- a/src/types/offer.c +++ b/src/types/offer.c @@ -1,6 +1,6 @@ /* wl-clipboard * - * Copyright © 2019 Sergey Bugaev + * Copyright © 2018-2026 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 @@ -105,3 +105,13 @@ INIT(zwlr_data_control_offer_v1) #endif /* HAVE_WLR_DATA_CONTROL */ + +/* ext-data-control implementation */ + +#ifdef HAVE_EXT_DATA_CONTROL + +OFFER_HANDLER(ext_data_control_offer_v1) +LISTENER(ext_data_control_offer_v1) +INIT(ext_data_control_offer_v1) + +#endif /* HAVE_EXT_DATA_CONTROL */ diff --git a/src/types/offer.h b/src/types/offer.h index 21dd567..c8eaed1 100644 --- a/src/types/offer.h +++ b/src/types/offer.h @@ -1,6 +1,6 @@ /* wl-clipboard * - * Copyright © 2019 Sergey Bugaev + * Copyright © 2018-2026 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 @@ -61,4 +61,8 @@ void offer_init_zwp_primary_selection_offer_v1(struct offer *self); void offer_init_zwlr_data_control_offer_v1(struct offer *self); #endif +#ifdef HAVE_EXT_DATA_CONTROL +void offer_init_ext_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 index 1aad115..b8eee53 100644 --- a/src/types/popup-surface.c +++ b/src/types/popup-surface.c @@ -1,6 +1,6 @@ /* wl-clipboard * - * Copyright © 2019 Sergey Bugaev + * Copyright © 2018-2026 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 @@ -42,7 +42,7 @@ static void forward_on_focus( void popup_surface_init(struct popup_surface *self) { self->shell = registry_find_shell(self->registry); if (self->shell == NULL) { - bail("Missing a shell"); + complain_about_missing_shell(); } self->keyboard = seat_get_keyboard(self->seat); @@ -61,7 +61,7 @@ void popup_surface_init(struct popup_surface *self) { struct wl_compositor *wl_compositor = self->registry->wl_compositor; if (wl_compositor == NULL) { - bail("Missing the compositor"); + complain_about_missing_global("wl_compositor"); } self->wl_surface = wl_compositor_create_surface(wl_compositor); self->shell_surface = shell_create_shell_surface( @@ -69,13 +69,22 @@ void popup_surface_init(struct popup_surface *self) { self->wl_surface ); +#ifdef HAVE_GTK_SHELL + if (self->registry->gtk_shell1 != NULL) { + self->gtk_surface = gtk_shell1_get_gtk_surface( + self->registry->gtk_shell1, + self->wl_surface + ); + } +#endif + /* 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 + * (without ever committing 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 @@ -97,12 +106,15 @@ void popup_surface_init(struct popup_surface *self) { /* Open an anonymous file and write some zero bytes to it */ int fd = create_anonymous_file(); - ftruncate(fd, size); + int rc = ftruncate(fd, size); + if (rc < 0) { + perror("ftruncate"); + } /* Create a shared memory pool */ struct wl_shm *wl_shm = self->registry->wl_shm; if (wl_shm == NULL) { - bail("Missing the shm"); + complain_about_missing_global("wl_shm"); } struct wl_shm_pool *wl_shm_pool = wl_shm_create_pool(wl_shm, fd, size); @@ -122,6 +134,32 @@ void popup_surface_init(struct popup_surface *self) { wl_surface_attach(self->wl_surface, wl_buffer, 0, 0); wl_surface_damage(self->wl_surface, 0, 0, width, height); + + /* Ask the compositor nicely to give us focus */ +#ifdef HAVE_GTK_SHELL + if (self->gtk_surface != NULL) { + gtk_surface1_present(self->gtk_surface, 0); + } +#endif +#ifdef HAVE_XDG_ACTIVATION + if (self->registry->xdg_activation_v1 != NULL) { + /* See if someone was kind enough to leave + * some tokens for us in the environment. + */ + const char *token = getenv("XDG_ACTIVATION_TOKEN"); + if (token == NULL) { + token = getenv("DESKTOP_STARTUP_ID"); + } + if (token != NULL) { + xdg_activation_v1_activate( + self->registry->xdg_activation_v1, + token, + self->wl_surface + ); + } + } +#endif + wl_surface_commit(self->wl_surface); } @@ -135,7 +173,21 @@ void popup_surface_destroy(struct popup_surface *self) { self->keyboard->data = NULL; shell_surface_destroy(self->shell_surface); +#ifdef HAVE_GTK_SHELL + if (self->gtk_surface != NULL) { + if ( + gtk_surface1_get_version(self->gtk_surface) >= + GTK_SURFACE1_RELEASE_SINCE_VERSION + ) { + gtk_surface1_release(self->gtk_surface); + } else { + gtk_surface1_destroy(self->gtk_surface); + } + } +#endif wl_surface_destroy(self->wl_surface); + /* Let popup_surface_init() know we're already done */ + self->wl_surface = NULL; free(self->shell); if (self->should_free_self) { diff --git a/src/types/popup-surface.h b/src/types/popup-surface.h index 8847519..e6148a7 100644 --- a/src/types/popup-surface.h +++ b/src/types/popup-surface.h @@ -1,6 +1,6 @@ /* wl-clipboard * - * Copyright © 2019 Sergey Bugaev + * Copyright © 2018-2026 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 @@ -19,6 +19,8 @@ #ifndef TYPES_POPUP_SURFACE_H #define TYPES_POPUP_SURFACE_H +#include "includes/shell-protocols.h" + #include struct registry; @@ -40,6 +42,10 @@ struct popup_surface { struct wl_surface *wl_surface; struct keyboard *keyboard; int should_free_self; + +#ifdef HAVE_GTK_SHELL + struct gtk_surface1 *gtk_surface; +#endif }; void popup_surface_init(struct popup_surface *self); diff --git a/src/types/registry.c b/src/types/registry.c index 67c7fde..7eeaec4 100644 --- a/src/types/registry.c +++ b/src/types/registry.c @@ -1,6 +1,6 @@ /* wl-clipboard * - * Copyright © 2019 Sergey Bugaev + * Copyright © 2018-2026 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 @@ -29,12 +29,14 @@ #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 \ - ); \ + if (version >= (known_version)) { \ + self->interface_name = wl_registry_bind( \ + wl_registry, \ + name, \ + &interface_name ## _interface, \ + known_version \ + ); \ + } \ } static void wl_registry_global_handler( @@ -49,6 +51,10 @@ static void wl_registry_global_handler( BIND(wl_compositor, 2) BIND(wl_shm, 1) +#ifdef HAVE_XDG_ACTIVATION + BIND(xdg_activation_v1, 1) +#endif + /* Shells */ BIND(wl_shell, 1) @@ -57,6 +63,10 @@ static void wl_registry_global_handler( BIND(xdg_wm_base, 1) #endif +#ifdef HAVE_GTK_SHELL + BIND(gtk_shell1, 4) +#endif + /* Device managers */ BIND(wl_data_device_manager, 1) @@ -73,7 +83,11 @@ static void wl_registry_global_handler( BIND(zwlr_data_control_manager_v1, version > 2 ? 2 : version) #endif - if (strcmp(interface, "wl_seat") == 0) { +#ifdef HAVE_EXT_DATA_CONTROL + BIND(ext_data_control_manager_v1, 1) +#endif + + if (strcmp(interface, "wl_seat") == 0 && version >= 2) { struct seat *seat = calloc(1, sizeof(struct seat)); seat->proxy = wl_registry_bind( wl_registry, @@ -139,12 +153,15 @@ struct device_manager *registry_find_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. + /* For regular selection, we just look at the three supported + * protocols. We prefer ext-data-control or wlr-data-control, + * as they don't require us to use the popup surface hack. */ if (!primary) { +#ifdef HAVE_EXT_DATA_CONTROL + TRY(ext_data_control_manager_v1) +#endif #ifdef HAVE_WLR_DATA_CONTROL TRY(zwlr_data_control_manager_v1) #endif @@ -162,8 +179,16 @@ struct device_manager *registry_find_device_manager( * 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. + * + * The same goes for the newer ext-data-control, which has + * potential support for primary selection since v1, so no need + * for the version check there. */ +#ifdef HAVE_EXT_DATA_CONTROL + TRY(ext_data_control_manager_v1) +#endif + #ifdef HAVE_WLR_DATA_CONTROL if (self->zwlr_data_control_manager_v1 != NULL) { struct wl_proxy *proxy diff --git a/src/types/registry.h b/src/types/registry.h index e52e427..30a62b5 100644 --- a/src/types/registry.h +++ b/src/types/registry.h @@ -1,6 +1,6 @@ /* wl-clipboard * - * Copyright © 2019 Sergey Bugaev + * Copyright © 2018-2026 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 @@ -40,12 +40,19 @@ struct registry { struct wl_compositor *wl_compositor; struct wl_shm *wl_shm; +#ifdef HAVE_XDG_ACTIVATION + struct xdg_activation_v1 *xdg_activation_v1; +#endif + /* Shells */ struct wl_shell *wl_shell; #ifdef HAVE_XDG_SHELL struct xdg_wm_base *xdg_wm_base; #endif +#ifdef HAVE_GTK_SHELL + struct gtk_shell1 *gtk_shell1; +#endif /* Device managers */ @@ -62,6 +69,10 @@ struct registry { struct zwlr_data_control_manager_v1 *zwlr_data_control_manager_v1; #endif +#ifdef HAVE_EXT_DATA_CONTROL + struct ext_data_control_manager_v1 + *ext_data_control_manager_v1; +#endif }; void registry_init(struct registry *self); diff --git a/src/types/seat.c b/src/types/seat.c index 6e48450..3d7e604 100644 --- a/src/types/seat.c +++ b/src/types/seat.c @@ -1,6 +1,6 @@ /* wl-clipboard * - * Copyright © 2019 Sergey Bugaev + * Copyright © 2018-2026 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 diff --git a/src/types/seat.h b/src/types/seat.h index cd92073..edcbcd6 100644 --- a/src/types/seat.h +++ b/src/types/seat.h @@ -1,6 +1,6 @@ /* wl-clipboard * - * Copyright © 2019 Sergey Bugaev + * Copyright © 2018-2026 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 diff --git a/src/types/shell-surface.c b/src/types/shell-surface.c index ba7e61b..f2b73ec 100644 --- a/src/types/shell-surface.c +++ b/src/types/shell-surface.c @@ -1,6 +1,6 @@ /* wl-clipboard * - * Copyright © 2019 Sergey Bugaev + * Copyright © 2018-2026 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 @@ -67,6 +67,7 @@ void shell_surface_init_wl_shell_surface(struct shell_surface *self) { 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"); + wl_shell_surface_set_class(proxy, "io.github.bugaevc.wl-clipboard"); self->do_destroy = destroy_wl_shell_surface; } @@ -119,6 +120,7 @@ void shell_surface_init_xdg_surface(struct shell_surface *self) { self->proxy2 = (struct wl_proxy *) toplevel; xdg_toplevel_add_listener(toplevel, &xdg_toplevel_listener, self); xdg_toplevel_set_title(toplevel, "wl-clipboard"); + xdg_toplevel_set_app_id(toplevel, "io.github.bugaevc.wl-clipboard"); self->do_destroy = destroy_xdg_surface; } diff --git a/src/types/shell-surface.h b/src/types/shell-surface.h index d888553..fe70baf 100644 --- a/src/types/shell-surface.h +++ b/src/types/shell-surface.h @@ -1,6 +1,6 @@ /* wl-clipboard * - * Copyright © 2019 Sergey Bugaev + * Copyright © 2018-2026 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 diff --git a/src/types/shell.c b/src/types/shell.c index 36d192a..2a57f9f 100644 --- a/src/types/shell.c +++ b/src/types/shell.c @@ -1,6 +1,6 @@ /* wl-clipboard * - * Copyright © 2019 Sergey Bugaev + * Copyright © 2018-2026 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 diff --git a/src/types/shell.h b/src/types/shell.h index 9b866eb..5c8c2dd 100644 --- a/src/types/shell.h +++ b/src/types/shell.h @@ -1,6 +1,6 @@ /* wl-clipboard * - * Copyright © 2019 Sergey Bugaev + * Copyright © 2018-2026 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 diff --git a/src/types/source.c b/src/types/source.c index 5f28e2c..78f00ae 100644 --- a/src/types/source.c +++ b/src/types/source.c @@ -1,6 +1,6 @@ /* wl-clipboard * - * Copyright © 2019 Sergey Bugaev + * Copyright © 2018-2026 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 @@ -23,7 +23,7 @@ #include -void source_offer(struct source *self, const char *mime_type) { +void source_offer(struct source *self, char *mime_type) { self->do_offer(self->proxy, mime_type); } @@ -127,3 +127,14 @@ INIT(zwlr_data_control_source_v1) #endif /* HAVE_WLR_DATA_CONTROL */ + +/* ext-data-control implementation */ + +#ifdef HAVE_EXT_DATA_CONTROL + +SEND_HANDLER(ext_data_control_source_v1) +CANCELLED_HANDLER(ext_data_control_source_v1) +LISTENER(ext_data_control_source_v1) +INIT(ext_data_control_source_v1) + +#endif /* HAVE_EXT_DATA_CONTROL */ diff --git a/src/types/source.h b/src/types/source.h index a55ffba..a29d816 100644 --- a/src/types/source.h +++ b/src/types/source.h @@ -1,6 +1,6 @@ /* wl-clipboard * - * Copyright © 2019 Sergey Bugaev + * Copyright © 2018-2026 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 @@ -34,7 +34,7 @@ struct source { void (*do_offer)(struct wl_proxy *proxy, const char *mime_type); }; -void source_offer(struct source *self, const char *mime_type); +void source_offer(struct source *self, char *mime_type); /* Initializers */ @@ -52,4 +52,8 @@ void source_init_zwp_primary_selection_source_v1(struct source *self); void source_init_zwlr_data_control_source_v1(struct source *self); #endif +#ifdef HAVE_EXT_DATA_CONTROL +void source_init_ext_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 750aa0b..078c051 100644 --- a/src/util/files.c +++ b/src/util/files.c @@ -1,6 +1,6 @@ /* wl-clipboard * - * Copyright © 2019 Sergey Bugaev + * Copyright © 2018-2026 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 @@ -27,12 +27,15 @@ #include #include #include -#include // open +#include +#include #include // open #include // open #include // exit #include // basename #include +#include +#include #ifdef HAVE_MEMFD # include // syscall, SYS_memfd_create @@ -41,6 +44,42 @@ # include // shm_open, SHM_ANON #endif +#include // wl_display_get_fd + + +void complain_about_closed_stdio(struct wl_display *wl_display) { + const char *message = "wl-clipboard has been launched with a closed" + " standard file descriptor. This is a bug in the software that" + " has launched wl-clipboard. Aborting."; + + /* See if we can write to stderr */ + if (wl_display_get_fd(wl_display) < STDERR_FILENO) { + int rc = fcntl(STDERR_FILENO, F_GETFL); + if (rc > 0) { + rc &= O_ACCMODE; + if (rc == O_WRONLY || rc == O_RDWR) { + /* Yes, we can! */ + fprintf(stderr, "%s\n", message); + fflush(stderr); + abort(); + } + } + } + + /* Maybe there is a tty we could write to? */ + FILE *tty = fopen("/dev/tty", "w"); + if (tty != NULL) { + fprintf(tty, "%s\n", message); + fflush(tty); + abort(); + } + + /* As a last resort, try syslog */ + openlog("wl-clipboard", LOG_CONS | LOG_PID, LOG_USER); + syslog(LOG_ERR, "%s", message); + closelog(); + abort(); +} int create_anonymous_file() { int res; @@ -130,13 +169,15 @@ char *infer_mime_type_from_contents(const char *file_path) { /* If we cannot open /dev/null, just close stdin */ close(STDIN_FILENO); } + signal(SIGHUP, SIG_DFL); + signal(SIGPIPE, SIG_DFL); execlp("xdg-mime", "xdg-mime", "query", "filetype", file_path, NULL); exit(1); } close(pipefd[1]); int wstatus; - wait(&wstatus); + waitpid(pid, &wstatus, 0); /* See if that worked */ if (!WIFEXITED(wstatus) || WEXITSTATUS(wstatus) != 0) { @@ -146,11 +187,15 @@ char *infer_mime_type_from_contents(const char *file_path) { /* Read the result */ char *res = malloc(256); - size_t len = read(pipefd[0], res, 256); + ssize_t len = read(pipefd[0], res, 256); + close(pipefd[0]); + if (len <= 0) { + free(res); + return NULL; + } /* Trim the newline */ len--; res[len] = 0; - close(pipefd[0]); if (str_has_prefix(res, "inode/")) { free(res); @@ -160,9 +205,8 @@ char *infer_mime_type_from_contents(const char *file_path) { return res; } -char *infer_mime_type_from_name(const char *file_path) { - const char *actual_ext = get_file_extension(file_path); - if (actual_ext == NULL) { +static char *search_mime_dot_types_for_ext(const char *ext) { + if (ext == NULL) { return NULL; } @@ -175,7 +219,7 @@ char *infer_mime_type_from_name(const char *file_path) { } for (char line[200]; fgets(line, sizeof(line), f) != NULL;) { - /* Skip comments and black lines */ + /* Skip comments and blank lines */ if (line[0] == '#' || line[0] == '\n') { continue; } @@ -185,11 +229,12 @@ char *infer_mime_type_from_name(const char *file_path) { int consumed; if (sscanf(line, "%199s%n", mime_type, &consumed) != 1) { /* A malformed line, perhaps? */ + fprintf(stderr, "malformed mime.types line: %s\n", line); continue; } char *lineptr = line + consumed; - for (char ext[200]; sscanf(lineptr, "%199s%n", ext, &consumed) == 1;) { - if (strcmp(ext, actual_ext) == 0) { + for (char ext_pattern[200]; sscanf(lineptr, "%199s%n", ext_pattern, &consumed) == 1;) { + if (strcmp(ext_pattern, ext) == 0) { fclose(f); return strdup(mime_type); } @@ -200,28 +245,86 @@ 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); +static char *search_shared_mime_info_globs_for_filename(const char *filename) { + FILE *f = fopen("/usr/share/mime/globs2", "r"); + if (f == NULL) { + f = fopen("/usr/local/share/mime/globs2", "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 colon-separated + * weight, mime type, and glob pattern. + * We ignore the weight. + */ + char mime_type[200]; + char filename_glob[200]; + if (sscanf(line, "%*d:%199[^:]:%199s\n", mime_type, filename_glob) != 2) { + /* A malformed line, perhaps? */ + fprintf(stderr, "malformed globs2 line: %s\n", line); + continue; + } + if (fnmatch(filename_glob, filename, 0) == 0) { + fclose(f); + return strdup(mime_type); + } + } + fclose(f); + return NULL; +} + +char *infer_mime_type_from_name(const char *file_path) { + const char *ext = get_file_extension(file_path); + char *file_path_dup = strdup(file_path); + const char *filename = basename(file_path_dup); + char *mime_type = search_shared_mime_info_globs_for_filename(filename); + if (!mime_type) { + mime_type = search_mime_dot_types_for_ext(ext); + } + free(file_path_dup); + return mime_type; +} + +/* Returns the name of a new file */ +char *dump_stdin_into_a_temp_file() { /* Pick a name for the file we'll be * creating inside that directory. We - * try to preserve the origial name for + * try to preserve the original 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)); - res_path[sizeof(dirpath) - 1] = '/'; - strcpy(res_path + sizeof(dirpath), name); + /* Create a temp directory to host out file */ + const char *tmpdir = getenv("TMPDIR"); + if (tmpdir == NULL) { + tmpdir = "/tmp"; + } + size_t tmpdir_len = strlen(tmpdir); + static const char dir_template[] = "wl-copy-buffer-XXXXXX"; + char *path = malloc( + tmpdir_len + 1 + strlen(dir_template) + 1 + strlen(name) + 1 + ); + memcpy(path, tmpdir, tmpdir_len); + path[tmpdir_len] = '/'; + memcpy(path + tmpdir_len + 1, dir_template, strlen(dir_template)); + size_t prefix_len = tmpdir_len + 1 + strlen(dir_template); + path[prefix_len] = 0; + + if (mkdtemp(path) != path) { + perror("mkdtemp"); + exit(1); + } + + path[prefix_len] = '/'; + strcpy(path + prefix_len + 1, name); /* Spawn cat to perform the copy */ pid_t pid = fork(); @@ -230,25 +333,25 @@ char *dump_stdin_into_a_temp_file() { exit(1); } if (pid == 0) { - int fd = creat(res_path, S_IRUSR | S_IWUSR); + int fd = creat(path, S_IRUSR | S_IWUSR); if (fd < 0) { perror("creat"); exit(1); } dup2(fd, STDOUT_FILENO); close(fd); + signal(SIGHUP, SIG_DFL); + signal(SIGPIPE, SIG_DFL); execlp("cat", "cat", NULL); perror("exec cat"); exit(1); } int wstatus; - wait(&wstatus); - if (original_path != NULL) { - free(original_path); - } + waitpid(pid, &wstatus, 0); + free(original_path); if (!WIFEXITED(wstatus) || WEXITSTATUS(wstatus) != 0) { bail("Failed to copy the file"); } - return res_path; + return path; } diff --git a/src/util/files.h b/src/util/files.h index 2a2d4b6..e39ca4f 100644 --- a/src/util/files.h +++ b/src/util/files.h @@ -1,6 +1,6 @@ /* wl-clipboard * - * Copyright © 2019 Sergey Bugaev + * Copyright © 2018-2026 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 @@ -19,6 +19,9 @@ #ifndef UTIL_FILES_H #define UTIL_FILES_H +struct wl_display; +void complain_about_closed_stdio(struct wl_display *wl_display); + int create_anonymous_file(void); void trim_trailing_newline(const char *file_path); diff --git a/src/util/misc.c b/src/util/misc.c index e9f7671..4edd822 100644 --- a/src/util/misc.c +++ b/src/util/misc.c @@ -1,6 +1,6 @@ /* wl-clipboard * - * Copyright © 2019 Sergey Bugaev + * Copyright © 2018-2026 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 @@ -20,11 +20,14 @@ #include "util/misc.h" #include +#include +#include +#include void print_version_info() { printf( "wl-clipboard " PROJECT_VERSION "\n" - "Copyright (C) 2019 Sergey Bugaev\n" + "Copyright (C) 2018-2026 Sergey Bugaev\n" "License GPLv3+: GNU GPL version 3 or later" " .\n" "This is free software: you are free to change and redistribute it.\n" @@ -35,7 +38,7 @@ void print_version_info() { 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"); + complain_about_missing_global("wl_data_device_manager"); } #if !defined(HAVE_WP_PRIMARY_SELECTION) && !defined(HAVE_GTK_PRIMARY_SELECTION) @@ -46,14 +49,91 @@ void complain_about_selection_support(int primary) { } void complain_about_watch_mode_support() { -#ifdef HAVE_WLR_DATA_CONTROL +#if defined(HAVE_WLR_DATA_CONTROL) || defined(HAVE_EXT_DATA_CONTROL) bail( - "Watch mode requires a compositor that supports " - "wlroots data-control protocol" + "Watch mode requires a compositor that supports" + " the data-control protocol" ); #else bail( - "wl-clipboard was built without wlroots data-control protocol support" + "wl-clipboard was built without support for" + " the data-control protocol" ); #endif } + +void complain_about_wayland_connection() { + int saved_errno = errno; + fprintf(stderr, "Failed to connect to a Wayland server"); + if (saved_errno != 0) { + fprintf(stderr, ": %s", strerror(saved_errno)); + } + fputc('\n', stderr); + + const char *display = getenv("WAYLAND_DISPLAY"); + const char *runtime_dir = getenv("XDG_RUNTIME_DIR"); + if (display != NULL) { + fprintf(stderr, "Note: WAYLAND_DISPLAY is set to %s\n", display); + } else { + fprintf( + stderr, + "Note: WAYLAND_DISPLAY is unset" + " (falling back to wayland-0)\n" + ); + display = "wayland-0"; + } + if (runtime_dir != NULL) { + fprintf(stderr, "Note: XDG_RUNTIME_DIR is set to %s\n", runtime_dir); + } else { + fprintf(stderr, "Note: XDG_RUNTIME_DIR is unset\n"); + } + if (display[0] != '/' && runtime_dir != NULL) { + fprintf( + stderr, + "Please check whether %s/%s socket exists and is accessible.\n", + runtime_dir, + display + ); + } + exit(1); +} + +void complain_about_missing_seat(const char *seat_name) { + if (seat_name != NULL) { + fprintf(stderr, "No such seat: %s\n", seat_name); + } else { + complain_about_missing_global("seat"); + } + exit(1); +} + +void complain_about_missing_global(const char *global) { + fprintf( + stderr, + "The compositor does not seem to implement %s," + " which is required for wl-clipboard to work\n", + global + ); + exit(1); +} + +void complain_about_missing_shell() { +#ifndef HAVE_XDG_SHELL + fprintf( + stderr, + "wl-clipboard was built without xdg-shell support, and the compositor" + " does not seem to support any other Wayland shell.\nPlease rebuild" + " wl-clipboard with xdg-shell support.\n" + ); + exit(1); +#endif + + + fprintf( + stderr, + "The compositor does not seem to implement a Wayland shell," + " which is required for wl-clipboard to work\n" + ); + + exit(1); +} diff --git a/src/util/misc.h b/src/util/misc.h index 02c9984..299a0a6 100644 --- a/src/util/misc.h +++ b/src/util/misc.h @@ -1,6 +1,6 @@ /* wl-clipboard * - * Copyright © 2019 Sergey Bugaev + * Copyright © 2018-2026 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 @@ -28,5 +28,9 @@ void print_version_info(void); void complain_about_selection_support(int primary); void complain_about_watch_mode_support(void); +void complain_about_wayland_connection(void); +void complain_about_missing_seat(const char *seat_name); +void complain_about_missing_global(const char *global); +void complain_about_missing_shell(void); #endif /* UTIL_MISC_H */ diff --git a/src/util/string.c b/src/util/string.c index 8a98e1e..9482bf9 100644 --- a/src/util/string.c +++ b/src/util/string.c @@ -1,6 +1,6 @@ /* wl-clipboard * - * Copyright © 2019 Sergey Bugaev + * Copyright © 2018-2026 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 @@ -36,7 +36,9 @@ int mime_type_is_text(const char *mime_type) { = strstr(mime_type, "json") != NULL || str_has_suffix(mime_type, "script") || str_has_suffix(mime_type, "xml") - || str_has_suffix(mime_type, "yaml"); + || str_has_suffix(mime_type, "yaml") + || str_has_suffix(mime_type, "csv") + || str_has_suffix(mime_type, "ini"); /* Special-case PGP and SSH keys. * A public SSH key is typically stored @@ -79,11 +81,3 @@ const char *get_file_extension(const char *file_path) { } return ext + 1; } - -const char *get_file_basename(const char *file_path) { - const char *last_slash = strrchr(file_path, '/'); - if (last_slash == NULL) { - return file_path; - } - return last_slash + 1; -} diff --git a/src/util/string.h b/src/util/string.h index c69ea35..e007f2e 100644 --- a/src/util/string.h +++ b/src/util/string.h @@ -1,6 +1,6 @@ /* wl-clipboard * - * Copyright © 2019 Sergey Bugaev + * Copyright © 2018-2026 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 @@ -21,6 +21,7 @@ #define text_plain "text/plain" #define text_plain_utf8 "text/plain;charset=utf-8" +#define x_kde_password_manager_hint "x-kde-passwordManagerHint" typedef char * const *argv_t; @@ -30,6 +31,5 @@ int str_has_prefix(const char *string, const char *prefix); int str_has_suffix(const char *string, const char *suffix); const char *get_file_extension(const char *file_path); -const char *get_file_basename(const char *file_path); #endif /* UTIL_STRING_H */ diff --git a/src/wl-clipboard-x11.c b/src/wl-clipboard-x11.c deleted file mode 100644 index db1cbbf..0000000 --- a/src/wl-clipboard-x11.c +++ /dev/null @@ -1,613 +0,0 @@ -/* wl-clipboard - * - * Copyright © 2019-2021 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/source.h" -#include "types/device.h" -#include "types/device-manager.h" -#include "types/registry.h" -#include "types/popup-surface.h" -#include "types/offer.h" - -#include "util/files.h" -#include "util/string.h" -#include "util/misc.h" - -#include -#include -#include -#include // open -#include -#include -#include - -static struct { - int in; - int out; - int filter; - int append; - int follow; - int zero_flush; - int stay_in_foreground; - int clear; - char *mime_type; - int trim_newline; - size_t loops; - int primary; - unsigned long timeout; - int sensitive; -} options; - -static struct wl_display *wl_display = NULL; -static struct popup_surface *popup_surface = NULL; -static size_t remaining_loops = 0; - -static void selection_callback(struct offer *offer, int primary) { - /* Ignore events we're not interested in */ - if (!options.out || primary != options.primary) { - if (offer != NULL) { - offer_destroy(offer); - }; - return; - } - - if (offer == NULL) { - bail("No selection"); - } - - // TODO - printf("Would receive the selection here\n"); -} - -static void did_set_selection_callback(struct copy_action *copy_action) { - if (options.clear) { - exit(0); - } - - if (!options.stay_in_foreground) { - /* Move to background. - * We fork our process and leave the - * child running in the background, - * while exiting in the parent. - * Also replace stdin/stdout with - * /dev/null so the stdout file - * descriptor isn't kept alive. - */ - int devnull = open("/dev/null", O_RDWR); - if (devnull >= 0) { - dup2(devnull, STDOUT_FILENO); - dup2(devnull, STDIN_FILENO); - close(devnull); - } else { - /* If we cannot open /dev/null, just close stdin/stdout */ - close(STDIN_FILENO); - close(STDOUT_FILENO); - } - signal(SIGHUP, SIG_IGN); - 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, int code) { - /* 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->file_to_copy; - if (temp_file != NULL) { - /* Clean up our temporary file */ - execlp("rm", "rm", "-r", dirname(temp_file), NULL); - perror("exec rm"); - exit(1); - } else { - exit(code); - } -} - -static void cancelled_callback(struct copy_action *copy_action) { - cleanup_and_exit(copy_action, 0); -} - -static void pasted_callback(struct copy_action *copy_action) { - if (remaining_loops == 0) { - /* Unlimited loops */ - return; - } - remaining_loops--; - if (remaining_loops == 0) { - cleanup_and_exit(copy_action, 0); - } -} - -static void print_usage(FILE *f, const char *argv0) { - fprintf( - f, - "Usage as xsel:\n" - "\t%s [options]\n\n" - "Options:\n" - "\t-i, --input\tCopy to the clipboard.\n" - "\t-o, --output\tPaste from the clipboard.\n" - "\t-a, --append\tAppend to the clipboard.\n" - "\t-f, --follow\tContinuously append to the clipboard.\n" - "\t-c, --clear\tInstead of copying anything, clear the clipboard.\n" - "\t-p, --primary\tUse the \"primary\" clipboard (default).\n" - "\t-b, --clipboard\tUse the regular clipboard.\n" - "\t-n, --nodetach\tStay in the foreground instead of forking.\n" - "\t-v, --version\tDisplay version info.\n" - "\t-h, --help\tDisplay this message.\n" - "Some arcane options not listed, see xsel(1) for more details.\n\n" - "Usage as xclip:\n" - "\t%s [options] [files]\n" - "Options:\n" - "\t-in\t\tCopy to the clipboard.\n" - "\t-out\t\tPaste from the clipboard.\n" - "\t-loops number\tOnly serve this many paste requests and then exit.\n" - "\t-selection name\tClipboard to use," - " \"primary\" (default) or \"clipboard\".\n" - "\t-quiet\t\tStay in the foreground instead of forking.\n" - "\t-target\t\tSpecify MIME type for the content.\n" - "\t-rmlastnl\tDo not copy the trailing newline character.\n" - "\t-version\tDisplay version info.\n" - "\t-help\t\tDisplay this message.\n" - "Some arcane options not listed, see xclip(1) for more details.\n", - argv0, - argv0 - ); -} - -static int getopt_xclip(int argc, argv_t argv, const struct option *options) { - int initial_optind = optind; - int consumed_args; - const struct option *matching_option = NULL; - - /* Loop, looking for an option we recognize */ - for (;; optind++) { - if (optind >= argc) { - /* We have reached the end of argv */ - return -1; - } - - const char *arg = argv[optind]; - if (arg[0] != '-') { - /* This isn't an option */ - continue; - } - - for (const struct option *option = options; option->name; option++) { - /* See if this option matches */ - if (!str_has_prefix(option->name, arg + 1)) { - continue; - } - if (matching_option != NULL) { - /* If multiple options match, - * treat it like it doesn't - * match at all. */ - matching_option = NULL; - break; - } - matching_option = option; - } - if (matching_option == NULL) { - continue; - } - if (matching_option->has_arg == required_argument) { - if (optind + 1 >= argc) { - /* We need an argument, but - * there are no more arguments. - * Do not report an error, just - * consider this not to match. */ - return -1; - } - optarg = argv[optind + 1]; - consumed_args = 2; - } else { - optarg = NULL; - consumed_args = 1; - } - - /* We have found an option, stop the loop */ - break; - } - - /* We started looking at initial_optind, - * and found the option at optind. Move - * forward everything in between. */ - if (optind != initial_optind) { - char *buffer[consumed_args]; - memcpy( - buffer, - argv + optind, - consumed_args * sizeof (char *) - ); - memmove( - (char **) argv + initial_optind + consumed_args, - argv + initial_optind, - (optind - initial_optind) * sizeof (char *) - ); - memcpy( - (char **) argv + initial_optind, - buffer, - consumed_args * sizeof (char *) - ); - } - - optind = initial_optind + consumed_args; - - return matching_option->val; -} - -static void parse_options(int argc, argv_t argv) { - if (argc < 1) { - bail("Empty argv"); - } - - const char *argv0_basename = get_file_basename(argv[0]); - int xclip = strcmp(argv0_basename, "xclip") == 0; - int xsel = strcmp(argv0_basename, "xsel") == 0; - - if (!xclip && !xsel) { - fprintf(stderr, "Invoked as neither xclip nor xsel\n\n"); - print_usage(stderr, argv[0]); - exit(1); - } - - /* This is the default in both tools */ - options.primary = 1; - - if (xsel) { - /* Parse xsel options */ - static struct option long_options[] = { - {"version", no_argument, 0, 'V'}, - {"help", no_argument, 0, 'h'}, - {"verbose", no_argument, 0, 'v'}, - {"append", no_argument, 0, 'a'}, - {"input", no_argument, 0, 'i'}, - {"clear", no_argument, 0, 'c'}, - {"output", no_argument, 0, 'o'}, - {"follow", no_argument, 0, 'f'}, - {"zeroflush", no_argument, 0, 'z'}, - {"primary", no_argument, 0, 'p'}, - {"secondary", no_argument, 0, 's'}, - {"clipboard", no_argument, 0, 'b'}, - {"keep", no_argument, 0, 'k'}, - {"exchange", no_argument, 0, 'x'}, - {"display", required_argument, 0, 'D'}, - {"selectionTimeout", required_argument, 0, 't'}, - {"nodetach", no_argument, 0, 'n'}, - {"delete", no_argument, 0, 'd'}, - {"logfile", required_argument, 0, 'l'}, - {0, 0, 0, 0} - }; - while (1) { - int option_index; - const char *opts = "hvaicofzpsbkxt:ndl:"; - int c = getopt_long(argc, argv, opts, long_options, &option_index); - if (c == -1) { - break; - } - if (c == 0) { - c = long_options[option_index].val; - } - switch (c) { - case 'V': - print_version_info(); - exit(0); - case 'h': - print_usage(stdout, argv[0]); - exit(0); - case 'v': - /* We don't have a verbose mode */ - break; - case 'a': - options.append = 1; - options.in = 1; - options.out = 0; - break; - case 'i': - options.in = 1; - options.out = 0; - break; - case 'c': - options.clear = 1; - break; - case 'o': - options.out = 1; - options.in = 0; - break; - case 'f': - options.follow = 1; - options.in = 1; - options.out = 0; - break; - case 'z': - options.zero_flush = 1; - options.follow = 1; - options.in = 1; - options.out = 0; - break; - case 'p': - options.primary = 1; - break; - case 's': - case 'x': - bail("No secondary selection on Wayland"); - break; - case 'b': - options.primary = 0; - break; - case 'k': - /* Complain, but exit successfully */ - fprintf(stderr, "Keep mode is unsupported\n"); - exit(0); - case 'D': - fprintf(stderr, "Ignoring --display on Wayland\n"); - break; - case 't': - options.timeout = atol(optarg); - break; - case 'n': - options.stay_in_foreground = 1; - break; - case 'd': - options.clear = 1; - break; - case 'l': - fprintf(stderr, "Ignoring --logfile\n"); - break; - default: - /* getopt has already printed an error message */ - print_usage(stderr, argv[0]); - exit(1); - } - } - } else { - /* Parse xclip options */ - static struct option xclip_options[] = { - {"version", no_argument, 0, 'V'}, - {"help", no_argument, 0, 'h'}, - {"loops", required_argument, 0, 'l'}, - {"display", required_argument, 0, 'd'}, - {"selection", required_argument, 0, 's'}, - {"filter", no_argument, 0, 'f'}, - {"in", no_argument, 0, 'i'}, - {"out", no_argument, 0, 'o'}, - {"silent", no_argument, 0, 'S'}, - {"quiet", no_argument, 0, 'q'}, - {"verbose", no_argument, 0, 'v'}, - {"debug", no_argument, 0, 'D'}, - {"noutf8", no_argument, 0, 'n'}, - {"target", no_argument, 0, 't'}, - {"rmlastnl", no_argument, 0, 'r'}, - {"sensitive", no_argument, 0, 'Z'}, - {"wait", required_argument, 0, 'w'}, - {0, 0, 0, 0} - }; - - while (1) { - int c = getopt_xclip(argc, argv, xclip_options); - if (c == -1) { - break; - } - switch (c) { - case 'V': - print_version_info(); - /* Do not exit */ - break; - case 'h': - print_usage(stdout, argv[0]); - /* Do not exit */ - break; - case 'l': - options.loops = atol(optarg); - break; - case 'd': - fprintf(stderr, "Ignoring -display on Wayland\n"); - break; - case 's': - /* xclip only really checks the first letter */ - switch (optarg[0]) { - case 'p': - case 'P': - options.primary = 1; - break; - case 's': - case 'S': - bail("No secondary selection on Wayland"); - break; - case 'c': - case 'C': - options.primary = 0; - break; - case 'b': - case 'B': - bail("No cut buffers on Wayland"); - break; - default: - bail("Unknown clipboard name"); - break; - } - break; - case 'f': - options.filter = 1; - break; - case 'i': - options.in = 1; - options.out = 0; - break; - case 'o': - options.out = 1; - options.in = 0; - break; - case 'S': - /* We don't have a silent mode */ - break; - case 'q': - case 'v': - case 'D': - /* We don't have quiet, verbose or - * debug modes, but they cause us to - * stay in the foreground. */ - options.stay_in_foreground = 1; - break; - case 'n': - /* We don't have a no-UTF-8 mode */ - break; - case 't': - options.mime_type = strdup(optarg); - break; - case 'r': - options.trim_newline = 1; - break; - case 'Z': - options.sensitive = 1; - break; - case 'w': - break; - } - } - } - - if (!options.in && !options.out) { - if (xclip && optind < argc) { - /* We have some files to copy, default to input */ - options.in = 1; - } else { - /* Default to output unless stdin is redirected */ - options.out = isatty(0); - options.in = !options.out; - } - } -} - -int main(int argc, argv_t argv) { - parse_options(argc, argv); - - struct wl_display *wl_display = wl_display_connect(NULL); - if (wl_display == NULL) { - bail("Failed to connect to a Wayland server"); - } - - 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, NULL); - if (seat == NULL) { - 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); - } - - struct copy_action *copy_action = NULL; - if (options.in || options.clear) { - /* Create and initialize the copy action */ - copy_action = calloc(1, sizeof(struct copy_action)); - copy_action->device = device; - copy_action->primary = options.primary; - - if (!options.clear) { - if (optind < argc) { - /* Copy files from our command-line arguments */ - // TODO - copy_action->argv_to_copy = &argv[optind]; - } else { - /* Copy data from our stdin. - * It's important that we only do this - * after going through the initial stages - * that are likely to result in errors, - * so that we don't forget to clean up - * the temp file. - */ - char *temp_file = dump_stdin_into_a_temp_file(); - if (options.trim_newline) { - trim_trailing_newline(temp_file); - } - copy_action->file_to_copy = temp_file; - } - - /* Create the source */ - copy_action->source = device_manager_create_source(device_manager); - if (options.mime_type != NULL) { - source_offer(copy_action->source, options.mime_type); - } - 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"); - } - free(options.mime_type); - options.mime_type = NULL; - } - } - - if (device->needs_popup_surface) { - popup_surface = calloc(1, sizeof(struct popup_surface)); - popup_surface->registry = registry; - popup_surface->seat = seat; - if (copy_action != NULL) { - copy_action->popup_surface = popup_surface; - } else { - popup_surface_init(popup_surface); - } - } - - if (copy_action != NULL) { - 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); - remaining_loops = options.loops; - } - - while (wl_display_dispatch(wl_display) >= 0); - - perror("wl_display_dispatch"); - cleanup_and_exit(copy_action, 1); - return 1; -} diff --git a/src/wl-copy.c b/src/wl-copy.c index f08cf17..660fb80 100644 --- a/src/wl-copy.c +++ b/src/wl-copy.c @@ -1,6 +1,6 @@ /* wl-clipboard * - * Copyright © 2019 Sergey Bugaev + * Copyright © 2018-2026 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 @@ -22,6 +22,7 @@ #include "types/device-manager.h" #include "types/registry.h" #include "types/popup-surface.h" +#include "types/offer.h" #include "util/files.h" #include "util/string.h" @@ -42,6 +43,7 @@ static struct { int trim_newline; int paste_once; int primary; + int sensitive; const char *seat_name; } options; @@ -54,10 +56,12 @@ static void did_set_selection_callback(struct copy_action *copy_action) { /* Move to background. * We fork our process and leave the * child running in the background, - * while exiting in the parent. - * Also replace stdin/stdout with - * /dev/null so the stdout file - * descriptor isn't kept alive. + * while exiting in the parent. We also + * replace stdin/stdout with /dev/null + * so the stdout file descriptor isn't + * kept alive, and chdir to the root, to + * prevent blocking file systems from + * being unmounted. */ int devnull = open("/dev/null", O_RDWR); if (devnull >= 0) { @@ -65,10 +69,15 @@ static void did_set_selection_callback(struct copy_action *copy_action) { dup2(devnull, STDIN_FILENO); close(devnull); } else { - /* If we cannot open /dev/null, just close stdin/stdout */ + /* If we cannot open /dev/null, + * just close stdin/stdout. + */ close(STDIN_FILENO); close(STDOUT_FILENO); } + if (chdir("/") < 0) { + perror("chdir /"); + } signal(SIGHUP, SIG_IGN); pid_t pid = fork(); if (pid < 0) { @@ -81,28 +90,20 @@ static void did_set_selection_callback(struct copy_action *copy_action) { } } -static void cleanup_and_exit(struct copy_action *copy_action, int code) { - /* 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->file_to_copy; - if (temp_file != NULL) { - /* Clean up our temporary file */ - execlp("rm", "rm", "-r", dirname(temp_file), NULL); - perror("exec rm"); - exit(1); - } else { - exit(code); - } -} - static void cancelled_callback(struct copy_action *copy_action) { - cleanup_and_exit(copy_action, 0); + exit(0); } static void pasted_callback(struct copy_action *copy_action) { if (options.paste_once) { - cleanup_and_exit(copy_action, 0); + exit(0); + } +} + +static void selection_callback(struct offer *offer, int primary) { + /* We're not interested */ + if (offer != NULL) { + offer_destroy(offer); } } @@ -116,11 +117,12 @@ static void print_usage(FILE *f, const char *argv0) { "Options:\n" "\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-c, --clear\t\tInstead of copying, clear the 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" "Override the inferred MIME type for the content.\n" + "\t --sensitive\t\tHint that the content is sensitive.\n" "\t-s, --seat seat-name\t" "Pick the seat to work with.\n" "\t-v, --version\t\tDisplay version info.\n" @@ -147,6 +149,7 @@ static void parse_options(int argc, argv_t argv) { {"foreground", no_argument, 0, 'f'}, {"clear", no_argument, 0, 'c'}, {"type", required_argument, 0, 't'}, + {"sensitive", no_argument, 0, 'S'}, {"seat", required_argument, 0, 's'}, {0, 0, 0, 0} }; @@ -185,6 +188,9 @@ static void parse_options(int argc, argv_t argv) { case 't': options.mime_type = strdup(optarg); break; + case 'S': + options.sensitive = 1; + break; case 's': options.seat_name = strdup(optarg); break; @@ -199,9 +205,20 @@ static void parse_options(int argc, argv_t argv) { int main(int argc, argv_t argv) { parse_options(argc, argv); + /* Ignore SIGPIPE. + * We don't really output anything + * to our stdout, yet we don't want + * to get killed when writing clipboard + * contents to a closed pipe. + */ + signal(SIGPIPE, SIG_IGN); + struct wl_display *wl_display = wl_display_connect(NULL); if (wl_display == NULL) { - bail("Failed to connect to a Wayland server"); + complain_about_wayland_connection(); + } + if (wl_display_get_fd(wl_display) <= STDERR_FILENO) { + complain_about_closed_stdio(wl_display); } struct registry *registry = calloc(1, sizeof(struct registry)); @@ -213,11 +230,7 @@ int main(int argc, argv_t argv) { 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"); - } + complain_about_missing_seat(options.seat_name); } /* Create the device */ @@ -228,6 +241,7 @@ int main(int argc, argv_t argv) { } struct device *device = device_manager_get_device(device_manager, seat); + device->selection_callback = selection_callback; if (!device_supports_selection(device, options.primary)) { complain_about_selection_support(options.primary); @@ -235,8 +249,10 @@ int main(int argc, argv_t argv) { /* Create and initialize the copy action */ struct copy_action *copy_action = calloc(1, sizeof(struct copy_action)); + copy_action->fd_to_copy_from = -1; copy_action->device = device; copy_action->primary = options.primary; + copy_action->sensitive = options.sensitive; if (!options.clear) { if (optind < argc) { @@ -257,7 +273,28 @@ int main(int argc, argv_t argv) { if (options.mime_type == NULL) { options.mime_type = infer_mime_type_from_contents(temp_file); } - copy_action->file_to_copy = temp_file; + copy_action->fd_to_copy_from = open( + temp_file, + O_RDONLY | O_CLOEXEC + ); + if (copy_action->fd_to_copy_from < 0) { + perror("Failed to open temp file"); + return 1; + } + /* Now, remove the temp file and its + * containing directory. We still keep + * access to the file through our open + * file descriptor. + */ + int rc = unlink(temp_file); + if (rc < 0) { + perror("Failed to unlink temp file"); + } + rc = rmdir(dirname(temp_file)); + if (rc < 0) { + perror("Failed to remove temp file directory"); + } + free(temp_file); } /* Create the source */ @@ -275,6 +312,9 @@ int main(int argc, argv_t argv) { } free(options.mime_type); options.mime_type = NULL; + if (options.sensitive) { + source_offer(copy_action->source, x_kde_password_manager_hint); + } } if (device->needs_popup_surface) { @@ -291,6 +331,5 @@ int main(int argc, argv_t argv) { while (wl_display_dispatch(wl_display) >= 0); perror("wl_display_dispatch"); - cleanup_and_exit(copy_action, 1); return 1; } diff --git a/src/wl-paste.c b/src/wl-paste.c index 1c61974..39b49fe 100644 --- a/src/wl-paste.c +++ b/src/wl-paste.c @@ -1,6 +1,6 @@ /* wl-clipboard * - * Copyright © 2019 Sergey Bugaev + * Copyright © 2018-2026 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 @@ -28,6 +28,8 @@ #include #include +#include +#include #include #include #include @@ -51,6 +53,7 @@ struct types { int inferred_available; int plain_text_utf8_available; int plain_text_available; + int has_sensitive_hint; const char *having_explicit_as_prefix; const char *any_text; const char *any; @@ -58,6 +61,7 @@ struct types { static struct wl_display *wl_display = NULL; static struct popup_surface *popup_surface = NULL; +static int offer_received = 0; static struct types classify_offer_types(struct offer *offer) { struct types types = { 0 }; @@ -96,6 +100,14 @@ static struct types classify_offer_types(struct offer *offer) { ) { types.having_explicit_as_prefix = mime_type; } + if (strcmp(mime_type, x_kde_password_manager_hint) == 0) { + /* We should be checking if it contains + * the string "secret" as opposed to "public", + * but for now let's just use the presence + * of the type as an indication. + */ + types.has_sensitive_hint = 1; + } } return types; } @@ -171,7 +183,84 @@ static const char *mime_type_to_request(struct types types) { #undef try_any_text #undef try_any +static int run_paste_command( + int stdin_fd, + const char *clipboard_state, + const char *mime_type +) { + /* 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"); + close(stdin_fd); + return 0; + } + if (pid == 0) { + dup2(stdin_fd, STDIN_FILENO); + close(stdin_fd); + if (options.watch) { + if (clipboard_state != NULL) { + setenv("CLIPBOARD_STATE", clipboard_state, 1); + } + if (mime_type != NULL) { + setenv("CLIPBOARD_TYPE", mime_type, 1); + } + 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(stdin_fd); + waitpid(pid, NULL, 0); + return 1; +} + +static void complain_no_suitable_type(const struct types *types) { + if (types->any == NULL) { + /* Report this the same way as + * there being no offer at all. + */ + bail("Nothing is copied"); + } + fprintf(stderr, "Clipboard content is not available as "); + if (options.explicit_type != NULL) { + fprintf(stderr, "requested type \"%s\"\n", options.explicit_type); + } else { + assert(options.inferred_type); + fprintf( + stderr, + "inferred output type \"%s\"\n", + options.inferred_type + ); + } + fprintf(stderr, "Use \"wl-paste --list-types\" to view available types."); + if (options.explicit_type == NULL) { + fprintf(stderr, " Use \"--type\" to explicitly specify a type."); + } + fputc('\n', stderr); + exit(1); +} + static void selection_callback(struct offer *offer, int primary) { + /* Ignore all but the first non-NULL offer. + * This could happen due to reentrancy, though + * we try to prevent it in other ways. + */ + if (offer_received && !options.watch) { + return; + } + /* Ignore events we're not interested in */ if (primary != options.primary) { if (offer != NULL) { @@ -181,12 +270,20 @@ static void selection_callback(struct offer *offer, int primary) { } if (offer == NULL) { - if (options.watch) { + if (!options.watch) { + bail("Nothing is copied"); + } + int devnull = open("/dev/null", O_RDONLY | O_CLOEXEC); + if (devnull < 0) { + perror("open /dev/null"); return; } - bail("No selection"); + run_paste_command(devnull, "nil", NULL); + return; } + offer_received = 1; + if (options.list_types) { offer_for_each_mime_type(offer, mime_type) { printf("%s\n", mime_type); @@ -202,7 +299,7 @@ static void selection_callback(struct offer *offer, int primary) { offer_destroy(offer); return; } - bail("No suitable type of content copied"); + complain_no_suitable_type(&types); } /* Never append a newline character to binary content */ @@ -230,45 +327,28 @@ static void selection_callback(struct offer *offer, int primary) { popup_surface_destroy(popup_surface); popup_surface = NULL; } - wl_display_roundtrip(wl_display); - - /* Spawn a cat to perform the copy. - * If watch mode is active, we spawn - * a custom command instead. + /* Make sure the receive request reaches + * the compositor before we block on reading. + * We call flush() instead of dispatch() to + * prevent reentrancy. */ - pid_t pid = fork(); - if (pid < 0) { - perror("fork"); + wl_display_flush(wl_display); + + close(pipefd[1]); + const char *clipboard_state = "data"; + if (types.has_sensitive_hint) { + clipboard_state = "sensitive"; + } + rc = run_paste_command(pipefd[0], clipboard_state, mime_type); + if (!rc) { 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]); - 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 && !options.watch) { rc = write(STDOUT_FILENO, "\n", 1); if (rc != 1) { @@ -418,7 +498,10 @@ int main(int argc, argv_t argv) { wl_display = wl_display_connect(NULL); if (wl_display == NULL) { - bail("Failed to connect to a Wayland server"); + complain_about_wayland_connection(); + } + if (wl_display_get_fd(wl_display) <= STDERR_FILENO) { + complain_about_closed_stdio(wl_display); } struct registry *registry = calloc(1, sizeof(struct registry)); @@ -430,11 +513,7 @@ int main(int argc, argv_t argv) { 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"); - } + complain_about_missing_seat(options.seat_name); } /* Create the device */ diff --git a/subprojects/expat.wrap b/subprojects/expat.wrap new file mode 100644 index 0000000..bda9011 --- /dev/null +++ b/subprojects/expat.wrap @@ -0,0 +1,13 @@ +[wrap-file] +directory = expat-2.5.0 +source_url = https://github.com/libexpat/libexpat/releases/download/R_2_5_0/expat-2.5.0.tar.xz +source_filename = expat-2.5.0.tar.bz2 +source_hash = ef2420f0232c087801abf705e89ae65f6257df6b7931d37846a193ef2e8cdcbe +patch_filename = expat_2.5.0-3_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/expat_2.5.0-3/get_patch +patch_hash = e30c8c32f79fd4563a86a5c9cfe419568c35b775077c2d1428170081129406b4 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/expat_2.5.0-3/expat-2.5.0.tar.bz2 +wrapdb_version = 2.5.0-3 + +[provide] +expat = expat_dep diff --git a/subprojects/libffi.wrap b/subprojects/libffi.wrap new file mode 100644 index 0000000..18baccc --- /dev/null +++ b/subprojects/libffi.wrap @@ -0,0 +1,13 @@ +[wrap-file] +directory = libffi-3.5.2 +source_url = https://github.com/libffi/libffi/releases/download/v3.5.2/libffi-3.5.2.tar.gz +source_filename = libffi-3.5.2.tar.gz +source_hash = f3a3082a23b37c293a4fcd1053147b371f2ff91fa7ea1b2a52e335676bac82dc +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/libffi_3.5.2-2/libffi-3.5.2.tar.gz +patch_filename = libffi_3.5.2-2_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/libffi_3.5.2-2/get_patch +patch_hash = 74ed624f74cd860be3bdf6d473b70ab88707bdf2f940191fbcb577e2a49a9710 +wrapdb_version = 3.5.2-2 + +[provide] +dependency_names = libffi diff --git a/subprojects/wayland-protocols.wrap b/subprojects/wayland-protocols.wrap new file mode 100644 index 0000000..71650c7 --- /dev/null +++ b/subprojects/wayland-protocols.wrap @@ -0,0 +1,8 @@ +[wrap-file] +directory = wayland-protocols-1.24 +source_url = https://wayland.freedesktop.org/releases/wayland-protocols-1.24.tar.xz +source_filename = wayland-protocols-1.24.tar.xz +source_hash = bff0d8cffeeceb35159d6f4aa6bab18c807b80642c9d50f66cba52ecf7338bc2 + +[provide] +wayland-protocols = wayland_protocols diff --git a/subprojects/wayland.wrap b/subprojects/wayland.wrap new file mode 100644 index 0000000..e0991a2 --- /dev/null +++ b/subprojects/wayland.wrap @@ -0,0 +1,9 @@ +[wrap-file] +directory = wayland-1.22.0 +source_url = https://gitlab.freedesktop.org/wayland/wayland/-/releases/1.22.0/downloads/wayland-1.22.0.tar.xz +source_filename = wayland-1.22.0.tar.xz +source_hash = 1540af1ea698a471c2d8e9d288332c7e0fd360c8f1d12936ebb7e7cbc2425842 + +[provide] +dependency_names = wayland-client, wayland-server, wayland-cursor, wayland-egl +program_names = wayland-scanner