a7n-platform 0.19.0

Native desktop backends for the Apeirogon software KVM
Documentation
// SPDX-License-Identifier: GPL-3.0-or-later

//! Channels shared by the daemon and future clipboard backend threads.

use a7n_core::clipboard::{Generation, Item, LocalChange};
use tokio::sync::{mpsc, oneshot};

#[cfg(any(windows, target_os = "macos"))]
mod fetch;
#[cfg(all(unix, not(target_os = "macos")))]
mod formats;
#[cfg(target_os = "macos")]
mod macos;
pub mod uri_list;
#[cfg(all(unix, not(target_os = "macos")))]
pub(crate) mod wayland;
#[cfg(all(unix, not(target_os = "macos")))]
mod x11;

#[cfg(any(windows, test))]
#[path = "clipboard/windows/cf_html.rs"]
mod cf_html;
#[cfg(windows)]
mod windows;
#[cfg(all(test, not(windows)))]
#[path = "clipboard/windows/drop_files.rs"]
mod windows_drop_files;
#[cfg(all(test, not(windows)))]
#[path = "clipboard/windows/image.rs"]
mod windows_image;

/// Backends must suppress changes caused by their own `Set` commands so that
/// remote clipboard writes cannot create a copy/announce feedback loop.
#[derive(Debug)]
pub struct Clipboard {
    /// The backend can read and offer PNG images.
    pub images: bool,
    /// The backend can read copied file lists into `LocalChange::files` and
    /// offer a [`LazyItem`] of mime `a7n_core::clipboard::FILES`. Its paste
    /// reply is a `text/uri-list` ([`uri_list`]) of local paths.
    pub files: bool,
    pub changes: mpsc::Receiver<LocalChange>,
    pub commands: mpsc::Sender<ClipboardCommand>,
    /// The OS asked to paste an item that was offered without data.
    pub pastes: mpsc::Receiver<PasteRequest>,
}

#[derive(Debug)]
pub enum ClipboardCommand {
    Set {
        generation: Generation,
        items: Vec<Item>,
        /// Offered now, fetched when something pastes them.
        lazy: Vec<LazyItem>,
    },
}

/// An item whose data stays on the origin until a paste requests it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LazyItem {
    pub mime: String,
    pub size: u64,
    /// Index in the origin's `ClipboardAnnounce.items`.
    pub item_index: u32,
    /// For a file list: how many top-level entries a paste publishes, so a
    /// backend that offers one pasteboard item per entry knows the count up
    /// front. 0 for every other item.
    pub top_level: u32,
}

impl LazyItem {
    /// Files are limited by policy, not by the clipboard size limits.
    pub(crate) fn within_limit(&self) -> bool {
        self.mime == a7n_core::clipboard::FILES
            || self.size <= a7n_core::clipboard::limit_for(&self.mime) as u64
    }
}

/// How long a backend waits for a pasted file list: the reply arrives only
/// after the files are downloaded. The daemon answers `None` earlier on failure.
pub(crate) const FILE_PASTE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10 * 60);

/// Backends block the paste until `reply` answers; `None` means the data is
/// gone (newer generation, peer unreachable, integrity failure).
#[derive(Debug)]
pub struct PasteRequest {
    pub generation: Generation,
    pub item_index: u32,
    pub reply: oneshot::Sender<Option<Vec<u8>>>,
}

#[derive(Debug, thiserror::Error)]
pub enum ClipboardError {
    #[error("clipboard is not supported on this platform")]
    Unsupported,
    #[cfg(target_os = "macos")]
    #[error("macOS clipboard failed: {0}")]
    Macos(#[from] std::io::Error),
    #[cfg(windows)]
    #[error("Windows clipboard failed: {0}")]
    Windows(#[from] std::io::Error),
    #[cfg(all(unix, not(target_os = "macos")))]
    #[error("X11 clipboard failed: {0}")]
    X11(#[from] crate::input::InputError),
    #[cfg(all(unix, not(target_os = "macos")))]
    #[error("Wayland clipboard failed: {0}")]
    Wayland(#[from] wayland::WaylandError),
}

/// On Wayland, call `input::start()` first: on desktops without data-control
/// the clipboard rides on the input backend's portal session.
///
/// # Errors
/// Returns an error when the session is unsupported or the backend cannot start.
pub fn start() -> Result<Clipboard, ClipboardError> {
    #[cfg(all(unix, not(target_os = "macos")))]
    {
        use crate::input::{Backend, detect_backend};
        match detect_backend() {
            Backend::X11 => return Ok(x11::start()?),
            Backend::Wayland => return Ok(wayland::start()?),
            Backend::None => {}
        }
    }
    #[cfg(windows)]
    {
        Ok(windows::start()?)
    }
    #[cfg(target_os = "macos")]
    {
        Ok(macos::start()?)
    }
    #[cfg(not(any(windows, target_os = "macos")))]
    Err(ClipboardError::Unsupported)
}