T09: the tap holds one mouse tracking and one format as xterm does, any reset ends it; every reported button keeps its release, motion reports the first held; URLs keep combining marks; selections log start, run and end; shot hands real buttons to the view and shows its window without activating
CI / platforms (windows-latest) (push) Failing after 7s
CI / macos (push) Failing after 8m26s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s

This commit is contained in:
ZacharyZhang-NY committed 2026-10-14 15:51:44 -04:00
1 parent 9f81f1f704
commit d6fa38e60b
10 files changed
+311 -206

No files matched your search

+119 -69
View File
@@ -1,4 +1,4 @@
//! `cargo run --example shot -- [--keys "a ctrl-c lopt-f repeat-up mark:é commit:é shift-click@40,20"] <png> <program> [args…]`: runs a program in the main window, types the keys and mouse gestures as AppKit events, then photographs the window.
//! Runs a program in the main window, plays keys and mouse gestures on it, and photographs it.
#[cfg(target_os = "macos")]
fn main() {
@@ -31,11 +31,12 @@ fn main() {
app::start(cx).expect("the app starts");
let window = app::open(launch, cx).expect("the window opens");
cx.spawn(async move |cx| {
cx.background_executor().timer(Duration::from_secs(1)).await;
let number = window
.update(cx, |_, window, _| capture::number(window))
.expect("the window lives")
.expect("a window number");
capture::front(number).expect("the window on screen");
cx.background_executor().timer(Duration::from_secs(1)).await;
for spec in &keys {
typing::post(spec, number).expect("a key event");
cx.background_executor()
@@ -60,7 +61,7 @@ mod capture {
use std::path::Path;
use anyhow::{Context as _, bail};
use objc::{msg_send, runtime::Object, sel, sel_impl};
use objc::{class, msg_send, runtime::Object, sel, sel_impl};
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
/// A gpui window's number, as CoreGraphics knows it.
@@ -76,6 +77,17 @@ mod capture {
u32::try_from(number).context("window number out of range")
}
/// Shows the window without activating the app: gpui draws it, real input stays put.
pub fn front(number: u32) -> anyhow::Result<()> {
unsafe {
let app: *mut Object = msg_send![class!(NSApplication), sharedApplication];
let window: *mut Object = msg_send![app, windowWithWindowNumber: i64::from(number)];
anyhow::ensure!(!window.is_null(), "no window {number}");
let _: () = msg_send![window, orderFrontRegardless];
}
Ok(())
}
/// Writes the window `number` as a PNG; the app's own window needs no permission.
pub fn snapshot(number: u32, path: &Path) -> anyhow::Result<()> {
use core_graphics::{
@@ -117,14 +129,14 @@ mod capture {
mod typing {
use anyhow::Context as _;
use core_graphics::{
event::{CGEvent, CGEventFlags, ScrollEventUnit},
event::{CGEvent, CGEventFlags, CGEventType, CGMouseButton, EventField, ScrollEventUnit},
event_source::{CGEventSource, CGEventSourceStateID},
geometry::{CGPoint, CGRect},
};
use foreign_types::ForeignType;
use objc::{
class, msg_send,
runtime::{NO, Object, YES},
runtime::{NO, Object, Sel, YES},
sel, sel_impl,
};
@@ -196,7 +208,68 @@ mod typing {
fn CGEventSetLocation(event: *mut core_graphics::sys::CGEvent, location: CGPoint);
}
/// `click@x,y` (`click2`, `click3`, `rclick`), `drag@x,y@x,y`, `move@x,y`, `scrollup@x,y@lines` or `scrolldown`, at points from the content's top-left; AppKit routes no window-less scroll, so it goes to the view.
/// A gesture's steps: each one's name, point and click count.
fn steps(gesture: &str, points: &[CGPoint]) -> anyhow::Result<Vec<(String, CGPoint, i64)>> {
let step = |name: &str, at: CGPoint, clicks| (name.to_string(), at, clicks);
let (at, to) = (points[0], points.get(1).copied());
Ok(match gesture {
"click" | "click2" | "click3" => (1..=gesture[5..].parse().unwrap_or(1))
.flat_map(|clicks| [step("ldown", at, clicks), step("lup", at, clicks)])
.collect(),
"rclick" => vec![step("rdown", at, 1), step("rup", at, 1)],
"mclick" => vec![step("mdown", at, 1), step("mup", at, 1)],
"drag" => {
let to = to.context("drag needs a second point")?;
vec![
step("ldown", at, 1),
step("ldrag", to, 1),
step("lup", to, 1),
]
}
name => vec![step(name, at, 1)],
})
}
/// A step's event type, button and the selector gpui's view takes it by.
fn kind(step: &str) -> anyhow::Result<(CGEventType, CGMouseButton, &'static str)> {
Ok(match step {
"ldown" => (
CGEventType::LeftMouseDown,
CGMouseButton::Left,
"mouseDown:",
),
"lup" => (CGEventType::LeftMouseUp, CGMouseButton::Left, "mouseUp:"),
"ldrag" => (
CGEventType::LeftMouseDragged,
CGMouseButton::Left,
"mouseDragged:",
),
"rdown" => (
CGEventType::RightMouseDown,
CGMouseButton::Right,
"rightMouseDown:",
),
"rup" => (
CGEventType::RightMouseUp,
CGMouseButton::Right,
"rightMouseUp:",
),
"mdown" => (
CGEventType::OtherMouseDown,
CGMouseButton::Center,
"otherMouseDown:",
),
"mup" => (
CGEventType::OtherMouseUp,
CGMouseButton::Center,
"otherMouseUp:",
),
"move" => (CGEventType::MouseMoved, CGMouseButton::Left, "mouseMoved:"),
other => anyhow::bail!("no mouse gesture {other}"),
})
}
/// Hands gpui's view a mouse gesture as AppKit events, even while the window is inactive.
fn mouse(name: &str, flags: u64, number: u32) -> anyhow::Result<()> {
let mut fields = name.split('@');
let gesture = fields.next().context("a gesture")?;
@@ -205,84 +278,61 @@ mod typing {
.map(str::parse)
.collect::<Result<_, _>>()
.with_context(|| format!("numbers in {name}"))?;
let needed = match gesture {
"drag" => 4,
"scrollup" | "scrolldown" => 3,
_ => 2,
};
anyhow::ensure!(numbers.len() == needed, "{name} needs {needed} numbers");
anyhow::ensure!(numbers.len() >= 2, "{name} needs a point");
unsafe {
let app: *mut Object = msg_send![class!(NSApplication), sharedApplication];
let window: *mut Object = msg_send![app, windowWithWindowNumber: i64::from(number)];
anyhow::ensure!(!window.is_null(), "no window {number}");
let view: *mut Object = msg_send![window, firstResponder];
let content: *mut Object = msg_send![window, contentView];
let height = {
let frame: CGRect = msg_send![content, frame];
frame.size.height
let frame: CGRect = msg_send![content, frame];
let height = frame.size.height;
let points: Vec<CGPoint> = numbers
.chunks_exact(2)
.map(|pair| CGPoint::new(pair[0], height - pair[1]))
.collect();
let screens: *mut Object = msg_send![class!(NSScreen), screens];
let main: *mut Object = msg_send![screens, objectAtIndex: 0u64];
let screen: CGRect = msg_send![main, frame];
let source = || {
CGEventSource::new(CGEventSourceStateID::Private)
.map_err(|_| anyhow::anyhow!("no event source"))
};
let at = |index: usize| CGPoint::new(numbers[index], height - numbers[index + 1]);
let post = |kind: u64, point: CGPoint, clicks: i64| -> anyhow::Result<()> {
let event: *mut Object = msg_send![
class!(NSEvent),
mouseEventWithType: kind
location: point
modifierFlags: flags
timestamp: 0.0f64
windowNumber: i64::from(number)
context: std::ptr::null_mut::<Object>()
eventNumber: 0i64
clickCount: clicks
pressure: 1.0f32
];
anyhow::ensure!(!event.is_null(), "AppKit refused the mouse event {name}");
let _: () = msg_send![app, postEvent: event atStart: NO];
let deliver = |event: CGEvent, at: CGPoint, selector: &str| -> anyhow::Result<()> {
CGEventSetLocation(
event.as_ptr(),
CGPoint::new(at.x, screen.size.height - at.y),
);
event.set_flags(CGEventFlags::from_bits_truncate(flags));
let native: *mut Object =
msg_send![class!(NSEvent), eventWithCGEvent: event.as_ptr()];
anyhow::ensure!(!native.is_null(), "AppKit refused {name}");
let selector = Sel::register(selector);
let _: *mut Object = msg_send![view, performSelector: selector withObject: native];
Ok(())
};
match gesture {
"click" | "click2" | "click3" => {
for clicks in 1..=gesture[5..].parse().unwrap_or(1) {
post(1, at(0), clicks)?;
post(2, at(0), clicks)?;
}
if let Some(up) = gesture.strip_prefix("scroll") {
let lines = *numbers.get(2).context("a scroll needs lines")? as i32;
let lines = if up == "up" { lines } else { -lines };
let event =
CGEvent::new_scroll_event(source()?, ScrollEventUnit::LINE, 1, lines, 0, 0)
.map_err(|_| anyhow::anyhow!("no scroll event"))?;
deliver(event, points[0], "scrollWheel:")?;
} else {
for (step, at, clicks) in steps(gesture, &points)? {
let (kind, button, selector) = kind(&step)?;
let event = CGEvent::new_mouse_event(source()?, kind, at, button)
.map_err(|_| anyhow::anyhow!("no mouse event"))?;
event.set_integer_value_field(EventField::MOUSE_EVENT_CLICK_STATE, clicks);
deliver(event, at, selector)?;
}
"rclick" => {
post(3, at(0), 1)?;
post(4, at(0), 1)?;
}
"drag" => {
post(1, at(0), 1)?;
post(6, at(2), 1)?;
post(2, at(2), 1)?;
}
"move" => post(5, at(0), 0)?,
"scrollup" | "scrolldown" => {
let lines = numbers[2] as i32 * if gesture == "scrollup" { 1 } else { -1 };
let screens: *mut Object = msg_send![class!(NSScreen), screens];
let main: *mut Object = msg_send![screens, objectAtIndex: 0u64];
let screen: CGRect = msg_send![main, frame];
let point = at(0);
let source = CGEventSource::new(CGEventSourceStateID::Private)
.map_err(|_| anyhow::anyhow!("no event source"))?;
let event =
CGEvent::new_scroll_event(source, ScrollEventUnit::LINE, 1, lines, 0, 0)
.map_err(|_| anyhow::anyhow!("no scroll event"))?;
let place = CGPoint::new(point.x, screen.size.height - point.y);
CGEventSetLocation(event.as_ptr(), place);
event.set_flags(CGEventFlags::from_bits_truncate(flags));
let native: *mut Object =
msg_send![class!(NSEvent), eventWithCGEvent: event.as_ptr()];
anyhow::ensure!(!native.is_null(), "AppKit refused the scroll {name}");
let view: *mut Object = msg_send![window, firstResponder];
let _: () = msg_send![view, scrollWheel: native];
}
other => anyhow::bail!("no mouse gesture {other}"),
}
}
log::info!("shot: mouse {name}");
Ok(())
}
/// Posts `spec`, such as `ctrl-c`, `lopt-f`, `repeat-up` or `shift-drag@0,0@40,20`, as AppKit events on window `number`.
/// Posts `spec`, a key, an input method call or a mouse gesture, on window `number`.
pub fn post(spec: &str, number: u32) -> anyhow::Result<()> {
if let Some((call, text)) = spec.split_once(':') {
return compose(call, text, number);