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