Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support file paths on macs #7

Merged
merged 1 commit into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
13 changes: 13 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Box<dyn std::error::Error>> {
_get_selected_text()
}

#[cfg(target_os = "macos")]
pub fn get_selected_text() -> Result<SelectedText, Box<dyn std::error::Error>> {
_get_selected_text()
}

#[derive(Debug, Clone, serde::Serialize)]
pub struct SelectedText {
is_file_paths: bool,
app_name: String,
text: Vec<String>,
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
150 changes: 136 additions & 14 deletions src/macos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Option<LruCache<String, u8>>> = Mutex::new(None);

pub fn get_selected_text() -> Result<String, Box<dyn std::error::Error>> {
// TDO: optimize / refactor / test later
fn split_file_paths(input: &str) -> Vec<String> {
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<SelectedText, Box<dyn std::error::Error>> {
if GET_SELECTED_TEXT_METHOD.lock().is_none() {
let cache = LruCache::new(NonZeroUsize::new(100).unwrap());
*GET_SELECTED_TEXT_METHOD.lock() = Some(cache);
Expand All @@ -19,28 +54,56 @@ pub fn get_selected_text() -> Result<String, Box<dyn std::error::Error>> {
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),
},
Expand Down Expand Up @@ -79,7 +142,7 @@ fn get_selected_text_by_ax() -> Result<String, Box<dyn std::error::Error>> {
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"
Expand Down Expand Up @@ -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<String, Box<dyn std::error::Error>>
{
// 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::<String>()
.into();
Err(err)
}
}

fn get_selected_file_paths_by_clipboard_using_applescript(
) -> Result<String, Box<dyn std::error::Error>> {
// 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)?;
Expand Down
Loading