From aa64f11950ebe3e88bbafd10e0a6e0259ef2129d Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Tue, 16 Mar 2021 16:09:59 +0100 Subject: [PATCH] feat(clipboard): implement clipboard on X11 systems --- espanso-clipboard/Cargo.toml | 2 +- espanso-clipboard/build.rs | 7 +- espanso-clipboard/src/lib.rs | 8 +- espanso-clipboard/src/x11/mod.rs | 20 + espanso-clipboard/src/x11/native/README.md | 4 + .../src/x11/native/clip/LICENSE.txt | 20 + .../src/x11/native/clip/clip.cpp | 174 +++ espanso-clipboard/src/x11/native/clip/clip.h | 178 +++ .../src/x11/native/clip/clip_common.h | 76 ++ .../src/x11/native/clip/clip_lock_impl.h | 33 + .../src/x11/native/clip/clip_x11.cpp | 1091 +++++++++++++++++ .../src/x11/native/clip/clip_x11_png.h | 226 ++++ .../src/x11/native/clip/image.cpp | 83 ++ espanso-clipboard/src/x11/native/ffi.rs | 28 + espanso-clipboard/src/x11/native/mod.rs | 106 ++ espanso-clipboard/src/x11/native/native.cpp | 76 ++ espanso-clipboard/src/x11/native/native.h | 31 + espanso/Cargo.toml | 2 +- 18 files changed, 2153 insertions(+), 12 deletions(-) create mode 100644 espanso-clipboard/src/x11/mod.rs create mode 100644 espanso-clipboard/src/x11/native/README.md create mode 100644 espanso-clipboard/src/x11/native/clip/LICENSE.txt create mode 100644 espanso-clipboard/src/x11/native/clip/clip.cpp create mode 100644 espanso-clipboard/src/x11/native/clip/clip.h create mode 100644 espanso-clipboard/src/x11/native/clip/clip_common.h create mode 100644 espanso-clipboard/src/x11/native/clip/clip_lock_impl.h create mode 100644 espanso-clipboard/src/x11/native/clip/clip_x11.cpp create mode 100644 espanso-clipboard/src/x11/native/clip/clip_x11_png.h create mode 100644 espanso-clipboard/src/x11/native/clip/image.cpp create mode 100644 espanso-clipboard/src/x11/native/ffi.rs create mode 100644 espanso-clipboard/src/x11/native/mod.rs create mode 100644 espanso-clipboard/src/x11/native/native.cpp create mode 100644 espanso-clipboard/src/x11/native/native.h diff --git a/espanso-clipboard/Cargo.toml b/espanso-clipboard/Cargo.toml index f364a3f..80e2e0c 100644 --- a/espanso-clipboard/Cargo.toml +++ b/espanso-clipboard/Cargo.toml @@ -7,7 +7,7 @@ build="build.rs" [features] # If the wayland feature is enabled, all X11 dependencies will be dropped -# and only EVDEV-based methods will be supported. +# and wayland support will be enabled wayland = [] [dependencies] diff --git a/espanso-clipboard/build.rs b/espanso-clipboard/build.rs index 4a23b65..cf3c379 100644 --- a/espanso-clipboard/build.rs +++ b/espanso-clipboard/build.rs @@ -49,16 +49,13 @@ fn cc_config() { .file("src/x11/native/clip/clip_x11.cpp") .file("src/x11/native/clip/image.cpp") .file("src/x11/native/native.cpp") - .define("CLIP_X11_WITH_PNG", None) .compile("espansoclipboardx11"); println!("cargo:rustc-link-search=native=/usr/lib/x86_64-linux-gnu/"); println!("cargo:rustc-link-lib=static=espansoclipboardx11"); - - // TODO: link xcb, libpng? - println!("cargo:rustc-link-lib=dylib=xcb"); - //println!("cargo:rustc-link-lib=dylib=X11"); + } else { + // TODO: wayland } } diff --git a/espanso-clipboard/src/lib.rs b/espanso-clipboard/src/lib.rs index 99af568..9e361df 100644 --- a/espanso-clipboard/src/lib.rs +++ b/espanso-clipboard/src/lib.rs @@ -30,14 +30,12 @@ mod win32; mod x11; //#[cfg(target_os = "linux")] -//mod evdev; +//#[cfg(feature = "wayland")] +//mod wayland; #[cfg(target_os = "macos")] mod mac; -#[macro_use] -extern crate lazy_static; - pub trait Clipboard { fn get_text(&self) -> Option; fn set_text(&self, text: &str) -> Result<()>; @@ -69,7 +67,7 @@ pub fn get_injector(_options: InjectorCreationOptions) -> Result Result> { +pub fn get_clipboard(_: ClipboardOptions) -> Result> { info!("using X11NativeClipboard"); Ok(Box::new(x11::native::X11NativeClipboard::new()?)) } diff --git a/espanso-clipboard/src/x11/mod.rs b/espanso-clipboard/src/x11/mod.rs new file mode 100644 index 0000000..6ce93e8 --- /dev/null +++ b/espanso-clipboard/src/x11/mod.rs @@ -0,0 +1,20 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso 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. + * + * espanso 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 espanso. If not, see . + */ + +pub mod native; \ No newline at end of file diff --git a/espanso-clipboard/src/x11/native/README.md b/espanso-clipboard/src/x11/native/README.md new file mode 100644 index 0000000..af2b759 --- /dev/null +++ b/espanso-clipboard/src/x11/native/README.md @@ -0,0 +1,4 @@ +The X11NativeClipboard modules uses the wonderful [clip](https://github.com/dacap/clip) library +by David Capello to manipulate the clipboard. + +At the time of writing, the library is MIT licensed. \ No newline at end of file diff --git a/espanso-clipboard/src/x11/native/clip/LICENSE.txt b/espanso-clipboard/src/x11/native/clip/LICENSE.txt new file mode 100644 index 0000000..8b436bd --- /dev/null +++ b/espanso-clipboard/src/x11/native/clip/LICENSE.txt @@ -0,0 +1,20 @@ +Copyright (c) 2015-2020 David Capello + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/espanso-clipboard/src/x11/native/clip/clip.cpp b/espanso-clipboard/src/x11/native/clip/clip.cpp new file mode 100644 index 0000000..bd221fc --- /dev/null +++ b/espanso-clipboard/src/x11/native/clip/clip.cpp @@ -0,0 +1,174 @@ +// Clip Library +// Copyright (c) 2015-2018 David Capello +// +// This file is released under the terms of the MIT license. +// Read LICENSE.txt for more information. + +#include "clip.h" +#include "clip_lock_impl.h" + +#include +#include + +namespace clip { + +namespace { + +void default_error_handler(ErrorCode code) { + static const char* err[] = { + "Cannot lock clipboard", + "Image format is not supported" + }; + throw std::runtime_error(err[static_cast(code)]); +} + +} // anonymous namespace + +error_handler g_error_handler = default_error_handler; + +lock::lock(void* native_window_handle) + : p(new impl(native_window_handle)) { +} + +lock::~lock() = default; + +bool lock::locked() const { + return p->locked(); +} + +bool lock::clear() { + return p->clear(); +} + +bool lock::is_convertible(format f) const { + return p->is_convertible(f); +} + +bool lock::set_data(format f, const char* buf, size_t length) { + return p->set_data(f, buf, length); +} + +bool lock::get_data(format f, char* buf, size_t len) const { + return p->get_data(f, buf, len); +} + +size_t lock::get_data_length(format f) const { + return p->get_data_length(f); +} + +bool lock::set_image(const image& img) { + return p->set_image(img); +} + +bool lock::get_image(image& img) const { + return p->get_image(img); +} + +bool lock::get_image_spec(image_spec& spec) const { + return p->get_image_spec(spec); +} + +format empty_format() { return 0; } +format text_format() { return 1; } +format image_format() { return 2; } + +bool has(format f) { + lock l; + if (l.locked()) + return l.is_convertible(f); + else + return false; +} + +bool clear() { + lock l; + if (l.locked()) + return l.clear(); + else + return false; +} + +bool set_text(const std::string& value) { + lock l; + if (l.locked()) { + l.clear(); + return l.set_data(text_format(), value.c_str(), value.size()); + } + else + return false; +} + +bool get_text(std::string& value) { + lock l; + if (!l.locked()) + return false; + + format f = text_format(); + if (!l.is_convertible(f)) + return false; + + size_t len = l.get_data_length(f); + if (len > 0) { + std::vector buf(len); + l.get_data(f, &buf[0], len); + value = &buf[0]; + return true; + } + else { + value.clear(); + return true; + } +} + +bool set_image(const image& img) { + lock l; + if (l.locked()) { + l.clear(); + return l.set_image(img); + } + else + return false; +} + +bool get_image(image& img) { + lock l; + if (!l.locked()) + return false; + + format f = image_format(); + if (!l.is_convertible(f)) + return false; + + return l.get_image(img); +} + +bool get_image_spec(image_spec& spec) { + lock l; + if (!l.locked()) + return false; + + format f = image_format(); + if (!l.is_convertible(f)) + return false; + + return l.get_image_spec(spec); +} + +void set_error_handler(error_handler handler) { + g_error_handler = handler; +} + +error_handler get_error_handler() { + return g_error_handler; +} + +#ifdef HAVE_XCB_XLIB_H +static int g_x11_timeout = 1000; +void set_x11_wait_timeout(int msecs) { g_x11_timeout = msecs; } +int get_x11_wait_timeout() { return g_x11_timeout; } +#else +void set_x11_wait_timeout(int) { } +int get_x11_wait_timeout() { return 1000; } +#endif + +} // namespace clip diff --git a/espanso-clipboard/src/x11/native/clip/clip.h b/espanso-clipboard/src/x11/native/clip/clip.h new file mode 100644 index 0000000..b74f275 --- /dev/null +++ b/espanso-clipboard/src/x11/native/clip/clip.h @@ -0,0 +1,178 @@ +// Clip Library +// Copyright (c) 2015-2018 David Capello +// +// This file is released under the terms of the MIT license. +// Read LICENSE.txt for more information. + +#ifndef CLIP_H_INCLUDED +#define CLIP_H_INCLUDED +#pragma once + +#include +#include +#include + +namespace clip { + + // ====================================================================== + // Low-level API to lock the clipboard/pasteboard and modify it + // ====================================================================== + + // Clipboard format identifier. + typedef size_t format; + + class image; + struct image_spec; + + class lock { + public: + // You can give your current HWND as the "native_window_handle." + // Windows clipboard functions use this handle to open/close + // (lock/unlock) the clipboard. From the MSDN documentation we + // need this handler so SetClipboardData() doesn't fail after a + // EmptyClipboard() call. Anyway it looks to work just fine if we + // call OpenClipboard() with a null HWND. + lock(void* native_window_handle = nullptr); + ~lock(); + + // Returns true if we've locked the clipboard successfully in + // lock() constructor. + bool locked() const; + + // Clears the clipboard content. If you don't clear the content, + // previous clipboard content (in unknown formats) could persist + // after the unlock. + bool clear(); + + // Returns true if the clipboard can be converted to the given + // format. + bool is_convertible(format f) const; + bool set_data(format f, const char* buf, size_t len); + bool get_data(format f, char* buf, size_t len) const; + size_t get_data_length(format f) const; + + // For images + bool set_image(const image& image); + bool get_image(image& image) const; + bool get_image_spec(image_spec& spec) const; + + private: + class impl; + std::unique_ptr p; + }; + + format register_format(const std::string& name); + + // This format is when the clipboard has no content. + format empty_format(); + + // When the clipboard has UTF8 text. + format text_format(); + + // When the clipboard has an image. + format image_format(); + + // Returns true if the clipboard has content of the given type. + bool has(format f); + + // Clears the clipboard content. + bool clear(); + + // ====================================================================== + // Error handling + // ====================================================================== + + enum class ErrorCode { + CannotLock, + ImageNotSupported, + }; + + typedef void (*error_handler)(ErrorCode code); + + void set_error_handler(error_handler f); + error_handler get_error_handler(); + + // ====================================================================== + // Text + // ====================================================================== + + // High-level API to put/get UTF8 text in/from the clipboard. These + // functions returns false in case of error. + bool set_text(const std::string& value); + bool get_text(std::string& value); + + // ====================================================================== + // Image + // ====================================================================== + + struct image_spec { + unsigned long width = 0; + unsigned long height = 0; + unsigned long bits_per_pixel = 0; + unsigned long bytes_per_row = 0; + unsigned long red_mask = 0; + unsigned long green_mask = 0; + unsigned long blue_mask = 0; + unsigned long alpha_mask = 0; + unsigned long red_shift = 0; + unsigned long green_shift = 0; + unsigned long blue_shift = 0; + unsigned long alpha_shift = 0; + }; + + // The image data must contain straight RGB values + // (non-premultiplied by alpha). The image retrieved from the + // clipboard will be non-premultiplied too. Basically you will be + // always dealing with straight alpha images. + // + // Details: Windows expects premultiplied images on its clipboard + // content, so the library code make the proper conversion + // automatically. macOS handles straight alpha directly, so there is + // no conversion at all. Linux/X11 images are transferred in + // image/png format which are specified in straight alpha. + class image { + public: + image(); + image(const image_spec& spec); + image(const void* data, const image_spec& spec); + image(const image& image); + image(image&& image); + ~image(); + + image& operator=(const image& image); + image& operator=(image&& image); + + char* data() const { return m_data; } + const image_spec& spec() const { return m_spec; } + + bool is_valid() const { return m_data != nullptr; } + void reset(); + + private: + void copy_image(const image& image); + void move_image(image&& image); + + bool m_own_data; + char* m_data; + image_spec m_spec; + }; + + // High-level API to set/get an image in/from the clipboard. These + // functions returns false in case of error. + bool set_image(const image& img); + bool get_image(image& img); + bool get_image_spec(image_spec& spec); + + // ====================================================================== + // Platform-specific + // ====================================================================== + + // Only for X11: Sets the time (in milliseconds) that we must wait + // for the selection/clipboard owner to receive the content. This + // value is 1000 (one second) by default. + void set_x11_wait_timeout(int msecs); + int get_x11_wait_timeout(); + +} // namespace clip + +#endif // CLIP_H_INCLUDED diff --git a/espanso-clipboard/src/x11/native/clip/clip_common.h b/espanso-clipboard/src/x11/native/clip/clip_common.h new file mode 100644 index 0000000..ebe0d72 --- /dev/null +++ b/espanso-clipboard/src/x11/native/clip/clip_common.h @@ -0,0 +1,76 @@ +// Clip Library +// Copyright (C) 2020 David Capello +// +// This file is released under the terms of the MIT license. +// Read LICENSE.txt for more information. + +#ifndef CLIP_COMMON_H_INCLUDED +#define CLIP_COMMON_H_INCLUDED +#pragma once + +namespace clip { +namespace details { + +inline void divide_rgb_by_alpha(image& img, + bool hasAlphaGreaterThanZero = false) { + const image_spec& spec = img.spec(); + + bool hasValidPremultipliedAlpha = true; + + for (unsigned long y=0; y> spec.red_shift ); + const int g = ((c & spec.green_mask) >> spec.green_shift); + const int b = ((c & spec.blue_mask ) >> spec.blue_shift ); + const int a = ((c & spec.alpha_mask) >> spec.alpha_shift); + + if (a > 0) + hasAlphaGreaterThanZero = true; + if (r > a || g > a || b > a) + hasValidPremultipliedAlpha = false; + } + } + + for (unsigned long y=0; y> spec.red_shift ); + int g = ((c & spec.green_mask) >> spec.green_shift); + int b = ((c & spec.blue_mask ) >> spec.blue_shift ); + int a = ((c & spec.alpha_mask) >> spec.alpha_shift); + + // If all alpha values = 0, we make the image opaque. + if (!hasAlphaGreaterThanZero) { + a = 255; + + // We cannot change the image spec (e.g. spec.alpha_mask=0) to + // make the image opaque, because the "spec" of the image is + // read-only. The image spec used by the client is the one + // returned by get_image_spec(). + } + // If there is alpha information and it's pre-multiplied alpha + else if (hasValidPremultipliedAlpha) { + if (a > 0) { + // Convert it to straight alpha + r = r * 255 / a; + g = g * 255 / a; + b = b * 255 / a; + } + } + + *dst = + (r << spec.red_shift ) | + (g << spec.green_shift) | + (b << spec.blue_shift ) | + (a << spec.alpha_shift); + } + } +} + +} // namespace details +} // namespace clip + +#endif // CLIP_H_INCLUDED diff --git a/espanso-clipboard/src/x11/native/clip/clip_lock_impl.h b/espanso-clipboard/src/x11/native/clip/clip_lock_impl.h new file mode 100644 index 0000000..3f08af7 --- /dev/null +++ b/espanso-clipboard/src/x11/native/clip/clip_lock_impl.h @@ -0,0 +1,33 @@ +// Clip Library +// Copyright (c) 2015-2018 David Capello +// +// This file is released under the terms of the MIT license. +// Read LICENSE.txt for more information. + +#ifndef CLIP_LOCK_IMPL_H_INCLUDED +#define CLIP_LOCK_IMPL_H_INCLUDED + +namespace clip { + +class lock::impl { +public: + impl(void* native_window_handle); + ~impl(); + + bool locked() const { return m_locked; } + bool clear(); + bool is_convertible(format f) const; + bool set_data(format f, const char* buf, size_t len); + bool get_data(format f, char* buf, size_t len) const; + size_t get_data_length(format f) const; + bool set_image(const image& image); + bool get_image(image& image) const; + bool get_image_spec(image_spec& spec) const; + +private: + bool m_locked; +}; + +} // namespace clip + +#endif diff --git a/espanso-clipboard/src/x11/native/clip/clip_x11.cpp b/espanso-clipboard/src/x11/native/clip/clip_x11.cpp new file mode 100644 index 0000000..bfcba15 --- /dev/null +++ b/espanso-clipboard/src/x11/native/clip/clip_x11.cpp @@ -0,0 +1,1091 @@ +// Clip Library +// Copyright (c) 2018-2019 David Capello +// +// This file is released under the terms of the MIT license. +// Read LICENSE.txt for more information. + +#include "clip.h" +#include "clip_lock_impl.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_PNG_H + #include "clip_x11_png.h" +#endif + +#define CLIP_SUPPORT_SAVE_TARGETS 1 + +namespace clip { + +namespace { + +enum CommonAtom { + ATOM, + INCR, + TARGETS, + CLIPBOARD, +#ifdef HAVE_PNG_H + MIME_IMAGE_PNG, +#endif +#ifdef CLIP_SUPPORT_SAVE_TARGETS + ATOM_PAIR, + SAVE_TARGETS, + MULTIPLE, + CLIPBOARD_MANAGER, +#endif +}; + +const char* kCommonAtomNames[] = { + "ATOM", + "INCR", + "TARGETS", + "CLIPBOARD", +#ifdef HAVE_PNG_H + "image/png", +#endif +#ifdef CLIP_SUPPORT_SAVE_TARGETS + "ATOM_PAIR", + "SAVE_TARGETS", + "MULTIPLE", + "CLIPBOARD_MANAGER", +#endif +}; + +const int kBaseForCustomFormats = 100; + +class Manager { +public: + typedef std::shared_ptr> buffer_ptr; + typedef std::vector atoms; + typedef std::function notify_callback; + + Manager() + : m_lock(m_mutex, std::defer_lock) + , m_connection(xcb_connect(nullptr, nullptr)) + , m_window(0) + , m_incr_process(false) { + if (!m_connection) + return; + + const xcb_setup_t* setup = xcb_get_setup(m_connection); + if (!setup) + return; + + xcb_screen_t* screen = xcb_setup_roots_iterator(setup).data; + if (!screen) + return; + + uint32_t event_mask = + // Just in case that some program reports SelectionNotify events + // with XCB_EVENT_MASK_PROPERTY_CHANGE mask. + XCB_EVENT_MASK_PROPERTY_CHANGE | + // To receive DestroyNotify event and stop the message loop. + XCB_EVENT_MASK_STRUCTURE_NOTIFY; + + m_window = xcb_generate_id(m_connection); + xcb_create_window(m_connection, 0, + m_window, + screen->root, + 0, 0, 1, 1, 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, + screen->root_visual, + XCB_CW_EVENT_MASK, + &event_mask); + + m_thread = std::thread( + [this]{ + process_x11_events(); + }); + } + + ~Manager() { +#ifdef CLIP_SUPPORT_SAVE_TARGETS + if (!m_data.empty() && + m_window && + m_window == get_x11_selection_owner()) { + // Check if there is a CLIPBOARD_MANAGER running to save all + // targets before we exit. + xcb_window_t x11_clipboard_manager = 0; + xcb_get_selection_owner_cookie_t cookie = + xcb_get_selection_owner(m_connection, + get_atom(CLIPBOARD_MANAGER)); + + xcb_get_selection_owner_reply_t* reply = + xcb_get_selection_owner_reply(m_connection, cookie, nullptr); + if (reply) { + x11_clipboard_manager = reply->owner; + free(reply); + } + + if (x11_clipboard_manager) { + // Start the SAVE_TARGETS mechanism so the X11 + // CLIPBOARD_MANAGER will save our clipboard data + // from now on. + get_data_from_selection_owner( + { get_atom(SAVE_TARGETS) }, + [this]() -> bool { return true; }, + get_atom(CLIPBOARD_MANAGER)); + } + } +#endif + + if (m_window) { + xcb_destroy_window(m_connection, m_window); + xcb_flush(m_connection); + } + + if (m_thread.joinable()) + m_thread.join(); + + if (m_connection) + xcb_disconnect(m_connection); + } + + bool try_lock() { + bool res = m_lock.try_lock(); + if (!res) { + // TODO make this configurable (the same for Windows retries) + for (int i=0; i<5 && !res; ++i) { + res = m_lock.try_lock(); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + } + } + return res; + } + + void unlock() { + m_lock.unlock(); + } + + // Clear our data + void clear_data() { + m_data.clear(); + m_image.reset(); + } + + void clear() { + clear_data(); + + // Clear the clipboard data from the selection owner + const xcb_window_t owner = get_x11_selection_owner(); + if (m_window != owner) { + xcb_selection_clear_event_t event; + event.response_type = XCB_SELECTION_CLEAR; + event.pad0 = 0; + event.sequence = 0; + event.time = XCB_CURRENT_TIME; + event.owner = owner; + event.selection = get_atom(CLIPBOARD); + + xcb_send_event(m_connection, false, + owner, + XCB_EVENT_MASK_NO_EVENT, + (const char*)&event); + + xcb_flush(m_connection); + } + } + + bool is_convertible(format f) const { + const atoms atoms = get_format_atoms(f); + const xcb_window_t owner = get_x11_selection_owner(); + + // If we are the owner, we just can check the m_data map + if (owner == m_window) { + for (xcb_atom_t atom : atoms) { + auto it = m_data.find(atom); + if (it != m_data.end()) + return true; + } + } + // Ask to the selection owner the available formats/atoms/targets. + else if (owner) { + return + get_data_from_selection_owner( + { get_atom(TARGETS) }, + [this, &atoms]() -> bool { + assert(m_reply_data); + if (!m_reply_data) + return false; + + const xcb_atom_t* sel_atoms = (const xcb_atom_t*)&(*m_reply_data)[0]; + int sel_natoms = m_reply_data->size() / sizeof(xcb_atom_t); + auto atoms_begin = atoms.begin(); + auto atoms_end = atoms.end(); + for (int i=0; i>(len); + std::copy(buf, + buf+len, + shared_data_buf->begin()); + for (xcb_atom_t atom : atoms) + m_data[atom] = shared_data_buf; + + return true; + } + + bool get_data(format f, char* buf, size_t len) const { + const atoms atoms = get_format_atoms(f); + const xcb_window_t owner = get_x11_selection_owner(); + if (owner == m_window) { + for (xcb_atom_t atom : atoms) { + auto it = m_data.find(atom); + if (it != m_data.end()) { + size_t n = std::min(len, it->second->size()); + std::copy(it->second->begin(), + it->second->begin()+n, + buf); + + if (f == text_format()) { + // Add an extra null char + if (n < len) + buf[n] = 0; + } + + return true; + } + } + } + else if (owner) { + if (get_data_from_selection_owner( + atoms, + [this, buf, len, f]() -> bool { + size_t n = std::min(len, m_reply_data->size()); + std::copy(m_reply_data->begin(), + m_reply_data->begin()+n, + buf); + + if (f == text_format()) { + if (n < len) + buf[n] = 0; // Include a null character + } + + return true; + })) { + return true; + } + } + return false; + } + + size_t get_data_length(format f) const { + size_t len = 0; + const atoms atoms = get_format_atoms(f); + const xcb_window_t owner = get_x11_selection_owner(); + if (owner == m_window) { + for (xcb_atom_t atom : atoms) { + auto it = m_data.find(atom); + if (it != m_data.end()) { + len = it->second->size(); + break; + } + } + } + else if (owner) { + if (!get_data_from_selection_owner( + atoms, + [this, &len]() -> bool { + len = m_reply_data->size(); + return true; + })) { + // Error getting data length + return 0; + } + } + if (f == text_format() && len > 0) { + ++len; // Add an extra byte for the null char + } + return len; + } + + bool set_image(const image& image) { + if (!set_x11_selection_owner()) + return false; + + m_image = image; + +#ifdef HAVE_PNG_H + // Put a nullptr in the m_data for image/png format and then we'll + // encode the png data when the image is requested in this format. + m_data[get_atom(MIME_IMAGE_PNG)] = buffer_ptr(); +#endif + + return true; + } + + bool get_image(image& output_img) const { + const xcb_window_t owner = get_x11_selection_owner(); + if (owner == m_window) { + if (m_image.is_valid()) { + output_img = m_image; + return true; + } + } +#ifdef HAVE_PNG_H + else if (owner && + get_data_from_selection_owner( + { get_atom(MIME_IMAGE_PNG) }, + [this, &output_img]() -> bool { + return x11::read_png(&(*m_reply_data)[0], + m_reply_data->size(), + &output_img, nullptr); + })) { + return true; + } +#endif + return false; + } + + bool get_image_spec(image_spec& spec) const { + const xcb_window_t owner = get_x11_selection_owner(); + if (owner == m_window) { + if (m_image.is_valid()) { + spec = m_image.spec(); + return true; + } + } +#ifdef HAVE_PNG_H + else if (owner && + get_data_from_selection_owner( + { get_atom(MIME_IMAGE_PNG) }, + [this, &spec]() -> bool { + return x11::read_png(&(*m_reply_data)[0], + m_reply_data->size(), + nullptr, &spec); + })) { + return true; + } +#endif + return false; + } + + format register_format(const std::string& name) { + xcb_atom_t atom = get_atom(name.c_str()); + m_custom_formats.push_back(atom); + return (format)(m_custom_formats.size()-1) + kBaseForCustomFormats; + } + +private: + + void process_x11_events() { + bool stop = false; + xcb_generic_event_t* event; + while (!stop && (event = xcb_wait_for_event(m_connection))) { + int type = (event->response_type & ~0x80); + + switch (type) { + + case XCB_DESTROY_NOTIFY: + // To stop the message loop we can just destroy the window + stop = true; + break; + + // Someone else has new content in the clipboard, so is + // notifying us that we should delete our data now. + case XCB_SELECTION_CLEAR: + handle_selection_clear_event( + (xcb_selection_clear_event_t*)event); + break; + + // Someone is requesting the clipboard content from us. + case XCB_SELECTION_REQUEST: + handle_selection_request_event( + (xcb_selection_request_event_t*)event); + break; + + // We've requested the clipboard content and this is the + // answer. + case XCB_SELECTION_NOTIFY: + handle_selection_notify_event( + (xcb_selection_notify_event_t*)event); + break; + + case XCB_PROPERTY_NOTIFY: + handle_property_notify_event( + (xcb_property_notify_event_t*)event); + break; + + } + + free(event); + } + } + + void handle_selection_clear_event(xcb_selection_clear_event_t* event) { + if (event->selection == get_atom(CLIPBOARD)) { + std::lock_guard lock(m_mutex); + clear_data(); // Clear our clipboard data + } + } + + void handle_selection_request_event(xcb_selection_request_event_t* event) { + std::lock_guard lock(m_mutex); + + if (event->target == get_atom(TARGETS)) { + atoms targets; + targets.push_back(get_atom(TARGETS)); +#ifdef CLIP_SUPPORT_SAVE_TARGETS + targets.push_back(get_atom(SAVE_TARGETS)); + targets.push_back(get_atom(MULTIPLE)); +#endif + for (const auto& it : m_data) + targets.push_back(it.first); + + // Set the "property" of "requestor" with the clipboard + // formats ("targets", atoms) that we provide. + xcb_change_property( + m_connection, + XCB_PROP_MODE_REPLACE, + event->requestor, + event->property, + get_atom(ATOM), + 8*sizeof(xcb_atom_t), + targets.size(), + &targets[0]); + } +#ifdef CLIP_SUPPORT_SAVE_TARGETS + else if (event->target == get_atom(SAVE_TARGETS)) { + // Do nothing + } + else if (event->target == get_atom(MULTIPLE)) { + xcb_get_property_reply_t* reply = + get_and_delete_property(event->requestor, + event->property, + get_atom(ATOM_PAIR), + false); + if (reply) { + for (xcb_atom_t + *ptr=(xcb_atom_t*)xcb_get_property_value(reply), + *end=ptr + (xcb_get_property_value_length(reply)/sizeof(xcb_atom_t)); + ptrrequestor, + property, + target)) { + xcb_change_property( + m_connection, + XCB_PROP_MODE_REPLACE, + event->requestor, + event->property, + XCB_ATOM_NONE, 0, 0, nullptr); + } + } + + free(reply); + } + } +#endif // CLIP_SUPPORT_SAVE_TARGETS + else { + if (!set_requestor_property_with_clipboard_content( + event->requestor, + event->property, + event->target)) { + return; + } + } + + // Notify the "requestor" that we've already updated the property. + xcb_selection_notify_event_t notify; + notify.response_type = XCB_SELECTION_NOTIFY; + notify.pad0 = 0; + notify.sequence = 0; + notify.time = event->time; + notify.requestor = event->requestor; + notify.selection = event->selection; + notify.target = event->target; + notify.property = event->property; + + xcb_send_event(m_connection, false, + event->requestor, + XCB_EVENT_MASK_NO_EVENT, // SelectionNotify events go without mask + (const char*)¬ify); + + xcb_flush(m_connection); + } + + bool set_requestor_property_with_clipboard_content(const xcb_atom_t requestor, + const xcb_atom_t property, + const xcb_atom_t target) { + auto it = m_data.find(target); + if (it == m_data.end()) { + // Nothing to do (unsupported target) + return false; + } + + // This can be null of the data was set from an image but we + // didn't encode the image yet (e.g. to image/png format). + if (!it->second) { + encode_data_on_demand(*it); + + // Return nothing, the given "target" cannot be constructed + // (maybe by some encoding error). + if (!it->second) + return false; + } + + // Set the "property" of "requestor" with the + // clipboard content in the requested format ("target"). + xcb_change_property( + m_connection, + XCB_PROP_MODE_REPLACE, + requestor, + property, + target, + 8, + it->second->size(), + &(*it->second)[0]); + return true; + } + + void handle_selection_notify_event(xcb_selection_notify_event_t* event) { + assert(event->requestor == m_window); + + if (event->target == get_atom(TARGETS)) + m_target_atom = get_atom(ATOM); + else + m_target_atom = event->target; + + xcb_get_property_reply_t* reply = + get_and_delete_property(event->requestor, + event->property, + m_target_atom); + if (reply) { + // In this case, We're going to receive the clipboard content in + // chunks of data with several PropertyNotify events. + if (reply->type == get_atom(INCR)) { + free(reply); + + reply = get_and_delete_property(event->requestor, + event->property, + get_atom(INCR)); + if (reply) { + if (xcb_get_property_value_length(reply) == 4) { + uint32_t n = *(uint32_t*)xcb_get_property_value(reply); + m_reply_data = std::make_shared>(n); + m_reply_offset = 0; + m_incr_process = true; + m_incr_received = true; + } + free(reply); + } + } + else { + // Simple case, the whole clipboard content in just one reply + // (without the INCR method). + m_reply_data.reset(); + m_reply_offset = 0; + copy_reply_data(reply); + + call_callback(reply); + + free(reply); + } + } + } + + void handle_property_notify_event(xcb_property_notify_event_t* event) { + if (m_incr_process && + event->state == XCB_PROPERTY_NEW_VALUE && + event->atom == get_atom(CLIPBOARD)) { + xcb_get_property_reply_t* reply = + get_and_delete_property(event->window, + event->atom, + m_target_atom); + if (reply) { + m_incr_received = true; + + // When the length is 0 it means that the content was + // completely sent by the selection owner. + if (xcb_get_property_value_length(reply) > 0) { + copy_reply_data(reply); + } + else { + // Now that m_reply_data has the complete clipboard content, + // we can call the m_callback. + call_callback(reply); + m_incr_process = false; + } + free(reply); + } + } + } + + xcb_get_property_reply_t* get_and_delete_property(xcb_window_t window, + xcb_atom_t property, + xcb_atom_t atom, + bool delete_prop = true) { + xcb_get_property_cookie_t cookie = + xcb_get_property(m_connection, + delete_prop, + window, + property, + atom, + 0, 0x1fffffff); // 0x1fffffff = INT32_MAX / 4 + + xcb_generic_error_t* err = nullptr; + xcb_get_property_reply_t* reply = + xcb_get_property_reply(m_connection, cookie, &err); + if (err) { + // TODO report error + free(err); + } + return reply; + } + + // Concatenates the new data received in "reply" into "m_reply_data" + // buffer. + void copy_reply_data(xcb_get_property_reply_t* reply) { + const uint8_t* src = (const uint8_t*)xcb_get_property_value(reply); + // n = length of "src" in bytes + size_t n = xcb_get_property_value_length(reply); + + size_t req = m_reply_offset+n; + if (!m_reply_data) { + m_reply_data = std::make_shared>(req); + } + // The "m_reply_data" size can be smaller because the size + // specified in INCR property is just a lower bound. + else if (req > m_reply_data->size()) { + m_reply_data->resize(req); + } + + std::copy(src, src+n, m_reply_data->begin()+m_reply_offset); + m_reply_offset += n; + } + + // Calls the current m_callback() to handle the clipboard content + // received from the owner. + void call_callback(xcb_get_property_reply_t* reply) { + m_callback_result = false; + if (m_callback) + m_callback_result = m_callback(); + + m_cv.notify_one(); + + m_reply_data.reset(); + } + + bool get_data_from_selection_owner(const atoms& atoms, + const notify_callback&& callback, + xcb_atom_t selection = 0) const { + if (!selection) + selection = get_atom(CLIPBOARD); + + // Put the callback on "m_callback" so we can call it on + // SelectionNotify event. + m_callback = std::move(callback); + + // Clear data if we are not the selection owner. + if (m_window != get_x11_selection_owner()) + m_data.clear(); + + // Ask to the selection owner for its content on each known + // text format/atom. + for (xcb_atom_t atom : atoms) { + xcb_convert_selection(m_connection, + m_window, // Send us the result + selection, // Clipboard selection + atom, // The clipboard format that we're requesting + get_atom(CLIPBOARD), // Leave result in this window's property + XCB_CURRENT_TIME); + + xcb_flush(m_connection); + + // We use the "m_incr_received" to wait several timeouts in case + // that we've received the INCR SelectionNotify or + // PropertyNotify events. + do { + m_incr_received = false; + + // Wait a response for 100 milliseconds + std::cv_status status = + m_cv.wait_for(m_lock, + std::chrono::milliseconds(get_x11_wait_timeout())); + if (status == std::cv_status::no_timeout) { + // If the condition variable was notified, it means that the + // callback was called correctly. + return m_callback_result; + } + } while (m_incr_received); + } + + // Reset callback + m_callback = notify_callback(); + return false; + } + + atoms get_atoms(const char** names, + const int n) const { + atoms result(n, 0); + std::vector cookies(n); + + for (int i=0; isecond; + else + cookies[i] = xcb_intern_atom( + m_connection, 0, + std::strlen(names[i]), names[i]); + } + + for (int i=0; iatom; + free(reply); + } + } + } + + return result; + } + + xcb_atom_t get_atom(const char* name) const { + auto it = m_atoms.find(name); + if (it != m_atoms.end()) + return it->second; + + xcb_atom_t result = 0; + xcb_intern_atom_cookie_t cookie = + xcb_intern_atom(m_connection, 0, + std::strlen(name), name); + + xcb_intern_atom_reply_t* reply = + xcb_intern_atom_reply(m_connection, + cookie, + nullptr); + if (reply) { + result = m_atoms[name] = reply->atom; + free(reply); + } + return result; + } + + xcb_atom_t get_atom(CommonAtom i) const { + if (m_common_atoms.empty()) { + m_common_atoms = + get_atoms(kCommonAtomNames, + sizeof(kCommonAtomNames) / sizeof(kCommonAtomNames[0])); + } + return m_common_atoms[i]; + } + + const atoms& get_text_format_atoms() const { + if (m_text_atoms.empty()) { + const char* names[] = { + // Prefer utf-8 formats first + "UTF8_STRING", + "text/plain;charset=utf-8", + "text/plain;charset=UTF-8", + // ANSI C strings? + "STRING", + "TEXT", + "text/plain", + }; + m_text_atoms = get_atoms(names, sizeof(names) / sizeof(names[0])); + } + return m_text_atoms; + } + + const atoms& get_image_format_atoms() const { + if (m_image_atoms.empty()) { +#ifdef HAVE_PNG_H + m_image_atoms.push_back(get_atom(MIME_IMAGE_PNG)); +#endif + } + return m_image_atoms; + } + + atoms get_format_atoms(const format f) const { + atoms atoms; + if (f == text_format()) { + atoms = get_text_format_atoms(); + } + else if (f == image_format()) { + atoms = get_image_format_atoms(); + } + else { + xcb_atom_t atom = get_format_atom(f); + if (atom) + atoms.push_back(atom); + } + return atoms; + } + +#if !defined(NDEBUG) + // This can be used to print debugging messages. + std::string get_atom_name(xcb_atom_t atom) const { + std::string result; + xcb_get_atom_name_cookie_t cookie = + xcb_get_atom_name(m_connection, atom); + xcb_generic_error_t* err = nullptr; + xcb_get_atom_name_reply_t* reply = + xcb_get_atom_name_reply(m_connection, cookie, &err); + if (err) { + free(err); + } + if (reply) { + int len = xcb_get_atom_name_name_length(reply); + if (len > 0) { + result.resize(len); + char* name = xcb_get_atom_name_name(reply); + if (name) + std::copy(name, name+len, result.begin()); + } + free(reply); + } + return result; + } +#endif + + bool set_x11_selection_owner() const { + xcb_void_cookie_t cookie = + xcb_set_selection_owner_checked(m_connection, + m_window, + get_atom(CLIPBOARD), + XCB_CURRENT_TIME); + xcb_generic_error_t* err = + xcb_request_check(m_connection, + cookie); + if (err) { + free(err); + return false; + } + return true; + } + + xcb_window_t get_x11_selection_owner() const { + xcb_window_t result = 0; + xcb_get_selection_owner_cookie_t cookie = + xcb_get_selection_owner(m_connection, + get_atom(CLIPBOARD)); + + xcb_get_selection_owner_reply_t* reply = + xcb_get_selection_owner_reply(m_connection, cookie, nullptr); + if (reply) { + result = reply->owner; + free(reply); + } + return result; + } + + xcb_atom_t get_format_atom(const format f) const { + int i = f - kBaseForCustomFormats; + if (i >= 0 && i < int(m_custom_formats.size())) + return m_custom_formats[i]; + else + return 0; + } + + void encode_data_on_demand(std::pair& e) { +#ifdef HAVE_PNG_H + if (e.first == get_atom(MIME_IMAGE_PNG)) { + assert(m_image.is_valid()); + if (!m_image.is_valid()) + return; + + std::vector output; + if (x11::write_png(m_image, output)) { + e.second = + std::make_shared>( + std::move(output)); + } + // else { TODO report png conversion errors } + } +#endif + } + + // Access to the whole Manager + std::mutex m_mutex; + + // Lock used in the main thread using the Manager (i.e. by lock::impl) + mutable std::unique_lock m_lock; + + // Connection to X11 server + xcb_connection_t* m_connection; + + // Temporal background window used to own the clipboard and process + // all events related about the clipboard in a background thread + xcb_window_t m_window; + + // Used to wait/notify the arrival of the SelectionNotify event when + // we requested the clipboard content from other selection owner. + mutable std::condition_variable m_cv; + + // Thread used to run a background message loop to wait X11 events + // about clipboard. The X11 selection owner will be a hidden window + // created by us just for the clipboard purpose/communication. + std::thread m_thread; + + // Internal callback used when a SelectionNotify is received (or the + // whole data content is received by the INCR method). So this + // callback can use the notification by different purposes (e.g. get + // the data length only, or get/process the data content, etc.). + mutable notify_callback m_callback; + + // Result returned by the m_callback. Used as return value in the + // get_data_from_selection_owner() function. For example, if the + // callback must read a "image/png" file from the clipboard data and + // fails, the callback can return false and finally the get_image() + // will return false (i.e. there is data, but it's not a valid image + // format). + bool m_callback_result; + + // Cache of known atoms + mutable std::map m_atoms; + + // Cache of common used atoms by us + mutable atoms m_common_atoms; + + // Cache of atoms related to text or image content + mutable atoms m_text_atoms; + mutable atoms m_image_atoms; + + // Actual clipboard data generated by us (when we "copy" content in + // the clipboard, it means that we own the X11 "CLIPBOARD" + // selection, and in case of SelectionRequest events, we've to + // return the data stored in this "m_data" field) + mutable std::map m_data; + + // Copied image in the clipboard. As we have to transfer the image + // in some specific format (e.g. image/png) we want to keep a copy + // of the image and make the conversion when the clipboard data is + // requested by other process. + mutable image m_image; + + // True if we have received an INCR notification so we're going to + // process several PropertyNotify to concatenate all data chunks. + bool m_incr_process; + + // Variable used to wait more time if we've received an INCR + // notification, which means that we're going to receive large + // amounts of data from the selection owner. + mutable bool m_incr_received; + + // Target/selection format used in the SelectionNotify. Used in the + // INCR method to get data from the same property in the same format + // (target) on each PropertyNotify. + xcb_atom_t m_target_atom; + + // Each time we receive data from the selection owner, we put that + // data in this buffer. If we get the data with the INCR method, + // we'll concatenate chunks of data in this buffer to complete the + // whole clipboard content. + buffer_ptr m_reply_data; + + // Used to concatenate chunks of data in "m_reply_data" from several + // PropertyNotify when we are getting the selection owner data with + // the INCR method. + size_t m_reply_offset; + + // List of user-defined formats/atoms. + std::vector m_custom_formats; +}; + +Manager* manager = nullptr; + +void delete_manager_atexit() { + if (manager) { + delete manager; + manager = nullptr; + } +} + +Manager* get_manager() { + if (!manager) { + manager = new Manager; + std::atexit(delete_manager_atexit); + } + return manager; +} + +} // anonymous namespace + +lock::impl::impl(void*) : m_locked(false) { + m_locked = get_manager()->try_lock(); +} + +lock::impl::~impl() { + if (m_locked) + manager->unlock(); +} + +bool lock::impl::clear() { + manager->clear(); + return true; +} + +bool lock::impl::is_convertible(format f) const { + return manager->is_convertible(f); +} + +bool lock::impl::set_data(format f, const char* buf, size_t len) { + return manager->set_data(f, buf, len); +} + +bool lock::impl::get_data(format f, char* buf, size_t len) const { + return manager->get_data(f, buf, len); +} + +size_t lock::impl::get_data_length(format f) const { + return manager->get_data_length(f); +} + +bool lock::impl::set_image(const image& image) { + return manager->set_image(image); +} + +bool lock::impl::get_image(image& output_img) const { + return manager->get_image(output_img); +} + +bool lock::impl::get_image_spec(image_spec& spec) const { + return manager->get_image_spec(spec); +} + +format register_format(const std::string& name) { + return get_manager()->register_format(name); +} + +} // namespace clip diff --git a/espanso-clipboard/src/x11/native/clip/clip_x11_png.h b/espanso-clipboard/src/x11/native/clip/clip_x11_png.h new file mode 100644 index 0000000..22c30a6 --- /dev/null +++ b/espanso-clipboard/src/x11/native/clip/clip_x11_png.h @@ -0,0 +1,226 @@ +// Clip Library +// Copyright (c) 2018 David Capello +// +// This file is released under the terms of the MIT license. +// Read LICENSE.txt for more information. + +#include "clip.h" + +#include +#include + +#include "png.h" + +namespace clip { +namespace x11 { + +////////////////////////////////////////////////////////////////////// +// Functions to convert clip::image into png data to store it in the +// clipboard. + +void write_data_fn(png_structp png, png_bytep buf, png_size_t len) { + std::vector& output = *(std::vector*)png_get_io_ptr(png); + const size_t i = output.size(); + output.resize(i+len); + std::copy(buf, buf+len, output.begin()+i); +} + +bool write_png(const image& image, + std::vector& output) { + png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, + nullptr, nullptr, nullptr); + if (!png) + return false; + + png_infop info = png_create_info_struct(png); + if (!info) { + png_destroy_write_struct(&png, nullptr); + return false; + } + + if (setjmp(png_jmpbuf(png))) { + png_destroy_write_struct(&png, &info); + return false; + } + + png_set_write_fn(png, + (png_voidp)&output, + write_data_fn, + nullptr); // No need for a flush function + + const image_spec& spec = image.spec(); + int color_type = (spec.alpha_mask ? + PNG_COLOR_TYPE_RGB_ALPHA: + PNG_COLOR_TYPE_RGB); + + png_set_IHDR(png, info, + spec.width, spec.height, 8, color_type, + PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE); + png_write_info(png, info); + png_set_packing(png); + + png_bytep row = + (png_bytep)png_malloc(png, png_get_rowbytes(png, info)); + + for (png_uint_32 y=0; y> spec.red_shift; + *(dst++) = (c & spec.green_mask) >> spec.green_shift; + *(dst++) = (c & spec.blue_mask ) >> spec.blue_shift; + if (color_type == PNG_COLOR_TYPE_RGB_ALPHA) + *(dst++) = (c & spec.alpha_mask) >> spec.alpha_shift; + } + + png_write_rows(png, &row, 1); + } + + png_free(png, row); + png_write_end(png, info); + png_destroy_write_struct(&png, &info); + return true; +} + +////////////////////////////////////////////////////////////////////// +// Functions to convert png data stored in the clipboard to a +// clip::image. + +struct read_png_io { + const uint8_t* buf; + size_t len; + size_t pos; +}; + +void read_data_fn(png_structp png, png_bytep buf, png_size_t len) { + read_png_io& io = *(read_png_io*)png_get_io_ptr(png); + if (io.pos < io.len) { + size_t n = std::min(len, io.len-io.pos); + if (n > 0) { + std::copy(io.buf+io.pos, + io.buf+io.pos+n, + buf); + io.pos += n; + } + } +} + +bool read_png(const uint8_t* buf, + const size_t len, + image* output_image, + image_spec* output_spec) { + png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, + nullptr, nullptr, nullptr); + if (!png) + return false; + + png_infop info = png_create_info_struct(png); + if (!info) { + png_destroy_read_struct(&png, nullptr, nullptr); + return false; + } + + if (setjmp(png_jmpbuf(png))) { + png_destroy_read_struct(&png, &info, nullptr); + return false; + } + + read_png_io io = { buf, len, 0 }; + png_set_read_fn(png, (png_voidp)&io, read_data_fn); + + png_read_info(png, info); + + png_uint_32 width, height; + int bit_depth, color_type, interlace_type; + png_get_IHDR(png, info, &width, &height, + &bit_depth, &color_type, + &interlace_type, + nullptr, nullptr); + + image_spec spec; + spec.width = width; + spec.height = height; + spec.bits_per_pixel = 32; + spec.bytes_per_row = png_get_rowbytes(png, info); + + spec.red_mask = 0x000000ff; + spec.green_mask = 0x0000ff00; + spec.blue_mask = 0x00ff0000; + spec.red_shift = 0; + spec.green_shift = 8; + spec.blue_shift = 16; + + // TODO indexed images with alpha + if (color_type == PNG_COLOR_TYPE_RGB_ALPHA || + color_type == PNG_COLOR_TYPE_GRAY_ALPHA) { + spec.alpha_mask = 0xff000000; + spec.alpha_shift = 24; + } + else { + spec.alpha_mask = 0; + spec.alpha_shift = 0; + } + + if (output_spec) + *output_spec = spec; + + if (output_image && + width > 0 && + height > 0) { + image img(spec); + + // We want RGB 24-bit or RGBA 32-bit as a result + png_set_strip_16(png); // Down to 8-bit (TODO we might support 16-bit values) + png_set_packing(png); // Use one byte if color depth < 8-bit + png_set_expand_gray_1_2_4_to_8(png); + png_set_palette_to_rgb(png); + png_set_gray_to_rgb(png); + png_set_tRNS_to_alpha(png); + + int number_passes = png_set_interlace_handling(png); + png_read_update_info(png, info); + + png_bytepp rows = (png_bytepp)png_malloc(png, sizeof(png_bytep)*height); + png_uint_32 y; + for (y=0; y. + */ + +use std::os::raw::c_char; + +#[link(name = "espansoclipboardx11", kind = "static")] +extern "C" { + pub fn clipboard_x11_get_text(buffer: *mut c_char, buffer_size: i32) -> i32; + pub fn clipboard_x11_set_text(text: *const c_char) -> i32; + pub fn clipboard_x11_set_html(html: *const c_char, fallback_text: *const c_char) -> i32; + pub fn clipboard_x11_set_image(buffer: *const u8, buffer_size: i32) -> i32; +} diff --git a/espanso-clipboard/src/x11/native/mod.rs b/espanso-clipboard/src/x11/native/mod.rs new file mode 100644 index 0000000..6535c07 --- /dev/null +++ b/espanso-clipboard/src/x11/native/mod.rs @@ -0,0 +1,106 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso 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. + * + * espanso 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 espanso. If not, see . + */ + +use std::{ffi::{CStr, CString}, io::Read, path::PathBuf}; + +use crate::Clipboard; +use anyhow::Result; +use libc::c_char; +use thiserror::Error; + +mod ffi; + +pub struct X11NativeClipboard {} + +impl X11NativeClipboard { + pub fn new() -> Result { + Ok(Self {}) + } +} + +impl Clipboard for X11NativeClipboard { + fn get_text(&self) -> Option { + let mut buffer: [c_char; 2048] = [0; 2048]; + let native_result = + unsafe { ffi::clipboard_x11_get_text(buffer.as_mut_ptr(), (buffer.len() - 1) as i32) }; + if native_result > 0 { + let string = unsafe { CStr::from_ptr(buffer.as_ptr()) }; + Some(string.to_string_lossy().to_string()) + } else { + None + } + } + + fn set_text(&self, text: &str) -> anyhow::Result<()> { + let string = CString::new(text)?; + let native_result = unsafe { ffi::clipboard_x11_set_text(string.as_ptr()) }; + if native_result > 0 { + Ok(()) + } else { + Err(X11NativeClipboardError::SetOperationFailed().into()) + } + } + + fn set_image(&self, image_path: &std::path::Path) -> anyhow::Result<()> { + if !image_path.exists() || !image_path.is_file() { + return Err(X11NativeClipboardError::ImageNotFound(image_path.to_path_buf()).into()); + } + + // Load the image data + let mut file = std::fs::File::open(image_path)?; + let mut data = Vec::new(); + file.read_to_end(&mut data)?; + + let native_result = unsafe { + ffi::clipboard_x11_set_image(data.as_ptr(), data.len() as i32) + }; + + if native_result > 0 { + Ok(()) + } else { + Err(X11NativeClipboardError::SetOperationFailed().into()) + } + } + + fn set_html(&self, html: &str, fallback_text: Option<&str>) -> anyhow::Result<()> { + let html_string = CString::new(html)?; + let fallback_string = CString::new(fallback_text.unwrap_or_default())?; + let fallback_ptr = if fallback_text.is_some() { + fallback_string.as_ptr() + } else { + std::ptr::null() + }; + + let native_result = unsafe { ffi::clipboard_x11_set_html(html_string.as_ptr(), fallback_ptr) }; + if native_result > 0 { + Ok(()) + } else { + Err(X11NativeClipboardError::SetOperationFailed().into()) + } + } +} + +#[derive(Error, Debug)] +pub enum X11NativeClipboardError { + #[error("clipboard set operation failed")] + SetOperationFailed(), + + #[error("image not found: `{0}`")] + ImageNotFound(PathBuf), +} diff --git a/espanso-clipboard/src/x11/native/native.cpp b/espanso-clipboard/src/x11/native/native.cpp new file mode 100644 index 0000000..44c2c23 --- /dev/null +++ b/espanso-clipboard/src/x11/native/native.cpp @@ -0,0 +1,76 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso 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. + * + * espanso 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 espanso. If not, see . + */ + +#include "native.h" +#include "clip/clip.h" +#include "string.h" +#include + +clip::format html_format = clip::register_format("text/html"); +clip::format png_format = clip::register_format("image/png"); + +int32_t clipboard_x11_get_text(char * buffer, int32_t buffer_size) { + std::string value; + if (!clip::get_text(value)) { + return 0; + } + + if (value.length() == 0) { + return 0; + } + + strncpy(buffer, value.c_str(), buffer_size - 1); + return 1; +} + +int32_t clipboard_x11_set_text(char * text) { + if (!clip::set_text(text)) { + return 0; + } else { + return 1; + } +} + +int32_t clipboard_x11_set_html(char * html, char * fallback_text) { + clip::lock l; + if (!l.clear()) { + return 0; + } + if (!l.set_data(html_format, html, strlen(html))) { + return 0; + } + if (fallback_text) { + // Best effort to set the fallback + l.set_data(clip::text_format(), fallback_text, strlen(fallback_text)); + } + return 1; +} + +int32_t clipboard_x11_set_image(char * buffer, int32_t size) { + clip::lock l; + if (!l.clear()) { + return 0; + } + + if (!l.set_data(png_format, buffer, size)) { + return 0; + } + + return 1; +} \ No newline at end of file diff --git a/espanso-clipboard/src/x11/native/native.h b/espanso-clipboard/src/x11/native/native.h new file mode 100644 index 0000000..204a161 --- /dev/null +++ b/espanso-clipboard/src/x11/native/native.h @@ -0,0 +1,31 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso 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. + * + * espanso 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 espanso. If not, see . + */ + +#ifndef ESPANSO_X11_CLIPBOARD_H +#define ESPANSO_X11_CLIPBOARD_H + +#include + +extern "C" int32_t clipboard_x11_get_text(char * buffer, int32_t buffer_size); + +extern "C" int32_t clipboard_x11_set_text(char * text); +extern "C" int32_t clipboard_x11_set_html(char * html, char * fallback_text); +extern "C" int32_t clipboard_x11_set_image(char * buffer, int32_t buffer_size); + +#endif //ESPANSO_X11_CLIPBOARD_H \ No newline at end of file diff --git a/espanso/Cargo.toml b/espanso/Cargo.toml index 8f26761..2df4be9 100644 --- a/espanso/Cargo.toml +++ b/espanso/Cargo.toml @@ -11,7 +11,7 @@ edition = "2018" [features] # If the wayland feature is enabled, all X11 dependencies will be dropped # and only EVDEV-based methods will be supported. -wayland = ["espanso-detect/wayland", "espanso-inject/wayland"] +wayland = ["espanso-detect/wayland", "espanso-inject/wayland", "espanso-clipboard/wayland"] [dependencies] espanso-detect = { path = "../espanso-detect" }