use accessibility_ng::{AXAttribute, AXUIElement};
use accessibility_sys_ng::{kAXFocusedUIElementAttribute, kAXSelectedTextAttribute};
use core_foundation::string::CFString;
use log::{error, info};
use std::process::Command;
use crate::{Selection, SelectionError, Selector};
pub struct MacOSSelector;
impl MacOSSelector {
pub fn new() -> Self {
MacOSSelector
}
}
impl Default for MacOSSelector {
fn default() -> Self {
Self::new()
}
}
impl Selector for MacOSSelector {
fn get_selection(&self) -> Result<Selection, SelectionError> {
match get_selection_by_accessibility() {
Ok(selection) if !selection.is_empty() => {
info!("Retrieved selection via macOS accessibility API");
Ok(selection)
}
Ok(_) => {
info!("Selection via macOS accessibility API is empty");
get_selection_by_clipboard()
}
Err(err) => {
error!(
"Error getting selection via macOS accessibility API: {}",
err
);
get_selection_by_clipboard()
}
}
}
}
pub fn get_macos_text() -> Result<String, SelectionError> {
let selector = MacOSSelector::new();
match selector.get_selection() {
Ok(selection) => selection
.as_text()
.ok_or(SelectionError::InvalidContentType {
expected: "text".to_string(),
received: selection.content_type.to_string(),
}),
Err(err) => Err(err),
}
}
fn get_selection_by_accessibility() -> Result<Selection, SelectionError> {
let system_element = AXUIElement::system_wide();
let focused_element_result = system_element.attribute(&AXAttribute::new(
&CFString::from_static_string(kAXFocusedUIElementAttribute),
));
if focused_element_result.is_err() {
return Err(SelectionError::NoFocusedElement);
}
let element_value = focused_element_result.unwrap();
let focused_element = match element_value.downcast_into::<AXUIElement>() {
Some(element) => element,
None => return Err(SelectionError::NoFocusedElement),
};
let selected_text_result = focused_element.attribute(&AXAttribute::new(
&CFString::from_static_string(kAXSelectedTextAttribute),
));
if selected_text_result.is_err() {
return Err(SelectionError::NoSelectedContent);
}
let text_value = selected_text_result.unwrap();
let selected_text = match text_value.downcast_into::<CFString>() {
Some(text) => text,
None => return Err(SelectionError::NoSelectedContent),
};
Ok(Selection::new_text(selected_text.to_string()))
}
fn get_selection_by_clipboard() -> Result<Selection, SelectionError> {
const APPLE_SCRIPT: &str = r#"
use AppleScript version "2.4"
use scripting additions
use framework "Foundation"
use framework "AppKit"
set initialClipboard to the clipboard
tell application "System Events"
keystroke "c" using {command down}
end tell
delay 0.1
set copiedText to the clipboard
if copiedText is not initialClipboard and copiedText is not "" then
set the clipboard to initialClipboard
end if
copiedText
"#;
let output = Command::new("osascript")
.arg("-e")
.arg(APPLE_SCRIPT)
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(SelectionError::AppleScriptError(stderr.to_string()));
}
let content = String::from_utf8(output.stdout)?;
if content.starts_with("[FILE]") {
let file_path = content.trim_start_matches("[FILE]").to_string();
Ok(Selection::new_file(file_path))
}
else {
Ok(Selection::new_text(content))
}
}