use std::os::raw::c_void;
use std::sync::Once;
use std::thread::sleep;
use std::time::Duration;
use super::capture;
use super::ffi::*;
use super::keymap::{VK_BACKSPACE, VK_E, VK_LEFT, VK_RIGHT};
const MODS_CLEAR_TIMEOUT_MS: u64 = 250;
#[derive(Debug, thiserror::Error)]
pub enum EmitError {
#[error(
"Accessibility permission is not granted — open System Settings → \
Privacy & Security → Accessibility, enable hyprcorrect, then restart it"
)]
Permission,
#[error("could not create a CGEventSource for synthetic input")]
Source,
}
pub fn replace(backspaces: usize, text: &str) -> Result<(), EmitError> {
replace_with_delay(backspaces, text, 8, 1)
}
pub fn replace_with_delay(
backspaces: usize,
text: &str,
pause_per_backspace_ms: u32,
pause_per_char_ms: u32,
) -> Result<(), EmitError> {
replace_around_caret_with_delay(
backspaces,
0,
text,
pause_per_backspace_ms,
pause_per_char_ms,
)
}
pub fn replace_around_caret_with_delay(
backspaces: usize,
deletes: usize,
text: &str,
pause_per_backspace_ms: u32,
pause_per_char_ms: u32,
) -> Result<(), EmitError> {
capture::wait_mods_clear(Duration::from_millis(MODS_CLEAR_TIMEOUT_MS));
let src = EventSource::new()?;
for _ in 0..deletes {
src.tap_key(VK_RIGHT, 0);
}
for _ in 0..(backspaces + deletes) {
src.tap_key(VK_BACKSPACE, 0);
sleep(Duration::from_millis(pause_per_backspace_ms as u64));
}
src.type_text(text, pause_per_char_ms);
Ok(())
}
pub fn anchored_replace_with_delay(
chars_from_end: usize,
word_chars: usize,
insert: &str,
pause_per_backspace_ms: u32,
pause_per_char_ms: u32,
) -> Result<(), EmitError> {
capture::wait_mods_clear(Duration::from_millis(MODS_CLEAR_TIMEOUT_MS));
let src = EventSource::new()?;
src.tap_key(VK_E, kCGEventFlagMaskControl);
for _ in 0..chars_from_end {
src.tap_key(VK_LEFT, 0);
}
for _ in 0..word_chars {
src.tap_key(VK_BACKSPACE, 0);
sleep(Duration::from_millis(pause_per_backspace_ms as u64));
}
src.type_text(insert, pause_per_char_ms);
Ok(())
}
struct EventSource {
source: CGEventSourceRef,
}
impl EventSource {
fn new() -> Result<Self, EmitError> {
ensure_post_access()?;
let source = unsafe { CGEventSourceCreate(kCGEventSourceStateHIDSystemState) };
if source.is_null() {
return Err(EmitError::Source);
}
Ok(Self { source })
}
fn tap_key(&self, keycode: u16, flags: u64) {
for down in [true, false] {
let ev = unsafe { CGEventCreateKeyboardEvent(self.source, keycode, down) };
if ev.is_null() {
continue;
}
unsafe {
CGEventSetFlags(ev, flags);
CGEventSetIntegerValueField(ev, kCGEventSourceUserData, SYNTHETIC_MARK);
CGEventPost(kCGHIDEventTap, ev);
CFRelease(ev);
}
}
}
fn type_text(&self, text: &str, pause_per_char_ms: u32) {
if text.is_empty() {
return;
}
let mut first = true;
for segment in text.split('\n') {
if !first {
self.tap_key(0x24 , kCGEventFlagMaskShift);
}
first = false;
for ch in segment.chars() {
self.type_char(ch);
if pause_per_char_ms > 0 {
std::thread::sleep(Duration::from_millis(pause_per_char_ms as u64));
}
}
}
}
fn type_char(&self, ch: char) {
let mut buf = [0u16; 2];
let utf16 = ch.encode_utf16(&mut buf);
let len = utf16.len();
for down in [true, false] {
let ev = unsafe { CGEventCreateKeyboardEvent(self.source, 0, down) };
if ev.is_null() {
continue;
}
unsafe {
CGEventSetFlags(ev, 0);
CGEventSetIntegerValueField(ev, kCGEventSourceUserData, SYNTHETIC_MARK);
CGEventKeyboardSetUnicodeString(ev, len, utf16.as_ptr());
CGEventPost(kCGHIDEventTap, ev);
CFRelease(ev);
}
}
}
}
impl Drop for EventSource {
fn drop(&mut self) {
if !self.source.is_null() {
unsafe { CFRelease(self.source as *const c_void) };
}
}
}
fn ensure_post_access() -> Result<(), EmitError> {
if unsafe { AXIsProcessTrusted() } || unsafe { CGPreflightPostEventAccess() } {
return Ok(());
}
static REQUEST_ONCE: Once = Once::new();
REQUEST_ONCE.call_once(|| unsafe {
CGRequestPostEventAccess();
});
Err(EmitError::Permission)
}