use std::{ io, os::raw::*, path::{Path, PathBuf}, str::Utf8Error, sync::Arc, }; use percent_encoding::percent_decode; use x11rb::protocol::xproto::{self, ConnectionExt}; use super::{ atoms::{AtomName::None as DndNone, *}, util, CookieResultExt, X11Error, XConnection, }; #[derive(Debug, Clone, Copy)] pub enum DndState { Accepted, Rejected, } #[derive(Debug)] pub enum DndDataParseError { EmptyData, InvalidUtf8(Utf8Error), HostnameSpecified(String), UnexpectedProtocol(String), UnresolvablePath(io::Error), } impl From for DndDataParseError { fn from(e: Utf8Error) -> Self { DndDataParseError::InvalidUtf8(e) } } impl From for DndDataParseError { fn from(e: io::Error) -> Self { DndDataParseError::UnresolvablePath(e) } } pub(crate) struct Dnd { xconn: Arc, // Populated by XdndEnter event handler pub version: Option, pub type_list: Option>, // Populated by XdndPosition event handler pub source_window: Option, // Populated by SelectionNotify event handler (triggered by XdndPosition event handler) pub result: Option, DndDataParseError>>, } impl Dnd { pub fn new(xconn: Arc) -> Result { Ok(Dnd { xconn, version: None, type_list: None, source_window: None, result: None, }) } pub fn reset(&mut self) { self.version = None; self.type_list = None; self.source_window = None; self.result = None; } pub unsafe fn send_status( &self, this_window: xproto::Window, target_window: xproto::Window, state: DndState, ) -> Result<(), X11Error> { let atoms = self.xconn.atoms(); let (accepted, action) = match state { DndState::Accepted => (1, atoms[XdndActionPrivate]), DndState::Rejected => (0, atoms[DndNone]), }; self.xconn .send_client_msg( target_window, target_window, atoms[XdndStatus] as _, None, [this_window, accepted, 0, 0, action as _], )? .ignore_error(); Ok(()) } pub unsafe fn send_finished( &self, this_window: xproto::Window, target_window: xproto::Window, state: DndState, ) -> Result<(), X11Error> { let atoms = self.xconn.atoms(); let (accepted, action) = match state { DndState::Accepted => (1, atoms[XdndActionPrivate]), DndState::Rejected => (0, atoms[DndNone]), }; self.xconn .send_client_msg( target_window, target_window, atoms[XdndFinished] as _, None, [this_window, accepted, action as _, 0, 0], )? .ignore_error(); Ok(()) } pub unsafe fn get_type_list( &self, source_window: xproto::Window, ) -> Result, util::GetPropertyError> { let atoms = self.xconn.atoms(); self.xconn.get_property( source_window, atoms[XdndTypeList], xproto::Atom::from(xproto::AtomEnum::ATOM), ) } pub unsafe fn convert_selection(&self, window: xproto::Window, time: xproto::Timestamp) { let atoms = self.xconn.atoms(); self.xconn .xcb_connection() .convert_selection( window, atoms[XdndSelection], atoms[TextUriList], atoms[XdndSelection], time, ) .expect_then_ignore_error("Failed to send XdndSelection event") } pub unsafe fn read_data( &self, window: xproto::Window, ) -> Result, util::GetPropertyError> { let atoms = self.xconn.atoms(); self.xconn .get_property(window, atoms[XdndSelection], atoms[TextUriList]) } pub fn parse_data(&self, data: &mut [c_uchar]) -> Result, DndDataParseError> { if !data.is_empty() { let mut path_list = Vec::new(); let decoded = percent_decode(data).decode_utf8()?.into_owned(); for uri in decoded.split("\r\n").filter(|u| !u.is_empty()) { // The format is specified as protocol://host/path // However, it's typically simply protocol:///path let path_str = if uri.starts_with("file://") { let path_str = uri.replace("file://", ""); if !path_str.starts_with('/') { // A hostname is specified // Supporting this case is beyond the scope of my mental health return Err(DndDataParseError::HostnameSpecified(path_str)); } path_str } else { // Only the file protocol is supported return Err(DndDataParseError::UnexpectedProtocol(uri.to_owned())); }; let path = Path::new(&path_str).canonicalize()?; path_list.push(path); } Ok(path_list) } else { Err(DndDataParseError::EmptyData) } } }