clipboard.rs

   1/*
   2 * Copyright 2022 - 2025 Zed Industries, Inc.
   3 * License: Apache-2.0
   4 * See LICENSE-APACHE for complete license terms
   5 *
   6 * Adapted from the x11 submodule of the arboard project https://github.com/1Password/arboard
   7 *
   8 * SPDX-License-Identifier: Apache-2.0 OR MIT
   9 *
  10 * Copyright 2022 The Arboard contributors
  11 *
  12 * The project to which this file belongs is licensed under either of
  13 * the Apache 2.0 or the MIT license at the licensee's choice. The terms
  14 * and conditions of the chosen license apply to this file.
  15*/
  16
  17// More info about using the clipboard on X11:
  18// https://tronche.com/gui/x/icccm/sec-2.html#s-2.6
  19// https://freedesktop.org/wiki/ClipboardManager/
  20
  21use std::{
  22    borrow::Cow,
  23    cell::RefCell,
  24    collections::{HashMap, hash_map::Entry},
  25    sync::{
  26        Arc,
  27        atomic::{AtomicBool, Ordering},
  28    },
  29    thread::JoinHandle,
  30    thread_local,
  31    time::{Duration, Instant},
  32};
  33
  34use parking_lot::{Condvar, Mutex, MutexGuard, RwLock};
  35use x11rb::{
  36    COPY_DEPTH_FROM_PARENT, COPY_FROM_PARENT, NONE,
  37    connection::Connection,
  38    protocol::{
  39        Event,
  40        xproto::{
  41            Atom, AtomEnum, ConnectionExt as _, CreateWindowAux, EventMask, PropMode, Property,
  42            PropertyNotifyEvent, SELECTION_NOTIFY_EVENT, SelectionNotifyEvent,
  43            SelectionRequestEvent, Time, WindowClass,
  44        },
  45    },
  46    rust_connection::RustConnection,
  47    wrapper::ConnectionExt as _,
  48};
  49
  50use crate::{ClipboardItem, Image, ImageFormat, hash};
  51
  52type Result<T, E = Error> = std::result::Result<T, E>;
  53
  54static CLIPBOARD: Mutex<Option<GlobalClipboard>> = parking_lot::const_mutex(None);
  55
  56x11rb::atom_manager! {
  57    pub Atoms: AtomCookies {
  58        CLIPBOARD,
  59        PRIMARY,
  60        SECONDARY,
  61
  62        CLIPBOARD_MANAGER,
  63        SAVE_TARGETS,
  64        TARGETS,
  65        ATOM,
  66        INCR,
  67
  68        UTF8_STRING,
  69        UTF8_MIME_0: b"text/plain;charset=utf-8",
  70        UTF8_MIME_1: b"text/plain;charset=UTF-8",
  71        // Text in ISO Latin-1 encoding
  72        // See: https://tronche.com/gui/x/icccm/sec-2.html#s-2.6.2
  73        STRING,
  74        // Text in unknown encoding
  75        // See: https://tronche.com/gui/x/icccm/sec-2.html#s-2.6.2
  76        TEXT,
  77        TEXT_MIME_UNKNOWN: b"text/plain",
  78
  79        // HTML: b"text/html",
  80        // URI_LIST: b"text/uri-list",
  81
  82        PNG__MIME: ImageFormat::mime_type(ImageFormat::Png ).as_bytes(),
  83        JPEG_MIME: ImageFormat::mime_type(ImageFormat::Jpeg).as_bytes(),
  84        WEBP_MIME: ImageFormat::mime_type(ImageFormat::Webp).as_bytes(),
  85        GIF__MIME: ImageFormat::mime_type(ImageFormat::Gif ).as_bytes(),
  86        SVG__MIME: ImageFormat::mime_type(ImageFormat::Svg ).as_bytes(),
  87        BMP__MIME: ImageFormat::mime_type(ImageFormat::Bmp ).as_bytes(),
  88        TIFF_MIME: ImageFormat::mime_type(ImageFormat::Tiff).as_bytes(),
  89
  90        // This is just some random name for the property on our window, into which
  91        // the clipboard owner writes the data we requested.
  92        ARBOARD_CLIPBOARD,
  93    }
  94}
  95
  96thread_local! {
  97    static ATOM_NAME_CACHE: RefCell<HashMap<Atom, &'static str>> = Default::default();
  98}
  99
 100// Some clipboard items, like images, may take a very long time to produce a
 101// `SelectionNotify`. Multiple seconds long.
 102const LONG_TIMEOUT_DUR: Duration = Duration::from_millis(4000);
 103const SHORT_TIMEOUT_DUR: Duration = Duration::from_millis(10);
 104
 105#[derive(Debug, PartialEq, Eq)]
 106enum ManagerHandoverState {
 107    Idle,
 108    InProgress,
 109    Finished,
 110}
 111
 112struct GlobalClipboard {
 113    inner: Arc<Inner>,
 114
 115    /// Join handle to the thread which serves selection requests.
 116    server_handle: JoinHandle<()>,
 117}
 118
 119struct XContext {
 120    conn: RustConnection,
 121    win_id: u32,
 122}
 123
 124struct Inner {
 125    /// The context for the thread which serves clipboard read
 126    /// requests coming to us.
 127    server: XContext,
 128    atoms: Atoms,
 129
 130    clipboard: Selection,
 131    primary: Selection,
 132    secondary: Selection,
 133
 134    handover_state: Mutex<ManagerHandoverState>,
 135    handover_cv: Condvar,
 136
 137    serve_stopped: AtomicBool,
 138}
 139
 140impl XContext {
 141    fn new() -> Result<Self> {
 142        // create a new connection to an X11 server
 143        let (conn, screen_num): (RustConnection, _) =
 144            RustConnection::connect(None).map_err(|_| {
 145                Error::unknown("X11 server connection timed out because it was unreachable")
 146            })?;
 147        let screen = conn
 148            .setup()
 149            .roots
 150            .get(screen_num)
 151            .ok_or(Error::unknown("no screen found"))?;
 152        let win_id = conn.generate_id().map_err(into_unknown)?;
 153
 154        let event_mask =
 155            // Just in case that some program reports SelectionNotify events
 156            // with XCB_EVENT_MASK_PROPERTY_CHANGE mask.
 157            EventMask::PROPERTY_CHANGE |
 158            // To receive DestroyNotify event and stop the message loop.
 159            EventMask::STRUCTURE_NOTIFY;
 160        // create the window
 161        conn.create_window(
 162            // copy as much as possible from the parent, because no other specific input is needed
 163            COPY_DEPTH_FROM_PARENT,
 164            win_id,
 165            screen.root,
 166            0,
 167            0,
 168            1,
 169            1,
 170            0,
 171            WindowClass::COPY_FROM_PARENT,
 172            COPY_FROM_PARENT,
 173            // don't subscribe to any special events because we are requesting everything we need ourselves
 174            &CreateWindowAux::new().event_mask(event_mask),
 175        )
 176        .map_err(into_unknown)?;
 177        conn.flush().map_err(into_unknown)?;
 178
 179        Ok(Self { conn, win_id })
 180    }
 181}
 182
 183#[derive(Default)]
 184struct Selection {
 185    data: RwLock<Option<Vec<ClipboardData>>>,
 186    /// Mutex around nothing to use with the below condvar.
 187    mutex: Mutex<()>,
 188    /// A condvar that is notified when the contents of this clipboard are changed.
 189    ///
 190    /// This is associated with `Self::mutex`.
 191    data_changed: Condvar,
 192}
 193
 194#[derive(Debug, Clone)]
 195struct ClipboardData {
 196    bytes: Vec<u8>,
 197
 198    /// The atom representing the format in which the data is encoded.
 199    format: Atom,
 200}
 201
 202enum ReadSelNotifyResult {
 203    GotData(ClipboardData),
 204    IncrStarted,
 205    EventNotRecognized,
 206}
 207
 208impl Inner {
 209    fn new() -> Result<Self> {
 210        let server = XContext::new()?;
 211        let atoms = Atoms::new(&server.conn)
 212            .map_err(into_unknown)?
 213            .reply()
 214            .map_err(into_unknown)?;
 215
 216        Ok(Self {
 217            server,
 218            atoms,
 219            clipboard: Selection::default(),
 220            primary: Selection::default(),
 221            secondary: Selection::default(),
 222            handover_state: Mutex::new(ManagerHandoverState::Idle),
 223            handover_cv: Condvar::new(),
 224            serve_stopped: AtomicBool::new(false),
 225        })
 226    }
 227
 228    fn write(
 229        &self,
 230        data: Vec<ClipboardData>,
 231        selection: ClipboardKind,
 232        wait: WaitConfig,
 233    ) -> Result<()> {
 234        if self.serve_stopped.load(Ordering::Relaxed) {
 235            return Err(Error::unknown(
 236                "The clipboard handler thread seems to have stopped. Logging messages may reveal the cause. (See the `log` crate.)",
 237            ));
 238        }
 239
 240        let server_win = self.server.win_id;
 241
 242        // ICCCM version 2, section 2.6.1.3 states that we should re-assert ownership whenever data
 243        // changes.
 244        self.server
 245            .conn
 246            .set_selection_owner(server_win, self.atom_of(selection), Time::CURRENT_TIME)
 247            .map_err(|_| Error::ClipboardOccupied)?;
 248
 249        self.server.conn.flush().map_err(into_unknown)?;
 250
 251        // Just setting the data, and the `serve_requests` will take care of the rest.
 252        let selection = self.selection_of(selection);
 253        let mut data_guard = selection.data.write();
 254        *data_guard = Some(data);
 255
 256        // Lock the mutex to both ensure that no wakers of `data_changed` can wake us between
 257        // dropping the `data_guard` and calling `wait[_for]` and that we don't we wake other
 258        // threads in that position.
 259        let mut guard = selection.mutex.lock();
 260
 261        // Notify any existing waiting threads that we have changed the data in the selection.
 262        // It is important that the mutex is locked to prevent this notification getting lost.
 263        selection.data_changed.notify_all();
 264
 265        match wait {
 266            WaitConfig::None => {}
 267            WaitConfig::Forever => {
 268                drop(data_guard);
 269                selection.data_changed.wait(&mut guard);
 270            }
 271            WaitConfig::Until(deadline) => {
 272                drop(data_guard);
 273                selection.data_changed.wait_until(&mut guard, deadline);
 274            }
 275        }
 276
 277        Ok(())
 278    }
 279
 280    /// `formats` must be a slice of atoms, where each atom represents a target format.
 281    /// The first format from `formats`, which the clipboard owner supports will be the
 282    /// format of the return value.
 283    fn read(&self, formats: &[Atom], selection: ClipboardKind) -> Result<ClipboardData> {
 284        // if we are the current owner, we can get the current clipboard ourselves
 285        if self.is_owner(selection)? {
 286            let data = self.selection_of(selection).data.read();
 287            if let Some(data_list) = &*data {
 288                for data in data_list {
 289                    for format in formats {
 290                        if *format == data.format {
 291                            return Ok(data.clone());
 292                        }
 293                    }
 294                }
 295            }
 296            return Err(Error::ContentNotAvailable);
 297        }
 298        let reader = XContext::new()?;
 299
 300        let highest_precedence_format =
 301            match self.read_single(&reader, selection, self.atoms.TARGETS) {
 302                Err(err) => {
 303                    log::trace!("Clipboard TARGETS query failed with {err:?}");
 304                    None
 305                }
 306                Ok(ClipboardData { bytes, format }) => {
 307                    if format == self.atoms.ATOM {
 308                        let available_formats = Self::parse_formats(&bytes);
 309                        formats
 310                            .iter()
 311                            .find(|format| available_formats.contains(format))
 312                    } else {
 313                        log::trace!(
 314                            "Unexpected clipboard TARGETS format {}",
 315                            self.atom_name(format)
 316                        );
 317                        None
 318                    }
 319                }
 320            };
 321
 322        if let Some(&format) = highest_precedence_format {
 323            let data = self.read_single(&reader, selection, format)?;
 324            if !formats.contains(&data.format) {
 325                // This shouldn't happen since the format is from the TARGETS list.
 326                log::trace!(
 327                    "Conversion to {} responded with {} which is not supported",
 328                    self.atom_name(format),
 329                    self.atom_name(data.format),
 330                );
 331                return Err(Error::ConversionFailure);
 332            }
 333            return Ok(data);
 334        }
 335
 336        log::trace!("Falling back on attempting to convert clipboard to each format.");
 337        for format in formats {
 338            match self.read_single(&reader, selection, *format) {
 339                Ok(data) => {
 340                    if formats.contains(&data.format) {
 341                        return Ok(data);
 342                    } else {
 343                        log::trace!(
 344                            "Conversion to {} responded with {} which is not supported",
 345                            self.atom_name(*format),
 346                            self.atom_name(data.format),
 347                        );
 348                        continue;
 349                    }
 350                }
 351                Err(Error::ContentNotAvailable) => {
 352                    continue;
 353                }
 354                Err(e) => {
 355                    log::trace!("Conversion to {} failed: {}", self.atom_name(*format), e);
 356                    return Err(e);
 357                }
 358            }
 359        }
 360        log::trace!("All conversions to supported formats failed.");
 361        Err(Error::ContentNotAvailable)
 362    }
 363
 364    fn parse_formats(bytes: &[u8]) -> Vec<Atom> {
 365        bytes
 366            .chunks_exact(4)
 367            .map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
 368            .collect()
 369    }
 370
 371    fn read_single(
 372        &self,
 373        reader: &XContext,
 374        selection: ClipboardKind,
 375        target_format: Atom,
 376    ) -> Result<ClipboardData> {
 377        // Delete the property so that we can detect (using property notify)
 378        // when the selection owner receives our request.
 379        reader
 380            .conn
 381            .delete_property(reader.win_id, self.atoms.ARBOARD_CLIPBOARD)
 382            .map_err(into_unknown)?;
 383
 384        // request to convert the clipboard selection to our data type(s)
 385        reader
 386            .conn
 387            .convert_selection(
 388                reader.win_id,
 389                self.atom_of(selection),
 390                target_format,
 391                self.atoms.ARBOARD_CLIPBOARD,
 392                Time::CURRENT_TIME,
 393            )
 394            .map_err(into_unknown)?;
 395        reader.conn.sync().map_err(into_unknown)?;
 396
 397        log::trace!("Finished `convert_selection`");
 398
 399        let mut incr_data: Vec<u8> = Vec::new();
 400        let mut using_incr = false;
 401
 402        let mut timeout_end = Instant::now() + LONG_TIMEOUT_DUR;
 403
 404        while Instant::now() < timeout_end {
 405            let event = reader.conn.poll_for_event().map_err(into_unknown)?;
 406            let event = match event {
 407                Some(e) => e,
 408                None => {
 409                    std::thread::sleep(Duration::from_millis(1));
 410                    continue;
 411                }
 412            };
 413            match event {
 414                // The first response after requesting a selection.
 415                Event::SelectionNotify(event) => {
 416                    log::trace!("Read SelectionNotify");
 417                    let result = self.handle_read_selection_notify(
 418                        reader,
 419                        target_format,
 420                        &mut using_incr,
 421                        &mut incr_data,
 422                        event,
 423                    )?;
 424                    match result {
 425                        ReadSelNotifyResult::GotData(data) => return Ok(data),
 426                        ReadSelNotifyResult::IncrStarted => {
 427                            // This means we received an indication that an the
 428                            // data is going to be sent INCRementally. Let's
 429                            // reset our timeout.
 430                            timeout_end += SHORT_TIMEOUT_DUR;
 431                        }
 432                        ReadSelNotifyResult::EventNotRecognized => (),
 433                    }
 434                }
 435                // If the previous SelectionNotify event specified that the data
 436                // will be sent in INCR segments, each segment is transferred in
 437                // a PropertyNotify event.
 438                Event::PropertyNotify(event) => {
 439                    let result = self.handle_read_property_notify(
 440                        reader,
 441                        target_format,
 442                        using_incr,
 443                        &mut incr_data,
 444                        &mut timeout_end,
 445                        event,
 446                    )?;
 447                    if result {
 448                        return Ok(ClipboardData {
 449                            bytes: incr_data,
 450                            format: target_format,
 451                        });
 452                    }
 453                }
 454                _ => log::trace!(
 455                    "An unexpected event arrived while reading the clipboard: {:?}",
 456                    event
 457                ),
 458            }
 459        }
 460        log::info!("Time-out hit while reading the clipboard.");
 461        Err(Error::ContentNotAvailable)
 462    }
 463
 464    fn atom_of(&self, selection: ClipboardKind) -> Atom {
 465        match selection {
 466            ClipboardKind::Clipboard => self.atoms.CLIPBOARD,
 467            ClipboardKind::Primary => self.atoms.PRIMARY,
 468            ClipboardKind::Secondary => self.atoms.SECONDARY,
 469        }
 470    }
 471
 472    fn selection_of(&self, selection: ClipboardKind) -> &Selection {
 473        match selection {
 474            ClipboardKind::Clipboard => &self.clipboard,
 475            ClipboardKind::Primary => &self.primary,
 476            ClipboardKind::Secondary => &self.secondary,
 477        }
 478    }
 479
 480    fn kind_of(&self, atom: Atom) -> Option<ClipboardKind> {
 481        match atom {
 482            a if a == self.atoms.CLIPBOARD => Some(ClipboardKind::Clipboard),
 483            a if a == self.atoms.PRIMARY => Some(ClipboardKind::Primary),
 484            a if a == self.atoms.SECONDARY => Some(ClipboardKind::Secondary),
 485            _ => None,
 486        }
 487    }
 488
 489    fn is_owner(&self, selection: ClipboardKind) -> Result<bool> {
 490        let current = self
 491            .server
 492            .conn
 493            .get_selection_owner(self.atom_of(selection))
 494            .map_err(into_unknown)?
 495            .reply()
 496            .map_err(into_unknown)?
 497            .owner;
 498
 499        Ok(current == self.server.win_id)
 500    }
 501
 502    fn query_atom_name(&self, atom: x11rb::protocol::xproto::Atom) -> Result<String> {
 503        String::from_utf8(
 504            self.server
 505                .conn
 506                .get_atom_name(atom)
 507                .map_err(into_unknown)?
 508                .reply()
 509                .map_err(into_unknown)?
 510                .name,
 511        )
 512        .map_err(into_unknown)
 513    }
 514
 515    fn atom_name(&self, atom: x11rb::protocol::xproto::Atom) -> &'static str {
 516        ATOM_NAME_CACHE.with(|cache| {
 517            let mut cache = cache.borrow_mut();
 518            match cache.entry(atom) {
 519                Entry::Occupied(entry) => *entry.get(),
 520                Entry::Vacant(entry) => {
 521                    let s = self
 522                        .query_atom_name(atom)
 523                        .map(|s| Box::leak(s.into_boxed_str()) as &str)
 524                        .unwrap_or("FAILED-TO-GET-THE-ATOM-NAME");
 525                    entry.insert(s);
 526                    s
 527                }
 528            }
 529        })
 530    }
 531
 532    fn handle_read_selection_notify(
 533        &self,
 534        reader: &XContext,
 535        target_format: u32,
 536        using_incr: &mut bool,
 537        incr_data: &mut Vec<u8>,
 538        event: SelectionNotifyEvent,
 539    ) -> Result<ReadSelNotifyResult> {
 540        // The property being set to NONE means that the `convert_selection`
 541        // failed.
 542
 543        // According to: https://tronche.com/gui/x/icccm/sec-2.html#s-2.4
 544        // the target must be set to the same as what we requested.
 545        if event.property == NONE || event.target != target_format {
 546            return Err(Error::ContentNotAvailable);
 547        }
 548        if self.kind_of(event.selection).is_none() {
 549            log::info!(
 550                "Received a SelectionNotify for a selection other than CLIPBOARD, PRIMARY or SECONDARY. This is unexpected."
 551            );
 552            return Ok(ReadSelNotifyResult::EventNotRecognized);
 553        }
 554        if *using_incr {
 555            log::warn!("Received a SelectionNotify while already expecting INCR segments.");
 556            return Ok(ReadSelNotifyResult::EventNotRecognized);
 557        }
 558        // Accept any property type. The property type will typically match the format type except
 559        // when it is `TARGETS` in which case it is `ATOM`. `ANY` is provided to handle the case
 560        // where the clipboard is not convertible to the requested format. In this case
 561        // `reply.type_` will have format information, but `bytes` will only be non-empty if `ANY`
 562        // is provided.
 563        let property_type = AtomEnum::ANY;
 564        // request the selection
 565        let mut reply = reader
 566            .conn
 567            .get_property(
 568                true,
 569                event.requestor,
 570                event.property,
 571                property_type,
 572                0,
 573                u32::MAX / 4,
 574            )
 575            .map_err(into_unknown)?
 576            .reply()
 577            .map_err(into_unknown)?;
 578
 579        // we found something
 580        if reply.type_ == self.atoms.INCR {
 581            // Note that we call the get_property again because we are
 582            // indicating that we are ready to receive the data by deleting the
 583            // property, however deleting only works if the type matches the
 584            // property type. But the type didn't match in the previous call.
 585            reply = reader
 586                .conn
 587                .get_property(
 588                    true,
 589                    event.requestor,
 590                    event.property,
 591                    self.atoms.INCR,
 592                    0,
 593                    u32::MAX / 4,
 594                )
 595                .map_err(into_unknown)?
 596                .reply()
 597                .map_err(into_unknown)?;
 598            log::trace!("Receiving INCR segments");
 599            *using_incr = true;
 600            if reply.value_len == 4 {
 601                let min_data_len = reply
 602                    .value32()
 603                    .and_then(|mut vals| vals.next())
 604                    .unwrap_or(0);
 605                incr_data.reserve(min_data_len as usize);
 606            }
 607            Ok(ReadSelNotifyResult::IncrStarted)
 608        } else {
 609            Ok(ReadSelNotifyResult::GotData(ClipboardData {
 610                bytes: reply.value,
 611                format: reply.type_,
 612            }))
 613        }
 614    }
 615
 616    /// Returns Ok(true) when the incr_data is ready
 617    fn handle_read_property_notify(
 618        &self,
 619        reader: &XContext,
 620        target_format: u32,
 621        using_incr: bool,
 622        incr_data: &mut Vec<u8>,
 623        timeout_end: &mut Instant,
 624        event: PropertyNotifyEvent,
 625    ) -> Result<bool> {
 626        if event.atom != self.atoms.ARBOARD_CLIPBOARD || event.state != Property::NEW_VALUE {
 627            return Ok(false);
 628        }
 629        if !using_incr {
 630            // This must mean the selection owner received our request, and is
 631            // now preparing the data
 632            return Ok(false);
 633        }
 634        let reply = reader
 635            .conn
 636            .get_property(
 637                true,
 638                event.window,
 639                event.atom,
 640                if target_format == self.atoms.TARGETS {
 641                    self.atoms.ATOM
 642                } else {
 643                    target_format
 644                },
 645                0,
 646                u32::MAX / 4,
 647            )
 648            .map_err(into_unknown)?
 649            .reply()
 650            .map_err(into_unknown)?;
 651
 652        // log::trace!("Received segment. value_len {}", reply.value_len,);
 653        if reply.value_len == 0 {
 654            // This indicates that all the data has been sent.
 655            return Ok(true);
 656        }
 657        incr_data.extend(reply.value);
 658
 659        // Let's reset our timeout, since we received a valid chunk.
 660        *timeout_end = Instant::now() + SHORT_TIMEOUT_DUR;
 661
 662        // Not yet complete
 663        Ok(false)
 664    }
 665
 666    fn handle_selection_request(&self, event: SelectionRequestEvent) -> Result<()> {
 667        let selection = match self.kind_of(event.selection) {
 668            Some(kind) => kind,
 669            None => {
 670                log::warn!(
 671                    "Received a selection request to a selection other than the CLIPBOARD, PRIMARY or SECONDARY. This is unexpected."
 672                );
 673                return Ok(());
 674            }
 675        };
 676
 677        let success;
 678        // we are asked for a list of supported conversion targets
 679        if event.target == self.atoms.TARGETS {
 680            log::trace!(
 681                "Handling TARGETS, dst property is {}",
 682                self.atom_name(event.property)
 683            );
 684            let mut targets = Vec::with_capacity(10);
 685            targets.push(self.atoms.TARGETS);
 686            targets.push(self.atoms.SAVE_TARGETS);
 687            let data = self.selection_of(selection).data.read();
 688            if let Some(data_list) = &*data {
 689                for data in data_list {
 690                    targets.push(data.format);
 691                    if data.format == self.atoms.UTF8_STRING {
 692                        // When we are storing a UTF8 string,
 693                        // add all equivalent formats to the supported targets
 694                        targets.push(self.atoms.UTF8_MIME_0);
 695                        targets.push(self.atoms.UTF8_MIME_1);
 696                    }
 697                }
 698            }
 699            self.server
 700                .conn
 701                .change_property32(
 702                    PropMode::REPLACE,
 703                    event.requestor,
 704                    event.property,
 705                    // TODO: change to `AtomEnum::ATOM`
 706                    self.atoms.ATOM,
 707                    &targets,
 708                )
 709                .map_err(into_unknown)?;
 710            self.server.conn.flush().map_err(into_unknown)?;
 711            success = true;
 712        } else {
 713            log::trace!("Handling request for (probably) the clipboard contents.");
 714            let data = self.selection_of(selection).data.read();
 715            if let Some(data_list) = &*data {
 716                success = match data_list.iter().find(|d| d.format == event.target) {
 717                    Some(data) => {
 718                        self.server
 719                            .conn
 720                            .change_property8(
 721                                PropMode::REPLACE,
 722                                event.requestor,
 723                                event.property,
 724                                event.target,
 725                                &data.bytes,
 726                            )
 727                            .map_err(into_unknown)?;
 728                        self.server.conn.flush().map_err(into_unknown)?;
 729                        true
 730                    }
 731                    None => false,
 732                };
 733            } else {
 734                // This must mean that we lost ownership of the data
 735                // since the other side requested the selection.
 736                // Let's respond with the property set to none.
 737                success = false;
 738            }
 739        }
 740        // on failure we notify the requester of it
 741        let property = if success {
 742            event.property
 743        } else {
 744            AtomEnum::NONE.into()
 745        };
 746        // tell the requestor that we finished sending data
 747        self.server
 748            .conn
 749            .send_event(
 750                false,
 751                event.requestor,
 752                EventMask::NO_EVENT,
 753                SelectionNotifyEvent {
 754                    response_type: SELECTION_NOTIFY_EVENT,
 755                    sequence: event.sequence,
 756                    time: event.time,
 757                    requestor: event.requestor,
 758                    selection: event.selection,
 759                    target: event.target,
 760                    property,
 761                },
 762            )
 763            .map_err(into_unknown)?;
 764
 765        self.server.conn.flush().map_err(into_unknown)
 766    }
 767
 768    fn ask_clipboard_manager_to_request_our_data(&self) -> Result<()> {
 769        if self.server.win_id == 0 {
 770            // This shouldn't really ever happen but let's just check.
 771            log::error!("The server's window id was 0. This is unexpected");
 772            return Ok(());
 773        }
 774
 775        if !self.is_owner(ClipboardKind::Clipboard)? {
 776            // We are not owning the clipboard, nothing to do.
 777            return Ok(());
 778        }
 779        if self
 780            .selection_of(ClipboardKind::Clipboard)
 781            .data
 782            .read()
 783            .is_none()
 784        {
 785            // If we don't have any data, there's nothing to do.
 786            return Ok(());
 787        }
 788
 789        // It's important that we lock the state before sending the request
 790        // because we don't want the request server thread to lock the state
 791        // after the request but before we can lock it here.
 792        let mut handover_state = self.handover_state.lock();
 793
 794        log::trace!("Sending the data to the clipboard manager");
 795        self.server
 796            .conn
 797            .convert_selection(
 798                self.server.win_id,
 799                self.atoms.CLIPBOARD_MANAGER,
 800                self.atoms.SAVE_TARGETS,
 801                self.atoms.ARBOARD_CLIPBOARD,
 802                Time::CURRENT_TIME,
 803            )
 804            .map_err(into_unknown)?;
 805        self.server.conn.flush().map_err(into_unknown)?;
 806
 807        *handover_state = ManagerHandoverState::InProgress;
 808        let max_handover_duration = Duration::from_millis(100);
 809
 810        // Note that we are using a parking_lot condvar here, which doesn't wake up
 811        // spuriously
 812        let result = self
 813            .handover_cv
 814            .wait_for(&mut handover_state, max_handover_duration);
 815
 816        if *handover_state == ManagerHandoverState::Finished {
 817            return Ok(());
 818        }
 819        if result.timed_out() {
 820            log::warn!(
 821                "Could not hand the clipboard contents over to the clipboard manager. The request timed out."
 822            );
 823            return Ok(());
 824        }
 825
 826        Err(Error::unknown(
 827            "The handover was not finished and the condvar didn't time out, yet the condvar wait ended. This should be unreachable.",
 828        ))
 829    }
 830}
 831
 832fn serve_requests(context: Arc<Inner>) -> Result<(), Box<dyn std::error::Error>> {
 833    fn handover_finished(clip: &Arc<Inner>, mut handover_state: MutexGuard<ManagerHandoverState>) {
 834        log::trace!("Finishing clipboard manager handover.");
 835        *handover_state = ManagerHandoverState::Finished;
 836
 837        // Not sure if unlocking the mutex is necessary here but better safe than sorry.
 838        drop(handover_state);
 839
 840        clip.handover_cv.notify_all();
 841    }
 842
 843    log::trace!("Started serve requests thread.");
 844
 845    let _guard = util::defer(|| {
 846        context.serve_stopped.store(true, Ordering::Relaxed);
 847    });
 848
 849    let mut written = false;
 850    let mut notified = false;
 851
 852    loop {
 853        match context.server.conn.wait_for_event().map_err(into_unknown)? {
 854            Event::DestroyNotify(_) => {
 855                // This window is being destroyed.
 856                log::trace!("Clipboard server window is being destroyed x_x");
 857                return Ok(());
 858            }
 859            Event::SelectionClear(event) => {
 860                // TODO: check if this works
 861                // Someone else has new content in the clipboard, so it is
 862                // notifying us that we should delete our data now.
 863                log::trace!("Somebody else owns the clipboard now");
 864
 865                if let Some(selection) = context.kind_of(event.selection) {
 866                    let selection = context.selection_of(selection);
 867                    let mut data_guard = selection.data.write();
 868                    *data_guard = None;
 869
 870                    // It is important that this mutex is locked at the time of calling
 871                    // `notify_all` to prevent notifications getting lost in case the sleeping
 872                    // thread has unlocked its `data_guard` and is just about to sleep.
 873                    // It is also important that the RwLock is kept write-locked for the same
 874                    // reason.
 875                    let _guard = selection.mutex.lock();
 876                    selection.data_changed.notify_all();
 877                }
 878            }
 879            Event::SelectionRequest(event) => {
 880                log::trace!(
 881                    "SelectionRequest - selection is: {}, target is {}",
 882                    context.atom_name(event.selection),
 883                    context.atom_name(event.target),
 884                );
 885                // Someone is requesting the clipboard content from us.
 886                context
 887                    .handle_selection_request(event)
 888                    .map_err(into_unknown)?;
 889
 890                // if we are in the progress of saving to the clipboard manager
 891                // make sure we save that we have finished writing
 892                let handover_state = context.handover_state.lock();
 893                if *handover_state == ManagerHandoverState::InProgress {
 894                    // Only set written, when the actual contents were written,
 895                    // not just a response to what TARGETS we have.
 896                    if event.target != context.atoms.TARGETS {
 897                        log::trace!("The contents were written to the clipboard manager.");
 898                        written = true;
 899                        // if we have written and notified, make sure to notify that we are done
 900                        if notified {
 901                            handover_finished(&context, handover_state);
 902                        }
 903                    }
 904                }
 905            }
 906            Event::SelectionNotify(event) => {
 907                // We've requested the clipboard content and this is the answer.
 908                // Considering that this thread is not responsible for reading
 909                // clipboard contents, this must come from the clipboard manager
 910                // signaling that the data was handed over successfully.
 911                if event.selection != context.atoms.CLIPBOARD_MANAGER {
 912                    log::error!(
 913                        "Received a `SelectionNotify` from a selection other than the CLIPBOARD_MANAGER. This is unexpected in this thread."
 914                    );
 915                    continue;
 916                }
 917                let handover_state = context.handover_state.lock();
 918                if *handover_state == ManagerHandoverState::InProgress {
 919                    // Note that some clipboard managers send a selection notify
 920                    // before even sending a request for the actual contents.
 921                    // (That's why we use the "notified" & "written" flags)
 922                    log::trace!(
 923                        "The clipboard manager indicated that it's done requesting the contents from us."
 924                    );
 925                    notified = true;
 926
 927                    // One would think that we could also finish if the property
 928                    // here is set 0, because that indicates failure. However
 929                    // this is not the case; for example on KDE plasma 5.18, we
 930                    // immediately get a SelectionNotify with property set to 0,
 931                    // but following that, we also get a valid SelectionRequest
 932                    // from the clipboard manager.
 933                    if written {
 934                        handover_finished(&context, handover_state);
 935                    }
 936                }
 937            }
 938            _event => {
 939                // May be useful for debugging but nothing else really.
 940                //log::trace!("Received unwanted event: {:?}", event);
 941            }
 942        }
 943    }
 944}
 945
 946pub(crate) struct Clipboard {
 947    inner: Arc<Inner>,
 948}
 949
 950impl Clipboard {
 951    pub(crate) fn new() -> Result<Self> {
 952        let mut global_cb = CLIPBOARD.lock();
 953        if let Some(global_cb) = &*global_cb {
 954            return Ok(Self {
 955                inner: Arc::clone(&global_cb.inner),
 956            });
 957        }
 958        // At this point we know that the clipboard does not exist.
 959        let ctx = Arc::new(Inner::new()?);
 960        let join_handle = std::thread::Builder::new()
 961            .name("Clipboard".to_owned())
 962            .spawn({
 963                let ctx = Arc::clone(&ctx);
 964                move || {
 965                    if let Err(error) = serve_requests(ctx) {
 966                        log::error!("Worker thread errored with: {}", error);
 967                    }
 968                }
 969            })
 970            .unwrap();
 971        *global_cb = Some(GlobalClipboard {
 972            inner: Arc::clone(&ctx),
 973            server_handle: join_handle,
 974        });
 975        Ok(Self { inner: ctx })
 976    }
 977
 978    pub(crate) fn set_text(
 979        &self,
 980        message: Cow<'_, str>,
 981        selection: ClipboardKind,
 982        wait: WaitConfig,
 983    ) -> Result<()> {
 984        let data = vec![ClipboardData {
 985            bytes: message.into_owned().into_bytes(),
 986            format: self.inner.atoms.UTF8_STRING,
 987        }];
 988        self.inner.write(data, selection, wait)
 989    }
 990
 991    #[allow(unused)]
 992    pub(crate) fn set_image(
 993        &self,
 994        image: Image,
 995        selection: ClipboardKind,
 996        wait: WaitConfig,
 997    ) -> Result<()> {
 998        let format = match image.format {
 999            ImageFormat::Png => self.inner.atoms.PNG__MIME,
1000            ImageFormat::Jpeg => self.inner.atoms.JPEG_MIME,
1001            ImageFormat::Webp => self.inner.atoms.WEBP_MIME,
1002            ImageFormat::Gif => self.inner.atoms.GIF__MIME,
1003            ImageFormat::Svg => self.inner.atoms.SVG__MIME,
1004            ImageFormat::Bmp => self.inner.atoms.BMP__MIME,
1005            ImageFormat::Tiff => self.inner.atoms.TIFF_MIME,
1006        };
1007        let data = vec![ClipboardData {
1008            bytes: image.bytes,
1009            format: self.inner.atoms.PNG__MIME,
1010        }];
1011        self.inner.write(data, selection, wait)
1012    }
1013
1014    pub(crate) fn get_any(&self, selection: ClipboardKind) -> Result<ClipboardItem> {
1015        const IMAGE_FORMAT_COUNT: usize = 7;
1016        let image_format_atoms: [Atom; IMAGE_FORMAT_COUNT] = [
1017            self.inner.atoms.PNG__MIME,
1018            self.inner.atoms.JPEG_MIME,
1019            self.inner.atoms.WEBP_MIME,
1020            self.inner.atoms.GIF__MIME,
1021            self.inner.atoms.SVG__MIME,
1022            self.inner.atoms.BMP__MIME,
1023            self.inner.atoms.TIFF_MIME,
1024        ];
1025        let image_formats: [ImageFormat; IMAGE_FORMAT_COUNT] = [
1026            ImageFormat::Png,
1027            ImageFormat::Jpeg,
1028            ImageFormat::Webp,
1029            ImageFormat::Gif,
1030            ImageFormat::Svg,
1031            ImageFormat::Bmp,
1032            ImageFormat::Tiff,
1033        ];
1034
1035        const TEXT_FORMAT_COUNT: usize = 6;
1036        let text_format_atoms: [Atom; TEXT_FORMAT_COUNT] = [
1037            self.inner.atoms.UTF8_STRING,
1038            self.inner.atoms.UTF8_MIME_0,
1039            self.inner.atoms.UTF8_MIME_1,
1040            self.inner.atoms.STRING,
1041            self.inner.atoms.TEXT,
1042            self.inner.atoms.TEXT_MIME_UNKNOWN,
1043        ];
1044
1045        let atom_none: Atom = AtomEnum::NONE.into();
1046
1047        const FORMAT_ATOM_COUNT: usize = TEXT_FORMAT_COUNT + IMAGE_FORMAT_COUNT;
1048
1049        let mut format_atoms: [Atom; FORMAT_ATOM_COUNT] = [atom_none; FORMAT_ATOM_COUNT];
1050
1051        // image formats first, as they are more specific, and read will return the first
1052        // format that the contents can be converted to
1053        format_atoms[0..IMAGE_FORMAT_COUNT].copy_from_slice(&image_format_atoms);
1054        format_atoms[IMAGE_FORMAT_COUNT..].copy_from_slice(&text_format_atoms);
1055        debug_assert!(!format_atoms.contains(&atom_none));
1056
1057        let result = self.inner.read(&format_atoms, selection)?;
1058
1059        log::trace!(
1060            "read clipboard as format {:?}",
1061            self.inner.atom_name(result.format)
1062        );
1063
1064        for (format_atom, image_format) in image_format_atoms.into_iter().zip(image_formats) {
1065            if result.format == format_atom {
1066                let bytes = result.bytes;
1067                let id = hash(&bytes);
1068                return Ok(ClipboardItem::new_image(&Image {
1069                    id,
1070                    format: image_format,
1071                    bytes,
1072                }));
1073            }
1074        }
1075
1076        let text = if result.format == self.inner.atoms.STRING {
1077            // ISO Latin-1
1078            // See: https://stackoverflow.com/questions/28169745/what-are-the-options-to-convert-iso-8859-1-latin-1-to-a-string-utf-8
1079            result.bytes.into_iter().map(|c| c as char).collect()
1080        } else {
1081            String::from_utf8(result.bytes).map_err(|_| Error::ConversionFailure)?
1082        };
1083        Ok(ClipboardItem::new_string(text))
1084    }
1085
1086    pub fn is_owner(&self, selection: ClipboardKind) -> bool {
1087        self.inner.is_owner(selection).unwrap_or(false)
1088    }
1089}
1090
1091impl Drop for Clipboard {
1092    fn drop(&mut self) {
1093        // There are always at least 3 owners:
1094        // the global, the server thread, and one `Clipboard::inner`
1095        const MIN_OWNERS: usize = 3;
1096
1097        // We start with locking the global guard to prevent race
1098        // conditions below.
1099        let mut global_cb = CLIPBOARD.lock();
1100        if Arc::strong_count(&self.inner) == MIN_OWNERS {
1101            // If the are the only owners of the clipboard are ourselves and
1102            // the global object, then we should destroy the global object,
1103            // and send the data to the clipboard manager
1104
1105            if let Err(e) = self.inner.ask_clipboard_manager_to_request_our_data() {
1106                log::error!(
1107                    "Could not hand the clipboard data over to the clipboard manager: {}",
1108                    e
1109                );
1110            }
1111            let global_cb = global_cb.take();
1112            if let Err(e) = self
1113                .inner
1114                .server
1115                .conn
1116                .destroy_window(self.inner.server.win_id)
1117            {
1118                log::error!("Failed to destroy the clipboard window. Error: {}", e);
1119                return;
1120            }
1121            if let Err(e) = self.inner.server.conn.flush() {
1122                log::error!("Failed to flush the clipboard window. Error: {}", e);
1123                return;
1124            }
1125            if let Some(global_cb) = global_cb
1126                && let Err(e) = global_cb.server_handle.join()
1127            {
1128                // Let's try extracting the error message
1129                let message;
1130                if let Some(msg) = e.downcast_ref::<&'static str>() {
1131                    message = Some((*msg).to_string());
1132                } else if let Some(msg) = e.downcast_ref::<String>() {
1133                    message = Some(msg.clone());
1134                } else {
1135                    message = None;
1136                }
1137                if let Some(message) = message {
1138                    log::error!(
1139                        "The clipboard server thread panicked. Panic message: '{}'",
1140                        message,
1141                    );
1142                } else {
1143                    log::error!("The clipboard server thread panicked.");
1144                }
1145            }
1146        }
1147    }
1148}
1149
1150fn into_unknown<E: std::fmt::Display>(error: E) -> Error {
1151    Error::Unknown {
1152        description: error.to_string(),
1153    }
1154}
1155
1156/// Clipboard selection
1157///
1158/// Linux has a concept of clipboard "selections" which tend to be used in different contexts. This
1159/// enum provides a way to get/set to a specific clipboard
1160///
1161/// See <https://specifications.freedesktop.org/clipboards-spec/clipboards-0.1.txt> for a better
1162/// description of the different clipboards.
1163#[derive(Copy, Clone, Debug)]
1164pub enum ClipboardKind {
1165    /// Typically used selection for explicit cut/copy/paste actions (ie. windows/macos like
1166    /// clipboard behavior)
1167    Clipboard,
1168
1169    /// Typically used for mouse selections and/or currently selected text. Accessible via middle
1170    /// mouse click.
1171    Primary,
1172
1173    /// The secondary clipboard is rarely used but theoretically available on X11.
1174    Secondary,
1175}
1176
1177/// Configuration on how long to wait for a new X11 copy event is emitted.
1178#[derive(Default)]
1179pub(crate) enum WaitConfig {
1180    /// Waits until the given [`Instant`] has reached.
1181    #[allow(
1182        unused,
1183        reason = "Right now we don't wait for clipboard contents to sync on app close, but we may in the future"
1184    )]
1185    Until(Instant),
1186
1187    /// Waits forever until a new event is reached.
1188    #[allow(unused)]
1189    #[allow(
1190        unused,
1191        reason = "Right now we don't wait for clipboard contents to sync on app close, but we may in the future"
1192    )]
1193    Forever,
1194
1195    /// It shouldn't wait.
1196    #[default]
1197    None,
1198}
1199
1200#[non_exhaustive]
1201pub enum Error {
1202    /// The clipboard contents were not available in the requested format.
1203    /// This could either be due to the clipboard being empty or the clipboard contents having
1204    /// an incompatible format to the requested one (eg when calling `get_image` on text)
1205    ContentNotAvailable,
1206
1207    /// The native clipboard is not accessible due to being held by an other party.
1208    ///
1209    /// This "other party" could be a different process or it could be within
1210    /// the same program. So for example you may get this error when trying
1211    /// to interact with the clipboard from multiple threads at once.
1212    ///
1213    /// Note that it's OK to have multiple `Clipboard` instances. The underlying
1214    /// implementation will make sure that the native clipboard is only
1215    /// opened for transferring data and then closed as soon as possible.
1216    ClipboardOccupied,
1217
1218    /// The image or the text that was about the be transferred to/from the clipboard could not be
1219    /// converted to the appropriate format.
1220    ConversionFailure,
1221
1222    /// Any error that doesn't fit the other error types.
1223    ///
1224    /// The `description` field is only meant to help the developer and should not be relied on as a
1225    /// means to identify an error case during runtime.
1226    Unknown { description: String },
1227}
1228
1229impl std::fmt::Display for Error {
1230    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1231        match self {
1232			Error::ContentNotAvailable => f.write_str("The clipboard contents were not available in the requested format or the clipboard is empty."),
1233			Error::ClipboardOccupied => f.write_str("The native clipboard is not accessible due to being held by an other party."),
1234			Error::ConversionFailure => f.write_str("The image or the text that was about the be transferred to/from the clipboard could not be converted to the appropriate format."),
1235			Error::Unknown { description } => f.write_fmt(format_args!("Unknown error while interacting with the clipboard: {description}")),
1236		}
1237    }
1238}
1239
1240impl std::error::Error for Error {}
1241
1242impl std::fmt::Debug for Error {
1243    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1244        use Error::*;
1245        macro_rules! kind_to_str {
1246			($( $e: pat ),*) => {
1247				match self {
1248					$(
1249						$e => stringify!($e),
1250					)*
1251				}
1252			}
1253		}
1254        let name = kind_to_str!(
1255            ContentNotAvailable,
1256            ClipboardOccupied,
1257            ConversionFailure,
1258            Unknown { .. }
1259        );
1260        f.write_fmt(format_args!("{name} - \"{self}\""))
1261    }
1262}
1263
1264impl Error {
1265    pub(crate) fn unknown<M: Into<String>>(message: M) -> Self {
1266        Error::Unknown {
1267            description: message.into(),
1268        }
1269    }
1270}