From d9496899ed780b1e648a7ca51f824fa8d03d46bb Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Wed, 17 Mar 2021 10:27:00 +0100 Subject: [PATCH] feat(clipboard): add windows clipboard implementation --- espanso-clipboard/build.rs | 2 +- espanso-clipboard/src/lib.rs | 6 +- espanso-clipboard/src/win32/ffi.rs | 28 ++++ espanso-clipboard/src/win32/mod.rs | 145 +++++++++++++++++++ espanso-clipboard/src/win32/native.cpp | 185 +++++++++++++++++++++++++ espanso-clipboard/src/win32/native.h | 30 ++++ espanso/src/main.rs | 6 +- 7 files changed, 395 insertions(+), 7 deletions(-) create mode 100644 espanso-clipboard/src/win32/ffi.rs create mode 100644 espanso-clipboard/src/win32/mod.rs create mode 100644 espanso-clipboard/src/win32/native.cpp create mode 100644 espanso-clipboard/src/win32/native.h diff --git a/espanso-clipboard/build.rs b/espanso-clipboard/build.rs index cf3c379..325e9ac 100644 --- a/espanso-clipboard/build.rs +++ b/espanso-clipboard/build.rs @@ -55,7 +55,7 @@ fn cc_config() { println!("cargo:rustc-link-lib=static=espansoclipboardx11"); println!("cargo:rustc-link-lib=dylib=xcb"); } else { - // TODO: wayland + // Nothing to compile on wayland } } diff --git a/espanso-clipboard/src/lib.rs b/espanso-clipboard/src/lib.rs index 7ad301c..c60cb69 100644 --- a/espanso-clipboard/src/lib.rs +++ b/espanso-clipboard/src/lib.rs @@ -60,9 +60,9 @@ impl Default for ClipboardOptions { } #[cfg(target_os = "windows")] -pub fn get_injector(_options: InjectorCreationOptions) -> Result> { - info!("using Win32Injector"); - Ok(Box::new(win32::Win32Injector::new())) +pub fn get_clipboard(_: ClipboardOptions) -> Result> { + info!("using Win32Clipboard"); + Ok(Box::new(win32::Win32Clipboard::new()?)) } #[cfg(target_os = "macos")] diff --git a/espanso-clipboard/src/win32/ffi.rs b/espanso-clipboard/src/win32/ffi.rs new file mode 100644 index 0000000..601d8a8 --- /dev/null +++ b/espanso-clipboard/src/win32/ffi.rs @@ -0,0 +1,28 @@ +/* + * 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::os::raw::c_char; + +#[link(name = "espansoclipboard", kind = "static")] +extern "C" { + pub fn clipboard_get_text(buffer: *mut u16, buffer_size: i32) -> i32; + pub fn clipboard_set_text(text: *const u16) -> i32; + pub fn clipboard_set_image(image_path: *const u16) -> i32; + pub fn clipboard_set_html(html_descriptor: *const c_char, fallback_text: *const u16) -> i32; +} diff --git a/espanso-clipboard/src/win32/mod.rs b/espanso-clipboard/src/win32/mod.rs new file mode 100644 index 0000000..98831cb --- /dev/null +++ b/espanso-clipboard/src/win32/mod.rs @@ -0,0 +1,145 @@ +/* + * 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 . + */ + +mod ffi; + +use std::{ffi::CString, path::PathBuf}; + +use crate::Clipboard; +use anyhow::Result; +use log::error; +use thiserror::Error; +use widestring::{U16CStr, U16CString}; + +pub struct Win32Clipboard {} + +impl Win32Clipboard { + pub fn new() -> Result { + Ok(Self {}) + } +} + +impl Clipboard for Win32Clipboard { + fn get_text(&self) -> Option { + let mut buffer: [u16; 2048] = [0; 2048]; + let native_result = + unsafe { ffi::clipboard_get_text(buffer.as_mut_ptr(), (buffer.len() - 1) as i32) }; + if native_result > 0 { + let string = unsafe { U16CStr::from_ptr_str(buffer.as_ptr()) }; + Some(string.to_string_lossy()) + } else { + None + } + } + + fn set_text(&self, text: &str) -> anyhow::Result<()> { + let string = U16CString::from_str(text)?; + let native_result = unsafe { ffi::clipboard_set_text(string.as_ptr()) }; + if native_result > 0 { + Ok(()) + } else { + Err(Win32ClipboardError::SetOperationFailed().into()) + } + } + + fn set_image(&self, image_path: &std::path::Path) -> anyhow::Result<()> { + if !image_path.exists() || !image_path.is_file() { + return Err(Win32ClipboardError::ImageNotFound(image_path.to_path_buf()).into()); + } + + let path = U16CString::from_os_str(image_path.as_os_str())?; + let native_result = unsafe { ffi::clipboard_set_image(path.as_ptr()) }; + + if native_result > 0 { + Ok(()) + } else { + Err(Win32ClipboardError::SetOperationFailed().into()) + } + } + + fn set_html(&self, html: &str, fallback_text: Option<&str>) -> anyhow::Result<()> { + let html_descriptor = generate_html_descriptor(html); + let html_string = CString::new(html_descriptor)?; + let fallback_string = U16CString::from_str(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_set_html(html_string.as_ptr(), fallback_ptr) }; + if native_result > 0 { + Ok(()) + } else { + Err(Win32ClipboardError::SetOperationFailed().into()) + } + } +} + +fn generate_html_descriptor(html: &str) -> String { + // In order to set the HTML clipboard, we have to create a prefix with a specific format + // For more information, look here: + // https://docs.microsoft.com/en-us/windows/win32/dataxchg/html-clipboard-format + // https://docs.microsoft.com/en-za/troubleshoot/cpp/add-html-code-clipboard + let mut tokens = Vec::new(); + tokens.push("Version:0.9"); + tokens.push("StartHTML:<"); + tokens.push("EndHTML:<"); + tokens.push("StartFragment:<"); + tokens.push("EndFragment:<"); + tokens.push(""); + tokens.push(""); + let content = format!("{}", html); + tokens.push(&content); + tokens.push(""); + tokens.push(""); + + let mut render = tokens.join("\r\n"); + + // Now replace the placeholders with the actual positions + render = render.replace( + "<", + &format!("{:0>8}", render.find("").unwrap_or_default()), + ); + render = render.replace("<", &format!("{:0>8}", render.len())); + render = render.replace( + "<", + &format!( + "{:0>8}", + render.find("").unwrap_or_default() + "".len() + ), + ); + render = render.replace( + "<", + &format!( + "{:0>8}", + render.find("").unwrap_or_default() + ), + ); + render +} + +#[derive(Error, Debug)] +pub enum Win32ClipboardError { + #[error("clipboard set operation failed")] + SetOperationFailed(), + + #[error("image not found: `{0}`")] + ImageNotFound(PathBuf), +} diff --git a/espanso-clipboard/src/win32/native.cpp b/espanso-clipboard/src/win32/native.cpp new file mode 100644 index 0000000..1727eaf --- /dev/null +++ b/espanso-clipboard/src/win32/native.cpp @@ -0,0 +1,185 @@ +/* + * 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 +#include +#include +#include +#include +#include + +#define UNICODE + +#ifdef __MINGW32__ +#ifndef WINVER +#define WINVER 0x0606 +#endif +#define STRSAFE_NO_DEPRECATE +#endif + +#include +#include +#include + +#pragma comment(lib, "gdiplus.lib") +#include + +#include + +int32_t clipboard_get_text(wchar_t *buffer, int32_t buffer_size) +{ + int32_t result = 0; + + if (OpenClipboard(NULL)) + { + HANDLE hData; + if (hData = GetClipboardData(CF_UNICODETEXT)) + { + HGLOBAL hMem; + if (hMem = GlobalLock(hData)) + { + GlobalUnlock(hMem); + wcsncpy(buffer, (wchar_t *)hMem, buffer_size); + if (wcsnlen_s(buffer, buffer_size) > 0) + { + result = 1; + } + } + } + + CloseClipboard(); + } + + return result; +} + +int32_t clipboard_set_text(wchar_t *text) +{ + int32_t result = 0; + const size_t len = wcslen(text) + 1; + + if (OpenClipboard(NULL)) + { + EmptyClipboard(); + + HGLOBAL hMem; + if (hMem = GlobalAlloc(GMEM_MOVEABLE, len * sizeof(wchar_t))) + { + memcpy(GlobalLock(hMem), text, len * sizeof(wchar_t)); + GlobalUnlock(hMem); + + if (SetClipboardData(CF_UNICODETEXT, hMem)) + { + result = 1; + } + } + + CloseClipboard(); + } + + return result; +} + +int32_t clipboard_set_image(wchar_t *path) +{ + int32_t result = 0; + + Gdiplus::GdiplusStartupInput gdiplusStartupInput; + ULONG_PTR gdiplusToken; + Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL); + + Gdiplus::Bitmap *gdibmp = Gdiplus::Bitmap::FromFile(path); + if (gdibmp) + { + HBITMAP hbitmap; + gdibmp->GetHBITMAP(0, &hbitmap); + if (OpenClipboard(NULL)) + { + EmptyClipboard(); + DIBSECTION ds; + if (GetObject(hbitmap, sizeof(DIBSECTION), &ds)) + { + HDC hdc = GetDC(HWND_DESKTOP); + //create compatible bitmap (get DDB from DIB) + HBITMAP hbitmap_ddb = CreateDIBitmap(hdc, &ds.dsBmih, CBM_INIT, + ds.dsBm.bmBits, (BITMAPINFO *)&ds.dsBmih, DIB_RGB_COLORS); + ReleaseDC(HWND_DESKTOP, hdc); + SetClipboardData(CF_BITMAP, hbitmap_ddb); + DeleteObject(hbitmap_ddb); + result = 1; + } + CloseClipboard(); + } + + DeleteObject(hbitmap); + delete gdibmp; + } + + Gdiplus::GdiplusShutdown(gdiplusToken); + + return result; +} + +// Inspired by https://docs.microsoft.com/en-za/troubleshoot/cpp/add-html-code-clipboard +int32_t clipboard_set_html(char * html_descriptor, wchar_t * fallback_text) { + // Get clipboard id for HTML format + static int cfid = 0; + if(!cfid) { + cfid = RegisterClipboardFormat(L"HTML Format"); + } + + int32_t result = 0; + + const size_t html_len = strlen(html_descriptor) + 1; + const size_t fallback_len = (fallback_text != nullptr) ? wcslen(fallback_text) + 1 : 0; + + if (OpenClipboard(NULL)) + { + EmptyClipboard(); + + // First copy the HTML + HGLOBAL hMem; + if (hMem = GlobalAlloc(GMEM_MOVEABLE, html_len * sizeof(char))) + { + memcpy(GlobalLock(hMem), html_descriptor, html_len * sizeof(char)); + GlobalUnlock(hMem); + + if (SetClipboardData(cfid, hMem)) + { + result = 1; + } + } + + // Then try to set the fallback text, if present. + if (fallback_len > 0) { + if (hMem = GlobalAlloc(GMEM_MOVEABLE, fallback_len * sizeof(wchar_t))) + { + memcpy(GlobalLock(hMem), fallback_text, fallback_len * sizeof(wchar_t)); + GlobalUnlock(hMem); + + SetClipboardData(CF_UNICODETEXT, hMem); + } + } + + CloseClipboard(); + } + + return result; +} \ No newline at end of file diff --git a/espanso-clipboard/src/win32/native.h b/espanso-clipboard/src/win32/native.h new file mode 100644 index 0000000..ce49cea --- /dev/null +++ b/espanso-clipboard/src/win32/native.h @@ -0,0 +1,30 @@ +/* + * 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_CLIPBOARD_H +#define ESPANSO_CLIPBOARD_H + +#include + +extern "C" int32_t clipboard_get_text(wchar_t * buffer, int32_t buffer_size); +extern "C" int32_t clipboard_set_text(wchar_t * text); +extern "C" int32_t clipboard_set_image(wchar_t * image); +extern "C" int32_t clipboard_set_html(char * html_descriptor, wchar_t * fallback_text); + +#endif //ESPANSO_CLIPBOARD_H \ No newline at end of file diff --git a/espanso/src/main.rs b/espanso/src/main.rs index cef5d47..6bf296c 100644 --- a/espanso/src/main.rs +++ b/espanso/src/main.rs @@ -101,9 +101,9 @@ fn main() { if hotkey.hotkey_id == 2 { println!("clip {:?}", clipboard.get_text()); } else if hotkey.hotkey_id == 1 { - clipboard.set_text("test text").unwrap(); - //clipboard.set_html("test text", Some("test text fallback")).unwrap(); - //clipboard.set_image(&PathBuf::from("/home/freddy/insync/Development/Espanso/Images/icongreen.png")).unwrap(); + //clipboard.set_text("test text").unwrap(); + clipboard.set_html("test text", Some("test text fallback")).unwrap(); + //clipboard.set_image(&PathBuf::from(r"C:\Users\Freddy\Insync\Development\Espanso\Images\icongreen.png")).unwrap(); } } }