use crate::models::ScreenshotResponse;
use crate::{Error, Result};
use image;
use log::info;
use tauri::Runtime;
use crate::desktop::{ScreenshotContext, create_success_response};
use crate::platform::shared::{get_window_title, handle_screenshot_task};
use crate::shared::ScreenshotParams;
use crate::tools::take_screenshot::process_image;
/// Take a screenshot on macOS.
///
/// Uses `screencapture -l <CGWindowID>` rather than xcap, which avoids the
/// Screen Recording TCC permission requirement for the host app and also
/// avoids a crash in `xcap::Window::all()` that occurs on macOS when any
/// on-screen window has coordinates that don't map to a display (a known
/// regression with certain system UI windows on recent macOS versions).
pub async fn take_screenshot<R: Runtime>(
params: ScreenshotParams,
window_context: ScreenshotContext<R>,
) -> Result<ScreenshotResponse> {
let params_clone = params.clone();
let window_clone = window_context.window.clone();
let window_label = params
.window_label
.clone()
.unwrap_or_else(|| "main".to_string());
let application_name = params.application_name.clone().unwrap_or_default();
handle_screenshot_task(move || {
// Get the Tauri window title (used to find the CGWindowID below)
let window_title = get_window_title(&window_clone)
.unwrap_or_else(|_| window_label.clone());
info!("[TAURI-MCP] Screenshot requested for window '{}' (label: {})", window_title, window_label);
// Find the CGWindowID without requiring Screen Recording permission
let wid = find_window_id(&window_title, &application_name)
.ok_or_else(|| Error::WindowOperationFailed(
format!("Window '{}' not found on screen. Make sure the window is visible and not minimized.", window_title)
))?;
info!("[TAURI-MCP] Capturing window WID={} via screencapture", wid);
let tmp_path = format!("/tmp/tauri-mcp-screenshot-{}.png", wid);
// `screencapture -l <WID>` captures a specific window by CGWindowID.
// This binary has its own TCC entry — it does not inherit the host
// app's permission level, so it works even when the Tauri debug binary
// has not been granted Screen Recording access.
let status = std::process::Command::new("screencapture")
.args(["-l", &wid.to_string(), "-x", &tmp_path])
.status()
.map_err(|e| Error::WindowOperationFailed(
format!("Failed to launch screencapture: {}", e)
))?;
if !status.success() {
return Err(Error::WindowOperationFailed(
"screencapture exited with a non-zero status code. \
Ensure Terminal (or your terminal emulator) has Screen Recording permission \
in System Settings → Privacy & Security → Screen Recording.".to_string()
));
}
let png_data = std::fs::read(&tmp_path)
.map_err(|e| Error::WindowOperationFailed(
format!("Failed to read screenshot file: {}", e)
))?;
let _ = std::fs::remove_file(&tmp_path);
info!("[TAURI-MCP] Captured {} bytes", png_data.len());
let dynamic_image = image::load_from_memory(&png_data)
.map_err(|e| Error::WindowOperationFailed(
format!("Failed to decode captured PNG: {}", e)
))?;
process_image(dynamic_image, ¶ms_clone).map(create_success_response)
})
.await
}
/// Enumerate on-screen windows via `CGWindowListCopyWindowInfo` and return
/// the `CGWindowID` of the best matching window.
///
/// Search order:
/// 1. Exact match on owner name
/// 2. Case-insensitive owner name contains `window_title` or `application_name`
/// 3. Case-insensitive window name (kCGWindowName) contains either term
///
/// `CGWindowListCopyWindowInfo` does **not** require Screen Recording permission.
fn find_window_id(window_title: &str, application_name: &str) -> Option<u32> {
use std::ffi::{c_void, CStr, CString};
#[allow(non_camel_case_types)]
type CGWindowID = u32;
#[allow(non_camel_case_types)]
type CGWindowListOption = u32;
const KCG_WINDOW_LIST_OPTION_ON_SCREEN_ONLY: CGWindowListOption = 1 << 0;
const KCG_NULL_WINDOW_ID: CGWindowID = 0;
const KCF_STRING_ENCODING_UTF8: u32 = 0x08000100;
const KCF_NUMBER_SINT32_TYPE: i32 = 3;
#[link(name = "CoreGraphics", kind = "framework")]
unsafe extern "C" {
fn CGWindowListCopyWindowInfo(
option: CGWindowListOption,
relative_to: CGWindowID,
) -> *mut c_void;
}
#[link(name = "CoreFoundation", kind = "framework")]
unsafe extern "C" {
fn CFArrayGetCount(arr: *const c_void) -> isize;
fn CFArrayGetValueAtIndex(arr: *const c_void, idx: isize) -> *const c_void;
fn CFDictionaryGetValue(dict: *const c_void, key: *const c_void) -> *const c_void;
fn CFStringCreateWithCString(
alloc: *const c_void,
c_str: *const i8,
encoding: u32,
) -> *mut c_void;
fn CFStringGetCString(
s: *const c_void,
buf: *mut i8,
buf_size: isize,
encoding: u32,
) -> bool;
fn CFNumberGetValue(
number: *const c_void,
the_type: i32,
value_ptr: *mut c_void,
) -> bool;
fn CFRelease(cf: *const c_void);
}
/// Create a temporary CFStringRef from a Rust `&str`. Caller must CFRelease.
unsafe fn make_cfstring(s: &str) -> *mut c_void {
let c = CString::new(s).unwrap_or_default();
CFStringCreateWithCString(std::ptr::null(), c.as_ptr(), KCF_STRING_ENCODING_UTF8)
}
/// Read a CFStringRef into a Rust String (max 1 KiB).
unsafe fn read_cfstring(s: *const c_void) -> Option<String> {
if s.is_null() {
return None;
}
let mut buf = vec![0i8; 1024];
if CFStringGetCString(s, buf.as_mut_ptr(), 1024, KCF_STRING_ENCODING_UTF8) {
CStr::from_ptr(buf.as_ptr())
.to_str()
.ok()
.map(|s| s.to_owned())
} else {
None
}
}
let title_lc = window_title.to_lowercase();
let app_lc = if application_name.is_empty() {
title_lc.clone()
} else {
application_name.to_lowercase()
};
unsafe {
let list = CGWindowListCopyWindowInfo(
KCG_WINDOW_LIST_OPTION_ON_SCREEN_ONLY,
KCG_NULL_WINDOW_ID,
);
if list.is_null() {
return None;
}
let owner_key = make_cfstring("kCGWindowOwnerName");
let name_key = make_cfstring("kCGWindowName");
let number_key = make_cfstring("kCGWindowNumber");
let onscreen_key = make_cfstring("kCGWindowIsOnscreen");
let count = CFArrayGetCount(list);
// Collect candidates with a priority score (lower = better)
let mut best: Option<(u32, u32)> = None; // (priority, wid)
for i in 0..count {
let dict = CFArrayGetValueAtIndex(list, i);
if dict.is_null() {
continue;
}
// Skip off-screen windows
let onscreen_ref = CFDictionaryGetValue(dict, onscreen_key as *const c_void);
if onscreen_ref.is_null() {
continue;
}
let owner = read_cfstring(
CFDictionaryGetValue(dict, owner_key as *const c_void)
)
.unwrap_or_default();
let win_name = read_cfstring(
CFDictionaryGetValue(dict, name_key as *const c_void)
)
.unwrap_or_default();
let owner_lc = owner.to_lowercase();
let win_name_lc = win_name.to_lowercase();
// Priority 1: exact owner name match
// Priority 2: owner contains app_lc
// Priority 3: owner contains title_lc
// Priority 4: window name contains title_lc
let priority = if owner_lc == app_lc || owner_lc == title_lc {
1
} else if owner_lc.contains(&app_lc) {
2
} else if owner_lc.contains(&title_lc) {
3
} else if win_name_lc.contains(&title_lc) || win_name_lc.contains(&app_lc) {
4
} else {
continue;
};
let num_ref = CFDictionaryGetValue(dict, number_key as *const c_void);
if num_ref.is_null() {
continue;
}
let mut wid: i32 = 0;
if CFNumberGetValue(
num_ref,
KCF_NUMBER_SINT32_TYPE,
&mut wid as *mut _ as *mut c_void,
) {
if best.map_or(true, |(p, _)| priority < p) {
info!(
"[TAURI-MCP] Candidate: owner='{}' name='{}' WID={} priority={}",
owner, win_name, wid, priority
);
best = Some((priority, wid as u32));
if priority == 1 {
break; // can't do better
}
}
}
}
CFRelease(owner_key as *const c_void);
CFRelease(name_key as *const c_void);
CFRelease(number_key as *const c_void);
CFRelease(onscreen_key as *const c_void);
CFRelease(list);
best.map(|(_, wid)| wid)
}
}