wl-data-control-protocol-evt 1.0.0

Implementation of ext-data-control Wayland protocol, pollable and event based
Documentation
use crate::{
    ExtDataControlReadError,
    app_connection::AppConnection,
    mime_types::MimeTypes,
    offer_seq::OfferSeq,
    reader::Reader,
    wl_event::{WlEvent, WlOfferEvent, WlSourceEvent},
    writer::Writer,
};
use rustix::pipe::PipeFlags;
use std::{collections::HashMap, os::fd::AsFd as _};
use wayland_protocols::ext::data_control::v1::client::ext_data_control_source_v1::ExtDataControlSourceV1;

pub struct ReaderWriterStream {
    offer_seq: OfferSeq,
    source_to_text: HashMap<ExtDataControlSourceV1, String>,
    mime_types: MimeTypes,
}

impl ReaderWriterStream {
    pub(crate) fn new() -> Self {
        Self {
            offer_seq: OfferSeq::Empty,
            source_to_text: HashMap::new(),
            mime_types: MimeTypes::new(),
        }
    }

    pub(crate) fn read_until_blocked(
        &mut self,
        conn: &mut AppConnection,
    ) -> Result<Vec<ReaderWriterEvent>, ExtDataControlReadError> {
        let mut events = vec![];
        for event in conn.read_until_blocked()? {
            if let Some(event) = self.map_any(event) {
                events.push(event);
            }
        }
        Ok(events)
    }

    pub(crate) fn save_offer(&mut self, text: impl Into<String>, source: ExtDataControlSourceV1) {
        self.source_to_text.insert(source, text.into());
    }

    pub(crate) fn cleanup(&mut self) {
        self.offer_seq.destroy();
        for source in self.source_to_text.keys() {
            source.destroy();
        }
    }

    pub(crate) fn mime_type_mask(&self) -> &str {
        self.mime_types.mask()
    }

    fn map_any(&mut self, event: WlEvent) -> Option<ReaderWriterEvent> {
        match event {
            WlEvent::Offer(event) => self.map_offer(event),
            WlEvent::Source(event) => self.map_source(event),
        }
    }

    fn map_offer(&mut self, event: WlOfferEvent) -> Option<ReaderWriterEvent> {
        match event {
            WlOfferEvent::DataOffer(offer) => {
                self.offer_seq.start(offer);
            }

            WlOfferEvent::MimeTime(offer, mime_type) => {
                self.offer_seq.extend(offer, mime_type);
            }

            WlOfferEvent::Selection(Some(offer)) => {
                let (offer, mime_types) = self.offer_seq.finish(offer)?;
                let Some(mime_type_to_ask_for) = self.mime_types.choose(&mime_types) else {
                    offer.destroy();
                    return None;
                };

                match rustix::pipe::pipe_with(PipeFlags::NONBLOCK) {
                    Ok((reader, writer)) => {
                        offer.receive(mime_type_to_ask_for, writer.as_fd());
                        drop(writer);
                        return Some(ReaderWriterEvent::NewReader(Box::new(Reader::new(
                            reader, offer,
                        ))));
                    }
                    Err(err) => {
                        log::error!("failed to create pipe: {err:?}");
                        offer.destroy();
                    }
                }
            }
            WlOfferEvent::Selection(None) => {
                self.offer_seq.destroy();
            }

            WlOfferEvent::PrimarySelection(offer) => {
                self.offer_seq.destroy();
                if let Some(offer) = offer {
                    offer.destroy();
                }
            }

            WlOfferEvent::Finished => {
                log::warn!("ExtDataControlDeviceV1 has finished");
                return Some(ReaderWriterEvent::Finished);
            }
        }

        None
    }

    fn map_source(&mut self, event: WlSourceEvent) -> Option<ReaderWriterEvent> {
        match event {
            WlSourceEvent::Requested(source, mime_type, fd) => {
                if !MimeTypes::is_text(&mime_type) {
                    return None;
                }
                let text = self.source_to_text.get(&source)?;

                match Writer::new(fd, text.clone()) {
                    Ok(writer) => Some(ReaderWriterEvent::NewWriter(writer)),
                    Err(err) => {
                        log::error!("filed to create a writer: {err:?}");
                        None
                    }
                }
            }
            WlSourceEvent::Cancelled(source) => {
                self.source_to_text.remove(&source);
                source.destroy();
                None
            }
        }
    }
}

pub enum ReaderWriterEvent {
    NewReader(Box<Reader>),
    NewWriter(Writer),
    Finished,
}