Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion crates/glint-core/src/capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,25 @@ pub fn generate_fallback_capture(width: u32, height: u32, x: i32, y: i32) -> Scr
}
}

#[cfg(not(target_os = "windows"))]
/// macOS: one image of the main display, positioned at the global origin.
#[cfg(target_os = "macos")]
pub fn capture_screen() -> Result<ScreenCaptureResult, String> {
let frame = crate::screen::capture_via_screencapture().map_err(|err| err.to_string())?;
Comment thread
KuaaMU marked this conversation as resolved.
Resolved
Ok(ScreenCaptureResult {
image_data_url: frame.image_data_url,
geometry: ScreenGeometry {
x: 0,
y: 0,
width: frame.width,
height: frame.height,
},
window_title: crate::platform::get_active_window_title_opt(),
})
}

/// Linux uses the XDG screenshot portal from the shell, so this placeholder is
/// only reachable through `glint-cli screen`, which calls the portal directly.
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
pub fn capture_screen() -> Result<ScreenCaptureResult, String> {
Ok(generate_fallback_capture(800, 600, 0, 0))
}
Expand Down
4 changes: 3 additions & 1 deletion crates/glint-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ pub use hotkey::{
pub use llm::{
ChatMessage, QueryRequest, StreamChunk, StreamSink, build_api_payload, stream_explanation,
};
pub use screen::{CapturedFrame, ScreenError, capture_via_portal, png_file_to_data_url};
pub use screen::{
CapturedFrame, ScreenError, capture_via_portal, capture_via_screencapture, png_file_to_data_url,
};
pub use selection::{
CaptureError, MAX_CHARS, MAX_RAW_BYTES, READ_TIMEOUT_MS, SelectionCandidate, SelectionSensor,
SelectionSource, normalize_selection, platform_sensor,
Expand Down
76 changes: 76 additions & 0 deletions crates/glint-core/src/screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ pub enum ScreenError {
Unavailable(String),
#[error("screen capture was cancelled or denied")]
Denied,
#[error(
"screen recording permission is not granted; enable it in System Settings > Privacy & Security > Screen Recording"
)]
PermissionDenied,
#[error("screen capture failed: {0}")]
Failed(String),
}
Expand Down Expand Up @@ -118,6 +122,78 @@ pub async fn capture_via_portal() -> Result<CapturedFrame, ScreenError> {
))
}

/// Capture the main display with Apple's `screencapture`.
///
/// `screencapture -x` is the only path that works without linking
/// ScreenCaptureKit or asking for a custom permission prompt. It needs the
/// Screen Recording permission; without it macOS either fails or returns a
/// desktop-only image, and we cannot tell those apart from the exit code.
///
/// Multi-display Macs capture the main display only. The overlay and the crop
/// use the image's own size, so the result is correct, just limited to one
/// screen.
#[cfg(target_os = "macos")]
pub fn capture_via_screencapture() -> Result<CapturedFrame, ScreenError> {
use std::process::Command;

// `screencapture` exits 0 even when the process lacks Screen Recording
// access; it just returns an image of the desktop wallpaper. Preflight the
// permission so the user gets an error instead of a useless capture.
if !macos_screen_capture_access() {
return Err(ScreenError::PermissionDenied);
}

let path = std::env::temp_dir().join(format!(
"glint-screen-{}-{}.png",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0)
));

let output = Command::new("/usr/sbin/screencapture")
.args(["-x", "-t", "png"])
.arg(&path)
.output()
.map_err(|err| ScreenError::Unavailable(err.to_string()))?;

if !output.status.success() {
Comment thread
KuaaMU marked this conversation as resolved.
Resolved
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
let _ = std::fs::remove_file(&path);
return Err(ScreenError::Failed(if stderr.is_empty() {
"screencapture exited with an error; check the Screen Recording permission".to_string()
} else {
stderr
}));
}

let frame = png_file_to_data_url(&path);
// The temp file is ours; never leave it behind.
let _ = std::fs::remove_file(&path);
frame
}

#[cfg(target_os = "macos")]
fn macos_screen_capture_access() -> bool {
#[link(name = "CoreGraphics", kind = "framework")]
unsafe extern "C" {
/// Available since macOS 10.15. Returns whether this process may read
/// screen contents; it does not prompt.
fn CGPreflightScreenCaptureAccess() -> bool;
}

// SAFETY: no arguments, no pointers, no lifetime concerns.
unsafe { CGPreflightScreenCaptureAccess() }
}

#[cfg(not(target_os = "macos"))]
pub fn capture_via_screencapture() -> Result<CapturedFrame, ScreenError> {
Err(ScreenError::Unavailable(
"screencapture is macOS-only".to_string(),
))
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
Empty file modified scripts/check.sh
100644 → 100755
Empty file.
Empty file modified scripts/dev-run.sh
100644 → 100755
Empty file.
Empty file modified scripts/glint-x11.sh
100644 → 100755
Empty file.
Empty file modified scripts/install-hooks.sh
100644 → 100755
Empty file.
Empty file modified scripts/shot.sh
100644 → 100755
Empty file.
14 changes: 9 additions & 5 deletions src-tauri/src/capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ use std::time::Duration;
#[cfg(test)]
pub use glint_core::encode_rgba_to_png_data_url;
pub use glint_core::{ScreenCaptureResult, SelectionResult};
// Only the non-Windows placeholder screenshot uses this.
#[cfg(not(target_os = "windows"))]
// Only the placeholder screenshot above uses this.
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
pub use glint_core::generate_fallback_capture;

#[cfg(not(target_os = "linux"))]
Expand All @@ -28,10 +28,14 @@ pub fn capture_screen() -> Result<ScreenCaptureResult, String> {
glint_core::capture::capture_screen()
}

#[cfg(not(target_os = "windows"))]
#[cfg(target_os = "macos")]
pub fn capture_screen() -> Result<ScreenCaptureResult, String> {
glint_core::capture::capture_screen()
}

// Linux never reaches this: `trigger_snipping_and_show` uses the XDG portal.
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
pub fn capture_screen() -> Result<ScreenCaptureResult, String> {
// TODO(P2): replace with xdg-desktop-portal Screenshot on Linux. Until then
// this is an explicit placeholder, not a screenshot.
Ok(generate_fallback_capture(800, 600, 0, 0))
}

Expand Down
44 changes: 41 additions & 3 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -512,11 +512,49 @@ pub fn trigger_snipping_and_show(app: &AppHandle) {
};
match capture::capture_screen() {
Ok(cap) => {
let _ = window.set_position(PhysicalPosition::new(cap.geometry.x, cap.geometry.y));
let _ = window.set_size(PhysicalSize::new(cap.geometry.width, cap.geometry.height));
let _ = window.emit("glint-snipping-capture", &cap);
// Same payload shape as the Linux branch: the frontend crops
// 1:1 from the capture using the overlay's real geometry,
// which the compositor may shrink (menu bar on macOS).
let (gx, gy) = (cap.geometry.x, cap.geometry.y);
let (gw, gh) = (cap.geometry.width, cap.geometry.height);
let _ = window.set_position(PhysicalPosition::new(gx, gy));
let _ = window.set_size(PhysicalSize::new(gw, gh));

let payload = serde_json::json!({
"image_data_url": cap.image_data_url,
"capture": {
"width": gw,
"height": gh,
"origin_x": gx,
"origin_y": gy
},
"monitor": {
"x": gx,
"y": gy,
"width": gw,
"height": gh
},
"window_title": cap.window_title
});
let _ = window.emit("glint-snipping-capture", &payload);
let _ = window.show();
let _ = window.set_focus();

let verify = window.clone();
tauri::async_runtime::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(300)).await;
if let (Ok(pos), Ok(size)) = (verify.outer_position(), verify.outer_size()) {
let _ = verify.emit(
"glint-snipping-geometry",
serde_json::json!({
"x": pos.x,
"y": pos.y,
"width": size.width,
"height": size.height
}),
);
}
});
}
Err(e) => {
eprintln!("[Glint] 截屏错误: {}", e);
Expand Down
Loading