Fixed clipboard operations on Linux

This commit is contained in:
Thorsten Sommer 2026-07-19 19:54:05 +02:00
parent e6293b50a3
commit 7fe357d44a
Signed by untrusted user who does not match committer: tsommer
GPG Key ID: 371BBA77A02C0108
3 changed files with 129 additions and 78 deletions

128
runtime/Cargo.lock generated
View File

@ -149,7 +149,7 @@ version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@ -160,7 +160,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@ -215,6 +215,7 @@ dependencies = [
"parking_lot",
"percent-encoding",
"windows-sys 0.60.2",
"wl-clipboard-rs",
"x11rb",
]
@ -242,27 +243,6 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "ashpd"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2f3f79755c74fd155000314eb349864caa787c6592eace6c6882dad873d9c39"
dependencies = [
"enumflags2",
"futures-channel",
"futures-util",
"rand 0.9.4",
"raw-window-handle",
"serde",
"serde_repr",
"tokio",
"url",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"zbus",
]
[[package]]
name = "ashpd"
version = "0.13.12"
@ -1885,7 +1865,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.61.2",
"windows-sys 0.59.0",
]
[[package]]
@ -1911,15 +1891,6 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "dlib"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
dependencies = [
"libloading 0.7.4",
]
[[package]]
name = "dlopen2"
version = "0.8.2"
@ -2226,7 +2197,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
"windows-sys 0.59.0",
]
[[package]]
@ -4165,7 +4136,7 @@ dependencies = [
"aes 0.9.1",
"apple-native-keyring-store",
"arboard",
"ashpd 0.13.12",
"ashpd",
"async-stream",
"axum",
"axum-server",
@ -4280,7 +4251,7 @@ dependencies = [
"png 0.18.1",
"serde",
"thiserror 2.0.18",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@ -5230,12 +5201,6 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "pollster"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
[[package]]
name = "portable-atomic"
version = "1.13.1"
@ -5897,18 +5862,18 @@ version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
dependencies = [
"ashpd 0.11.1",
"block2 0.6.2",
"dispatch2",
"glib-sys",
"gobject-sys",
"gtk-sys",
"js-sys",
"log",
"objc2 0.6.4",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-foundation 0.3.2",
"pollster",
"raw-window-handle",
"urlencoding",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
@ -6080,7 +6045,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.12.1",
"windows-sys 0.61.2",
"windows-sys 0.59.0",
]
[[package]]
@ -6139,7 +6104,7 @@ dependencies = [
"security-framework",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.61.2",
"windows-sys 0.59.0",
]
[[package]]
@ -6252,12 +6217,6 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "scopeguard"
version = "1.2.0"
@ -6748,7 +6707,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
dependencies = [
"libc",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@ -7675,10 +7634,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.4.2",
"getrandom 0.3.1",
"once_cell",
"rustix 1.1.4",
"windows-sys 0.61.2",
"windows-sys 0.59.0",
]
[[package]]
@ -8137,7 +8096,19 @@ dependencies = [
"png 0.18.1",
"serde",
"thiserror 2.0.18",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
name = "tree_magic_mini"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f943391d896cdfe8eec03a04d7110332d445be7df856db382dd96a730667562c"
dependencies = [
"memchr",
"nom 7.1.3",
"once_cell",
"petgraph",
]
[[package]]
@ -8178,7 +8149,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [
"memoffset",
"tempfile",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@ -8286,12 +8257,6 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "urlpattern"
version = "0.3.0"
@ -8639,7 +8604,6 @@ dependencies = [
"cc",
"downcast-rs",
"rustix 1.1.4",
"scoped-tls",
"smallvec",
"wayland-sys",
]
@ -8668,6 +8632,19 @@ dependencies = [
"wayland-scanner",
]
[[package]]
name = "wayland-protocols-wlr"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9597cdf02cf0c34cd5823786dce6b5ae8598f05c2daf5621b6e178d4f7345f3"
dependencies = [
"bitflags 2.11.1",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-scanner",
]
[[package]]
name = "wayland-scanner"
version = "0.31.8"
@ -8685,8 +8662,6 @@ version = "0.31.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e6dbfc3ac5ef974c92a2235805cc0114033018ae1290a72e474aa8b28cbbdfd"
dependencies = [
"dlib",
"log",
"pkg-config",
]
@ -9574,6 +9549,24 @@ dependencies = [
"wasmparser",
]
[[package]]
name = "wl-clipboard-rs"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9651471a32e87d96ef3a127715382b2d11cc7c8bb9822ded8a7cc94072eb0a3"
dependencies = [
"libc",
"log",
"os_pipe",
"rustix 1.1.4",
"thiserror 2.0.18",
"tree_magic_mini",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-protocols-wlr",
]
[[package]]
name = "write16"
version = "1.0.0"
@ -10054,7 +10047,6 @@ dependencies = [
"endi",
"enumflags2",
"serde",
"url",
"winnow 1.0.2",
"zvariant_derive",
"zvariant_utils",

View File

@ -12,13 +12,13 @@ tauri-build = { version = "2.6.3", features = [] }
tauri = { version = "2.11.5", features = [] }
tauri-plugin-window-state = { version = "2.4.1" }
tauri-plugin-shell = "2.3.5"
tauri-plugin-dialog = { version = "2.7.1", default-features = false, features = ["xdg-portal"] }
tauri-plugin-dialog = "2.7.1"
tauri-plugin-opener = "2.5.4"
tauri-plugin-single-instance = "2"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.150"
keyring-core = "1.0.0"
arboard = "3.6.1"
arboard = { version = "3.6.1", features = ["wayland-data-control"] }
tokio = { version = "1.52.3", features = ["rt", "rt-multi-thread", "macros", "process"] }
tokio-stream = { version = "0.1.18", features = ["sync"] }
futures = "0.3.32"

View File

@ -1,9 +1,9 @@
use std::fmt::Display;
use std::sync::Mutex;
use arboard::Clipboard;
use axum::Json;
use log::{debug, error, warn};
use once_cell::sync::Lazy;
use axum::Json;
use serde::Serialize;
use crate::api_token::APIToken;
use crate::encryption::{EncryptedText, ENCRYPTION};
@ -26,17 +26,32 @@ impl ClipboardBackend for Clipboard {
}
}
#[derive(Debug, PartialEq, Eq)]
enum ClipboardOperationError<E> {
Initialization(E),
Write(E),
}
impl<E: Display> Display for ClipboardOperationError<E> {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Initialization(error) => write!(formatter, "Failed to initialize the clipboard backend: {error}"),
Self::Write(error) => write!(formatter, "Failed to write to the clipboard: {error}"),
}
}
}
fn set_text_with_retry<B, F>(
clipboard: &mut Option<B>,
text: String,
mut create_clipboard: F,
) -> Result<(), B::Error>
) -> Result<(), ClipboardOperationError<B::Error>>
where
B: ClipboardBackend,
F: FnMut() -> Result<B, B::Error>,
{
if clipboard.is_none() {
*clipboard = Some(create_clipboard()?);
*clipboard = Some(create_clipboard().map_err(ClipboardOperationError::Initialization)?);
}
let first_result = clipboard.as_mut().unwrap().set_text(text.clone());
@ -44,10 +59,10 @@ where
warn!(Source = "Clipboard"; "Failed to set text using the current clipboard backend; reinitializing it once: {first_error}.");
*clipboard = None;
let mut retry_clipboard = create_clipboard()?;
let mut retry_clipboard = create_clipboard().map_err(ClipboardOperationError::Initialization)?;
if let Err(retry_error) = retry_clipboard.set_text(text) {
error!(Source = "Clipboard"; "Failed to set text after reinitializing the clipboard backend: {retry_error}.");
return Err(retry_error);
return Err(ClipboardOperationError::Write(retry_error));
}
*clipboard = Some(retry_clipboard);
@ -87,7 +102,7 @@ pub async fn set_clipboard(_token: APIToken, encrypted_text: String) -> Json<Set
},
Err(e) => {
error!(Source = "Clipboard"; "Failed to set text to the clipboard: {e}.");
error!(Source = "Clipboard"; "Clipboard operation failed: {e}.");
Json(SetClipboardResponse {
success: false,
issue: e.to_string(),
@ -116,7 +131,7 @@ mod tests {
use std::collections::VecDeque;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use super::{release_clipboard, set_text_with_retry, ClipboardBackend};
use super::{ClipboardOperationError, release_clipboard, set_text_with_retry, ClipboardBackend};
struct MockClipboard {
id: usize,
@ -186,6 +201,30 @@ mod tests {
assert!(clipboard.is_some());
}
#[test]
fn reports_initialization_failures_and_retries_on_the_next_request() {
let mut clipboard: Option<MockClipboard> = None;
let mut factory = MockFactory::new([false]);
let mut fail_initialization = true;
let error = set_text_with_retry(&mut clipboard, "first".to_string(), || {
if fail_initialization {
fail_initialization = false;
Err("initialization failed".to_string())
} else {
factory.create()
}
}).unwrap_err();
assert_eq!(error, ClipboardOperationError::Initialization("initialization failed".to_string()));
assert!(clipboard.is_none());
set_text_with_retry(&mut clipboard, "second".to_string(), || factory.create()).unwrap();
assert_eq!(factory.created, 1);
assert!(clipboard.is_some());
}
#[test]
fn reuses_the_same_instance_for_multiple_writes() {
let mut clipboard = None;
@ -210,6 +249,26 @@ mod tests {
assert_eq!(*factory.writes.lock().unwrap(), vec![(0, "text".to_string()), (1, "text".to_string())]);
}
#[test]
fn reports_reinitialization_failures_and_discards_the_failed_instance() {
let mut clipboard = None;
let mut factory = MockFactory::new([true]);
let mut initialization_attempts = 0;
let error = set_text_with_retry(&mut clipboard, "text".to_string(), || {
initialization_attempts += 1;
if initialization_attempts == 1 {
factory.create()
} else {
Err("reinitialization failed".to_string())
}
}).unwrap_err();
assert_eq!(error, ClipboardOperationError::Initialization("reinitialization failed".to_string()));
assert_eq!(initialization_attempts, 2);
assert!(clipboard.is_none());
}
#[test]
fn returns_the_retry_error_and_discards_the_failed_instance() {
let mut clipboard = None;
@ -217,7 +276,7 @@ mod tests {
let error = set_text_with_retry(&mut clipboard, "text".to_string(), || factory.create()).unwrap_err();
assert_eq!(error, "backend 1 failed");
assert_eq!(error, ClipboardOperationError::Write("backend 1 failed".to_string()));
assert_eq!(factory.created, 2);
assert!(clipboard.is_none());
}