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