1use std::{
2 io,
3 os::raw::*,
4 path::{Path, PathBuf},
5 str::Utf8Error,
6 sync::Arc,
7};
8
9use percent_encoding::percent_decode;
10use x11rb::protocol::xproto::{self, ConnectionExt};
11
12use super::{
13 atoms::{AtomName::None as DndNone, *},
14 util, CookieResultExt, X11Error, XConnection,
15};
16
17#[derive(Debug, Clone, Copy)]
18pub enum DndState {
19 Accepted,
20 Rejected,
21}
22
23#[derive(Debug)]
24pub enum DndDataParseError {
25 EmptyData,
26 InvalidUtf8(Utf8Error),
27 HostnameSpecified(String),
28 UnexpectedProtocol(String),
29 UnresolvablePath(io::Error),
30}
31
32impl From<Utf8Error> for DndDataParseError {
33 fn from(e: Utf8Error) -> Self {
34 DndDataParseError::InvalidUtf8(e)
35 }
36}
37
38impl From<io::Error> for DndDataParseError {
39 fn from(e: io::Error) -> Self {
40 DndDataParseError::UnresolvablePath(e)
41 }
42}
43
44pub struct Dnd {
45 xconn: Arc<XConnection>,
46 // Populated by XdndEnter event handler
47 pub version: Option<c_long>,
48 pub type_list: Option<Vec<xproto::Atom>>,
49 // Populated by XdndPosition event handler
50 pub source_window: Option<xproto::Window>,
51 // Populated by SelectionNotify event handler (triggered by XdndPosition event handler)
52 pub result: Option<Result<Vec<PathBuf>, DndDataParseError>>,
53}
54
55impl Dnd {
56 pub fn new(xconn: Arc<XConnection>) -> Result<Self, X11Error> {
57 Ok(Dnd {
58 xconn,
59 version: None,
60 type_list: None,
61 source_window: None,
62 result: None,
63 })
64 }
65
66 pub fn reset(&mut self) {
67 self.version = None;
68 self.type_list = None;
69 self.source_window = None;
70 self.result = None;
71 }
72
73 pub unsafe fn send_status(
74 &self,
75 this_window: xproto::Window,
76 target_window: xproto::Window,
77 state: DndState,
78 ) -> Result<(), X11Error> {
79 let atoms = self.xconn.atoms();
80 let (accepted, action) = match state {
81 DndState::Accepted => (1, atoms[XdndActionPrivate]),
82 DndState::Rejected => (0, atoms[DndNone]),
83 };
84 self.xconn
85 .send_client_msg(
86 target_window,
87 target_window,
88 atoms[XdndStatus] as _,
89 None,
90 [this_window, accepted, 0, 0, action as _],
91 )?
92 .ignore_error();
93
94 Ok(())
95 }
96
97 pub unsafe fn send_finished(
98 &self,
99 this_window: xproto::Window,
100 target_window: xproto::Window,
101 state: DndState,
102 ) -> Result<(), X11Error> {
103 let atoms = self.xconn.atoms();
104 let (accepted, action) = match state {
105 DndState::Accepted => (1, atoms[XdndActionPrivate]),
106 DndState::Rejected => (0, atoms[DndNone]),
107 };
108 self.xconn
109 .send_client_msg(
110 target_window,
111 target_window,
112 atoms[XdndFinished] as _,
113 None,
114 [this_window, accepted, action as _, 0, 0],
115 )?
116 .ignore_error();
117
118 Ok(())
119 }
120
121 pub unsafe fn get_type_list(
122 &self,
123 source_window: xproto::Window,
124 ) -> Result<Vec<xproto::Atom>, util::GetPropertyError> {
125 let atoms = self.xconn.atoms();
126 self.xconn.get_property(
127 source_window,
128 atoms[XdndTypeList],
129 xproto::Atom::from(xproto::AtomEnum::ATOM),
130 )
131 }
132
133 pub unsafe fn convert_selection(&self, window: xproto::Window, time: xproto::Timestamp) {
134 let atoms = self.xconn.atoms();
135 self.xconn
136 .xcb_connection()
137 .convert_selection(
138 window,
139 atoms[XdndSelection],
140 atoms[TextUriList],
141 atoms[XdndSelection],
142 time,
143 )
144 .expect_then_ignore_error("Failed to send XdndSelection event")
145 }
146
147 pub unsafe fn read_data(
148 &self,
149 window: xproto::Window,
150 ) -> Result<Vec<c_uchar>, util::GetPropertyError> {
151 let atoms = self.xconn.atoms();
152 self.xconn
153 .get_property(window, atoms[XdndSelection], atoms[TextUriList])
154 }
155
156 pub fn parse_data(&self, data: &mut [c_uchar]) -> Result<Vec<PathBuf>, DndDataParseError> {
157 if !data.is_empty() {
158 let mut path_list = Vec::new();
159 let decoded = percent_decode(data).decode_utf8()?.into_owned();
160 for uri in decoded.split("\r\n").filter(|u| !u.is_empty()) {
161 // The format is specified as protocol://host/path
162 // However, it's typically simply protocol:///path
163 let path_str = if uri.starts_with("file://") {
164 let path_str = uri.replace("file://", "");
165 if !path_str.starts_with('/') {
166 // A hostname is specified
167 // Supporting this case is beyond the scope of my mental health
168 return Err(DndDataParseError::HostnameSpecified(path_str));
169 }
170 path_str
171 } else {
172 // Only the file protocol is supported
173 return Err(DndDataParseError::UnexpectedProtocol(uri.to_owned()));
174 };
175
176 let path = Path::new(&path_str).canonicalize()?;
177 path_list.push(path);
178 }
179 Ok(path_list)
180 } else {
181 Err(DndDataParseError::EmptyData)
182 }
183 }
184}
185