Skip to content
Draft
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
95 changes: 93 additions & 2 deletions crates/coldvox-text-injection/src/focus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,106 @@ impl<B: FocusBackend> FocusProvider for FocusTracker<B> {
#[derive(Default, Clone)]
pub struct SystemFocusAdapter;

#[cfg(not(feature = "atspi"))]
#[async_trait]
impl FocusBackend for SystemFocusAdapter {
async fn query_focus(&self) -> Result<FocusStatus, InjectionError> {
// Temporarily disabled due to AT-SPI API changes
// TODO(#38): Update to work with current atspi crate API
// Stub implementation when AT-SPI is not enabled.
Ok(FocusStatus::Unknown)
}
}

#[cfg(feature = "atspi")]
#[async_trait]
impl FocusBackend for SystemFocusAdapter {
async fn query_focus(&self) -> Result<FocusStatus, InjectionError> {
use crate::log_throttle::log_atspi_connection_failure;
use atspi::{
connection::AccessibilityConnection, proxy::accessible::AccessibleProxy,
proxy::collection::CollectionProxy, Interface, MatchType, ObjectMatchRule, SortOrder,
State,
};

let conn = match AccessibilityConnection::new().await {
Ok(c) => c,
Err(e) => {
log_atspi_connection_failure(&e.to_string());
return Ok(FocusStatus::Unknown);
}
};

let zbus_conn = conn.connection();

let collection = match CollectionProxy::builder(zbus_conn)
.destination("org.a11y.atspi.Registry")
.map_err(|e| InjectionError::Other(format!("Collection destination failed: {e}")))?
.path("/org/a11y/atspi/accessible/root")
.map_err(|e| InjectionError::Other(format!("Collection path failed: {e}")))?
.build()
.await
{
Ok(p) => p,
Err(e) => {
debug!("Failed to build CollectionProxy: {}", e);
return Ok(FocusStatus::Unknown);
}
};

let mut rule = ObjectMatchRule::default();
rule.states = State::Focused.into();
rule.states_mt = MatchType::All;

let matches = match collection
.get_matches(rule, SortOrder::Canonical, 1, false)
.await
{
Ok(m) => m,
Err(e) => {
debug!("Failed to get matches from CollectionProxy: {}", e);
return Ok(FocusStatus::Unknown);
}
};

if let Some(obj_ref) = matches.first() {
let accessible = match AccessibleProxy::builder(zbus_conn)
.destination(obj_ref.name.clone())
.map_err(|e| {
InjectionError::Other(format!("AccessibleProxy destination failed: {e}"))
})?
.path(obj_ref.path.clone())
.map_err(|e| InjectionError::Other(format!("AccessibleProxy path failed: {e}")))?
.build()
.await
{
Ok(p) => p,
Err(e) => {
debug!("Failed to build AccessibleProxy: {}", e);
return Ok(FocusStatus::Unknown);
}
};

let ifaces = match accessible.get_interfaces().await {
Ok(i) => i,
Err(e) => {
debug!("Failed to get interfaces: {}", e);
return Ok(FocusStatus::Unknown);
}
};

if ifaces.contains(Interface::EditableText) {
debug!("Focused element is editable: {:?}", obj_ref.name);
Ok(FocusStatus::EditableText)
} else {
debug!("Focused element is not editable: {:?}", obj_ref.name);
Ok(FocusStatus::NonEditable)
}
} else {
debug!("No focused element found");
Ok(FocusStatus::Unknown)
}
}
}

#[async_trait]
impl<T> FocusBackend for std::sync::Arc<T>
where
Expand Down
40 changes: 40 additions & 0 deletions crates/coldvox-text-injection/src/tests/focus_backend_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@

use crate::focus::{FocusStatus, FocusTracker};
use crate::tests::test_harness::{TestAppManager, TestEnvironment};
use crate::types::InjectionConfig;

#[tokio::test]
#[cfg_attr(
not(feature = "live-hardware-tests"),
ignore = "Skipping live hardware test for focus backend"
)]
async fn test_atspi_focus_backend_identifies_editable_text() {
let env = TestEnvironment::current();
if !env.can_run_real_tests() {
println!("Skipping focus backend test: No display available.");
return;
}

// Launch the GTK test application.
let _test_app = TestAppManager::launch_gtk_app()
.expect("Failed to launch GTK test app. Is 'build.rs' configured correctly?");

// Give the app a moment to launch and gain focus.
tokio::time::sleep(std::time::Duration::from_millis(500)).await;

// Create a FocusTracker and check the focus status.
let config = InjectionConfig::default();
let mut focus_tracker = FocusTracker::new(config);

let status = focus_tracker
.get_focus_status()
.await
.expect("Failed to get focus status");

// The GTK test app should have a focused, editable text field.
assert_eq!(
status,
FocusStatus::EditableText,
"Focus backend should have identified the GTK app's focused text field as editable."
);
}
4 changes: 4 additions & 0 deletions crates/coldvox-text-injection/src/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
//! Test modules for coldvox-text-injection

// pub mod real_injection;
pub mod test_harness;
pub mod test_utils;
pub mod wl_copy_basic_test;
pub mod wl_copy_simple_test;
pub mod wl_copy_stdin_test;

#[cfg(all(test, feature = "atspi"))]
mod focus_backend_test;
Loading