diff --git a/Cargo.toml b/Cargo.toml index b845d98..17317e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ enigo = { version = "0.2.0", features = [ "xdo" ] } [target.'cfg(target_os = "macos")'.dependencies] cocoa = "0.24" objc = "0.2.7" +serde = { version = "1.0", features = ["derive"] } macos-accessibility-client = "0.0.1" core-foundation = "0.9.3" core-graphics = "0.22.3" diff --git a/src/lib.rs b/src/lib.rs index f92cc15..45c322c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,10 +23,23 @@ use crate::windows::get_selected_text as _get_selected_text; /// let text = get_selected_text().unwrap(); /// println!("{}", text); /// ``` +#[cfg(not(target_os = "macos"))] pub fn get_selected_text() -> Result> { _get_selected_text() } +#[cfg(target_os = "macos")] +pub fn get_selected_text() -> Result> { + _get_selected_text() +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct SelectedText { + is_file_paths: bool, + app_name: String, + text: Vec, +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/macos.rs b/src/macos.rs index 384e4df..6c93321 100644 --- a/src/macos.rs +++ b/src/macos.rs @@ -8,9 +8,44 @@ use debug_print::debug_println; use lru::LruCache; use parking_lot::Mutex; +use crate::SelectedText; + static GET_SELECTED_TEXT_METHOD: Mutex>> = Mutex::new(None); -pub fn get_selected_text() -> Result> { +// TDO: optimize / refactor / test later +fn split_file_paths(input: &str) -> Vec { + let mut paths = Vec::new(); + let mut current_path = String::new(); + let mut in_quotes = false; + + for ch in input.chars() { + match ch { + '\'' => { + current_path.push(ch); + in_quotes = !in_quotes; + if !in_quotes { + paths.push(current_path.clone()); + current_path.clear(); + } + } + ' ' if !in_quotes => { + if !current_path.is_empty() { + paths.push(current_path.clone()); + current_path.clear(); + } + } + _ => current_path.push(ch), + } + } + + if !current_path.is_empty() { + paths.push(current_path); + } + + paths +} + +pub fn get_selected_text() -> Result> { if GET_SELECTED_TEXT_METHOD.lock().is_none() { let cache = LruCache::new(NonZeroUsize::new(100).unwrap()); *GET_SELECTED_TEXT_METHOD.lock() = Some(cache); @@ -19,28 +54,56 @@ pub fn get_selected_text() -> Result> { let cache = cache.as_mut().unwrap(); let app_name = match get_active_window() { Ok(window) => window.app_name, - Err(_) => return Err("No active window found".into()), + Err(_) => { + // user might be in the desktop / home view + String::new() + } + }; + + if app_name == "Finder" || app_name.is_empty() { + if let Ok(text) = get_selected_file_paths_by_clipboard_using_applescript() { + return Ok(SelectedText { + is_file_paths: true, + app_name: app_name, + text: split_file_paths(&text), + }); + } + } + + let mut selected_text = SelectedText { + is_file_paths: false, + app_name: app_name.clone(), + text: vec![], }; - // debug_println!("app_name: {}", app_name); + if let Some(text) = cache.get(&app_name) { if *text == 0 { - return get_selected_text_by_ax(); + let ax_text = get_selected_text_by_ax()?; + if !ax_text.is_empty() { + cache.put(app_name.clone(), 0); + selected_text.text = vec![ax_text]; + return Ok(selected_text); + } } - return get_selected_text_by_clipboard_using_applescript(); + let txt = get_selected_text_by_clipboard_using_applescript()?; + selected_text.text = vec![txt]; + return Ok(selected_text); } match get_selected_text_by_ax() { - Ok(text) => { - if !text.is_empty() { - cache.put(app_name, 0); + Ok(txt) => { + if !txt.is_empty() { + cache.put(app_name.clone(), 0); } - Ok(text) + selected_text.text = vec![txt]; + Ok(selected_text) } Err(_) => match get_selected_text_by_clipboard_using_applescript() { - Ok(text) => { - if !text.is_empty() { + Ok(txt) => { + if !txt.is_empty() { cache.put(app_name, 1); } - Ok(text) + selected_text.text = vec![txt]; + Ok(selected_text) } Err(e) => Err(e), }, @@ -79,7 +142,7 @@ fn get_selected_text_by_ax() -> Result> { Ok(selected_text.to_string()) } -const APPLE_SCRIPT: &str = r#" +const REGULAR_TEXT_COPY_APPLE_SCRIPT: &str = r#" use AppleScript version "2.4" use scripting additions use framework "Foundation" @@ -116,12 +179,71 @@ set the clipboard to savedClipboard theSelectedText "#; +const FILE_PATH_COPY_APPLE_SCRIPT: &str = r#" +use AppleScript version "2.4" +use scripting additions +use framework "Foundation" +use framework "AppKit" + +set savedAlertVolume to alert volume of (get volume settings) + +-- Back up clipboard contents: +set savedClipboard to the clipboard + +set thePasteboard to current application's NSPasteboard's generalPasteboard() +set theCount to thePasteboard's changeCount() + +tell application "System Events" + set volume alert volume 0 +end tell + +-- Copy selected text to clipboard: +tell application "System Events" to keystroke "c" using {command down, option down} +delay 0.1 -- Without this, the clipboard may have stale data. + +tell application "System Events" + set volume alert volume savedAlertVolume +end tell + +if thePasteboard's changeCount() is theCount then + return "" +end if + +set theSelectedText to the clipboard + +set the clipboard to savedClipboard + +theSelectedText +"#; + fn get_selected_text_by_clipboard_using_applescript() -> Result> { // debug_println!("get_selected_text_by_clipboard_using_applescript"); let output = std::process::Command::new("osascript") .arg("-e") - .arg(APPLE_SCRIPT) + .arg(REGULAR_TEXT_COPY_APPLE_SCRIPT) + .output()?; + if output.status.success() { + let content = String::from_utf8(output.stdout)?; + let content = content.trim(); + Ok(content.to_string()) + } else { + let err = output + .stderr + .into_iter() + .map(|c| c as char) + .collect::() + .into(); + Err(err) + } +} + +fn get_selected_file_paths_by_clipboard_using_applescript( +) -> Result> { + // debug_println!("get_selected_text_by_clipboard_using_applescript"); + let output = std::process::Command::new("osascript") + .arg("-e") + .arg(FILE_PATH_COPY_APPLE_SCRIPT) .output()?; if output.status.success() { let content = String::from_utf8(output.stdout)?;