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