Skip to repository content539 lines · 18.8 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T03:40:55.599Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
pasteboard.rs
1use core::slice;
2use std::ffi::{CStr, c_void};
3use std::path::PathBuf;
4
5use cocoa::{
6 appkit::{
7 NSFilenamesPboardType, NSPasteboard, NSPasteboardTypePNG, NSPasteboardTypeString,
8 NSPasteboardTypeTIFF,
9 },
10 base::{id, nil},
11 foundation::{NSArray, NSData, NSFastEnumeration, NSString},
12};
13use objc::{msg_send, rc::StrongPtr, runtime::Object, sel, sel_impl};
14use smallvec::SmallVec;
15use strum::IntoEnumIterator as _;
16
17use crate::ns_string;
18use gpui::{
19 ClipboardEntry, ClipboardItem, ClipboardString, ExternalPaths, Image, ImageFormat, hash,
20};
21
22pub struct Pasteboard {
23 inner: StrongPtr,
24 text_hash_type: StrongPtr,
25 metadata_type: StrongPtr,
26}
27
28impl Pasteboard {
29 pub fn general() -> Self {
30 unsafe { Self::new(NSPasteboard::generalPasteboard(nil)) }
31 }
32
33 pub fn find() -> Self {
34 unsafe { Self::new(NSPasteboard::pasteboardWithName(nil, NSPasteboardNameFind)) }
35 }
36
37 #[cfg(test)]
38 pub fn unique() -> Self {
39 unsafe { Self::new(NSPasteboard::pasteboardWithUniqueName(nil)) }
40 }
41
42 unsafe fn new(inner: id) -> Self {
43 // These constructors return autoreleased objects, but a Pasteboard can
44 // outlive the autorelease pool in which it was created.
45 Self {
46 inner: unsafe { StrongPtr::retain(inner) },
47 text_hash_type: unsafe { StrongPtr::retain(ns_string("zed-text-hash")) },
48 metadata_type: unsafe { StrongPtr::retain(ns_string("zed-metadata")) },
49 }
50 }
51
52 pub fn read(&self) -> Option<ClipboardItem> {
53 unsafe {
54 // Check for file paths first
55 let filenames = NSPasteboard::propertyListForType(*self.inner, NSFilenamesPboardType);
56 if filenames != nil && NSArray::count(filenames) > 0 {
57 let mut paths = SmallVec::new();
58 for file in filenames.iter() {
59 let f = NSString::UTF8String(file);
60 let path = CStr::from_ptr(f).to_string_lossy().into_owned();
61 paths.push(PathBuf::from(path));
62 }
63 if !paths.is_empty() {
64 let mut entries = vec![ClipboardEntry::ExternalPaths(ExternalPaths(paths))];
65
66 // Also include the string representation so text editors can
67 // paste the path as text.
68 if let Some(string_item) = self.read_string_from_pasteboard() {
69 entries.push(string_item);
70 }
71
72 return Some(ClipboardItem { entries });
73 }
74 }
75
76 // Next, check for a plain string.
77 if let Some(string_entry) = self.read_string_from_pasteboard() {
78 return Some(ClipboardItem {
79 entries: vec![string_entry],
80 });
81 }
82
83 // Finally, try the various supported image types.
84 for format in ImageFormat::iter() {
85 if let Some(item) = self.read_image(format) {
86 return Some(item);
87 }
88 }
89 }
90
91 None
92 }
93
94 fn read_image(&self, format: ImageFormat) -> Option<ClipboardItem> {
95 let ut_type: UTType = format.into();
96
97 unsafe {
98 let types: id = self.inner.types();
99 if msg_send![types, containsObject: ut_type.inner()] {
100 self.data_for_type(ut_type.inner_mut()).map(|bytes| {
101 let bytes = bytes.to_vec();
102 let id = hash(&bytes);
103
104 ClipboardItem {
105 entries: vec![ClipboardEntry::Image(Image { format, bytes, id })],
106 }
107 })
108 } else {
109 None
110 }
111 }
112 }
113
114 unsafe fn read_string_from_pasteboard(&self) -> Option<ClipboardEntry> {
115 unsafe {
116 let pasteboard_types: id = self.inner.types();
117 let string_type: id = ns_string("public.utf8-plain-text");
118
119 if !msg_send![pasteboard_types, containsObject: string_type] {
120 return None;
121 }
122
123 let text_bytes = self.data_for_type(string_type)?;
124
125 let text = String::from_utf8_lossy(&text_bytes).to_string();
126 let metadata = self
127 .data_for_type(*self.text_hash_type)
128 .and_then(|hash_bytes| {
129 let hash_bytes = hash_bytes.as_slice().try_into().ok()?;
130 let hash = u64::from_be_bytes(hash_bytes);
131 let metadata = self.data_for_type(*self.metadata_type)?;
132
133 if hash == ClipboardString::text_hash(&text) {
134 String::from_utf8(metadata).ok()
135 } else {
136 None
137 }
138 });
139
140 Some(ClipboardEntry::String(ClipboardString { text, metadata }))
141 }
142 }
143
144 unsafe fn data_for_type(&self, kind: id) -> Option<Vec<u8>> {
145 unsafe {
146 let data = self.inner.dataForType(kind);
147 if data == nil {
148 None
149 } else if data.bytes().is_null() {
150 Some(Vec::new())
151 } else {
152 Some(
153 slice::from_raw_parts(data.bytes() as *mut u8, data.length() as usize).to_vec(),
154 )
155 }
156 }
157 }
158
159 pub fn write(&self, item: ClipboardItem) {
160 unsafe {
161 match item.entries.as_slice() {
162 [] => {
163 // Writing an empty list of entries just clears the clipboard.
164 self.inner.clearContents();
165 }
166 [ClipboardEntry::String(string)] => {
167 self.write_plaintext(string);
168 }
169 [ClipboardEntry::Image(image)] => {
170 self.write_image(image);
171 }
172 [ClipboardEntry::ExternalPaths(_)] => {}
173 _ => {
174 // Agus NB: We're currently only writing string entries to the clipboard when we have more than one.
175 //
176 // This was the existing behavior before I refactored the outer clipboard code:
177 // https://github.com/zed-industries/zed/blob/65f7412a0265552b06ce122655369d6cc7381dd6/crates/gpui/src/platform/mac/platform.rs#L1060-L1110
178 //
179 // Note how `any_images` is always `false`. We should fix that, but that's orthogonal to the refactor.
180
181 let mut combined = ClipboardString {
182 text: String::new(),
183 metadata: None,
184 };
185
186 for entry in item.entries {
187 match entry {
188 ClipboardEntry::String(text) => {
189 combined.text.push_str(&text.text());
190 if combined.metadata.is_none() {
191 combined.metadata = text.metadata;
192 }
193 }
194 _ => {}
195 }
196 }
197
198 self.write_plaintext(&combined);
199 }
200 }
201 }
202 }
203
204 fn write_plaintext(&self, string: &ClipboardString) {
205 unsafe {
206 self.inner.clearContents();
207
208 let text_bytes = NSData::dataWithBytes_length_(
209 nil,
210 string.text.as_ptr() as *const c_void,
211 string.text.len() as u64,
212 );
213 self.inner
214 .setData_forType(text_bytes, NSPasteboardTypeString);
215
216 if let Some(metadata) = string.metadata.as_ref() {
217 let hash_bytes = ClipboardString::text_hash(&string.text).to_be_bytes();
218 let hash_bytes = NSData::dataWithBytes_length_(
219 nil,
220 hash_bytes.as_ptr() as *const c_void,
221 hash_bytes.len() as u64,
222 );
223 self.inner.setData_forType(hash_bytes, *self.text_hash_type);
224
225 let metadata_bytes = NSData::dataWithBytes_length_(
226 nil,
227 metadata.as_ptr() as *const c_void,
228 metadata.len() as u64,
229 );
230 self.inner
231 .setData_forType(metadata_bytes, *self.metadata_type);
232 }
233 }
234 }
235
236 unsafe fn write_image(&self, image: &Image) {
237 unsafe {
238 self.inner.clearContents();
239
240 let bytes = NSData::dataWithBytes_length_(
241 nil,
242 image.bytes.as_ptr() as *const c_void,
243 image.bytes.len() as u64,
244 );
245
246 self.inner
247 .setData_forType(bytes, Into::<UTType>::into(image.format).inner_mut());
248 }
249 }
250}
251
252#[link(name = "AppKit", kind = "framework")]
253unsafe extern "C" {
254 /// [Apple's documentation](https://developer.apple.com/documentation/appkit/nspasteboardnamefind?language=objc)
255 pub static NSPasteboardNameFind: id;
256}
257
258impl From<ImageFormat> for UTType {
259 fn from(value: ImageFormat) -> Self {
260 match value {
261 ImageFormat::Png => Self::png(),
262 ImageFormat::Jpeg => Self::jpeg(),
263 ImageFormat::Tiff => Self::tiff(),
264 ImageFormat::Webp => Self::webp(),
265 ImageFormat::Gif => Self::gif(),
266 ImageFormat::Bmp => Self::bmp(),
267 ImageFormat::Svg => Self::svg(),
268 ImageFormat::Ico => Self::ico(),
269 ImageFormat::Pnm => Self::pnm(),
270 }
271 }
272}
273
274// See https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/
275pub struct UTType(id);
276
277impl UTType {
278 pub fn png() -> Self {
279 // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/png
280 Self(unsafe { NSPasteboardTypePNG }) // This is a rare case where there's a built-in NSPasteboardType
281 }
282
283 pub fn jpeg() -> Self {
284 // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/jpeg
285 Self(unsafe { ns_string("public.jpeg") })
286 }
287
288 pub fn gif() -> Self {
289 // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/gif
290 Self(unsafe { ns_string("com.compuserve.gif") })
291 }
292
293 pub fn webp() -> Self {
294 // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/webp
295 Self(unsafe { ns_string("org.webmproject.webp") })
296 }
297
298 pub fn bmp() -> Self {
299 // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/bmp
300 Self(unsafe { ns_string("com.microsoft.bmp") })
301 }
302
303 pub fn svg() -> Self {
304 // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/svg
305 Self(unsafe { ns_string("public.svg-image") })
306 }
307
308 pub fn ico() -> Self {
309 // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/ico
310 Self(unsafe { ns_string("com.microsoft.ico") })
311 }
312
313 pub fn tiff() -> Self {
314 // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/tiff
315 Self(unsafe { NSPasteboardTypeTIFF }) // This is a rare case where there's a built-in NSPasteboardType
316 }
317
318 pub fn pnm() -> Self {
319 //https://en.wikipedia.org/w/index.php?title=Netpbm&oldid=1336679433 under Uniform Type Identifier
320 Self(unsafe { ns_string("public.pbm") })
321 }
322
323 fn inner(&self) -> *const Object {
324 self.0
325 }
326
327 pub fn inner_mut(&self) -> *mut Object {
328 self.0 as *mut _
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use cocoa::{
335 appkit::{NSFilenamesPboardType, NSPasteboard, NSPasteboardTypeString},
336 base::{id, nil},
337 foundation::{NSArray, NSData},
338 };
339 use std::ffi::c_void;
340
341 use gpui::{ClipboardEntry, ClipboardItem, ClipboardString, ImageFormat};
342 use objc::rc::autoreleasepool;
343
344 use super::*;
345
346 unsafe fn simulate_external_file_copy(pasteboard: &Pasteboard, paths: &[&str]) {
347 unsafe {
348 let ns_paths: Vec<id> = paths.iter().map(|p| ns_string(p)).collect();
349 let ns_array = NSArray::arrayWithObjects(nil, &ns_paths);
350
351 let mut types = vec![NSFilenamesPboardType];
352 types.push(NSPasteboardTypeString);
353
354 let types_array = NSArray::arrayWithObjects(nil, &types);
355 pasteboard.inner.declareTypes_owner(types_array, nil);
356
357 pasteboard
358 .inner
359 .setPropertyList_forType(ns_array, NSFilenamesPboardType);
360
361 let joined = paths.join("\n");
362 let bytes = NSData::dataWithBytes_length_(
363 nil,
364 joined.as_ptr() as *const c_void,
365 joined.len() as u64,
366 );
367 pasteboard
368 .inner
369 .setData_forType(bytes, NSPasteboardTypeString);
370 }
371 }
372
373 #[test]
374 fn test_string() {
375 let pasteboard = Pasteboard::unique();
376 assert_eq!(pasteboard.read(), None);
377
378 let item = ClipboardItem::new_string("1".to_string());
379 pasteboard.write(item.clone());
380 assert_eq!(pasteboard.read(), Some(item));
381
382 let item = ClipboardItem {
383 entries: vec![ClipboardEntry::String(
384 ClipboardString::new("2".to_string()).with_json_metadata(vec![3, 4]),
385 )],
386 };
387 pasteboard.write(item.clone());
388 assert_eq!(pasteboard.read(), Some(item));
389
390 let text_from_other_app = "text from other app";
391 unsafe {
392 let bytes = NSData::dataWithBytes_length_(
393 nil,
394 text_from_other_app.as_ptr() as *const c_void,
395 text_from_other_app.len() as u64,
396 );
397 pasteboard
398 .inner
399 .setData_forType(bytes, NSPasteboardTypeString);
400 }
401 assert_eq!(
402 pasteboard.read(),
403 Some(ClipboardItem::new_string(text_from_other_app.to_string()))
404 );
405 }
406
407 #[test]
408 fn test_custom_types_survive_creation_autorelease_pool() {
409 let pasteboard = autoreleasepool(|| unsafe { Pasteboard::new(nil) });
410
411 unsafe {
412 let text_hash_type = CStr::from_ptr(NSString::UTF8String(*pasteboard.text_hash_type));
413 let metadata_type = CStr::from_ptr(NSString::UTF8String(*pasteboard.metadata_type));
414 assert_eq!(text_hash_type.to_bytes(), b"zed-text-hash");
415 assert_eq!(metadata_type.to_bytes(), b"zed-metadata");
416 }
417 }
418
419 #[test]
420 fn test_read_external_path() {
421 let pasteboard = Pasteboard::unique();
422
423 unsafe {
424 simulate_external_file_copy(&pasteboard, &["/test.txt"]);
425 }
426
427 let item = pasteboard.read().expect("should read clipboard item");
428
429 // Test both ExternalPaths and String entries exist
430 assert_eq!(item.entries.len(), 2);
431
432 // Test first entry is ExternalPaths
433 match &item.entries[0] {
434 ClipboardEntry::ExternalPaths(ep) => {
435 assert_eq!(ep.paths(), &[PathBuf::from("/test.txt")]);
436 }
437 other => panic!("expected ExternalPaths, got {:?}", other),
438 }
439
440 // Test second entry is String
441 match &item.entries[1] {
442 ClipboardEntry::String(s) => {
443 assert_eq!(s.text(), "/test.txt");
444 }
445 other => panic!("expected String, got {:?}", other),
446 }
447 }
448
449 #[test]
450 fn test_read_external_paths_with_spaces() {
451 let pasteboard = Pasteboard::unique();
452 let paths = ["/some file with spaces.txt"];
453
454 unsafe {
455 simulate_external_file_copy(&pasteboard, &paths);
456 }
457
458 let item = pasteboard.read().expect("should read clipboard item");
459
460 match &item.entries[0] {
461 ClipboardEntry::ExternalPaths(ep) => {
462 assert_eq!(ep.paths(), &[PathBuf::from("/some file with spaces.txt")]);
463 }
464 other => panic!("expected ExternalPaths, got {:?}", other),
465 }
466 }
467
468 #[test]
469 fn test_read_multiple_external_paths() {
470 let pasteboard = Pasteboard::unique();
471 let paths = ["/file.txt", "/image.png"];
472
473 unsafe {
474 simulate_external_file_copy(&pasteboard, &paths);
475 }
476
477 let item = pasteboard.read().expect("should read clipboard item");
478 assert_eq!(item.entries.len(), 2);
479
480 // Test both ExternalPaths and String entries exist
481 match &item.entries[0] {
482 ClipboardEntry::ExternalPaths(ep) => {
483 assert_eq!(
484 ep.paths(),
485 &[PathBuf::from("/file.txt"), PathBuf::from("/image.png"),]
486 );
487 }
488 other => panic!("expected ExternalPaths, got {:?}", other),
489 }
490
491 match &item.entries[1] {
492 ClipboardEntry::String(s) => {
493 assert_eq!(s.text(), "/file.txt\n/image.png");
494 assert_eq!(s.metadata, None);
495 }
496 other => panic!("expected String, got {:?}", other),
497 }
498 }
499
500 #[test]
501 fn test_read_image() {
502 let pasteboard = Pasteboard::unique();
503
504 // Smallest valid PNG: 1x1 transparent pixel
505 let png_bytes: &[u8] = &[
506 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48,
507 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00,
508 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, 0x54, 0x78,
509 0x9C, 0x62, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0xE5, 0x27, 0xDE, 0xFC, 0x00, 0x00,
510 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
511 ];
512
513 unsafe {
514 let ns_png_type = NSPasteboardTypePNG;
515 let types_array = NSArray::arrayWithObjects(nil, &[ns_png_type]);
516 pasteboard.inner.declareTypes_owner(types_array, nil);
517
518 let data = NSData::dataWithBytes_length_(
519 nil,
520 png_bytes.as_ptr() as *const c_void,
521 png_bytes.len() as u64,
522 );
523 pasteboard.inner.setData_forType(data, ns_png_type);
524 }
525
526 let item = pasteboard.read().expect("should read PNG image");
527
528 // Test Image entry exists
529 assert_eq!(item.entries.len(), 1);
530 match &item.entries[0] {
531 ClipboardEntry::Image(img) => {
532 assert_eq!(img.format, ImageFormat::Png);
533 assert_eq!(img.bytes, png_bytes);
534 }
535 other => panic!("expected Image, got {:?}", other),
536 }
537 }
538}
539