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