1 | use std::{ |
2 | io, |
3 | os::raw::*, |
4 | path::{Path, PathBuf}, |
5 | str::Utf8Error, |
6 | sync::Arc, |
7 | }; |
8 | |
9 | use percent_encoding::percent_decode; |
10 | use x11rb::protocol::xproto::{self, ConnectionExt}; |
11 | |
12 | use super::{ |
13 | atoms::{AtomName::None as DndNone, *}, |
14 | util, CookieResultExt, X11Error, XConnection, |
15 | }; |
16 | |
17 | #[derive (Debug, Clone, Copy)] |
18 | pub enum DndState { |
19 | Accepted, |
20 | Rejected, |
21 | } |
22 | |
23 | #[derive (Debug)] |
24 | pub enum DndDataParseError { |
25 | EmptyData, |
26 | InvalidUtf8(Utf8Error), |
27 | HostnameSpecified(String), |
28 | UnexpectedProtocol(String), |
29 | UnresolvablePath(io::Error), |
30 | } |
31 | |
32 | impl From<Utf8Error> for DndDataParseError { |
33 | fn from(e: Utf8Error) -> Self { |
34 | DndDataParseError::InvalidUtf8(e) |
35 | } |
36 | } |
37 | |
38 | impl From<io::Error> for DndDataParseError { |
39 | fn from(e: io::Error) -> Self { |
40 | DndDataParseError::UnresolvablePath(e) |
41 | } |
42 | } |
43 | |
44 | pub 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 | |
55 | impl 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 | |