Skip to main content

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