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
This commit is contained in:
10 files changed
+311
-206
No files matched your search
+119
-69
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user