wl_clipboard_rs/
utils.rs

1//! Helper functions.
2
3use std::ffi::OsString;
4use std::os::unix::net::UnixStream;
5use std::path::PathBuf;
6use std::{env, io};
7
8use wayland_client::protocol::wl_registry::{self, WlRegistry};
9use wayland_client::protocol::wl_seat::WlSeat;
10use wayland_client::{
11    event_created_child, ConnectError, Connection, Dispatch, DispatchError, Proxy,
12};
13use wayland_protocols::ext::data_control::v1::client::ext_data_control_manager_v1::ExtDataControlManagerV1;
14use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1;
15
16use crate::data_control::{
17    impl_dispatch_device, impl_dispatch_manager, impl_dispatch_offer, Manager,
18};
19
20/// Checks if the given MIME type represents plain text.
21///
22/// # Examples
23///
24/// ```
25/// use wl_clipboard_rs::utils::is_text;
26///
27/// assert!(is_text("text/plain"));
28/// assert!(!is_text("application/octet-stream"));
29/// ```
30pub fn is_text(mime_type: &str) -> bool {
31    // Based on wl-clipboard:
32    // https://github.com/bugaevc/wl-clipboard/blob/e8082035dafe0241739d7f7d16f7ecfd2ce06172/src/util/string.c#L24
33
34    match mime_type {
35        "TEXT" | "STRING" | "UTF8_STRING" => true,
36        x if x.starts_with("text/") => true,
37        // Common script and markup types.
38        x if x.contains("json")
39            | x.ends_with("script")
40            | x.ends_with("xml")
41            | x.ends_with("yaml")
42            | x.ends_with("csv")
43            | x.ends_with("ini") =>
44        {
45            true
46        }
47        _ => false,
48    }
49}
50
51struct PrimarySelectionState {
52    // Any seat that we get from the compositor.
53    seat: Option<WlSeat>,
54    clipboard_manager: Option<Manager>,
55    saw_zwlr_v1: bool,
56    got_primary_selection: bool,
57}
58
59impl Dispatch<WlRegistry, ()> for PrimarySelectionState {
60    fn event(
61        state: &mut Self,
62        registry: &WlRegistry,
63        event: <WlRegistry as wayland_client::Proxy>::Event,
64        _data: &(),
65        _conn: &Connection,
66        qh: &wayland_client::QueueHandle<Self>,
67    ) {
68        if let wl_registry::Event::Global {
69            name,
70            interface,
71            version,
72        } = event
73        {
74            if interface == WlSeat::interface().name && version >= 2 && state.seat.is_none() {
75                let seat = registry.bind(name, 2, qh, ());
76                state.seat = Some(seat);
77            }
78
79            if state.clipboard_manager.is_none() {
80                if interface == ZwlrDataControlManagerV1::interface().name {
81                    if version == 1 {
82                        state.saw_zwlr_v1 = true;
83                    } else {
84                        let manager = registry.bind(name, 2, qh, ());
85                        state.clipboard_manager = Some(Manager::Zwlr(manager));
86                    }
87                }
88
89                if interface == ExtDataControlManagerV1::interface().name {
90                    let manager = registry.bind(name, 1, qh, ());
91                    state.clipboard_manager = Some(Manager::Ext(manager));
92                }
93            }
94        }
95    }
96}
97
98impl Dispatch<WlSeat, ()> for PrimarySelectionState {
99    fn event(
100        _state: &mut Self,
101        _proxy: &WlSeat,
102        _event: <WlSeat as Proxy>::Event,
103        _data: &(),
104        _conn: &Connection,
105        _qhandle: &wayland_client::QueueHandle<Self>,
106    ) {
107    }
108}
109
110impl_dispatch_manager!(PrimarySelectionState);
111
112impl_dispatch_device!(PrimarySelectionState, (), |state: &mut Self, event, _| {
113    if let Event::PrimarySelection { id: _ } = event {
114        state.got_primary_selection = true;
115    }
116});
117
118impl_dispatch_offer!(PrimarySelectionState);
119
120/// Errors that can occur when checking whether the primary selection is supported.
121#[derive(thiserror::Error, Debug)]
122pub enum PrimarySelectionCheckError {
123    #[error("There are no seats")]
124    NoSeats,
125
126    #[error("Couldn't open the provided Wayland socket")]
127    SocketOpenError(#[source] io::Error),
128
129    #[error("Couldn't connect to the Wayland compositor")]
130    WaylandConnection(#[source] ConnectError),
131
132    #[error("Wayland compositor communication error")]
133    WaylandCommunication(#[source] DispatchError),
134
135    #[error(
136        "A required Wayland protocol (ext-data-control, or wlr-data-control version 1) \
137         is not supported by the compositor"
138    )]
139    MissingProtocol,
140}
141
142/// Checks if the compositor supports the primary selection.
143///
144/// # Examples
145///
146/// ```no_run
147/// # extern crate wl_clipboard_rs;
148/// # fn foo() -> Result<(), Box<dyn std::error::Error>> {
149/// use wl_clipboard_rs::utils::{is_primary_selection_supported, PrimarySelectionCheckError};
150///
151/// match is_primary_selection_supported() {
152///     Ok(supported) => {
153///         // We have our definitive result. False means that ext/wlr-data-control is present
154///         // and did not signal the primary selection support, or that only wlr-data-control
155///         // version 1 is present (which does not support primary selection).
156///     },
157///     Err(PrimarySelectionCheckError::NoSeats) => {
158///         // Impossible to give a definitive result. Primary selection may or may not be
159///         // supported.
160///
161///         // The required protocol (ext-data-control, or wlr-data-control version 2) is there,
162///         // but there are no seats. Unfortunately, at least one seat is needed to check for the
163///         // primary clipboard support.
164///     },
165///     Err(PrimarySelectionCheckError::MissingProtocol) => {
166///         // The data-control protocol (required for wl-clipboard-rs operation) is not
167///         // supported by the compositor.
168///     },
169///     Err(_) => {
170///         // Some communication error occurred.
171///     }
172/// }
173/// # Ok(())
174/// # }
175/// ```
176#[inline]
177pub fn is_primary_selection_supported() -> Result<bool, PrimarySelectionCheckError> {
178    is_primary_selection_supported_internal(None)
179}
180
181pub(crate) fn is_primary_selection_supported_internal(
182    socket_name: Option<OsString>,
183) -> Result<bool, PrimarySelectionCheckError> {
184    // Connect to the Wayland compositor.
185    let conn = match socket_name {
186        Some(name) => {
187            let mut socket_path = env::var_os("XDG_RUNTIME_DIR")
188                .map(Into::<PathBuf>::into)
189                .ok_or(ConnectError::NoCompositor)
190                .map_err(PrimarySelectionCheckError::WaylandConnection)?;
191            if !socket_path.is_absolute() {
192                return Err(PrimarySelectionCheckError::WaylandConnection(
193                    ConnectError::NoCompositor,
194                ));
195            }
196            socket_path.push(name);
197
198            let stream = UnixStream::connect(socket_path)
199                .map_err(PrimarySelectionCheckError::SocketOpenError)?;
200            Connection::from_socket(stream)
201        }
202        None => Connection::connect_to_env(),
203    }
204    .map_err(PrimarySelectionCheckError::WaylandConnection)?;
205    let display = conn.display();
206
207    let mut queue = conn.new_event_queue();
208    let qh = queue.handle();
209
210    let mut state = PrimarySelectionState {
211        seat: None,
212        clipboard_manager: None,
213        saw_zwlr_v1: false,
214        got_primary_selection: false,
215    };
216
217    // Retrieve the global interfaces.
218    let _registry = display.get_registry(&qh, ());
219    queue
220        .roundtrip(&mut state)
221        .map_err(PrimarySelectionCheckError::WaylandCommunication)?;
222
223    // If data control is present but is version 1, then return false as version 1 does not support
224    // primary clipboard.
225    if state.clipboard_manager.is_none() && state.saw_zwlr_v1 {
226        return Ok(false);
227    }
228
229    // Verify that we got the clipboard manager.
230    let Some(ref clipboard_manager) = state.clipboard_manager else {
231        return Err(PrimarySelectionCheckError::MissingProtocol);
232    };
233
234    // Check if there are no seats.
235    let Some(ref seat) = state.seat else {
236        return Err(PrimarySelectionCheckError::NoSeats);
237    };
238
239    clipboard_manager.get_data_device(seat, &qh, ());
240
241    queue
242        .roundtrip(&mut state)
243        .map_err(PrimarySelectionCheckError::WaylandCommunication)?;
244
245    Ok(state.got_primary_selection)
246}