Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:40:55.599Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

pasteboard.rs

539 lines · 18.8 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.