Skip to content
Open
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
663 changes: 300 additions & 363 deletions Cargo.lock

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ dependent-version = "upgrade"

[workspace.dependencies]
atspi = { version = "0.27.0" }
config = { version = "0.15.11", default-features = false, features = ["toml"] }
config = { version = "0.15.19", default-features = false, features = ["toml"] }
futures-concurrency = { version = "7.6.3", default-features = false, features = ["alloc"] }
futures-lite = { version = "2.6.0", default-features = false }
futures-util = { version = "0.3.31", default-features = false, features = ["alloc", "async-await-macro"] }
Expand All @@ -46,14 +46,14 @@ odilia-cache = { version = "0.3.0", path = "./cache" }
nix = { version = "0.30.0", default-features = false, features = ["user"] }
pin-project-lite = { version = "0.2.16", default-features = false }
serde_json = { version = "1.0.100", default-features = false, features = ["alloc"] }
serde = { version = "1.0.200", default-features = false, features = ["derive"] }
serde = { version = "1.0.228", default-features = false, features = ["derive"] }
thiserror = { version = "2.0", default-features = false, features = ["std"] }
tokio = { version = "^1.44.2", default-features = false, features = ["macros", "rt"] }
smol-cancellation-token = { version = "0.1", default-features = false }
tower = { version = "0.5.2", default-features = false }
tracing = { version = "^0.1.40", default-features = false, features = ["release_max_level_off"] }
tracing-subscriber = { version = "0.3.16", default-features = false, features = ["ansi"] }
zbus = { version = "5.8", default-features = false, features = ["async-io"] }
tracing = { version = "^0.1.41", default-features = false, features = ["release_max_level_off"] }
tracing-subscriber = { version = "0.3.20", default-features = false, features = ["ansi"] }
zbus = { version = "5.12", default-features = false, features = ["async-io"] }
serde_plain = "1.0.1"
ssip = { version = "0.5", default-features = false }
ssip-client-async = { version = "0.19.0", default-features = false, features = ["async-io"] }
Expand Down
2 changes: 1 addition & 1 deletion cache/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ odilia-common.workspace = true
serde.workspace = true
tracing.workspace = true
zbus.workspace = true
fxhash = "0.2.1"
ahash = "0.8.12"
serde_plain.workspace = true
futures-concurrency.workspace = true
futures-lite.workspace = true
Expand Down
1 change: 1 addition & 0 deletions cache/src/event_handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ impl EventHandler for TextChangedEvent {
.length
.try_into()
.expect("Positive length for text insertion/deletion");
assert_eq!(len, self.text.len(), "The length as reported by AT-SPI and the length of the string are different! This means that some application is incorrectly reporting the length of its strings (likely the number of bytes instead of the unicode character length). The offending string is {}", self.text);
cache.modify_if_not_new(&key, |item: &mut CacheItem| {
match (self.operation, item.text.as_mut()) {
(Operation::Insert, Some(text)) => {
Expand Down
63 changes: 17 additions & 46 deletions cache/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,17 @@
mod convertable;
pub use convertable::Convertable;
mod accessible_ext;
use std::{collections::HashMap, fmt, fmt::Debug, future::Future};
use std::{collections::HashMap, fmt, future::Future};
mod relation_set;
pub use relation_set::{RelationSet, Relations};
mod event_handlers;

pub use accessible_ext::AccessibleExt;
use ahash::RandomState;
use async_channel::{Receiver, Sender};
use atspi::{
proxy::{accessible::AccessibleProxy, cache::CacheProxy, text::TextProxy},
Event, EventProperties, InterfaceSet, ObjectRef, RelationType, Role, StateSet,
Event, EventProperties, ObjectRef, RelationType,
};
pub use event_handlers::{
CacheRequest, CacheResponse, Children, ConstRelationType, ControlledBy, ControllerFor,
Expand All @@ -31,13 +32,12 @@ pub use event_handlers::{
use futures_concurrency::future::TryJoin;
use futures_lite::future::FutureExt as LiteExt;
use futures_util::future::{FutureExt, TryFutureExt};
use fxhash::FxBuildHasher;
pub use odilia_common::cache::CacheItem;
use odilia_common::{
cache::AccessiblePrimitive,
errors::{CacheError, OdiliaError},
result::OdiliaResult,
};
use serde::{Deserialize, Serialize};
use smol_cancellation_token::CancellationToken;
use static_assertions::assert_impl_all;
use zbus::proxy::CacheProperties;
Expand Down Expand Up @@ -147,7 +147,7 @@ impl AllText for TextProxy<'_> {
}

pub type CacheKey = AccessiblePrimitive;
struct NewCache(HashMap<String, HashMap<String, CacheItem, FxBuildHasher>, FxBuildHasher>);
struct NewCache(HashMap<String, HashMap<String, CacheItem, RandomState>, RandomState>);

impl NewCache {
fn has_app(&self, key: &CacheKey) -> bool {
Expand All @@ -174,38 +174,6 @@ impl NewCache {
}
}

#[derive(Clone, Debug, Deserialize, Serialize)]
/// A struct representing an accessible. To get any information from the cache other than the stored information like role, interfaces, and states, you will need to instantiate an [`atspi::proxy::accessible::AccessibleProxy`] or other `*Proxy` type from atspi to query further info.
pub struct CacheItem {
/// The accessible object (within the application) (so)
pub object: AccessiblePrimitive,
/// The application (root object(?) (so)
pub app: AccessiblePrimitive,
/// The parent object. (so)
pub parent: AccessiblePrimitive,
/// The accessible index in parent.I
pub index: Option<usize>,
/// Child count of the accessible.I
pub children_num: Option<usize>,
/// The exposed interface(s) set
pub interfaces: InterfaceSet,
/// Accessible role. u
pub role: Role,
/// The states applicable to the accessible. au
pub states: StateSet,
/// The children (ids) of the accessible
pub children: Vec<AccessiblePrimitive>,
/// The human-readable short name of the item. `None` if string is empty.
pub name: Option<String>,
/// The human-readable longer name (description) of the item. `None` if string is empty.
pub description: Option<String>,
/// The help-text of the item. `None` if string is empty.
pub help_text: Option<String>,
/// The actual, internal text of the item; this will be `None` if either the text interface isn't
/// implemented, or if the response contains an empty string: "".
pub text: Option<String>,
}

/// An internal cache used within Odilia.
///
/// This contains (mostly) all accessibles in the entire accessibility tree, and
Expand Down Expand Up @@ -251,27 +219,27 @@ pub trait CacheDriver {
&self,
key: &CacheKey,
) -> impl Future<Output = OdiliaResult<CacheItem>> + Send;
/// Bulk query an application based on the [`CacheKey.sender`] field.
/// Bulk query an application based on the `sender` field in [`CacheKey`].
fn lookup_bulk(
&self,
key: &CacheKey,
) -> impl Future<Output = OdiliaResult<Vec<CacheItem>>> + Send;
/// A seperate method from [`lookup_external`] for getting relation sets.
/// A seperate method from [`Self::lookup_external`] for getting relation sets.
/// This is separate because it can have rather large results and should only be called when
/// absolutely necessary.
fn lookup_relations(
&self,
key: &CacheKey,
ty: RelationType,
) -> impl Future<Output = OdiliaResult<Vec<CacheKey>>> + Send;
/// A separate method from [`lookup_external`] for converting an [`atspi::CacheItem`] into a
/// A separate method from [`Self::lookup_external`] for converting an [`atspi::CacheItem`] into a
/// [`CacheItem`].
/// This will call out to `DBus` for the remaining details.
fn lookup_from_cache_item(
&self,
cache_item: atspi::CacheItem,
) -> impl Future<Output = OdiliaResult<CacheItem>> + Send;
/// A separate method from [`lookup_external`] for converting an [`atspi::LegacyCacheItem`] into a
/// A separate method from [`Self::lookup_external`] for converting an [`atspi::LegacyCacheItem`] into a
/// [`CacheItem`].
/// This will call out to `DBus` for the remaining details.
fn lookup_from_legacy_cache_item(
Expand Down Expand Up @@ -483,7 +451,7 @@ impl<D: CacheDriver + Send> Cache<D> {
}
pub fn tree(
&self,
) -> &HashMap<String, HashMap<String, CacheItem, FxBuildHasher>, FxBuildHasher> {
) -> &HashMap<String, HashMap<String, CacheItem, RandomState>, RandomState> {
&self.tree.0
}
}
Expand All @@ -496,7 +464,7 @@ impl<D: CacheDriver> Cache<D> {
#[must_use]
#[tracing::instrument(level = "debug", ret, skip_all)]
pub fn new(driver: D) -> Self {
Self { tree: NewCache(HashMap::with_hasher(FxBuildHasher::default())), driver }
Self { tree: NewCache(HashMap::with_hasher(RandomState::default())), driver }
}

/// Remove a single cache item. This function can not fail.
Expand Down Expand Up @@ -573,12 +541,12 @@ impl<D: CacheDriver> Cache<D> {
Ok(self.add_all(found))
}

/// Modify the given item with closure [`F`] if it was already contained in the cache.
/// Modify the given item with closure `F` if it was already contained in the cache.
/// Otherwise, fetch a new item over the [`CacheDriver`].
///
/// # Errors
///
/// See: [`get_or_create`]
/// See: [`Self::get_or_create`]
pub async fn modify_if_not_new<F>(
&mut self,
key: &AccessiblePrimitive,
Expand Down Expand Up @@ -607,7 +575,10 @@ impl<D: CacheDriver> Cache<D> {
///
/// This function technically has a `.expect()` which could panic. But we gaurs against this.
#[tracing::instrument(level = "trace", ret, err(level = "warn"), skip(self))]
async fn get_or_create(&mut self, key: &AccessiblePrimitive) -> OdiliaResult<CacheItem> {
pub async fn get_or_create(
&mut self,
key: &AccessiblePrimitive,
) -> OdiliaResult<CacheItem> {
// if the item already exists in the cache, return it
if let Some(cache_item) = self.get(key) {
return Ok(cache_item);
Expand Down
2 changes: 1 addition & 1 deletion clippy.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
allowed-duplicate-crates = [
# this is needed to avoid clippy telling usthat a series of windows crates have different versions.
# since Odilia does not support windows (but zbus does), it causes a lot of transitive dependencies to be calculated in the graph, even if we do not use them.
"windows-sys", "windows-targets", "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc",
"windows-sys", "windows-targets", "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", "windows-link",
# this is due to tracing using a very old version of the `regex` crate.
"regex-syntax", "regex-automata",
# due to a mismatch in zbus/config
Expand Down
37 changes: 36 additions & 1 deletion common/src/cache.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use atspi::{proxy::accessible::AccessibleProxy, EventProperties, ObjectRef};
use atspi::{
proxy::accessible::AccessibleProxy, EventProperties, InterfaceSet, ObjectRef, Role,
StateSet,
};
use serde::{Deserialize, Serialize};
use zbus::{
names::OwnedUniqueName,
Expand Down Expand Up @@ -87,3 +90,35 @@ impl From<&AccessibleProxy<'_>> for AccessiblePrimitive {
AccessiblePrimitive { sender, id }
}
}

#[derive(Clone, Debug, Deserialize, Serialize)]
/// A struct representing an accessible. To get any information from the cache other than the stored information like role, interfaces, and states, you will need to instantiate an [`atspi::proxy::accessible::AccessibleProxy`] or other `*Proxy` type from atspi to query further info.
pub struct CacheItem {
/// The accessible object (within the application) (so)
pub object: AccessiblePrimitive,
/// The application (root object(?) (so)
pub app: AccessiblePrimitive,
/// The parent object. (so)
pub parent: AccessiblePrimitive,
/// The accessible index in parent.I
pub index: Option<usize>,
/// Child count of the accessible.I
pub children_num: Option<usize>,
/// The exposed interface(s) set
pub interfaces: InterfaceSet,
/// Accessible role. u
pub role: Role,
/// The states applicable to the accessible. au
pub states: StateSet,
/// The children (ids) of the accessible
pub children: Vec<AccessiblePrimitive>,
/// The human-readable short name of the item. `None` if string is empty.
pub name: Option<String>,
/// The human-readable longer name (description) of the item. `None` if string is empty.
pub description: Option<String>,
/// The help-text of the item. `None` if string is empty.
pub help_text: Option<String>,
/// The actual, internal text of the item; this will be `None` if either the text interface isn't
/// implemented, or if the response contains an empty string: "".
pub text: Option<String>,
}
17 changes: 15 additions & 2 deletions common/src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use std::{fmt, fmt::Debug, str::FromStr};
use std::{collections::BTreeMap, fmt, fmt::Debug, str::FromStr};

use atspi::AtspiError;
use serde_plain::Error as SerdePlainError;
use thiserror::Error;

use crate::{cache::AccessiblePrimitive, command::OdiliaCommand};
use crate::{
cache::{AccessiblePrimitive, CacheItem},
command::OdiliaCommand,
};

#[derive(Error, Debug)]
pub enum OdiliaError {
Expand All @@ -26,6 +29,8 @@ pub enum OdiliaError {
SendError(SendError),
#[error("Cache: {0}")]
Cache(#[from] CacheError),
#[error("Text: {0}")]
Text(#[from] TextError),
#[error("N/A: {0}")]
InfallibleConversion(#[from] std::convert::Infallible),
#[error("From int: {0}")]
Expand All @@ -52,6 +57,14 @@ pub enum OdiliaError {
Ssip(#[from] ssip_client_async::ClientError),
}

#[derive(Error, Debug)]
pub enum TextError {
#[error("The number of children of this element ({1}) is not equal to the number of object replacement characters in the following string: \"[0]\"")]
InvalidHyperlinkText(String, usize),
#[error("The following subtree has a child that has an associated object replacemnt character, but does not implement the `org.a11y.atspi.Text` interface")]
NonTextChildren(BTreeMap<AccessiblePrimitive, CacheItem>),
}

impl From<&'static str> for OdiliaError {
fn from(s: &'static str) -> OdiliaError {
Self::Static(s)
Expand Down
2 changes: 1 addition & 1 deletion input-server-keyboard/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -735,7 +735,7 @@ pub struct State {

/// The callback function to call in a tight loop.
/// Returns [`None`] to indicate a desire to swallow an event,
/// Returns [`Some(Event)`] to indicate a passthrough of the event.
/// Returns `Some(Event)` to indicate a passthrough of the event.
///
/// # Panics
///
Expand Down
Loading
Loading