| author | |
| committer | |
| log | 01c718333a8b2a5e8afe547db7ace2aef0215179 |
| tree | 1054a923fbfe06d3a0dd80aad4b258b092898302 |
| parent | cc25073ba74de4601e128acde1e294cc4bf1134a |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU |
The clipboard now lives on winit's own Wayland connection, on a queue the
event loop dispatches, instead of smithay-clipboard's thread: other apps'
paste requests wake the event loop, which answers them, now that no dialog
holds it up. Files, pages and pictures are read from the Wayland selection
itself rather than through a new X11 connection to Xwayland on every paste,
so they paste on compositors without Xwayland or that share the selection
with it only while an X11 window has focus. Copy now offers HTML and
Snowbound's own format on Wayland too, beside the text. X11 sessions keep
arboard.
The clipboard borrows winit's connection through a frame callback moved
onto winit's event queue. libwayland names a proxy's queue only from 1.23
(wl_proxy_get_queue), so it is read from struct wl_proxy, laid out alike
since 1.0.
smithay-clipboard goes, and with it the second smithay-client-toolkit and
calloop, calloop-wayland-source, wayland-protocols-experimental and
wayland-protocols-misc.
Assisted-by: claude-opus-5.56 files changed, 593 insertions(+), 200 deletions(-)
Cargo.lock+7-97| ... | @@ -601,43 +601,18 @@ dependencies = [ | ... | @@ -601,43 +601,18 @@ dependencies = [ |
| 601 | "thiserror 1.0.69", | 601 | "thiserror 1.0.69", |
| 602 | ] | 602 | ] |
| 603 | 603 | ||
| 604 | [[package]] | ||
| 605 | name = "calloop" | ||
| 606 | version = "0.14.4" | ||
| 607 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 608 | checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" | ||
| 609 | dependencies = [ | ||
| 610 | "bitflags 2.13.1", | ||
| 611 | "polling", | ||
| 612 | "rustix 1.1.4", | ||
| 613 | "slab", | ||
| 614 | "tracing", | ||
| 615 | ] | ||
| 616 | |||
| 617 | [[package]] | 604 | [[package]] |
| 618 | name = "calloop-wayland-source" | 605 | name = "calloop-wayland-source" |
| 619 | version = "0.3.0" | 606 | version = "0.3.0" |
| 620 | source = "registry+https://github.com/rust-lang/crates.io-index" | 607 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 621 | checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" | 608 | checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" |
| 622 | dependencies = [ | 609 | dependencies = [ |
| 623 | "calloop 0.13.0", | 610 | "calloop", |
| 624 | "rustix 0.38.44", | 611 | "rustix 0.38.44", |
| 625 | "wayland-backend", | 612 | "wayland-backend", |
| 626 | "wayland-client", | 613 | "wayland-client", |
| 627 | ] | 614 | ] |
| 628 | 615 | ||
| 629 | [[package]] | ||
| 630 | name = "calloop-wayland-source" | ||
| 631 | version = "0.4.1" | ||
| 632 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 633 | checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" | ||
| 634 | dependencies = [ | ||
| 635 | "calloop 0.14.4", | ||
| 636 | "rustix 1.1.4", | ||
| 637 | "wayland-backend", | ||
| 638 | "wayland-client", | ||
| 639 | ] | ||
| 640 | |||
| 641 | [[package]] | 616 | [[package]] |
| 642 | name = "canvas" | 617 | name = "canvas" |
| 643 | version = "0.1.0" | 618 | version = "0.1.0" |
| ... | @@ -3377,7 +3352,7 @@ dependencies = [ | ... | @@ -3377,7 +3352,7 @@ dependencies = [ |
| 3377 | "ab_glyph", | 3352 | "ab_glyph", |
| 3378 | "log", | 3353 | "log", |
| 3379 | "memmap2", | 3354 | "memmap2", |
| 3380 | "smithay-client-toolkit 0.19.2", | 3355 | "smithay-client-toolkit", |
| 3381 | "tiny-skia", | 3356 | "tiny-skia", |
| 3382 | ] | 3357 | ] |
| 3383 | 3358 | ||
| ... | @@ -3604,8 +3579,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | ... | @@ -3604,8 +3579,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" |
| 3604 | checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" | 3579 | checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" |
| 3605 | dependencies = [ | 3580 | dependencies = [ |
| 3606 | "bitflags 2.13.1", | 3581 | "bitflags 2.13.1", |
| 3607 | "calloop 0.13.0", | 3582 | "calloop", |
| 3608 | "calloop-wayland-source 0.3.0", | 3583 | "calloop-wayland-source", |
| 3609 | "cursor-icon", | 3584 | "cursor-icon", |
| 3610 | "libc", | 3585 | "libc", |
| 3611 | "log", | 3586 | "log", |
| ... | @@ -3622,44 +3597,6 @@ dependencies = [ | ... | @@ -3622,44 +3597,6 @@ dependencies = [ |
| 3622 | "xkeysym", | 3597 | "xkeysym", |
| 3623 | ] | 3598 | ] |
| 3624 | 3599 | ||
| 3625 | [[package]] | ||
| 3626 | name = "smithay-client-toolkit" | ||
| 3627 | version = "0.20.0" | ||
| 3628 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3629 | checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" | ||
| 3630 | dependencies = [ | ||
| 3631 | "bitflags 2.13.1", | ||
| 3632 | "calloop 0.14.4", | ||
| 3633 | "calloop-wayland-source 0.4.1", | ||
| 3634 | "cursor-icon", | ||
| 3635 | "libc", | ||
| 3636 | "log", | ||
| 3637 | "memmap2", | ||
| 3638 | "rustix 1.1.4", | ||
| 3639 | "thiserror 2.0.20", | ||
| 3640 | "wayland-backend", | ||
| 3641 | "wayland-client", | ||
| 3642 | "wayland-csd-frame", | ||
| 3643 | "wayland-cursor", | ||
| 3644 | "wayland-protocols", | ||
| 3645 | "wayland-protocols-experimental", | ||
| 3646 | "wayland-protocols-misc", | ||
| 3647 | "wayland-protocols-wlr", | ||
| 3648 | "wayland-scanner", | ||
| 3649 | "xkeysym", | ||
| 3650 | ] | ||
| 3651 | |||
| 3652 | [[package]] | ||
| 3653 | name = "smithay-clipboard" | ||
| 3654 | version = "0.7.3" | ||
| 3655 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3656 | checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226" | ||
| 3657 | dependencies = [ | ||
| 3658 | "libc", | ||
| 3659 | "smithay-client-toolkit 0.20.0", | ||
| 3660 | "wayland-backend", | ||
| 3661 | ] | ||
| 3662 | |||
| 3663 | [[package]] | 3600 | [[package]] |
| 3664 | name = "smol_str" | 3601 | name = "smol_str" |
| 3665 | version = "0.2.2" | 3602 | version = "0.2.2" |
| ... | @@ -3700,7 +3637,6 @@ dependencies = [ | ... | @@ -3700,7 +3637,6 @@ dependencies = [ |
| 3700 | "sctk-adwaita", | 3637 | "sctk-adwaita", |
| 3701 | "serde", | 3638 | "serde", |
| 3702 | "serde_json", | 3639 | "serde_json", |
| 3703 | "smithay-clipboard", | ||
| 3704 | "spellbook", | 3640 | "spellbook", |
| 3705 | "ui", | 3641 | "ui", |
| 3706 | "unicode_names2", | 3642 | "unicode_names2", |
| ... | @@ -3710,6 +3646,7 @@ dependencies = [ | ... | @@ -3710,6 +3646,7 @@ dependencies = [ |
| 3710 | "wayland-backend", | 3646 | "wayland-backend", |
| 3711 | "wayland-client", | 3647 | "wayland-client", |
| 3712 | "wayland-protocols", | 3648 | "wayland-protocols", |
| 3649 | "wayland-sys", | ||
| 3713 | "web-sys", | 3650 | "web-sys", |
| 3714 | "web-time", | 3651 | "web-time", |
| 3715 | "webpki-root-certs", | 3652 | "webpki-root-certs", |
| ... | @@ -4067,7 +4004,6 @@ version = "0.1.44" | ... | @@ -4067,7 +4004,6 @@ version = "0.1.44" |
| 4067 | source = "registry+https://github.com/rust-lang/crates.io-index" | 4004 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 4068 | checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" | 4005 | checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" |
| 4069 | dependencies = [ | 4006 | dependencies = [ |
| 4070 | "log", | ||
| 4071 | "pin-project-lite", | 4007 | "pin-project-lite", |
| 4072 | "tracing-attributes", | 4008 | "tracing-attributes", |
| 4073 | "tracing-core", | 4009 | "tracing-core", |
| ... | @@ -4420,32 +4356,6 @@ dependencies = [ | ... | @@ -4420,32 +4356,6 @@ dependencies = [ |
| 4420 | "wayland-scanner", | 4356 | "wayland-scanner", |
| 4421 | ] | 4357 | ] |
| 4422 | 4358 | ||
| 4423 | [[package]] | ||
| 4424 | name = "wayland-protocols-experimental" | ||
| 4425 | version = "20250721.0.1" | ||
| 4426 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 4427 | checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" | ||
| 4428 | dependencies = [ | ||
| 4429 | "bitflags 2.13.1", | ||
| 4430 | "wayland-backend", | ||
| 4431 | "wayland-client", | ||
| 4432 | "wayland-protocols", | ||
| 4433 | "wayland-scanner", | ||
| 4434 | ] | ||
| 4435 | |||
| 4436 | [[package]] | ||
| 4437 | name = "wayland-protocols-misc" | ||
| 4438 | version = "0.3.12" | ||
| 4439 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 4440 | checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" | ||
| 4441 | dependencies = [ | ||
| 4442 | "bitflags 2.13.1", | ||
| 4443 | "wayland-backend", | ||
| 4444 | "wayland-client", | ||
| 4445 | "wayland-protocols", | ||
| 4446 | "wayland-scanner", | ||
| 4447 | ] | ||
| 4448 | |||
| 4449 | [[package]] | 4359 | [[package]] |
| 4450 | name = "wayland-protocols-plasma" | 4360 | name = "wayland-protocols-plasma" |
| 4451 | version = "0.3.12" | 4361 | version = "0.3.12" |
| ... | @@ -5040,7 +4950,7 @@ dependencies = [ | ... | @@ -5040,7 +4950,7 @@ dependencies = [ |
| 5040 | "bitflags 2.13.1", | 4950 | "bitflags 2.13.1", |
| 5041 | "block2 0.5.1", | 4951 | "block2 0.5.1", |
| 5042 | "bytemuck", | 4952 | "bytemuck", |
| 5043 | "calloop 0.13.0", | 4953 | "calloop", |
| 5044 | "cfg_aliases", | 4954 | "cfg_aliases", |
| 5045 | "concurrent-queue", | 4955 | "concurrent-queue", |
| 5046 | "core-foundation 0.9.4", | 4956 | "core-foundation 0.9.4", |
| ... | @@ -5062,7 +4972,7 @@ dependencies = [ | ... | @@ -5062,7 +4972,7 @@ dependencies = [ |
| 5062 | "redox_syscall 0.4.1", | 4972 | "redox_syscall 0.4.1", |
| 5063 | "rustix 0.38.44", | 4973 | "rustix 0.38.44", |
| 5064 | "sctk-adwaita", | 4974 | "sctk-adwaita", |
| 5065 | "smithay-client-toolkit 0.19.2", | 4975 | "smithay-client-toolkit", |
| 5066 | "smol_str", | 4976 | "smol_str", |
| 5067 | "tracing", | 4977 | "tracing", |
| 5068 | "unicode-segmentation", | 4978 | "unicode-segmentation", |
arc/platforms.md+4-1| ... | @@ -104,7 +104,10 @@ keyboard, the toolbar and the macOS menu bar all run commands from it. | ... | @@ -104,7 +104,10 @@ keyboard, the toolbar and the macOS menu bar all run commands from it. |
| 104 | and so is Enchant, which checks spelling with whatever dictionaries its | 104 | and so is Enchant, which checks spelling with whatever dictionaries its |
| 105 | providers have; without it, words go unmarked. | 105 | providers have; without it, words go unmarked. |
| 106 | - Wayland's clipboard goes through the window's own connection, since not every | 106 | - Wayland's clipboard goes through the window's own connection, since not every |
| 107 | compositor offers a clipboard to clients without a window. | 107 | compositor offers a clipboard to clients without a window. Its queue wakes the |
| 108 | event loop, which answers other apps' pastes with no thread of its own. Text, | ||
| 109 | pages, pictures and files are read there too, not through Xwayland. X11 | ||
| 110 | sessions use arboard. | ||
| 108 | - The executable carries its desktop entry and icon (`desktop_linux.rs`). The | 111 | - The executable carries its desktop entry and icon (`desktop_linux.rs`). The |
| 109 | window is `net.paperclover.snowbound` to Wayland and X11 alike. Where a | 112 | window is `net.paperclover.snowbound` to Wayland and X11 alike. Where a |
| 110 | Wayland compositor lacks xdg-toplevel-icon, as GNOME's does, it finds the | 113 | Wayland compositor lacks xdg-toplevel-icon, as GNOME's does, it finds the |
crates/snowbound/Cargo.toml+1-1| ... | @@ -103,7 +103,6 @@ windows-sys = { version = "0.61", features = [ | ... | @@ -103,7 +103,6 @@ windows-sys = { version = "0.61", features = [ |
| 103 | 103 | ||
| 104 | [target.'cfg(target_os = "linux")'.dependencies] | 104 | [target.'cfg(target_os = "linux")'.dependencies] |
| 105 | accesskit_winit = { version = "0.34.0", default-features = false, features = ["accesskit_unix", "async-io"] } | 105 | accesskit_winit = { version = "0.34.0", default-features = false, features = ["accesskit_unix", "async-io"] } |
| 106 | smithay-clipboard = "0.7.3" | ||
| 107 | # Scales the window's icon. | 106 | # Scales the window's icon. |
| 108 | image = { version = "0.25", default-features = false } | 107 | image = { version = "0.25", default-features = false } |
| 109 | # Loads fontconfig at run time, so builds need no fontconfig headers or library. | 108 | # Loads fontconfig at run time, so builds need no fontconfig headers or library. |
| ... | @@ -118,6 +117,7 @@ sctk-adwaita = { version = "0.10.1", default-features = false } | ... | @@ -118,6 +117,7 @@ sctk-adwaita = { version = "0.10.1", default-features = false } |
| 118 | wayland-backend = { version = "0.3.17", features = ["client_system"] } | 117 | wayland-backend = { version = "0.3.17", features = ["client_system"] } |
| 119 | wayland-client = "0.31.15" | 118 | wayland-client = "0.31.15" |
| 120 | wayland-protocols = { version = "0.32.13", features = ["client", "staging"] } | 119 | wayland-protocols = { version = "0.32.13", features = ["client", "staging"] } |
| 120 | wayland-sys = { version = "0.31.11", features = ["client"] } | ||
| 121 | # Brings the window forward on X11, on a connection of its own. | 121 | # Brings the window forward on X11, on a connection of its own. |
| 122 | x11rb = "0.13.2" | 122 | x11rb = "0.13.2" |
| 123 | 123 |
crates/snowbound/src/clipboard_linux.rs created+567| ... | @@ -0,0 +1,567 @@ | ||
| 1 | //! The clipboard on Linux. On Wayland it lives on winit's own connection, so other apps' | ||
| 2 | //! paste requests are answered on the event loop as they arrive, with no thread of its own; | ||
| 3 | //! text, pages, pictures and files are all read from the selection there. On X11, arboard's. | ||
| 4 | |||
| 5 | use std::{ | ||
| 6 | error::Error, | ||
| 7 | io::Read, | ||
| 8 | os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd}, | ||
| 9 | path::PathBuf, | ||
| 10 | sync::{Arc, Mutex}, | ||
| 11 | task::{Context, Wake, Waker}, | ||
| 12 | time::{Duration, Instant}, | ||
| 13 | }; | ||
| 14 | use wayland_client::{ | ||
| 15 | Connection, Dispatch, EventQueue, Proxy, QueueHandle, WEnum, | ||
| 16 | backend::{Backend, ObjectData, ObjectId, protocol::Message}, | ||
| 17 | protocol::{ | ||
| 18 | wl_data_device::{self, WlDataDevice}, | ||
| 19 | wl_data_device_manager::WlDataDeviceManager, | ||
| 20 | wl_data_offer::{self, WlDataOffer}, | ||
| 21 | wl_data_source::{self, WlDataSource}, | ||
| 22 | wl_keyboard::{self, WlKeyboard}, | ||
| 23 | wl_pointer::{self, WlPointer}, | ||
| 24 | wl_registry::{self, WlRegistry}, | ||
| 25 | wl_seat::{self, WlSeat}, | ||
| 26 | wl_surface::{self, WlSurface}, | ||
| 27 | }, | ||
| 28 | }; | ||
| 29 | use wayland_sys::{ | ||
| 30 | client::{wayland_client_handle, wl_event_queue, wl_proxy}, | ||
| 31 | ffi_dispatch, | ||
| 32 | }; | ||
| 33 | use winit::{ | ||
| 34 | raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle}, | ||
| 35 | window::Window, | ||
| 36 | }; | ||
| 37 | |||
| 38 | /// The MIME types text is offered and read as, the most precise first. | ||
| 39 | const TEXT: [&str; 4] = [ | ||
| 40 | "text/plain;charset=utf-8", | ||
| 41 | "UTF8_STRING", | ||
| 42 | "text/plain", | ||
| 43 | "STRING", | ||
| 44 | ]; | ||
| 45 | /// Snowbound's own format, [`crate::paste::Copied::clip`]. | ||
| 46 | const CLIP: &str = "application/x-snowbound-clip"; | ||
| 47 | /// How long a paste waits on the app that copied. | ||
| 48 | const PATIENCE: Duration = Duration::from_secs(2); | ||
| 49 | |||
| 50 | /// The HTML and Snowbound's own format of this process's last copy through X11. | ||
| 51 | static LAST_COPY: Mutex<Option<(String, String)>> = Mutex::new(None); | ||
| 52 | |||
| 53 | pub enum Clipboard { | ||
| 54 | Wayland(Box<Wayland>), | ||
| 55 | X11(arboard::Clipboard), | ||
| 56 | } | ||
| 57 | |||
| 58 | impl Clipboard { | ||
| 59 | pub fn new(window: &Window) -> Result<Self, arboard::Error> { | ||
| 60 | let handles = window | ||
| 61 | .display_handle() | ||
| 62 | .ok() | ||
| 63 | .zip(window.window_handle().ok()); | ||
| 64 | match handles.map(|(display, window)| (display.as_raw(), window.as_raw())) { | ||
| 65 | Some((RawDisplayHandle::Wayland(display), RawWindowHandle::Wayland(window))) => Ok( | ||
| 66 | Self::Wayland(Box::new(Wayland::new(display.display, window.surface))), | ||
| 67 | ), | ||
| 68 | _ => arboard::Clipboard::new().map(Self::X11), | ||
| 69 | } | ||
| 70 | } | ||
| 71 | |||
| 72 | pub fn set_text(&mut self, text: String) -> Result<(), Box<dyn Error>> { | ||
| 73 | match self { | ||
| 74 | Self::Wayland(wayland) => { | ||
| 75 | wayland.copy(TEXT.map(|kind| (kind, text.clone().into_bytes())).to_vec()) | ||
| 76 | } | ||
| 77 | Self::X11(clipboard) => Ok(clipboard.set_text(text)?), | ||
| 78 | } | ||
| 79 | } | ||
| 80 | |||
| 81 | /// Text, HTML and Snowbound's own format, as one copy: see [`crate::paste::Copied`]. | ||
| 82 | pub fn set(&mut self, copied: &crate::paste::Copied) -> Result<(), Box<dyn Error>> { | ||
| 83 | match self { | ||
| 84 | Self::Wayland(wayland) => { | ||
| 85 | let text = TEXT.map(|kind| (kind, copied.text.clone().into_bytes())); | ||
| 86 | let rich = [("text/html", &copied.html), (CLIP, &copied.clip)] | ||
| 87 | .map(|(kind, bytes)| (kind, bytes.clone().into_bytes())); | ||
| 88 | wayland.copy(text.into_iter().chain(rich).collect()) | ||
| 89 | } | ||
| 90 | Self::X11(clipboard) => { | ||
| 91 | clipboard.set().html(&copied.html, Some(&copied.text))?; | ||
| 92 | *LAST_COPY.lock().unwrap() = Some((copied.html.clone(), copied.clip.clone())); | ||
| 93 | Ok(()) | ||
| 94 | } | ||
| 95 | } | ||
| 96 | } | ||
| 97 | |||
| 98 | /// What Snowbound itself copied, if it did: on X11, which arboard offers no format of | ||
| 99 | /// Snowbound's own through, the last copy's, while the clipboard holds its HTML. | ||
| 100 | pub fn get_clip(&mut self) -> Option<String> { | ||
| 101 | if let Self::Wayland(wayland) = self { | ||
| 102 | return String::from_utf8(wayland.paste(CLIP)?).ok(); | ||
| 103 | } | ||
| 104 | let html = self.get_html()?; | ||
| 105 | match &*LAST_COPY.lock().unwrap() { | ||
| 106 | Some((copied, clip)) if *copied == html => Some(clip.clone()), | ||
| 107 | _ => None, | ||
| 108 | } | ||
| 109 | } | ||
| 110 | |||
| 111 | pub fn get_text(&mut self) -> Result<String, Box<dyn Error>> { | ||
| 112 | match self { | ||
| 113 | Self::Wayland(wayland) => { | ||
| 114 | let bytes = TEXT.iter().find_map(|kind| wayland.paste(kind)); | ||
| 115 | Ok(String::from_utf8_lossy(&bytes.unwrap_or_default()).into_owned()) | ||
| 116 | } | ||
| 117 | Self::X11(clipboard) => Ok(clipboard.get_text()?), | ||
| 118 | } | ||
| 119 | } | ||
| 120 | |||
| 121 | pub fn get_files(&mut self) -> Vec<PathBuf> { | ||
| 122 | use std::os::unix::ffi::OsStrExt; | ||
| 123 | match self { | ||
| 124 | Self::Wayland(wayland) => files(&wayland.paste("text/uri-list").unwrap_or_default()), | ||
| 125 | // arboard splits text/uri-list at LF and keeps the CR its CRLF lines end in. | ||
| 126 | Self::X11(clipboard) => (clipboard.get().file_list().unwrap_or_default().into_iter()) | ||
| 127 | .map(|path| { | ||
| 128 | let bytes = path.as_os_str().as_bytes(); | ||
| 129 | let bytes = bytes.strip_suffix(b"\r").unwrap_or(bytes); | ||
| 130 | std::ffi::OsStr::from_bytes(bytes).into() | ||
| 131 | }) | ||
| 132 | .collect(), | ||
| 133 | } | ||
| 134 | } | ||
| 135 | |||
| 136 | pub fn get_html(&mut self) -> Option<String> { | ||
| 137 | match self { | ||
| 138 | Self::Wayland(wayland) => wayland | ||
| 139 | .paste("text/html") | ||
| 140 | .map(|bytes| String::from_utf8_lossy(&bytes).into_owned()), | ||
| 141 | Self::X11(clipboard) => clipboard.get().html().ok(), | ||
| 142 | } | ||
| 143 | } | ||
| 144 | |||
| 145 | pub fn get_picture(&mut self) -> Option<Vec<u8>> { | ||
| 146 | match self { | ||
| 147 | Self::Wayland(wayland) => ["image/png", "image/jpeg", "image/gif"] | ||
| 148 | .iter() | ||
| 149 | .find_map(|kind| wayland.paste(kind)), | ||
| 150 | Self::X11(clipboard) => crate::paste::bitmap(clipboard.get_image().ok()?), | ||
| 151 | } | ||
| 152 | } | ||
| 153 | |||
| 154 | /// Answers what the compositor asked of the clipboard since it last woke the event loop. | ||
| 155 | pub fn serve(&mut self) { | ||
| 156 | if let Self::Wayland(wayland) = self { | ||
| 157 | wayland.serve(); | ||
| 158 | } | ||
| 159 | } | ||
| 160 | } | ||
| 161 | |||
| 162 | /// The local files a `text/uri-list` names; its lines end in CRLF, though some apps end | ||
| 163 | /// them in LF alone. | ||
| 164 | fn files(list: &[u8]) -> Vec<PathBuf> { | ||
| 165 | use std::os::unix::ffi::OsStringExt; | ||
| 166 | String::from_utf8_lossy(list) | ||
| 167 | .lines() | ||
| 168 | .map(|line| line.trim_end_matches('\r')) | ||
| 169 | .filter(|line| !line.starts_with('#')) | ||
| 170 | .filter_map(|uri| { | ||
| 171 | let path = uri.strip_prefix("file://")?; | ||
| 172 | let path = path.strip_prefix("localhost").unwrap_or(path); | ||
| 173 | path.starts_with('/') | ||
| 174 | .then(|| std::ffi::OsString::from_vec(crate::paste::percent_decode(path)).into()) | ||
| 175 | }) | ||
| 176 | .collect() | ||
| 177 | } | ||
| 178 | |||
| 179 | /// A copy's bytes in each MIME type it is offered as. | ||
| 180 | type Formats = Vec<(&'static str, Vec<u8>)>; | ||
| 181 | |||
| 182 | /// The Wayland clipboard, on winit's connection once the window's first frame lends it. | ||
| 183 | pub struct Wayland { | ||
| 184 | lent: Arc<Mutex<Option<Backend>>>, | ||
| 185 | joined: Option<(Connection, EventQueue<Seat>, Seat)>, | ||
| 186 | } | ||
| 187 | |||
| 188 | /// What the clipboard knows of the seat: the selection and the serial to claim it with. | ||
| 189 | #[derive(Default)] | ||
| 190 | struct Seat { | ||
| 191 | seat: Option<WlSeat>, | ||
| 192 | manager: Option<WlDataDeviceManager>, | ||
| 193 | device: Option<WlDataDevice>, | ||
| 194 | keyboard: Option<WlKeyboard>, | ||
| 195 | pointer: Option<WlPointer>, | ||
| 196 | /// The latest input event's, which claiming the selection must name. | ||
| 197 | serial: u32, | ||
| 198 | /// The selection another app offers. | ||
| 199 | offer: Option<WlDataOffer>, | ||
| 200 | /// What this app copied, in each format it offers, while it holds the selection. | ||
| 201 | copied: Option<(WlDataSource, Formats)>, | ||
| 202 | } | ||
| 203 | |||
| 204 | /// Wakes the event loop to serve the clipboard. | ||
| 205 | struct Serve; | ||
| 206 | |||
| 207 | impl Wake for Serve { | ||
| 208 | fn wake(self: Arc<Self>) { | ||
| 209 | if let Some(proxy) = crate::platform::PROXY.get() { | ||
| 210 | let _ = proxy.send_event(crate::UserEvent::Clipboard); | ||
| 211 | } | ||
| 212 | } | ||
| 213 | } | ||
| 214 | |||
| 215 | /// Takes winit's connection from the event that reaches it, a frame callback moved onto | ||
| 216 | /// winit's queue: winit dispatches only that queue, with its own connection. | ||
| 217 | struct Lend(Arc<Mutex<Option<Backend>>>); | ||
| 218 | |||
| 219 | impl ObjectData for Lend { | ||
| 220 | fn event( | ||
| 221 | self: Arc<Self>, | ||
| 222 | backend: &Backend, | ||
| 223 | _: Message<ObjectId, OwnedFd>, | ||
| 224 | ) -> Option<Arc<dyn ObjectData>> { | ||
| 225 | *self.0.lock().unwrap() = Some(backend.clone()); | ||
| 226 | Arc::new(Serve).wake(); | ||
| 227 | None | ||
| 228 | } | ||
| 229 | |||
| 230 | fn destroyed(&self, _: ObjectId) {} | ||
| 231 | } | ||
| 232 | |||
| 233 | impl Wayland { | ||
| 234 | fn new( | ||
| 235 | display: std::ptr::NonNull<std::ffi::c_void>, | ||
| 236 | surface: std::ptr::NonNull<std::ffi::c_void>, | ||
| 237 | ) -> Self { | ||
| 238 | let lent = Arc::new(Mutex::new(None)); | ||
| 239 | // Safety: winit's display and surface outlive the window, which outlives this. | ||
| 240 | let backend = unsafe { Backend::from_foreign_display(display.as_ptr().cast()) }; | ||
| 241 | let connection = Connection::from_backend(backend); | ||
| 242 | let surface = | ||
| 243 | unsafe { ObjectId::from_ptr(WlSurface::interface(), surface.as_ptr().cast()) } | ||
| 244 | .and_then(|id| WlSurface::from_id(&connection, id)); | ||
| 245 | if let Ok(surface) = surface { | ||
| 246 | let lend: Arc<dyn ObjectData> = Arc::new(Lend(Arc::clone(&lent))); | ||
| 247 | let frame = wl_surface::Request::Frame {}; | ||
| 248 | if let Ok(callback) = connection.send_request(&surface, frame, Some(lend)) { | ||
| 249 | // Safety: both proxies are live, and the callback has had no event yet. | ||
| 250 | unsafe { onto_queue_of(callback.as_ptr(), surface.id().as_ptr()) }; | ||
| 251 | } | ||
| 252 | let _ = connection.flush(); | ||
| 253 | } | ||
| 254 | // The callback, on winit's queue, ends under winit's connection, which leaves this one | ||
| 255 | // a record of it that dropping it would free a second time. | ||
| 256 | std::mem::forget(connection); | ||
| 257 | Self { lent, joined: None } | ||
| 258 | } | ||
| 259 | |||
| 260 | /// Dispatches the seat's events and asks to be woken for the next; first joins winit's | ||
| 261 | /// connection once lent. | ||
| 262 | fn serve(&mut self) { | ||
| 263 | if self.joined.is_none() | ||
| 264 | && let Some(backend) = self.lent.lock().unwrap().take() | ||
| 265 | { | ||
| 266 | let connection = Connection::from_backend(backend); | ||
| 267 | let queue = connection.new_event_queue(); | ||
| 268 | connection.display().get_registry(&queue.handle(), ()); | ||
| 269 | self.joined = Some((connection, queue, Seat::default())); | ||
| 270 | } | ||
| 271 | if let Some((_, queue, seat)) = &mut self.joined { | ||
| 272 | let waker = Waker::from(Arc::new(Serve)); | ||
| 273 | if let std::task::Poll::Ready(Err(error)) = | ||
| 274 | queue.poll_dispatch_pending(&mut Context::from_waker(&waker), seat) | ||
| 275 | { | ||
| 276 | eprintln!("The clipboard stopped: {error}"); | ||
| 277 | self.joined = None; | ||
| 278 | } | ||
| 279 | } | ||
| 280 | } | ||
| 281 | |||
| 282 | fn copy(&mut self, formats: Formats) -> Result<(), Box<dyn Error>> { | ||
| 283 | self.serve(); | ||
| 284 | let Some((connection, queue, seat)) = &mut self.joined else { | ||
| 285 | return Err("The clipboard isn't ready yet.".into()); | ||
| 286 | }; | ||
| 287 | let (Some(manager), Some(device)) = (&seat.manager, &seat.device) else { | ||
| 288 | return Err("The compositor offers no clipboard.".into()); | ||
| 289 | }; | ||
| 290 | let source = manager.create_data_source(&queue.handle(), ()); | ||
| 291 | for (kind, _) in &formats { | ||
| 292 | source.offer(kind.to_string()); | ||
| 293 | } | ||
| 294 | device.set_selection(Some(&source), seat.serial); | ||
| 295 | // The source replaced is cancelled, and destroyed then. | ||
| 296 | seat.copied = Some((source, formats)); | ||
| 297 | connection.flush()?; | ||
| 298 | Ok(()) | ||
| 299 | } | ||
| 300 | |||
| 301 | /// The selection as `kind`, where it is offered as that: this app's own copy without a | ||
| 302 | /// round trip through the compositor, or what another app answers. | ||
| 303 | fn paste(&mut self, kind: &str) -> Option<Vec<u8>> { | ||
| 304 | self.serve(); | ||
| 305 | let (connection, _, seat) = self.joined.as_ref()?; | ||
| 306 | if let Some((_, formats)) = &seat.copied { | ||
| 307 | return offered(formats, kind).map(<[u8]>::to_vec); | ||
| 308 | } | ||
| 309 | let offer = seat.offer.as_ref()?; | ||
| 310 | let kinds = offer.data::<Mutex<Vec<String>>>()?; | ||
| 311 | if !kinds.lock().unwrap().iter().any(|offered| offered == kind) { | ||
| 312 | return None; | ||
| 313 | } | ||
| 314 | let (mut reader, writer) = std::io::pipe().ok()?; | ||
| 315 | offer.receive(kind.to_owned(), writer.as_fd()); | ||
| 316 | drop(writer); | ||
| 317 | connection.flush().ok()?; | ||
| 318 | let deadline = Instant::now() + PATIENCE; | ||
| 319 | let mut bytes = Vec::new(); | ||
| 320 | loop { | ||
| 321 | if !ready(reader.as_fd(), libc::POLLIN, deadline) { | ||
| 322 | eprintln!("The app that copied didn't answer the paste in time."); | ||
| 323 | return None; | ||
| 324 | } | ||
| 325 | let mut buffer = [0; 1 << 16]; | ||
| 326 | match reader.read(&mut buffer) { | ||
| 327 | Ok(0) => return Some(bytes), | ||
| 328 | Ok(read) => bytes.extend_from_slice(&buffer[..read]), | ||
| 329 | Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {} | ||
| 330 | Err(_) => return None, | ||
| 331 | } | ||
| 332 | } | ||
| 333 | } | ||
| 334 | } | ||
| 335 | |||
| 336 | /// Moves `proxy` onto `owner`'s event queue. libwayland names a proxy's queue only from 1.23 | ||
| 337 | /// (`wl_proxy_get_queue`), so this reads it from `struct wl_proxy`, laid out alike since 1.0. | ||
| 338 | unsafe fn onto_queue_of(proxy: *mut wl_proxy, owner: *mut wl_proxy) { | ||
| 339 | #[repr(C)] | ||
| 340 | struct Head { | ||
| 341 | interface: *const std::ffi::c_void, | ||
| 342 | implementation: *const std::ffi::c_void, | ||
| 343 | id: u32, | ||
| 344 | display: *mut std::ffi::c_void, | ||
| 345 | queue: *mut wl_event_queue, | ||
| 346 | } | ||
| 347 | let queue = unsafe { (*owner.cast::<Head>()).queue }; | ||
| 348 | unsafe { ffi_dispatch!(wayland_client_handle(), wl_proxy_set_queue, proxy, queue) }; | ||
| 349 | } | ||
| 350 | |||
| 351 | /// The bytes of `formats` in the format `kind`. | ||
| 352 | fn offered<'a>(formats: &'a [(&str, Vec<u8>)], kind: &str) -> Option<&'a [u8]> { | ||
| 353 | let (_, bytes) = formats.iter().find(|(offered, _)| *offered == kind)?; | ||
| 354 | Some(bytes) | ||
| 355 | } | ||
| 356 | |||
| 357 | /// Whether `fd` is ready for `events` before `deadline`. | ||
| 358 | fn ready(fd: BorrowedFd, events: i16, deadline: Instant) -> bool { | ||
| 359 | let left = deadline.saturating_duration_since(Instant::now()); | ||
| 360 | let mut poll = libc::pollfd { | ||
| 361 | fd: fd.as_raw_fd(), | ||
| 362 | events, | ||
| 363 | revents: 0, | ||
| 364 | }; | ||
| 365 | !left.is_zero() && unsafe { libc::poll(&mut poll, 1, left.as_millis() as i32) } > 0 | ||
| 366 | } | ||
| 367 | |||
| 368 | /// Writes `bytes` to the pipe `fd`, waiting on a slow reader no longer than a paste waits. | ||
| 369 | fn answer(fd: OwnedFd, bytes: &[u8]) { | ||
| 370 | use std::io::Write; | ||
| 371 | let mut pipe = std::fs::File::from(fd); | ||
| 372 | let deadline = Instant::now() + PATIENCE; | ||
| 373 | // A pipe ready for writing takes PIPE_BUF bytes without blocking. | ||
| 374 | for chunk in bytes.chunks(libc::PIPE_BUF) { | ||
| 375 | if !ready(pipe.as_fd(), libc::POLLOUT, deadline) || pipe.write_all(chunk).is_err() { | ||
| 376 | return; | ||
| 377 | } | ||
| 378 | } | ||
| 379 | } | ||
| 380 | |||
| 381 | impl Dispatch<WlRegistry, ()> for Seat { | ||
| 382 | fn event( | ||
| 383 | seat: &mut Self, | ||
| 384 | registry: &WlRegistry, | ||
| 385 | event: wl_registry::Event, | ||
| 386 | _: &(), | ||
| 387 | _: &Connection, | ||
| 388 | queue: &QueueHandle<Self>, | ||
| 389 | ) { | ||
| 390 | let wl_registry::Event::Global { | ||
| 391 | name, | ||
| 392 | interface, | ||
| 393 | version, | ||
| 394 | } = event | ||
| 395 | else { | ||
| 396 | return; | ||
| 397 | }; | ||
| 398 | match interface.as_str() { | ||
| 399 | "wl_seat" if seat.seat.is_none() => { | ||
| 400 | seat.seat = Some(registry.bind(name, version.min(5), queue, ())); | ||
| 401 | } | ||
| 402 | "wl_data_device_manager" if seat.manager.is_none() => { | ||
| 403 | seat.manager = Some(registry.bind(name, version.min(3), queue, ())); | ||
| 404 | } | ||
| 405 | _ => return, | ||
| 406 | } | ||
| 407 | if let (Some(manager), Some(wl_seat), None) = (&seat.manager, &seat.seat, &seat.device) { | ||
| 408 | seat.device = Some(manager.get_data_device(wl_seat, queue, ())); | ||
| 409 | } | ||
| 410 | } | ||
| 411 | } | ||
| 412 | |||
| 413 | impl Dispatch<WlSeat, ()> for Seat { | ||
| 414 | fn event( | ||
| 415 | seat: &mut Self, | ||
| 416 | wl_seat: &WlSeat, | ||
| 417 | event: wl_seat::Event, | ||
| 418 | _: &(), | ||
| 419 | _: &Connection, | ||
| 420 | queue: &QueueHandle<Self>, | ||
| 421 | ) { | ||
| 422 | let wl_seat::Event::Capabilities { | ||
| 423 | capabilities: WEnum::Value(capabilities), | ||
| 424 | } = event | ||
| 425 | else { | ||
| 426 | return; | ||
| 427 | }; | ||
| 428 | let keyboard = capabilities.contains(wl_seat::Capability::Keyboard); | ||
| 429 | if keyboard != seat.keyboard.is_some() { | ||
| 430 | seat.keyboard = keyboard.then(|| wl_seat.get_keyboard(queue, ())); | ||
| 431 | } | ||
| 432 | let pointer = capabilities.contains(wl_seat::Capability::Pointer); | ||
| 433 | if pointer != seat.pointer.is_some() { | ||
| 434 | seat.pointer = pointer.then(|| wl_seat.get_pointer(queue, ())); | ||
| 435 | } | ||
| 436 | } | ||
| 437 | } | ||
| 438 | |||
| 439 | impl Dispatch<WlKeyboard, ()> for Seat { | ||
| 440 | fn event( | ||
| 441 | seat: &mut Self, | ||
| 442 | _: &WlKeyboard, | ||
| 443 | event: wl_keyboard::Event, | ||
| 444 | _: &(), | ||
| 445 | _: &Connection, | ||
| 446 | _: &QueueHandle<Self>, | ||
| 447 | ) { | ||
| 448 | if let wl_keyboard::Event::Enter { serial, .. } | wl_keyboard::Event::Key { serial, .. } = | ||
| 449 | event | ||
| 450 | { | ||
| 451 | seat.serial = serial; | ||
| 452 | } | ||
| 453 | } | ||
| 454 | } | ||
| 455 | |||
| 456 | impl Dispatch<WlPointer, ()> for Seat { | ||
| 457 | fn event( | ||
| 458 | seat: &mut Self, | ||
| 459 | _: &WlPointer, | ||
| 460 | event: wl_pointer::Event, | ||
| 461 | _: &(), | ||
| 462 | _: &Connection, | ||
| 463 | _: &QueueHandle<Self>, | ||
| 464 | ) { | ||
| 465 | if let wl_pointer::Event::Enter { serial, .. } | wl_pointer::Event::Button { serial, .. } = | ||
| 466 | event | ||
| 467 | { | ||
| 468 | seat.serial = serial; | ||
| 469 | } | ||
| 470 | } | ||
| 471 | } | ||
| 472 | |||
| 473 | impl Dispatch<WlDataDeviceManager, ()> for Seat { | ||
| 474 | fn event( | ||
| 475 | _: &mut Self, | ||
| 476 | _: &WlDataDeviceManager, | ||
| 477 | _: <WlDataDeviceManager as Proxy>::Event, | ||
| 478 | _: &(), | ||
| 479 | _: &Connection, | ||
| 480 | _: &QueueHandle<Self>, | ||
| 481 | ) { | ||
| 482 | } | ||
| 483 | } | ||
| 484 | |||
| 485 | impl Dispatch<WlDataDevice, ()> for Seat { | ||
| 486 | fn event( | ||
| 487 | seat: &mut Self, | ||
| 488 | _: &WlDataDevice, | ||
| 489 | event: wl_data_device::Event, | ||
| 490 | _: &(), | ||
| 491 | _: &Connection, | ||
| 492 | _: &QueueHandle<Self>, | ||
| 493 | ) { | ||
| 494 | match event { | ||
| 495 | wl_data_device::Event::Selection { id } => { | ||
| 496 | if let Some(old) = std::mem::replace(&mut seat.offer, id) { | ||
| 497 | old.destroy(); | ||
| 498 | } | ||
| 499 | } | ||
| 500 | // Drags aren't taken. | ||
| 501 | wl_data_device::Event::Enter { | ||
| 502 | id: Some(offer), .. | ||
| 503 | } => offer.destroy(), | ||
| 504 | _ => {} | ||
| 505 | } | ||
| 506 | } | ||
| 507 | |||
| 508 | wayland_client::event_created_child!(Seat, WlDataDevice, [ | ||
| 509 | wl_data_device::EVT_DATA_OFFER_OPCODE => (WlDataOffer, Mutex::new(Vec::<String>::new())), | ||
| 510 | ]); | ||
| 511 | } | ||
| 512 | |||
| 513 | impl Dispatch<WlDataOffer, Mutex<Vec<String>>> for Seat { | ||
| 514 | fn event( | ||
| 515 | _: &mut Self, | ||
| 516 | _: &WlDataOffer, | ||
| 517 | event: wl_data_offer::Event, | ||
| 518 | kinds: &Mutex<Vec<String>>, | ||
| 519 | _: &Connection, | ||
| 520 | _: &QueueHandle<Self>, | ||
| 521 | ) { | ||
| 522 | if let wl_data_offer::Event::Offer { mime_type } = event { | ||
| 523 | kinds.lock().unwrap().push(mime_type); | ||
| 524 | } | ||
| 525 | } | ||
| 526 | } | ||
| 527 | |||
| 528 | impl Dispatch<WlDataSource, ()> for Seat { | ||
| 529 | fn event( | ||
| 530 | seat: &mut Self, | ||
| 531 | source: &WlDataSource, | ||
| 532 | event: wl_data_source::Event, | ||
| 533 | _: &(), | ||
| 534 | _: &Connection, | ||
| 535 | _: &QueueHandle<Self>, | ||
| 536 | ) { | ||
| 537 | let copied = seat.copied.as_ref().filter(|(copied, _)| copied == source); | ||
| 538 | match event { | ||
| 539 | wl_data_source::Event::Send { mime_type, fd } => { | ||
| 540 | if let Some(bytes) = copied.and_then(|(_, formats)| offered(formats, &mime_type)) { | ||
| 541 | answer(fd, bytes); | ||
| 542 | } | ||
| 543 | } | ||
| 544 | wl_data_source::Event::Cancelled => { | ||
| 545 | if copied.is_some() { | ||
| 546 | seat.copied = None; | ||
| 547 | } | ||
| 548 | source.destroy(); | ||
| 549 | } | ||
| 550 | _ => {} | ||
| 551 | } | ||
| 552 | } | ||
| 553 | } | ||
| 554 | |||
| 555 | #[cfg(test)] | ||
| 556 | mod tests { | ||
| 557 | use super::*; | ||
| 558 | |||
| 559 | #[test] | ||
| 560 | fn uri_lists_name_local_files_whatever_their_line_ends() { | ||
| 561 | let list = b"# copied\r\nfile:///home/ben/Notes%20One.one\r\nfile://localhost/tmp/a.png\nhttps://example.com/b.png\r\n"; | ||
| 562 | assert_eq!( | ||
| 563 | files(list), | ||
| 564 | ["/home/ben/Notes One.one", "/tmp/a.png"].map(PathBuf::from) | ||
| 565 | ); | ||
| 566 | } | ||
| 567 | } | ||
crates/snowbound/src/linux.rs+1-100| ... | @@ -212,106 +212,7 @@ pub fn install_title_bar(window: &Window) { | ... | @@ -212,106 +212,7 @@ pub fn install_title_bar(window: &Window) { |
| 212 | crate::desktop::set_toplevel_icon(window); | 212 | crate::desktop::set_toplevel_icon(window); |
| 213 | } | 213 | } |
| 214 | 214 | ||
| 215 | /// The HTML and Snowbound's own format of this process's last copy through X11. | 215 | pub use crate::clipboard::Clipboard; |
| 216 | static LAST_COPY: std::sync::Mutex<Option<(String, String)>> = std::sync::Mutex::new(None); | ||
| 217 | |||
| 218 | /// Wayland's clipboard through the window's own connection, as not every compositor offers | ||
| 219 | /// a clipboard to clients without a window; X11's otherwise. | ||
| 220 | pub enum Clipboard { | ||
| 221 | Wayland(smithay_clipboard::Clipboard), | ||
| 222 | X11(arboard::Clipboard), | ||
| 223 | } | ||
| 224 | |||
| 225 | impl Clipboard { | ||
| 226 | pub fn new(window: &Window) -> Result<Self, arboard::Error> { | ||
| 227 | use winit::raw_window_handle::{HasDisplayHandle, RawDisplayHandle}; | ||
| 228 | match window.display_handle().map(|handle| handle.as_raw()) { | ||
| 229 | // The display outlives the clipboard, which the window's state owns. | ||
| 230 | Ok(RawDisplayHandle::Wayland(handle)) => Ok(Self::Wayland(unsafe { | ||
| 231 | smithay_clipboard::Clipboard::new(handle.display.as_ptr()) | ||
| 232 | })), | ||
| 233 | _ => arboard::Clipboard::new().map(Self::X11), | ||
| 234 | } | ||
| 235 | } | ||
| 236 | |||
| 237 | pub fn set_text(&mut self, text: String) -> Result<(), arboard::Error> { | ||
| 238 | match self { | ||
| 239 | Self::Wayland(clipboard) => { | ||
| 240 | clipboard.store(text); | ||
| 241 | Ok(()) | ||
| 242 | } | ||
| 243 | Self::X11(clipboard) => clipboard.set_text(text), | ||
| 244 | } | ||
| 245 | } | ||
| 246 | |||
| 247 | /// Text, HTML and Snowbound's own format, as one copy: see [`crate::paste::Copied`]. | ||
| 248 | /// smithay-clipboard offers text alone. | ||
| 249 | pub fn set(&mut self, copied: &crate::paste::Copied) -> Result<(), arboard::Error> { | ||
| 250 | match self { | ||
| 251 | Self::Wayland(clipboard) => { | ||
| 252 | clipboard.store(copied.text.clone()); | ||
| 253 | Ok(()) | ||
| 254 | } | ||
| 255 | Self::X11(clipboard) => { | ||
| 256 | clipboard.set().html(&copied.html, Some(&copied.text))?; | ||
| 257 | *LAST_COPY.lock().unwrap() = Some((copied.html.clone(), copied.clip.clone())); | ||
| 258 | Ok(()) | ||
| 259 | } | ||
| 260 | } | ||
| 261 | } | ||
| 262 | |||
| 263 | /// What Snowbound itself copied, if it did: on X11, which arboard offers no format of | ||
| 264 | /// Snowbound's own through, the last copy's, while the clipboard holds its HTML. | ||
| 265 | pub fn get_clip(&mut self) -> Option<String> { | ||
| 266 | if !matches!(self, Self::X11(_)) { | ||
| 267 | return None; | ||
| 268 | } | ||
| 269 | let html = self.get_html()?; | ||
| 270 | match &*LAST_COPY.lock().unwrap() { | ||
| 271 | Some((copied, clip)) if *copied == html => Some(clip.clone()), | ||
| 272 | _ => None, | ||
| 273 | } | ||
| 274 | } | ||
| 275 | |||
| 276 | pub fn get_text(&mut self) -> Result<String, Box<dyn std::error::Error>> { | ||
| 277 | Ok(match self { | ||
| 278 | Self::Wayland(clipboard) => clipboard.load()?, | ||
| 279 | Self::X11(clipboard) => clipboard.get_text()?, | ||
| 280 | }) | ||
| 281 | } | ||
| 282 | |||
| 283 | pub fn get_files(&mut self) -> Vec<std::path::PathBuf> { | ||
| 284 | use std::os::unix::ffi::OsStrExt; | ||
| 285 | let files = self.through_x11(|clipboard| clipboard.get().file_list().ok()); | ||
| 286 | // arboard splits text/uri-list at LF and keeps the CR its CRLF lines end in. | ||
| 287 | let path = |path: std::path::PathBuf| { | ||
| 288 | let bytes = path.as_os_str().as_bytes(); | ||
| 289 | let bytes = bytes.strip_suffix(b"\r").unwrap_or(bytes); | ||
| 290 | std::ffi::OsStr::from_bytes(bytes).into() | ||
| 291 | }; | ||
| 292 | files.unwrap_or_default().into_iter().map(path).collect() | ||
| 293 | } | ||
| 294 | |||
| 295 | pub fn get_html(&mut self) -> Option<String> { | ||
| 296 | self.through_x11(|clipboard| clipboard.get().html().ok()) | ||
| 297 | } | ||
| 298 | |||
| 299 | pub fn get_picture(&mut self) -> Option<Vec<u8>> { | ||
| 300 | self.through_x11(|clipboard| crate::paste::bitmap(clipboard.get_image().ok()?)) | ||
| 301 | } | ||
| 302 | |||
| 303 | /// Files, pages and pictures come through X11 on Wayland too, whose clipboard Xwayland | ||
| 304 | /// shares. | ||
| 305 | fn through_x11<T>( | ||
| 306 | &mut self, | ||
| 307 | read: impl FnOnce(&mut arboard::Clipboard) -> Option<T>, | ||
| 308 | ) -> Option<T> { | ||
| 309 | match self { | ||
| 310 | Self::Wayland(_) => read(&mut arboard::Clipboard::new().ok()?), | ||
| 311 | Self::X11(clipboard) => read(clipboard), | ||
| 312 | } | ||
| 313 | } | ||
| 314 | } | ||
| 315 | 216 | ||
| 316 | /// Whether the system draws the title bar: the window manager, or on Wayland the compositor. | 217 | /// Whether the system draws the title bar: the window manager, or on Wayland the compositor. |
| 317 | /// Where winit's Adwaita frame decorates the window instead, as on GNOME, the frame has no | 218 | /// Where winit's Adwaita frame decorates the window instead, as on GNOME, the frame has no |
crates/snowbound/src/main.rs+13-1| ... | @@ -6,6 +6,9 @@ mod aqua; | ... | @@ -6,6 +6,9 @@ mod aqua; |
| 6 | mod art; | 6 | mod art; |
| 7 | mod attachment; | 7 | mod attachment; |
| 8 | mod background; | 8 | mod background; |
| 9 | #[cfg(target_os = "linux")] | ||
| 10 | #[path = "clipboard_linux.rs"] | ||
| 11 | mod clipboard; | ||
| 9 | mod commands; | 12 | mod commands; |
| 10 | #[cfg(all(test, feature = "wgpu", not(windows)))] | 13 | #[cfg(all(test, feature = "wgpu", not(windows)))] |
| 11 | mod conflict_render; | 14 | mod conflict_render; |
| ... | @@ -278,6 +281,9 @@ fn confirm(message: &str, detail: &str, cancel: &str, action: &str, reply: Reply | ... | @@ -278,6 +281,9 @@ fn confirm(message: &str, detail: &str, cancel: &str, action: &str, reply: Reply |
| 278 | 281 | ||
| 279 | enum UserEvent { | 282 | enum UserEvent { |
| 280 | Quit, | 283 | Quit, |
| 284 | /// The Wayland compositor asked something of the clipboard. | ||
| 285 | #[cfg(target_os = "linux")] | ||
| 286 | Clipboard, | ||
| 281 | /// Quits without asking, as after the user agreed to discard a temporary page. | 287 | /// Quits without asking, as after the user agreed to discard a temporary page. |
| 282 | #[cfg(not(target_arch = "wasm32"))] | 288 | #[cfg(not(target_arch = "wasm32"))] |
| 283 | Exit, | 289 | Exit, |
| ... | @@ -6194,6 +6200,12 @@ impl State { | ... | @@ -6194,6 +6200,12 @@ impl State { |
| 6194 | UserEvent::Quit | UserEvent::Replay(Replay::Quit) => {} | 6200 | UserEvent::Quit | UserEvent::Replay(Replay::Quit) => {} |
| 6195 | #[cfg(not(target_arch = "wasm32"))] | 6201 | #[cfg(not(target_arch = "wasm32"))] |
| 6196 | UserEvent::Exit => {} | 6202 | UserEvent::Exit => {} |
| 6203 | #[cfg(target_os = "linux")] | ||
| 6204 | UserEvent::Clipboard => { | ||
| 6205 | if let Clipboard::System(clipboard) = &mut self.clipboard { | ||
| 6206 | clipboard.serve(); | ||
| 6207 | } | ||
| 6208 | } | ||
| 6197 | UserEvent::Picture(bytes) => { | 6209 | UserEvent::Picture(bytes) => { |
| 6198 | if let Err(error) = self.insert_picture(bytes, None) { | 6210 | if let Err(error) = self.insert_picture(bytes, None) { |
| 6199 | eprintln!("{error}"); | 6211 | eprintln!("{error}"); |
| ... | @@ -6623,7 +6635,7 @@ impl ApplicationHandler<UserEvent> for App { | ... | @@ -6623,7 +6635,7 @@ impl ApplicationHandler<UserEvent> for App { |
| 6623 | } | 6635 | } |
| 6624 | 6636 | ||
| 6625 | /// Drops the window's state while the event loop still holds the Wayland connection the | 6637 | /// Drops the window's state while the event loop still holds the Wayland connection the |
| 6626 | /// clipboard's thread and the surface use; `run_app` closes it on returning. | 6638 | /// clipboard and the surface use; `run_app` closes it on returning. |
| 6627 | fn exiting(&mut self, _: &ActiveEventLoop) { | 6639 | fn exiting(&mut self, _: &ActiveEventLoop) { |
| 6628 | self.restart = self | 6640 | self.restart = self |
| 6629 | .state | 6641 | .state |