1use std::sync::LazyLock;
2
3use anyhow::Result;
4use collections::FxHashMap;
5use itertools::Itertools;
6use windows::Win32::{
7 Foundation::{HANDLE, HGLOBAL},
8 System::{
9 DataExchange::{
10 CloseClipboard, CountClipboardFormats, EmptyClipboard, EnumClipboardFormats,
11 GetClipboardData, GetClipboardFormatNameW, OpenClipboard, RegisterClipboardFormatW,
12 SetClipboardData,
13 },
14 Memory::{GMEM_MOVEABLE, GlobalAlloc, GlobalLock, GlobalSize, GlobalUnlock},
15 Ole::{CF_DIB, CF_HDROP, CF_UNICODETEXT},
16 },
17 UI::Shell::{DragQueryFileW, HDROP},
18};
19use windows::core::{Owned, PCWSTR};
20
21use gpui::{
22 ClipboardEntry, ClipboardItem, ClipboardString, ExternalPaths, Image, ImageFormat, hash,
23};
24
25const DRAGDROP_GET_FILES_COUNT: u32 = 0xFFFFFFFF;
26
27static CLIPBOARD_HASH_FORMAT: LazyLock<u32> =
28 LazyLock::new(|| register_clipboard_format(windows::core::w!("GPUI internal text hash")));
29static CLIPBOARD_METADATA_FORMAT: LazyLock<u32> =
30 LazyLock::new(|| register_clipboard_format(windows::core::w!("GPUI internal metadata")));
31static CLIPBOARD_SVG_FORMAT: LazyLock<u32> =
32 LazyLock::new(|| register_clipboard_format(windows::core::w!("image/svg+xml")));
33static CLIPBOARD_GIF_FORMAT: LazyLock<u32> =
34 LazyLock::new(|| register_clipboard_format(windows::core::w!("GIF")));
35static CLIPBOARD_PNG_FORMAT: LazyLock<u32> =
36 LazyLock::new(|| register_clipboard_format(windows::core::w!("PNG")));
37static CLIPBOARD_JPG_FORMAT: LazyLock<u32> =
38 LazyLock::new(|| register_clipboard_format(windows::core::w!("JFIF")));
39
40static IMAGE_FORMATS_MAP: LazyLock<FxHashMap<u32, ImageFormat>> = LazyLock::new(|| {
41 let mut map = FxHashMap::default();
42 map.insert(*CLIPBOARD_PNG_FORMAT, ImageFormat::Png);
43 map.insert(*CLIPBOARD_GIF_FORMAT, ImageFormat::Gif);
44 map.insert(*CLIPBOARD_JPG_FORMAT, ImageFormat::Jpeg);
45 map.insert(*CLIPBOARD_SVG_FORMAT, ImageFormat::Svg);
46 map
47});
48
49fn register_clipboard_format(format: PCWSTR) -> u32 {
50 let ret = unsafe { RegisterClipboardFormatW(format) };
51 if ret == 0 {
52 panic!(
53 "Error when registering clipboard format: {}",
54 std::io::Error::last_os_error()
55 );
56 }
57 log::debug!(
58 "Registered clipboard format {} as {}",
59 unsafe { format.display() },
60 ret
61 );
62 ret
63}
64
65fn get_clipboard_data(format: u32) -> Option<LockedGlobal> {
66 let global = HGLOBAL(unsafe { GetClipboardData(format).ok() }?.0);
67 LockedGlobal::lock(global)
68}
69
70pub(crate) fn write_to_clipboard(item: ClipboardItem) {
71 let Some(_clip) = ClipboardGuard::open() else {
72 return;
73 };
74
75 let result: Result<()> = (|| {
76 unsafe { EmptyClipboard()? };
77 for entry in item.entries() {
78 match entry {
79 ClipboardEntry::String(string) => write_string(string)?,
80 ClipboardEntry::Image(image) => write_image(image)?,
81 ClipboardEntry::ExternalPaths(_) => {}
82 }
83 }
84 Ok(())
85 })();
86
87 if let Err(e) = result {
88 log::error!("Failed to write to clipboard: {e}");
89 }
90}
91
92pub(crate) fn read_from_clipboard() -> Option<ClipboardItem> {
93 let _clip = ClipboardGuard::open()?;
94
95 let mut entries = Vec::new();
96 let mut have_text = false;
97 let mut have_image = false;
98 let mut have_files = false;
99
100 let count = unsafe { CountClipboardFormats() };
101 let mut format = 0;
102 for _ in 0..count {
103 format = unsafe { EnumClipboardFormats(format) };
104
105 if !have_text && format == CF_UNICODETEXT.0 as u32 {
106 if let Some(entry) = read_string() {
107 entries.push(entry);
108 have_text = true;
109 }
110 } else if !have_image && is_image_format(format) {
111 if let Some(entry) = read_image(format) {
112 entries.push(entry);
113 have_image = true;
114 }
115 } else if !have_files && format == CF_HDROP.0 as u32 {
116 if let Some(entry) = read_files() {
117 entries.push(entry);
118 have_files = true;
119 }
120 }
121 }
122
123 if entries.is_empty() {
124 log_unsupported_clipboard_formats();
125 return None;
126 }
127 Some(ClipboardItem { entries })
128}
129
130pub(crate) fn with_file_names<F>(hdrop: HDROP, mut f: F)
131where
132 F: FnMut(String),
133{
134 let file_count = unsafe { DragQueryFileW(hdrop, DRAGDROP_GET_FILES_COUNT, None) };
135 for file_index in 0..file_count {
136 let filename_length = unsafe { DragQueryFileW(hdrop, file_index, None) } as usize;
137 let mut buffer = vec![0u16; filename_length + 1];
138 let ret = unsafe { DragQueryFileW(hdrop, file_index, Some(buffer.as_mut_slice())) };
139 if ret == 0 {
140 log::error!("unable to read file name of dragged file");
141 continue;
142 }
143 match String::from_utf16(&buffer[0..filename_length]) {
144 Ok(file_name) => f(file_name),
145 Err(e) => log::error!("dragged file name is not UTF-16: {}", e),
146 }
147 }
148}
149
150fn set_clipboard_bytes<T>(data: &[T], format: u32) -> Result<()> {
151 unsafe {
152 let global = Owned::new(GlobalAlloc(GMEM_MOVEABLE, std::mem::size_of_val(data))?);
153 let ptr = GlobalLock(*global);
154 anyhow::ensure!(!ptr.is_null(), "GlobalLock returned null");
155 std::ptr::copy_nonoverlapping(data.as_ptr(), ptr as _, data.len());
156 GlobalUnlock(*global).ok();
157 SetClipboardData(format, Some(HANDLE(global.0)))?;
158 // SetClipboardData succeeded — the system now owns the memory.
159 std::mem::forget(global);
160 }
161 Ok(())
162}
163
164fn get_clipboard_string(format: u32) -> Option<String> {
165 let locked = get_clipboard_data(format)?;
166 let bytes = locked.as_bytes();
167 let words_len = bytes.len() / std::mem::size_of::<u16>();
168 if words_len == 0 {
169 return Some(String::new());
170 }
171 let slice = unsafe { std::slice::from_raw_parts(bytes.as_ptr() as *const u16, words_len) };
172 let actual_len = slice.iter().position(|&c| c == 0).unwrap_or(words_len);
173 Some(String::from_utf16_lossy(&slice[..actual_len]))
174}
175
176fn is_image_format(format: u32) -> bool {
177 IMAGE_FORMATS_MAP.contains_key(&format) || format == CF_DIB.0 as u32
178}
179
180fn write_string(item: &ClipboardString) -> Result<()> {
181 let wide: Vec<u16> = item.text.encode_utf16().chain(Some(0)).collect_vec();
182 set_clipboard_bytes(&wide, CF_UNICODETEXT.0 as u32)?;
183
184 if let Some(metadata) = item.metadata.as_ref() {
185 let hash_bytes = ClipboardString::text_hash(&item.text).to_ne_bytes();
186 set_clipboard_bytes(&hash_bytes, *CLIPBOARD_HASH_FORMAT)?;
187
188 let wide: Vec<u16> = metadata.encode_utf16().chain(Some(0)).collect_vec();
189 set_clipboard_bytes(&wide, *CLIPBOARD_METADATA_FORMAT)?;
190 }
191 Ok(())
192}
193
194fn write_image(item: &Image) -> Result<()> {
195 let native_format = match item.format {
196 ImageFormat::Svg => Some(*CLIPBOARD_SVG_FORMAT),
197 ImageFormat::Gif => Some(*CLIPBOARD_GIF_FORMAT),
198 ImageFormat::Png => Some(*CLIPBOARD_PNG_FORMAT),
199 ImageFormat::Jpeg => Some(*CLIPBOARD_JPG_FORMAT),
200 _ => None,
201 };
202 if let Some(format) = native_format {
203 set_clipboard_bytes(item.bytes(), format)?;
204 }
205
206 // Also provide a PNG copy for broad compatibility.
207 // SVG can't be rasterized by the image crate, so skip it.
208 if item.format != ImageFormat::Svg && native_format != Some(*CLIPBOARD_PNG_FORMAT) {
209 if let Some(png_bytes) = convert_to_png(item.bytes(), item.format) {
210 set_clipboard_bytes(&png_bytes, *CLIPBOARD_PNG_FORMAT)?;
211 }
212 }
213 Ok(())
214}
215
216fn convert_to_png(bytes: &[u8], format: ImageFormat) -> Option<Vec<u8>> {
217 let img_format = gpui_to_image_format(format)?;
218 let image = image::load_from_memory_with_format(bytes, img_format)
219 .map_err(|e| log::warn!("Failed to decode image for PNG conversion: {e}"))
220 .ok()?;
221 let mut buf = Vec::new();
222 image
223 .write_to(&mut std::io::Cursor::new(&mut buf), image::ImageFormat::Png)
224 .map_err(|e| log::warn!("Failed to encode PNG: {e}"))
225 .ok()?;
226 Some(buf)
227}
228
229fn read_string() -> Option<ClipboardEntry> {
230 let text = get_clipboard_string(CF_UNICODETEXT.0 as u32)?;
231 let metadata = read_clipboard_metadata(&text);
232 Some(ClipboardEntry::String(ClipboardString { text, metadata }))
233}
234
235fn read_clipboard_metadata(text: &str) -> Option<String> {
236 let locked = get_clipboard_data(*CLIPBOARD_HASH_FORMAT)?;
237 let hash_bytes: [u8; 8] = locked.as_bytes().get(..8)?.try_into().ok()?;
238 let hash = u64::from_ne_bytes(hash_bytes);
239 if hash != ClipboardString::text_hash(text) {
240 return None;
241 }
242 get_clipboard_string(*CLIPBOARD_METADATA_FORMAT)
243}
244
245fn read_image(format: u32) -> Option<ClipboardEntry> {
246 let locked = get_clipboard_data(format)?;
247 let (bytes, image_format) = if format == CF_DIB.0 as u32 {
248 (convert_dib_to_bmp(locked.as_bytes())?, ImageFormat::Bmp)
249 } else {
250 let image_format = *IMAGE_FORMATS_MAP.get(&format)?;
251 (locked.as_bytes().to_vec(), image_format)
252 };
253 let id = hash(&bytes);
254 Some(ClipboardEntry::Image(Image {
255 format: image_format,
256 bytes,
257 id,
258 }))
259}
260
261fn read_files() -> Option<ClipboardEntry> {
262 let locked = get_clipboard_data(CF_HDROP.0 as u32)?;
263 let hdrop = HDROP(locked.ptr as *mut _);
264 let mut filenames = Vec::new();
265 with_file_names(hdrop, |name| filenames.push(std::path::PathBuf::from(name)));
266 Some(ClipboardEntry::ExternalPaths(ExternalPaths(
267 filenames.into(),
268 )))
269}
270
271/// DIB is BMP without the 14-byte BITMAPFILEHEADER. Prepend one.
272fn convert_dib_to_bmp(dib: &[u8]) -> Option<Vec<u8>> {
273 if dib.len() < 40 {
274 return None;
275 }
276
277 let header_size = u32::from_le_bytes(dib[0..4].try_into().ok()?);
278 let bit_count = u16::from_le_bytes(dib[14..16].try_into().ok()?);
279 let compression = u32::from_le_bytes(dib[16..20].try_into().ok()?);
280
281 let color_table_size = if bit_count <= 8 {
282 let colors_used = u32::from_le_bytes(dib[32..36].try_into().ok()?);
283 (if colors_used == 0 {
284 1u32 << bit_count
285 } else {
286 colors_used
287 }) * 4
288 } else if compression == 3 {
289 12 // BI_BITFIELDS
290 } else {
291 0
292 };
293
294 let pixel_offset = 14 + header_size + color_table_size;
295 let file_size = 14 + dib.len() as u32;
296
297 let mut bmp = Vec::with_capacity(file_size as usize);
298 bmp.extend_from_slice(b"BM");
299 bmp.extend_from_slice(&file_size.to_le_bytes());
300 bmp.extend_from_slice(&[0u8; 4]); // reserved
301 bmp.extend_from_slice(&pixel_offset.to_le_bytes());
302 bmp.extend_from_slice(dib);
303 Some(bmp)
304}
305
306fn log_unsupported_clipboard_formats() {
307 let count = unsafe { CountClipboardFormats() };
308 let mut format = 0;
309 for _ in 0..count {
310 format = unsafe { EnumClipboardFormats(format) };
311 let mut buffer = [0u16; 64];
312 unsafe { GetClipboardFormatNameW(format, &mut buffer) };
313 let format_name = String::from_utf16_lossy(&buffer);
314 log::warn!(
315 "Try to paste with unsupported clipboard format: {}, {}.",
316 format,
317 format_name
318 );
319 }
320}
321
322fn gpui_to_image_format(value: ImageFormat) -> Option<image::ImageFormat> {
323 match value {
324 ImageFormat::Png => Some(image::ImageFormat::Png),
325 ImageFormat::Jpeg => Some(image::ImageFormat::Jpeg),
326 ImageFormat::Webp => Some(image::ImageFormat::WebP),
327 ImageFormat::Gif => Some(image::ImageFormat::Gif),
328 ImageFormat::Bmp => Some(image::ImageFormat::Bmp),
329 ImageFormat::Tiff => Some(image::ImageFormat::Tiff),
330 other => {
331 log::warn!("No image crate equivalent for format: {other:?}");
332 None
333 }
334 }
335}
336
337struct ClipboardGuard;
338
339impl ClipboardGuard {
340 fn open() -> Option<Self> {
341 match unsafe { OpenClipboard(None) } {
342 Ok(()) => Some(Self),
343 Err(e) => {
344 log::error!("Failed to open clipboard: {e}");
345 None
346 }
347 }
348 }
349}
350
351impl Drop for ClipboardGuard {
352 fn drop(&mut self) {
353 if let Err(e) = unsafe { CloseClipboard() } {
354 log::error!("Failed to close clipboard: {e}");
355 }
356 }
357}
358
359struct LockedGlobal {
360 global: HGLOBAL,
361 ptr: *const u8,
362 size: usize,
363}
364
365impl LockedGlobal {
366 fn lock(global: HGLOBAL) -> Option<Self> {
367 let size = unsafe { GlobalSize(global) };
368 let ptr = unsafe { GlobalLock(global) };
369 if ptr.is_null() {
370 return None;
371 }
372 Some(Self {
373 global,
374 ptr: ptr as *const u8,
375 size,
376 })
377 }
378
379 fn as_bytes(&self) -> &[u8] {
380 unsafe { std::slice::from_raw_parts(self.ptr, self.size) }
381 }
382}
383
384impl Drop for LockedGlobal {
385 fn drop(&mut self) {
386 unsafe { GlobalUnlock(self.global).ok() };
387 }
388}