fix(share): copy links through QClipboard, not arboard (#684)

Copy link failed on GNOME + Flatpak: arboard's Wayland path needs the
wlr/ext-data-control protocol, which Mutter does not implement, so it fell
back to X11 — reachable natively through XWayland, unreachable inside the
sandbox ("clipboard unavailable: X11 server connection timed out"). KWin
has the protocol, which is why the failure never reproduced on Plasma.

Replace it with a C++ shim (cxx/clipboard.cpp) that queues the write onto
the GUI thread and hands it to QClipboard: the base wl_data_device protocol
every compositor implements, no extra sandbox permission, and the offer
lives as long as the application (the #514 lifetime rule, without the
long-lived arboard instance). arboard is dropped from the crate.

Verified on Plasma/Wayland and on an Ubuntu GNOME/Wayland VM with DISPLAY
unset (the sandbox condition), where the copy now lands.

cc
This commit is contained in:
vicrodh committed 2026-09-11 13:53:27 -06:00
1 parent 8b1941d9ad
commit 6b81cfa84d
5 files changed
+66 -313

No files matched your search

+6 -251
View File
@@ -154,27 +154,6 @@ dependencies = [
"num-traits",
]
[[package]]
name = "arboard"
version = "3.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf"
dependencies = [
"clipboard-win",
"image",
"log",
"objc2 0.6.4",
"objc2-app-kit 0.3.2",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-foundation 0.3.2",
"parking_lot",
"percent-encoding",
"windows-sys 0.60.2",
"wl-clipboard-rs",
"x11rb",
]
[[package]]
name = "arrayvec"
version = "0.7.8"
@@ -678,15 +657,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "clipboard-win"
version = "5.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4"
dependencies = [
"error-code",
]
[[package]]
name = "cmake"
version = "0.1.58"
@@ -1079,7 +1049,7 @@ dependencies = [
"cxxbridge-cmd",
"cxxbridge-flags",
"cxxbridge-macro",
"foldhash 0.2.0",
"foldhash",
"link-cplusplus",
]
@@ -1487,12 +1457,6 @@ dependencies = [
"litrs",
]
[[package]]
name = "downcast-rs"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
[[package]]
name = "dst-decoder"
version = "0.1.2"
@@ -1601,12 +1565,6 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "error-code"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7"
[[package]]
name = "euclid"
version = "0.22.14"
@@ -1724,12 +1682,6 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "fixedbitset"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
[[package]]
name = "flacenc"
version = "0.5.1"
@@ -1785,12 +1737,6 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "foldhash"
version = "0.2.0"
@@ -1936,16 +1882,6 @@ dependencies = [
"version_check",
]
[[package]]
name = "gethostname"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8"
dependencies = [
"rustix 1.1.4",
"windows-link 0.2.1",
]
[[package]]
name = "getrandom"
version = "0.2.17"
@@ -2017,15 +1953,6 @@ dependencies = [
"byteorder",
]
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"foldhash 0.1.5",
]
[[package]]
name = "hashbrown"
version = "0.16.1"
@@ -2034,7 +1961,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash 0.2.0",
"foldhash",
]
[[package]]
@@ -2045,7 +1972,7 @@ checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash 0.2.0",
"foldhash",
]
[[package]]
@@ -3125,15 +3052,6 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "nom"
version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
dependencies = [
"memchr",
]
[[package]]
name = "notify"
version = "8.2.0"
@@ -3355,7 +3273,6 @@ dependencies = [
"bitflags 2.13.1",
"block2 0.6.2",
"objc2 0.6.4",
"objc2-core-graphics",
"objc2-foundation 0.3.2",
]
@@ -3434,19 +3351,6 @@ dependencies = [
"objc2 0.6.4",
]
[[package]]
name = "objc2-core-graphics"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
dependencies = [
"bitflags 2.13.1",
"dispatch2",
"objc2 0.6.4",
"objc2-core-foundation",
"objc2-io-surface",
]
[[package]]
name = "objc2-core-image"
version = "0.2.2"
@@ -3490,17 +3394,6 @@ dependencies = [
"objc2-core-foundation",
]
[[package]]
name = "objc2-io-surface"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
dependencies = [
"bitflags 2.13.1",
"objc2 0.6.4",
"objc2-core-foundation",
]
[[package]]
name = "objc2-metal"
version = "0.2.2"
@@ -3594,16 +3487,6 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "os_pipe"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
dependencies = [
"libc",
"windows-sys 0.61.2",
]
[[package]]
name = "palette"
version = "0.7.7"
@@ -3726,17 +3609,6 @@ dependencies = [
"pest",
]
[[package]]
name = "petgraph"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455"
dependencies = [
"fixedbitset 0.5.7",
"hashbrown 0.15.5",
"indexmap",
]
[[package]]
name = "phf"
version = "0.11.3"
@@ -4524,7 +4396,6 @@ dependencies = [
name = "qbz-qt"
version = "2.1.1"
dependencies = [
"arboard",
"async-io",
"async-trait",
"cc",
@@ -6161,7 +6032,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662"
dependencies = [
"fnv",
"nom 7.1.3",
"nom",
"phf",
"phf_codegen",
]
@@ -6187,7 +6058,7 @@ dependencies = [
"fancy-regex",
"filedescriptor",
"finl_unicode",
"fixedbitset 0.4.2",
"fixedbitset",
"hex",
"lazy_static",
"libc",
@@ -6577,17 +6448,6 @@ dependencies = [
"strength_reduce",
]
[[package]]
name = "tree_magic_mini"
version = "3.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6"
dependencies = [
"memchr",
"nom 8.0.0",
"petgraph",
]
[[package]]
name = "try-lock"
version = "0.2.5"
@@ -6757,7 +6617,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f25d498b63d1fdb376b4250f39ab3a5ee8d103957346abacd911e2d8b612c139"
dependencies = [
"itertools 0.13.0",
"nom 7.1.3",
"nom",
]
[[package]]
@@ -6884,76 +6744,6 @@ dependencies = [
"web-sys",
]
[[package]]
name = "wayland-backend"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078"
dependencies = [
"cc",
"downcast-rs",
"rustix 1.1.4",
"smallvec",
"wayland-sys",
]
[[package]]
name = "wayland-client"
version = "0.31.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073"
dependencies = [
"bitflags 2.13.1",
"rustix 1.1.4",
"wayland-backend",
"wayland-scanner",
]
[[package]]
name = "wayland-protocols"
version = "0.32.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6"
dependencies = [
"bitflags 2.13.1",
"wayland-backend",
"wayland-client",
"wayland-scanner",
]
[[package]]
name = "wayland-protocols-wlr"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234"
dependencies = [
"bitflags 2.13.1",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-scanner",
]
[[package]]
name = "wayland-scanner"
version = "0.31.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0"
dependencies = [
"proc-macro2",
"quick-xml",
"quote",
]
[[package]]
name = "wayland-sys"
version = "0.31.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be"
dependencies = [
"pkg-config",
]
[[package]]
name = "web-sys"
version = "0.3.104"
@@ -7657,47 +7447,12 @@ version = "0.57.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
[[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.20",
"tree_magic_mini",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-protocols-wlr",
]
[[package]]
name = "writeable"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
[[package]]
name = "x11rb"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414"
dependencies = [
"gethostname",
"rustix 1.1.4",
"x11rb-protocol",
]
[[package]]
name = "x11rb-protocol"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
[[package]]
name = "xdg-home"
version = "1.3.0"
+4 -11
View File
@@ -37,17 +37,10 @@ url = "2"
open = "5"
dirs = "5"
# Share links (src/share_qt.rs).
# Rust owns the clipboard on this side because the toast does: cxx-qt-lib 0.7
# ships no QClipboard binding, and QML cannot publish QbzShell.toastJson, so
# splitting the copy and its confirmation across the boundary would buy
# nothing.
#
# wayland-data-control: Wayland-native clipboard via the data-control
# protocols (GNOME/KDE/sway/niri/Hyprland), falling back to X11 when Wayland
# is absent (#514 — without it arboard is X11-only and every copy fails on
# Wayland-only sessions).
arboard = { version = "3", features = ["wayland-data-control"] }
# Share links (src/share_qt.rs) copy through Qt's own QClipboard
# (cxx/clipboard.cpp) since #684: arboard's Wayland path needs the
# wlr/ext-data-control protocol, which Mutter lacks, and its X11 fallback is
# unreachable inside the Flatpak sandbox.
qbz-core = { path = "../qbz-core" }
qbz-app = { path = "../qbz-app" }
+2
View File
@@ -401,6 +401,8 @@ fn build_rhi_items() {
cc.file("cxx/win_shell.cpp");
println!("cargo:rerun-if-changed=cxx/app_lifecycle.cpp");
cc.file("cxx/app_lifecycle.cpp");
println!("cargo:rerun-if-changed=cxx/clipboard.cpp");
cc.file("cxx/clipboard.cpp");
// Shell_NotifyIconW tray. No Q_OBJECT, so no moc; the body is inside
// `#ifdef _WIN32` and compiles to nothing on Linux and macOS.
println!("cargo:rerun-if-changed=cxx/win_tray.cpp");
+35
View File
@@ -0,0 +1,35 @@
// System clipboard through Qt (#684).
//
// The Rust side used arboard, whose Wayland path needs the wlr/ext-data-control
// protocol. KWin has it; Mutter (GNOME) does not, so arboard fell back to X11
// there — which works natively through XWayland and FAILS inside the Flatpak
// sandbox (no X11 socket): "clipboard unavailable: X11 server connection timed
// out". QClipboard speaks the base wl_data_device protocol every compositor
// implements, needs no extra sandbox permission, and the offer stays alive for
// as long as the application does (the #514 lifetime rule, for free).
//
// The clipboard is a GUI-thread object; the call is queued onto the
// application's thread so any caller (a cxx-qt invokable already on it, or a
// tokio worker) is fine.
#include <QtCore/QCoreApplication>
#include <QtCore/QMetaObject>
#include <QtCore/QString>
#include <QtGui/QClipboard>
#include <QtGui/QGuiApplication>
extern "C" void qbz_clipboard_set_text(const char *utf8, int len) {
QCoreApplication *core = QCoreApplication::instance();
if (!core || !qobject_cast<QGuiApplication *>(core)) {
return;
}
const QString text = QString::fromUtf8(utf8, len);
QMetaObject::invokeMethod(
core,
[text]() {
if (QClipboard *clipboard = QGuiApplication::clipboard()) {
clipboard->setText(text, QClipboard::Clipboard);
}
},
Qt::QueuedConnection);
}
+19 -51
View File
@@ -58,63 +58,31 @@ pub(crate) fn qobuz_label_url(label_id: &str) -> String {
format!("https://play.qobuz.com/label/{label_id}")
}
/// Long-lived clipboard instance. arboard ties the offer's lifetime to the
/// LAST live `Clipboard` object: dropping it destroys the X11 selection
/// window (contents survive only when a clipboard MANAGER accepts the
/// handoff — KDE ships one, stock GNOME/XFCE/Cinnamon do not) and ends the
/// Wayland offer with the same rule. The old create-per-copy pattern
/// therefore worked on KDE and silently lost the text everywhere else
/// (HiFi-wizard copy report, #514). One instance kept alive for the whole
/// process serves the offer like any normal app.
///
/// This is the #514 fix, verbatim from `share.rs:32-41` — it is NOT
/// boilerplate to be simplified into a local. A create-per-copy port would
/// pass every test run on KDE and lose the text everywhere else.
static CLIPBOARD: std::sync::OnceLock<std::sync::Mutex<Option<arboard::Clipboard>>> =
std::sync::OnceLock::new();
unsafe extern "C" {
fn qbz_clipboard_set_text(utf8: *const std::os::raw::c_char, len: i32);
}
/// Copy `text` to the system clipboard. Runs on a blocking thread —
/// clipboard backends (X11/Wayland) can block. `share.rs:43-70`.
/// Copy `text` to the system clipboard through Qt (`cxx/clipboard.cpp`).
///
/// The one adaptation vs the reference: in the Slint app the arm already runs
/// inside a tokio context, whereas a cxx-qt invokable runs on the **Qt event
/// loop thread**, where a bare `tokio::task::spawn_blocking` panics ("must be
/// called from the context of a Tokio runtime"). It therefore goes through
/// `crate::spawn` (main.rs) onto the process-global runtime first — the same
/// `spawn` + `spawn_blocking` sandwich `local_ephemeral.rs`, `browse_qt.rs`
/// and `ambient_qt.rs` already use.
/// #684: the arboard path before this needed `wlr/ext-data-control`, which
/// KWin has and Mutter does not; on GNOME it fell back to X11, which works
/// natively through XWayland and is unreachable inside the Flatpak sandbox —
/// every Copy link on GNOME + Flatpak logged "clipboard unavailable" and
/// copied nothing. QClipboard uses the base Wayland data-device protocol,
/// needs no sandbox permission, and QGuiApplication keeps the offer alive
/// for the life of the process (the #514 lifetime rule, without the
/// long-lived instance the arboard port had to carry).
///
/// Fire-and-forget by design: the caller never learns whether the copy
/// worked, which is why the toast at the call site is unconditional
/// (`main.rs:12758-12761` does the same).
/// (`main.rs:12758-12761` does the same). The C++ side queues the write onto
/// the GUI thread, so this is safe from a cxx-qt invokable or a worker.
pub(crate) fn copy_to_clipboard(text: String) {
crate::spawn(async move {
let _ = tokio::task::spawn_blocking(move || {
let cell = CLIPBOARD.get_or_init(|| std::sync::Mutex::new(None));
let mut guard = match cell.lock() {
Ok(g) => g,
Err(poisoned) => poisoned.into_inner(),
};
if guard.is_none() {
match arboard::Clipboard::new() {
Ok(c) => *guard = Some(c),
Err(e) => {
log::warn!("[qbz-qt] clipboard unavailable: {e}");
return;
}
}
}
if let Some(clipboard) = guard.as_mut() {
if let Err(e) = clipboard.set_text(text) {
log::warn!("[qbz-qt] clipboard set failed: {e}");
// Drop the instance so the next copy reconnects — the
// display connection may have gone away.
*guard = None;
}
}
})
.await;
});
let len = i32::try_from(text.len()).unwrap_or(i32::MAX);
// SAFETY: the pointer/length pair is read synchronously inside the call
// (the C++ side copies into a QString before queueing), so `text` may be
// dropped as soon as the call returns.
unsafe { qbz_clipboard_set_text(text.as_ptr().cast(), len) };
}
/// Artist header ⋯ → Share. `artist/ArtistPageView.slint:530-538` fires