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 8bb01cd..b0ebd16 100644 --- a/README.md +++ b/README.md @@ -4,93 +4,45 @@ 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. -* `-l`, `--list-types` Instead of pasting the selection, output the list of MIME types it is offered in. - -For both: - -* `-p`, `--primary` Use the "primary" clipboard instead of the regular clipboard. This uses the private GTK+ primary selection protocol. See [the GNOME Wiki page on primary selection under Wayland](https://wiki.gnome.org/Initiatives/Wayland/PrimarySelection) for more details. -* `-t mime/type`, `--type mime/type` Override the inferred MIME type for the content. For `wl-copy` this option controls which type `wl-copy` will offer the content as. For `wl-paste` it controls which of the offered types `wl-paste` will request the content in. In addition to specific MIME types such as _image/png_, `wl-paste` also accepts generic type names such as _text_ and _image_ which make it automatically pick some offered MIME type that matches the given generic name. -* `-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. The only mandatory dependency is -the `wayland-client` library (try package named `wayland-devel` or -`libwayland-dev`). - -Optional dependencies for building: -* `wayland-scanner` for primary selection support using the bundled [gtk-primary-selection protocol](src/protocol/gtk-primary-selection.xml) -* `wayland-protocols` (version 1.12 or later) for xdg-shell support (otherwise it won't run under compositors lacking `wl_shell` support, see [the issue #2](https://github.com/bugaevc/wl-clipboard/issues/2)) - -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 wl-clipboard is free software, available under the GNU General Public License 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`. diff --git a/completions/bash/meson.build b/completions/bash/meson.build new file mode 100644 index 0000000..b618d4e --- /dev/null +++ b/completions/bash/meson.build @@ -0,0 +1,4 @@ +bash_completion_dir = join_paths(get_option('datadir'), 'bash-completion', 'completions') + +install_data('wl-copy', install_dir: bash_completion_dir) +install_data('wl-paste', install_dir: bash_completion_dir) diff --git a/completions/bash/wl-copy b/completions/bash/wl-copy new file mode 100644 index 0000000..83ec084 --- /dev/null +++ b/completions/bash/wl-copy @@ -0,0 +1,62 @@ +# wl-copy(1) completion + +_wl_clipboard_list_seats() { + weston-info 2>/dev/null | sed -n '/wl_seat/{n;s/\s*name: //;p}' +} + +_wl_clipboard_complete_types() { + local cur prev types + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + types="$1" + if [ "${cur:0:1}" = \' -o "${cur:0:1}" = \" ]; then + COMPREPLY=($(compgen -W "$types" -- "$cur")) + else + COMPREPLY=($(compgen -W "$types" -- "$cur" | sed 's/;/\\;/g')) + fi +} + +_wl_copy_completion() { + compopt +o default + local cur prev opts types seats + + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + + case "$cur" in + "<"*) + compopt -o default + COMPREPLY=() + return + ;; + esac + + case "$prev" in + "<"*) + compopt -o default + COMPREPLY=() + ;; + -t | -[a-z]*t | --type) + types="$(sed -e 's/#.*$//' -e '/^$/d' -e 's/^\(\S\+\).*/\1/' /etc/mime.types 2>/dev/null)" + _wl_clipboard_complete_types "$types" + ;; + -s | -[a-z]*s | --seat) + seats="$(_wl_clipboard_list_seats)" + COMPREPLY=($(compgen -W "$seats" -- "$cur")) + ;; + *) + opts="-o --paste-once " + opts+="-f --foreground " + opts+="-c --clear " + opts+="-p --primary " + opts+="-n --trim-newline " + opts+="-t --type " + opts+="-s --seat " + opts+="-v --version " + opts+="-h --help " + COMPREPLY=($(compgen -W "$opts" -- "$cur")) + ;; + esac +} + +complete -o default -F _wl_copy_completion wl-copy diff --git a/completions/bash/wl-paste b/completions/bash/wl-paste new file mode 100644 index 0000000..44d9708 --- /dev/null +++ b/completions/bash/wl-paste @@ -0,0 +1,98 @@ +# wl-paste(1) completion + +_wl_clipboard_list_seats() { + weston-info 2>/dev/null | sed -n '/wl_seat/{n;s/\s*name: //;p}' +} + +_wl_clipboard_complete_types() { + local cur prev types + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + types="$1" + if [ "${cur:0:1}" = \' -o "${cur:0:1}" = \" ]; then + COMPREPLY=($(compgen -W "$types" -- "$cur")) + else + COMPREPLY=($(compgen -W "$types" -- "$cur" | sed 's/;/\\;/g')) + fi +} + +_wl_clipboard_complete_paste_types() { + local i cur primary seat cmd types + for (( i = 0; i < "${#COMP_WORDS[@]}"; ++i )); do + cur="${COMP_WORDS[i]}" + case "$cur" in + -p* | -[a-z]*p* | --primary) + primary="yes" + ;; + esac + case "$cur" in + -s | -[a-z]*s | --seat) + seat="${COMP_WORDS[i+1]}" + ;; + -s* | -[a-z]s*) + seat=$(echo "$cur" | sed 's/-[^s]*s//') + ;; + esac + done + cmd="${COMP_WORDS[0]}" + if [ -n "$primary" ]; then + cmd="$cmd -p" + fi + if [ -n "$seat" ]; then + cmd="$cmd -s $seat" + fi + cmd="$cmd -l" + types="$($cmd 2>/dev/null)" + _wl_clipboard_complete_types "$types" +} + +_wl_paste_completion() { + compopt +o default + local cur prev opts types seats offset + + for (( offset=1; offset < COMP_CWORD; offset++ )); do + cur="${COMP_WORDS[offset]}" + if [ "$cur" = "-w" -o "$cur" = "--watch" ]; then + _command_offset $(($offset+1)) + return + fi + done + + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + + case "$cur" in + ">"*) + compopt -o default + COMPREPLY=() + return + ;; + esac + + case "$prev" in + ">"*) + compopt -o default + COMPREPLY=() + ;; + -t | -[a-z]*t | --type) + _wl_clipboard_complete_paste_types + ;; + -s | -[a-z]*s | --seat) + seats="$(_wl_clipboard_list_seats)" + COMPREPLY=($(compgen -W "$seats" -- "$cur")) + ;; + *) + opts="-n --no-newline " + opts+="-l --list-types " + opts+="-p --primary " + opts+="-w --watch " + opts+="-t --type " + opts+="-s --seat " + opts+="-v --version " + opts+="-h --help " + COMPREPLY=($(compgen -W "$opts" -- "$cur")) + ;; + esac +} + +complete -o default -F _wl_paste_completion wl-paste diff --git a/completions/fish/meson.build b/completions/fish/meson.build new file mode 100644 index 0000000..cb1280c --- /dev/null +++ b/completions/fish/meson.build @@ -0,0 +1,22 @@ +fish_completion_dir = get_option('fishcompletiondir') +fish = dependency('fish', required : false) + +if fish_completion_dir == '' and fish.found() + 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 + # custom completions to be installed into /usr/share/fish/vendor_completions.d, + # so we use that. Note that this is a fallback in case you neither + # have fish installed, nor have specified a path explicitly + fish_completion_dir = '/usr/share/fish/vendor_completions.d' +endif + +if fish_completion_dir != 'no' + install_data('wl-copy.fish', install_dir: fish_completion_dir) + install_data('wl-paste.fish', install_dir: fish_completion_dir) +endif diff --git a/completions/fish/wl-copy.fish b/completions/fish/wl-copy.fish new file mode 100644 index 0000000..dafd32a --- /dev/null +++ b/completions/fish/wl-copy.fish @@ -0,0 +1,21 @@ +function __wayland_seats --description 'Print all wayland seats' + if type -q weston-info + weston-info 2>/dev/null | sed -n '/wl_seat/{n;s/\s*name: //;p}' + else if type -q loginctl + loginctl --no-legend --no-pager list-seats 2>/dev/null + else + # Fallback seat + echo "seat0" + end + +end + +complete -c wl-copy -x +complete -c wl-copy -s h -l help -d 'Display a help message' +complete -c wl-copy -s v -l version -d 'Display version info' +complete -c wl-copy -s o -l paste-once -d 'Only serve one paste request and then exit' +complete -c wl-copy -s c -l clear -d 'Instead of copying anything, clear the clipboard' +complete -c wl-copy -s p -l primary -d 'Use the "primary" clipboard' +complete -c wl-copy -s n -l trim-newline -d 'Do not copy the trailing newline character' +complete -c wl-copy -s t -l type -x -d 'Override the inferred MIME type for the content' -a "(__fish_print_xdg_mimetypes)" +complete -c wl-copy -s s -l seat -x -d 'Pick the seat to work with' -a "(__wayland_seats)" diff --git a/completions/fish/wl-paste.fish b/completions/fish/wl-paste.fish new file mode 100644 index 0000000..04df3a4 --- /dev/null +++ b/completions/fish/wl-paste.fish @@ -0,0 +1,46 @@ +function __wayland_seats --description 'Print all wayland seats' + if type -q weston-info + weston-info 2>/dev/null | sed -n '/wl_seat/{n;s/\s*name: //;p}' + else if type -q loginctl + loginctl --no-legend --no-pager list-seats 2>/dev/null + else + # Fallback seat + echo "seat0" + end +end + +function __wl_paste_types --description 'Print types with context' + set -l type (commandline -o) + + for i in (seq (count $type)) + if [ "$type[$i]" = '--primary' ]; or [ "$type[$i]" = '-p' ] + set clip "primary" + else if [ "$type[$i]" = '--seat' ]; or [ "$type[$i]" = '-s' ] + set seat $type[(math $i + 1)] + end + end + + # Note fish does not handle passing unset variables + # to commands well, thus setting clip to "--primary" and passing + # 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" + wl-paste 2>/dev/null --seat "$seat" -l + else if test -n "$clip" + wl-paste 2>/dev/null -p -l + else + wl-paste 2>/dev/null -l + end +end + +complete -c wl-paste -f +complete -c wl-paste -f -n '__fish_contains_opt -s w watch' -a "(__fish_complete_subcommand -- -s --seat -t --type)" +complete -c wl-paste -n '__fish_not_contain_opt -s w watch' -s h -l help -d 'Display a help message' +complete -c wl-paste -n '__fish_not_contain_opt -s w watch' -s v -l version -d 'Display version info' +complete -c wl-paste -n '__fish_not_contain_opt -s w watch' -s n -l no-newline -d 'Do not append a newline character' +complete -c wl-paste -n '__fish_not_contain_opt -s w watch' -s l -l list-types -d 'Instead of pasting, list the offered types' +complete -c wl-paste -n '__fish_not_contain_opt -s w watch' -s p -l primary -d 'Use the "primary" clipboard' +complete -c wl-paste -n '__fish_not_contain_opt -s w watch' -s w -l watch -d 'Run a command each time the selection changes' +complete -c wl-paste -n '__fish_not_contain_opt -s w watch' -s t -l type -x -d 'Override the inferred MIME type for the content' -a "(__wl_paste_types)" +complete -c wl-paste -n '__fish_not_contain_opt -s w watch' -s s -l seat -x -d 'Pick the seat to work with' -a "(__wayland_seats)" diff --git a/completions/meson.build b/completions/meson.build new file mode 100644 index 0000000..96542c5 --- /dev/null +++ b/completions/meson.build @@ -0,0 +1,3 @@ +subdir('bash') +subdir('zsh') +subdir('fish') diff --git a/completions/zsh/_wl-copy b/completions/zsh/_wl-copy new file mode 100644 index 0000000..197a5b0 --- /dev/null +++ b/completions/zsh/_wl-copy @@ -0,0 +1,42 @@ +#compdef wl-copy + +__xdg_mimetypes() { + local expl SEARCH=${XDG_DATA_HOME:-$HOME/.local/share} + SEARCH+=:${XDG_DATA_DIRS:-/usr/share:/usr/local/share} + + local -a _all_mimetypes + for dir in ${(s.:.)SEARCH}; do + [[ -f "${dir}/mime/types" ]] && + _all_mimetypes+=(${(f)"$(<"${dir}/mime/types")"}) + done + + _wanted mimetypes expl 'mimetypes' _multi_parts / _all_mimetypes +} + +__all_seats() { + local -a seats + if (( $+commands[weston-info] && $+commands[sed] )); then + seats=( ${(@f)"$(weston-info 2>/dev/null | sed -n '/wl_seat/{n;s/\s*name: //;p}')"} ) + elif (( $+commands[loginctl] )); then + seats=( ${(@f)"$(loginctl --no-legend --no-pager list-seats 2>/dev/null)"} ) + fi + + if [[ -z $seats ]]; then + # seat0 is always a valid seat and covers most cases, so it's a good fallback. + compadd "$@" - seat0 + else + compadd "$@" -a seats + fi +} + +_arguments -S -s \ + {-o,--paste-once}'[Only serve one paste request and then exit]' \ + {-f,--foreground}'[Stay in the foreground instead of forking]' \ + {-c,--clear}'[Instead of copying anything, clear the clipboard]' \ + {-p,--primary}'[Use the "primary" clipboard]' \ + {-n,--trim-newline}'[Do not copy the trailing newline character]' \ + {-t+,--type=}'[Override the inferred MIME type for the content]:mimetype:__xdg_mimetypes' \ + {-s+,--seat=}'[Pick the seat to work with]:seat:__all_seats' \ + {-v,--version}'[Display version info]' \ + {-h,--help}'[Display a help message]' \ + '*::text' diff --git a/completions/zsh/_wl-paste b/completions/zsh/_wl-paste new file mode 100644 index 0000000..852d630 --- /dev/null +++ b/completions/zsh/_wl-paste @@ -0,0 +1,42 @@ +#compdef wl-paste + +local expl +typeset -A opt_args + +_wl-paste_types() { + local -a types args + args=( + ${(kv)opt_args[(I)-p|--primary]} + ${(kv)opt_args[(I)-s|--seat]} + ) + types=( ${(@f)"$(wl-paste $args -l 2>/dev/null)"} ) + + local expl + _wanted types expl 'type' compadd "$@" -a types +} + +__all_seats() { + local -a seats + if (( $+commands[weston-info] && $+commands[sed] )); then + seats=( ${(@f)"$(weston-info 2>/dev/null | sed -n '/wl_seat/{n;s/\s*name: //;p}')"} ) + elif (( $+commands[loginctl] )); then + seats=( ${(@f)"$(loginctl --no-legend --no-pager list-seats 2>/dev/null)"} ) + fi + + if [[ -z $seats ]]; then + # seat0 covers most cases, so its a good fallback. + compadd "$@" - seat0 + else + compadd "$@" -a seats + fi +} + +_arguments -S -s \ + {-n,--no-newline}'[Do not append a newline character]' \ + {-l,--list-types}'[Instead of pasting, list the offered types]' \ + {-p,--primary}'[Use the "primary" clipboard]' \ + {-w,--watch}'[Run a command wach time the selection changes]:*::command:_normal' \ + {-t+,--type=}'[Override the inferred MIME type for the content]:mimetype:_wl-paste_types' \ + {-s+,--seat=}'[Pick the seat to work with]:seat:__all_seats' \ + {-v,--version}'[Display version info]' \ + {-h,--help}'[Display a help message]' diff --git a/completions/zsh/meson.build b/completions/zsh/meson.build new file mode 100644 index 0000000..dc2820d --- /dev/null +++ b/completions/zsh/meson.build @@ -0,0 +1,9 @@ +zsh_completion_dir = get_option('zshcompletiondir') +if zsh_completion_dir == '' + zsh_completion_dir = join_paths(get_option('datadir'), 'zsh', 'site-functions') +endif + +if zsh_completion_dir != 'no' + install_data('_wl-copy', install_dir: zsh_completion_dir) + install_data('_wl-paste', install_dir: zsh_completion_dir) +endif diff --git a/data/wl-clipboard.1 b/data/wl-clipboard.1 index b080201..4243a91 100644 --- a/data/wl-clipboard.1 +++ b/data/wl-clipboard.1 @@ -1,23 +1,17 @@ -.TH WL-CLIPBOARD 1 2019-01-22 wl-clipboard +.TH WL-CLIPBOARD 1 2026-08-06 wl-clipboard .SH NAME -wl-clipboard \- Wayland copy and paste command line utiltites +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\-\-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. @@ -31,40 +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 -Use the "primary" clipboard instead of the regular clipboard. This uses the -private GTK+ primary selection protocol. See -.UR https://wiki.gnome.org/Initiatives/Wayland/PrimarySelection -the GNOME Wiki page on primary selection under Wayland -.UE -for more details. +\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. +option is automatically enabled for non-text content types and when using the +\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 @@ -72,12 +69,12 @@ 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 name of the default seat is likely \fIdefault\fR or \fIseat0\fR, and additional -seat names normally come form +seat names normally come from the .BR udev (7) property \fBENV{WL_SEAT}\fR. You can view the list of the currently available seats as advertised by the compositor using the @@ -86,57 +83,184 @@ 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-v\fR, \fB--version +\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\-\-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 aplications, that connect to this Wayland server. It +clients, such as graphical applications, that connect to this Wayland server. It is normally set up automatically by the graphical session and the Wayland compositor. See .BR wl_display_connect (3) for more details. .TP -WAYLAND_DEBUG +.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. There is NO WARRANTY, to the extent permitted by law. .SH SEE ALSO -.BR xclip (1) +.BR xclip (1), +.BR xsel (1), +.BR wl-clipboard-x11 (1) diff --git a/meson.build b/meson.build index 673e9f5..3109a50 100644 --- a/meson.build +++ b/meson.build @@ -1,8 +1,10 @@ project('wl-clipboard', 'c', - version: '1.0.0', - 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' ) subdir('src') subdir('data') +subdir('completions') diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 0000000..b6e621c --- /dev/null +++ b/meson_options.txt @@ -0,0 +1,13 @@ +option('protocols', + type: 'feature', + value: 'enabled') +option('zshcompletiondir', + type: 'string', + value: '', + description: 'Directory for zsh completions. Set "no" to disable.' +) +option('fishcompletiondir', + type: 'string', + value: '', + description: 'Directory for fish completions. Set "no" to disable.' +) diff --git a/src/boilerplate.c b/src/boilerplate.c deleted file mode 100644 index ec2d632..0000000 --- a/src/boilerplate.c +++ /dev/null @@ -1,855 +0,0 @@ -/* wl-clipboard - * - * Copyright © 2019 Sergey Bugaev - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "boilerplate.h" - -void registry_global_handler -( - void *data, - struct wl_registry *registry, - uint32_t name, - const char *interface, - uint32_t version -) { - if (strcmp(interface, "wl_data_device_manager") == 0) { - data_device_manager = wl_registry_bind( - registry, - name, - &wl_data_device_manager_interface, - 1 - ); - } else if (strcmp(interface, "wl_seat") == 0) { - struct wl_seat *new_seat = wl_registry_bind( - registry, - name, - &wl_seat_interface, - 2 - ); - process_new_seat(new_seat); - } else if (strcmp(interface, "wl_compositor") == 0) { - compositor = wl_registry_bind( - registry, - name, - &wl_compositor_interface, - 3 - ); - } else if (strcmp(interface, "wl_shm") == 0) { - shm = wl_registry_bind( - registry, - name, - &wl_shm_interface, - 1 - ); - } else if (strcmp(interface, "wl_shell") == 0) { - shell = wl_registry_bind( - registry, - name, - &wl_shell_interface, - 1 - ); - } -#ifdef HAVE_XDG_SHELL - else if (strcmp(interface, "xdg_wm_base") == 0) { - xdg_wm_base = wl_registry_bind( - registry, - name, - &xdg_wm_base_interface, - 1 - ); - } -#endif -#ifdef HAVE_WLR_LAYER_SHELL - else if (strcmp(interface, "zwlr_layer_shell_v1") == 0) { - layer_shell = wl_registry_bind( - registry, - name, - &zwlr_layer_shell_v1_interface, - 1 - ); - } -#endif -#ifdef HAVE_GTK_PRIMARY_SELECTION - else if (strcmp(interface, "gtk_primary_selection_device_manager") == 0) { - gtk_primary_selection_device_manager = wl_registry_bind( - registry, - name, - >k_primary_selection_device_manager_interface, - 1 - ); - } -#endif -#ifdef HAVE_WP_PRIMARY_SELECTION - else if (strcmp(interface, "zwp_primary_selection_device_manager_v1") == 0) { - primary_selection_device_manager = wl_registry_bind( - registry, - name, - &zwp_primary_selection_device_manager_v1_interface, - 1 - ); - } -#endif -#ifdef HAVE_WLR_DATA_CONTROL - else if (strcmp(interface, "zwlr_data_control_manager_v1") == 0) { - if (version > 2) { - // the maximum verison wl-clipboard knows about - version = 2; - } - data_control_manager = wl_registry_bind( - registry, - name, - &zwlr_data_control_manager_v1_interface, - version - ); - data_control_version = version; - } -#endif -} - -void registry_global_remove_handler -( - void *data, - struct wl_registry *registry, - uint32_t name -) {} - -const struct wl_registry_listener registry_listener = { - .global = registry_global_handler, - .global_remove = registry_global_remove_handler -}; - -void keyboard_keymap_handler -( - void *data, - struct wl_keyboard *keyboard, - uint32_t format, - int fd, - uint32_t size -) { - close(fd); -} - -void keyboard_enter_handler -( - void *data, - struct wl_keyboard *keyboard, - uint32_t serial, - struct wl_surface *surface, - struct wl_array *keys -) { - struct wl_seat *this_seat = (struct wl_seat *) data; - // when we get to here, global seat is already initialized - if (this_seat != seat) { - return; - } - if (action_on_popup_surface_getting_focus != NULL) { - action_on_popup_surface_getting_focus(serial); - } -} - -void keyboard_leave_handler -( - void *data, - struct wl_keyboard *keyboard, - uint32_t serial, - struct wl_surface *surface -) {} - -void keyboard_key_handler -( - void *data, - struct wl_keyboard *keyboard, - uint32_t serial, - uint32_t time, - uint32_t key, - uint32_t state -) {} - -void keyboard_modifiers_handler -( - void *data, - struct wl_keyboard *keyboard, - uint32_t serial, - uint32_t mods_depressed, - uint32_t mods_latched, - uint32_t mods_locked, - uint32_t group -) {} - -const struct wl_keyboard_listener keayboard_listener = { - .keymap = keyboard_keymap_handler, - .enter = keyboard_enter_handler, - .leave = keyboard_leave_handler, - .key = keyboard_key_handler, - .modifiers = keyboard_modifiers_handler, -}; - -void seat_capabilities_handler -( - void *data, - struct wl_seat *this_seat, - uint32_t capabilities -) { - // stash the capabilities of this seat for later - void *user_data = (void *) (unsigned long) capabilities; - wl_seat_set_user_data(this_seat, user_data); - - if (capabilities & WL_SEAT_CAPABILITY_KEYBOARD) { - struct wl_keyboard *keyboard = wl_seat_get_keyboard(this_seat); - wl_keyboard_add_listener(keyboard, &keayboard_listener, this_seat); - } -} - -void seat_name_handler -( - void *data, - struct wl_seat *this_seat, - const char *name -) { - if (requested_seat_name == NULL) { - return; - } - if (strcmp(name, requested_seat_name) == 0) { - seat = this_seat; - } -} - -const struct wl_seat_listener seat_listener = { - .capabilities = seat_capabilities_handler, - .name = seat_name_handler -}; - -#define UNSET_CAPABILITIES ((void *) (uint32_t) 35) - -void process_new_seat(struct wl_seat *new_seat) { - if (seat != NULL) { - wl_seat_destroy(new_seat); - return; - } - if (requested_seat_name == NULL) { - seat = new_seat; - } - wl_seat_add_listener(new_seat, &seat_listener, UNSET_CAPABILITIES); -} - -int ensure_seat_has_keyboard() { - void *user_data = wl_seat_get_user_data(seat); - while (user_data == UNSET_CAPABILITIES) { - wl_display_roundtrip(display); - user_data = wl_seat_get_user_data(seat); - } - - uint32_t capabilities = (uint32_t) (unsigned long) user_data; - if (capabilities & WL_SEAT_CAPABILITY_KEYBOARD) { - return 1; - } - - if (action_on_no_keyboard != NULL) { - action_on_no_keyboard(); - } - return 0; -} - -#undef UNSET_CAPABILITIES - -void shell_surface_ping -( - void *data, - struct wl_shell_surface *shell_surface, - uint32_t serial -) { - wl_shell_surface_pong(shell_surface, serial); -} - -void shell_surface_configure -( - void *data, - struct wl_shell_surface *shell_surface, - uint32_t edges, - int32_t width, - int32_t height -) {} - -void shell_surface_popup_done -( - void *data, - struct wl_shell_surface *shell_surface -) {} - -const struct wl_shell_surface_listener shell_surface_listener = { - .ping = shell_surface_ping, - .configure = shell_surface_configure, - .popup_done = shell_surface_popup_done -}; - -#ifdef HAVE_XDG_SHELL - -void xdg_toplevel_configure_handler -( - void *data, - struct xdg_toplevel *xdg_toplevel, - int32_t width, - int32_t height, - struct wl_array *states -) {} - -void xdg_toplevel_close_handler -( - void *data, - struct xdg_toplevel *xdg_toplevel -) {} - -const struct xdg_toplevel_listener xdg_toplevel_listener = { - .configure = xdg_toplevel_configure_handler, - .close = xdg_toplevel_close_handler -}; - -void xdg_surface_configure_handler -( - void *data, - struct xdg_surface *xdg_surface, - uint32_t serial -) { - xdg_surface_ack_configure(xdg_surface, serial); -} - -const struct xdg_surface_listener xdg_surface_listener = { - .configure = xdg_surface_configure_handler -}; - -void xdg_wm_base_ping_handler -( - void *data, - struct xdg_wm_base *xdg_wm_base, - uint32_t serial -) { - xdg_wm_base_pong(xdg_wm_base, serial); -} - -const struct xdg_wm_base_listener xdg_wm_base_listener = { - .ping = xdg_wm_base_ping_handler -}; - -#endif - -#ifdef HAVE_WLR_LAYER_SHELL - -void layer_surface_configure_handler -( - void *data, - struct zwlr_layer_surface_v1 *layer_surface, - uint32_t serial, - uint32_t width, - uint32_t height -) { - zwlr_layer_surface_v1_ack_configure(layer_surface, serial); -} - -void layer_surface_closed_handler -( - void *data, - struct zwlr_layer_surface_v1 *layer_surface -) {} - -const struct zwlr_layer_surface_v1_listener layer_surface_listener = { - .configure = layer_surface_configure_handler, - .closed = layer_surface_closed_handler -}; - -#endif - -#ifdef HAVE_WLR_DATA_CONTROL - -void data_control_manager_primary_selection -( - void *data, - struct zwlr_data_control_manager_v1 *data_control_manager -) { - data_control_supports_primary_selection = 1; -} - -const struct zwlr_data_control_manager_v1_listener -data_control_manager_listener = { - .primary_selection = data_control_manager_primary_selection -}; - -#endif - -void init_wayland_globals() { - display = wl_display_connect(NULL); - if (display == NULL) { - bail("Failed to connect to a Wayland server"); - } - - struct wl_registry *registry = wl_display_get_registry(display); - wl_registry_add_listener(registry, ®istry_listener, NULL); - - // wait for the "initial" set of globals to appear - wl_display_roundtrip(display); - - if ( - data_device_manager == NULL || - compositor == NULL || - shm == NULL || - (shell == NULL -#ifdef HAVE_XDG_SHELL - && xdg_wm_base == NULL -#endif -#ifdef HAVE_WLR_LAYER_SHELL - && layer_shell == NULL -#endif - ) - ) { - bail("Missing a required global object"); - } - - if (seat == NULL && requested_seat_name != NULL) { - wl_display_roundtrip(display); - } - if (seat == NULL) { - if (requested_seat_name == NULL) { - bail("No seat available"); - } - bail("Cannot find the requested seat"); - } - - data_device = wl_data_device_manager_get_data_device( - data_device_manager, - seat - ); -#ifdef HAVE_GTK_PRIMARY_SELECTION - if (gtk_primary_selection_device_manager != NULL) { - gtk_primary_selection_device = - gtk_primary_selection_device_manager_get_device( - gtk_primary_selection_device_manager, - seat - ); - } -#endif -#ifdef HAVE_WP_PRIMARY_SELECTION - if (primary_selection_device_manager != NULL) { - primary_selection_device = - zwp_primary_selection_device_manager_v1_get_device( - primary_selection_device_manager, - seat - ); - } -#endif -#ifdef HAVE_WLR_DATA_CONTROL - if (data_control_manager != NULL) { - zwlr_data_control_manager_v1_add_listener( - data_control_manager, - &data_control_manager_listener, - NULL - ); - data_control_device = - zwlr_data_control_manager_v1_get_data_device( - data_control_manager, - seat - ); - } -#endif -} - -void ensure_has_primary_selection() { -#ifdef HAVE_WLR_DATA_CONTROL - if (data_control_version >= - ZWLR_DATA_CONTROL_MANAGER_V1_PRIMARY_SELECTION_SINCE_VERSION) { - - if (!data_control_supports_primary_selection) { - wl_display_roundtrip(display); - } - if (data_control_supports_primary_selection) { - return; - } - } -#endif -#ifdef HAVE_GTK_PRIMARY_SELECTION - if (gtk_primary_selection_device_manager != NULL) { - return; - } -#endif -#ifdef HAVE_WP_PRIMARY_SELECTION - if (primary_selection_device_manager != NULL) { - return; - } -#endif - -#if defined(HAVE_GTK_PRIMARY_SELECTION) || defined(HAVE_WP_PRIMARY_SELECTION) - bail("Primary selection is not supported on this compositor"); -#else - bail("wl-clipboard was built without primary selection support"); -#endif -} - -int create_anonymous_file() { -#ifdef HAVE_MEMFD - return syscall(SYS_memfd_create, "buffer", 0); -#endif -#ifdef HAVE_SHM_ANON - return shm_open(SHM_ANON, O_RDWR | O_CREAT, 0600); -#endif - return fileno(tmpfile()); -} - -void popup_tiny_invisible_surface() { - // HACK: - // pop up a tiny invisible surface to get the keyboard focus, - // otherwise we won't be notified of the selection - - if (!ensure_seat_has_keyboard()) { - return; - } - - // make sure that we get the keyboard - // object before creating the surface, - // so that we get the enter event - wl_display_dispatch(display); - - surface = wl_compositor_create_surface(compositor); - -#ifdef HAVE_WLR_LAYER_SHELL - if (layer_shell != NULL) { - // use wlr-layer-shell - layer_surface = zwlr_layer_shell_v1_get_layer_surface( - layer_shell, - surface, - NULL, // output - ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY, - "wl-clipboard" // namespace - ); - zwlr_layer_surface_v1_add_listener( - layer_surface, - &layer_surface_listener, - NULL - ); - zwlr_layer_surface_v1_set_keyboard_interactivity(layer_surface, 1); - // signal that the surface is ready to be configured - wl_surface_commit(surface); - // wait for the surface to be configured - wl_display_roundtrip(display); - } else -#endif - if (shell != NULL) { - // use wl_shell - shell_surface = wl_shell_get_shell_surface(shell, surface); - wl_shell_surface_set_toplevel(shell_surface); - wl_shell_surface_set_title(shell_surface, "wl-clipboard"); - } else { -#ifdef HAVE_XDG_SHELL - // use xdg-shell - xdg_wm_base_add_listener(xdg_wm_base, &xdg_wm_base_listener, NULL); - xdg_surface = xdg_wm_base_get_xdg_surface(xdg_wm_base, surface); - xdg_surface_add_listener(xdg_surface, &xdg_surface_listener, NULL); - xdg_toplevel = xdg_surface_get_toplevel(xdg_surface); - xdg_toplevel_add_listener(xdg_toplevel, &xdg_toplevel_listener, NULL); - xdg_toplevel_set_title(xdg_toplevel, "wl-clipboard"); - // signal that the surface is ready to be configured - wl_surface_commit(surface); - // wait for the surface to be configured - wl_display_roundtrip(display); -#else - bail("Unreachable: HAVE_XDG_SHELL undefined and no wl_shell"); -#endif - } - - if (surface == NULL) { - // it's possible that we've been given focus without us - // ever commiting a buffer, in which case the handlers - // may have already destroyed the surface; there's no - // way or need for us to commit a buffer in that case - return; - } - - int width = 1; - int height = 1; - int stride = width * 4; - int size = stride * height; // bytes - - // open an anonymous file and write some zero bytes to it - int fd = create_anonymous_file(); - ftruncate(fd, size); - - // turn it into a shared memory pool - struct wl_shm_pool *pool = wl_shm_create_pool(shm, fd, size); - - // allocate the buffer in that pool - struct wl_buffer *buffer = wl_shm_pool_create_buffer(pool, - 0, width, height, stride, WL_SHM_FORMAT_ARGB8888); - // zeros in ARGB8888 mean fully transparent - - wl_surface_attach(surface, buffer, 0, 0); - wl_surface_damage(surface, 0, 0, width, height); - wl_surface_commit(surface); -} - -void destroy_popup_surface() { -#ifdef HAVE_WLR_LAYER_SHELL - if (layer_surface != NULL) { - zwlr_layer_surface_v1_destroy(layer_surface); - layer_surface = NULL; - } -#endif - if (shell_surface != NULL) { - wl_shell_surface_destroy(shell_surface); - shell_surface = NULL; - } -#ifdef HAVE_XDG_SHELL - if (xdg_toplevel != NULL) { - xdg_toplevel_destroy(xdg_toplevel); - xdg_toplevel = NULL; - } - if (xdg_surface != NULL) { - xdg_surface_destroy(xdg_surface); - xdg_surface = NULL; - } -#endif - if (surface != NULL) { - wl_surface_destroy(surface); - surface = NULL; - } -} - -static uint32_t global_serial; - -void callback_done -( - void *data, - struct wl_callback *callback, - uint32_t serial -) { - global_serial = serial; -} - -const struct wl_callback_listener callback_listener = { - .done = callback_done -}; - -uint32_t get_serial() { - struct wl_callback *callback = wl_display_sync(display); - wl_callback_add_listener(callback, &callback_listener, NULL); - - while (global_serial == 0) { - wl_display_dispatch(display); - } - - return global_serial; -} - -int mime_type_is_text(const char *mime_type) { - return str_has_prefix(mime_type, "text/") - || strcmp(mime_type, "TEXT") == 0 - || strcmp(mime_type, "STRING") == 0 - || strcmp(mime_type, "UTF8_STRING") == 0 - || str_has_suffix(mime_type, "script") - || str_has_suffix(mime_type, "xml") - || strstr(mime_type, "json") != NULL; -} - -int str_has_prefix(const char *string, const char *prefix) { - size_t prefix_length = strlen(prefix); - return strncmp(string, prefix, prefix_length) == 0; -} - -int str_has_suffix(const char *string, const char *suffix) { - size_t string_length = strlen(string); - size_t suffix_length = strlen(suffix); - if (string_length < suffix_length) { - return 0; - } - size_t offset = string_length - suffix_length; - return strcmp(string + offset, suffix) == 0; -} - -void print_version_info() { - printf( - "wl-clipboard " PROJECT_VERSION "\n" - "Copyright (C) 2019 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" - "There is NO WARRANTY, to the extent permitted by law.\n" - ); -} - -char *path_for_fd(int fd) { - char fdpath[64]; - snprintf(fdpath, sizeof(fdpath), "/dev/fd/%d", fd); - return realpath(fdpath, NULL); -} - -char *infer_mime_type_from_contents(const char *file_path) { - int pipefd[2]; - pipe(pipefd); - if (fork() == 0) { - dup2(pipefd[1], STDOUT_FILENO); - close(pipefd[0]); - close(pipefd[1]); - int devnull = open("/dev/null", O_RDONLY); - dup2(devnull, STDIN_FILENO); - close(devnull); - execlp("xdg-mime", "xdg-mime", "query", "filetype", file_path, NULL); - exit(1); - } - - close(pipefd[1]); - int wstatus; - wait(&wstatus); - if (WIFEXITED(wstatus) && WEXITSTATUS(wstatus) == 0) { - char *res = malloc(PATH_MAX + 1); - size_t len = read(pipefd[0], res, PATH_MAX + 1); - len--; // trim the newline - close(pipefd[0]); - res[len] = 0; - - if (str_has_prefix(res, "inode/")) { - free(res); - return NULL; - } - - return res; - } - close(pipefd[0]); - return NULL; -} - -const char *get_file_extension(const char *file_path) { - const char *name = strrchr(file_path, '/'); - if (name == NULL) { - name = file_path; - } - const char *ext = strrchr(name, '.'); - if (ext == NULL) { - return NULL; - } - return ext + 1; -} - -char *infer_mime_type_from_name(const char *file_path) { - const char *actual_ext = get_file_extension(file_path); - if (actual_ext == NULL) { - return NULL; - } - - FILE *f = fopen("/etc/mime.types", "r"); - if (f == NULL) { - f = fopen("/usr/local/etc/mime.types", "r"); - } - if (f == NULL) { - return NULL; - } - - for (char line[200]; fgets(line, sizeof(line), f) != NULL;) { - // skip comments and blank lines - if (line[0] == '#' || line[0] == '\n') { - continue; - } - - // each line consists of a mime type and a list of extensions - char mime_type[200]; - int consumed; - if (sscanf(line, "%s%n", mime_type, &consumed) != 1) { - // malformed line? - continue; - } - char *lineptr = line + consumed; - for (char ext[200]; sscanf(lineptr, "%s%n", ext, &consumed) == 1;) { - if (strcmp(ext, actual_ext) == 0) { - fclose(f); - return strdup(mime_type); - } - lineptr += consumed; - } - } - fclose(f); - return NULL; -} - -char *dump_stdin_into_a_temp_file() { - char dirpath[] = "/tmp/wl-copy-buffer-XXXXXX"; - if (mkdtemp(dirpath) != dirpath) { - perror("mkdtemp"); - exit(1); - } - char *original_path = path_for_fd(STDIN_FILENO); - - char *res_path = malloc(PATH_MAX + 1); - memcpy(res_path, dirpath, sizeof(dirpath)); - strcat(res_path, "/"); - - if (original_path != NULL) { - char *name = basename(original_path); - strcat(res_path, name); - } else { - strcat(res_path, "stdin"); - } - - if (fork() == 0) { - FILE *res = fopen(res_path, "w"); - if (res == NULL) { - perror("fopen"); - exit(1); - } - dup2(fileno(res), STDOUT_FILENO); - fclose(res); - execlp("cat", "cat", NULL); - perror("exec cat"); - exit(1); - } - - int wstatus; - wait(&wstatus); - if (original_path != NULL) { - free(original_path); - } - if (WIFEXITED(wstatus) && WEXITSTATUS(wstatus) == 0) { - return res_path; - } - bail("Failed to copy the file"); -} - -void trim_trailing_newline(const char *file_path) { - int fd = open(file_path, O_RDWR); - if (fd < 0) { - perror("open file for trimming"); - return; - } - - int seek_res = lseek(fd, -1, SEEK_END); - if (seek_res < 0 && errno == EINVAL) { - // empty file - goto out; - } else if (seek_res < 0) { - perror("lseek"); - goto out; - } - // otherwise, seek_res is the new file size - - char last_char; - int read_res = read(fd, &last_char, 1); - if (read_res != 1) { - perror("read"); - goto out; - } - if (last_char != '\n') { - goto out; - } - - ftruncate(fd, seek_res); -out: - close(fd); -} diff --git a/src/boilerplate.h b/src/boilerplate.h deleted file mode 100644 index dd2396f..0000000 --- a/src/boilerplate.h +++ /dev/null @@ -1,140 +0,0 @@ -/* wl-clipboard - * - * Copyright © 2019 Sergey Bugaev - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "config.h" - -#include -#include -#include -#include -#include -#include -#include // isupper -#include // open -#include // open -#include // open -#include // exit -#include // basename -#include -#include // PATH_MAX - -#ifdef HAVE_MEMFD -# include // syscall, SYS_memfd_create -#endif -#ifdef HAVE_SHM_ANON -# include // shm_open, SHM_ANON -#endif - - -#ifdef HAVE_XDG_SHELL -# include "xdg-shell.h" -#endif - -#ifdef HAVE_WP_PRIMARY_SELECTION -# include "wp-primary-selection.h" -#endif - -#ifdef HAVE_WLR_LAYER_SHELL -# include "wlr-layer-shell.h" -#endif - -#ifdef HAVE_GTK_PRIMARY_SELECTION -# include "gtk-primary-selection.h" -#endif - -#ifdef HAVE_WLR_DATA_CONTROL -# include "wlr-data-control.h" -#endif - -#define bail(message) do { fprintf(stderr, message "\n"); exit(1); } while (0) - -#define text_plain "text/plain" -#define text_plain_utf8 "text/plain;charset=utf-8" - -struct wl_display *display; -struct wl_data_device_manager *data_device_manager; -struct wl_seat *seat; -struct wl_compositor *compositor; -struct wl_shm *shm; -struct wl_shell *shell; -struct wl_surface *surface; -struct wl_shell_surface *shell_surface; - -#ifdef HAVE_XDG_SHELL -struct xdg_wm_base *xdg_wm_base; -struct xdg_surface *xdg_surface; -struct xdg_toplevel *xdg_toplevel; -#endif - -#ifdef HAVE_WLR_LAYER_SHELL -struct zwlr_layer_shell_v1 *layer_shell; -struct zwlr_layer_surface_v1 *layer_surface; -#endif - -struct wl_data_device *data_device; - -#ifdef HAVE_GTK_PRIMARY_SELECTION -struct gtk_primary_selection_device_manager *gtk_primary_selection_device_manager; -struct gtk_primary_selection_device *gtk_primary_selection_device; -#endif - -#ifdef HAVE_WP_PRIMARY_SELECTION -struct zwp_primary_selection_device_manager_v1 *primary_selection_device_manager; -struct zwp_primary_selection_device_v1 *primary_selection_device; -#endif - -#ifdef HAVE_WLR_DATA_CONTROL -struct zwlr_data_control_manager_v1 *data_control_manager; -struct zwlr_data_control_device_v1 *data_control_device; - -int data_control_supports_primary_selection; -#endif - -void process_new_seat(struct wl_seat *new_seat); -const char *requested_seat_name; - -void init_wayland_globals(void); -int data_control_version; - -void popup_tiny_invisible_surface(void); -void destroy_popup_surface(void); - -void (*action_on_popup_surface_getting_focus)(uint32_t serial); -void (*action_on_no_keyboard)(void); - -void ensure_has_primary_selection(void); - -uint32_t get_serial(void); - -int mime_type_is_text(const char *mime_type); -int str_has_prefix(const char *string, const char *prefix); -int str_has_suffix(const char *string, const char *suffix); - -void print_version_info(void); - -void trim_trailing_newline(const char *file_path); - -// functions below this line return owned strings, -// free() their return values when done with them - -char *path_for_fd(int fd); -char *infer_mime_type_from_contents(const char *file_path); -char *infer_mime_type_from_name(const char *file_path); - -// returns the name of a new file -char *dump_stdin_into_a_temp_file(void); diff --git a/src/includes/selection-protocols.h b/src/includes/selection-protocols.h new file mode 100644 index 0000000..f01070b --- /dev/null +++ b/src/includes/selection-protocols.h @@ -0,0 +1,42 @@ +/* wl-clipboard + * + * 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef INCLUDES_SELECTION_PROTOCOLS_H +#define INCLUDES_SELECTION_PROTOCOLS_H + +#include "config.h" + +#include + +#ifdef HAVE_WP_PRIMARY_SELECTION +# include "wp-primary-selection.h" +#endif + +#ifdef HAVE_GTK_PRIMARY_SELECTION +# include "gtk-primary-selection.h" +#endif + +#ifdef HAVE_WLR_DATA_CONTROL +# include "wlr-data-control.h" +#endif + +#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 new file mode 100644 index 0000000..5d700d1 --- /dev/null +++ b/src/includes/shell-protocols.h @@ -0,0 +1,39 @@ +/* wl-clipboard + * + * 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef INCLUDES_SHELL_PROTOCOLS_H +#define INCLUDES_SHELL_PROTOCOLS_H + +#include "config.h" + +#include + +#ifdef HAVE_XDG_SHELL +# include "xdg-shell.h" +#endif + +#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 fd1d3e0..e3b9bcf 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,27 +1,7 @@ -wayland = dependency('wayland-client') - -wayland_scanner = find_program('wayland-scanner', required: false) -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_wlr_layer_shell = have_xdg_shell # depends on xdg_popup - 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_wlr_layer_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') @@ -29,76 +9,66 @@ 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_WLR_LAYER_SHELL', have_wlr_layer_shell) -conf_data.set('HAVE_GTK_PRIMARY_SELECTION', have_gtk_primary_selection) -conf_data.set('HAVE_WLR_DATA_CONTROL', have_wlr_data_control) - 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 +lib = static_library( + 'wl-clipboard', + protocol_headers, -protocols = [] + 'includes/shell-protocols.h', + 'includes/selection-protocols.h', -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 + 'util/string.h', + 'util/string.c', + 'util/files.h', + 'util/files.c', + 'util/misc.h', + 'util/misc.c', -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 have_wlr_layer_shell - wlr_layer_shell_xml = join_paths('protocol', 'wlr-layer-shell-unstable-v1.xml') - protocols += [['wlr-layer-shell', wlr_layer_shell_xml]] -endif - -if not have_gtk_primary_selection - warning('Building without primary selection support') -else - 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 = [] - -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@'] - ) - lib = static_library(name, impl, header, dependencies: wayland) - protocol_deps += lib -endforeach - -boilerplate = static_library( - 'wl-clipboard-boilerplate', - 'boilerplate.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', dependencies: wayland, link_with: boilerplate, install: true) -executable('wl-paste', 'wl-paste.c', dependencies: wayland, link_with: boilerplate, install: true) +executable( + 'wl-copy', + 'wl-copy.c', + protocol_headers, + dependencies: wayland, + link_with: lib, + install: true +) +executable( + 'wl-paste', + '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/protocol/wlr-data-control-unstable-v1.xml b/src/protocol/wlr-data-control-unstable-v1.xml index c806db2..75e8671 100644 --- a/src/protocol/wlr-data-control-unstable-v1.xml +++ b/src/protocol/wlr-data-control-unstable-v1.xml @@ -69,15 +69,6 @@ appropriate destroy request has been called. - - - - - - This event can be sent when binding to the wlr_data_control_manager - global to advertise that it supports the primary selection. - - @@ -92,6 +83,10 @@ This request asks the compositor to set the selection to the data from the source on behalf of the client. + The given source may not be used in any further set_selection or + set_primary_selection requests. Attempting to use a previously used + source is a protocol error. + To unset the selection, set the source to NULL. + + + + diff --git a/src/protocol/wlr-layer-shell-unstable-v1.xml b/src/protocol/wlr-layer-shell-unstable-v1.xml deleted file mode 100644 index 216e0d9..0000000 --- a/src/protocol/wlr-layer-shell-unstable-v1.xml +++ /dev/null @@ -1,285 +0,0 @@ - - - - Copyright © 2017 Drew DeVault - - Permission to use, copy, modify, distribute, and sell this - software and its documentation for any purpose is hereby granted - without fee, provided that the above copyright notice appear in - all copies and that both that copyright notice and this permission - notice appear in supporting documentation, and that the name of - the copyright holders not be used in advertising or publicity - pertaining to distribution of the software without specific, - written prior permission. The copyright holders make no - representations about the suitability of this software for any - purpose. It is provided "as is" without express or implied - warranty. - - THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS - SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND - FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY - SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN - AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, - ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF - THIS SOFTWARE. - - - - - Clients can use this interface to assign the surface_layer role to - wl_surfaces. Such surfaces are assigned to a "layer" of the output and - rendered with a defined z-depth respective to each other. They may also be - anchored to the edges and corners of a screen and specify input handling - semantics. This interface should be suitable for the implementation of - many desktop shell components, and a broad number of other applications - that interact with the desktop. - - - - - Create a layer surface for an existing surface. This assigns the role of - layer_surface, or raises a protocol error if another role is already - assigned. - - Creating a layer surface from a wl_surface which has a buffer attached - or committed is a client error, and any attempts by a client to attach - or manipulate a buffer prior to the first layer_surface.configure call - must also be treated as errors. - - You may pass NULL for output to allow the compositor to decide which - output to use. Generally this will be the one that the user most - recently interacted with. - - Clients can specify a namespace that defines the purpose of the layer - surface. - - - - - - - - - - - - - - - - - These values indicate which layers a surface can be rendered in. They - are ordered by z depth, bottom-most first. Traditional shell surfaces - will typically be rendered between the bottom and top layers. - Fullscreen shell surfaces are typically rendered at the top layer. - Multiple surfaces can share a single layer, and ordering within a - single layer is undefined. - - - - - - - - - - - - An interface that may be implemented by a wl_surface, for surfaces that - are designed to be rendered as a layer of a stacked desktop-like - environment. - - Layer surface state (size, anchor, exclusive zone, margin, interactivity) - is double-buffered, and will be applied at the time wl_surface.commit of - the corresponding wl_surface is called. - - - - - Sets the size of the surface in surface-local coordinates. The - compositor will display the surface centered with respect to its - anchors. - - If you pass 0 for either value, the compositor will assign it and - inform you of the assignment in the configure event. You must set your - anchor to opposite edges in the dimensions you omit; not doing so is a - protocol error. Both values are 0 by default. - - Size is double-buffered, see wl_surface.commit. - - - - - - - - Requests that the compositor anchor the surface to the specified edges - and corners. If two orthoginal edges are specified (e.g. 'top' and - 'left'), then the anchor point will be the intersection of the edges - (e.g. the top left corner of the output); otherwise the anchor point - will be centered on that edge, or in the center if none is specified. - - Anchor is double-buffered, see wl_surface.commit. - - - - - - - Requests that the compositor avoids occluding an area of the surface - with other surfaces. The compositor's use of this information is - implementation-dependent - do not assume that this region will not - actually be occluded. - - A positive value is only meaningful if the surface is anchored to an - edge, rather than a corner. The zone is the number of surface-local - coordinates from the edge that are considered exclusive. - - Surfaces that do not wish to have an exclusive zone may instead specify - how they should interact with surfaces that do. If set to zero, the - surface indicates that it would like to be moved to avoid occluding - surfaces with a positive excluzive zone. If set to -1, the surface - indicates that it would not like to be moved to accommodate for other - surfaces, and the compositor should extend it all the way to the edges - it is anchored to. - - For example, a panel might set its exclusive zone to 10, so that - maximized shell surfaces are not shown on top of it. A notification - might set its exclusive zone to 0, so that it is moved to avoid - occluding the panel, but shell surfaces are shown underneath it. A - wallpaper or lock screen might set their exclusive zone to -1, so that - they stretch below or over the panel. - - The default value is 0. - - Exclusive zone is double-buffered, see wl_surface.commit. - - - - - - - Requests that the surface be placed some distance away from the anchor - point on the output, in surface-local coordinates. Setting this value - for edges you are not anchored to has no effect. - - The exclusive zone includes the margin. - - Margin is double-buffered, see wl_surface.commit. - - - - - - - - - - Set to 1 to request that the seat send keyboard events to this layer - surface. For layers below the shell surface layer, the seat will use - normal focus semantics. For layers above the shell surface layers, the - seat will always give exclusive keyboard focus to the top-most layer - which has keyboard interactivity set to true. - - Layer surfaces receive pointer, touch, and tablet events normally. If - you do not want to receive them, set the input region on your surface - to an empty region. - - Events is double-buffered, see wl_surface.commit. - - - - - - - This assigns an xdg_popup's parent to this layer_surface. This popup - should have been created via xdg_surface::get_popup with the parent set - to NULL, and this request must be invoked before committing the popup's - initial state. - - See the documentation of xdg_popup for more details about what an - xdg_popup is and how it is used. - - - - - - - When a configure event is received, if a client commits the - surface in response to the configure event, then the client - must make an ack_configure request sometime before the commit - request, passing along the serial of the configure event. - - If the client receives multiple configure events before it - can respond to one, it only has to ack the last configure event. - - A client is not required to commit immediately after sending - an ack_configure request - it may even ack_configure several times - before its next surface commit. - - A client may send multiple ack_configure requests before committing, but - only the last request sent before a commit indicates which configure - event the client really is responding to. - - - - - - - This request destroys the layer surface. - - - - - - The configure event asks the client to resize its surface. - - Clients should arrange their surface for the new states, and then send - an ack_configure request with the serial sent in this configure event at - some point before committing the new surface. - - The client is free to dismiss all but the last configure event it - received. - - The width and height arguments specify the size of the window in - surface-local coordinates. - - The size is a hint, in the sense that the client is free to ignore it if - it doesn't resize, pick a smaller size (to satisfy aspect ratio or - resize in steps of NxM pixels). If the client picks a smaller size and - is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the - surface will be centered on this axis. - - If the width or height arguments are zero, it means the client should - decide its own window dimension. - - - - - - - - - The closed event is sent by the compositor when the surface will no - longer be shown. The output may have been destroyed or the user may - have asked for it to be removed. Further changes to the surface will be - ignored. The client should destroy the resource after receiving this - event, and create a new surface if they so choose. - - - - - - - - - - - - - - - - - diff --git a/src/types/copy-action.c b/src/types/copy-action.c new file mode 100644 index 0000000..79b0b0a --- /dev/null +++ b/src/types/copy-action.c @@ -0,0 +1,188 @@ +/* wl-clipboard + * + * 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "types/copy-action.h" +#include "types/device.h" +#include "types/source.h" +#include "types/popup-surface.h" + +#include "util/misc.h" + +#include +#include +#include +#include +#include +#include +#include +#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->popup_surface != NULL) { + popup_surface_destroy(self->popup_surface); + self->popup_surface = NULL; + } + + /* And invoke the callback */ + if (self->did_set_selection_callback != NULL) { + self->did_set_selection_callback(self); + } +} + +static void on_focus( + struct popup_surface *popup_surface, + uint32_t serial +) { + struct copy_action *self = (struct copy_action *) popup_surface->data; + do_set_selection(self, serial); +} + +static void do_send(struct source *source, const char *mime_type, int fd) { + struct copy_action *self = source->data; + + /* Unset O_NONBLOCK */ + fcntl(fd, F_SETFL, 0); + + if (!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. + */ + pid_t pid = fork(); + if (pid < 0) { + perror("fork"); + close(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); + signal(SIGHUP, SIG_DFL); + signal(SIGPIPE, SIG_DFL); + execlp("cat", "cat", NULL); + perror("exec cat"); + exit(1); + } + close(fd); + /* Wait for the cat process to exit. This effectively + * means waiting for the other side to read the whole + * file. In theory, a malicious client could perform a + * denial-of-serivice attack against us. Perhaps we + * should switch to an asynchronous child waiting scheme + * instead. + */ + 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"); + if (f == NULL) { + perror("fdopen"); + close(fd); + return; + } + + if (self->data_to_copy.ptr != NULL) { + /* Just copy the given chunk of data */ + fwrite(self->data_to_copy.ptr, 1, self->data_to_copy.len, f); + } else if (self->argv_to_copy != NULL) { + /* Copy an argv-style string array, + * inserting spaces between items. + */ + int is_first = 1; + for (argv_t word = self->argv_to_copy; *word != NULL; word++) { + if (!is_first) { + fwrite(" ", 1, 1, f); + } + is_first = 0; + fwrite(*word, 1, strlen(*word), f); + } + } else { + bail("Unreachable: nothing to copy"); + } + + fclose(f); + } + + + if (self->pasted_callback != NULL) { + self->pasted_callback(self); + } +} + +static void forward_cancel(struct source *source) { + struct copy_action *self = source->data; + if (self->cancelled_callback != NULL) { + self->cancelled_callback(self); + } +} + +void copy_action_init(struct copy_action *self) { + if (self->source != NULL) { + self->source->send_callback = do_send; + self->source->cancelled_callback = forward_cancel; + self->source->data = self; + } + /* See if we can just set the selection directly */ + if (!self->device->needs_popup_surface) { + /* If we can, it doesn't actually require + * a serial, so passing zero will do. + */ + do_set_selection(self, 0); + } else { + /* If we cannot, schedule to do it later, + * when our popup surface gains keyboard focus. + */ + self->popup_surface->on_focus = on_focus; + self->popup_surface->data = self; + popup_surface_init(self->popup_surface); + } +} diff --git a/src/types/copy-action.h b/src/types/copy-action.h new file mode 100644 index 0000000..6c92e9a --- /dev/null +++ b/src/types/copy-action.h @@ -0,0 +1,56 @@ +/* wl-clipboard + * + * 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TYPES_COPY_ACTION_H +#define TYPES_COPY_ACTION_H + +#include "util/string.h" + +#include + +struct device; +struct source; +struct popup_surface; + +struct copy_action { + /* These fields are initialized by the creator */ + struct device *device; + 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); + void (*cancelled_callback)(struct copy_action *self); + + /* 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. + */ + int fd_to_copy_from; + argv_t argv_to_copy; + struct { + const char *ptr; + size_t len; + } data_to_copy; +}; + +void copy_action_init(struct copy_action *self); + +#endif /* TYPES_COPY_ACTION_H */ diff --git a/src/types/device-manager.c b/src/types/device-manager.c new file mode 100644 index 0000000..55e7aa7 --- /dev/null +++ b/src/types/device-manager.c @@ -0,0 +1,160 @@ +/* wl-clipboard + * + * 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "types/device-manager.h" +#include "types/device.h" +#include "types/source.h" +#include "types/seat.h" +#include "includes/selection-protocols.h" + +#include + + +struct source *device_manager_create_source(struct device_manager *self) { + return self->do_create_source(self); +} + +struct device *device_manager_get_device( + struct device_manager *self, + struct seat *seat +) { + return self->do_get_device(self, seat); +} + +/* Macros to reduce implementation boilerplate */ + +#define CREATE_SOURCE(type, source_type, method_name) \ +static struct source *device_manager_ ## type ## _do_create_source( \ + struct device_manager *self \ +) { \ + struct type *proxy = (struct type *) self->proxy; \ + struct source *source = calloc(1, sizeof(struct source)); \ + source->proxy = (struct wl_proxy *) type ## _ ## method_name(proxy); \ + source_init_ ## source_type(source); \ + return source; \ +} + +#define GET_DEVICE(type, device_type, method_name) \ +static struct device *device_manager_ ## type ## _do_get_device( \ + struct device_manager *self, \ + struct seat *seat_wrapper \ +) { \ + struct type *proxy = (struct type *) self->proxy; \ + struct wl_seat *seat = (struct wl_seat *) seat_wrapper->proxy; \ + struct device *device = calloc(1, sizeof(struct device)); \ + device->proxy = (struct wl_proxy *) type ## _ ## method_name(proxy, seat); \ + device->wl_display = self->wl_display; \ + device_init_ ## device_type(device); \ + return device; \ +} + +#define INIT(type) \ +void device_manager_init_ ## type(struct device_manager *self) { \ + self->do_create_source = device_manager_ ## type ## _do_create_source; \ + self->do_get_device = device_manager_ ## type ## _do_get_device; \ +} + + +/* Core Wayland implementation */ + +CREATE_SOURCE(wl_data_device_manager, wl_data_source, create_data_source) +GET_DEVICE(wl_data_device_manager, wl_data_device, get_data_device) +INIT(wl_data_device_manager) + + +/* gtk-primary-selection implementation */ + +#ifdef HAVE_GTK_PRIMARY_SELECTION + +CREATE_SOURCE( + gtk_primary_selection_device_manager, + gtk_primary_selection_source, + create_source +) + +GET_DEVICE( + gtk_primary_selection_device_manager, + gtk_primary_selection_device, + get_device +) + +INIT(gtk_primary_selection_device_manager) + +#endif /* HAVE_GTK_PRIMARY_SELECTION */ + + +/* wp-primary-selection implementation */ + +#ifdef HAVE_WP_PRIMARY_SELECTION + +CREATE_SOURCE( + zwp_primary_selection_device_manager_v1, + zwp_primary_selection_source_v1, + create_source +) + +GET_DEVICE( + zwp_primary_selection_device_manager_v1, + zwp_primary_selection_device_v1, + get_device +) + +INIT(zwp_primary_selection_device_manager_v1) + +#endif /* HAVE_WP_PRIMARY_SELECTION */ + + +/* wlr-data-control implementation */ + +#ifdef HAVE_WLR_DATA_CONTROL + +CREATE_SOURCE( + zwlr_data_control_manager_v1, + zwlr_data_control_source_v1, + create_data_source +) + +GET_DEVICE( + zwlr_data_control_manager_v1, + zwlr_data_control_device_v1, + get_data_device +) + +INIT(zwlr_data_control_manager_v1) + +#endif /* HAVE_WLR_DATA_CONTROL */ + +/* 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 new file mode 100644 index 0000000..8629a57 --- /dev/null +++ b/src/types/device-manager.h @@ -0,0 +1,78 @@ +/* wl-clipboard + * + * 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TYPES_DEVICE_MANAGER_H +#define TYPES_DEVICE_MANAGER_H + +#include "includes/selection-protocols.h" + +#include + +struct seat; +struct device; +struct source; + +struct device_manager { + /* These fields are initialized by the creator */ + struct wl_proxy *proxy; + struct wl_display *wl_display; + + /* These fields are initialized by the implementation */ + struct source *(*do_create_source)(struct device_manager *self); + struct device *(*do_get_device)( + struct device_manager *self, + struct seat *seat + ); +}; + +struct source *device_manager_create_source(struct device_manager *self); + +struct device *device_manager_get_device( + struct device_manager *self, + struct seat *seat +); + +/* Initializers */ + +void device_manager_init_wl_data_device_manager(struct device_manager *self); + +#ifdef HAVE_GTK_PRIMARY_SELECTION +void device_manager_init_gtk_primary_selection_device_manager( + struct device_manager *self +); +#endif + +#ifdef HAVE_WP_PRIMARY_SELECTION +void device_manager_init_zwp_primary_selection_device_manager_v1( + struct device_manager *self +); +#endif + +#ifdef HAVE_WLR_DATA_CONTROL +void device_manager_init_zwlr_data_control_manager_v1( + struct device_manager *self +); +#endif + +#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 new file mode 100644 index 0000000..6282c1f --- /dev/null +++ b/src/types/device.c @@ -0,0 +1,351 @@ +/* wl-clipboard + * + * 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "config.h" +#include "types/device.h" +#include "types/offer.h" +#include "types/source.h" +#include "includes/selection-protocols.h" + +#include +#include + +enum TriState { + Unknown, + Yes, + No +}; + +int device_supports_selection(struct device *self, int primary) { + return self->supports_selection(self, primary); +} + +void device_set_selection( + struct device *self, + struct source *source, + uint32_t serial, + int primary +) { + self->do_set_selection(self, source, serial, primary); +} + +/* Macros to reduce implementation boilerplate */ + +#define SUPPORTS_SELECTION(type, expr) \ +static int device_supports_selection_on_ ## type( \ + struct device *self, \ + int primary \ +) { \ + return expr; \ +} + +#define SET_SELECTION_IMPL(type, source_type, impl) \ +static void device_set_selection_on_ ## type( \ + struct device *self, \ + struct source *source_wrapper, \ + uint32_t serial, \ + int primary \ +) { \ + struct type *device = (struct type *) self->proxy; \ + struct source_type *source = NULL; \ + if (source_wrapper != NULL) { \ + source = (struct source_type *) source_wrapper->proxy; \ + } \ + impl \ +} + +#define DATA_OFFER_HANDLER(type, offer_type) \ +static void type ## _data_offer_handler( \ + void *data, \ + struct type *device, \ + struct offer_type *offer_proxy \ +) { \ + struct device *self = data; \ + struct offer *offer = calloc(1, sizeof(struct offer)); \ + offer->proxy = (struct wl_proxy *) offer_proxy; \ + offer_init_ ## offer_type(offer); \ + if (self->new_offer_callback) { \ + self->new_offer_callback(offer); \ + } \ +} + +#define SELECTION_HANDLER(type, offer_type, selection_event, primary) \ +static void type ## _ ## selection_event ## _handler( \ + void *data, \ + struct type *device, \ + struct offer_type *offer_proxy \ +) { \ + struct device *self = data; \ + struct offer *offer = NULL; \ + if (offer_proxy != NULL) { \ + offer = wl_proxy_get_user_data((struct wl_proxy *) offer_proxy); \ + } \ + if (self->selection_callback) { \ + self->selection_callback(offer, primary); \ + } \ +} + +#define INIT(type, needs_surface) \ +void device_init_ ## type(struct device *self) { \ + struct type *device = (struct type *) self->proxy; \ + type ## _add_listener(device, &type ## _listener, self); \ + self->supports_selection = device_supports_selection_on_ ## type; \ + self->needs_popup_surface = needs_surface; \ + self->do_set_selection = device_set_selection_on_ ## type; \ +} + + +/* Core Wayland implementation */ + +SUPPORTS_SELECTION(wl_data_device, !primary) + +SET_SELECTION_IMPL(wl_data_device, wl_data_source, { + wl_data_device_set_selection(device, source, serial); +}) + +DATA_OFFER_HANDLER(wl_data_device, wl_data_offer) + +SELECTION_HANDLER(wl_data_device, wl_data_offer, selection, 0) + +static const struct wl_data_device_listener wl_data_device_listener = { + .data_offer = wl_data_device_data_offer_handler, + .selection = wl_data_device_selection_handler +}; + +INIT(wl_data_device, 1) + + +/* gtk-primary-selection implementation */ + +#ifdef HAVE_GTK_PRIMARY_SELECTION + +SUPPORTS_SELECTION(gtk_primary_selection_device, primary) + +SET_SELECTION_IMPL(gtk_primary_selection_device, gtk_primary_selection_source, { + gtk_primary_selection_device_set_selection(device, source, serial); +}) + +DATA_OFFER_HANDLER(gtk_primary_selection_device, gtk_primary_selection_offer) + +SELECTION_HANDLER( + gtk_primary_selection_device, + gtk_primary_selection_offer, + selection, + 1 +) + +static const struct gtk_primary_selection_device_listener +gtk_primary_selection_device_listener = { + .data_offer = gtk_primary_selection_device_data_offer_handler, + .selection = gtk_primary_selection_device_selection_handler +}; + +INIT(gtk_primary_selection_device, 1) + +#endif /* HAVE_GTK_PRIMARY_SELECTION */ + + +/* wp-primary-selection implementation */ + +#ifdef HAVE_WP_PRIMARY_SELECTION + +SUPPORTS_SELECTION(zwp_primary_selection_device_v1, primary) + +SET_SELECTION_IMPL( + zwp_primary_selection_device_v1, + zwp_primary_selection_source_v1, + { + zwp_primary_selection_device_v1_set_selection(device, source, serial); + } +) + +DATA_OFFER_HANDLER( + zwp_primary_selection_device_v1, + zwp_primary_selection_offer_v1 +) + +SELECTION_HANDLER( + zwp_primary_selection_device_v1, + zwp_primary_selection_offer_v1, + selection, + 1 +) + +static const struct zwp_primary_selection_device_v1_listener +zwp_primary_selection_device_v1_listener = { + .data_offer = zwp_primary_selection_device_v1_data_offer_handler, + .selection = zwp_primary_selection_device_v1_selection_handler +}; + +INIT(zwp_primary_selection_device_v1, 1) + +#endif /* HAVE_WP_PRIMARY_SELECTION */ + + +/* wlr-data-control implementation */ + +#ifdef HAVE_WLR_DATA_CONTROL + +/* Whether wlr-data-control supports primary selection */ +static enum TriState device_wlr_supports_primary_selection = Unknown; +static int device_get_wlr_supports_selection(struct device *self, int primary) { + if (!primary) { + return 1; + } + + if (device_wlr_supports_primary_selection == Yes) { + return 1; + } else if (device_wlr_supports_primary_selection == No) { + return 0; + } + + wl_display_roundtrip(self->wl_display); + + if (device_wlr_supports_primary_selection == Yes) { + return 1; + } else { + device_wlr_supports_primary_selection = No; + return 0; + } +} + +SUPPORTS_SELECTION( + zwlr_data_control_device_v1, + device_get_wlr_supports_selection(self, primary) +) + +SET_SELECTION_IMPL(zwlr_data_control_device_v1, zwlr_data_control_source_v1, { + if (!primary) { + zwlr_data_control_device_v1_set_selection(device, source); + } else { + zwlr_data_control_device_v1_set_primary_selection(device, source); + } +}) + +DATA_OFFER_HANDLER(zwlr_data_control_device_v1, zwlr_data_control_offer_v1) + +SELECTION_HANDLER( + zwlr_data_control_device_v1, + zwlr_data_control_offer_v1, + selection, + 0 +) + +static void zwlr_data_control_device_v1_primary_selection_handler( + void *data, + struct zwlr_data_control_device_v1 *device, + struct zwlr_data_control_offer_v1 *offer_proxy +) { + device_wlr_supports_primary_selection = Yes; + struct device *self = data; + struct offer *offer = NULL; + if (offer_proxy != NULL) { + offer = wl_proxy_get_user_data((struct wl_proxy *) offer_proxy); + } + if (self->selection_callback != NULL) { + self->selection_callback(offer, 1); + } +} + +static const struct zwlr_data_control_device_v1_listener +zwlr_data_control_device_v1_listener = { + .data_offer = zwlr_data_control_device_v1_data_offer_handler, + .selection = zwlr_data_control_device_v1_selection_handler, + .primary_selection = zwlr_data_control_device_v1_primary_selection_handler +}; + +INIT(zwlr_data_control_device_v1, 0) + +#endif /* HAVE_WLR_DATA_CONTROL */ + + +/* 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 new file mode 100644 index 0000000..ad780e0 --- /dev/null +++ b/src/types/device.h @@ -0,0 +1,78 @@ +/* wl-clipboard + * + * 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TYPES_DEVICE_H +#define TYPES_DEVICE_H + +#include "includes/selection-protocols.h" + +#include + +struct offer; +struct source; + +struct device { + /* These fields are initialized by the creator */ + void (*new_offer_callback)(struct offer *offer); + void (*selection_callback)(struct offer *offer, int primary); + void *data; + + struct wl_proxy *proxy; + struct wl_display *wl_display; + + /* These fields are initialized by the implementation */ + int (*supports_selection)(struct device *self, int primary); + int needs_popup_surface; + void (*do_set_selection)( + struct device *self, + struct source *source, + uint32_t serial, + int primary + ); +}; + +int device_supports_selection(struct device *self, int primary); + +void device_set_selection( + struct device *self, + struct source *source, + uint32_t serial, + int primary +); + +/* Initializers */ + +void device_init_wl_data_device(struct device *self); + +#ifdef HAVE_GTK_PRIMARY_SELECTION +void device_init_gtk_primary_selection_device(struct device *self); +#endif + +#ifdef HAVE_WP_PRIMARY_SELECTION +void device_init_zwp_primary_selection_device_v1(struct device *self); +#endif + +#ifdef HAVE_WLR_DATA_CONTROL +void device_init_zwlr_data_control_device_v1(struct device *self); +#endif + +#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 new file mode 100644 index 0000000..12eb82f --- /dev/null +++ b/src/types/keyboard.c @@ -0,0 +1,82 @@ +/* wl-clipboard + * + * 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "types/keyboard.h" + +#include + +static void wl_keyboard_keymap_handler( + void *data, + struct wl_keyboard *keyboard, + uint32_t format, + int fd, + uint32_t size +) { + close(fd); +} + +static void wl_keyboard_enter_handler( + void *data, + struct wl_keyboard *keyboard, + uint32_t serial, + struct wl_surface *surface, + struct wl_array *keys +) { + struct keyboard *self = (struct keyboard *) data; + if (self->on_focus != NULL) { + self->on_focus(self, serial); + } +} + +static void wl_keyboard_leave_handler( + void *data, + struct wl_keyboard *keyboard, + uint32_t serial, + struct wl_surface *surface +) {} + +static void wl_keyboard_key_handler( + void *data, + struct wl_keyboard *keyboard, + uint32_t serial, + uint32_t time, + uint32_t key, + uint32_t state +) {} + +static void wl_keyboard_modifiers_handler( + void *data, + struct wl_keyboard *keyboard, + uint32_t serial, + uint32_t mods_depressed, + uint32_t mods_latched, + uint32_t mods_locked, + uint32_t group +) {} + +static const struct wl_keyboard_listener wl_keyboard_listener = { + .keymap = wl_keyboard_keymap_handler, + .enter = wl_keyboard_enter_handler, + .leave = wl_keyboard_leave_handler, + .key = wl_keyboard_key_handler, + .modifiers = wl_keyboard_modifiers_handler, +}; + +void keyboard_init(struct keyboard *self) { + wl_keyboard_add_listener(self->proxy, &wl_keyboard_listener, self); +} diff --git a/src/types/keyboard.h b/src/types/keyboard.h new file mode 100644 index 0000000..4e95d85 --- /dev/null +++ b/src/types/keyboard.h @@ -0,0 +1,34 @@ +/* wl-clipboard + * + * 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TYPES_KEYBOARD_H +#define TYPES_KEYBOARD_H + +#include +#include + +struct keyboard { + /* These fields are initialized by the creator */ + struct wl_keyboard *proxy; + void (*on_focus)(struct keyboard *self, uint32_t serial); + void *data; +}; + +void keyboard_init(struct keyboard *self); + +#endif /* TYPES_KEYBOARD_H */ diff --git a/src/types/offer.c b/src/types/offer.c new file mode 100644 index 0000000..cf3c7ff --- /dev/null +++ b/src/types/offer.c @@ -0,0 +1,117 @@ +/* wl-clipboard + * + * 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "types/offer.h" +#include "includes/selection-protocols.h" + +#include +#include +#include + +void offer_receive(struct offer *self, const char *mime_type, int fd) { + self->do_receive(self->proxy, mime_type, fd); +} + +void offer_destroy(struct offer *self) { + self->do_destroy(self->proxy); + wl_array_release(&self->offered_mime_types); + free(self); +} + + +/* Macros to reduce implementation boilerplate */ + +#define OFFER_HANDLER(type) \ +static void type ## _offer_handler( \ + void *data, \ + struct type *proxy, \ + const char *mime_type \ +) { \ + struct offer *self = data; \ + char *ptr = wl_array_add( \ + &self->offered_mime_types, \ + strlen(mime_type) + 1 \ + ); \ + strcpy(ptr, mime_type); \ +} + +#define LISTENER(type) \ +static const struct type ## _listener type ## _listener = { \ + .offer = type ## _offer_handler \ +}; + +#define INIT(type) \ +void offer_init_ ## type(struct offer *self) { \ + self->do_receive = \ + (void (*)(struct wl_proxy *, const char *, int)) type ## _receive; \ + self->do_destroy = (void (*)(struct wl_proxy *)) type ## _destroy; \ + wl_array_init(&self->offered_mime_types); \ + struct type *proxy = (struct type *) self->proxy; \ + type ## _add_listener(proxy, &type ## _listener, self); \ +} + + +/* Core Wayland implementation */ + +OFFER_HANDLER(wl_data_offer) +LISTENER(wl_data_offer) +INIT(wl_data_offer) + + +/* gtk-primary-selection implementation */ + +#ifdef HAVE_GTK_PRIMARY_SELECTION + +OFFER_HANDLER(gtk_primary_selection_offer) +LISTENER(gtk_primary_selection_offer) +INIT(gtk_primary_selection_offer) + +#endif /* HAVE_GTK_PRIMARY_SELECTION */ + + +/* wp-primary-selection implementation */ + +#ifdef HAVE_WP_PRIMARY_SELECTION + +OFFER_HANDLER(zwp_primary_selection_offer_v1) +LISTENER(zwp_primary_selection_offer_v1) +INIT(zwp_primary_selection_offer_v1) + +#endif /* HAVE_WP_PRIMARY_SELECTION */ + + +/* wlr-data-control implementation */ + +#ifdef HAVE_WLR_DATA_CONTROL + +OFFER_HANDLER(zwlr_data_control_offer_v1) +LISTENER(zwlr_data_control_offer_v1) +INIT(zwlr_data_control_offer_v1) + +#endif /* HAVE_WLR_DATA_CONTROL */ + + +/* 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 new file mode 100644 index 0000000..c8eaed1 --- /dev/null +++ b/src/types/offer.h @@ -0,0 +1,68 @@ +/* wl-clipboard + * + * 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TYPES_OFFER_H +#define TYPES_OFFER_H + +#include "includes/selection-protocols.h" + +#include +#include + +struct offer { + /* This field is initialized by the creator */ + struct wl_proxy *proxy; + + /* These fields are initialized by the implementation */ + void (*do_receive)(struct wl_proxy *proxy, const char *mime_type, int fd); + void (*do_destroy)(struct wl_proxy *proxy); + struct wl_array offered_mime_types; +}; + +#define offer_for_each_mime_type(offer, mime_type) \ +for ( \ + const char *mime_type = offer->offered_mime_types.data; \ + mime_type != (const char *) offer->offered_mime_types.data + \ + offer->offered_mime_types.size; \ + mime_type += strlen(mime_type) + 1 \ +) + +void offer_receive(struct offer *self, const char *mime_type, int fd); +void offer_destroy(struct offer *self); + +/* Initializers */ + +void offer_init_wl_data_offer(struct offer *self); + +#ifdef HAVE_GTK_PRIMARY_SELECTION +void offer_init_gtk_primary_selection_offer(struct offer *self); +#endif + +#ifdef HAVE_WP_PRIMARY_SELECTION +void offer_init_zwp_primary_selection_offer_v1(struct offer *self); +#endif + +#ifdef HAVE_WLR_DATA_CONTROL +void offer_init_zwlr_data_control_offer_v1(struct offer *self); +#endif + +#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 new file mode 100644 index 0000000..b8eee53 --- /dev/null +++ b/src/types/popup-surface.c @@ -0,0 +1,196 @@ +/* wl-clipboard + * + * 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "types/popup-surface.h" +#include "types/registry.h" +#include "types/seat.h" +#include "types/keyboard.h" +#include "types/shell.h" +#include "types/shell-surface.h" +#include "util/files.h" +#include "util/misc.h" + +#include +#include +#include + +static void forward_on_focus( + struct keyboard *keyboard, + uint32_t serial +) { + struct popup_surface *self = (struct popup_surface *) keyboard->data; + if (self->on_focus != NULL) { + self->on_focus(self, serial); + } +} + +void popup_surface_init(struct popup_surface *self) { + self->shell = registry_find_shell(self->registry); + if (self->shell == NULL) { + complain_about_missing_shell(); + } + + self->keyboard = seat_get_keyboard(self->seat); + if (self->keyboard == NULL) { + bail("This seat has no keyboard"); + } + self->keyboard->on_focus = forward_on_focus; + self->keyboard->data = self; + + /* Make sure that we get the keyboard + * object before we create the surface, + * so that we get the enter event. + */ + wl_display_dispatch(self->registry->wl_display); + + + struct wl_compositor *wl_compositor = self->registry->wl_compositor; + if (wl_compositor == NULL) { + complain_about_missing_global("wl_compositor"); + } + self->wl_surface = wl_compositor_create_surface(wl_compositor); + self->shell_surface = shell_create_shell_surface( + self->shell, + 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 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 + * do anything further in that case. + */ + free(self); + return; + } + + /* Remember that after this point, we should + * free() self when it gets destroyed. + */ + self->should_free_self = 1; + + int width = 1; + int height = 1; + int stride = width * 4; + int size = stride * height; + + /* Open an anonymous file and write some zero bytes to it */ + int fd = create_anonymous_file(); + 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) { + complain_about_missing_global("wl_shm"); + } + struct wl_shm_pool *wl_shm_pool = wl_shm_create_pool(wl_shm, fd, size); + + /* Allocate the buffer in that pool */ + struct wl_buffer *wl_buffer = wl_shm_pool_create_buffer( + wl_shm_pool, + 0, + width, + height, + stride, + WL_SHM_FORMAT_ARGB8888 + ); + /* We're using ARGB, so zero bytes mean + * a fully transparent pixel, which happens + * to be exactly what we want. + */ + + wl_surface_attach(self->wl_surface, wl_buffer, 0, 0); + wl_surface_damage(self->wl_surface, 0, 0, width, height); + + /* 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); +} + +void popup_surface_destroy(struct popup_surface *self) { + /* We cannot destroy the keyboard + * because it acts as a listener, + * and there's no way to reset that. + * So just unreference ourselves. + */ + self->keyboard->on_focus = NULL; + 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) { + free(self); + } +} diff --git a/src/types/popup-surface.h b/src/types/popup-surface.h new file mode 100644 index 0000000..e6148a7 --- /dev/null +++ b/src/types/popup-surface.h @@ -0,0 +1,54 @@ +/* wl-clipboard + * + * 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TYPES_POPUP_SURFACE_H +#define TYPES_POPUP_SURFACE_H + +#include "includes/shell-protocols.h" + +#include + +struct registry; +struct shell; +struct shell_surface; +struct seat; +struct keyboard; + +struct popup_surface { + /* These fields are initialized by the creator */ + struct registry *registry; + struct seat *seat; + void (*on_focus)(struct popup_surface *self, uint32_t serial); + void *data; + + /* These fields are initialized by the implementation */ + struct shell *shell; + struct shell_surface *shell_surface; + struct wl_surface *wl_surface; + struct keyboard *keyboard; + int should_free_self; + +#ifdef HAVE_GTK_SHELL + struct gtk_surface1 *gtk_surface; +#endif +}; + +void popup_surface_init(struct popup_surface *self); +void popup_surface_destroy(struct popup_surface *self); + +#endif /* TYPES_POPUP_SURFACE_H */ diff --git a/src/types/registry.c b/src/types/registry.c new file mode 100644 index 0000000..7eeaec4 --- /dev/null +++ b/src/types/registry.c @@ -0,0 +1,231 @@ +/* wl-clipboard + * + * 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "types/registry.h" +#include "types/seat.h" +#include "types/shell.h" +#include "types/device-manager.h" +#include "includes/shell-protocols.h" +#include "includes/selection-protocols.h" +#include "util/misc.h" + +#include +#include + +#define BIND(interface_name, known_version) \ +if (strcmp(interface, #interface_name) == 0) { \ + if (version >= (known_version)) { \ + self->interface_name = wl_registry_bind( \ + wl_registry, \ + name, \ + &interface_name ## _interface, \ + known_version \ + ); \ + } \ +} + +static void wl_registry_global_handler( + void *data, + struct wl_registry *wl_registry, + uint32_t name, + const char *interface, + uint32_t version +) { + struct registry *self = (struct registry *) data; + + BIND(wl_compositor, 2) + BIND(wl_shm, 1) + +#ifdef HAVE_XDG_ACTIVATION + BIND(xdg_activation_v1, 1) +#endif + + /* Shells */ + + BIND(wl_shell, 1) + +#ifdef HAVE_XDG_SHELL + BIND(xdg_wm_base, 1) +#endif + +#ifdef HAVE_GTK_SHELL + BIND(gtk_shell1, 4) +#endif + + /* Device managers */ + + BIND(wl_data_device_manager, 1) + +#ifdef HAVE_GTK_PRIMARY_SELECTION + BIND(gtk_primary_selection_device_manager, 1) +#endif + +#ifdef HAVE_WP_PRIMARY_SELECTION + BIND(zwp_primary_selection_device_manager_v1, 1) +#endif + +#ifdef HAVE_WLR_DATA_CONTROL + BIND(zwlr_data_control_manager_v1, version > 2 ? 2 : version) +#endif + +#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, + name, + &wl_seat_interface, + 2 + ); + seat_init(seat); + struct seat **ptr = wl_array_add(&self->seats, sizeof(struct seat *)); + *ptr = seat; + } +} + +static void wl_registry_global_remove_handler( + void *data, + struct wl_registry *wl_registry, + uint32_t name +) {} + +static const struct wl_registry_listener wl_registry_listener = { + .global = wl_registry_global_handler, + .global_remove = wl_registry_global_remove_handler +}; + +void registry_init(struct registry *self) { + self->proxy = wl_display_get_registry(self->wl_display); + wl_registry_add_listener(self->proxy, &wl_registry_listener, self); +} + +struct shell *registry_find_shell(struct registry *self) { + struct shell *shell = calloc(1, sizeof(struct shell)); + + if (self->wl_shell != NULL) { + shell->proxy = (struct wl_proxy *) self->wl_shell; + shell_init_wl_shell(shell); + return shell; + } + +#ifdef HAVE_XDG_SHELL + if (self->xdg_wm_base != NULL) { + shell->proxy = (struct wl_proxy *) self->xdg_wm_base; + shell_init_xdg_shell(shell); + return shell; + } +#endif + + free(shell); + return NULL; +} + +#define TRY(type) \ +if (self->type != NULL) { \ + device_manager->proxy = (struct wl_proxy *) self->type; \ + device_manager_init_ ## type(device_manager); \ + return device_manager; \ +} + +struct device_manager *registry_find_device_manager( + struct registry *self, + int primary +) { + struct device_manager *device_manager + = calloc(1, sizeof(struct device_manager)); + device_manager->wl_display = self->wl_display; + + /* For regular selection, we just look at the 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 + TRY(wl_data_device_manager) + + free(device_manager); + return NULL; + } + + /* For primary selection, it's a bit more complicated. We also + * prefer wlr-data-control, but we don't know in advance whether + * the compositor supports primary selection, as unlike with + * other protocols here, the mere presence of wlr-data-control + * does not imply primary selection support. However, we assume + * that if a compositor supports primary selection at all, then + * if it supports wlr-data-control v2 it also supports primary + * selection over wlr-data-control; which is only reasonable. + * + * 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 + = (struct wl_proxy *) self->zwlr_data_control_manager_v1; + if (wl_proxy_get_version(proxy) >= 2) { + device_manager->proxy = proxy; + device_manager_init_zwlr_data_control_manager_v1(device_manager); + return device_manager; + } + } +#endif + +#ifdef HAVE_WP_PRIMARY_SELECTION + TRY(zwp_primary_selection_device_manager_v1) +#endif + +#ifdef HAVE_GTK_PRIMARY_SELECTION + TRY(gtk_primary_selection_device_manager) +#endif + + free(device_manager); + return NULL; +} + +struct seat *registry_find_seat( + struct registry *self, + const char *name +) { + /* Ensure we get all the seat info */ + wl_display_roundtrip(self->wl_display); + + struct seat **ptr; + wl_array_for_each(ptr, &self->seats) { + struct seat *seat = *ptr; + if (name == NULL || strcmp(seat->name, name) == 0) { + return seat; + } + } + return NULL; +} diff --git a/src/types/registry.h b/src/types/registry.h new file mode 100644 index 0000000..30a62b5 --- /dev/null +++ b/src/types/registry.h @@ -0,0 +1,92 @@ +/* wl-clipboard + * + * 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TYPES_REGISTRY_H +#define TYPES_REGISTRY_H + +#include "includes/shell-protocols.h" +#include "includes/selection-protocols.h" + +#include + +struct shell; +struct device_manager; +struct seat; + +struct registry { + /* This field is initialized by the creator */ + struct wl_display *wl_display; + + /* These fields are initialized by the implementation */ + + struct wl_registry *proxy; + struct wl_array seats; + + struct wl_compositor *wl_compositor; + struct wl_shm *wl_shm; + +#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 */ + + struct wl_data_device_manager *wl_data_device_manager; +#ifdef HAVE_GTK_PRIMARY_SELECTION + struct gtk_primary_selection_device_manager + *gtk_primary_selection_device_manager; +#endif +#ifdef HAVE_WP_PRIMARY_SELECTION + struct zwp_primary_selection_device_manager_v1 + *zwp_primary_selection_device_manager_v1; +#endif +#ifdef HAVE_WLR_DATA_CONTROL + struct zwlr_data_control_manager_v1 + *zwlr_data_control_manager_v1; +#endif +#ifdef HAVE_EXT_DATA_CONTROL + struct ext_data_control_manager_v1 + *ext_data_control_manager_v1; +#endif +}; + +void registry_init(struct registry *self); + +struct shell *registry_find_shell(struct registry *self); + +struct device_manager *registry_find_device_manager( + struct registry *self, + int primary +); + +struct seat *registry_find_seat( + struct registry *self, + const char *name +); + +#endif /* TYPES_REGISTRY_H */ diff --git a/src/types/seat.c b/src/types/seat.c new file mode 100644 index 0000000..3d7e604 --- /dev/null +++ b/src/types/seat.c @@ -0,0 +1,61 @@ +/* wl-clipboard + * + * 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "types/seat.h" +#include "types/keyboard.h" + +#include +#include +#include + +static void wl_seat_capabilities_handler( + void *data, + struct wl_seat *seat, + uint32_t capabilities +) { + struct seat *self = (struct seat *) data; + self->capabilities = capabilities; +} + +static void wl_seat_name_handler( + void *data, + struct wl_seat *seat, + const char *name +) { + struct seat *self = (struct seat *) data; + self->name = strdup(name); +} + +static const struct wl_seat_listener wl_seat_listener = { + .capabilities = wl_seat_capabilities_handler, + .name = wl_seat_name_handler +}; + +void seat_init(struct seat *self) { + wl_seat_add_listener(self->proxy, &wl_seat_listener, self); +} + +struct keyboard *seat_get_keyboard(struct seat *self) { + if ((self->capabilities & WL_SEAT_CAPABILITY_KEYBOARD) == 0) { + return NULL; + } + struct keyboard *keyboard = calloc(1, sizeof(struct keyboard)); + keyboard->proxy = wl_seat_get_keyboard(self->proxy); + keyboard_init(keyboard); + return keyboard; +} diff --git a/src/types/seat.h b/src/types/seat.h new file mode 100644 index 0000000..edcbcd6 --- /dev/null +++ b/src/types/seat.h @@ -0,0 +1,39 @@ +/* wl-clipboard + * + * 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TYPES_SEAT_H +#define TYPES_SEAT_H + +#include + +struct keyboard; + +struct seat { + /* This field is initialized by the creator */ + struct wl_seat *proxy; + + /* These fields are initialized by the implementation */ + char *name; + uint32_t capabilities; +}; + +void seat_init(struct seat *self); +struct keyboard *seat_get_keyboard(struct seat *self); + + +#endif /* TYPES_SEAT_H */ diff --git a/src/types/shell-surface.c b/src/types/shell-surface.c new file mode 100644 index 0000000..f2b73ec --- /dev/null +++ b/src/types/shell-surface.c @@ -0,0 +1,127 @@ +/* wl-clipboard + * + * 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "types/shell-surface.h" +#include "includes/shell-protocols.h" + +#include +#include + +void shell_surface_destroy(struct shell_surface *self) { + self->do_destroy(self); + free(self); +} + + +/* Core Wayland implementation */ + +static void wl_shell_surface_ping( + void *data, + struct wl_shell_surface *wl_shell_surface, + uint32_t serial +) { + wl_shell_surface_pong(wl_shell_surface, serial); +} + +static void wl_shell_surface_configure( + void *data, + struct wl_shell_surface *wl_shell_surface, + uint32_t edges, + int32_t width, + int32_t height +) {} + +static void wl_shell_surface_popup_done( + void *data, + struct wl_shell_surface *wl_shell_surface +) {} + +static const struct wl_shell_surface_listener wl_shell_surface_listener = { + .ping = wl_shell_surface_ping, + .configure = wl_shell_surface_configure, + .popup_done = wl_shell_surface_popup_done +}; + +static void destroy_wl_shell_surface(struct shell_surface *self) { + struct wl_shell_surface *proxy = (struct wl_shell_surface *) self->proxy; + wl_shell_surface_destroy(proxy); +} + +void shell_surface_init_wl_shell_surface(struct shell_surface *self) { + struct wl_shell_surface *proxy = (struct wl_shell_surface *) self->proxy; + wl_shell_surface_add_listener(proxy, &wl_shell_surface_listener, self); + wl_shell_surface_set_toplevel(proxy); + wl_shell_surface_set_title(proxy, "wl-clipboard"); + wl_shell_surface_set_class(proxy, "io.github.bugaevc.wl-clipboard"); + self->do_destroy = destroy_wl_shell_surface; +} + + +/* xdg-shell implementation */ + +#ifdef HAVE_XDG_SHELL + +static void xdg_toplevel_configure_handler( + void *data, + struct xdg_toplevel *xdg_toplevel, + int32_t width, + int32_t height, + struct wl_array *states +) {} + +static void xdg_toplevel_close_handler( + void *data, + struct xdg_toplevel *xdg_toplevel +) {} + +static const struct xdg_toplevel_listener xdg_toplevel_listener = { + .configure = xdg_toplevel_configure_handler, + .close = xdg_toplevel_close_handler +}; + +static void xdg_surface_configure_handler( + void *data, + struct xdg_surface *xdg_surface, + uint32_t serial +) { + xdg_surface_ack_configure(xdg_surface, serial); +} + +static const struct xdg_surface_listener xdg_surface_listener = { + .configure = xdg_surface_configure_handler +}; + +static void destroy_xdg_surface(struct shell_surface *self) { + struct xdg_toplevel *toplevel = (struct xdg_toplevel *) self->proxy2; + struct xdg_surface *proxy = (struct xdg_surface *) self->proxy; + xdg_toplevel_destroy(toplevel); + xdg_surface_destroy(proxy); +} + +void shell_surface_init_xdg_surface(struct shell_surface *self) { + struct xdg_surface *proxy = (struct xdg_surface *) self->proxy; + xdg_surface_add_listener(proxy, &xdg_surface_listener, self); + struct xdg_toplevel *toplevel = xdg_surface_get_toplevel(proxy); + self->proxy2 = (struct wl_proxy *) toplevel; + xdg_toplevel_add_listener(toplevel, &xdg_toplevel_listener, self); + xdg_toplevel_set_title(toplevel, "wl-clipboard"); + xdg_toplevel_set_app_id(toplevel, "io.github.bugaevc.wl-clipboard"); + self->do_destroy = destroy_xdg_surface; +} + +#endif /* HAVE_XDG_SHELL */ diff --git a/src/types/shell-surface.h b/src/types/shell-surface.h new file mode 100644 index 0000000..fe70baf --- /dev/null +++ b/src/types/shell-surface.h @@ -0,0 +1,44 @@ +/* wl-clipboard + * + * 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TYPES_SHELL_SURFACE_H +#define TYPES_SHELL_SURFACE_H + +#include "includes/shell-protocols.h" + +struct shell_surface { + /* This field is initialized by the creator */ + struct wl_proxy *proxy; + + /* These fields are initialized by the implementation */ + struct wl_proxy *proxy2; + void (*do_destroy)(struct shell_surface *self); +}; + +void shell_surface_destroy(struct shell_surface *self); + + +/* Initializers */ + +void shell_surface_init_wl_shell_surface(struct shell_surface *self); + +#ifdef HAVE_XDG_SHELL +void shell_surface_init_xdg_surface(struct shell_surface *self); +#endif + +#endif /* TYPES_SHELL_SURFACE_H */ diff --git a/src/types/shell.c b/src/types/shell.c new file mode 100644 index 0000000..2a57f9f --- /dev/null +++ b/src/types/shell.c @@ -0,0 +1,93 @@ +/* wl-clipboard + * + * 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "types/shell-surface.h" +#include "types/shell.h" +#include "includes/shell-protocols.h" + +#include + +struct shell_surface *shell_create_shell_surface( + struct shell *self, + struct wl_surface *surface +) { + return self->do_create_shell_surface(self, surface); +} + + +/* Core Wayland implementation */ + +static struct shell_surface *wl_shell_create_shell_surface( + struct shell *self, + struct wl_surface *surface +) { + struct wl_shell *shell = (struct wl_shell *) self->proxy; + struct wl_shell_surface *wl_shell_surface = + wl_shell_get_shell_surface(shell, surface); + + struct shell_surface *shell_surface + = calloc(1, sizeof(struct shell_surface)); + shell_surface->proxy = (struct wl_proxy *) wl_shell_surface; + shell_surface_init_wl_shell_surface(shell_surface); + return shell_surface; +} + +void shell_init_wl_shell(struct shell *self) { + self->do_create_shell_surface = wl_shell_create_shell_surface; +} + + +/* xdg-shell implementation */ + +#ifdef HAVE_XDG_SHELL + +static struct shell_surface *xdg_shell_create_shell_surfacce( + struct shell *self, + struct wl_surface *surface +) { + struct xdg_wm_base *wm_base = (struct xdg_wm_base *) self->proxy; + + struct shell_surface *shell_surface + = calloc(1, sizeof(struct shell_surface)); + + struct xdg_surface *xdg_surface = + xdg_wm_base_get_xdg_surface(wm_base, surface); + shell_surface->proxy = (struct wl_proxy *) xdg_surface; + shell_surface_init_xdg_surface(shell_surface); + return shell_surface; +} + +static void xdg_wm_base_ping_handler( + void *data, + struct xdg_wm_base *wm_base, + uint32_t serial +) { + xdg_wm_base_pong(wm_base, serial); +} + +static const struct xdg_wm_base_listener xdg_wm_base_listener = { + .ping = xdg_wm_base_ping_handler +}; + +void shell_init_xdg_shell(struct shell *self) { + struct xdg_wm_base *wm_base = (struct xdg_wm_base *) self->proxy; + xdg_wm_base_add_listener(wm_base, &xdg_wm_base_listener, self); + self->do_create_shell_surface = xdg_shell_create_shell_surfacce; +} + +#endif /* HAVE_XDG_SHELL */ diff --git a/src/types/shell.h b/src/types/shell.h new file mode 100644 index 0000000..5c8c2dd --- /dev/null +++ b/src/types/shell.h @@ -0,0 +1,53 @@ +/* wl-clipboard + * + * 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TYPES_SHELL_H +#define TYPES_SHELL_H + +#include "includes/shell-protocols.h" + +#include + +struct shell_surface; + +struct shell { + /* This field is initialized by the creator */ + struct wl_proxy *proxy; + + /* This field is initialized by the implementation */ + struct shell_surface *(*do_create_shell_surface)( + struct shell *self, + struct wl_surface *surface + ); +}; + +struct shell_surface *shell_create_shell_surface( + struct shell *self, + struct wl_surface *surface +); + + +/* Initializers */ + +void shell_init_wl_shell(struct shell *self); + +#ifdef HAVE_XDG_SHELL +void shell_init_xdg_shell(struct shell *self); +#endif + +#endif /* TYPES_SHELL_H */ diff --git a/src/types/source.c b/src/types/source.c new file mode 100644 index 0000000..78f00ae --- /dev/null +++ b/src/types/source.c @@ -0,0 +1,140 @@ +/* wl-clipboard + * + * 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "config.h" +#include "includes/selection-protocols.h" +#include "types/source.h" +#include "util/string.h" + +#include + +void source_offer(struct source *self, char *mime_type) { + self->do_offer(self->proxy, mime_type); +} + + +/* Macros to reduce implementation boilerplate */ + +#define SEND_HANDLER(type) \ +static void type ## _send_handler( \ + void *data, \ + struct type *proxy, \ + const char *mime_type, \ + int fd \ +) { \ + struct source *self = data; \ + if (self->send_callback != NULL) { \ + self->send_callback(self, mime_type, fd); \ + } else { \ + close(fd); \ + } \ +} + +#define CANCELLED_HANDLER(type) \ +static void type ## _cancelled_handler( \ + void *data, \ + struct type *proxy \ +) { \ + struct source *self = data; \ + if (self->cancelled_callback != NULL) { \ + self->cancelled_callback(self); \ + } \ +} + +#define LISTENER(type) \ +static const struct type ## _listener type ## _listener = { \ + .send = type ## _send_handler, \ + .cancelled = type ## _cancelled_handler \ +}; + +#define INIT(type) \ +void source_init_ ## type(struct source *self) { \ + self->do_offer = \ + (void (*)(struct wl_proxy *, const char *)) type ## _offer; \ + struct type *proxy = (struct type *) self->proxy; \ + type ## _add_listener(proxy, &type ## _listener, self); \ +} + + +/* Core Wayland implementation */ + +static void wl_data_source_target_handler +( + void *data, + struct wl_data_source *wl_data_source, + const char *mime_type +) {} + +SEND_HANDLER(wl_data_source) +CANCELLED_HANDLER(wl_data_source) + +static const struct wl_data_source_listener wl_data_source_listener = { + .target = wl_data_source_target_handler, + .send = wl_data_source_send_handler, + .cancelled = wl_data_source_cancelled_handler +}; + +INIT(wl_data_source) + + +/* gtk-primary-selection implementation */ + +#ifdef HAVE_GTK_PRIMARY_SELECTION + +SEND_HANDLER(gtk_primary_selection_source) +CANCELLED_HANDLER(gtk_primary_selection_source) +LISTENER(gtk_primary_selection_source) +INIT(gtk_primary_selection_source) + +#endif /* HAVE_GTK_PRIMARY_SELECTION */ + + +/* wp-primary-selection implementation */ + +#ifdef HAVE_WP_PRIMARY_SELECTION + +SEND_HANDLER(zwp_primary_selection_source_v1) +CANCELLED_HANDLER(zwp_primary_selection_source_v1) +LISTENER(zwp_primary_selection_source_v1) +INIT(zwp_primary_selection_source_v1) + +#endif /* HAVE_WP_PRIMARY_SELECTION */ + + +/* wlr-data-control implementation */ + +#ifdef HAVE_WLR_DATA_CONTROL + +SEND_HANDLER(zwlr_data_control_source_v1) +CANCELLED_HANDLER(zwlr_data_control_source_v1) +LISTENER(zwlr_data_control_source_v1) +INIT(zwlr_data_control_source_v1) + +#endif /* HAVE_WLR_DATA_CONTROL */ + + +/* 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 new file mode 100644 index 0000000..a29d816 --- /dev/null +++ b/src/types/source.h @@ -0,0 +1,59 @@ +/* wl-clipboard + * + * 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TYPES_SOURCE_H +#define TYPES_SOURCE_H + +#include "includes/selection-protocols.h" + + +struct source { + /* These fields are initialized by the creator */ + void (*send_callback)(struct source *self, const char *mime_type, int fd); + void (*cancelled_callback)(struct source *self); + void *data; + + struct wl_proxy *proxy; + + /* This field is initialized by the implementation */ + void (*do_offer)(struct wl_proxy *proxy, const char *mime_type); +}; + +void source_offer(struct source *self, char *mime_type); + +/* Initializers */ + +void source_init_wl_data_source(struct source *self); + +#ifdef HAVE_GTK_PRIMARY_SELECTION +void source_init_gtk_primary_selection_source(struct source *self); +#endif + +#ifdef HAVE_WP_PRIMARY_SELECTION +void source_init_zwp_primary_selection_source_v1(struct source *self); +#endif + +#ifdef HAVE_WLR_DATA_CONTROL +void source_init_zwlr_data_control_source_v1(struct source *self); +#endif + +#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 new file mode 100644 index 0000000..078c051 --- /dev/null +++ b/src/util/files.c @@ -0,0 +1,357 @@ +/* wl-clipboard + * + * 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 + * 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 "util/files.h" +#include "util/string.h" +#include "util/misc.h" + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include // open +#include // open +#include // exit +#include // basename +#include +#include +#include + +#ifdef HAVE_MEMFD +# include // syscall, SYS_memfd_create +#endif +#ifdef HAVE_SHM_ANON +# 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; +#ifdef HAVE_MEMFD + res = syscall(SYS_memfd_create, "buffer", 0); + if (res >= 0) { + return res; + } +#endif +#ifdef HAVE_SHM_ANON + res = shm_open(SHM_ANON, O_RDWR | O_CREAT, 0600); + if (res >= 0) { + return res; + } +#endif + (void) res; + return fileno(tmpfile()); +} + +void trim_trailing_newline(const char *file_path) { + int fd = open(file_path, O_RDWR); + if (fd < 0) { + perror("open file for trimming"); + return; + } + + int seek_res = lseek(fd, -1, SEEK_END); + if (seek_res < 0 && errno == EINVAL) { + /* It was an empty file */ + goto out; + } else if (seek_res < 0) { + perror("lseek"); + goto out; + } + /* If the seek was successful, seek_res is the + * new file size after trimming the newline. + */ + char last_char; + int read_res = read(fd, &last_char, 1); + if (read_res != 1) { + perror("read"); + goto out; + } + if (last_char != '\n') { + goto out; + } + + int rc = ftruncate(fd, seek_res); + if (rc < 0) { + perror("ftruncate"); + } +out: + close(fd); +} + +char *path_for_fd(int fd) { + char fdpath[64]; + snprintf(fdpath, sizeof(fdpath), "/dev/fd/%d", fd); + return realpath(fdpath, NULL); +} + +char *infer_mime_type_from_contents(const char *file_path) { + /* Spawn xdg-mime query filetype */ + int pipefd[2]; + int rc = pipe(pipefd); + if (rc < 0) { + perror("pipe"); + return NULL; + } + + pid_t pid = fork(); + if (pid < 0) { + perror("fork"); + close(pipefd[0]); + close(pipefd[1]); + return NULL; + } + if (pid == 0) { + dup2(pipefd[1], STDOUT_FILENO); + close(pipefd[0]); + close(pipefd[1]); + int devnull = open("/dev/null", O_RDONLY); + if (devnull >= 0) { + dup2(devnull, STDIN_FILENO); + close(devnull); + } else { + /* 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; + waitpid(pid, &wstatus, 0); + + /* See if that worked */ + if (!WIFEXITED(wstatus) || WEXITSTATUS(wstatus) != 0) { + close(pipefd[0]); + return NULL; + } + + /* Read the result */ + char *res = malloc(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; + + if (str_has_prefix(res, "inode/")) { + free(res); + return NULL; + } + + return res; +} + +static char *search_mime_dot_types_for_ext(const char *ext) { + if (ext == NULL) { + return NULL; + } + + FILE *f = fopen("/etc/mime.types", "r"); + if (f == NULL) { + f = fopen("/usr/local/etc/mime.types", "r"); + } + if (f == NULL) { + return NULL; + } + + for (char line[200]; fgets(line, sizeof(line), f) != NULL;) { + /* Skip comments and blank lines */ + if (line[0] == '#' || line[0] == '\n') { + continue; + } + + /* Each line consists of a mime type and a list of extensions */ + char mime_type[200]; + int consumed; + if (sscanf(line, "%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_pattern[200]; sscanf(lineptr, "%199s%n", ext_pattern, &consumed) == 1;) { + if (strcmp(ext_pattern, ext) == 0) { + fclose(f); + return strdup(mime_type); + } + lineptr += consumed; + } + } + fclose(f); + return NULL; +} + +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 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"; + + /* 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(); + if (pid < 0) { + perror("fork"); + exit(1); + } + if (pid == 0) { + 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; + waitpid(pid, &wstatus, 0); + free(original_path); + if (!WIFEXITED(wstatus) || WEXITSTATUS(wstatus) != 0) { + bail("Failed to copy the file"); + } + return path; +} diff --git a/src/util/files.h b/src/util/files.h new file mode 100644 index 0000000..e39ca4f --- /dev/null +++ b/src/util/files.h @@ -0,0 +1,40 @@ +/* wl-clipboard + * + * 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef 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); + +/* These functions return owned strings, so make sure + * to free() their return values when done with them. + */ + +char *path_for_fd(int fd); +char *infer_mime_type_from_contents(const char *file_path); +char *infer_mime_type_from_name(const char *file_path); + +/* Returns the name of a new file */ +char *dump_stdin_into_a_temp_file(void); + +#endif /* UTIL_FILES_H */ diff --git a/src/util/misc.c b/src/util/misc.c new file mode 100644 index 0000000..4edd822 --- /dev/null +++ b/src/util/misc.c @@ -0,0 +1,139 @@ +/* wl-clipboard + * + * 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "config.h" +#include "util/misc.h" + +#include +#include +#include +#include + +void print_version_info() { + printf( + "wl-clipboard " PROJECT_VERSION "\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" + "There is NO WARRANTY, to the extent permitted by law.\n" + ); +} + +void complain_about_selection_support(int primary) { + if (!primary) { + /* We always expect to find at least wl_data_device_manager */ + complain_about_missing_global("wl_data_device_manager"); + } + +#if !defined(HAVE_WP_PRIMARY_SELECTION) && !defined(HAVE_GTK_PRIMARY_SELECTION) + bail("wl-clipboard was built without primary selection support"); +#endif + + bail("The compositor does not seem to support primary selection"); +} + +void complain_about_watch_mode_support() { +#if defined(HAVE_WLR_DATA_CONTROL) || defined(HAVE_EXT_DATA_CONTROL) + bail( + "Watch mode requires a compositor that supports" + " the data-control protocol" + ); +#else + bail( + "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 new file mode 100644 index 0000000..299a0a6 --- /dev/null +++ b/src/util/misc.h @@ -0,0 +1,36 @@ +/* wl-clipboard + * + * 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef UTIL_MISC_H +#define UTIL_MISC_H + +#include +#include + +#define bail(message) do { fprintf(stderr, message "\n"); exit(1); } while (0) + +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 new file mode 100644 index 0000000..9482bf9 --- /dev/null +++ b/src/util/string.c @@ -0,0 +1,83 @@ +/* wl-clipboard + * + * 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 + * 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 "util/string.h" + +#include +#include + +int mime_type_is_text(const char *mime_type) { + /* A heuristic to detect plain text mime types */ + + /* Types that explicitly declare they're textual */ + int basic + = str_has_prefix(mime_type, "text/") + || strcmp(mime_type, "TEXT") == 0 + || strcmp(mime_type, "STRING") == 0 + || strcmp(mime_type, "UTF8_STRING") == 0; + + /* Common script and markup types */ + int common + = 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, "csv") + || str_has_suffix(mime_type, "ini"); + + /* Special-case PGP and SSH keys. + * A public SSH key is typically stored + * in a file that has a name similar to + * id_rsa.pub, which xdg-mime misidentifies + * as being a Publisher file. Note that it + * handles private keys, which do not have + * a .pub extension, correctly. + */ + int special + = strstr(mime_type, "application/vnd.ms-publisher") != NULL + || str_has_suffix(mime_type, "pgp-keys"); + + return basic || common || special; +} + +int str_has_prefix(const char *string, const char *prefix) { + size_t prefix_length = strlen(prefix); + return strncmp(string, prefix, prefix_length) == 0; +} + +int str_has_suffix(const char *string, const char *suffix) { + size_t string_length = strlen(string); + size_t suffix_length = strlen(suffix); + if (string_length < suffix_length) { + return 0; + } + size_t offset = string_length - suffix_length; + return strcmp(string + offset, suffix) == 0; +} + +const char *get_file_extension(const char *file_path) { + const char *name = strrchr(file_path, '/'); + if (name == NULL) { + name = file_path; + } + const char *ext = strrchr(name, '.'); + if (ext == NULL) { + return NULL; + } + return ext + 1; +} diff --git a/src/util/string.h b/src/util/string.h new file mode 100644 index 0000000..e007f2e --- /dev/null +++ b/src/util/string.h @@ -0,0 +1,35 @@ +/* wl-clipboard + * + * 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef UTIL_STRING_H +#define UTIL_STRING_H + +#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; + +int mime_type_is_text(const char *mime_type); + +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); + +#endif /* UTIL_STRING_H */ diff --git a/src/wl-copy.c b/src/wl-copy.c index 395c4cc..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 @@ -16,358 +16,98 @@ * along with this program. If not, see . */ -#include "boilerplate.h" +#include "types/copy-action.h" +#include "types/source.h" +#include "types/device.h" +#include "types/device-manager.h" +#include "types/registry.h" +#include "types/popup-surface.h" +#include "types/offer.h" -char * const *data_to_copy = NULL; -char *temp_file_to_copy = NULL; -int paste_once = 0; +#include "util/files.h" +#include "util/string.h" +#include "util/misc.h" -void do_cancel() { - // we're done! - if (temp_file_to_copy != NULL) { - execlp("rm", "rm", "-r", dirname(temp_file_to_copy), NULL); - perror("exec rm"); - exit(1); - } else { +#include +#include +#include +#include // open +#include +#include +#include + +static struct { + int stay_in_foreground; + int clear; + char *mime_type; + int trim_newline; + int paste_once; + int primary; + int sensitive; + const char *seat_name; +} options; + +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. 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) { + 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); + } + if (chdir("/") < 0) { + perror("chdir /"); + } + signal(SIGHUP, SIG_IGN); + pid_t pid = fork(); + if (pid < 0) { + perror("fork"); + /* Proceed without forking */ + } + if (pid > 0) { + exit(0); + } + } +} + +static void cancelled_callback(struct copy_action *copy_action) { + exit(0); +} + +static void pasted_callback(struct copy_action *copy_action) { + if (options.paste_once) { exit(0); } } -void do_send(const char *mime_type, int fd) { - // unset O_NONBLOCK - fcntl(fd, F_SETFL, 0); - if (data_to_copy != NULL) { - // copy the specified data, separated by spaces - FILE *f = fdopen(fd, "w"); - if (f == NULL) { - perror("fdopen"); - exit(1); - } - char * const *dataptr = data_to_copy; - for (int is_first = 1; *dataptr != NULL; dataptr++, is_first = 0) { - if (!is_first) { - fwrite(" ", 1, 1, f); - } - fwrite(*dataptr, 1, strlen(*dataptr), f); - } - fclose(f); - } else { - // copy from the temp file; for that, we delegate to a - // (hopefully) highly optimized implementation of copying - if (fork() == 0) { - dup2(fd, STDOUT_FILENO); - execlp("cat", "cat", temp_file_to_copy, NULL); - perror("exec cat"); - exit(1); - } - close(fd); - wait(NULL); - } - - if (paste_once) { - do_cancel(); +static void selection_callback(struct offer *offer, int primary) { + /* We're not interested */ + if (offer != NULL) { + offer_destroy(offer); } } -void data_source_target_handler -( - void *data, - struct wl_data_source *data_source, - const char *mime_type -) {} - -void data_source_send_handler -( - void *data, - struct wl_data_source *data_source, - const char *mime_type, - int fd -) { - do_send(mime_type, fd); -} - -void data_source_cancelled_handler -( - void *data, - struct wl_data_source *data_source -) { - do_cancel(); -} - -const struct wl_data_source_listener data_source_listener = { - .target = data_source_target_handler, - .send = data_source_send_handler, - .cancelled = data_source_cancelled_handler -}; - -struct wl_data_source *data_source; - -void set_data_selection(uint32_t serial) { - wl_data_device_set_selection(data_device, data_source, serial); - wl_display_roundtrip(display); - destroy_popup_surface(); -} - -void try_setting_data_selection_directly() { - set_data_selection(get_serial()); -} - - -void complain_about_missing_keyboard() { - bail("Setting primary selection is not supported without a keyboard"); -} - -#ifdef HAVE_GTK_PRIMARY_SELECTION - -void gtk_primary_selection_source_send_handler -( - void *data, - struct gtk_primary_selection_source *gtk_primary_selection_source, - const char *mime_type, - int fd -) { - do_send(mime_type, fd); -} - -void gtk_primary_selection_source_cancelled_handler -( - void *data, - struct gtk_primary_selection_source *gtk_primary_selection_source -) { - do_cancel(); -} - -const struct gtk_primary_selection_source_listener -gtk_primary_selection_source_listener = { - .send = gtk_primary_selection_source_send_handler, - .cancelled = gtk_primary_selection_source_cancelled_handler -}; - -struct gtk_primary_selection_source *gtk_primary_selection_source; - -void set_gtk_primary_selection(uint32_t serial) { - - gtk_primary_selection_device_set_selection( - gtk_primary_selection_device, - gtk_primary_selection_source, - serial - ); - - wl_display_roundtrip(display); - destroy_popup_surface(); -} - -#endif - -#ifdef HAVE_WP_PRIMARY_SELECTION - -void primary_selection_source_send_handler -( - void *data, - struct zwp_primary_selection_source_v1 *primary_selection_source, - const char *mime_type, - int fd -) { - do_send(mime_type, fd); -} - -void primary_selection_source_cancelled_handler -( - void *data, - struct zwp_primary_selection_source_v1 *primary_selection_source -) { - do_cancel(); -} - -const struct zwp_primary_selection_source_v1_listener -primary_selection_source_listener = { - .send = primary_selection_source_send_handler, - .cancelled = primary_selection_source_cancelled_handler -}; - -struct zwp_primary_selection_source_v1 *primary_selection_source; - -void set_primary_selection(uint32_t serial) { - - zwp_primary_selection_device_v1_set_selection( - primary_selection_device, - primary_selection_source, - serial - ); - - wl_display_roundtrip(display); - destroy_popup_surface(); -} - -#endif - -#ifdef HAVE_WLR_DATA_CONTROL - -void data_control_source_send_handler -( - void *data, - struct zwlr_data_control_source_v1 *data_control_source, - const char *mime_type, - int fd -) { - do_send(mime_type, fd); -} - -void data_control_source_cancelled_handler -( - void *data, - struct zwlr_data_control_source_v1 *data_source -) { - do_cancel(); -} - -const struct zwlr_data_control_source_v1_listener -data_control_source_listener = { - .send = data_control_source_send_handler, - .cancelled = data_control_source_cancelled_handler -}; -#endif - -void do_offer -( - char *mime_type, - void *source, - void (*offer_f)(void *source, const char *type) -) { - if (mime_type == NULL || mime_type_is_text(mime_type)) { - // offer a few generic plain text formats - offer_f(source, text_plain); - offer_f(source, text_plain_utf8); - offer_f(source, "TEXT"); - offer_f(source, "STRING"); - offer_f(source, "UTF8_STRING"); - } - if (mime_type != NULL) { - offer_f(source, mime_type); - } - free(mime_type); -} - -#ifdef HAVE_WLR_DATA_CONTROL - -struct zwlr_data_control_source_v1 * -create_data_control_source(char *mime_type) { - struct zwlr_data_control_source_v1 *data_control_source = - zwlr_data_control_manager_v1_create_data_source(data_control_manager); - zwlr_data_control_source_v1_add_listener( - data_control_source, - &data_control_source_listener, - NULL - ); - - do_offer( - mime_type, - data_control_source, - (void (*)(void *, const char *)) zwlr_data_control_source_v1_offer - ); - return data_control_source; -} - -#endif - -void init_selection(char *mime_type) { - if (data_control_version) { -#ifdef HAVE_WLR_DATA_CONTROL - struct zwlr_data_control_source_v1 *data_control_source = - create_data_control_source(mime_type); - - zwlr_data_control_device_v1_set_selection( - data_control_device, - data_control_source - ); -#endif - } else { - data_source = wl_data_device_manager_create_data_source( - data_device_manager - ); - wl_data_source_add_listener(data_source, &data_source_listener, NULL); - - do_offer( - mime_type, - data_source, - (void (*)(void *, const char *)) wl_data_source_offer - ); - - action_on_popup_surface_getting_focus = set_data_selection; - action_on_no_keyboard = try_setting_data_selection_directly; - popup_tiny_invisible_surface(); - } -} - -void init_primary_selection(char *mime_type) { - ensure_has_primary_selection(); - -#ifdef HAVE_WLR_DATA_CONTROL - if (data_control_supports_primary_selection) { - struct zwlr_data_control_source_v1 *data_control_source = - create_data_control_source(mime_type); - - zwlr_data_control_device_v1_set_primary_selection( - data_control_device, - data_control_source - ); - return; - } -#endif - -#ifdef HAVE_WP_PRIMARY_SELECTION - if (primary_selection_device_manager != NULL) { - primary_selection_source = - zwp_primary_selection_device_manager_v1_create_source( - primary_selection_device_manager - ); - zwp_primary_selection_source_v1_add_listener( - primary_selection_source, - &primary_selection_source_listener, - NULL - ); - - do_offer( - mime_type, - primary_selection_source, - (void (*)(void *, const char *)) - zwp_primary_selection_source_v1_offer - ); - - action_on_popup_surface_getting_focus = set_primary_selection; - action_on_no_keyboard = complain_about_missing_keyboard; - popup_tiny_invisible_surface(); - return; - } -#endif - -#ifdef HAVE_GTK_PRIMARY_SELECTION - if (gtk_primary_selection_device_manager != NULL) { - gtk_primary_selection_source = - gtk_primary_selection_device_manager_create_source( - gtk_primary_selection_device_manager - ); - gtk_primary_selection_source_add_listener( - gtk_primary_selection_source, - >k_primary_selection_source_listener, - NULL - ); - - do_offer( - mime_type, - gtk_primary_selection_source, - (void (*)(void *, const char *)) gtk_primary_selection_source_offer - ); - - action_on_popup_surface_getting_focus = set_gtk_primary_selection; - action_on_no_keyboard = complain_about_missing_keyboard; - popup_tiny_invisible_surface(); - return; - } -#endif -} - -void print_usage(FILE *f, const char *argv0) { +static void print_usage(FILE *f, const char *argv0) { fprintf( f, "Usage:\n" @@ -377,11 +117,12 @@ 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" @@ -394,18 +135,11 @@ void print_usage(FILE *f, const char *argv0) { ); } -int main(int argc, char * const argv[]) { - +static void parse_options(int argc, argv_t argv) { if (argc < 1) { bail("Empty argv"); } - int stay_in_foreground = 0; - int clear = 0; - char *mime_type = NULL; - int primary = 0; - int trim_newline = 0; - static struct option long_options[] = { {"version", no_argument, 0, 'v'}, {"help", no_argument, 0, 'h'}, @@ -415,6 +149,7 @@ int main(int argc, char * const 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} }; @@ -436,75 +171,164 @@ int main(int argc, char * const argv[]) { print_usage(stdout, argv[0]); exit(0); case 'p': - primary = 1; + options.primary = 1; break; case 'n': - trim_newline = 1; + options.trim_newline = 1; break; case 'o': - paste_once = 1; + options.paste_once = 1; break; case 'f': - stay_in_foreground = 1; + options.stay_in_foreground = 1; break; case 'c': - clear = 1; + options.clear = 1; break; case 't': - mime_type = strdup(optarg); + options.mime_type = strdup(optarg); + break; + case 'S': + options.sensitive = 1; break; case 's': - requested_seat_name = strdup(optarg); + options.seat_name = strdup(optarg); break; default: - // getopt has already printed an error message + /* getopt has already printed an error message */ print_usage(stderr, argv[0]); exit(1); } } +} - init_wayland_globals(); +int main(int argc, argv_t argv) { + parse_options(argc, argv); - if (primary) { - ensure_has_primary_selection(); + /* 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) { + complain_about_wayland_connection(); + } + if (wl_display_get_fd(wl_display) <= STDERR_FILENO) { + complain_about_closed_stdio(wl_display); } - if (!clear) { + struct registry *registry = calloc(1, sizeof(struct registry)); + registry->wl_display = wl_display; + registry_init(registry); + + /* Wait for the initial set of globals to appear */ + wl_display_roundtrip(wl_display); + + struct seat *seat = registry_find_seat(registry, options.seat_name); + if (seat == NULL) { + complain_about_missing_seat(options.seat_name); + } + + /* 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); + device->selection_callback = selection_callback; + + if (!device_supports_selection(device, options.primary)) { + complain_about_selection_support(options.primary); + } + + /* 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) { - // copy our command-line args - data_to_copy = &argv[optind]; + /* Copy our command-line arguments */ + copy_action->argv_to_copy = &argv[optind]; } else { - // copy stdin - temp_file_to_copy = dump_stdin_into_a_temp_file(); - if (trim_newline) { - trim_trailing_newline(temp_file_to_copy); + /* Copy data from our stdin. + * 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); } - if (mime_type == NULL) { - mime_type = infer_mime_type_from_contents(temp_file_to_copy); + if (options.mime_type == NULL) { + options.mime_type = infer_mime_type_from_contents(temp_file); } + copy_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 */ + 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 (options.sensitive) { + source_offer(copy_action->source, x_kde_password_manager_hint); } } - if (!stay_in_foreground && !clear) { - if (fork() != 0) { - // exit in the parent, but leave the - // child running in the background - exit(0); - } + if (device->needs_popup_surface) { + copy_action->popup_surface = calloc(1, sizeof(struct popup_surface)); + copy_action->popup_surface->registry = registry; + copy_action->popup_surface->seat = seat; } - if (!primary) { - init_selection(mime_type); - } else { - init_primary_selection(mime_type); - } + 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); - if (clear) { - wl_display_roundtrip(display); - exit(0); - } - - while (wl_display_dispatch(display) >= 0); + while (wl_display_dispatch(wl_display) >= 0); perror("wl_display_dispatch"); return 1; diff --git a/src/wl-paste.c b/src/wl-paste.c index 962fe3d..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 @@ -16,98 +16,131 @@ * along with this program. If not, see . */ -#include "boilerplate.h" +#include "types/offer.h" +#include "types/device.h" +#include "types/device-manager.h" +#include "types/registry.h" +#include "types/popup-surface.h" -struct { +#include "util/files.h" +#include "util/string.h" +#include "util/misc.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static struct { char *explicit_type; char *inferred_type; int no_newline; int list_types; int primary; + int watch; + argv_t watch_command; + const char *seat_name; } options; -struct { +struct types { int explicit_available; int inferred_available; int plain_text_utf8_available; int plain_text_available; - char *having_explicit_as_prefix; - char *any_text; - char *any; -} available_types; + int has_sensitive_hint; + const char *having_explicit_as_prefix; + const char *any_text; + const char *any; +}; -void do_process_offer(const char *offered_type) { - if (options.list_types) { - printf("%s\n", offered_type); - } else { +static struct wl_display *wl_display = NULL; +static struct popup_surface *popup_surface = NULL; +static int offer_received = 0; + +static struct types classify_offer_types(struct offer *offer) { + struct types types = { 0 }; + offer_for_each_mime_type(offer, mime_type) { if ( options.explicit_type != NULL && - strcmp(offered_type, options.explicit_type) == 0 + strcmp(mime_type, options.explicit_type) == 0 ) { - available_types.explicit_available = 1; + types.explicit_available = 1; } if ( options.inferred_type != NULL && - strcmp(offered_type, options.inferred_type) == 0 + strcmp(mime_type, options.inferred_type) == 0 ) { - available_types.inferred_available = 1; + types.inferred_available = 1; + } + if (strcmp(mime_type, text_plain_utf8) == 0) { + types.plain_text_utf8_available = 1; + } + if (strcmp(mime_type, text_plain) == 0) { + types.plain_text_available = 1; } if ( - strcmp(offered_type, text_plain_utf8) == 0) { - available_types.plain_text_utf8_available = 1; - } - if ( - strcmp(offered_type, text_plain) == 0) { - available_types.plain_text_available = 1; - } - if ( - available_types.any_text == NULL && - mime_type_is_text(offered_type) + types.any_text == NULL && + mime_type_is_text(mime_type) ) { - available_types.any_text = strdup(offered_type); + types.any_text = mime_type; } - if (available_types.any == NULL) { - available_types.any = strdup(offered_type); + if (types.any == NULL) { + types.any = mime_type; } if ( options.explicit_type != NULL && - available_types.having_explicit_as_prefix == NULL && - str_has_prefix(offered_type, options.explicit_type) + types.having_explicit_as_prefix == NULL && + str_has_prefix(mime_type, options.explicit_type) ) { - available_types.having_explicit_as_prefix = strdup(offered_type); + types.having_explicit_as_prefix = mime_type; + } + 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; } #define try_explicit \ -if (available_types.explicit_available) \ +if (types.explicit_available) \ return options.explicit_type #define try_inferred \ -if (available_types.inferred_available) \ +if (types.inferred_available) \ return options.inferred_type #define try_text_plain_utf8 \ -if (available_types.plain_text_utf8_available) \ +if (types.plain_text_utf8_available) \ return text_plain_utf8 #define try_text_plain \ -if (available_types.plain_text_available) \ +if (types.plain_text_available) \ return text_plain #define try_prefixed \ -if (available_types.having_explicit_as_prefix != NULL) \ - return available_types.having_explicit_as_prefix +if (types.having_explicit_as_prefix != NULL) \ + return types.having_explicit_as_prefix #define try_any_text \ -if (available_types.any_text != NULL) \ - return available_types.any_text +if (types.any_text != NULL) \ + return types.any_text #define try_any \ -if (available_types.any != NULL) \ - return available_types.any +if (types.any != NULL) \ + return types.any -const char *mime_type_to_request() { +static const char *mime_type_to_request(struct types types) { if (options.explicit_type != NULL) { if (strcmp(options.explicit_type, "text") == 0) { try_text_plain_utf8; @@ -122,7 +155,9 @@ const char *mime_type_to_request() { try_prefixed; } } else { - // no mime type requested explicitly, try to guess + /* No mime type requested explicitly, + * so try to guess. + */ if (options.inferred_type == NULL) { try_text_plain_utf8; try_text_plain; @@ -137,7 +172,7 @@ const char *mime_type_to_request() { try_inferred; } } - bail("No suitable type of content copied"); + return NULL; } #undef try_explicit @@ -148,265 +183,189 @@ const char *mime_type_to_request() { #undef try_any_text #undef try_any -void free_types() { - free(available_types.having_explicit_as_prefix); - free(available_types.any_text); - free(available_types.any); - free(options.explicit_type); - free(options.inferred_type); +static 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; } -void do_paste -( - void *offer, - void (*receive_f)(void *offer, const char *mime_type, int fd) -) { - if (offer == NULL) { - bail("No selection"); +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) { + offer_destroy(offer); + }; + return; + } + + if (offer == NULL) { + if (!options.watch) { + bail("Nothing is copied"); + } + int devnull = open("/dev/null", O_RDONLY | O_CLOEXEC); + if (devnull < 0) { + perror("open /dev/null"); + return; + } + 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); + } exit(0); } - const char *mime_type = mime_type_to_request(); - // never append a newline character to binary content + struct types types = classify_offer_types(offer); + const char *mime_type = mime_type_to_request(types); + + if (mime_type == NULL) { + if (options.watch) { + offer_destroy(offer); + return; + } + complain_no_suitable_type(&types); + } + + /* Never append a newline character to binary content */ if (!mime_type_is_text(mime_type)) { options.no_newline = 1; } + /* Create a pipe which we'll + * use to receive the data. + */ int pipefd[2]; - pipe(pipefd); - - receive_f(offer, mime_type, pipefd[1]); - - free_types(); - destroy_popup_surface(); - - wl_display_roundtrip(display); - - if (fork() == 0) { - dup2(pipefd[0], STDIN_FILENO); - close(pipefd[0]); - close(pipefd[1]); - execlp("cat", "cat", NULL); - perror("exec cat"); + int rc = pipe(pipefd); + if (rc < 0) { + perror("pipe"); + offer_destroy(offer); + if (options.watch) { + return; + } exit(1); } - close(pipefd[0]); + + offer_receive(offer, mime_type, pipefd[1]); + + if (popup_surface != NULL) { + popup_surface_destroy(popup_surface); + popup_surface = NULL; + } + /* Make sure the receive request reaches + * the compositor before we block on reading. + * We call flush() instead of dispatch() to + * prevent reentrancy. + */ + wl_display_flush(wl_display); + close(pipefd[1]); - wait(NULL); - if (!options.no_newline) { - write(STDOUT_FILENO, "\n", 1); + const char *clipboard_state = "data"; + if (types.has_sensitive_hint) { + clipboard_state = "sensitive"; } - exit(0); -} - -void data_offer_offer -( - void *data, - struct wl_data_offer *data_offer, - const char *offered_mime_type -) { - do_process_offer(offered_mime_type); -} - -const struct wl_data_offer_listener data_offer_listener = { - .offer = data_offer_offer -}; - -void data_device_data_offer -( - void *data, - struct wl_data_device *data_device, - struct wl_data_offer *data_offer -) { - wl_data_offer_add_listener(data_offer, &data_offer_listener, NULL); -} - -void data_device_selection -( - void *data, - struct wl_data_device *data_device, - struct wl_data_offer *data_offer -) { - do_paste( - data_offer, - (void (*)(void *, const char *, int)) wl_data_offer_receive - ); -} - -const struct wl_data_device_listener data_device_listener = { - .data_offer = data_device_data_offer, - .selection = data_device_selection -}; - -#ifdef HAVE_GTK_PRIMARY_SELECTION - -void gtk_primary_selection_offer_offer -( - void *data, - struct gtk_primary_selection_offer *gtk_primary_selection_offer, - const char *offered_mime_type -) { - do_process_offer(offered_mime_type); -} - -const struct gtk_primary_selection_offer_listener -gtk_primary_selection_offer_listener = { - .offer = gtk_primary_selection_offer_offer -}; - -void gtk_primary_selection_device_data_offer -( - void *data, - struct gtk_primary_selection_device *gtk_primary_selection_device, - struct gtk_primary_selection_offer *gtk_primary_selection_offer -) { - gtk_primary_selection_offer_add_listener( - gtk_primary_selection_offer, - >k_primary_selection_offer_listener, - NULL - ); -} - -void gtk_primary_selection_device_selection -( - void *data, - struct gtk_primary_selection_device *gtk_primary_selection_device, - struct gtk_primary_selection_offer *gtk_primary_selection_offer -) { - do_paste( - gtk_primary_selection_offer, - (void (*)(void *, const char *, int)) - gtk_primary_selection_offer_receive - ); -} - -const struct gtk_primary_selection_device_listener -gtk_primary_selection_device_listener = { - .data_offer = gtk_primary_selection_device_data_offer, - .selection = gtk_primary_selection_device_selection -}; - -#endif - -#ifdef HAVE_WP_PRIMARY_SELECTION - -void primary_selection_offer_offer -( - void *data, - struct zwp_primary_selection_offer_v1 *primary_selection_offer, - const char *offered_mime_type -) { - do_process_offer(offered_mime_type); -} - -const struct zwp_primary_selection_offer_v1_listener -primary_selection_offer_listener = { - .offer = primary_selection_offer_offer -}; - -void primary_selection_device_data_offer -( - void *data, - struct zwp_primary_selection_device_v1 *primary_selection_device, - struct zwp_primary_selection_offer_v1 *primary_selection_offer -) { - zwp_primary_selection_offer_v1_add_listener( - primary_selection_offer, - &primary_selection_offer_listener, - NULL - ); -} - -void primary_selection_device_selection -( - void *data, - struct zwp_primary_selection_device_v1 *primary_selection_device, - struct zwp_primary_selection_offer_v1 *primary_selection_offer -) { - do_paste( - primary_selection_offer, - (void (*)(void *, const char *, int)) - zwp_primary_selection_offer_v1_receive - ); -} - -const struct zwp_primary_selection_device_v1_listener -primary_selection_device_listener = { - .data_offer = primary_selection_device_data_offer, - .selection = primary_selection_device_selection -}; - -#endif - -#ifdef HAVE_WLR_DATA_CONTROL -void data_control_offer_offer -( - void *data, - struct zwlr_data_control_offer_v1 *data_offer, - const char *offered_mime_type -) { - do_process_offer(offered_mime_type); -} - -const struct zwlr_data_control_offer_v1_listener data_control_offer_listener = { - .offer = data_control_offer_offer -}; - -void data_control_device_data_offer -( - void *data, - struct zwlr_data_control_device_v1 *data_control_device, - struct zwlr_data_control_offer_v1 *data_control_offer -) { - zwlr_data_control_offer_v1_add_listener( - data_control_offer, - &data_control_offer_listener, - NULL - ); -} - -void data_control_device_selection -( - void *data, - struct zwlr_data_control_device_v1 *data_control_device, - struct zwlr_data_control_offer_v1 *data_control_offer -) { - if (options.primary) { - return; + rc = run_paste_command(pipefd[0], clipboard_state, mime_type); + if (!rc) { + if (options.watch) { + /* Try to cope without exiting completely */ + offer_destroy(offer); + return; + } + exit(1); } - do_paste( - data_control_offer, - (void (*)(void *, const char *, int)) zwlr_data_control_offer_v1_receive - ); -} -void data_control_device_primary_selection -( - void *data, - struct zwlr_data_control_device_v1 *data_control_device, - struct zwlr_data_control_offer_v1 *data_control_offer -) { - if (!options.primary) { - return; + if (!options.no_newline && !options.watch) { + rc = write(STDOUT_FILENO, "\n", 1); + if (rc != 1) { + perror("write"); + } + } + + offer_destroy(offer); + + if (!options.watch) { + free(options.explicit_type); + free(options.inferred_type); + exit(0); } - do_paste( - data_control_offer, - (void (*)(void *, const char *, int)) zwlr_data_control_offer_v1_receive - ); } -const struct zwlr_data_control_device_v1_listener -data_control_device_listener = { - .data_offer = data_control_device_data_offer, - .selection = data_control_device_selection, - .primary_selection = data_control_device_primary_selection -}; -#endif - -void print_usage(FILE *f, const char *argv0) { +static void print_usage(FILE *f, const char *argv0) { fprintf( f, "Usage:\n" @@ -416,6 +375,8 @@ void print_usage(FILE *f, const char *argv0) { "\t-n, --no-newline\tDo not append a newline character.\n" "\t-l, --list-types\tInstead of pasting, list the offered types.\n" "\t-p, --primary\t\tUse the \"primary\" clipboard.\n" + "\t-w, --watch command\t" + "Run a command each time the selection changes.\n" "\t-t, --type mime/type\t" "Override the inferred MIME type for the content.\n" "\t-s, --seat seat-name\t" @@ -429,62 +390,7 @@ void print_usage(FILE *f, const char *argv0) { ); } -void init_selection() { - if (data_control_version) { -#ifdef HAVE_WLR_DATA_CONTROL - zwlr_data_control_device_v1_add_listener( - data_control_device, - &data_control_device_listener, - NULL - ); -#endif - } else { - wl_data_device_add_listener(data_device, &data_device_listener, NULL); - popup_tiny_invisible_surface(); - } -} - -void init_primary_selection() { - ensure_has_primary_selection(); - -#ifdef HAVE_WLR_DATA_CONTROL - if (data_control_supports_primary_selection) { - zwlr_data_control_device_v1_add_listener( - data_control_device, - &data_control_device_listener, - NULL - ); - return; - } -#endif - -#ifdef HAVE_WP_PRIMARY_SELECTION - if (primary_selection_device != NULL) { - zwp_primary_selection_device_v1_add_listener( - primary_selection_device, - &primary_selection_device_listener, - NULL - ); - popup_tiny_invisible_surface(); - return; - } -#endif - -#ifdef HAVE_GTK_PRIMARY_SELECTION - if (gtk_primary_selection_device != NULL) { - gtk_primary_selection_device_add_listener( - gtk_primary_selection_device, - >k_primary_selection_device_listener, - NULL - ); - popup_tiny_invisible_surface(); - return; - } -#endif -} - -int main(int argc, char * const argv[]) { - +static void parse_options(int argc, argv_t argv) { if (argc < 1) { bail("Empty argv"); } @@ -495,13 +401,14 @@ int main(int argc, char * const argv[]) { {"primary", no_argument, 0, 'p'}, {"no-newline", no_argument, 0, 'n'}, {"list-types", no_argument, 0, 'l'}, + {"watch", required_argument, 0, 'w'}, {"type", required_argument, 0, 't'}, {"seat", required_argument, 0, 's'}, {0, 0, 0, 0} }; while (1) { int option_index; - const char *opts = "vhpnlt:s:"; + const char *opts = "vhpnlw:t:s:"; int c = getopt_long(argc, argv, opts, long_options, &option_index); if (c == -1) { break; @@ -525,34 +432,124 @@ int main(int argc, char * const argv[]) { case 'l': options.list_types = 1; break; + case 'w': + options.watch = 1; + /* We tell getopt that --watch requires an argument, + * but it's more nuanced than that. We actually take + * that argument and everything that follows it as a + * subcommand to spawn. When we get here, getopt sets + * the optind variable to point to the *next* option + * to be processed, after both the current option and + * its argument, if any. So if we're invoked like this: + * $ wl-paste --primary --watch foo bar baz + * then optind will point to bar, as it considers foo + * to be the argument that --watch requires. However, + * if we get invoked like this: + * $ wl-paste --watch=foo bar baz + * or like this: + * $ wl-paste -wfoo bar baz + * then getopt will not consider it an error, and optind + * will also point to bar. We do consider that an error, + * so detect this case and print an error message. + */ + options.watch_command = (argv_t) &argv[optind - 1]; + if (options.watch_command[0][0] == '-') { + fprintf( + stderr, + "Expected a subcommand instead of an argument" + " after --watch\n" + ); + print_usage(stderr, argv[0]); + exit(1); + } + /* We're going to forward the rest of our + * arguments to the command we spawn, so stop + * trying to process further options. + */ + return; case 't': options.explicit_type = strdup(optarg); break; case 's': - requested_seat_name = strdup(optarg); + options.seat_name = strdup(optarg); break; default: - // getopt has already printed an error message + /* getopt has already printed an error message */ print_usage(stderr, argv[0]); exit(1); } } + if (optind != argc) { + fprintf(stderr, "Unexpected argument: %s\n", argv[optind]); + print_usage(stderr, argv[0]); + exit(1); + } +} + +int main(int argc, argv_t argv) { + parse_options(argc, argv); + char *path = path_for_fd(STDOUT_FILENO); if (path != NULL && options.explicit_type == NULL) { options.inferred_type = infer_mime_type_from_name(path); } free(path); - init_wayland_globals(); - - if (!options.primary) { - init_selection(); - } else { - init_primary_selection(); + wl_display = wl_display_connect(NULL); + if (wl_display == NULL) { + complain_about_wayland_connection(); + } + if (wl_display_get_fd(wl_display) <= STDERR_FILENO) { + complain_about_closed_stdio(wl_display); } - while (wl_display_dispatch(display) >= 0); + struct registry *registry = calloc(1, sizeof(struct registry)); + registry->wl_display = wl_display; + registry_init(registry); + + /* Wait for the initial set of globals to appear */ + wl_display_roundtrip(wl_display); + + struct seat *seat = registry_find_seat(registry, options.seat_name); + if (seat == NULL) { + complain_about_missing_seat(options.seat_name); + } + + /* Create the device */ + struct device_manager *device_manager + = registry_find_device_manager(registry, options.primary); + if (device_manager == NULL) { + complain_about_selection_support(options.primary); + } + + struct device *device = device_manager_get_device(device_manager, seat); + /* Set up the callback before checking whether the device + * actually supports the kind of selection we need, because + * checking for the support might roundtrip. + */ + device->selection_callback = selection_callback; + + if (!device_supports_selection(device, options.primary)) { + complain_about_selection_support(options.primary); + } + + if (device->needs_popup_surface) { + if (options.watch) { + complain_about_watch_mode_support(); + } + /* If we cannot get the selection directly, pop up + * a surface. When it gets focus, we'll immediately + * get the selection events, se we don't need to do + * anything special on the surface getting focus. + */ + popup_surface = calloc(1, sizeof(struct popup_surface)); + popup_surface->registry = registry; + popup_surface->seat = seat; + popup_surface_init(popup_surface); + } + + while (wl_display_dispatch(wl_display) >= 0); perror("wl_display_dispatch"); return 1; 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