1use crate::{
2 common::{Result, RustImage},
3 ClipboardContent, ClipboardHandler, ContentFormat, RustImageData,
4};
5use crate::{Clipboard, ClipboardWatcher};
6use std::sync::mpsc::{self, Receiver, Sender};
7use std::{
8 sync::{Arc, RwLock},
9 thread,
10 time::{Duration, Instant},
11};
12use x11rb::{
13 connection::Connection,
14 protocol::{
15 xfixes,
16 xproto::{
17 Atom, AtomEnum, ConnectionExt as _, CreateWindowAux, EventMask, PropMode, Property,
18 SelectionNotifyEvent, SelectionRequestEvent, WindowClass, SELECTION_NOTIFY_EVENT,
19 },
20 Event,
21 },
22 rust_connection::RustConnection,
23 wrapper::ConnectionExt as _,
24 COPY_DEPTH_FROM_PARENT, CURRENT_TIME,
25};
26
27x11rb::atom_manager! {
28 pub Atoms: AtomCookies {
29 CLIPBOARD,
30 CLIPBOARD_MANAGER,
31 PROPERTY,
32 SAVE_TARGETS,
33 TARGETS,
34 ATOM,
35 INCR,
36 TIMESTAMP,
37 MULTIPLE,
38
39 UTF8_STRING,
40 UTF8_MIME_0: b"text/plain;charset=utf-8",
41 UTF8_MIME_1: b"text/plain;charset=UTF-8",
42 STRING,
45 TEXT,
48 TEXT_MIME_UNKNOWN: b"text/plain",
49 RTF: b"text/rtf",
51 RTF_1: b"text/richtext",
52 HTML: b"text/html",
53 PNG_MIME: b"image/png",
54 FILE_LIST: b"text/uri-list",
55 GNOME_COPY_FILES: b"x-special/gnome-copied-files",
56 NAUTILUS_FILE_LIST: b"x-special/nautilus-clipboard",
57 }
58}
59
60pub const DEFAULT_READ_TIMEOUT: u64 = 500;
61
62pub struct ClipboardContextX11Options {
65 pub read_timeout: Option<Duration>,
68}
69
70const FILE_PATH_PREFIX: &str = "file://";
71pub struct ClipboardContext {
72 inner: Arc<InnerContext>,
73 read_timeout: Option<Duration>,
74}
75
76struct ClipboardData {
77 format: Atom,
78 data: Vec<u8>,
79}
80
81struct InnerContext {
82 server: XServerContext,
83 server_for_write: XServerContext,
84 ignore_formats: Vec<Atom>,
85 wait_write_data: RwLock<Vec<ClipboardData>>,
87}
88
89impl InnerContext {
90 pub fn new() -> Result<Self> {
91 let server = XServerContext::new()?;
92 let server_for_write = XServerContext::new()?;
93 let wait_write_data = RwLock::new(Vec::new());
94
95 let ignore_formats = vec![
96 server.atoms.TIMESTAMP,
97 server.atoms.MULTIPLE,
98 server.atoms.TARGETS,
99 server.atoms.SAVE_TARGETS,
100 ];
101
102 Ok(Self {
103 server,
104 server_for_write,
105 ignore_formats,
106 wait_write_data,
107 })
108 }
109
110 pub fn handle_selection_request(&self, event: SelectionRequestEvent) -> Result<()> {
111 let success;
112 let ctx = &self.server_for_write;
113 let atoms = ctx.atoms;
114 if event.target == atoms.TARGETS {
116 let reader = self.wait_write_data.read();
117 match reader {
118 Ok(data_list) => {
119 let mut targets = Vec::with_capacity(10);
120 targets.push(atoms.TARGETS);
121 targets.push(atoms.SAVE_TARGETS);
122 if data_list.len() > 0 {
123 data_list.iter().for_each(|data| {
124 targets.push(data.format);
125 });
126 }
127 ctx.conn.change_property32(
128 PropMode::REPLACE,
129 event.requestor,
130 event.property,
131 AtomEnum::ATOM,
132 &targets,
133 )?;
134 success = true;
135 }
136 Err(_) => return Err("Failed to read clipboard data".into()),
137 }
138 } else {
139 let reader = self.wait_write_data.read();
140 match reader {
141 Ok(data_list) => {
142 success = match data_list.iter().find(|d| d.format == event.target) {
143 Some(data) => {
144 ctx.conn.change_property8(
145 PropMode::REPLACE,
146 event.requestor,
147 event.property,
148 event.target,
149 &data.data,
150 )?;
151 true
152 }
153 None => false,
154 };
155 }
156 Err(_) => return Err("Failed to read clipboard data".into()),
157 }
158 }
159 let property = if success {
161 event.property
162 } else {
163 AtomEnum::NONE.into()
164 };
165 ctx.conn.send_event(
167 false,
168 event.requestor,
169 EventMask::NO_EVENT,
170 SelectionNotifyEvent {
171 response_type: SELECTION_NOTIFY_EVENT,
172 sequence: event.sequence,
173 time: event.time,
174 requestor: event.requestor,
175 selection: event.selection,
176 target: event.target,
177 property,
178 },
179 )?;
180 ctx.conn.flush()?;
181 Ok(())
182 }
183
184 pub fn process_event(
185 &self,
186 buff: &mut Vec<u8>,
187 selection: Atom,
188 target: Atom,
189 property: Atom,
190 timeout: Option<Duration>,
191 sequence_number: u64,
192 ) -> Result<()> {
193 let mut is_incr = false;
194 let start_time = if timeout.is_some() {
195 Some(Instant::now())
196 } else {
197 None
198 };
199 let ctx = &self.server;
200 let atoms = ctx.atoms;
201 loop {
202 if timeout
203 .into_iter()
204 .zip(start_time)
205 .next()
206 .map(|(timeout, time)| (Instant::now() - time) >= timeout)
207 .unwrap_or(false)
208 {
209 return Err("Timeout while waiting for clipboard data".into());
210 }
211
212 let (event, seq) = match ctx.conn.poll_for_event_with_sequence()? {
213 Some(event) => event,
214 None => {
215 thread::park_timeout(Duration::from_millis(50));
216 continue;
217 }
218 };
219
220 if seq < sequence_number {
221 continue;
222 }
223
224 match event {
225 Event::SelectionNotify(event) => {
226 if event.selection != selection {
227 continue;
228 };
229
230 let target_type = {
231 if target == atoms.TARGETS {
232 atoms.ATOM
233 } else {
234 target
235 }
236 };
237
238 let reply = ctx
239 .conn
240 .get_property(
241 false,
242 event.requestor,
243 event.property,
244 target_type,
245 buff.len() as u32,
246 u32::MAX,
247 )?
248 .reply()?;
249
250 if reply.type_ == atoms.INCR {
251 if let Some(mut value) = reply.value32() {
252 if let Some(size) = value.next() {
253 buff.reserve(size as usize);
254 }
255 }
256 ctx.conn.delete_property(ctx.win_id, property)?.check()?;
257 is_incr = true;
258 continue;
259 } else if reply.type_ != target && reply.type_ != atoms.ATOM {
260 return Err("Clipboard data type mismatch".into());
261 }
262 buff.extend_from_slice(&reply.value);
263 break;
264 }
265
266 Event::PropertyNotify(event) if is_incr => {
267 if event.state != Property::NEW_VALUE {
268 continue;
269 };
270
271 let cookie =
272 ctx.conn
273 .get_property(false, ctx.win_id, property, AtomEnum::ATOM, 0, 0)?;
274
275 let length = cookie.reply()?.bytes_after;
276
277 let cookie = ctx.conn.get_property(
278 true,
279 ctx.win_id,
280 property,
281 AtomEnum::NONE,
282 0,
283 length,
284 )?;
285 let reply = cookie.reply()?;
286 if reply.type_ != target {
287 continue;
288 };
289
290 let value = reply.value;
291
292 if !value.is_empty() {
293 buff.extend_from_slice(&value);
294 } else {
295 break;
296 }
297 }
298 _ => (),
299 }
300 }
301 Ok(())
302 }
303}
304
305impl ClipboardContext {
306 pub fn new() -> Result<Self> {
307 Self::new_with_options(ClipboardContextX11Options {
308 read_timeout: Some(Duration::from_millis(DEFAULT_READ_TIMEOUT)),
309 })
310 }
311
312 pub fn new_with_options(options: ClipboardContextX11Options) -> Result<Self> {
313 let ctx = InnerContext::new()?;
315 let ctx_arc = Arc::new(ctx);
316 let ctx_clone = ctx_arc.clone();
317
318 thread::spawn(move || {
319 let res = process_server_req(&ctx_clone);
320 if let Err(e) = res {
321 println!("process_server_req error: {:?}", e);
322 }
323 });
324
325 Ok(Self {
326 inner: ctx_arc,
327 read_timeout: options.read_timeout,
328 })
329 }
330
331 fn read(&self, format: &Atom) -> Result<Vec<u8>> {
332 let ctx = &self.inner.server;
333 let atoms = ctx.atoms;
334 let clipboard = atoms.CLIPBOARD;
335 let win_id = ctx.win_id;
336 let cookie =
337 ctx.conn
338 .convert_selection(win_id, clipboard, *format, atoms.PROPERTY, CURRENT_TIME)?;
339 let sequence_num = cookie.sequence_number();
340 cookie.check()?;
341 let mut buff = Vec::new();
342
343 self.inner.process_event(
344 &mut buff,
345 clipboard,
346 *format,
347 atoms.PROPERTY,
348 self.read_timeout,
349 sequence_num,
350 )?;
351
352 ctx.conn.delete_property(win_id, atoms.PROPERTY)?.check()?;
353
354 Ok(buff)
355 }
356
357 fn write(&self, data: Vec<ClipboardData>) -> Result<()> {
358 let writer = self.inner.wait_write_data.write();
359 match writer {
360 Ok(mut writer) => {
361 writer.clear();
362 writer.extend(data);
363 }
364 Err(_) => return Err("Failed to write clipboard data".into()),
365 }
366 let ctx = &self.inner.server_for_write;
367 let atoms = ctx.atoms;
368
369 let win_id = ctx.win_id;
370 let clipboard = atoms.CLIPBOARD;
371 ctx.conn
372 .set_selection_owner(win_id, clipboard, CURRENT_TIME)?
373 .check()?;
374
375 if ctx
376 .conn
377 .get_selection_owner(clipboard)?
378 .reply()
379 .map(|reply| reply.owner == win_id)
380 .unwrap_or(false)
381 {
382 Ok(())
383 } else {
384 Err("Failed to take ownership of the clipboard".into())
385 }
386 }
387}
388
389fn process_server_req(context: &InnerContext) -> Result<()> {
390 let atoms = context.server_for_write.atoms;
391 loop {
392 match context
393 .server_for_write
394 .conn
395 .wait_for_event()
396 .map_err(|e| format!("wait_for_event error: {:?}", e))?
397 {
398 Event::DestroyNotify(_) => {
399 println!("Clipboard server window is being destroyed x_x");
401 break;
402 }
403 Event::SelectionClear(event) => {
404 println!("Somebody else owns the clipboard now");
407 if event.selection == atoms.CLIPBOARD {
408 context
410 .wait_write_data
411 .write()
412 .map(|mut writer| writer.clear())
413 .map_err(|e| format!("write clipboard data error: {:?}", e))?;
414 }
415 }
416 Event::SelectionRequest(event) => {
417 context
419 .handle_selection_request(event)
420 .map_err(|e| format!("handle_selection_request error: {:?}", e))?;
421 }
422 Event::SelectionNotify(event) => {
423 if event.selection != atoms.CLIPBOARD_MANAGER {
428 println!("Received a `SelectionNotify` from a selection other than the CLIPBOARD_MANAGER. This is unexpected in this thread.");
429 continue;
430 }
431 }
432 _event => {
433 }
436 }
437 }
438 Ok(())
439}
440
441impl Clipboard for ClipboardContext {
442 fn available_formats(&self) -> Result<Vec<String>> {
444 let ctx = &self.inner.server;
445 let atoms = ctx.atoms;
446 self.read(&atoms.TARGETS).map(|data| {
447 let mut formats = Vec::new();
448 let atom_list: Vec<Atom> = parse_atom_list(&data);
450 for atom in atom_list {
451 if self.inner.ignore_formats.contains(&atom) {
452 continue;
453 }
454 let atom_name = ctx.get_atom_name(atom).unwrap_or("Unknown".to_string());
455 formats.push(atom_name);
456 }
457 formats
458 })
459 }
460
461 fn has(&self, format: crate::ContentFormat) -> bool {
462 let ctx = &self.inner.server;
463 let atoms = ctx.atoms;
464 let atom_list = self.read(&atoms.TARGETS).map(|data| parse_atom_list(&data));
465 match atom_list {
466 Ok(formats) => match format {
467 ContentFormat::Text => formats.contains(&atoms.UTF8_STRING),
468 ContentFormat::Rtf => formats.contains(&atoms.RTF),
469 ContentFormat::Html => formats.contains(&atoms.HTML),
470 ContentFormat::Image => formats.contains(&atoms.PNG_MIME),
471 ContentFormat::Files => formats.contains(&atoms.FILE_LIST),
472 ContentFormat::Other(format_name) => {
473 let atom = ctx.get_atom(format_name.as_str());
474 match atom {
475 Ok(atom) => formats.contains(&atom),
476 Err(_) => false,
477 }
478 }
479 },
480 Err(_) => false,
481 }
482 }
483
484 fn clear(&self) -> Result<()> {
485 self.write(vec![])
486 }
487
488 fn get_buffer(&self, format: &str) -> Result<Vec<u8>> {
489 let atom = self.inner.server.get_atom(format);
490 match atom {
491 Ok(atom) => self.read(&atom),
492 Err(_) => Err("Invalid format".into()),
493 }
494 }
495
496 fn get_text(&self) -> Result<String> {
497 let atoms = self.inner.server.atoms;
498 let text_data = self.read(&atoms.UTF8_STRING);
499 text_data.map_or_else(
500 |_| Ok("".to_string()),
501 |data| Ok(String::from_utf8_lossy(&data).to_string()),
502 )
503 }
504
505 fn get_rich_text(&self) -> Result<String> {
506 let atoms = self.inner.server.atoms;
507 let rtf_data = self.read(&atoms.RTF);
508 rtf_data.map_or_else(
509 |_| Ok("".to_string()),
510 |data| Ok(String::from_utf8_lossy(&data).to_string()),
511 )
512 }
513
514 fn get_html(&self) -> Result<String> {
515 let atoms = self.inner.server.atoms;
516 let html_data = self.read(&atoms.HTML);
517 html_data.map_or_else(
518 |_| Ok("".to_string()),
519 |data| Ok(String::from_utf8_lossy(&data).to_string()),
520 )
521 }
522
523 fn get_image(&self) -> Result<crate::RustImageData> {
524 let atoms = self.inner.server.atoms;
525 let image_bytes = self.read(&atoms.PNG_MIME);
526 match image_bytes {
527 Ok(bytes) => {
528 let image = RustImageData::from_bytes(&bytes);
529 match image {
530 Ok(image) => Ok(image),
531 Err(_) => Err("Invalid image data".into()),
532 }
533 }
534 Err(_) => Err("No image data found".into()),
535 }
536 }
537
538 fn get_files(&self) -> Result<Vec<String>> {
539 let atoms = self.inner.server.atoms;
540 let file_list_data = self.read(&atoms.FILE_LIST);
541 file_list_data.map_or_else(
542 |_| Ok(vec![]),
543 |data| {
544 let file_list_str = String::from_utf8_lossy(&data).to_string();
545 let mut list = Vec::new();
546 for line in file_list_str.lines() {
547 if !line.starts_with(FILE_PATH_PREFIX) {
548 continue;
549 }
550 list.push(line.to_string())
551 }
552 Ok(list)
553 },
554 )
555 }
556
557 fn get(&self, formats: &[ContentFormat]) -> Result<Vec<ClipboardContent>> {
558 let mut contents = Vec::new();
559 for format in formats {
560 match format {
561 ContentFormat::Text => match self.get_text() {
562 Ok(text) => contents.push(ClipboardContent::Text(text)),
563 Err(_) => continue,
564 },
565 ContentFormat::Rtf => match self.get_rich_text() {
566 Ok(rtf) => contents.push(ClipboardContent::Rtf(rtf)),
567 Err(_) => continue,
568 },
569 ContentFormat::Html => match self.get_html() {
570 Ok(html) => contents.push(ClipboardContent::Html(html)),
571 Err(_) => continue,
572 },
573 ContentFormat::Image => match self.get_image() {
574 Ok(image) => contents.push(ClipboardContent::Image(image)),
575 Err(_) => continue,
576 },
577 ContentFormat::Files => match self.get_files() {
578 Ok(files) => contents.push(ClipboardContent::Files(files)),
579 Err(_) => continue,
580 },
581 ContentFormat::Other(format_name) => match self.get_buffer(format_name) {
582 Ok(buffer) => {
583 contents.push(ClipboardContent::Other(format_name.clone(), buffer))
584 }
585 Err(_) => continue,
586 },
587 }
588 }
589 Ok(contents)
590 }
591
592 fn set_buffer(&self, format: &str, buffer: Vec<u8>) -> Result<()> {
593 let atom = self.inner.server_for_write.get_atom(format)?;
594 let data = ClipboardData {
595 format: atom,
596 data: buffer,
597 };
598 self.write(vec![data])
599 }
600
601 fn set_text(&self, text: String) -> Result<()> {
602 let atoms = self.inner.server_for_write.atoms;
603 let text_bytes = text.as_bytes().to_vec();
604
605 let data = ClipboardData {
606 format: atoms.UTF8_STRING,
607 data: text_bytes,
608 };
609 self.write(vec![data])
610 }
611
612 fn set_rich_text(&self, text: String) -> Result<()> {
613 let atoms = self.inner.server_for_write.atoms;
614 let text_bytes = text.as_bytes().to_vec();
615
616 let data = ClipboardData {
617 format: atoms.RTF,
618 data: text_bytes,
619 };
620 self.write(vec![data])
621 }
622
623 fn set_html(&self, html: String) -> Result<()> {
624 let atoms = self.inner.server_for_write.atoms;
625 let html_bytes = html.as_bytes().to_vec();
626
627 let data = ClipboardData {
628 format: atoms.HTML,
629 data: html_bytes,
630 };
631 self.write(vec![data])
632 }
633
634 fn set_image(&self, image: RustImageData) -> Result<()> {
635 let atoms = self.inner.server_for_write.atoms;
636 let image_png = image.to_png()?;
637 let data = ClipboardData {
638 format: atoms.PNG_MIME,
639 data: image_png.get_bytes().to_vec(),
640 };
641 self.write(vec![data])
642 }
643
644 fn set_files(&self, files: Vec<String>) -> Result<()> {
645 let atoms = self.inner.server_for_write.atoms;
646 let data = file_uri_list_to_clipboard_data(files, atoms);
647 self.write(data)
648 }
649
650 fn set(&self, contents: Vec<ClipboardContent>) -> Result<()> {
651 let mut data = Vec::new();
652 let atoms = self.inner.server_for_write.atoms;
653 for content in contents {
654 match content {
655 ClipboardContent::Text(text) => {
656 data.push(ClipboardData {
657 format: atoms.UTF8_STRING,
658 data: text.as_bytes().to_vec(),
659 });
660 }
661 ClipboardContent::Rtf(rtf) => {
662 data.push(ClipboardData {
663 format: atoms.RTF,
664 data: rtf.as_bytes().to_vec(),
665 });
666 }
667 ClipboardContent::Html(html) => {
668 data.push(ClipboardData {
669 format: atoms.HTML,
670 data: html.as_bytes().to_vec(),
671 });
672 }
673 ClipboardContent::Image(image) => {
674 let image_png = image.to_png()?;
675 data.push(ClipboardData {
676 format: atoms.PNG_MIME,
677 data: image_png.get_bytes().to_vec(),
678 });
679 }
680 ClipboardContent::Files(files) => {
681 let data_arr = file_uri_list_to_clipboard_data(files, atoms);
682 data.extend(data_arr);
683 }
684 ClipboardContent::Other(format_name, buffer) => {
685 let atom = self.inner.server_for_write.get_atom(&format_name)?;
686 data.push(ClipboardData {
687 format: atom,
688 data: buffer,
689 });
690 }
691 }
692 }
693 self.write(data)
694 }
695}
696
697pub struct ClipboardWatcherContext<T: ClipboardHandler> {
698 handlers: Vec<T>,
699 stop_signal: Sender<()>,
700 stop_receiver: Receiver<()>,
701}
702
703unsafe impl<T: ClipboardHandler> Send for ClipboardWatcherContext<T> {}
704
705impl<T: ClipboardHandler> ClipboardWatcherContext<T> {
706 pub fn new() -> Result<Self> {
707 let (tx, rx) = mpsc::channel();
708 Ok(Self {
709 handlers: Vec::new(),
710 stop_signal: tx,
711 stop_receiver: rx,
712 })
713 }
714}
715
716impl<T: ClipboardHandler> ClipboardWatcher<T> for ClipboardWatcherContext<T> {
717 fn add_handler(&mut self, f: T) -> &mut Self {
718 self.handlers.push(f);
719 self
720 }
721
722 fn start_watch(&mut self) {
723 let watch_server = XServerContext::new().expect("Failed to create X server context");
724 let screen = watch_server
725 .conn
726 .setup()
727 .roots
728 .get(watch_server._screen)
729 .expect("Failed to get screen");
730
731 xfixes::query_version(&watch_server.conn, 5, 0)
732 .expect("Failed to query version xfixes is not available");
733 let cookie = xfixes::select_selection_input(
734 &watch_server.conn,
735 screen.root,
736 watch_server.atoms.CLIPBOARD,
737 xfixes::SelectionEventMask::SET_SELECTION_OWNER
738 | xfixes::SelectionEventMask::SELECTION_CLIENT_CLOSE
739 | xfixes::SelectionEventMask::SELECTION_WINDOW_DESTROY,
740 )
741 .expect("Failed to select selection input");
742
743 cookie.check().unwrap();
744
745 loop {
746 if self
747 .stop_receiver
748 .recv_timeout(Duration::from_millis(500))
749 .is_ok()
750 {
751 break;
752 }
753 let event = match watch_server
754 .conn
755 .poll_for_event()
756 .expect("Failed to poll for event")
757 {
758 Some(event) => event,
759 None => {
760 continue;
761 }
762 };
763 if let Event::XfixesSelectionNotify(_) = event {
764 self.handlers
765 .iter_mut()
766 .for_each(|handler| handler.on_clipboard_change());
767 }
768 }
769 }
770
771 fn get_shutdown_channel(&self) -> WatcherShutdown {
772 WatcherShutdown {
773 sender: self.stop_signal.clone(),
774 }
775 }
776}
777
778pub struct WatcherShutdown {
779 sender: Sender<()>,
780}
781
782impl Drop for WatcherShutdown {
783 fn drop(&mut self) {
784 let _ = self.sender.send(());
785 }
786}
787
788struct XServerContext {
789 conn: RustConnection,
790 win_id: u32,
791 _screen: usize,
792 atoms: Atoms,
793}
794
795impl XServerContext {
796 fn new() -> Result<Self> {
797 let (conn, screen) = x11rb::connect(None)?;
798 let win_id = conn.generate_id()?;
799 {
800 let screen = conn.setup().roots.get(screen).unwrap();
801 conn.create_window(
802 COPY_DEPTH_FROM_PARENT,
803 win_id,
804 screen.root,
805 0,
806 0,
807 1,
808 1,
809 0,
810 WindowClass::INPUT_OUTPUT,
811 screen.root_visual,
812 &CreateWindowAux::new()
813 .event_mask(EventMask::STRUCTURE_NOTIFY | EventMask::PROPERTY_CHANGE),
814 )?
815 .check()?;
816 }
817 let atoms = Atoms::new(&conn)?.reply()?;
818 Ok(Self {
819 conn,
820 win_id,
821 _screen: screen,
822 atoms,
823 })
824 }
825
826 fn get_atom(&self, format: &str) -> Result<Atom> {
827 let cookie = self.conn.intern_atom(false, format.as_bytes())?;
828 Ok(cookie.reply()?.atom)
829 }
830
831 fn get_atom_name(&self, atom: Atom) -> Result<String> {
832 let cookie = self.conn.get_atom_name(atom)?;
833 Ok(String::from_utf8_lossy(&cookie.reply()?.name).to_string())
834 }
835}
836
837fn parse_atom_list(data: &[u8]) -> Vec<Atom> {
839 data.chunks(4)
840 .map(|chunk| {
841 let mut bytes = [0u8; 4];
842 bytes.copy_from_slice(chunk);
843 u32::from_ne_bytes(bytes)
844 })
845 .collect()
846}
847
848fn file_uri_list_to_clipboard_data(file_list: Vec<String>, atoms: Atoms) -> Vec<ClipboardData> {
849 let uri_list: Vec<String> = file_list
850 .iter()
851 .map(|f| {
852 if f.starts_with(FILE_PATH_PREFIX) {
853 f.to_owned()
854 } else {
855 format!("{}{}", FILE_PATH_PREFIX, f)
856 }
857 })
858 .collect();
859 let uri_str_list: Vec<String> = file_list
861 .iter()
862 .map(|f| {
863 if let Some(stripped) = f.strip_prefix(FILE_PATH_PREFIX) {
864 stripped.to_owned()
865 } else {
866 f.to_owned()
867 }
868 })
869 .collect();
870
871 let data_text_plain = uri_str_list.join("\r\n");
872 let data_text_utf8 = uri_str_list.join("\n");
873 let data_text_uri_list = uri_list.join("\r\n");
874 let data_gnome_copied_files = ["copy\n", uri_list.join("\n").as_str()].concat();
875
876 vec![
877 ClipboardData {
878 format: atoms.TEXT_MIME_UNKNOWN,
879 data: data_text_plain.as_bytes().to_vec(),
880 },
881 ClipboardData {
882 format: atoms.UTF8_MIME_0,
883 data: data_text_plain.as_bytes().to_vec(),
884 },
885 ClipboardData {
886 format: atoms.STRING,
887 data: data_text_utf8.as_bytes().to_vec(),
888 },
889 ClipboardData {
890 format: atoms.TEXT,
891 data: data_text_utf8.as_bytes().to_vec(),
892 },
893 ClipboardData {
894 format: atoms.UTF8_STRING,
895 data: data_text_utf8.as_bytes().to_vec(),
896 },
897 ClipboardData {
898 format: atoms.FILE_LIST,
899 data: data_text_uri_list.as_bytes().to_vec(),
900 },
901 ClipboardData {
902 format: atoms.GNOME_COPY_FILES,
903 data: data_gnome_copied_files.as_bytes().to_vec(),
904 },
905 ClipboardData {
906 format: atoms.NAUTILUS_FILE_LIST,
907 data: data_gnome_copied_files.as_bytes().to_vec(),
908 },
909 ]
910}