Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-renderer/src/windowing/winit/linux/clipboard.rs
1use std::ops::Not;
2 
3use arboard::{
4 self, Clipboard as LinuxClipboardInner, GetExtLinux, LinuxClipboardKind, SetExtLinux,
5};
6use zbus::zvariant::NoneValue;
7 
8use crate::{clipboard::ClipboardContent, Clipboard};
9 
10pub struct LinuxClipboard {
11 inner: LinuxClipboardInner,
12}
13 
14impl LinuxClipboard {
15 pub fn new() -> Result<Self, arboard::Error> {
16 Ok(Self {
17 inner: LinuxClipboardInner::new()?,
18 })
19 }
20}
21 
22impl Clipboard for LinuxClipboard {
23 fn write(&mut self, contents: ClipboardContent) {
24 if let Err(err) = self.write_to_specific_clipboard(LinuxClipboardKind::Clipboard, &contents)
25 {
26 if contents.html.is_some() {
27 log::warn!("Unable to set clipboard HTML: {err:?}");
28 } else {
29 log::warn!("Unable to set clipboard text: {err:?}");
30 }
31 }
32 }
33 
34 fn read(&mut self) -> ClipboardContent {
35 match self.read_from_specific_clipboard(LinuxClipboardKind::Clipboard) {
36 Ok(content) => content,
37 Err(err) => {
38 log::warn!("Failed to read from Linux clipboard: {err:?}");
39 ClipboardContent::null_value()
40 }
41 }
42 }
43 
44 fn write_to_primary_clipboard(&mut self, contents: ClipboardContent) {
45 match self.write_to_specific_clipboard(LinuxClipboardKind::Primary, &contents) {
46 Ok(_) => (),
47 Err(arboard::Error::ClipboardNotSupported) => {
48 log::info!(
49 "Primary clipboard is not supported, falling back to default clipboard."
50 );
51 // Try the default clipboard.
52 self.write(contents);
53 }
54 Err(err) => {
55 if contents.html.is_some() {
56 log::warn!("Unable to set primary clipboard HTML: {err:?}");
57 } else {
58 log::warn!("Unable to set primary clipboard text: {err:?}");
59 }
60 }
61 }
62 }
63 
64 fn read_from_primary_clipboard(&mut self) -> ClipboardContent {
65 match self.read_from_specific_clipboard(LinuxClipboardKind::Primary) {
66 Ok(content) => content,
67 Err(arboard::Error::ClipboardNotSupported) => {
68 log::info!(
69 "Primary clipboard is not supported, falling back to default clipboard."
70 );
71 // Try the default clipboard.
72 match self.read_from_specific_clipboard(LinuxClipboardKind::Clipboard) {
73 Ok(content) => content,
74 Err(err) => {
75 log::warn!("Unable to read from primary clipboard fallback: {err:?}");
76 ClipboardContent::null_value()
77 }
78 }
79 }
80 Err(err) => {
81 log::warn!("Unable to read from primary clipboard: {err:?}");
82 ClipboardContent::null_value()
83 }
84 }
85 }
86}
87 
88impl LinuxClipboard {
89 /// Parses Linux clipboard text for absolute file paths.
90 ///
91 /// When copying files, Linux file managers typically place the paths as text content onto
92 /// the clipboard. We parse this text to extract absolute paths, but if ANY line is not an
93 /// absolute path, we assume this is regular text content and return None (no paths).
94 fn parse_valid_filepaths_from_text(&mut self, text_content: &str) -> Option<Vec<String>> {
95 let mut file_paths = Vec::new();
96 
97 // Check for absolute filepaths
98 for line in text_content.trim().lines() {
99 let line = line.trim();
100 if line.is_empty() {
101 continue;
102 }
103 
104 let candidate_path_str = if let Some(uri_path) = line.strip_prefix("file://") {
105 match urlencoding::decode(uri_path) {
106 Ok(decoded_path) => decoded_path.into_owned(),
107 Err(_) => uri_path.to_string(),
108 }
109 } else {
110 line.to_string()
111 };
112 
113 let candidate_path = std::path::Path::new(&candidate_path_str);
114 if candidate_path.is_absolute() && candidate_path.exists() {
115 file_paths.push(candidate_path_str);
116 } else {
117 // Not an absolute-path indicates the text was not from copying files, so return
118 return None;
119 }
120 }
121 
122 if file_paths.is_empty() {
123 None
124 } else {
125 Some(file_paths)
126 }
127 }
128 
129 /// Reads clipboard content from a specific clipboard buffer.
130 fn read_from_specific_clipboard(
131 &mut self,
132 clipboard_kind: LinuxClipboardKind,
133 ) -> Result<ClipboardContent, arboard::Error> {
134 let text_result = self.inner.get().text();
135 let mut content = ClipboardContent {
136 plain_text: text_result.as_ref().map(|s| s.clone()).unwrap_or_default(),
137 ..Default::default()
138 };
139 
140 // Get file paths from clipboard (Linux-specific)
141 content.paths = self.parse_valid_filepaths_from_text(&content.plain_text);
142 
143 // Attempt to use HTML data first.
144 match self.inner.get().clipboard(clipboard_kind).html() {
145 Ok(html) => {
146 content.html = html.is_empty().not().then_some(html);
147 
148 // Try to get image content from clipboard
149 content.images = crate::clipboard_utils::read_images_from_clipboard(
150 &mut self.inner,
151 &content.html,
152 &content.plain_text,
153 );
154 
155 return Ok(content);
156 }
157 Err(err) => {
158 log::info!(
159 "Unable to read HTML from clipboard: {err:?}, falling back to plaintext."
160 );
161 }
162 }
163 
164 // Fallback to using plaintext
165 content.images = crate::clipboard_utils::read_images_from_clipboard(
166 &mut self.inner,
167 &None, // No HTML in fallback case
168 &content.plain_text,
169 );
170 
171 // Return success if we have ANY content (text, paths, OR images)
172 // Only error if ALL content types failed
173 if text_result.is_ok()
174 || content
175 .paths
176 .as_ref()
177 .is_some_and(|paths| !paths.is_empty())
178 || content.images.as_ref().is_some_and(|imgs| !imgs.is_empty())
179 {
180 Ok(content)
181 } else {
182 // All content types failed - return the text error
183 text_result.map(|_| content)
184 }
185 }
186 
187 fn write_to_specific_clipboard(
188 &mut self,
189 clipboard_kind: LinuxClipboardKind,
190 contents: &ClipboardContent,
191 ) -> Result<(), arboard::Error> {
192 if let Some(html) = &contents.html {
193 self.inner
194 .set()
195 .clipboard(clipboard_kind)
196 .html(html, Some(&contents.plain_text))
197 } else {
198 self.inner
199 .set()
200 .clipboard(clipboard_kind)
201 .text(&contents.plain_text)
202 }
203 }
204}
205 
206#[cfg(test)]
207#[path = "clipboard_tests.rs"]
208mod tests;
209