Sep 08, 2025
6 min read
Tauri,
Rust,
macOS,

Advanced Tauri Screenshot App Development: macOS Fullscreen Handling and Clipboard Operations

In-depth exploration of special handling on macOS platform when developing screenshot applications with Tauri, including fullscreen display issues, clipboard operation anomalies and solutions, as well as key technical details such as keyboard shortcuts and preview function optimization.

Full Screen on macOS

If you use "fullscreen": true on Windows or Linux, there might be no problem. However, as expected, macOS will have issues with the screenshot functionality.

As we mentioned before, screenshot software is essentially a transparent, topmost, full-screen application. Topmost has no problem on macOS. Transparency on macOS requires enabling the "macOSPrivateApi": true property and enabling the following features in the crate:

tauri = { version = "2", features = ["macos-private-api"] }

Fullscreen cannot use the "fullscreen": true property either. Because macOS fullscreen will automatically create a separate fullscreen space on the second desktop.

Instead, we should use "maximized": true. We should maximize the application after it loads:

  // Maximize the window after the page loads
  useEffect(() => {
    const maximizeWindow = async () => {
      const currentWindow = getCurrentWebviewWindow();
      await currentWindow.maximize();
    };
    
    maximizeWindow();
  }, []);

Copy Screenshot to Clipboard

First, the frontend has the ability to copy images to the clipboard, and this API should be supported by most modern browsers. The API is as follows:

const blob = new Blob([new Uint8Array(image).buffer], { type: "image/png" });
const clipboardItem = new ClipboardItem({ 'image/png': blob })
await navigator.clipboard.write([clipboardItem])

If you really use the above API, you will get the following error:

NotAllowedError: The request is not allowed by the user agent or the platform in the current context, possibly because the user denied permission

The reason is that the frontend API needs to be in a secure context (HTTPS or localhost) and requires explicit user permission when called.

The second method is through the clipboard plugin clipboard-manager:

pnpm tauri add clipboard-manager

import { Image as TauriImage } from "@tauri-apps/api/image";
const image: Uint8Array = await invoke("capture", {
	x: selectionRect.x,
	y: selectionRect.y,
	width: selectionRect.width,
	height: selectionRect.height
  });
// Write to clipboard
await writeImage(image);

However, on macOS, you might also encounter a bug:

thread 'tokio-runtime-worker' panicked at /Users/xxx/.cargo/registry/src/rsproxy.cn-e3de039b2554c837/arboard-3.6.1/src/platform/osx.rs:88:6:
called `Option::unwrap()` on a `None` value
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

The clipboard-manager depends on the arboard crate to implement clipboard functionality, and the arboard crate calls Cocoa framework APIs on the macOS platform. Unfortunately, arboard directly uses unwrap here:

	let cg_image = unsafe {
		CGImageCreate(
			width,
			height,
			8,
			32,
			4 * width,
			Some(&colorspace),
			CGBitmapInfo::ByteOrderDefault | CGBitmapInfo(CGImageAlphaInfo::Last.0),
			Some(&provider),
			ptr::null_mut(),
			false,
			CGColorRenderingIntent::RenderingIntentDefault,
		)
	}
	.unwrap();

Currently arboard has modified this line of code to return an Error instead.

How do we know the specific reason for this bug? The answer is the console.app application (also called Console in Chinese). By opening the console, clicking to start logging, and reproducing the operation that causes the bug, we can get the above error log.

If you directly write binary image data to the clipboard:

// image is binary file data
const img_data = await TauriImage.new(image, selectionRect.width, selectionRect.height)
 await writeImage(img_data);
 // Or directly writeImage(image);

You’ll probably find an error log like this in the console:

CGImageCreate: invalid image data size: 165 (height) x 836 (bytesPerRow) data provider size 27295

On Windows systems, this error will be displayed directly.

According to the official example, we might also encounter this error:

Unhandled Promise Rejection: expected RGBA image data, found raw bytes

In fact, going through all this trouble, this is just an issue with the official documentation. It’s not mentioned in the plugin section, but only in the javascript api documentation, which states that Tauri needs to add image-ico or image-png features:

[dependencies]
tauri = { version = "...", features = ["...", "image-png"] }

Save Screenshot

Nothing much to say, just use dialog to bring up the save to file dialog to get the file path, then pass it to the Rust layer for saving.

import { save } from '@tauri-apps/plugin-dialog';

const path = await save({
      filters: [
        {
          name: 'screenshot',
          extensions: ['png', 'jpeg'],
        },
      ],
    });
    
    try {

      await invoke("capture", {
        x: selectionRect.x,
        y: selectionRect.y,
        width: selectionRect.width,
        height: selectionRect.height,
        savePath: path,
      });

      const currentWindow = getCurrentWebviewWindow();
      await currentWindow.close();
    } catch (error) {
      console.error("Error closing window:", error);
    }

Keyboard Shortcuts

Screenshot applications should not always stay on top, so we need to hide the main window after taking a screenshot and call it up when needed. This requires implementing a keyboard shortcut wake-up function.

pnpm tauri add global-shortcut

Note that the above command will automatically add this line of code to lib.rs:

.plugin(tauri_plugin_global_shortcut::Builder::new().build())

Since we need to implement keyboard shortcut wake-up in the Rust layer, we need to remove this line and re-implement it. For example, if we want to implement a ctrl + shift + s (mac: command + shift + s) screenshot function, we implement showing the main window in the response callback.

.setup(|app| {
            #[cfg(desktop)]
            {
                use tauri::Manager;
                use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, Modifiers, Shortcut};
                let ctrl_shift_s_shortcut =
                    Shortcut::new(Some(Modifiers::SUPER | Modifiers::SHIFT), Code::KeyS);

                app.handle().plugin(
                    tauri_plugin_global_shortcut::Builder::new()
                        .with_handler(move |app, shortcut, _event| {
                            let app_handle = app.clone(); // Clone handle for closure
                            println!("{:?}", shortcut);
                            if shortcut == &ctrl_shift_s_shortcut {
                                println!("Ctrl-Shift-S Detected!");
                                app_handle
                                    .get_webview_window("main")
                                    .unwrap()
                                    .show()
                                    .unwrap();
                            }
                        })
                        .build(),
                )?;

                app.global_shortcut().register(ctrl_shift_s_shortcut)?;
            }
            Ok(())
        })

Others

Why does the screenshot preview flash on Tauri?

This is because we use a timer to call the Rust layer to take screenshots of the mouse preview area. The larger the timer interval, the more the preview function flashes. If we can optimize the overall time consumption of this function to less than 20ms per frame, we generally won’t see the flashing phenomenon.

According to my observation, on my device, a 100x100 preview screenshot takes about 5ms-15ms. The screenshot time plus transmission delay should be sufficient. This is also one of the reasons why Tauri is not very advantageous for writing screenshot applications - native frameworks work better.

If you’re interested, the complete code is here: Direct Link