Skip to content
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
90 changes: 83 additions & 7 deletions src-tauri/src/prediction/macos.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
use super::context::{Adapter, RawContext, Status};
use core_foundation::{
base::{CFEqual, CFRange, CFType, CFTypeRef, TCFType},
runloop::{kCFRunLoopDefaultMode, CFRunLoop},
string::{CFString, CFStringRef},
};

#[link(name = "ApplicationServices", kind = "framework")]
unsafe extern "C" {
fn AXUIElementCreateSystemWide() -> CFTypeRef;
fn AXUIElementCreateApplication(pid: i32) -> CFTypeRef;
fn AXUIElementCopyAttributeValue(e: CFTypeRef, a: CFStringRef, out: *mut CFTypeRef) -> i32;
fn AXUIElementCopyParameterizedAttributeValue(
e: CFTypeRef,
Expand Down Expand Up @@ -41,9 +43,7 @@ fn string(value: CFType) -> Result<String, Status> {
}
Ok(unsafe { CFString::wrap_under_get_rule(value.as_CFTypeRef().cast()) }.to_string())
}
pub struct MacAdapter {
system: CFType,
}
pub struct MacAdapter;
impl MacAdapter {
pub fn new() -> Result<Self, Status> {
let system = unsafe { AXUIElementCreateSystemWide() };
Expand All @@ -53,9 +53,8 @@ impl MacAdapter {
unsafe {
AXUIElementSetMessagingTimeout(system, 0.2);
}
Ok(Self {
system: unsafe { CFType::wrap_under_create_rule(system) },
})
let _system = unsafe { CFType::wrap_under_create_rule(system) };
Ok(Self)
}
fn text(&self, target: &CFType, range: CFRange) -> Result<String, Status> {
let parameter = unsafe { AXValueCreate(4, (&range as *const CFRange).cast()) };
Expand Down Expand Up @@ -83,7 +82,32 @@ impl MacAdapter {
impl Adapter for MacAdapter {
type Target = CFType;
fn focused(&mut self) -> Result<CFType, Status> {
attribute(&self.system, "AXFocusedUIElement")
objc2::rc::autoreleasepool(|_| {
focused_application(
|| {
// The pipe-driven worker has no AppKit event loop. Refresh
// NSWorkspace's foreground cache without waiting for events.
unsafe {
CFRunLoop::run_in_mode(
kCFRunLoopDefaultMode,
std::time::Duration::ZERO,
false,
);
}
objc2_app_kit::NSWorkspace::sharedWorkspace()
.frontmostApplication()
.map(|app| app.processIdentifier())
},
|pid| {
let app = unsafe { AXUIElementCreateApplication(pid) };
if app.is_null() {
return Err(Status::NoFocus);
}
let app = unsafe { CFType::wrap_under_create_rule(app) };
attribute(&app, "AXFocusedUIElement")
},
)
})
}
fn protected(&mut self, target: &CFType) -> Result<bool, Status> {
if unsafe { IsSecureEventInputEnabled() } {
Expand Down Expand Up @@ -184,3 +208,55 @@ impl Adapter for MacAdapter {
})
}
}

fn focused_application<T>(
mut foreground: impl FnMut() -> Option<i32>,
focused: impl FnOnce(i32) -> Result<T, Status>,
) -> Result<T, Status> {
let pid = foreground().filter(|pid| *pid > 0).ok_or(Status::NoFocus)?;
let target = focused(pid)?;
if foreground() != Some(pid) {
return Err(Status::NoFocus);
}
Ok(target)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn resolves_focus_from_current_application() {
assert_eq!(focused_application(|| Some(42), |pid| Ok(pid + 1)), Ok(43));
}

#[test]
fn rejects_missing_foreground_without_querying_accessibility() {
for pid in [None, Some(0), Some(-1)] {
assert_eq!(
focused_application(
|| pid,
|_| -> Result<(), Status> { panic!("must not query an invalid application") }
),
Err(Status::NoFocus)
);
}
}

#[test]
fn rejects_foreground_changes_during_discovery() {
let mut applications = [Some(42), Some(43)].into_iter();
assert_eq!(
focused_application(|| applications.next().unwrap(), |_| Ok(())),
Err(Status::NoFocus)
);
}

#[test]
fn preserves_accessibility_failures() {
assert_eq!(
focused_application(|| Some(42), |_| Err::<(), _>(Status::Unsupported)),
Err(Status::Unsupported)
);
}
}
60 changes: 55 additions & 5 deletions src-tauri/src/prediction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,16 +215,29 @@ pub fn select(token: u64, index: usize) -> Result<(), String> {
})
}
fn resource(app: &AppHandle) -> Result<std::path::PathBuf, ()> {
if cfg!(debug_assertions) {
return Ok(Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/WordData2017051601.db"));
}
app.path()
let bundled = app
.path()
.resolve(
"resources/WordData2017051601.db",
tauri::path::BaseDirectory::Resource,
)
.map_err(|_| ())
.map_err(|_| ());
let development = cfg!(debug_assertions)
.then(|| Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/WordData2017051601.db"));
database_resource(bundled, development)
}

fn database_resource(
bundled: Result<std::path::PathBuf, ()>,
development: Option<std::path::PathBuf>,
) -> Result<std::path::PathBuf, ()> {
bundled
.ok()
.filter(|path| path.is_file())
.or_else(|| development.filter(|path| path.is_file()))
.ok_or(())
}

pub fn poll(app: &AppHandle, keyboard: Option<&mut Keyboard>, enabled: bool, ignored: &[String]) {
let Some(keyboard) = keyboard else {
stop();
Expand Down Expand Up @@ -362,6 +375,43 @@ pub fn poll(app: &AppHandle, keyboard: Option<&mut Keyboard>, enabled: bool, ign
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn database_resolution_supports_transferred_bundles_and_unbundled_development() {
let root = std::env::temp_dir().join(format!("switchify-resource-{}", std::process::id()));
std::fs::create_dir_all(&root).unwrap();
let bundled = root.join("bundled.db");
let source = root.join("source.db");
std::fs::write(&bundled, []).unwrap();
std::fs::write(&source, []).unwrap();
assert_eq!(
database_resource(Ok(bundled.clone()), Some(source.clone())),
Ok(bundled.clone())
);
assert_eq!(
database_resource(Ok(bundled.clone()), None),
Ok(bundled.clone())
);
std::fs::remove_file(&source).unwrap();
assert_eq!(
database_resource(Ok(bundled.clone()), Some(source.clone())),
Ok(bundled.clone())
);
std::fs::write(&source, []).unwrap();
std::fs::remove_file(&bundled).unwrap();
assert_eq!(
database_resource(Ok(bundled.clone()), Some(source.clone())),
Ok(source.clone())
);
assert_eq!(
database_resource(Err(()), Some(source.clone())),
Ok(source.clone())
);
assert_eq!(database_resource(Ok(bundled), None), Err(()));
assert_eq!(database_resource(Ok(root.clone()), None), Err(()));
std::fs::remove_file(source).unwrap();
std::fs::remove_dir(root).unwrap();
}

#[test]
fn cancellation_kills_and_reaps_a_blocked_worker() {
#[cfg(target_os = "windows")]
Expand Down
Loading