/* * Copyright 2022 - 2025 Zed Industries, Inc. * License: Apache-2.0 * See LICENSE-APACHE for complete license terms * * Adapted from the x11 submodule of the arboard project https://github.com/1Password/arboard * * SPDX-License-Identifier: Apache-2.0 OR MIT * * Copyright 2022 The Arboard contributors * * The project to which this file belongs is licensed under either of * the Apache 2.0 or the MIT license at the licensee's choice. The terms * and conditions of the chosen license apply to this file.*/ // More info about using the clipboard on X11:// https://tronche.com/gui/x/icccm/sec-2.html#s-2.6// https://freedesktop.org/wiki/ClipboardManager/ use std::{ borrow::Cow, cell::RefCell, collections::{HashMap, hash_map::Entry}, sync::{ Arc, atomic::{AtomicBool, Ordering}, }, thread::JoinHandle, thread_local, time::{Duration, Instant},}; use parking_lot::{Condvar, Mutex, MutexGuard, RwLock};use x11rb::{ COPY_DEPTH_FROM_PARENT, COPY_FROM_PARENT, NONE, connection::Connection, protocol::{ Event, xproto::{ Atom, AtomEnum, ConnectionExt as _, CreateWindowAux, EventMask, PropMode, Property, PropertyNotifyEvent, SELECTION_NOTIFY_EVENT, SelectionNotifyEvent, SelectionRequestEvent, Time, WindowClass, }, }, rust_connection::RustConnection, wrapper::ConnectionExt as _,}; use gpui::{ClipboardItem, Image, ImageFormat, hash};use strum::IntoEnumIterator; type Result<T, E = Error> = std::result::Result<T, E>; static CLIPBOARD: Mutex<Option<GlobalClipboard>> = parking_lot::const_mutex(None); x11rb::atom_manager! { pub Atoms: AtomCookies { CLIPBOARD, PRIMARY, SECONDARY, CLIPBOARD_MANAGER, SAVE_TARGETS, TARGETS, ATOM, INCR, UTF8_STRING, UTF8_MIME_0: b"text/plain;charset=utf-8", UTF8_MIME_1: b"text/plain;charset=UTF-8", // Text in ISO Latin-1 encoding // See: https://tronche.com/gui/x/icccm/sec-2.html#s-2.6.2 STRING, // Text in unknown encoding // See: https://tronche.com/gui/x/icccm/sec-2.html#s-2.6.2 TEXT, TEXT_MIME_UNKNOWN: b"text/plain", // HTML: b"text/html", // URI_LIST: b"text/uri-list", PNG__MIME: ImageFormat::mime_type(ImageFormat::Png ).as_bytes(), JPEG_MIME: ImageFormat::mime_type(ImageFormat::Jpeg).as_bytes(), WEBP_MIME: ImageFormat::mime_type(ImageFormat::Webp).as_bytes(), GIF__MIME: ImageFormat::mime_type(ImageFormat::Gif ).as_bytes(), SVG__MIME: ImageFormat::mime_type(ImageFormat::Svg ).as_bytes(), BMP__MIME: ImageFormat::mime_type(ImageFormat::Bmp ).as_bytes(), TIFF_MIME: ImageFormat::mime_type(ImageFormat::Tiff).as_bytes(), ICO__MIME: ImageFormat::mime_type(ImageFormat::Ico ).as_bytes(), PNM__MIME: ImageFormat::mime_type(ImageFormat::Pnm ).as_bytes(), // This is just some random name for the property on our window, into which // the clipboard owner writes the data we requested. ARBOARD_CLIPBOARD, }} thread_local! { static ATOM_NAME_CACHE: RefCell<HashMap<Atom, &'static str>> = Default::default();} // Some clipboard items, like images, may take a very long time to produce a// `SelectionNotify`. Multiple seconds long.const LONG_TIMEOUT_DUR: Duration = Duration::from_millis(4000);const SHORT_TIMEOUT_DUR: Duration = Duration::from_millis(10); #[derive(Debug, PartialEq, Eq)]enum ManagerHandoverState { Idle, InProgress, Finished,} struct GlobalClipboard { inner: Arc<Inner>, /// Join handle to the thread which serves selection requests. server_handle: JoinHandle<()>,} struct XContext { conn: RustConnection, win_id: u32,} struct Inner { /// The context for the thread which serves clipboard read /// requests coming to us. server: XContext, atoms: Atoms, clipboard: Selection, primary: Selection, secondary: Selection, handover_state: Mutex<ManagerHandoverState>, handover_cv: Condvar, serve_stopped: AtomicBool,} impl XContext { fn new() -> Result<Self> { // create a new connection to an X11 server let (conn, screen_num): (RustConnection, _) = RustConnection::connect(None).map_err(|_| { Error::unknown("X11 server connection timed out because it was unreachable") })?; let screen = conn .setup() .roots .get(screen_num) .ok_or(Error::unknown("no screen found"))?; let win_id = conn.generate_id().map_err(into_unknown)?; let event_mask = // Just in case that some program reports SelectionNotify events // with XCB_EVENT_MASK_PROPERTY_CHANGE mask. EventMask::PROPERTY_CHANGE | // To receive DestroyNotify event and stop the message loop. EventMask::STRUCTURE_NOTIFY; // create the window conn.create_window( // copy as much as possible from the parent, because no other specific input is needed COPY_DEPTH_FROM_PARENT, win_id, screen.root, 0, 0, 1, 1, 0, WindowClass::COPY_FROM_PARENT, COPY_FROM_PARENT, // don't subscribe to any special events because we are requesting everything we need ourselves &CreateWindowAux::new().event_mask(event_mask), ) .map_err(into_unknown)?; conn.flush().map_err(into_unknown)?; Ok(Self { conn, win_id }) }} #[derive(Default)]struct Selection { data: RwLock<Option<Vec<ClipboardData>>>, /// Mutex around nothing to use with the below condvar. mutex: Mutex<()>, /// A condvar that is notified when the contents of this clipboard are changed. /// /// This is associated with `Self::mutex`. data_changed: Condvar,} #[derive(Debug, Clone)]struct ClipboardData { bytes: Vec<u8>, /// The atom representing the format in which the data is encoded. format: Atom,} enum ReadSelNotifyResult { GotData(ClipboardData), IncrStarted, EventNotRecognized,} impl Inner { fn new() -> Result<Self> { let server = XContext::new()?; let atoms = Atoms::new(&server.conn) .map_err(into_unknown)? .reply() .map_err(into_unknown)?; Ok(Self { server, atoms, clipboard: Selection::default(), primary: Selection::default(), secondary: Selection::default(), handover_state: Mutex::new(ManagerHandoverState::Idle), handover_cv: Condvar::new(), serve_stopped: AtomicBool::new(false), }) } fn write( &self, data: Vec<ClipboardData>, selection: ClipboardKind, wait: WaitConfig, ) -> Result<()> { if self.serve_stopped.load(Ordering::Relaxed) { return Err(Error::unknown( "The clipboard handler thread seems to have stopped. Logging messages may reveal the cause. (See the `log` crate.)", )); } let server_win = self.server.win_id; // ICCCM version 2, section 2.6.1.3 states that we should re-assert ownership whenever data // changes. self.server .conn .set_selection_owner(server_win, self.atom_of(selection), Time::CURRENT_TIME) .map_err(|_| Error::ClipboardOccupied)?; self.server.conn.flush().map_err(into_unknown)?; // Just setting the data, and the `serve_requests` will take care of the rest. let selection = self.selection_of(selection); let mut data_guard = selection.data.write(); *data_guard = Some(data); // Lock the mutex to both ensure that no wakers of `data_changed` can wake us between // dropping the `data_guard` and calling `wait[_for]` and that we don't we wake other // threads in that position. let mut guard = selection.mutex.lock(); // Notify any existing waiting threads that we have changed the data in the selection. // It is important that the mutex is locked to prevent this notification getting lost. selection.data_changed.notify_all(); match wait { WaitConfig::None => {} WaitConfig::Forever => { drop(data_guard); selection.data_changed.wait(&mut guard); } WaitConfig::Until(deadline) => { drop(data_guard); selection.data_changed.wait_until(&mut guard, deadline); } } Ok(()) } /// `formats` must be a slice of atoms, where each atom represents a target format. /// The first format from `formats`, which the clipboard owner supports will be the /// format of the return value. fn read(&self, formats: &[Atom], selection: ClipboardKind) -> Result<ClipboardData> { // if we are the current owner, we can get the current clipboard ourselves if self.is_owner(selection)? { let data = self.selection_of(selection).data.read(); if let Some(data_list) = &*data { for data in data_list { for format in formats { if *format == data.format { return Ok(data.clone()); } } } } return Err(Error::ContentNotAvailable); } let reader = XContext::new()?; let highest_precedence_format = match self.read_single(&reader, selection, self.atoms.TARGETS) { Err(err) => { log::trace!("Clipboard TARGETS query failed with {err:?}"); None } Ok(ClipboardData { bytes, format }) => { if format == self.atoms.ATOM { let available_formats = Self::parse_formats(&bytes); formats .iter() .find(|format| available_formats.contains(format)) } else { log::trace!( "Unexpected clipboard TARGETS format {}", self.atom_name(format) ); None } } }; if let Some(&format) = highest_precedence_format { let data = self.read_single(&reader, selection, format)?; if !formats.contains(&data.format) { // This shouldn't happen since the format is from the TARGETS list. log::trace!( "Conversion to {} responded with {} which is not supported", self.atom_name(format), self.atom_name(data.format), ); return Err(Error::ConversionFailure); } return Ok(data); } log::trace!("Falling back on attempting to convert clipboard to each format."); for format in formats { match self.read_single(&reader, selection, *format) { Ok(data) => { if formats.contains(&data.format) { return Ok(data); } else { log::trace!( "Conversion to {} responded with {} which is not supported", self.atom_name(*format), self.atom_name(data.format), ); continue; } } Err(Error::ContentNotAvailable) => { continue; } Err(e) => { log::trace!("Conversion to {} failed: {}", self.atom_name(*format), e); return Err(e); } } } log::trace!("All conversions to supported formats failed."); Err(Error::ContentNotAvailable) } fn parse_formats(bytes: &[u8]) -> Vec<Atom> { bytes .chunks_exact(4) .map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])) .collect() } fn read_single( &self, reader: &XContext, selection: ClipboardKind, target_format: Atom, ) -> Result<ClipboardData> { // Delete the property so that we can detect (using property notify) // when the selection owner receives our request. reader .conn .delete_property(reader.win_id, self.atoms.ARBOARD_CLIPBOARD) .map_err(into_unknown)?; // request to convert the clipboard selection to our data type(s) reader .conn .convert_selection( reader.win_id, self.atom_of(selection), target_format, self.atoms.ARBOARD_CLIPBOARD, Time::CURRENT_TIME, ) .map_err(into_unknown)?; reader.conn.sync().map_err(into_unknown)?; log::trace!("Finished `convert_selection`"); let mut incr_data: Vec<u8> = Vec::new(); let mut using_incr = false; let mut timeout_end = Instant::now() + LONG_TIMEOUT_DUR; while Instant::now() < timeout_end { let event = reader.conn.poll_for_event().map_err(into_unknown)?; let event = match event { Some(e) => e, None => { std::thread::sleep(Duration::from_millis(1)); continue; } }; match event { // The first response after requesting a selection. Event::SelectionNotify(event) => { log::trace!("Read SelectionNotify"); let result = self.handle_read_selection_notify( reader, target_format, &mut using_incr, &mut incr_data, event, )?; match result { ReadSelNotifyResult::GotData(data) => return Ok(data), ReadSelNotifyResult::IncrStarted => { // This means we received an indication that an the // data is going to be sent INCRementally. Let's // reset our timeout. timeout_end += SHORT_TIMEOUT_DUR; } ReadSelNotifyResult::EventNotRecognized => (), } } // If the previous SelectionNotify event specified that the data // will be sent in INCR segments, each segment is transferred in // a PropertyNotify event. Event::PropertyNotify(event) => { let result = self.handle_read_property_notify( reader, target_format, using_incr, &mut incr_data, &mut timeout_end, event, )?; if result { return Ok(ClipboardData { bytes: incr_data, format: target_format, }); } } _ => log::trace!( "An unexpected event arrived while reading the clipboard: {:?}", event ), } } log::info!("Time-out hit while reading the clipboard."); Err(Error::ContentNotAvailable) } fn atom_of(&self, selection: ClipboardKind) -> Atom { match selection { ClipboardKind::Clipboard => self.atoms.CLIPBOARD, ClipboardKind::Primary => self.atoms.PRIMARY, ClipboardKind::Secondary => self.atoms.SECONDARY, } } fn selection_of(&self, selection: ClipboardKind) -> &Selection { match selection { ClipboardKind::Clipboard => &self.clipboard, ClipboardKind::Primary => &self.primary, ClipboardKind::Secondary => &self.secondary, } } fn kind_of(&self, atom: Atom) -> Option<ClipboardKind> { match atom { a if a == self.atoms.CLIPBOARD => Some(ClipboardKind::Clipboard), a if a == self.atoms.PRIMARY => Some(ClipboardKind::Primary), a if a == self.atoms.SECONDARY => Some(ClipboardKind::Secondary), _ => None, } } fn is_owner(&self, selection: ClipboardKind) -> Result<bool> { let current = self .server .conn .get_selection_owner(self.atom_of(selection)) .map_err(into_unknown)? .reply() .map_err(into_unknown)? .owner; Lines 1–500 of 1255. Load more
/* * Copyright 2022 - 2025 Zed Industries, Inc. * License: Apache-2.0 * See LICENSE-APACHE for complete license terms * * Adapted from the x11 submodule of the arboard project https://github.com/1Password/arboard * * SPDX-License-Identifier: Apache-2.0 OR MIT * * Copyright 2022 The Arboard contributors * * The project to which this file belongs is licensed under either of * the Apache 2.0 or the MIT license at the licensee's choice. The terms * and conditions of the chosen license apply to this file. */ // More info about using the clipboard on X11: // https://tronche.com/gui/x/icccm/sec-2.html#s-2.6 // https://freedesktop.org/wiki/ClipboardManager/ use std::{ borrow::Cow, cell::RefCell, collections::{HashMap, hash_map::Entry}, sync::{ Arc, atomic::{AtomicBool, Ordering}, }, thread::JoinHandle, thread_local, time::{Duration, Instant}, }; use parking_lot::{Condvar, Mutex, MutexGuard, RwLock}; use x11rb::{ COPY_DEPTH_FROM_PARENT, COPY_FROM_PARENT, NONE, connection::Connection, protocol::{ Event, xproto::{ Atom, AtomEnum, ConnectionExt as _, CreateWindowAux, EventMask, PropMode, Property, PropertyNotifyEvent, SELECTION_NOTIFY_EVENT, SelectionNotifyEvent, SelectionRequestEvent, Time, WindowClass, }, }, rust_connection::RustConnection, wrapper::ConnectionExt as _, }; use gpui::{ClipboardItem, Image, ImageFormat, hash}; use strum::IntoEnumIterator; type Result<T, E = Error> = std::result::Result<T, E>; static CLIPBOARD: Mutex<Option<GlobalClipboard>> = parking_lot::const_mutex(None); x11rb::atom_manager! { pub Atoms: AtomCookies { CLIPBOARD, PRIMARY, SECONDARY, CLIPBOARD_MANAGER, SAVE_TARGETS, TARGETS, ATOM, INCR, UTF8_STRING, UTF8_MIME_0: b"text/plain;charset=utf-8", UTF8_MIME_1: b"text/plain;charset=UTF-8", // Text in ISO Latin-1 encoding // See: https://tronche.com/gui/x/icccm/sec-2.html#s-2.6.2 STRING, // Text in unknown encoding // See: https://tronche.com/gui/x/icccm/sec-2.html#s-2.6.2 TEXT, TEXT_MIME_UNKNOWN: b"text/plain", // HTML: b"text/html", // URI_LIST: b"text/uri-list", PNG__MIME: ImageFormat::mime_type(ImageFormat::Png ).as_bytes(), JPEG_MIME: ImageFormat::mime_type(ImageFormat::Jpeg).as_bytes(), WEBP_MIME: ImageFormat::mime_type(ImageFormat::Webp).as_bytes(), GIF__MIME: ImageFormat::mime_type(ImageFormat::Gif ).as_bytes(), SVG__MIME: ImageFormat::mime_type(ImageFormat::Svg ).as_bytes(), BMP__MIME: ImageFormat::mime_type(ImageFormat::Bmp ).as_bytes(), TIFF_MIME: ImageFormat::mime_type(ImageFormat::Tiff).as_bytes(), ICO__MIME: ImageFormat::mime_type(ImageFormat::Ico ).as_bytes(), PNM__MIME: ImageFormat::mime_type(ImageFormat::Pnm ).as_bytes(), // This is just some random name for the property on our window, into which // the clipboard owner writes the data we requested. ARBOARD_CLIPBOARD, } } thread_local! { static ATOM_NAME_CACHE: RefCell<HashMap<Atom, &'static str>> = Default::default(); } // Some clipboard items, like images, may take a very long time to produce a // `SelectionNotify`. Multiple seconds long. const LONG_TIMEOUT_DUR: Duration = Duration::from_millis(4000); const SHORT_TIMEOUT_DUR: Duration = Duration::from_millis(10); #[derive(Debug, PartialEq, Eq)] enum ManagerHandoverState { Idle, InProgress, Finished, } struct GlobalClipboard { inner: Arc<Inner>, /// Join handle to the thread which serves selection requests. server_handle: JoinHandle<()>, } struct XContext { conn: RustConnection, win_id: u32, } struct Inner { /// The context for the thread which serves clipboard read /// requests coming to us. server: XContext, atoms: Atoms, clipboard: Selection, primary: Selection, secondary: Selection, handover_state: Mutex<ManagerHandoverState>, handover_cv: Condvar, serve_stopped: AtomicBool, } impl XContext { fn new() -> Result<Self> { // create a new connection to an X11 server let (conn, screen_num): (RustConnection, _) = RustConnection::connect(None).map_err(|_| { Error::unknown("X11 server connection timed out because it was unreachable") })?; let screen = conn .setup() .roots .get(screen_num) .ok_or(Error::unknown("no screen found"))?; let win_id = conn.generate_id().map_err(into_unknown)?; let event_mask = // Just in case that some program reports SelectionNotify events // with XCB_EVENT_MASK_PROPERTY_CHANGE mask. EventMask::PROPERTY_CHANGE | // To receive DestroyNotify event and stop the message loop. EventMask::STRUCTURE_NOTIFY; // create the window conn.create_window( // copy as much as possible from the parent, because no other specific input is needed COPY_DEPTH_FROM_PARENT, win_id, screen.root, 0, 0, 1, 1, 0, WindowClass::COPY_FROM_PARENT, COPY_FROM_PARENT, // don't subscribe to any special events because we are requesting everything we need ourselves &CreateWindowAux::new().event_mask(event_mask), ) .map_err(into_unknown)?; conn.flush().map_err(into_unknown)?; Ok(Self { conn, win_id }) } } #[derive(Default)] struct Selection { data: RwLock<Option<Vec<ClipboardData>>>, /// Mutex around nothing to use with the below condvar. mutex: Mutex<()>, /// A condvar that is notified when the contents of this clipboard are changed. /// /// This is associated with `Self::mutex`. data_changed: Condvar, } #[derive(Debug, Clone)] struct ClipboardData { bytes: Vec<u8>, /// The atom representing the format in which the data is encoded. format: Atom, } enum ReadSelNotifyResult { GotData(ClipboardData), IncrStarted, EventNotRecognized, } impl Inner { fn new() -> Result<Self> { let server = XContext::new()?; let atoms = Atoms::new(&server.conn) .map_err(into_unknown)? .reply() .map_err(into_unknown)?; Ok(Self { server, atoms, clipboard: Selection::default(), primary: Selection::default(), secondary: Selection::default(), handover_state: Mutex::new(ManagerHandoverState::Idle), handover_cv: Condvar::new(), serve_stopped: AtomicBool::new(false), }) } fn write( &self, data: Vec<ClipboardData>, selection: ClipboardKind, wait: WaitConfig, ) -> Result<()> { if self.serve_stopped.load(Ordering::Relaxed) { return Err(Error::unknown( "The clipboard handler thread seems to have stopped. Logging messages may reveal the cause. (See the `log` crate.)", )); } let server_win = self.server.win_id; // ICCCM version 2, section 2.6.1.3 states that we should re-assert ownership whenever data // changes. self.server .conn .set_selection_owner(server_win, self.atom_of(selection), Time::CURRENT_TIME) .map_err(|_| Error::ClipboardOccupied)?; self.server.conn.flush().map_err(into_unknown)?; // Just setting the data, and the `serve_requests` will take care of the rest. let selection = self.selection_of(selection); let mut data_guard = selection.data.write(); *data_guard = Some(data); // Lock the mutex to both ensure that no wakers of `data_changed` can wake us between // dropping the `data_guard` and calling `wait[_for]` and that we don't we wake other // threads in that position. let mut guard = selection.mutex.lock(); // Notify any existing waiting threads that we have changed the data in the selection. // It is important that the mutex is locked to prevent this notification getting lost. selection.data_changed.notify_all(); match wait { WaitConfig::None => {} WaitConfig::Forever => { drop(data_guard); selection.data_changed.wait(&mut guard); } WaitConfig::Until(deadline) => { drop(data_guard); selection.data_changed.wait_until(&mut guard, deadline); } } Ok(()) } /// `formats` must be a slice of atoms, where each atom represents a target format. /// The first format from `formats`, which the clipboard owner supports will be the /// format of the return value. fn read(&self, formats: &[Atom], selection: ClipboardKind) -> Result<ClipboardData> { // if we are the current owner, we can get the current clipboard ourselves if self.is_owner(selection)? { let data = self.selection_of(selection).data.read(); if let Some(data_list) = &*data { for data in data_list { for format in formats { if *format == data.format { return Ok(data.clone()); } } } } return Err(Error::ContentNotAvailable); } let reader = XContext::new()?; let highest_precedence_format = match self.read_single(&reader, selection, self.atoms.TARGETS) { Err(err) => { log::trace!("Clipboard TARGETS query failed with {err:?}"); None } Ok(ClipboardData { bytes, format }) => { if format == self.atoms.ATOM { let available_formats = Self::parse_formats(&bytes); formats .iter() .find(|format| available_formats.contains(format)) } else { log::trace!( "Unexpected clipboard TARGETS format {}", self.atom_name(format) ); None } } }; if let Some(&format) = highest_precedence_format { let data = self.read_single(&reader, selection, format)?; if !formats.contains(&data.format) { // This shouldn't happen since the format is from the TARGETS list. log::trace!( "Conversion to {} responded with {} which is not supported", self.atom_name(format), self.atom_name(data.format), ); return Err(Error::ConversionFailure); } return Ok(data); } log::trace!("Falling back on attempting to convert clipboard to each format."); for format in formats { match self.read_single(&reader, selection, *format) { Ok(data) => { if formats.contains(&data.format) { return Ok(data); } else { log::trace!( "Conversion to {} responded with {} which is not supported", self.atom_name(*format), self.atom_name(data.format), ); continue; } } Err(Error::ContentNotAvailable) => { continue; } Err(e) => { log::trace!("Conversion to {} failed: {}", self.atom_name(*format), e); return Err(e); } } } log::trace!("All conversions to supported formats failed."); Err(Error::ContentNotAvailable) } fn parse_formats(bytes: &[u8]) -> Vec<Atom> { bytes .chunks_exact(4) .map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])) .collect() } fn read_single( &self, reader: &XContext, selection: ClipboardKind, target_format: Atom, ) -> Result<ClipboardData> { // Delete the property so that we can detect (using property notify) // when the selection owner receives our request. reader .conn .delete_property(reader.win_id, self.atoms.ARBOARD_CLIPBOARD) .map_err(into_unknown)?; // request to convert the clipboard selection to our data type(s) reader .conn .convert_selection( reader.win_id, self.atom_of(selection), target_format, self.atoms.ARBOARD_CLIPBOARD, Time::CURRENT_TIME, ) .map_err(into_unknown)?; reader.conn.sync().map_err(into_unknown)?; log::trace!("Finished `convert_selection`"); let mut incr_data: Vec<u8> = Vec::new(); let mut using_incr = false; let mut timeout_end = Instant::now() + LONG_TIMEOUT_DUR; while Instant::now() < timeout_end { let event = reader.conn.poll_for_event().map_err(into_unknown)?; let event = match event { Some(e) => e, None => { std::thread::sleep(Duration::from_millis(1)); continue; } }; match event { // The first response after requesting a selection. Event::SelectionNotify(event) => { log::trace!("Read SelectionNotify"); let result = self.handle_read_selection_notify( reader, target_format, &mut using_incr, &mut incr_data, event, )?; match result { ReadSelNotifyResult::GotData(data) => return Ok(data), ReadSelNotifyResult::IncrStarted => { // This means we received an indication that an the // data is going to be sent INCRementally. Let's // reset our timeout. timeout_end += SHORT_TIMEOUT_DUR; } ReadSelNotifyResult::EventNotRecognized => (), } } // If the previous SelectionNotify event specified that the data // will be sent in INCR segments, each segment is transferred in // a PropertyNotify event. Event::PropertyNotify(event) => { let result = self.handle_read_property_notify( reader, target_format, using_incr, &mut incr_data, &mut timeout_end, event, )?; if result { return Ok(ClipboardData { bytes: incr_data, format: target_format, }); } } _ => log::trace!( "An unexpected event arrived while reading the clipboard: {:?}", event ), } } log::info!("Time-out hit while reading the clipboard."); Err(Error::ContentNotAvailable) } fn atom_of(&self, selection: ClipboardKind) -> Atom { match selection { ClipboardKind::Clipboard => self.atoms.CLIPBOARD, ClipboardKind::Primary => self.atoms.PRIMARY, ClipboardKind::Secondary => self.atoms.SECONDARY, } } fn selection_of(&self, selection: ClipboardKind) -> &Selection { match selection { ClipboardKind::Clipboard => &self.clipboard, ClipboardKind::Primary => &self.primary, ClipboardKind::Secondary => &self.secondary, } } fn kind_of(&self, atom: Atom) -> Option<ClipboardKind> { match atom { a if a == self.atoms.CLIPBOARD => Some(ClipboardKind::Clipboard), a if a == self.atoms.PRIMARY => Some(ClipboardKind::Primary), a if a == self.atoms.SECONDARY => Some(ClipboardKind::Secondary), _ => None, } } fn is_owner(&self, selection: ClipboardKind) -> Result<bool> { let current = self .server .conn .get_selection_owner(self.atom_of(selection)) .map_err(into_unknown)? .reply() .map_err(into_unknown)? .owner;