@@ -166,7 +166,7 @@ impl<'a> TransferManager<'a> { |
| 166 | 166 | Ok(Some((data, mime_type))) |
| 167 | 167 | } |
| 168 | 168 | |
| 169 | | - /// Request clipboard content (text or image) |
| 169 | + /// Request clipboard content (files, images, or text) |
| 170 | 170 | pub fn request_content(&self, selection: Atom) -> Result<Option<ClipboardContent>> { |
| 171 | 171 | let atoms = self.atoms(); |
| 172 | 172 | |
@@ -176,7 +176,14 @@ impl<'a> TransferManager<'a> { |
| 176 | 176 | return Ok(None); |
| 177 | 177 | } |
| 178 | 178 | |
| 179 | | - // Prefer images over text (images often have text alternatives) |
| 179 | + // Prefer files first (most specialized content type) |
| 180 | + if let Some(file_target) = atoms.preferred_file_target(&targets) { |
| 181 | + if let Some((uris, is_cut)) = self.request_files_target(selection, file_target)? { |
| 182 | + return Ok(Some(ClipboardContent::Files { uris, is_cut })); |
| 183 | + } |
| 184 | + } |
| 185 | + |
| 186 | + // Then images (images often have text alternatives) |
| 180 | 187 | if let Some(img_target) = atoms.preferred_image_target(&targets) { |
| 181 | 188 | if let Some((data, mime)) = self.request_image_target(selection, img_target)? { |
| 182 | 189 | return Ok(Some(ClipboardContent::Image { data, mime_type: mime })); |
@@ -242,6 +249,56 @@ impl<'a> TransferManager<'a> { |
| 242 | 249 | Ok(Some((data, mime_type))) |
| 243 | 250 | } |
| 244 | 251 | |
| 252 | + /// Request file URIs from clipboard |
| 253 | + fn request_files_target( |
| 254 | + &self, |
| 255 | + selection: Atom, |
| 256 | + target: Atom, |
| 257 | + ) -> Result<Option<(Vec<String>, bool)>> { |
| 258 | + let atoms = self.atoms(); |
| 259 | + |
| 260 | + self.conn().convert_selection( |
| 261 | + self.window(), |
| 262 | + selection, |
| 263 | + target, |
| 264 | + atoms.garclip_data, |
| 265 | + CURRENT_TIME, |
| 266 | + )?; |
| 267 | + self.conn().flush()?; |
| 268 | + |
| 269 | + let event = self.wait_for_selection_notify(selection, target)?; |
| 270 | + if event.property == x11rb::NONE { |
| 271 | + return Ok(None); |
| 272 | + } |
| 273 | + |
| 274 | + let data = self.read_property_string(atoms.garclip_data)?; |
| 275 | + |
| 276 | + // Parse based on format |
| 277 | + if target == atoms.gnome_copied_files { |
| 278 | + // Format: "copy\nfile:///path1\nfile:///path2" or "cut\n..." |
| 279 | + let mut lines = data.lines(); |
| 280 | + let action = lines.next().unwrap_or("copy"); |
| 281 | + let is_cut = action == "cut"; |
| 282 | + let uris: Vec<String> = lines.map(|s| s.to_string()).collect(); |
| 283 | + if uris.is_empty() { |
| 284 | + return Ok(None); |
| 285 | + } |
| 286 | + Ok(Some((uris, is_cut))) |
| 287 | + } else { |
| 288 | + // text/uri-list format: "file:///path1\r\nfile:///path2\r\n" |
| 289 | + let uris: Vec<String> = data |
| 290 | + .lines() |
| 291 | + .map(|s| s.trim_end_matches('\r').to_string()) |
| 292 | + .filter(|s| !s.is_empty() && !s.starts_with('#')) |
| 293 | + .collect(); |
| 294 | + if uris.is_empty() { |
| 295 | + return Ok(None); |
| 296 | + } |
| 297 | + // text/uri-list doesn't indicate cut, assume copy |
| 298 | + Ok(Some((uris, false))) |
| 299 | + } |
| 300 | + } |
| 301 | + |
| 245 | 302 | /// Wait for a SelectionNotify event |
| 246 | 303 | fn wait_for_selection_notify( |
| 247 | 304 | &self, |
@@ -358,6 +415,15 @@ impl<'a> TransferManager<'a> { |
| 358 | 415 | targets.push(atoms.image_png); |
| 359 | 416 | } |
| 360 | 417 | } |
| 418 | + ClipboardContent::Files { is_cut, .. } => { |
| 419 | + targets.extend(atoms.supported_file_targets()); |
| 420 | + // Also offer text formats for compatibility |
| 421 | + targets.extend(atoms.supported_text_targets()); |
| 422 | + // Add KDE cut indicator if this is a cut operation |
| 423 | + if *is_cut { |
| 424 | + targets.push(atoms.kde_cut_selection); |
| 425 | + } |
| 426 | + } |
| 361 | 427 | } |
| 362 | 428 | |
| 363 | 429 | self.conn().change_property32( |
@@ -376,6 +442,48 @@ impl<'a> TransferManager<'a> { |
| 376 | 442 | Atom::from(x11rb::protocol::xproto::AtomEnum::INTEGER), |
| 377 | 443 | &[CURRENT_TIME], |
| 378 | 444 | )?; |
| 445 | + } else if atoms.is_file_target(target) { |
| 446 | + // Respond with file URIs |
| 447 | + match content { |
| 448 | + ClipboardContent::Files { uris, is_cut } => { |
| 449 | + let data = if target == atoms.gnome_copied_files { |
| 450 | + // x-special/gnome-copied-files format |
| 451 | + let action = if *is_cut { "cut" } else { "copy" }; |
| 452 | + std::iter::once(action.to_string()) |
| 453 | + .chain(uris.iter().cloned()) |
| 454 | + .collect::<Vec<_>>() |
| 455 | + .join("\n") |
| 456 | + } else { |
| 457 | + // text/uri-list format |
| 458 | + uris.iter() |
| 459 | + .map(|uri| format!("{}\r\n", uri)) |
| 460 | + .collect::<String>() |
| 461 | + }; |
| 462 | + self.conn().change_property8( |
| 463 | + PropMode::REPLACE, |
| 464 | + event.requestor, |
| 465 | + property, |
| 466 | + target, |
| 467 | + data.as_bytes(), |
| 468 | + )?; |
| 469 | + } |
| 470 | + _ => success = false, |
| 471 | + } |
| 472 | + } else if target == atoms.kde_cut_selection { |
| 473 | + // KDE cut selection indicator |
| 474 | + match content { |
| 475 | + ClipboardContent::Files { is_cut, .. } => { |
| 476 | + let data = if *is_cut { "1" } else { "0" }; |
| 477 | + self.conn().change_property8( |
| 478 | + PropMode::REPLACE, |
| 479 | + event.requestor, |
| 480 | + property, |
| 481 | + target, |
| 482 | + data.as_bytes(), |
| 483 | + )?; |
| 484 | + } |
| 485 | + _ => success = false, |
| 486 | + } |
| 379 | 487 | } else if atoms.is_text_target(target) { |
| 380 | 488 | // Respond with text |
| 381 | 489 | match content { |
@@ -388,6 +496,17 @@ impl<'a> TransferManager<'a> { |
| 388 | 496 | text.as_bytes(), |
| 389 | 497 | )?; |
| 390 | 498 | } |
| 499 | + ClipboardContent::Files { uris, .. } => { |
| 500 | + // For text targets, send URIs as newline-separated text |
| 501 | + let text = uris.join("\n"); |
| 502 | + self.conn().change_property8( |
| 503 | + PropMode::REPLACE, |
| 504 | + event.requestor, |
| 505 | + property, |
| 506 | + target, |
| 507 | + text.as_bytes(), |
| 508 | + )?; |
| 509 | + } |
| 391 | 510 | _ => success = false, |
| 392 | 511 | } |
| 393 | 512 | } else if atoms.is_image_target(target) { |