diff --git a/README.md b/README.md index 932723c..72377cc 100644 --- a/README.md +++ b/README.md @@ -172,15 +172,19 @@ axcli --app Lark watch axcli --app Lark watch --format json ``` -**Global mouse / keyboard (ignores --app/--pid):** +**Global mouse / keyboard:** ```sh axcli mouse pos # print current cursor position axcli mouse move 400 300 axcli mouse click 400 300 axcli mouse scroll 0 -120 # scroll down 120px at current cursor -axcli keyboard type 'hello world' +axcli keyboard type 'hello world' # global HID path; target must own first responder axcli keyboard press 'Command+Shift+4' + +# Type into a background app (no activation, no focus steal): +# Unicode (Chinese, emoji, ...) works on both paths. +axcli --app TextEdit keyboard type '你好 from background' --strategy pid ``` **List running apps:** @@ -223,6 +227,8 @@ Tested on AppKit (Calculator, TextEdit, Finder) and Chromium/Electron apps (Lark `press` defaults to the global HID path (which activates the app). Use `press --strategy pid` to deliver to a background app's first responder. +`keyboard type` defaults to global HID as well; use `--strategy pid --app ` (or `--pid `) to deliver Unicode strings to a background app without activation. Confirmed on TextEdit with Chinese / emoji input. + ## Locator Syntax axcli uses a CSS-like selector syntax to target elements in the accessibility tree: diff --git a/src/input.rs b/src/input.rs index 637d60f..6b2835a 100644 --- a/src/input.rs +++ b/src/input.rs @@ -344,6 +344,41 @@ pub fn type_text(text: &str) { } } +/// Type text directly to a specific pid via `CGEventPostToPid`. +/// +/// Background-safe counterpart to `type_text` — delivers Unicode keystrokes +/// to the target process's first responder without activating the app or +/// stealing focus. Mirrors `press_key_combo_bg` for parameterised keys. +/// +/// The chunk size (20 UTF-16 code units) matches `type_text`; per-chunk +/// sleeps are the same so timing-sensitive apps see identical pacing on +/// both paths. Confirmed working on TextEdit's document body with the +/// app fully occluded behind another window. Unicode (Chinese, emoji) +/// is supported because `CGEventKeyboardSetUnicodeString` feeds UTF-16 +/// directly, bypassing the keycode table. +pub fn type_text_bg(pid: i32, text: &str) { + let source = CGEventSource::new(CGEventSourceStateID::HIDSystemState); + let utf16: Vec = text.encode_utf16().collect(); + for chunk in utf16.chunks(20) { + let down = CGEvent::new_keyboard_event(source.as_deref(), 0, true); + if let Some(ref ev) = down { + unsafe { + CGEvent::keyboard_set_unicode_string(Some(ev), chunk.len() as _, chunk.as_ptr()); + } + CGEvent::post_to_pid(pid, Some(ev)); + } + std::thread::sleep(std::time::Duration::from_millis(5)); + let up = CGEvent::new_keyboard_event(source.as_deref(), 0, false); + if let Some(ref ev) = up { + unsafe { + CGEvent::keyboard_set_unicode_string(Some(ev), chunk.len() as _, chunk.as_ptr()); + } + CGEvent::post_to_pid(pid, Some(ev)); + } + std::thread::sleep(std::time::Duration::from_millis(10)); + } +} + /// Parse a key combo string like "Control+a", "Command+Shift+v", "Enter" /// into (keycode, modifier_flags). pub fn parse_key_combo(combo: &str) -> (u16, u64) { diff --git a/src/main.rs b/src/main.rs index de596e3..9deb137 100644 --- a/src/main.rs +++ b/src/main.rs @@ -414,11 +414,12 @@ Known attributes: #[command(subcommand)] action: MouseAction, }, - /// Global keyboard input — ignores --app/--pid. + /// Keyboard input. Default path is global HID (ignores --app/--pid); + /// `type --strategy pid` switches to `CGEventPostToPid` and respects + /// --app/--pid for background delivery (no focus steal). /// - /// Events are posted via `CGEventPost(HIDEventTap)` and delivered to - /// whichever process currently holds keyboard focus (first responder). - /// To target a specific background app use `press --strategy pid`. + /// For single-key presses targeting a background app, use the top-level + /// `press --strategy pid` instead. Keyboard { #[command(subcommand)] action: KeyboardAction, @@ -468,9 +469,25 @@ enum MouseAction { /// `axcli keyboard ...` actions. #[derive(Subcommand)] enum KeyboardAction { - /// Type literal text (Unicode via CGEventKeyboardSetUnicodeString) to - /// whatever app currently has keyboard focus. - Type { text: String }, + /// Type literal text (Unicode via `CGEventKeyboardSetUnicodeString`). + /// + /// Default strategy `hid` posts via global `CGEventPost(HIDEventTap)` + /// and lands on whichever app currently owns first responder — this + /// usually implies the target must be foregrounded. Strategy `pid` + /// posts via `CGEventPostToPid` and delivers directly to the target + /// process's first responder without activation or focus steal; pair + /// with `--app` / `--pid` to pick the target. + /// + /// Unicode (Chinese, emoji, ...) works on both paths. + Type { + text: String, + /// Post strategy. `hid` (default): global `CGEventPost(HIDEventTap)`, + /// activates the target first. `pid`: `CGEventPostToPid`, delivers + /// to the target process in the background without focus steal — + /// requires `--app` or `--pid` on the top-level invocation. + #[arg(long, value_enum, default_value_t = PressStrategy::Hid)] + strategy: PressStrategy, + }, /// Press a key or key combo. Examples: `Enter`, `Command+a`, /// `Control+Shift+v`, `F5`, `Escape`. Sent to the current first /// responder. @@ -511,7 +528,7 @@ fn main() { return; } if let Command::Keyboard { ref action } = cli.command { - if let Err(e) = cmd_keyboard(action) { + if let Err(e) = cmd_keyboard(&cli, action) { eprintln!("error: {e}"); std::process::exit(exit_code(&e)); } @@ -822,15 +839,22 @@ fn cmd_mouse(action: &MouseAction) -> Result<(), AxError> { Ok(()) } -fn cmd_keyboard(action: &KeyboardAction) -> Result<(), AxError> { +fn cmd_keyboard(cli: &Cli, action: &KeyboardAction) -> Result<(), AxError> { if !accessibility::is_trusted() { return Err(AxError::AccessDenied); } match action { - KeyboardAction::Type { text } => { - eprintln!("Typing: {text:?}"); - input::type_text(text); - } + KeyboardAction::Type { text, strategy } => match strategy { + PressStrategy::Hid => { + eprintln!("Typing: {text:?} [hid]"); + input::type_text(text); + } + PressStrategy::Pid => { + let (pid, _) = resolve_app(cli)?; + eprintln!("Typing: {text:?} [pid={pid}]"); + input::type_text_bg(pid, text); + } + }, KeyboardAction::Press { key } => { let (keycode, flags) = input::parse_key_combo(key); eprintln!("Pressing: {key} (keycode={keycode}, flags=0x{flags:x})");