first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -1,37 +1,86 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use objc2_core_graphics::{
|
||||
CGEvent, CGEventFlags, CGEventSource, CGEventSourceStateID, CGEventTapLocation, CGKeyCode,
|
||||
};
|
||||
use objc2_core_graphics::{CGEvent, CGEventFlags, CGEventSource, CGEventSourceStateID, CGKeyCode};
|
||||
|
||||
use super::keycode_cache;
|
||||
use crate::Key;
|
||||
use super::post::PostTarget;
|
||||
use super::{activation, keycode_cache, window};
|
||||
use crate::{Key, Target};
|
||||
|
||||
/// Manages keyboard state and posts keyboard events to the system.
|
||||
pub struct Keyboard {
|
||||
/// Cache of character-to-keycode mappings for the current keyboard layout.
|
||||
cache: HashMap<char, CGKeyCode>,
|
||||
/// Where synthesized events are delivered.
|
||||
post_target: PostTarget,
|
||||
/// The window id and pid of the current window target, used to activate the window before
|
||||
/// keyboard events are posted. `None` when targeting the HID tap (screen/frontmost behavior).
|
||||
///
|
||||
/// Mouse events activate the target window lazily from the event location; keyboard events
|
||||
/// carry no coordinates, so activation must be triggered explicitly here instead.
|
||||
window_context: Option<(u32, libc::pid_t)>,
|
||||
/// The currently-held modifier flags, accumulated from modifier key-down/up events.
|
||||
///
|
||||
/// Synthetic modifier key events posted via `CGEventPostToPid` do not update the session's
|
||||
/// modifier state, so we track it ourselves and stamp it onto every key event. Without this,
|
||||
/// a shortcut sent as discrete events (e.g. Command-down, n-down, n-up, Command-up) arrives as
|
||||
/// a plain "n" and is treated as text rather than as Cmd+N.
|
||||
current_flags: CGEventFlags,
|
||||
}
|
||||
|
||||
impl Keyboard {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(target: PostTarget) -> Self {
|
||||
Self {
|
||||
cache: keycode_cache::build_cache(),
|
||||
post_target: target,
|
||||
window_context: None,
|
||||
current_flags: CGEventFlags::empty(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets where subsequent synthesized key events are delivered. Called per-action so typing
|
||||
/// can be routed to a specific background process.
|
||||
pub fn set_target(&mut self, target: Target) {
|
||||
self.post_target = match target {
|
||||
Target::Screen => PostTarget::HidTap,
|
||||
Target::Window { pid, .. } => PostTarget::Pid(pid as libc::pid_t),
|
||||
};
|
||||
self.window_context = match target {
|
||||
Target::Window { window_id, pid } => Some((window_id, pid as libc::pid_t)),
|
||||
Target::Screen => None,
|
||||
};
|
||||
}
|
||||
|
||||
/// Sends a key down event for the given key.
|
||||
pub fn key_down(&self, key: &Key) -> Result<(), String> {
|
||||
post_key_down(self.resolve_keycode(key)?)
|
||||
///
|
||||
/// If the key is a modifier, its mask is folded into the held-modifier flags first so the
|
||||
/// key-down itself carries the now-active modifier; the flags are then stamped on the event.
|
||||
pub fn key_down(&mut self, key: &Key) -> Result<(), String> {
|
||||
self.ensure_window_activated();
|
||||
let keycode = self.resolve_keycode(key)?;
|
||||
if let Some(mask) = modifier_mask(keycode) {
|
||||
self.current_flags |= mask;
|
||||
}
|
||||
post_key_event(keycode, true, self.current_flags, self.post_target)
|
||||
}
|
||||
|
||||
/// Sends a key up event for the given key.
|
||||
pub fn key_up(&self, key: &Key) -> Result<(), String> {
|
||||
post_key_up(self.resolve_keycode(key)?)
|
||||
///
|
||||
/// If the key is a modifier, its mask is removed from the held-modifier flags so the key-up
|
||||
/// reflects the modifier being released; the updated flags are then stamped on the event.
|
||||
///
|
||||
/// Activation is not triggered here: `KeyUp` always follows a `KeyDown` that already
|
||||
/// activated the window, and a lone `KeyUp` with no prior `KeyDown` is a no-op regardless.
|
||||
pub fn key_up(&mut self, key: &Key) -> Result<(), String> {
|
||||
let keycode = self.resolve_keycode(key)?;
|
||||
if let Some(mask) = modifier_mask(keycode) {
|
||||
self.current_flags &= !mask;
|
||||
}
|
||||
post_key_event(keycode, false, self.current_flags, self.post_target)
|
||||
}
|
||||
|
||||
/// Simulates typing text by sending Quartz events.
|
||||
pub fn type_text(&self, text: &str) -> Result<(), String> {
|
||||
self.ensure_window_activated();
|
||||
let source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState);
|
||||
|
||||
// Send one character at a time for better compatibility with various applications.
|
||||
@@ -41,12 +90,27 @@ impl Keyboard {
|
||||
//
|
||||
// TODO(vorporeal): when sending an ASCII character, send it using virtual key codes
|
||||
// for better compatibility.
|
||||
type_unicode_char(ch, source.as_deref())?;
|
||||
type_unicode_char(ch, source.as_deref(), self.post_target)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensures the target window is activated before keyboard events are posted.
|
||||
///
|
||||
/// Mouse events activate the target window lazily from the event location (see
|
||||
/// [`super::mouse::Mouse`]); keyboard events carry no coordinates, so activation must be
|
||||
/// triggered here instead. The call is idempotent per `(pid, window)` pair, so calling it
|
||||
/// before each keyboard event is cheap and never re-sends the activation primer.
|
||||
fn ensure_window_activated(&self) {
|
||||
let Some((window_id, pid)) = self.window_context else {
|
||||
return;
|
||||
};
|
||||
if let Some(info) = window::window_by_id(window_id) {
|
||||
activation::ensure_activated(pid, &info);
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves a Key to a CGKeyCode.
|
||||
///
|
||||
/// The key can be:
|
||||
@@ -69,26 +133,51 @@ impl Keyboard {
|
||||
}
|
||||
}
|
||||
|
||||
/// Posts a key down event for the given virtual keycode.
|
||||
fn post_key_down(keycode: CGKeyCode) -> Result<(), String> {
|
||||
let source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState);
|
||||
let event = CGEvent::new_keyboard_event(source.as_deref(), keycode, true)
|
||||
.ok_or_else(|| format!("Failed to create key down event for keycode {}", keycode))?;
|
||||
CGEvent::post(CGEventTapLocation::HIDEventTap, Some(&event));
|
||||
Ok(())
|
||||
/// Maps a modifier virtual keycode to its `CGEventFlags` mask, handling both the left and right
|
||||
/// variants. Returns `None` for non-modifier keys.
|
||||
fn modifier_mask(keycode: CGKeyCode) -> Option<CGEventFlags> {
|
||||
Some(match keycode {
|
||||
// Command: left 0x37 (55), right 0x36 (54).
|
||||
54 | 55 => CGEventFlags::MaskCommand,
|
||||
// Shift: left 0x38 (56), right 0x3C (60).
|
||||
56 | 60 => CGEventFlags::MaskShift,
|
||||
// Control: left 0x3B (59), right 0x3E (62).
|
||||
59 | 62 => CGEventFlags::MaskControl,
|
||||
// Option/Alt: left 0x3A (58), right 0x3D (61).
|
||||
58 | 61 => CGEventFlags::MaskAlternate,
|
||||
// Fn: 0x3F (63).
|
||||
63 => CGEventFlags::MaskSecondaryFn,
|
||||
// Caps Lock: 0x39 (57).
|
||||
57 => CGEventFlags::MaskAlphaShift,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Posts a key up event for the given virtual keycode.
|
||||
fn post_key_up(keycode: CGKeyCode) -> Result<(), String> {
|
||||
/// Posts a key event (down or up) for the given virtual keycode, stamping the currently-held
|
||||
/// modifier flags so shortcuts route through the app's key-equivalent handling.
|
||||
fn post_key_event(
|
||||
keycode: CGKeyCode,
|
||||
is_down: bool,
|
||||
flags: CGEventFlags,
|
||||
target: PostTarget,
|
||||
) -> Result<(), String> {
|
||||
let source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState);
|
||||
let event = CGEvent::new_keyboard_event(source.as_deref(), keycode, false)
|
||||
.ok_or_else(|| format!("Failed to create key up event for keycode {}", keycode))?;
|
||||
CGEvent::post(CGEventTapLocation::HIDEventTap, Some(&event));
|
||||
let event =
|
||||
CGEvent::new_keyboard_event(source.as_deref(), keycode, is_down).ok_or_else(|| {
|
||||
let direction = if is_down { "down" } else { "up" };
|
||||
format!("Failed to create key {direction} event for keycode {keycode}")
|
||||
})?;
|
||||
CGEvent::set_flags(Some(&event), flags);
|
||||
target.post(&event);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generates a Quartz event signifying the typing of a single Unicode character.
|
||||
fn type_unicode_char(ch: char, source: Option<&CGEventSource>) -> Result<(), String> {
|
||||
fn type_unicode_char(
|
||||
ch: char,
|
||||
source: Option<&CGEventSource>,
|
||||
target: PostTarget,
|
||||
) -> Result<(), String> {
|
||||
let mut buf = [0u16; 2];
|
||||
let encoded = ch.encode_utf16(&mut buf);
|
||||
|
||||
@@ -110,13 +199,13 @@ fn type_unicode_char(ch: char, source: Option<&CGEventSource>) -> Result<(), Str
|
||||
CGEvent::set_flags(Some(&key_down), CGEventFlags::empty());
|
||||
|
||||
// Post the key down event.
|
||||
CGEvent::post(CGEventTapLocation::HIDEventTap, Some(&key_down));
|
||||
target.post(&key_down);
|
||||
|
||||
// Create and post a corresponding key up event.
|
||||
let key_up = CGEvent::new_keyboard_event(source, 0, false)
|
||||
.ok_or("Failed to create key up event for TypeText.")?;
|
||||
CGEvent::set_flags(Some(&key_up), CGEventFlags::empty());
|
||||
CGEvent::post(CGEventTapLocation::HIDEventTap, Some(&key_up));
|
||||
target.post(&key_up);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user